mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* feat(ship): GSTA-167 turn levers on the restored reviewers: ship preflight/release bins, own-jam meter, dispatch-pair metering, ship/DRIVER.md
Ports the non-reviewer parts of #3633 (gsta-167-pr34) and #3637 (gsta-167-pr5 @2914400)
onto main ae3d58c (v1.91.331.0). Reviewer briefs, checklists, the fix-check prompt and
the depth rules are byte-identical to main and now pinned in
test/review-packets-removed.test.ts.
Left out: gstack-review-packet and its libs, parallel/batched fanout, --like, collect,
the audit prompt split, the docs-audit packet, review-army/adversarial/outside-voice
edits, the gate-scan summary-only block and the review-depth scope-error change.
Packet-bytes reader pricing is dropped from gstack-ship-budget; handoff READ ranges
send each section whole; the driver runs the audit, review, adversarial and
documentation sections as written.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(context-budget): raise the capy/skill/gstack eager ceiling to its captured value after the main merge (own-jam meter, driver and reviewer-batch prose, GSTA-167)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(ship-driver): name ship/DRIVER.md.tmpl so the pregate maps the template to its test lane (GSTA-167)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ship-driver): keep the skill generator off lib/ship-budget so a minimal setup checkout still renders (GSTA-167)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(windows): keep test/ship-release.test.ts off the Windows subset; it launches the shebang bin and sh stubs directly (GSTA-167)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(ship): variant E orchestration: one program per review pass and Step 14.5 audit, the review child starts at Step 9, a metered dispatch:docs role, and the driver's record headings (GSTA-167)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(ship): variant G orchestration: one call per driver phase (9 for a clean pass, at most 12), gstack-ship-preflight --audit for single reads and deterministic audits, an observe-mode ledger with note and credits, 240 s wait pings, and small-model audit readers (GSTA-167)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ship-budget): a Capy handoff child needs no jam id: verify-handoff with no reading makes it parent-metered (METER: parent), it never delegates on an unknown charge, and reconcile charges the parent's counter delta (GSTA-167 replay 7e)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ship): a poll settles a reader only at done, failed or a final summary, never a fresh idle; return settles the child's open step reservation (GSTA-167 replay 7e)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ship-budget): return releases an open step reservation under the estimate meter instead of booking its projection; measured meters still settle it (GSTA-167 replay 7e)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(review): a clean fix-check after below-floor fixes is done; Step 11 and fix-check below-floor findings are notes at pass 1
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(ship): verbatim reviewer briefs and loss-free collection (gstack-review-dispatch render/sent/collect/judge/verify)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes
- verify requires each output's brief rendered and sent verbatim, reruns judge
against the recorded judged.jsonl hash, and flags outputs collected after judge
- judge drops self-folds, fold cycles and chains
- prose findings parse a leading label or a label heading, not any severity word
- releasePaths: manifests count as release metadata only for version-only edits,
lockfiles never; TODOS.md stays out of lane-evidence exemptions
- push stops when the sealed-output guard did not run
- ship-audit reads the test tree once instead of one git grep per file
- drop the uncalled --delegate measured mode; doc/help step-number fixes
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (pass 2)
- collect keeps a finding wherever its label stands (headings, table rows,
mid-line, Informational and upper-case labels); only top-level list items
inherit a label heading, and any heading or bold-only line ends it
- judge treats a merged file missing any collected id as a drop; verify flags
merged_ids_missing and a sent reader never collected (NOT_COLLECTED)
- finish computes release paths after --build, so a build that rewrites
package.json is a build-input change
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(review-dispatch): verify per render, voided superseded mismatches
- verify treats each render as one dispatch of its reader, so a two-pass
ledger compares pass-1 sends with pass-1 briefs (later renders print as
<reader>@2); outputs bind to the latest send of their own render
- a mismatched send re-sent byte-exact before anything was collected is void
and counted (voided=N on the verdict line); one that produced an output
stays MISMATCH
- an indented bold sub-line no longer ends a label section (fix-check)
- docs: a failed reader is collected from its failure text
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(review-dispatch): only the newest render of a path checks its file; absolute ledger paths
- a later render into the same briefs directory overwrites the earlier brief by
design, so verify checks a brief file only for the newest render of that path
- collect and judge record absolute output, merged and judged paths, so verify
reads them from any directory
- docs: a failed reader's failure text is parsed like any output
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore: changelog and TODOS
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ship-release): the changelog_fragments fragment is release metadata
Fix-check 4 (CRITICAL) after the main merge: releasePaths omitted
<changelog_fragments.dir>/<branch-slug>.md, so finish stopped on the
fragment Step 13 writes under gstack's policy, or routed it to re-review.
version now names the fragment in its Step 13 NEXT line instead of
[Unreleased], DRIVER.md Step 13 writes and lints where NEXT says, and the
preflight POLICY line says Steps 12-16.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore: changelog and TODOS
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
220 lines
12 KiB
TypeScript
220 lines
12 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { generateReviewDashboard } from '../scripts/resolvers/review-dashboard';
|
||
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||
import { ALL_HOST_CONFIGS } from '../hosts';
|
||
import { expectMentions } from './helpers/prompt-structure';
|
||
|
||
/**
|
||
* Template-drift tripwire for the content-binding wave. The bins are
|
||
* code-enforced; the GRADING rules live as prose in rendered templates that
|
||
* agents follow. This test pins the load-bearing rule text in the GENERATED
|
||
* files so a template refactor can't silently drop a rule while the bins keep
|
||
* working. (Prompt-followed prose is honest tier-2 enforcement — this tripwire
|
||
* is what keeps it from being tier-3 vibes.)
|
||
*/
|
||
|
||
const ROOT = path.resolve(import.meta.dir, '..');
|
||
|
||
function rendered(rel: string): string {
|
||
return fs.readFileSync(path.join(ROOT, rel), 'utf-8');
|
||
}
|
||
|
||
describe('content-binding template drift', () => {
|
||
test('design-lite records outside coverage after the outside step in ship', () => {
|
||
const text = rendered('ship/sections/review-army.md');
|
||
const outside = text.indexOf('design voice**');
|
||
expect(outside).toBeGreaterThan(-1);
|
||
expect(text.indexOf('--finish DESIGN_START')).toBeGreaterThan(outside);
|
||
expect(text).toContain('Use the original DESIGN_START token');
|
||
});
|
||
|
||
test('ship eval selection scopes the Rails example below the project-native path', () => {
|
||
const text = rendered('ship/sections/tests.md');
|
||
const native = text.indexOf('**Project-native path:**');
|
||
const rails = text.indexOf('**Rails example only');
|
||
expect(native).toBeGreaterThan(-1);
|
||
expect(rails).toBeGreaterThan(native);
|
||
expect(text).not.toContain('**If no matches:**');
|
||
expect(text).toContain('If any eval fails');
|
||
});
|
||
|
||
test('ship historical readiness does not replace the current pre-landing gate', () => {
|
||
const text = rendered('ship/SKILL.md');
|
||
expectMentions(text, [['never', 'convergence', 'approval']], 'text');
|
||
});
|
||
|
||
test('ship Step 16 carries the evidence check (mechanized IRON LAW)', () => {
|
||
const ship = rendered('ship/SKILL.md');
|
||
expect(ship).toMatch(/gstack-evidence check --label tests --expect-cmd '[^']+' --label vitest --expect-cmd '[^']+' --max-age 24 --allow-paths CHANGELOG\.md,VERSION,package\.json/);
|
||
expect(ship.replace(/\s+/g, ' ')).toContain("| STALE/MISSING: changed content, command or age, or no proven run | Run `~/.claude/skills/gstack/bin/gstack-evidence run --label <lane> -- '<command>'`, read the result and recheck once");
|
||
expect(ship.replace(/\s+/g, ' ')).toContain("**New, changed or unwaived test failure:** STOP publication. Run Steps 5–15, starting with Step 5's triage, then return to Step 16 stage 1");
|
||
expect(ship).toContain('return to Step 16 stage 1');
|
||
});
|
||
|
||
test('ship Step 5 lanes run wrapped with per-lane labels', () => {
|
||
const tests = rendered('ship/sections/tests.md');
|
||
expect(tests).toContain('gstack-ship-preflight --tests-only --base <base> --lane <label>=<command>');
|
||
expect(tests).toContain('Each lane runs as `gstack-evidence run --label <label> -- \'<command>\'`');
|
||
expect(tests).toContain('use the same lane labels and exact commands again in Step 16');
|
||
});
|
||
|
||
test('land-and-deploy grades staleness content-first (wtree rule) and checks evidence', () => {
|
||
// Carved (prompt-token-load-reduction): Step 3.5 moved out of the skeleton
|
||
// into the on-demand readiness-gate section — the grading rules live there.
|
||
const land = rendered('land-and-deploy/sections/readiness-gate.md');
|
||
expect(land).toContain('wtree');
|
||
expect(land).toContain('---WTREE---');
|
||
expect(land).toContain('gstack-evidence check --label tests --expect-cmd "$TEST_COMMAND" --max-age 24');
|
||
expect(land).toContain('gstack-evidence run --label tests -- "$TEST_COMMAND"');
|
||
expect(land).toContain('UNKNOWN');
|
||
});
|
||
|
||
test('the review dashboard staleness rule is wtree-first for diff-scoped rows', () => {
|
||
// The dashboard text is generated into every skill that embeds
|
||
// {{REVIEW_DASHBOARD}}; ship is the canonical carrier.
|
||
const ship = rendered('ship/SKILL.md');
|
||
expect(ship).toContain('---WTREE---');
|
||
expect(ship).toContain('Content-first rule');
|
||
expect(ship).toContain('A failed command means UNKNOWN, treated as stale');
|
||
});
|
||
|
||
test('the diff-scoped row list is IDENTICAL in both grading surfaces (no drift)', () => {
|
||
// The resolver (dashboard) and land-and-deploy each carry the row list;
|
||
// they diverged once (codex-review present in one, missing in the other).
|
||
// Rendered dashboards escape backticks (template-literal origin), so match
|
||
// structurally: the three row names in order inside the rule sentence.
|
||
const rowList = /Content-first rule[\s\S]{0,80}?`review`[\s\S]{0,80}?`adversarial-review`[\s\S]{0,80}?`codex-review`[\s\S]{0,80}?ship-stage (?:entries|reviews)[\s\S]{0,80}?`design-review-lite`/;
|
||
expect(rendered('ship/SKILL.md')).toMatch(rowList);
|
||
// land-and-deploy's copy of the row list lives in the carved readiness-gate
|
||
// section (Step 3.5a), not the skeleton.
|
||
expect(rendered('land-and-deploy/sections/readiness-gate.md')).toMatch(rowList);
|
||
});
|
||
|
||
test('both grading surfaces reject missing capture instead of falling back to HEAD', () => {
|
||
for (const file of ['ship/SKILL.md', 'land-and-deploy/sections/readiness-gate.md']) {
|
||
const text = rendered(file);
|
||
expect(text).toContain('review_freshness');
|
||
expect(text).toContain('UNVERIFIED');
|
||
expect(text).toContain('Never fall back');
|
||
expect(text).toMatch(/(?:0|zero) commits/);
|
||
expect(text.toLowerCase()).toMatch(/plan-tier|plan records/);
|
||
}
|
||
});
|
||
|
||
test('diff callers capture before reading and consume the original token', () => {
|
||
const review = rendered('review/SKILL.md');
|
||
expect(review).toContain('gstack-review-log --start review\ngit diff "$DIFF_BASE"');
|
||
expect(review).toContain('--finish REVIEW_START');
|
||
expect(review).toContain('"completed":COMPLETED,"converged":CONVERGED,"cycles":CYCLES');
|
||
const army = rendered('ship/sections/review-army.md');
|
||
expect(army.indexOf('gstack-review-log --start review')).toBeLessThan(army.indexOf('run `git diff origin/<base>`'));
|
||
expect(army).toContain('--finish REVIEW_START');
|
||
expect(army.replace(/\s+/g, ' ')).toContain('Complete items 5–6 exactly once with the original REVIEW_START');
|
||
expect(army).toContain('fixes also require `converged:false`');
|
||
const ship = rendered('ship/SKILL.md');
|
||
expect(army.replace(/\s+/g, ' ')).toContain('**Third fixing cycle reached (`CYCLES >= 3`):** STOP and report recurring findings with `converged:false`; do not run a fourth fixing cycle');
|
||
expectMentions(ship.replace(/\s+/g, ' '), [['never', 'approvals', 'expands']], 'ship.replace(/\s+/g, )');
|
||
expect(army).toContain('--start design-review-lite');
|
||
expect(army).toContain('--finish DESIGN_START');
|
||
const codex = rendered('codex/sections/review-mode.md');
|
||
const starts = [...codex.matchAll(/gstack-review-log --start codex-review/g)];
|
||
expect(starts).toHaveLength(2);
|
||
const nativeReview = codex.indexOf(`run-with-timeout --user-invoked --effort high --request-effort '<effort-level>' 330 codex review`);
|
||
expect(nativeReview).toBeGreaterThan(-1);
|
||
expect(starts[0].index).toBeLessThan(nativeReview);
|
||
expect(starts[1].index).toBeLessThan(codex.indexOf('git diff "<base>...HEAD"'));
|
||
expect(codex).toContain('--finish CODEX_REVIEW_START');
|
||
expect(codex).toContain('"completed":COMPLETED,"converged":CONVERGED');
|
||
expectMentions(codex, [['until', 'genuine', 'fixes']], 'codex');
|
||
for (const skill of ['ship', 'review']) {
|
||
const adversarial = rendered(`${skill}/sections/adversarial.md`);
|
||
expect(adversarial).toContain('--start adversarial-review');
|
||
expect(adversarial).toContain('--finish PASS_START');
|
||
expectMentions(adversarial.replace(/\s+/g, ' '), [['before', 'adversarial', 'structured']], 'adversarial.replace(/\s+/g, )');
|
||
expect(adversarial).toContain('Each token is consumed once');
|
||
}
|
||
});
|
||
|
||
test('dashboard selection and freshness precede a verdict without replacing the live ship gate', () => {
|
||
for (const host of ALL_HOST_CONFIGS) {
|
||
const text = generateReviewDashboard({ host: host.name, skillName: 'ship',
|
||
tmplPath: 'ship/SKILL.md.tmpl', paths: HOST_PATHS[host.name] }).replace(/\s+/g, ' ');
|
||
const positions = ['**1. Choose the records', '**2. Check freshness',
|
||
'**3. Choose the historical verdict', '**4. Display the dashboard'].map(marker => text.indexOf(marker));
|
||
expect(positions.every(position => position >= 0)).toBe(true);
|
||
expect(positions).toEqual([...positions].sort((a, b) => a - b));
|
||
expectMentions(text, [['never', 'substitute', 'success']], 'text');
|
||
expect(text).toContain('CLEARED requires the selected Eng Review to be `clean`, within 7 days and fresh under step 2');
|
||
expect(text).toContain('STALE or UNVERIFIED cannot clear Eng Review');
|
||
expect(text).toContain('Missing `review_freshness`, including legacy log-only records, means UNVERIFIED');
|
||
expectMentions(text, [['never', 'convergence', 'approval']], 'text');
|
||
expect(text).toContain('Continue Step 1 even when history is NOT CLEARED');
|
||
}
|
||
});
|
||
|
||
test('release-body write side carries the banner tripwire (and it actually fires)', () => {
|
||
const body = rendered('document-release/sections/release-body.md');
|
||
expect(body).toContain('grep -c "UNTRUSTED TRACKER CONTENT" "<run-dir>/body.md"');
|
||
expect(body).toContain('grep -c "UNTRUSTED TRACKER CONTENT" "<run-dir>/body-original.md"');
|
||
// The fail-open shape: grep -c prints 0 AND exits 1 on no-match, so an
|
||
// `|| echo 0` double-emits and breaks the -gt into the clean branch.
|
||
expect(body).not.toContain('|| echo 0');
|
||
expect(body).toContain('banner tripwire clean');
|
||
|
||
// Functional: execute the template's tripwire block against a 0-banner
|
||
// original and a 1-banner outgoing body — the ABORT branch must fire.
|
||
//
|
||
// Pass the script as an ARGV element (spawnSync array form), never by
|
||
// interpolating JSON.stringify into a shell line: JSON escaping is not
|
||
// shell escaping. Inside shell double quotes a JSON "\n" stays a literal
|
||
// backslash-n, which collapsed this multi-line script onto one line where
|
||
// `then\n` became the command word `thenn` and `>&2\nelse\n` became the
|
||
// redirect `>&2nelsen` — silently littering a `2nelsen` file (containing
|
||
// "bash: thenn: command not found") in the repo root on every suite run,
|
||
// while the old not-contains assertion passed vacuously because ALL
|
||
// output had been redirected into that file.
|
||
const block = body.match(/_ORIG_BANNERS=\$\(grep[\s\S]*?fi\n/);
|
||
expect(block).not.toBeNull();
|
||
const fs = require('fs');
|
||
const os = require('os');
|
||
const path = require('path');
|
||
const { spawnSync } = require('child_process');
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-banner-'));
|
||
try {
|
||
const scriptFor = (origContent: string, newContent: string) => {
|
||
fs.writeFileSync(path.join(dir, 'orig.md'), origContent);
|
||
fs.writeFileSync(path.join(dir, 'new.md'), newContent);
|
||
return block![0]
|
||
.replaceAll('<run-dir>/body-original.md', path.join(dir, 'orig.md'))
|
||
.replaceAll('<run-dir>/body.md', path.join(dir, 'new.md'));
|
||
};
|
||
|
||
// Banner leaked into the outgoing body → the ABORT branch fires, loudly.
|
||
const abort = spawnSync('bash', ['-c', scriptFor(
|
||
'clean body\n',
|
||
'body with UNTRUSTED TRACKER CONTENT banner leak\n',
|
||
)], { encoding: 'utf-8', timeout: 30_000 });
|
||
expect(abort.stderr).toContain('ABORT: envelope banner leaked');
|
||
expect(abort.stdout).not.toContain('banner tripwire clean');
|
||
|
||
// No banner delta → the clean branch fires.
|
||
const clean = spawnSync('bash', ['-c', scriptFor(
|
||
'clean body\n',
|
||
'also clean body\n',
|
||
)], { encoding: 'utf-8', timeout: 30_000 });
|
||
expect(clean.stdout).toContain('banner tripwire clean');
|
||
expect(clean.stderr).not.toContain('ABORT');
|
||
} finally {
|
||
fs.rmSync(dir, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test('greptile triage reads bodies through the guard (metadata/body split)', () => {
|
||
const triage = rendered('review/greptile-triage.md');
|
||
expect(triage).toContain('gstack-issue-guard --stdin --source greptile-line');
|
||
expect(triage).toContain('gstack-issue-guard --stdin --source greptile-replies');
|
||
});
|
||
});
|