mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 11:40:51 +01:00
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:
1 parent
3c8a65c36b
commit
e8d876e337
30 files changed
+1109
-52
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']), {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user