Repository navigation
refactor(#5170): typed gate evidence and a verdict-driven exit status (Phase 8 of #5056) - #5171
Merged
Merged
Conversation
trek-e
requested review from
Solvely-Colin,
davesienkowski and
jeremymcs
as code owners
October 2, 2026 01:35
trek-e
pushed a commit
that referenced
this pull request
Oct 2, 2026
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
| const script = [ | ||
| "const fs = require('node:fs');", | ||
| 'const real = fs.statSync;', | ||
| `fs.statSync = function (p, ...rest) { if (String(p) === ${JSON.stringify(phasesDir)}) { throw Object.assign(new Error('EACCES: injected'), { code: 'EACCES' }); } return real.call(fs, p, ...rest); };`, |
trek-e
force-pushed
the
chore/5170-gate-evidence
branch
from
October 2, 2026 12:47
2a3ce07 to
5a238b5
Compare
trek-e
pushed a commit
that referenced
this pull request
Oct 2, 2026
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… (T1) Add src/gate-evidence.cts: Evidence<T> (found / none / unreadable), readTextEvidence and readDirEvidence (ENOENT and ENOTDIR are none; every other failure is unreadable carrying the errno), and verdictFromEvidence whose unreadable arm must return a branded UnreadableVerdict. GateOutcome gains 'unreadable' and gate-verdict gains gateUnreadable. Add src/gate-exit.cts: gateExitOutcome is a total mapping from a verdict and a mode to PASS, FAIL or UNAVAILABLE, and declareGateExit declares it through the new cli-exit declareOutcome after output() has run. emitGateResult declares in payload mode, so a delivered verdict keeps exit 0 and an unreadable one exits UNAVAILABLE. phase uat-passed and verify artifacts derive their exit status from their verdict (status mode): a negative verdict exits 1, a plan with nothing to evaluate exits UNAVAILABLE. Their stdout JSON is unchanged. verify-work.md and gsd-verifier.md branch on the exit status in the same change. Refs #5170, #5056 Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
phase uat-passed with passed:false and verify artifacts with all_passed:false now exit 1, and a plan with no artifacts to evaluate exits UNAVAILABLE. These assertions required result.success on a negative verdict, which encoded the old exit-0-on-failure behavior; they now pin the exit status that follows the verdict, and the surfaces that only need an answer accept exit 0 or 1. Refs #5170, #5056 Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…atus Cover readTextEvidence and readDirEvidence (missing, directory, injected EACCES and EIO, empty file, ENOTDIR), the compile-time guarantee that the unreadable arm cannot return a passing verdict, the total gateExitOutcome mapping as a seeded fast-check property with a positive control, declareOutcome, and child-process CLI tests for phase uat-passed and verify artifacts. Refs #5170, #5056 Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s outcome unreadable (T2) Migrate every gate module read onto the Evidence seam and delete the tolerant readers (ADR-5057 section 4, third bullet): - gate-phase-context: readIfExists is deleted. resolvePhaseDirOrEmpty becomes resolvePhaseDir (Evidence), findUiSpecInDir and lookupRoadmapPhase answer none vs unreadable, locateUiSpec composes the two, and the two empty catch clauses are gone. - gate-evidence: evidenceFromError, readDirEntriesEvidence, statEvidence and the Observed type, so a gate never needs a bare catch around a read. - ui-plan-gate / ui-safety-gate: an unreadable ROADMAP or phase directory, and ui-safety's unresolvable scope, are outcome unreadable (block stays the gate's own policy). The verify-command-paths / verify-failure-directions probes that cannot look are outcome unreadable with the payload unchanged. - api-coverage verify-pre: the empty readdir catch at the COVERAGE.md lookup is unreadable and never falls through to the detector; an unreadable COVERAGE.md or plan scope stays block:true with outcome unreadable. - tdd-review-checkpoint: an unreadable plan or phase directory is unreadable, not "not a TDD plan"; an absent phase directory stays none and the existing skip. - predicate: findPhaseArtifact throws on an unreadable lookup (the existing usage failure) instead of reading it as "artifact not found". - tdd-red-evidence, decision-coverage plan/verify and their support module read CONTEXT.md, plans, summaries and listed files as evidence; an unreadable one is outcome unreadable instead of an empty string that certifies "no decisions". Delivered pass/skip/advisory/block payloads for readable evidence are unchanged (the cutover goldens still match byte for byte). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…p / exit 0 A verdict whose evidence could not be read is outcome unreadable and exits 69 (UNAVAILABLE); it is never a skip or exit 0. Update the tests that pinned the old outcome: - cutover goldens: the nine arms that cannot look (ui-safety unresolvable scope, both verify probes, unreadable COVERAGE.md / plan scope) now record exitCode 69; their stdout is unchanged byte for byte. - unit tests for ui-safety, verify-command-paths, verify-failure-directions and the evaluation-scope consumers expect outcome unreadable. - the execute-wave pipeline fixture wrote files_modified outside the plan's frontmatter; it now carries the field in the frontmatter block, the shape production plans have, because schema-drift reads it through the Frontmatter Module. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… Module; negated grep gates are scanned (#4562, #4541) verify schema-drift (#4562): the plan's files_modified came from a `files_modified:\s*\[...\]` regex that knew only the inline array, so a YAML block sequence yielded no files and a schema change reported "no drift". The list is now read the way phase-plan-index reads it (RawPlan.filesModified), so block sequences (one or many items), inline arrays and CRLF all yield the files. A plan or summary that cannot be read is reported as unreadable (the existing payload plus unreadable / unreadable_file / read_error, exit 69) and is never "no files, no drift"; --skip still bypasses the gate. verify plan-structure (#4541): the negative-grep comment-echo scan fired only for `grep -c 'LIT' f == 0`. `! grep -q 'LIT' f`, `if ! grep ...` and `grep ...; test $? -ne 0` are the same gate and now fire too, and the error names both forms; a plain positive `grep -q 'LIT' f` gate does not fire, and an inverted grep is still a positive assertion. A negated command pasted into an <action> does not flag itself. Tests: block-sequence (1 and N items), inline, CRLF, unreadable plan / summary, --skip control; negated spellings, positive controls, allowlist, bareword, and an end-to-end plan-structure case. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…AILABLE in every gate (matrix rows 16-23) Behavioral coverage, each with a control that reads cleanly: - cutover suite: exit status follows the verdict, driven through the real CLI (ui-safety unresolvable scope, both probes, unreadable api-coverage evidence exit 69 with the payload unchanged; a delivered block stays exit 0). - api-coverage: the phase directory that cannot be listed (the former empty catch) is unreadable and never the detector's pass; an unreadable COVERAGE.md and plan scope keep block:true with outcome unreadable. - tdd-review-checkpoint: an unreadable plan is unreadable, an absent phase directory is none and the existing skip. - predicate: an unlistable phase directory is the usage failure, an absent one is artifactNotFound. - ui-plan: unreadable phase directory and ROADMAP; decision-coverage plan and verify: unreadable CONTEXT.md, plan and listed file; tdd-red-evidence: unreadable record. Read and readdir failures are injected by replacing the fs method and restoring it in finally. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
check verify-command-paths and check verify-failure-directions exit 0 whenever they could look (a blocker finding is still exit 0) and exit 69 (UNAVAILABLE) with the same JSON when they could not. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… documented ports, and reports failed captures as not captured (#4176) The static capture block misread a redirecting dev server as no server (no -L, equality against 200), printed "Screenshots captured" when every capture failed (stderr discarded, no exit or file check), and never tried the 5173/8080 fallback the next line documents. It now probes 3000, 5173, 8080 with -L and a bounded --max-time, judges each capture by exit status and a non-empty file, and ends on captured / PARTIAL / NOT captured. Both agents/gsd-ui-auditor.md and the compact variant carry the fix; the test executes the extracted block against a local http server. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…lock harness Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…l reach the report (#4031) --test-force-exit (the Windows post-test hang backstop, #1051/#869) can end the node --test parent while part of a test file's results is still unread on the child's pipe (nodejs/node#64833). The runner then printed a smaller count and exited 0, so every reported count was a lower bound. The count of registered tests now comes from the child over a channel that does not share that pipe: scripts/lib/test-registration-ledger.cjs is loaded into each test-file child with --require, counts the test()/it() registrations made through node:test and appends one line per file at exit. The ndjson reporter records the kind (suite or test) of every pass/fail, and analyzeChunkAccounting compares the two per file. A chunk that exited 0 with registered > reported now fails, naming the chunk and the counts, and the remaining chunks still run. The check is one-sided (run-time subtests are reported but not registered), so it can miss a loss but cannot invent one. --test-force-exit stays the default, so the Windows hang protection is intact. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…t-* pattern and regenerate the conformance tiers A scripts/lib file whose basename matches Node's test-collection pattern would be collected and executed as a test by the remote runner, so the preload is registration-ledger-preload.cjs. The Windows and macOS conformance-tier lists are regenerated for the two new test files. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…at exit 69 as "could not look" The verify-command-paths / verify-failure-directions probes and the ui-plan-gate now exit 69 (UNAVAILABLE) when they could not look, still printing their JSON. plan-phase.md, the quick plan-checker loop and the ui-plan-gate capture branch on the captured exit status instead of reading the JSON blind: 0 is a normal look, 69 hands the unresolvable payload to the checker unchanged, any other non-zero is a command failure. verify-work.md's gate dispatch captures CHECK_EXIT like the other dispatch hosts, and the shared loop-hook-dispatch contract states once that exit 69 is a step-1 command failure routed by onError, never a verdict. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nd aborts on exit 69 (matrix row 30) Extracts both documented captures from verify-work.md and runs them under bash with set -e and a stub gsd_run: exit 0 and exit 1 with JSON are verdicts and are read, exit 1 with nothing on stdout, exit 69 and exit 2 abort with "could not run" and read nothing. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d exits 69; the workflow captures keep its JSON `check evaluation-scope` mapped scope.status 'unresolvable' (a missing git, a timeout, an unknown phase) to outcome 'skip' and exit 0: "could not look" reported as a pass-shaped exit. It is now outcome 'unreadable' (block stays false: the resolver is not a policy), so the router exits UNAVAILABLE (69). The JSON payload is unchanged and still printed. Consumers: the code-review and quick-task scope captures no longer discard the JSON that carries the degradation reason (`|| SCOPE_JSON=""`) nor abort under `set -e`; exit 69 keeps the JSON, any other non-zero status drops stdout. The gsd-code-reviewer agent (full and compact) notes exit 69. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…; workflow captures survive set -e Corrects the resolver test that pinned 'skip' for an unresolvable scope, and adds the CLI case (exit 69 with the JSON and reason on stdout, payload equal to the in-process verdict), the degraded control (exit 0), and a bash set -e harness over the code-review and quick-task captures (exit 0 / 69 / 68 / 70 / 1-with-empty-stdout). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ed evidence; schema-drift exceptions exit UNAVAILABLE src/gap-checker.cts (the engine behind `check gap-analysis-plan-post`) swallowed every read: an unreadable config, REQUIREMENTS.md, CONTEXT.md or plan became '' (so every ID read "Not covered", or "No requirements or decisions to check"), a phase directory read failure never reached the verdict, and `existsSync` answered "absent" for an EACCES on a parent. Every read is now `readTextEvidence`; anything that exists but cannot be read is listed in an additive `unreadable` field (present only when non-empty, so the delivered wire shape is unchanged) and the gate returns outcome `unreadable` (block:false, advisory policy unchanged) which exits 69. An unreadable REQUIREMENTS.md no longer makes every cited ID "Missing from REQUIREMENTS.md". `fs.existsSync` probes whose answer decides a verdict are replaced by `statEvidence`: api-coverage (the phases tree and the phase directory) and decision-coverage-plan (CONTEXT.md); the verify gate's redundant probe is dropped (its one read already yields none/unreadable). An EACCES is `unreadable`, never "not a GSD project" / "CONTEXT.md missing". `verify schema-drift`: the catch around the gate printed a non-blocking "exception:" payload and exited 0, so a gate that threw looked like "no drift"; it now declares UNAVAILABLE after printing the same payload, and an unexaminable phases directory is `unreadable` instead of "No phases directory". `tdd-red-evidence` parses the record through typed evidence instead of a catch that returned null. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…overage, decision-coverage) Real EISDIR fixtures where a real failure can be built; fs.statSync/readFileSync monkeypatched to throw EACCES (restored in finally, never chmod) where it cannot. Absent controls stay `none` (documented policy); the gap-analysis CLI case asserts exit 69 with the table on stdout and exit 0 for the control. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…int:ci scripts/lint-gate-evidence-drift.cjs parses the gate modules, check-auto-mode, gap-checker, decision-coverage-support and every gate-exit importer (router, phase, verify) with @typescript-eslint/parser and fails on: an empty or literal-only catch, any `readIfExists` reference, a gate verb entry owning its exit (process.exitCode / process.exit / numeric return / direct declareOutcome), a passing verdict in an `unreadable` arm, and a verb catch that prints a payload without declaring an exit. The allowlist is empty. Run over the pre-Phase-8 sources it flags the original census (the empty catches and readIfExists). Positive controls: one fixture per shape under tests/fixtures/gate-evidence-drift/ (stored as .cts.txt so no compiler or linter walks a deliberately violating file) plus a clean fixture; the test runs the guard against them, boundaries (0/1/2 catch statements), a seeded fast-check property, fail-closed cases, and publishes the census over the real tree as a function asserted zero. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… unexaminable phases directory Real CLI: no phases directory stays exit 0 (control); a file where the phases directory belongs throws inside the gate and exits 69 with the unchanged non-blocking payload; an EACCES on the phases stat (preload patch, never chmod) is `unreadable` with exit 69. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e is NO_INPUT verify plan-structure/phase-completeness/references/commits/key-links build real verdicts and exit 1 (negative), 66 (read, genuinely empty scope), 69 (could not look; an empty plan file is found '', not absent). schema-drift, codebase-drift, context-drift and prohibition-enforcement route through gate-exit in payload mode (the gate dispatch reads .block and routes a non-zero exit by onError). Duplicated CONTEXT.md-missing and schema-drift payload builders collapse; hand-built found literals use evidenceFound; gap-checker indentation fixed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
safe_resume_gate and the TDD gate fail closed on an unresolvable evaluation scope, completion reconciliation keeps the scope status, the autonomous UI gate and the decision-coverage and drift pre-checks no longer read exit 69 as "nothing there". Test helpers (fs failure injection, tool probe, CLI runner, doc bash spans) move to tests/helpers and the duplicated copies are removed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…stence collapses Entries come from the verify/phase/check routers, not a name list; new rules verb-no-gate-exit, verdict-owns-exit and exists-collapse (one named allowlist site, stale entries are problems), with positive-control fixtures and a census. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nts x 2 filters, its divergences are named; the io census explains why verify.cts left it The earlier claim of equality with real jq on 26 pairs was true when measured but only 14 cases were pinned. The test now pins all 13 documents against both filters with expected values measured from real jq, and pins the known divergences (empty stdin: jq exits 4, the stub 2; a string/array `.data` or a top-level array: jq errors with exit 5, the stub reads null; an object printed by -r: jq pretty-prints, the stub prints compact). The stub header lists them; none is reachable from the gate handler's JSON. tests/io.test.cjs: the stale "verify 8" breakdown is marked as measured then, and the census entry now states the real reason verify.cts left it. Read from src/verify.cts and src/gate-exit.cts: emitVerbVerdict prints the verdict payload through output() with a non-literal argument (so the literal-shape census no longer sees the sites), output() still records DEGRADED for a payload carrying an `error` key, and declareGateExit then overrides it for a non-PASS verdict (unreadable 69, empty 66, block in status mode 1) while a PASS leaves output()'s outcome untouched. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A misspelled method would inject nothing, and the test that relied on it would pass without exercising a failure. The Memtrace PR review flagged the unchecked fs[method] lookup. The other three review findings are not defects: labels[index] and FORMS[form] index collections with keys drawn from the same collection, and the setTimeout in the ui-auditor harness is a watchdog kill bound for a spawned child, cleared on close. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ut, so --test-force-exit no longer drops them (#4031) CI (docs-lint, plugin-validate) failed with "N tests registered, M reported" while the TAP summary itself said M, so the accounting was right and results really did not reach the report. Root cause, from node's v24 runner source and measured on a Linux node:24 container: the runner forwards --test-force-exit to every test-file child (getRunArgs); each forced child calls process.exit() the moment its root test ends, and its stdout, the pipe its V8-framed results travel on, is non-blocking on POSIX, so frames still queued in the Socket are discarded (nodejs/node#64833, fix proposed in nodejs/node#64875). It needs no concurrency: plugin-validate lost 23 of 54 in a single file. Measurement (Node 24.18.0, Linux, 20 files x 300 tests, concurrency 8, the runner's own events file and ledger, 4 runs each): count-only preload 5049-5074 reported of 6000 (ledger 6000: the child had emitted them all, so the loss is child to parent); preload that makes the child's stdout blocking 6000/6000 on every run; without force-exit 6000/6000. registration-ledger-preload.cjs now sets the child's stdout handle blocking at load (blockChildStdout, best-effort, test-file children only), the same operation the upstream fix performs at force-exit, but done up front so nothing is ever queued. --test-force-exit stays on every platform: it is the Windows post-test hang guard (#1051) and nothing about that changes, whereas dropping it on POSIX would trade a loud accounting failure for silent hangs on leaked handles. The accounting stays as the backstop for any other loss. Unit rows pin blockChildStdout; the behavioral proof is a real shard's own accounting. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s; acknowledge the code-reviewer agent growth
Read from the failed CI run of the previous head, each with its platform:
- ubuntu shards 1-3, windows 2/3: "the human_needed UAT precheck" and "the
phase-completion predicate" read PASSED=false for {"passed":true} at exit 0
(tests/verify-work-auto-transition.test.cjs:465). runBash defined `jq` as the
subset stub unconditionally, shadowing the REAL jq that GitHub's runners have,
and the stub knows only the two filters of the plan-phase block, so verify-work's
`jq -r '.passed // false'` failed and fell back to "false". It passed on the
bench because those rows skip there (no jq). The stub is now defined only when
`command -v jq` finds none.
- windows 2/3: every run-tests-accounting row that spawns the ndjson reporter died
with ERR_UNSUPPORTED_ESM_URL_SCHEME ("Received protocol 'd:'"): --test-reporter
takes an import specifier, and a bare Windows path is not one (run-tests.cjs itself
passes pathToFileURL for this reason). The tests do too.
- macOS: the ledger key is node's own realpath (/private/var/...) while the rows
expected path.resolve of the symlinked tmp (/var/...); they compare against the
realpath, as the runner's matching does.
- ubuntu 3/3, ubuntu, macOS: emitted-attribution against the CI base 2cc432e
names gsd-code-reviewer.md (+210) and its compact variant (+30): the exit-69
wording the resolver-driven file scope now carries. Acknowledged below; every
other grown emitted file already carries its trailer, and none is stale.
Emitted-Drift-Ack-Growth: gsd-code-reviewer.md — states that the evaluation-scope resolver exits 69 for status unresolvable while still printing its JSON, so an empty files list is read as could-not-look and fails closed instead of as a clean review
Emitted-Drift-Ack-Growth: gsd-code-reviewer.compact.md — the same exit-69 wording for the compact variant of the reviewer's scope resolution
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…w node:test's report frames The accounting still failed on CI after the blocking-stdout fix, on exactly two files: ci-pr-mergeability.test.cjs (81 registered, 38 reported) and ci-next-health.test.cjs (37 registered, 3 reported). The results were genuinely dropped child to parent, by the tests themselves, not over-counted: their callMain() does t.mock.method(process.stdout, 'write', ...) and t.mock.method(process.stderr, 'write', ...) to capture the CLI's output. In a test-file child node:test writes its V8-framed results as Buffers through process.stdout.write, looked up at write time, so while the mock is active the frames are captured into the test's array and never reach the parent: no error, exit 0. Measured on Node 24 with a real framed test-file child: 22 tests of which two mock process.stdout.write reported 11; the same shape with the fixed helper reported 22; 22 plain tests reported 22. tests/helpers/stdio-capture.cjs captureStringWrites() records the strings the code under test writes (the scripts write strings) and forwards everything else to the real write. Both files use it. A unit row pins it. Any other test that mocks the stdio writes the same way will now be named by the accounting, which is its job. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ro-result one macOS CI failed one row: `--test-only` expected the ledger file as /private/var/... and got /var/.... The ledger has two sources for a file name: node's own result events (`data.file`, already resolved through symlinks) and, for a child that reports no result, process.argv[1] as given. With the tmp dir symlinked (macOS /var -> /private/var) the two kinds of line disagreed about the same file. The preload now writes canonicalPath() (path.resolve + best-effort fs.realpathSync.native, falling back to the absolute spelling) on every line. analyzeChunkAccounting normalizes both sides regardless; a shim with a symlinked tmp dir confirms it matches both spellings. The accounting tests compare with fs.realpathSync.native throughout; the remaining path.resolve rows feed synthetic paths straight to analyzeChunkAccounting, which normalizes them itself. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…latest next Upstream added the Test-Report Adapter Module entry next to this branch's Gate Evidence and Gate Exit entries in CONTEXT.md; both generated indexes are rebuilt from the merged glossary (301 predicates). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
trek-e
force-pushed
the
chore/5170-gate-evidence
branch
from
October 3, 2026 15:50
3141c26 to
71e6bbd
Compare
This was referenced Oct 3, 2026
drakeo338
added a commit
to drakeo338/gsd-core
that referenced
this pull request
Oct 3, 2026
…hat-d Resolve the decision-coverage gate conflicts against open-gsd#5171's typed gate evidence and exit-status model, keeping open-gsd#4939's caller errors for a phase dir that names no directory (null totals, ENOENT/ENOTDIR -> not found). Regenerate compact-content-benchmark-baseline.json with the script.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 3, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 5, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 7, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 9, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 9, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 11, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 16, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 18, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 21, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 22, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski
added a commit
to davesienkowski/gsd-core
that referenced
this pull request
Oct 23, 2026
…and make the receipt helpers locale-proof Review follow-up on open-gsd#5192. - Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99 (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets; the rule that the routing marker comes from gsd_return_marker over {receipt} moves into the helpers doc that 7.99 loads. plan-phase.md is 98,273 LF bytes (cap < 98,300). - gsd_receipt_path and gsd_return_marker spell out their ASCII word characters instead of bracket ranges, so no locale (bash 3.2 has no globasciiranges) can widen them. - gsd_return_marker drops its trailing-CR strip: a CR after the marker is already a non-word character, so the strip changed no result (a mutation of it survived the new property test). - The compact-content benchmark baseline is regenerated against the rebased plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix PR
Linked Issue
Closes #5170
Refs #5056 (ADR-5057 §4, third and fourth bullets — Phase 8). Evidence, all closed: Refs #4562, Refs #4541, Refs #4259, Refs #4686, Refs #4031, Refs #4176.
What was broken
A gate that could not read its evidence treated it as empty and passed: five empty
catchblocks and a tolerantreadIfExistsin the gate modules turned an unreadable plan, directory,COVERAGE.mdor ROADMAP into "nothing there". A failing gate verb printed its negative verdict in JSON and exited 0 (phase uat-passed,verify artifacts, and the otherverifyverbs), so a shell caller could not tell a failed check from a passed one.What this fix does
Adds
Evidence<T>(found/none/unreadable,src/gate-evidence.cts) and a verdict-driven exit (src/gate-exit.cts, declared throughcli-exit). Anunreadablegate verdict exits 69 (UNAVAILABLE) and can never be a passing outcome; a negative status-mode verdict exits 1; an empty scope exits 66. Thecheckverbs and the three drift verbs stay payload-mode: a delivered blocking verdict is still exit 0, because the workflow gate-dispatch contract treats non-zero as a command failure routed byonError. Delivered-verdict JSON is unchanged. Every shell caller of an affected verb is exit-status-aware underset -e.Census (AST-measured, re-run on the final tree): empty or pass-shaped
catchin gate hosts 5 → 0;readIfExistsreferences 4 → 0; gate verbs that choose their own exit status: all → 0; "could not look" verdicts that exit 0: 3 + 1 (evaluation-scope) → 0.scripts/lint-gate-evidence-drift.cjsfails CI on each shape, with positive-control fixtures and an allowlist of exactly one named entry.Also fixed here (evidence for the epic's per-phase criteria): #4562 (
verify schema-driftreadfiles_modifiedwith an inline-array-only regex; it now reads through the Frontmatter Module), #4541 (verify plan-structureflags! grepnegative gates), #4686 (the exit statuses above), #4031 (run-testsfails a chunk whose registered tests never reached the report), #4176 (gsd-ui-auditorscreenshot block follows redirects, probes the documented ports and reports failed captures). #4259 needed no change: its fix is already onnext(tests/helpers/shell-doc-scan.cjs,1f84f45ed5).Root cause
No owner of what a gate found when it looked: each gate swallowed read failures in its own way, and the exit status was whatever
output()left behind (always 0). ADR-5057 §4 assigns the owners.Testing
How I verified the fix
gsd-teston the exact pushed shad9e3d9b562: 50577 passed, 0 failed (linux-node24);npm run lint:ciandnpm run build:libexit 0. Three earlier runs failed and were root-caused and fixed in this branch (agent size caps, golden install trees,run-testsledger keying, jq-dependent shell tests,ui-interaction-capturefence).Regression test added?
! grepnegative-gate form #4541, child-process CLI tests for each changed exit status,tests/gate-evidence.test.cjs,tests/gate-exit.test.cjs(seeded fast-check property with a positive control),tests/gate-plan-scan-truncated.test.cjs,tests/lint-gate-evidence-drift.test.cjs(positive-control fixtures), shell-block tests that run every documented capture under realset -e.Platforms tested
Runtimes tested
Checklist
Closes #5170confirmed-buglabelgsd-test.changeset/fragment addedBreaking changes
Gate verbs that could not read their evidence, or that returned a negative status-mode verdict, now exit non-zero (69, 1 or 66) where they exited 0; their stdout JSON is unchanged. Shell callers outside this repository that ignore the exit status are unaffected; callers that rely on
set -eor$?now see the verdict. Declared payload deltas:verify schema-driftnow sees a block-sequencefiles_modified,verify plan-structureflags! grepnegative gates, an empty plan file exits 66 forverify artifactsandverify key-links, andverify commitsoutside a git work tree returns{error: 'Not a git repository'}with exit 69.