feat(review,ship): Gate Integrity category, GATE_SCAN_BLOCK, one-way gate disposition, actor stamping, ZERO-RUN lanes, draft-while-open

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-10 17:34:22 +00:00
1 parent 3c8a65c36b
commit e8d876e337
30 files changed
+1109 -52

No files matched your search

+3 -1
View File
@@ -33,7 +33,9 @@ function walk(dir: string): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e =>
e.isDirectory() ? walk(path.join(dir, e.name)) : e.name.endsWith('.md') ? [path.join(dir, e.name)] : []);
}
const fences = (text: string) => [...text.matchAll(/```bash\n([\s\S]*?)\n```/g)].map(m => m[1]!);
// Indentation-aware: an indented fence (inside a list item) closes with its own indent,
// so an unindented regex would span from it to the next fence's opener.
const fences = (text: string) => [...text.matchAll(/^([ \t]*)```bash\n([\s\S]*?)\n\1```[ \t]*$/gm)].map(m => m[2]!);
function extractFunction(name: string): string {
const start = SETUP_SRC.indexOf(`\n${name}() {`);
+30 -2
View File
@@ -991,8 +991,10 @@ confirming that every allowed edit is release metadata:
| Receipt result | Next action |
|---|---|
| FRESH (exit 0) | Cite the label, exit, timestamp and log. |
| FRESH (exit 0) | Cite the label, exit, timestamp, log and `tests_ran`. |
| STALE/MISSING: changed content, command or age, or no proven run | Run `~/.gemini/antigravity-cli/skills/gstack/bin/gstack-evidence run --label <lane> -- '<command>'`, read the result and recheck once. Handle failures as described below. |
| ZERO-RUN: the receipt's `tests_ran` is 0 | STALE-equivalent: the lane proved nothing. Use Step 5's ZERO-RUN triage (a failed lane), never FRESH, even with exit 0. |
| `tests_ran: unknown` | Non-blocking: keep the exit-0 verdict, cite the lane as `count unavailable`. Never record it as ZERO-RUN or as a counted pass. |
| Only receipt storage/readback failed | Independently prove unchanged final content, the same command and valid age from the successful run's evidence. Cite its exact command, exit, timestamp and log as **ledger unavailable**, never FRESH. Without that proof, use STALE/MISSING. |
No test lanes: require Step 5's explicit untested-scope approval for final content,
@@ -1013,7 +1015,17 @@ Preserve unrelated user files.
Paste build/docs/test results. Reuse waivers only for the same verified
pre-existing failures and approved scope; cite the actual approval and failing
counts, never FRESH or all-green. A new, changed or unwaived test failure uses
stage 4's recovery before publication. Otherwise continue to Step 17.
stage 4's recovery before publication.
Then the honesty inventory, two lines, from Step 9's latest `Gate edits:` line and
its records:
- For each gate finding: `gate`, path, disposition (`kept`/`restored`/`open`),
`reason` and logger-stamped `actor`. If there are none, the coverage line
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
Write this report in the auditor's posture: describe this session's own gate edits
as evidence for the reader to examine, not as positions to defend; a kept gate
states its reason and nothing more. Otherwise continue to Step 17.
---
@@ -1082,6 +1094,22 @@ Branch on the echoed values:
3. **Declined earlier** — continue
without comment.
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
failure STOPs here. If one exists and is not a draft:
1. The preamble echoed `SESSION_KIND: spawned` or `headless`: **STOP before pushing.**
Report the open gate findings (`gate`, path) and the PR/MR; no push, no
publication claim. A later human dispositions them.
2. Otherwise AskUserQuestion once: A) Resolve the open items now — insert `9 → 10 →
11 → 11.5 → 12 → 13 → 14 → 14.5 → 15 → 16` before Step 17; B) Publish ready with
the `Gate edits: N open` banner. Record B in the invocation record as a
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
**Idempotency check:** Check if the branch is already pushed and up to date.
```bash
+30 -2
View File
@@ -996,8 +996,10 @@ confirming that every allowed edit is release metadata:
| Receipt result | Next action |
|---|---|
| FRESH (exit 0) | Cite the label, exit, timestamp and log. |
| FRESH (exit 0) | Cite the label, exit, timestamp, log and `tests_ran`. |
| 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. Handle failures as described below. |
| ZERO-RUN: the receipt's `tests_ran` is 0 | STALE-equivalent: the lane proved nothing. Use Step 5's ZERO-RUN triage (a failed lane), never FRESH, even with exit 0. |
| `tests_ran: unknown` | Non-blocking: keep the exit-0 verdict, cite the lane as `count unavailable`. Never record it as ZERO-RUN or as a counted pass. |
| Only receipt storage/readback failed | Independently prove unchanged final content, the same command and valid age from the successful run's evidence. Cite its exact command, exit, timestamp and log as **ledger unavailable**, never FRESH. Without that proof, use STALE/MISSING. |
No test lanes: require Step 5's explicit untested-scope approval for final content,
@@ -1018,7 +1020,17 @@ Preserve unrelated user files.
Paste build/docs/test results. Reuse waivers only for the same verified
pre-existing failures and approved scope; cite the actual approval and failing
counts, never FRESH or all-green. A new, changed or unwaived test failure uses
stage 4's recovery before publication. Otherwise continue to Step 17.
stage 4's recovery before publication.
Then the honesty inventory, two lines, from Step 9's latest `Gate edits:` line and
its records:
- For each gate finding: `gate`, path, disposition (`kept`/`restored`/`open`),
`reason` and logger-stamped `actor`. If there are none, the coverage line
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
Write this report in the auditor's posture: describe this session's own gate edits
as evidence for the reader to examine, not as positions to defend; a kept gate
states its reason and nothing more. Otherwise continue to Step 17.
---
@@ -1087,6 +1099,22 @@ Branch on the echoed values:
3. **Declined earlier** — continue
without comment.
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
failure STOPs here. If one exists and is not a draft:
1. The preamble echoed `SESSION_KIND: spawned` or `headless`: **STOP before pushing.**
Report the open gate findings (`gate`, path) and the PR/MR; no push, no
publication claim. A later human dispositions them.
2. Otherwise AskUserQuestion once: A) Resolve the open items now — insert `9 → 10 →
11 → 11.5 → 12 → 13 → 14 → 14.5 → 15 → 16` before Step 17; B) Publish ready with
the `Gate edits: N open` banner. Record B in the invocation record as a
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
**Idempotency check:** Check if the branch is already pushed and up to date.
```bash
+30 -2
View File
@@ -1017,8 +1017,10 @@ $GSTACK_ROOT/bin/gstack-evidence check --label tests --expect-cmd '<tests>' --la
| Receipt result | Next action |
|---|---|
| FRESH (exit 0) | Cite the label, exit, timestamp and log. |
| FRESH (exit 0) | Cite the label, exit, timestamp, log and `tests_ran`. |
| STALE/MISSING: changed content, command or age, or no proven run | Run `$GSTACK_ROOT/bin/gstack-evidence run --label <lane> -- '<command>'`, read the result and recheck once. Handle failures as described below. |
| ZERO-RUN: the receipt's `tests_ran` is 0 | STALE-equivalent: the lane proved nothing. Use Step 5's ZERO-RUN triage (a failed lane), never FRESH, even with exit 0. |
| `tests_ran: unknown` | Non-blocking: keep the exit-0 verdict, cite the lane as `count unavailable`. Never record it as ZERO-RUN or as a counted pass. |
| Only receipt storage/readback failed | Independently prove unchanged final content, the same command and valid age from the successful run's evidence. Cite its exact command, exit, timestamp and log as **ledger unavailable**, never FRESH. Without that proof, use STALE/MISSING. |
No test lanes: require Step 5's explicit untested-scope approval for final content,
@@ -1039,7 +1041,17 @@ Preserve unrelated user files.
Paste build/docs/test results. Reuse waivers only for the same verified
pre-existing failures and approved scope; cite the actual approval and failing
counts, never FRESH or all-green. A new, changed or unwaived test failure uses
stage 4's recovery before publication. Otherwise continue to Step 17.
stage 4's recovery before publication.
Then the honesty inventory, two lines, from Step 9's latest `Gate edits:` line and
its records:
- For each gate finding: `gate`, path, disposition (`kept`/`restored`/`open`),
`reason` and logger-stamped `actor`. If there are none, the coverage line
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
Write this report in the auditor's posture: describe this session's own gate edits
as evidence for the reader to examine, not as positions to defend; a kept gate
states its reason and nothing more. Otherwise continue to Step 17.
---
@@ -1110,6 +1122,22 @@ Branch on the echoed values:
3. **Declined earlier** — continue
without comment.
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
failure STOPs here. If one exists and is not a draft:
1. The preamble echoed `SESSION_KIND: spawned` or `headless`: **STOP before pushing.**
Report the open gate findings (`gate`, path) and the PR/MR; no push, no
publication claim. A later human dispositions them.
2. Otherwise AskUserQuestion once: A) Resolve the open items now — insert `9 → 10 →
11 → 11.5 → 12 → 13 → 14 → 14.5 → 15 → 16` before Step 17; B) Publish ready with
the `Gate edits: N open` banner. Record B in the invocation record as a
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
**Idempotency check:** Check if the branch is already pushed and up to date.
```bash
+30 -2
View File
@@ -998,8 +998,10 @@ $GSTACK_ROOT/bin/gstack-evidence check --label tests --expect-cmd '<tests>' --la
| Receipt result | Next action |
|---|---|
| FRESH (exit 0) | Cite the label, exit, timestamp and log. |
| FRESH (exit 0) | Cite the label, exit, timestamp, log and `tests_ran`. |
| STALE/MISSING: changed content, command or age, or no proven run | Run `$GSTACK_ROOT/bin/gstack-evidence run --label <lane> -- '<command>'`, read the result and recheck once. Handle failures as described below. |
| ZERO-RUN: the receipt's `tests_ran` is 0 | STALE-equivalent: the lane proved nothing. Use Step 5's ZERO-RUN triage (a failed lane), never FRESH, even with exit 0. |
| `tests_ran: unknown` | Non-blocking: keep the exit-0 verdict, cite the lane as `count unavailable`. Never record it as ZERO-RUN or as a counted pass. |
| Only receipt storage/readback failed | Independently prove unchanged final content, the same command and valid age from the successful run's evidence. Cite its exact command, exit, timestamp and log as **ledger unavailable**, never FRESH. Without that proof, use STALE/MISSING. |
No test lanes: require Step 5's explicit untested-scope approval for final content,
@@ -1020,7 +1022,17 @@ Preserve unrelated user files.
Paste build/docs/test results. Reuse waivers only for the same verified
pre-existing failures and approved scope; cite the actual approval and failing
counts, never FRESH or all-green. A new, changed or unwaived test failure uses
stage 4's recovery before publication. Otherwise continue to Step 17.
stage 4's recovery before publication.
Then the honesty inventory, two lines, from Step 9's latest `Gate edits:` line and
its records:
- For each gate finding: `gate`, path, disposition (`kept`/`restored`/`open`),
`reason` and logger-stamped `actor`. If there are none, the coverage line
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
Write this report in the auditor's posture: describe this session's own gate edits
as evidence for the reader to examine, not as positions to defend; a kept gate
states its reason and nothing more. Otherwise continue to Step 17.
---
@@ -1091,6 +1103,22 @@ Branch on the echoed values:
3. **Declined earlier** — continue
without comment.
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
failure STOPs here. If one exists and is not a draft:
1. The preamble echoed `SESSION_KIND: spawned` or `headless`: **STOP before pushing.**
Report the open gate findings (`gate`, path) and the PR/MR; no push, no
publication claim. A later human dispositions them.
2. Otherwise AskUserQuestion once: A) Resolve the open items now — insert `9 → 10 →
11 → 11.5 → 12 → 13 → 14 → 14.5 → 15 → 16` before Step 17; B) Publish ready with
the `Gate edits: N open` banner. Record B in the invocation record as a
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
**Idempotency check:** Check if the branch is already pushed and up to date.
```bash
+126
View File
@@ -0,0 +1,126 @@
/**
* Gate integrity in /ship and the shared scan block
* (docs/designs/HONEST_WORK_GATE_INTEGRITY.md W1 block, W3).
*
* What these protect: the one scan block is shared by /review and /ship and
* branches on the helper's exit instead of swallowing it; /ship runs it before
* the checklist read, again after the test-generation child returns and again
* after its own fixes; gate findings take the four gate options and the record
* fields the logger expects; zero-run lanes are a failed lane, not a pass; the
* PR body discloses gate edits and per-lane counts; an open finding keeps a new
* PR a draft. Tokens, order and meaning-level rules only (docs/test-value-bar.md).
*/
import { expect, test } from 'bun:test';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { generateGateScanBlock } from '../scripts/resolvers/gate-scan';
import { RESOLVERS } from '../scripts/resolvers/index';
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
import { between, compact, expectAbsent, expectMentions, expectOrdered, expectTokens } from './helpers/prompt-structure';
const root = join(import.meta.dir, '..');
const read = (rel: string) => readFileSync(join(root, rel), 'utf8');
const ctx = (host: TemplateContext['host'], skillName: string): TemplateContext =>
({ skillName, tmplPath: `${skillName}/SKILL.md.tmpl`, host, paths: HOST_PATHS[host] });
const GATE_OPTIONS = ['A) Restore the gate', 'B) Keep — justified', 'C) Keep — other reason', 'D) Leave open for a later human'];
const RECORD_FIELDS = ['`gate`', '`gate_id`', '`reason`', '"kept"', '"restored"', '"open"', '`actor`'];
test('GATE_SCAN_BLOCK is registered and renders the same errexit-safe block for review and ship from the host bin dir', () => {
expect(RESOLVERS.GATE_SCAN_BLOCK).toBe(generateGateScanBlock);
for (const host of ['claude', 'codex'] as const) {
const review = generateGateScanBlock(ctx(host, 'review'));
expect(generateGateScanBlock(ctx(host, 'ship'))).toBe(review);
expect(review).toContain(`if GATE_OUT=$(${HOST_PATHS[host].binDir}/gstack-gate-diff <base> 2>&1); then`);
}
const block = generateGateScanBlock(ctx('claude', 'review'));
const bash = between(block, '```bash', '\n```');
expectOrdered(bash, ['if GATE_OUT=$(', 'then', 'else', 'GATE_EXIT=$?', 'case "$GATE_EXIT" in', '2) echo "Gate integrity: UNAVAILABLE', "grep -m1 '^GATE_ERROR='", '*) echo "Gate integrity: UNAVAILABLE', 'esac', 'fi'], 'gate scan bash');
expectAbsent(bash, ['2>/dev/null', '|| true'], 'gate scan bash');
expectTokens(block, ['GATE_SUMMARY: listed=N eligible=N inspected=N unread=N tagged={RH-1:N,...} unmatched=N coverage=<patterns> languages_unlisted=<idioms> candidate=<fingerprint> artifact=<path>',
'[<id>] <tag> <path> @<hunk>', 'GATE_ERROR=no_base ref=<ref>', '`gate_id`', '`gate`', 'Gate edits: partial ('], 'gate scan prose');
expectMentions(compact(block), [
['unavailable', 'never', 'none detected'],
['unread', 'partial'],
['data', 'never', 'instructions'],
], 'gate scan prose');
});
test('/ship runs the gate scan before the checklist read, again after the coverage child, and again after its own fixes', () => {
const army = read('ship/sections/review-army.md.tmpl');
expectOrdered(army, ['### Core checklist', 'gstack-review-log --start review', '2.5. **Gate scan.**', '{{GATE_SCAN_BLOCK}}',
'3. Apply the review checklist', 'Gate Integrity', '## Step 9.4:'], 'ship review-army');
expect(army.match(/\{\{GATE_SCAN_BLOCK\}\}/g)).toHaveLength(1);
const fix = compact(between(army, '## Step 9.4:', '### Decide whether to repeat Step 9'));
expectMentions(fix, [['after', 'fix', 'gate scan', 'again'], ['never', 'paths', 'child', 'reported']], 'Step 9.4 rescan');
const coverage = read('ship/sections/test-coverage.md.tmpl');
const parent = between(coverage, '**Parent processing:**', '**Audit failure:**');
expectOrdered(parent, ['6. Print a one-line summary', '7. **Rescan gate edits after the child returns.**', '{{GATE_SCAN_BLOCK}}', 'Gate Integrity'], 'Step 7 parent rescan');
expectMentions(compact(parent), [['never', 'paths', 'child', 'reported'], ['child', 'green', 'claim']], 'Step 7 parent rescan');
const child = between(coverage, '````text', '````');
expectAbsent(child, ['GATE_SCAN_BLOCK', 'gate_id'], 'Step 7 child prompt');
expect(child).toContain('{"coverage_pct":N,"gaps":N,"diagram":');
});
test('/ship gate findings take the four gate options in order and persist the record fields', () => {
const army = read('ship/sections/review-army.md.tmpl');
const ask = compact(between(army, '3. **If ASK items remain,**', '4. **Finish and log this pass'));
expectOrdered(ask, ['A) Fix B) Skip', 'review-gate-disposition', '<gstack-qid:review-gate-disposition>', ...GATE_OPTIONS], 'Step 9.4 gate question');
expectMentions(ask, [['spawned', 'headless', 'd', 'recommended'], ['auto-chosen', 'never', 'disposition']], 'Step 9.4 gate question');
const persist = compact(between(army, '6. Persist the review result', '### Decide whether to repeat Step 9'));
expectTokens(persist, RECORD_FIELDS, 'Step 9.4 record');
expectMentions(persist, [['never', 'hunk text'], ['`open`', 'count', '`issues_found`'], ['logger', 'stamps', '`actor`']], 'Step 9.4 record');
const summary = compact(between(army, '5. Output summary:', '6. Persist the review result'));
expectOrdered(summary, ['`Gate edits: none detected (', '`Gate edits: N listed, M read, K findings —', '`Gate edits: partial (', '`Gate edits: UNAVAILABLE —'], 'Step 9.4 summary');
});
test('/ship passes a lane on its runner summary with three never-merged tests_ran states', () => {
const step5 = between(read('ship/sections/tests.md.tmpl'), '## Step 5: Run tests', '## Step 6:');
expectOrdered(step5, ['`tests_ran: N`', '`tests_ran: 0`', 'ZERO-RUN', '`tests_ran: unknown`', '`count unavailable`', '{{TEST_FAILURE_TRIAGE}}'], 'Step 5 count states');
expectMentions(compact(step5), [['runner summary', 'not', 'exit 0 alone'], ['zero-run', 'failed lane'], ['never', 'merge']], 'Step 5 count states');
const stage4 = compact(between(read('ship/SKILL.md.tmpl'), '### 4. Verify the frozen candidate', '### 5. Report, then push'));
expect(stage4).toMatch(/\| ZERO-RUN[^|]*\|[^|]*STALE-equivalent[^|]*never FRESH/);
expect(stage4).toMatch(/\| `tests_ran: unknown` \|[^|]*`count unavailable`/);
});
test('/ship Step 16 stage 5 reports each gate finding with disposition, reason and actor in the auditor posture', () => {
const stage5 = compact(between(read('ship/SKILL.md.tmpl'), '### 5. Report, then push', '## Step 17: Push'));
expectTokens(stage5, ['`gate`', '`kept`', '`restored`', '`open`', '`reason`', '`actor`', '`tests_ran`', 'none detected (', 'partial (', 'UNAVAILABLE'], 'Step 16 stage 5');
expectMentions(stage5, [['evidence', 'examine', 'not', 'defend']], 'Step 16 stage 5');
});
test('/ship PR body carries the Gate edits line and per-lane counts, and an open finding keeps a new PR a draft', () => {
const body = read('ship/sections/pr-body.md.tmpl');
const skeleton = between(body, '## Summary', '#### Compose the body');
const coverage = between(skeleton, '## Test Coverage', '## Pre-Landing Review');
expectTokens(coverage, ['Tests ran: <lane> N', 'Tests ran: <lane> count unavailable', '`tests_ran`'], 'PR body Test Coverage');
const review = between(skeleton, '## Pre-Landing Review', '## Exploratory QA');
expectOrdered(review, ['<findings from Step 9 code review', '<Gate edits:', 'none detected (', 'kept', 'open', 'UNAVAILABLE', '<Outside review:'], 'PR body Pre-Landing Review');
expectMentions(compact(review), [['never', 'none detected', 'partial', 'unavailable'], ['no hunk text']], 'PR body Pre-Landing Review');
const publish = between(body, '**Existing open PR/MR**', 'Branch on the exit code');
const existing = compact(between(publish, '**Existing open PR/MR**', '**No open PR/MR:**'));
expectOrdered(existing, ['gstack-post pr-body <pr-number>', 'gstack-post pr-title <pr-number>', 'no gate finding is `open`', 'gh pr ready <pr-number>', 'Draft kept: Gate edits: N open', 'Step 17'], 'existing PR draft state');
const create = compact(between(publish, '**No open PR/MR:**'));
expect(create).toContain('gstack-post pr-create --base <base> --title-file "$TITLE_FILE" --body-file');
expectMentions(create, [['`--draft`', 'gate finding', '`open`'], ['never', 'infers approval', 'pr existing']], 'new PR draft state');
const failure = compact(between(body, '- **3** `gh`/`glab` failed', '- Any other exit'));
expectMentions(failure, [['open gate findings', 'not disclosed'], ['never', 'disclosure', 'done']], 'disclosure failure');
});
test('/ship Step 17 stops a spawned or headless session before pushing open gate findings to a ready PR and asks an interactive one once', () => {
const step17 = compact(between(read('ship/SKILL.md.tmpl'), '## Step 17: Push', '**Idempotency check:**'));
expectOrdered(step17, ['**Open gate findings against an existing ready PR/MR:**', 'gh pr list --head <branch-name> --state open --json number,isDraft',
'`SESSION_KIND: spawned` or `headless`', '**STOP before pushing.**', 'A) Resolve the open items now', 'B) Publish ready with the `Gate edits: N open` banner'], 'Step 17 ready-PR gate');
expectMentions(step17, [['record', 'b', 'human-authorised'], ['never', 'convert', 'ready', 'draft'], ['new pr', 'draft', 'no question']], 'Step 17 ready-PR gate');
});
test('the catalog carries every id gstack uses with the source attribution', () => {
const catalog = read('review/gate-integrity.md');
expect(catalog).toContain('Adapted from the just-say-no-to-process-porn-and-ceremony skill');
expectMentions(compact(catalog), [['pattern names preserved', 'names are the deterrent']], 'catalog attribution');
for (const id of ['RH-1', 'RH-2', 'RH-3', 'RH-4', 'RH-5', 'RH-10', 'RH-12', 'RH-13', 'RH-14', 'RH-15', 'RH-16', 'PL-1', 'PL-2']) {
expect(catalog, `catalog lacks a heading for ${id}`).toMatch(new RegExp(`^### ${id} — `, 'm'));
}
});
+4 -4
View File
@@ -155,7 +155,7 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
},
behavioral: 'external',
externalTest: 'test/skill-e2e-ship-section-loading.test.ts',
maxSkeletonBytes: 79_300, // + v2.0 {{ASIDE_RESEARCH}} (Aside first, WebSearch fallback); measured 78_275
maxSkeletonBytes: 81_000, // + v2.0 {{ASIDE_RESEARCH}} (Aside first, WebSearch fallback); measured 78_275. + gate integrity (HONEST_WORK_GATE_INTEGRITY W3): Step 16 stage 4 ZERO-RUN/unknown receipt rows, stage 5 honesty inventory, Step 17 open-gate ready-PR gate; measured 80,540 (2026-10-10).
minUnionBytes: 181_000, // token-reduction Phases 1-2 (v1.69.x branch); measured union 201,464
mustContain: ['VERSION', 'CHANGELOG', 'review', 'merge', 'PR'],
// v1.58.5.0: pre-push-guard install (#2077) stacks on the shared first-run-guidance preamble.
@@ -168,7 +168,7 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
// wave's headline capability) grows the union to 1.195x. Deliberate:
// the section is on-demand (loads only for Apple store targets), so
// per-invocation cost for non-iOS ships is one manifest line.
maxSizeRatio: 1.5390, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 281,438 / 187,706 = 1.4994 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 281,556 / 187,706 = 1.5000 (2026-10-07). + Oct 7 wave B4: /ship publishes through gstack-post (title prepared in a file in the pr-body section, compose block plus gstack-post exit-code branches replacing the two scans); measured 281,894 / 187,706 = 1.5018 (2026-10-07). + B2 early Greptile PR (lazy Step 6.5 section: detect, public-repo consent, push, provisional title file, open; Step 10's bounded wait; the stop-leaves-the-draft rule); measured 286,626 / 187,706 = 1.5270 (2026-10-07). B3, the classifier, B4 and B2 merged: measured 287,128 / 187,706 = 1.5297 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 281,370 / 187,706 = 1.4990 (2026-10-07). Merged with the guard area: measured 287,440 / 187,706 = 1.5313 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 288,783 / 187,706 = 1.5385 (2026-10-08).
maxSizeRatio: 1.6050, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 281,438 / 187,706 = 1.4994 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 281,556 / 187,706 = 1.5000 (2026-10-07). + Oct 7 wave B4: /ship publishes through gstack-post (title prepared in a file in the pr-body section, compose block plus gstack-post exit-code branches replacing the two scans); measured 281,894 / 187,706 = 1.5018 (2026-10-07). + B2 early Greptile PR (lazy Step 6.5 section: detect, public-repo consent, push, provisional title file, open; Step 10's bounded wait; the stop-leaves-the-draft rule); measured 286,626 / 187,706 = 1.5270 (2026-10-07). B3, the classifier, B4 and B2 merged: measured 287,128 / 187,706 = 1.5297 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 281,370 / 187,706 = 1.4990 (2026-10-07). Merged with the guard area: measured 287,440 / 187,706 = 1.5313 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 288,783 / 187,706 = 1.5385 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W3): the shared GATE_SCAN_BLOCK in the core checklist (2.5) and after Step 7's child, the four-option gate question and record fields in 9.4, tests_ran states in Step 5, Gate edits and per-lane counts in the PR body with draft-while-open; measured 300,143 / 187,706 = 1.5990 (2026-10-10).
},
'plan-ceo-review': {
skill: 'plan-ceo-review',
@@ -484,10 +484,10 @@ do not launch the downstream skill or open a browser.`,
gateAfterStop: undefined, // operational multi-STOP skill, like ship
},
behavioral: 'plan',
maxSkeletonBytes: 74_884, // Shared-code identity/skip/action rules + critical-severity validation; measured 74,493 (2026-09-17). + v1.91.12.0 merge of #2999 (review clarity repairs: await reads, research alongside dispatch, /review deadline and setup authority, findings sources) with #3002 (guarded state-root lines, plan-check checkpoints); each fit alone; measured 74,881 (2026-10-01). + Oct 8 wave: shared voice directive bans "load-bearing" and adds the reply-language rule, offset by tightening the bounded closer (net +17 bytes per tier-2+ skill); measured 74,884 (2026-10-08).
maxSkeletonBytes: 80_600, // Shared-code identity/skip/action rules + critical-severity validation; measured 74,493 (2026-09-17). + v1.91.12.0 merge of #2999 (review clarity repairs: await reads, research alongside dispatch, /review deadline and setup authority, findings sources) with #3002 (guarded state-root lines, plan-check checkpoints); each fit alone; measured 74,881 (2026-10-01). + Oct 8 wave: shared voice directive bans "load-bearing" and adds the reply-language rule, offset by tightening the bounded closer (net +17 bytes per tier-2+ skill); measured 74,884 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W2): Step 3.5 Diff scans with the shared GATE_SCAN_BLOCK, the Step 4 carry, the four-option `review-gate-disposition` question in 5c, gate record fields in 5.8 and the Gate edits report line; measured 80,054 (2026-10-10).
minUnionBytes: 89_000, // Phase 4 wave 1; measured union 93,357
mustContain: ['confidence', 'P1', 'P2', 'Review Army', 'adversarial'],
maxSizeRatio: 1.2345, // Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 132,199 / 108,523 = 1.2182 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 132,258 / 108,523 = 1.2187 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 132,233 / 108,523 = 1.2185 (2026-10-07). Merged with the guard area: measured 132,522 / 108,523 = 1.2211 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 133,902 / 108,523 = 1.2339 (2026-10-08).
maxSizeRatio: 1.2900, // Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 132,199 / 108,523 = 1.2182 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 132,258 / 108,523 = 1.2187 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 132,233 / 108,523 = 1.2185 (2026-10-07). Merged with the guard area: measured 132,522 / 108,523 = 1.2211 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 133,902 / 108,523 = 1.2339 (2026-10-08). + gate integrity W2 (above); measured 1.282 (2026-10-10).
},
codex: {
skill: 'codex',
+1
View File
@@ -162,6 +162,7 @@ describe('one-way door safety', () => {
'ship-test-failure-triage', // shipping broken tests
'review-sql-safety', // SQL injection path
'review-llm-trust-boundary', // LLM trust boundary
'review-gate-disposition', // gate edit disposition: never-ask/AUTO_DECIDE must not waive it
'cso-global-scan-approval', // scans outside branch
'cso-finding-fix', // security finding
'land-and-deploy-merge-confirm', // actual merge
+66 -3
View File
@@ -4,15 +4,15 @@ import { chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, w
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { findFilesBySuffix, gitArgvIn } from './helpers/scratch-repo';
import { canReuseSharedLibsAdvisory, sharedLibsFingerprint } from '../lib/review-evidence';
import { canReuseSharedLibsAdvisory, sharedLibsFingerprint, stampGateFindings } from '../lib/review-evidence';
const ROOT = resolve(import.meta.dir, '..');
let repo: string;
let home: string;
function cli(name: string, args: string[] = [], cwd = repo) {
function cli(name: string, args: string[] = [], cwd = repo, extraEnv: Record<string, string | undefined> = {}) {
return execFileSync(join(ROOT, 'bin', name), args, {
cwd, env: { ...process.env, GSTACK_HOME: home }, encoding: 'utf8', timeout: 10_000,
cwd, env: { ...process.env, GSTACK_HOME: home, ...extraEnv }, encoding: 'utf8', timeout: 10_000,
}).trim();
}
@@ -51,6 +51,69 @@ afterEach(() => {
rmSync(home, { recursive: true, force: true });
});
// Gate findings: the logger, not the caller, says who dispositioned a gate
// edit. A child that relaxed a test cannot log `actor: interactive, action:
// kept` about itself; only a human-present session keeps a disposition.
describe('gate finding actor stamping', () => {
const gate = (overrides: Record<string, any> = {}) => ({
fingerprint: 'test/a.test.ts:12:gate-integrity', severity: 'CRITICAL',
gate: 'RH-15', gate_id: 'g-0001', action: 'kept', reason: 'measured in ship-measure report', ...overrides,
});
const sessionEnv = (kind: 'interactive' | 'spawned' | 'headless') => ({
GSTACK_SESSION_KIND: kind === 'spawned' ? 'spawned' : undefined,
GSTACK_HEADLESS: kind === 'headless' ? '1' : undefined,
OPENCLAW_SESSION: undefined,
CLAUDE_CODE_ENTRYPOINT: kind === 'interactive' ? 'cli' : undefined,
});
const logWith = (kind: 'interactive' | 'spawned' | 'headless', findings: Record<string, any>[]) => {
const token = cli('gstack-review-log', ['--start', 'review'], repo, sessionEnv(kind));
const record = {
skill: 'review', status: 'issues_found', timestamp: new Date().toISOString(),
commit: git('rev-parse', '--short', 'HEAD'), completed: true, converged: true, cycles: 0,
issues_found: findings.length, critical: findings.length, informational: 0, findings,
};
cli('gstack-review-log', [JSON.stringify(record), '--finish', token], repo, sessionEnv(kind));
return rows().at(-1)!.findings as Record<string, any>[];
};
test('an interactive session keeps the disposition and the logger replaces a caller-supplied actor', () => {
const [kept, restored, open, plain] = logWith('interactive', [
gate({ actor: 'human' }), gate({ action: 'restored', reason: undefined }),
gate({ action: 'open', reason: 'should be dropped' }),
{ fingerprint: 'src/a.ts:3:sql', severity: 'CRITICAL', action: 'skipped', actor: 'human' },
]);
expect(kept).toMatchObject({ actor: 'interactive', action: 'kept', reason: 'measured in ship-measure report' });
expect(restored).toMatchObject({ actor: 'interactive', action: 'restored' });
expect(open).toMatchObject({ actor: 'interactive', action: 'open' });
expect(open.reason).toBeUndefined();
expect(plain.actor).toBe('human');
expect(plain.action).toBe('skipped');
});
for (const kind of ['spawned', 'headless'] as const) {
test(`a ${kind} session cannot record kept or restored on a gate finding`, () => {
const [kept, restored, plain] = logWith(kind, [
gate({ actor: 'interactive' }), gate({ action: 'restored', reason: undefined }),
{ fingerprint: 'src/a.ts:3:sql', severity: 'CRITICAL', action: 'skipped' },
]);
expect(kept).toMatchObject({ actor: kind, action: 'open' });
expect(kept.reason).toBeUndefined();
expect(restored).toMatchObject({ actor: kind, action: 'open' });
expect(plain.actor).toBeUndefined();
expect(plain.action).toBe('skipped');
});
}
test('a malformed gate action is a missing disposition, not a kept gate', () => {
const findings = [gate({ action: 'skipped' }), gate({ action: 'fixed' }), gate({ action: 'auto-fixed' })];
stampGateFindings(findings, 'interactive');
for (const finding of findings) expect(finding).toMatchObject({ actor: 'interactive', action: 'open' });
const ignored = [{ gate: '', action: 'kept', actor: 'human' }, 'not a record', null];
stampGateFindings(ignored, 'spawned');
expect(ignored[0]).toEqual({ gate: '', action: 'kept', actor: 'human' });
});
});
describe('review start/end binding (#2803)', () => {
test('a matching core snapshot preserves incomplete coverage and a saved native tree exposes later untracked edits', () => {
const core = log(cli('gstack-review-log', ['--start', 'review']), {
+64 -2
View File
@@ -7,7 +7,7 @@ import { generatePlanCompletionAuditReview, generatePlanCompletionAuditShip, gen
import { generateQAReview } from '../scripts/resolvers/qa';
import { generateConfidenceCalibration } from '../scripts/resolvers/confidence';
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
import { between, compact, expectAbsent, expectMentions, expectTokens } from './helpers/prompt-structure';
import { between, compact, expectAbsent, expectMentions, expectOrdered, expectTokens } from './helpers/prompt-structure';
const root = join(import.meta.dir, '..');
const skill = readFileSync(join(root, 'review/SKILL.md.tmpl'), 'utf8');
@@ -148,7 +148,7 @@ test('caller QA runs charter and setup after resource loading and has a severity
test('review prepares context and deduplicates before classifying findings', () => {
const positions = [
'## Step 3.5: Slop scan', '## Step 3.6: Gather review context',
'## Step 3.5: Diff scans', '## Step 3.6: Gather review context',
'{{LEARNINGS_SEARCH}}', '{{ASIDE_RESEARCH}}', '## Step 4: Critical pass',
'## Step 5: Fix-First Review', '{{CROSS_REVIEW_DEDUP}}',
'**Keep decisions through fix cycles:**', '### Step 5a: Classify each finding',
@@ -488,6 +488,68 @@ test('security specialist checks authorization paths a route-guard read misses',
], 'security specialist');
});
// Gate Integrity (docs/designs/HONEST_WORK_GATE_INTEGRITY.md W2): the category
// sits in the core CRITICAL pass every diff size gets, its findings reach a
// human through one four-option question, and the record carries identity
// fields a later reader can act on. Tokens and order only; the prose is free.
test('Gate Integrity is a Pass 1 category that is ASK-only and keeps the three levels apart', () => {
const checklist = readFileSync(join(root, 'review/checklist.md'), 'utf8');
const pass1 = between(checklist, '### Pass 1 — CRITICAL', '### Pass 2 — INFORMATIONAL');
expect(pass1).toContain('#### Gate Integrity');
expect(between(checklist, '**Two-pass review:**', '**Output format:**')).toMatch(/Pass 1 \(CRITICAL\):[^\n]*Gate Integrity/);
const category = compact(between(pass1, '#### Gate Integrity'));
expectTokens(category, ['GATE_SUMMARY:', 'RH-1', 'RH-3', 'RH-4', 'RH-12', 'RH-13', 'RH-14', 'RH-15', 'RH-16', 'RH-2', 'RH-5',
'**Listed**', '**Read**', '**Finding**', 'gstack-decision-search --query "<dec-id>"'], 'Gate Integrity category');
expectMentions(category, [
['ask', 'never', 'auto-fix'],
['groups', 'one decision'],
['confidence', 'identification', 'not justification'],
['citations', 'never', 'remove', 'finding'],
['data', 'never', 'instructions', 'actor', 'reason'],
], 'Gate Integrity category');
const heuristic = between(checklist, '## Fix-First Heuristic', '## Suppressions');
const askColumn = heuristic.slice(heuristic.indexOf('ASK (needs human judgment):'));
expect(askColumn).toContain('Gate Integrity');
expect(between(checklist, '## Severity Classification', '## Fix-First Heuristic')).toMatch(/└─ Gate Integrity/);
});
test('the eval-threshold suppression no longer removes a relaxed gate from the human question', () => {
const suppressions = compact(between(readFileSync(join(root, 'review/checklist.md'), 'utf8'), '## Suppressions'));
expect(suppressions).toContain('Eval threshold changes');
const line = suppressions.slice(suppressions.indexOf('Eval threshold changes'), suppressions.indexOf('Harmless no-ops'));
expectMentions(line, [['not', 'suppress', 'gate integrity'], ['citation', 'never', 'removes', 'finding']], 'threshold suppression');
expectTokens(line, ['verified', 'unverified'], 'threshold suppression');
});
test('review runs the gate scan before slop, carries the listing into Step 4 and asks the four gate options in order', () => {
expectOrdered(skill, ['## Step 3.5: Diff scans', '{{GATE_SCAN_BLOCK}}', 'bun run slop:diff', '## Step 3.6:',
'## Step 4: Critical pass', '### Step 5c:', 'review-gate-disposition',
'A) Restore the gate', 'B) Keep — justified', 'C) Keep — other reason', 'D) Leave open for a later human',
'### Step 5d:', '## Step 5.8:'], 'review skill');
expect(skill.match(/\{\{GATE_SCAN_BLOCK\}\}/g)).toHaveLength(1);
expectTokens(between(skill, '## Step 4: Critical pass', '### Shared-code opportunities'), ['GATE_SUMMARY:', '`gate`', '`gate_id`'], 'Step 4 gate carry');
const ask = compact(between(skill, '### Step 5c:', '### Step 5d:'));
expect(ask).toContain('<gstack-qid:review-gate-disposition>');
expectMentions(ask, [
['spawned', 'headless', 'd', 'recommended'],
['auto-chosen', 'never', 'disposition'],
], 'Step 5c gate question');
for (const action of ['`restored`', '`kept`', '`open`']) expect(ask).toContain(action);
});
test('review persists gate identity fields, counts open findings and reports one Gate edits line', () => {
const record = compact(between(skill, '### 2. Fill the record', '### Report the final review'));
expectTokens(record, ['`gate`', '`gate_id`', '`reason`', '`kept`', '`restored`', '`open`', '`actor`', 'gstack-session-kind'], 'review record');
expectMentions(record, [
['never', 'hunk text'],
['`open`', 'count', '`issues_found`'],
['logger', 'stamps', '`actor`'],
], 'review record');
const report = compact(between(skill, '### Report the final review', '{{LEARNINGS_LOG}}'));
expectOrdered(report, ['`Gate edits: none detected (', '`Gate edits: N listed, M read, K findings —', '`Gate edits: partial (', '`Gate edits: UNAVAILABLE —'], 'final report');
expectMentions(report, [['unavailable', 'never', 'none detected']], 'final report');
});
test('review suppressions allow a clean result and skip common false positives but never suppress missing validation or a missing await', () => {
const suppressions = between(readFileSync(join(root, 'review/checklist.md'), 'utf8'), '## Suppressions');
expectMentions(compact(suppressions), [['zero findings', 'valid']], 'suppressions');
+5 -1
View File
@@ -67,6 +67,10 @@ describe("/ship Step 10 waits for the early review", () => {
describe("/ship Steps 18-19 finish the early PR", () => {
test("the provisional title is replaced and the draft is marked ready unless a draft was asked for", () => {
expect(flat(prBody)).toContain("For an existing open PR/MR (not Step 6.5's early PR, which takes item 2)");
expect(flat(prBody)).toContain("If Step 6.5 opened this PR as an early draft and the user did not ask for a draft, mark it ready now: `gh pr ready <pr-number>`");
// Gate integrity (HONEST_WORK_GATE_INTEGRITY W3): the early draft is marked
// ready only when the user did not ask for a draft AND no gate finding is open.
const ready = flat(prBody).slice(flat(prBody).indexOf('Then resolve the draft state'), flat(prBody).indexOf('**No open PR/MR:**'));
expect(ready).toContain('`gh pr ready <pr-number>`');
expect(ready).toMatch(/Step 6\.5 opened[^.]*did not ask for a draft[^.]*no gate finding is `open`[^.]*gh pr ready/);
});
});