v1.93.9.0 fix(receipt): --check-prose skips the required Review-loop exception line (GST-977) (#3863)

* fix(receipt): --check-prose skips the required Review-loop exception line (GST-977)

write --review-loop-exception requires the PR body to carry the exception line word for word, and read --check-prose read the owner reason in it as counter claims ("outside 1 and 2 skip detector" as 2 skip against 70, gstack-evals #139). The prose check now skips the line that matches the receipt exception exactly, as it skips the rendered Approvals block.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(receipt): a fenced copy of the exception line is skipped too (GST-977 review)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-02 17:33:51 +00:00
1 parent 40a3170335
commit d45c3501e1
8 files changed
+63 -7

No files matched your search

+10
View File
@@ -1,5 +1,15 @@
# Changelog
## [1.93.9.0] - 2026-11-02
**`gstack-ship-receipt read --check-prose` no longer reads the required Review-loop exception line as counter claims (GST-977).** `write --review-loop-exception` requires the PR body to carry the whole `Review-loop exception: …` line word for word, but the prose check read that line as prose. On gstack-evals #139 the owner's reason "outside 1 and 2 skip detector" was read as a `2 skip` claim against the receipt's 70 skips, so no body could pass both checks.
### Fixed
- **`lib/ship-receipt-prose.ts`:** the line that matches the receipt's own exception exactly (the same test `write` applies) is skipped, like the rendered `## Approvals` block. A copy of that exact line inside a code fence is skipped too, as `write` accepts one. The same words inside a longer line, or a line for a different exception, are still checked.
### Tests
- `test/ship-receipt-prose.test.ts`: the #139 exception text passes `--check-prose` beside a matching `70 skip` claim, and a wrong number elsewhere still fails. On main the same body failed with `gate.unit.skip body=2 receipt=70`.
## [1.93.8.0] - 2026-11-02
**/ship now runs Step 11's voices before any gate-class stop, and a recorded owner grant runs its fix-check.** On a gate-class change, a blocker fixed in review round 2 used to stop the run for the owner before Step 11 ran. If the owner then chose "ship with open notes", gate code shipped with no adversarial, outside or targeted review (C6-36 after-opus). `gstack-review-plan` now answers `NEXT: voices` for any gate-class stop while Step 11 has not run. /ship runs Step 11 as it does on `done`, asks the plan again with `--adversarial-ran`, and the stop then stands. The round cap still limits fix rounds; it no longer skips the required voices. Second, the owner's grant of one scoped fix-check after round 2 had no way in: the plan took no grant input, and the pass program printed `PASS: none next=stop`. Now `gstack-review-plan --grant <approval id>` checks that the branch's `gstack-approvals` record holds that id as an approved grant naming `review_loop_grant` and this run's stop head (exit 3 for any other row, a grant recorded for an earlier /ship on the branch included), and plans the one fix-check in place of the stop after a round-2 full pass. The pass program then dispatches that fix-check like any other. A fix-check never earns another, so the grant cannot repeat. After `NEXT: voices`, a blocking finding the voices queue goes back to the plan as `--blocking-open` instead of starting a new review pass.
+1 -1
View File
@@ -1 +1 @@
1.93.8.0
1.93.9.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.93.8.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.93.9.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+6 -1
View File
@@ -1227,7 +1227,12 @@ block `gstack-approvals` renders (its heading, the Date | Asked | Exact words
table and the `Source: gstack-approvals record` line) quotes the owner's
words, not this PR's numbers, so it is skipped: "one Step 11 pass" in that table
is not a `pass` claim. Text you write under the heading after that block, and
a `## Approvals` example inside a code fence, are still checked. Data PRs (evals,
a `## Approvals` example inside a code fence, are still checked. The
`Review-loop exception: …` line the receipt's own exception requires is
skipped the same way (GST-977): it quotes the owner's reason, so "outside 1 and
2 skip detector" is not a `2 skip` claim. Only a line that matches the rendered
line exactly is skipped, inside a code fence too, as the receipt check accepts
a fenced copy; the same words inside a longer line are still checked. Data PRs (evals,
measurements, sweeps) run it before READY so the body's numbers are the run's.
<a id="review-scope"></a>
+2 -1
View File
@@ -5903,7 +5903,8 @@ body that names a counter the receipt records (`passes=3` against
against `gate.passed: 41`) with a different value. The receipt is written from
the run; the prose was typed, so a reader of the body would learn a number the
run did not produce. The `PROSE:` line names each field with both values. The
`## Approvals` table `gstack-approvals` renders is never read as a claim.
`## Approvals` table `gstack-approvals` renders, and the `Review-loop exception: …`
line the receipt's exception requires, are never read as claims.
**Fix.** Correct the body to the receipt's number. When the receipt is the
stale one (the run was repeated), rerun `/ship` so the receipt is rewritten
+18 -2
View File
@@ -7,6 +7,7 @@
*/
import { APPROVALS_FOOTER_END, APPROVALS_TABLE_HEADER } from './approval-record';
import { RECEIPT_FENCE } from './ship-receipt';
import { parseReviewLoopException, reviewLoopExceptionLine } from './ship-receipt-depth';
export interface ProseClaim { field: string; prose: number; receipt: number }
export interface ProseCheck { checked: number; mismatches: ProseClaim[] }
@@ -65,9 +66,24 @@ function withoutApprovalsBlocks(body: string): string {
return out.join('');
}
/**
* The body without the `Review-loop exception: …` line the receipt's own exception requires (GST-977): write and the
* receipt check want it word for word, and it quotes the owner's reason (`outside 1 and 2 skip detector`), not this
* PR's counters. Only a line whose trimmed text is that rendered line, the test write applies, is left out (a fenced
* copy too, as write accepts one); the same words inside a longer line stay prose. A marker line takes its place so
* the text on either side never joins into a claim.
*/
function withoutExceptionLine(body: string, exception: unknown): string {
if (exception === undefined) return body;
const parsed = parseReviewLoopException(exception);
if (!parsed.ok) return body;
const line = reviewLoopExceptionLine(parsed.exception);
return body.split(/(?<=\n)/).map(l => (l.trim() === line ? '(review-loop exception line)\n' : l)).join('');
}
/**
* Every `<name>=<n>`, `<name>: <n>` and `<n> <name>` in the prose (the body
* without its receipt block and its rendered Approvals block), where `<name>` is a counter's leaf key with `_`
* without its receipt block, its rendered Approvals block and its review-loop exception line), where `<name>` is a counter's leaf key with `_`
* matched as `_`, space or `-` and not part of a longer word (`3 pass-through`
* is not a `pass` claim), compared with the receipt. A leaf key shared by
* several counters (`pass` under every gate lane, `passed` under gate and ci)
@@ -77,7 +93,7 @@ function withoutApprovalsBlocks(body: string): string {
* `<n> <name>` claim about the next key (#3513).
*/
export function checkReceiptProse(receipt: Record<string, unknown>, body: string): ProseCheck {
const prose = withoutApprovalsBlocks(body.replace(BLOCK_RE, ''));
const prose = withoutExceptionLine(withoutApprovalsBlocks(body.replace(BLOCK_RE, '')), receipt.review_loop_exception);
const byLeaf = new Map<string, Array<{ field: string; value: number }>>();
for (const [field, value] of receiptCounters(receipt)) {
const leaf = field.split('.').at(-1)!;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.93.8",
"version": "1.93.9",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+24
View File
@@ -118,6 +118,30 @@ describe('the rendered Approvals block is not prose', () => {
});
});
describe('the Review-loop exception line (GST-977, gstack-evals #139)', () => {
const excepted = {
...receipt,
review_loop: { ...receipt.review_loop, fix_checks: 1 },
gate: { status: 'green', run: '139', unit: { pass: 1181, skip: 70 } },
review_loop_exception: { accepted_by: 'Garry', date: '2026-11-01', via: 'GEVA-2', reason: 'outside 1 and 2 skip detector', fix_check: { head: 'b'.repeat(40), findings: 0 } },
};
const line = 'Review-loop exception: accepted by Garry 2026-11-01 via GEVA-2: outside 1 and 2 skip detector (fix-check at bbbbbbbbbbbb, 0 findings)';
// Value: protects=a body that carries the exception line write requires can also pass --check-prose; fails_when=the
// owner's reason is read as counter claims ("2 skip" against 70); seam=lib/ship-receipt-prose.ts checkReceiptProse
test('the exact line the receipt\'s exception requires is not prose; the rest of the body still is', () => {
expect(checkReceiptProse(excepted, `${line}\n\nGate: 70 skip.\n`)).toEqual({ checked: 1, mismatches: [] });
expect(checkReceiptProse(excepted, ` ${line}\r\nGate: 69 skip.`).mismatches).toEqual([{ field: 'gate.unit.skip', prose: 69, receipt: 70 }]);
});
test('the same words anywhere else, or a line for a different exception, stay prose', () => {
const twoSkip = [{ field: 'gate.unit.skip', prose: 2, receipt: 70 }];
expect(checkReceiptProse(excepted, `Note: ${line}`).mismatches).toEqual(twoSkip);
expect(checkReceiptProse(excepted, line.replace('GEVA-2', 'GEVA-3')).mismatches).toEqual(twoSkip);
expect(checkReceiptProse({ ...excepted, review_loop_exception: undefined }, line).mismatches).toEqual(twoSkip);
});
});
describe('gstack-ship-receipt read --check-prose', () => {
const run = (body: string) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'receipt-prose-'));