cd5b8643aee18e97994cc24d302f579d9feabbeb
4833 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cd5b8643ae |
fix(#2828): state sync reports correct total_phases on a flat unmilestoned roadmap (#2892)
* fix+test(#2828): total_phases uses roadmap count on flat unmilestoned roadmap The read-path disk-scan cache fell back to phaseDirs.length (1) when milestoneBounded was false, even though roadmapPhaseCount (6) was correct for a flat roadmap (no sibling milestones to conflate). Use roadmapPhaseCount as the floor when > 0, matching the write-path (cmdStateSync) which already did this. The milestoneBounded flag still flows to milestoneUnbounded for the percent-skip (#1761 guard preserved). Regression test asserts state-sync writes progress.total_phases:6 for a flat 6-phase roadmap + 1 phase dir. * chore(#2828): changeset fragment * test(#2828): add negative-space coverage (Math.max floor mutant + no-roadmap fallback) — review findings The 6-phase test alone couldn't kill a Math.max-dropping mutant (1<6). Add: a 3-phase-dir/2-roadmap-phase case proving Math.max(dirs,count) floor; a no-roadmap case proving phaseDirs.length fallback. * fix(#2828): refine — distinguish flat unmilestoned from milestoned-unbounded (preserve #1761) The first-pass fix (roadmapPhaseCount > 0 always) re-broke #1761: a milestoned- unbounded roadmap (asserted milestone not among existing version headings) conflated sibling milestones (8 = 4+4). Refine with a hasMilestoneSectioning discriminator: ^#{2,3}(?!Phase) detects non-Phase h2/h3 milestone section headings. A FLAT roadmap (only ### Phase headings + a # title) has none → safe to use roadmapPhaseCount; a SECTIONED-but-unbounded roadmap has them → fall back to phaseDirs.length (#1761). Verified both cases locally (flat→6, sectioned-unbounded→1). * test(#2828): remove two fragile negative-space tests (phase-dir scanner internals) The Math.max-floor and no-roadmap tests made assumptions about the phase-dir scanner's internals (which dirs count as 'realized') that didn't hold. The core regression test (6-phase flat → total_phases:6) plus the existing #1761 conflation tests (which the refined fix preserves) provide sufficient coverage. * chore(#2828): backfill changeset PR number 2892 * fix(#2828): replace ReDoS-prone regex in regression test with line-by-line parse CodeQL flagged the nested-quantifier regex (`(?:[ \t]+\w+:.+\r?\n?)*?`) in tests/issue-2828-flat-roadmap-total-phases.test.cjs as a high-severity catastrophic-backtracking risk. Rewrite the STATE.md progress.total_phases extraction as a ReDoS-safe line-by-line block walk. --------- Co-authored-by: Test <test@example.com> |
||
|
|
54cb4145bf |
fix(#2765): bump brace-expansion to patched 1.1.18/5.0.9 (high-severity devDep advisory) (#2888)
* fix(#2765): bump brace-expansion to patched 1.1.18/5.0.9 (high-severity devDep advisory) npm audit fix (non-breaking) bumps the lockfile: brace-expansion 1.1.15→1.1.18 (eslint-nested via minimatch@3.x) and 5.0.6→5.0.9 (stryker-nested). Both 1.1.18 and 5.0.9 were published 2026-07-30 as the patch backports for GHSA-3jxr-9vmj-r5cp / GHSA-mh99-v99m-4gvg (range <=5.0.7). No overrides needed (in-range bump), no major bumps, no --force. Production (npm audit --omit=dev) unaffected (devDep only). Add a structural test pinning the installed versions so the bump can't silently regress. * chore(#2765): changeset fragment * fix(#2765): correct changeset issue ref + parse patch version as number (review findings) - changeset cited #2762 (typo) — fix to (#2765). - test compared v.split('.')[2] as a string (false-pass for 1.1.9) — parse all segments as Number. * chore(#2765): backfill changeset PR number (2888) --------- Co-authored-by: Test <test@example.com> |
||
|
|
4bd6fb066b |
chore(#2880): close ADR-2143 deployment misses — table-regex fingerprint + state-document seam migration (#2889)
* chore(#2880): close ADR-2143 seam misses — widen table-regex fingerprint, migrate state-document onto the seam The no-adhoc-markdown-parsing rule matched only a negated class whose sole member was a pipe ([^|]), so the stricter and more common [^|\n] spelling evaded it entirely -- src/state-document.cts hand-rolled exactly that shape and linted clean. Widen the fingerprint to any negated class excluding a pipe, which is the ADR-2143 section 7 prohibition as written. With the rule fixed, state-document.cts goes red. Replace tableRowPattern with locateFieldRow: a line scan using the markdown-table seam's splitTableRow for cell semantics, returning the value cell's byte range, and splice that range instead of running a whole-document content.replace. An edit now physically cannot cross a row boundary (section 4). Behavior is frozen -- stateReplaceField has 79 dependents across 5 command processes. Characterization tests lock all 14 table-branch rows plus CRLF, extract round-trip and the withFallback caller shape; a fast-check property asserts every non-target line stays byte-identical. Refs #2880, epic #2143 * fix(#2880): address adversarial review — lone-CR rows, field-name padding, quadratic scan, over-broad fingerprint Isolated adversarial review found four defects in the first commit. 1. locateFieldRow split lines on \n only. JS treats a lone \r as a line terminator, so the regex it replaced matched rows separated by bare CR. "| Phase | 3 |\r| Other | 9 |" returned 3 before and null after. Now CR, LF and CRLF are all terminators, byte offsets unchanged. 2. The field name was normalised with trim().toLowerCase(). The old regex embedded it verbatim, so its whitespace had to be absorbed by the row's own padding -- and because the group is a literal-character match rather than a whitespace class, a tab-padded cell does not accept a space-padded name. Replaced with an offset-aligned search reproducing the original backtracking exactly. 3. The widened fingerprint regex had two unbounded [^\]]* around an optional and ran quadratically over every regex source in every linted file: 256000 chars took 23 seconds. Replaced with a single-pass scanner that never rescans; the same input is now ~1ms. 4. The fingerprint also matched non-table idioms such as [^\s|] and [^"|]. Narrowed to a class excluding the pipe plus only \n, \r or \t. Differential fuzz against origin/next: 20000 cases, 0 mismatches. Refs #2880, epic #2143 * test(#2880): drop wall-clock assertion from the ReDoS regression guard local/no-elapsed-assertion flagged the elapsed-time check, and CLAUDE.md bans timing assertions outright as flaky. The 256000-char input stays as the regression guard for the quadratic scan; correctness of the verdict is what is asserted. If the quadratic path returns, the test stops completing and surfaces as a suite timeout rather than a silent pass. Also adds the changeset fragment for #2880. Refs #2880 * fix(#2880): spec-correct case folding, property tests, naming Code-review findings. The field-name comparison used toLowerCase(). The regex it replaced used /i WITHOUT /u, and ECMAScript Canonicalize deliberately does not fold a non-ASCII character onto an ASCII one -- KELVIN SIGN U+212A matched ASCII K where the old code returned null. Replaced with spec-correct Canonicalize, including the multi-character uppercase case (eszett -> SS), which a naive uppercase comparison also gets wrong. Added the fast-check property tests CLAUDE.md requires for parsers: one for the negated-class scanner, one for the field-name fold semantics, each against an independent reference implementation. Both reference impls failed on first run against real bugs, so neither property is vacuous. Renamed p2/p3 to name the exactly-three-pipes invariant, and reduced a duplicated comment to a cross-reference. Differential fuzz vs origin/next: 20000 runs, 0 mismatches, with the harness proven to discriminate the KELVIN case. Refs #2880 * chore(#2880): backfill changeset PR number (#2889) * docs(#2890): correct the local ESLint plugin path in CONTEXT.md CONTEXT.md named the local AST-rule plugin directory as scripts/eslint-rules/, which does not exist. The real location is eslint-rules/ at the repo root -- what eslint.config.mjs actually imports -- and CONTEXT.md's own later entry already says so explicitly, so the file disagreed with itself. Found by a line-by-line audit of all 1036 lines against the live graph; this was the only confirmed inaccuracy. Closes #2890 --------- Co-authored-by: Test <test@example.com> |
||
|
|
7372d99a26 |
enhance(#2800): derive reviewer flag lists and gate reviewer lane docs across locales (#2882)
* chore(#2800): derive reviewer flag lists and gate reviewer lane docs across locales The reviewer lane roster was hand-enumerated across five documentation surfaces and three workflow files that had drifted apart: --kimi-code was missing from all four translated COMMANDS.md mirrors, --coderabbit from every workflow forwarding list, and --antigravity from FEATURES.md. Adds checkReviewerDocsParity, a second pure gate deliberately separate from checkReviewerLaneParity so a stale doc cannot make the runtime checker look red. Workflows now derive their flag lists from a new review-lane flags query instead of hand-enumerating them, which also retires the unanchored grep that matched --agy inside --antigravity. Documents the previously absent reviewer body and hostBehaviors field in the capability manifest reference. Closes #2800 Closes #2781 Closes #2272 * fix(#2800): key the docs parity table arm on first-cell position Review found the flag arm was file-scoped, so the forwarding row that lists every flag in its third cell satisfied it on its own. Deleting a lane's own reviewer-table row -- the #2781 regression this gate exists to prevent -- therefore passed undetected. Arm 4 keys on the FIRST table cell, which separates a lane row from the forwarding row structurally and in every locale. Regression test included. * fix(#2800): shape-filter the flags subcommand output All three consumers read review-lane flags through an unquoted command substitution so the output word-splits into loop items. Phase 2 admits third-party overlay lanes, so an overlay flag containing whitespace would inject a second loop item and one containing a glob would expand against the cwd. Emit only well-formed flags so neither reaches the shell. * fix(#2800): remove the regex length ceiling and count only prose mentions Review found two real defects in the docs parity gate. The never-throws contract was false: building a RegExp from a declared flag or section title throws SyntaxError past ~100k chars, and Phase 2 admits overlay lanes whose declared strings are untrusted in length. Every one of these matches is literal, so String.includes replaces the regex outright, which also deletes escapeLiteral and the llama.cpp escaping it existed for. Arm 1 was context-blind: a flag mentioned only inside a fenced example or a commented-out row counted as documented. Both are stripped before matching. Also advertises all 13 lane flags in the argument-hint and corrects a stale eleven-lane count in the slug grammar note. * test(#2800): repoint the convergence suite off deleted workflow text The derived flag loop deleted the literal per-flag grep lines four tests matched on. Two of those failed loudly. The behavioral and property tests failed SILENTLY instead: their end marker no longer resolved, so the parse block extracted empty and both passed vacuously, and the property test's gsd_run stub had a no-op default that hid it. All now share one extractor and execute the real deployed block through a gsd_run shim backed by the actual binary. The whitelist assertions become an anti-parity check: re-adding a hand-written flag list must fail. Also repairs two vacuous cases in the docs parity suite. The unreadable-doc test called its own mock rather than the reader, and the integration test bounded nothing, so a doc losing its marker would have been silently skipped and still passed green. * fix(#2800): run the derived flag loop after the launcher preamble The remote matrix caught a real runtime bug, not a test artifact. In autonomous.md and plan-review-convergence.md the launcher preamble that defines gsd_run lives in a separate, LATER bash fence than the derived loop. Each fence is its own shell, so gsd_run was undefined where the loop ran: the command substitution yielded nothing and zero reviewer flags would have been forwarded. Worse than the drift this epic fixes, and silent. The whole CONVERGENCE_ARGS construction moves as one unit, because the --max-cycles append sits between the loop and the preamble and would otherwise have run against an uninitialized variable and then been dropped by the relocated initializer. Also documents all 13 lane flags in help/modes/full.md, which the repo gates bidirectionally against each command's argument-hint. * test(#2800): repoint the two converge suites off deleted flag literals Both asserted workflow.includes('--codex') against the hand-enumerated list the derived loop removed. They now assert the derivation itself, keep --all and --text (convergence controls, still literal), and add an anti-parity guard so re-adding a hardcoded list fails. The lost pass-through proof is replaced with a real one: every flag the tests used to hardcode is asserted present in the actual roster emitted by the binary, which is the property the old assertion was protecting. * test(#2800): acknowledge the workflow byte growth from the derived flag loop * chore(#2800): backfill changeset pr number to 2882 * fix(#2800): strip HTML comments to a fixed point in the parity gate CodeQL js/incomplete-multi-character-sanitization (high) on PR #2882: the single-pass <!--...--> strip can leave a live <!-- behind, so a join-trick construction smuggles a commented-out row past the gate and it counts as documented. Not an injection risk here since nothing is rendered, but it is the exact false pass this helper exists to prevent. Strips to a fixed point, then treats any surviving opener as unterminated so the multi-line branch closes it on a later line. Terminates because every pass strictly shortens the string. * test(#2800): pin the comment-smuggling regression with a real reproducer The obvious fixture for this class does not reproduce it: <!--<!---->--> leaves a dangling --> rather than a live <!--, and is caught either way, so it would have passed with and without the fix. The join-trick construction (<!- + <!--DUMMY--> + -...-->), the <scr<script>ipt> shape, genuinely regresses on the single-pass strip and is what the test now uses. --------- Co-authored-by: Test <test@example.com> |
||
|
|
b7b5c3712c |
fix(#2762): chunked --reviews replans instead of no-op + outline resume marker written to file (#2887)
* test(#2762): chunked --reviews must replan, not no-op (outline marker + per-plan --reviews exception) * fix(#2762): chunked --reviews replans plans instead of skipping 100% + outline resume marker written to file Defect A: §8.5.1 outline resume-check greped for a marker the agent only RETURNED (never wrote to the file) → outline always re-ran (broke crash-resume). Fix: the outline agent writes ## OUTLINE COMPLETE into the file. Defect B: §8.5.2 per-plan resume-check skipped any plan with frontmatter, no --reviews exception → --reviews skipped 100% of plans (contradicted §6 'go straight to replanning'). Fix: gate the skip on --reviews being ABSENT. Crash-resume (non-reviews) still skips. Condensed adjacent §8.5 prose to keep plan-phase.md under the 94519B cap (net -33B). * chore(#2762): changeset fragment * chore(#2762): backfill changeset PR number (2887) --------- Co-authored-by: Test <test@example.com> |
||
|
|
3af1941948 |
fix(#2772): resolve four discuss-phase text inconsistencies (dead MAX_PASSES read, gate-prompts drift, circular auto_advance, answer_validation drift) (#2886)
* test(#2772): structural guards for the four discuss-phase text inconsistencies * fix(#2772): resolve four discuss-phase text inconsistencies 1. auto.md: remove the dead MAX_PASSES/max_discuss_passes config read (contradicted the mandated single-pass rule + wasted a shim invocation per auto run). 2. gate-prompts.md: context-handling options now match the actual check_existing flow (Update it | View it | Skip, not Overwrite|Append|Cancel); gray-area-option no longer mandates 'Let Claude decide' (contradicts discuss-phase.md's no-cop-out rule). 3. discuss-phase.md: auto_advance fallback ends the workflow instead of routing back to the already-run confirm_creation step (circular). 4. discuss-phase-assumptions.md: re-sync answer_validation to the parent canonical block (had drifted — lost the 'Other' empty-text branch). * chore(#2772): changeset fragment * fix+test(#2772): also fix the assumptions auto_advance circularity (review minor 1) + add positive test anchors (review minor 2) The sibling discuss-phase-assumptions.md had the identical auto_advance→confirm_creation circularity; fix it the same way (end the workflow). Add positive anchors to both auto_advance tests so a re-phrased regression can't slip past. File #2885 for the dead max_discuss_passes config still advertised in settings/registry/docs (review minor 3). * fix(#2772): keep discuss-phase.md under the 32000B #717 cap + ack assumptions growth The auto_advance fixes + the assumptions answer_validation re-sync grew both files past the emitted-attribution gate (and discuss-phase.md past the #717 32000B cap). Condense the auto_advance prose in both files (discuss-phase.md now net -11, under cap; auto.md already net -4650 from the MAX_PASSES shim removal). Add discuss-phase-assumptions.md to tests/emitted-drift-ack.json for its residual +220 (answer_validation re-sync + auto_advance fix). * chore(#2772): backfill changeset PR number (2886) --------- Co-authored-by: Test <test@example.com> |
||
|
|
dbb0a653be |
fix(#2771): advisor mode spawns registered gsd-advisor-researcher subagent instead of general-purpose (#2884)
* test(#2771): advisor mode must spawn gsd-advisor-researcher, not general-purpose * fix(#2771): spawn registered gsd-advisor-researcher subagent instead of general-purpose in advisor mode universal-anti-patterns rule 10 (injected into discuss-phase via <required_reading>) says NEVER use non-GSD agent types. The advisor mode spawned general-purpose and manually told the agent to read the def — but gsd-advisor-researcher IS registered, so spawning by type auto-loads it. Drop the manual-read prompt line (re-specifying the def is a drift risk) and use the registered type. * chore(#2771): changeset fragment (mentions follow-up #2883) * test(#2771): widen manual-read-line regex to deny phrasing variants (review minor) /read\s+@.*gsd-advisor-researcher\.md/i (case-insensitive, any 'read @' lead-in) so a drift variant like 'Read @' or 'Load @' can't sneak the manual-def-read back in. * chore(#2771): backfill changeset PR number (2884) --------- Co-authored-by: Test <test@example.com> |
||
|
|
185da024cb |
fix(#2770): decision-coverage gate fails closed on empty arg + workflow recomputes CONTEXT_PATH in-block (#2881)
* test(#2770): empty contextPath argument must fail closed, not green-skip the decision-coverage gate The handler conflated empty-arg (caller error) with file-missing (legitimate skip), returning passed:true/skipped on an empty argument. Add: empty arg → passed:false; real-path-to-absent-file → legitimate green skip preserved; omitted arg → fail closed. * fix(#2770): decision-coverage gate fails closed on empty arg + workflow recomputes CONTEXT_PATH in-block Handler (check-command-router.cts): split the guard — empty/missing contextPath argument is a caller error (fail closed, passed:false, mirrors #1365); a real path whose file genuinely does not exist keeps the legitimate green skip. Workflow (plan-phase.md): recompute CONTEXT_PATH inside the consuming Bash block (it was set in the step-1 init block, which does not survive into the separately- spawned gate block — so the gate ran with an empty arg and silently green-skipped). * chore(#2770): changeset fragment * fix(#2770): guard workflow empty-glob case (review blocker) + update drift-guard test The handler now fails closed on an empty contextPath arg, so the workflow's unguarded glob (empty when a phase genuinely has no CONTEXT.md) would invoke the gate with an empty arg → passed:false → exit 1, hard-halting the legitimate 'Continue without context' plan-phase path. Guard the empty-glob case: only run the gate when a CONTEXT.md actually exists. Update the F1 drift-guard test (which gave false coverage — it only checked for the ${CONTEXT_PATH} token) to assert the in-block recompute AND the empty-glob guard. * fix(#2770): keep plan-phase.md under ADR-857 size cap + ack emitted drift + fix drift-guard window The workflow fix grew plan-phase.md past the ADR-857 phase-6 size cap (94519B) and triggered emitted-attribution. Condense adjacent §13a prose/JSON to offset (net +89B, under cap). Add tests/emitted-drift-ack.json acknowledging the residual growth. Widen the drift-guard test window (the gate invocation is now nested in the empty-glob guard, so the old 400-char window missed the glob recompute). * chore(#2770): backfill changeset PR number (2881) --------- Co-authored-by: Test <test@example.com> |
||
|
|
7e9ce08e2c |
test(#2736): property-test parsePhaseFromProse name precedence (#2878)
#2821 reworked parsePhaseFromProse's NAME precedence (dash-vs-paren choice, status-keyword tails, `Milestone:` tails, paren-stripped separator search, the lone-ALL-CAPS-tail rule) but shipped it pinned only by hand-picked examples. The two pre-existing property blocks cover phase-token ANCHORING (#2111) and "N of M" phase extraction; neither touches name precedence. This adds nine properties over the canonical parser. P1 and P9 are the delta guards: both fail against the pre-#2821 paren-first parser (proved with a standalone mutation harness), because each requires a genuine em-dash name to win over a co-present parenthetical. P9 additionally exercises the paren-stripped separator search, since the losing parenthetical itself contains an em-dash. P2-P4 are characterization tests for precedence contracts both parser versions satisfy; P5-P8 pin totality and phase-token extraction, which #2821 left alone. The section comment states which is which so a future reader does not mistake the characterization tests for delta guards. The generator's status-word exclusion filter is a test-local mirror of the private, unexported STATUSY_TAIL_RE. A divergence-guard test pins that mirror to observable parser behavior (not source text, which the no-source-grep rule forbids), so an implementation vocabulary change fails loudly instead of silently weakening every property that depends on the filter. Also clears six pre-existing no-unused-vars lint warnings surfaced by the lint run for this change (dead bindings in four unrelated test files, deleted rather than underscore-renamed); removing the never-called openCodeBlock helper cascaded to its now-unused fs/path/reviewPath consts in both fold scopes. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7cbfb2819f |
fix(#2764): extend no-path-literal-in-assert to membership/substring checks (catches the #2728 Windows CI shape) (#2879)
* test(#2764): no-path-literal-in-assert must flag membership/substring checks over path-returners The equality-only visitor missed .includes/.indexOf/.startsWith/.endsWith over a path-returning receiver (directly or via a .map() hop) — these pass lint and fail on Windows. Add RuleTester invalid cases for each shape + valid cases for the suppressions (POSIX-normalized receiver, non-path receiver, no-slash arg). * fix(#2764): extend no-path-literal-in-assert to membership/substring checks over path-returners Add a third CallExpression shape: .includes/.indexOf/.startsWith/.endsWith/.match over a receiver that traces to a path-returning call (directly, or through ONE .map(f => path-…) hop — the #2728 shape that passed lint and failed Windows CI). Reuse isPathReturningCall/isPosixSlashStringLiteral/isPosixNormalizerCall; respect the normalizer + Windows-excluded suppressions. The rule is scoped to tests/**/*.test.cjs. * chore(#2764): changeset fragment * test+docs(#2764): add .match test coverage, Windows-excluded membership case, fix doc accuracy (review findings) - MAJOR: .match was supported but untested — add invalid (.match with slash) + valid (.match no-slash) RuleTester cases. - MINOR: Windows-excluded membership case was claimed by the matrix but absent — add a platform-guard valid case in membership form. - MINOR: .map() hop comment said 'ONE' but recursion allows nesting — reword to 'recursively'. - Add known-boundary (d) note for the membership/.match shape. - Fix a premature comment-close from a literal */ in the glob example. * chore(#2764): backfill changeset PR number (2879) --------- Co-authored-by: Test <test@example.com> |
||
|
|
ef7a71fc09 |
fix(#2754): make parseStateMd CRLF-safe — frontmatter no longer drops on Windows-authored STATE.md (#2865)
* test(#2754): parseStateMd must parse CRLF STATE.md identically to LF The frontmatter fence regex and downstream splits used literal \n, so a CRLF STATE.md dropped the ENTIRE frontmatter block. Assert CRLF/LF parity (the production contract) across full frontmatter, next_phases flow + block forms, the progress nested block, and null handling. * fix(#2754): make parseStateMd CRLF-safe — frontmatter fence + splits use \r?\n The fence regex, scalar-line split, next_phases block-list regex, and progress block regex all used literal \n, so a CRLF (Windows-authored) STATE.md dropped the ENTIRE frontmatter block — every statusline field was silently absent. Use \r?\n throughout, mirroring the CRLF-safe extractFrontmatter in src/frontmatter.cts. LF behavior unchanged. * chore(#2754): changeset fragment * test(#2754): pin parseStateMd↔extractFrontmatter parity (Generative-Fix Divergence guard) The statusline parseStateMd and the canonical extractFrontmatter both derive GSD state from STATE.md frontmatter and diverged once already (the CRLF bug this PR fixes). Add a cross-parser parity assertion (CLAUDE.md parallel-surfaces rule) over the overlapping fields, under both LF and CRLF, so a future divergence on a scalar shape or line ending is caught here. (isolated-review minor finding) * chore(#2754): backfill changeset PR number (2865) --------- Co-authored-by: Test <test@example.com> |
||
|
|
82ca13f5a5 |
fix(#2736): write current_phase_name from the transition intent, not the lossy prose round-trip (#2821)
* fix(#2736): intent-first current_phase_name on transitions; dash-first prose precedence Primary: completePhase (adapter) and beginPhase (via readModifyWriteStateMd options) pass the intent-held display name to syncStateFrontmatter as an authoritative override, applied after every derive/preserve/carry-forward step — so the lossy prose round-trip can never destroy a name the transition just resolved. Names containing a parenthetical (`Closer-ruling measurement (D1a)`) now land in frontmatter verbatim instead of collapsing to the parenthetical (`D1a`). Secondary (#1695 AC #3 residual): parsePhaseFromProse prefers the em-dash name when it is a genuine name (not a status keyword, not a `Milestone:` tail), else falls back to the parenthetical — satisfying both first-party writer shapes (`N — Name (aside)` and `N (Name) — EXECUTING`). Still lossy for paren-containing names, which is why the intent-first override is the primary fix. plannedPhase carries no name in its intent, so it is naturally out of scope. Fixes #2736 * docs(changeset): backfill PR number for #2736 fragment * fix(#2736): drop an unnecessary type assertion on result.data StateTransitionResult.data is already `Record<string, unknown> | undefined`, so the cast was a no-op and tripped @typescript-eslint/no-unnecessary-type- assertion (CI lint-tests red on the first push; every test lane was green). --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
3f6b063fbb |
chore(#2799): invoke_reviewers and write_reviews iterate declared lanes (#2861)
* chore(#2799): resolve reviewer lanes into executable invocation plans Phase 5b of ADR-2782. Adds the resolver and runner that let invoke_reviewers iterate declared lanes instead of hand-authored per-CLI bash. Five additive descriptor amendments, each forced by a lane that ships today: - LaneHandler gains 'opencode' — the lane rebuilds its review from assistant text parts of a --format json stream; a plain stdout copy re-breaks #1936. - modelConfigKey — antigravity's key is review.models.agy, not .antigravity, so resolving by slug silently dropped a configured model. - defaultHost/fallbackModel — Phase 4 federated every *_host with a default of empty string; the real fallback only existed in the bash. - args becomes an argv template with a closed four-placeholder vocabulary. Positional splicing produced 'codex --model M -o F exec --ephemeral', which is not a valid invocation: codex injects in the middle, twice. - kimi-code lane, with the bounded command-capability probe (needle --output-format) that tells Kimi Code from the legacy python kimi-cli. Parity gate re-pointed: the workflow-text families it scanned are the text this phase deletes, so they are replaced by descriptor-to-registry parity plus an anti-parity check that no bespoke leg returns. jq, curl and external timeout/gtimeout all drop out of the review path. Refs #2782 * chore(#2799): add review-lane query surface and widen the manifest vocabulary Adds the gsd-tools 'review-lane' route (plan/invoke/sections) the workflow loops over, projects all twelve lanes into their capability manifests, and widens capability-validator for the amendments. opencode admitted to VALID_LANE_HANDLERS under the second arm of the enum's own admission rule: one lane, justified by a documented upstream defect data cannot express (#1936 — the agent can end its turn with zero output tokens and --format default then drops the assistant text entirely). Two bugs caught by an end-to-end stub run and fixed here: - loadConfigResolved returns a provenance wrapper, not the config; using it directly resolved every key to undefined, which reads as 'nothing configured' and silently dropped every model override. - hasBinary used shell:true with an args array (Node 26 DEP0190). Replaced with a PATH scan that spawns nothing at all. Refs #2782 * chore(#2799): iterate declared lanes in invoke_reviewers and write_reviews Replaces the eleven hand-authored per-CLI bash legs with a loop over resolved lanes, and renders REVIEWS.md sections from each lane's declared reviewsSection instead of thirteen hardcoded headings. review.md drops from 1104 lines to 507 (61KB to 28.7KB). Parity gate re-pointed, as agreed: the leg-marker and section-heading families scanned exactly the text this phase deletes, so they are replaced by descriptor-to-registry parity in both directions, plus an anti-parity check that fires if a bespoke leg is ever re-added. Enum, emitting sites and the Object.keys lock moved together. The budget-trim helper is hoisted out of the Ollama leg: it was always lane-agnostic, and any lane may now declare a promptBudgetKey. Refs #2782 * feat(#2799): bind the consented egress host and re-verify it at invocation Completes ADR-2782 D5. Rule 1 was recorded in the ADR as delivered by Phase 3 but was not implemented: ConsentRecord had no host field and nothing in the tree bound one, so this phase's rule-4 comparison had no baseline. ConsentRecord gains an OPTIONAL reviewerHost. Optional is the whole design: isValidConsentRecord does not require it, so every record already on disk stays valid and no re-consent storm fires (D4 rule 5). It is deliberately excluded from disclosureSignature — the loader has no config resolver, so folding a config-derived value in would make loader and lifecycle compute different signatures for the same manifest and re-prompt forever. Install resolves hostConfigKey (falling back to the lane's declared defaultHost, which is what the invocation path uses) and records it. Invocation re-resolves and blocks on mismatch rather than silently redirecting. Absence allows: no record, or a record predating the field, means nothing to compare — denying there would break every existing local-model user on upgrade. Refs #2782 * test(#2799): cover the resolver, runner and handlers; retarget the parity suites Adds the golden invocation-plan table (one row per shipped lane, derived from the bash legs rather than the descriptor types) plus runner coverage for the probe, empty-output policy, the three handlers and the egress check. Retargets the existing suites onto the new contract: descriptor-to-registry parity, the anti-parity check, the opencode handler, and the twelfth lane. Two corrections found by running them: - modelConfigKey was required; that breaks D4 rule 2, since a reviewer manifest authored before this phase would fail validation on upgrade. It is optional, read as null when absent. - the antigravity non-zero-exit test pre-seeded the transcript, which asserted that a STALE entry leaks through — the exact bug the watermark prevents. The spawn now appends, as the real tool does. Refs #2782 * fix(#2799): restore agy --add-dir and the self-report prompt in the handler Retargeting the three legacy reviewer suites off the deleted bash surfaced two real regressions in the port, both #2176: - --add-dir was dropped. Without it agy's permission context never receives the cwd repo, so the agent anchors on its own scratch dir and reviews the plan text in isolation — the exact failure the Review Instructions forbid. It is capability-probed, because an older agy rejects the unknown flag outright and a lane that fails to start is worse than one running on the prompt anchor. - the prompt lost the clause mandating a REVIEWED-WITHOUT-REPO-ACCESS self-report, which is what makes a blind review distinguishable from a grounded one. antigravity now builds its own prompt variant. Also ports the #2073 mode-2 cli.log diagnostic, which was dropped: a pinned model that 404s exits 0 with empty stdout AND an empty transcript, so agy's own log is the only evidence that anything failed. The three suites now assert against the plan and the handler instead of matching fence text, so they no longer need allow-test-rule exemptions. Refs #2782 * docs(#2799): document the declared lanes, the new flag, and dropped prerequisites COMMANDS.md gains --kimi-code and replaces the jq-prerequisite paragraph, which is now false: no lane requires jq, curl or an external timeout. Adds the changed-egress-destination behavior, since a blocked lane is something a user can hit. CONFIGURATION.md records that the model config key is declared per lane rather than derived from the flag — antigravity's is review.models.agy — and adds review.models.kimi-code. reviewer-instances.md now routes an instance through its lane's single invocation seam instead of a copied per-adapter bash block, which is what lets a cross-cutting fix reach instances for free. That required implementing the --model/--agent/--as flags it documents; --model re-resolves through the lane's argv template rather than splicing, so the flag lands where the lane declares it rather than ahead of a subcommand. CONTEXT.md glossary gains both new modules. Refs #2782 * chore(#2799): drop the stale emitted-drift acknowledgment The only entry was #2797's, acknowledging COMMENT-ONLY GROWTH in review.md. That file now shrinks by ~32KB and every emitted hash that moved is attributable to this diff, so the ack no longer explains anything. Removing the last entry means removing the file: its presence is the alarm, and an empty one signals nothing. Verified by deleting it and re-running the attribution and provenance gates plus lint:ci — all green without it. Refs #2782 * docs(#2799): record the Phase 5b vocabulary widenings in ADR-2782 Five additive amendments, each forced by a lane that ships today, plus two corrections the phase had to make rather than work around: - D5 rule 1 was recorded as delivered by Phase 3 and was not implemented, so this phase's rule-4 comparison had no baseline. Recorded because an ADR asserting a rule was delivered is exactly what stops a later phase checking. - The DEFECT.GENERATIVE-FIX gate is re-pointed: its workflow-text families scanned the text this phase deletes. Also records that D7's 'skip the probe where no bounding mechanism exists' carve-out is obsolete — in practice it meant the Antigravity lane ran unbounded on every stock macOS host, which ships neither timeout nor gtimeout. Refs #2782 * fix(#2799): close four defects found by adversarial review Two confirmed bugs, both reproduced before fixing: - resolveLanePlan was not total. An openai-http lane with a missing or non-object invoke dereferenced inv.hostConfigKey and threw, contradicting the module's own documented contract; the spawn branch guarded correctly and the http branch did not. The CLI seam resolves every selected lane in one map, so one malformed overlay manifest would have aborted the whole review rather than dropping its own lane. Guarded, plus a per-lane try/catch at the seam so a throw can never take down siblings. - A reviewer-instance model was silently dropped for any lane declaring modelConfigKey null (cursor, qwen, coderabbit). reviewer_instances validates that cli is a known slug but never that the slug accepts a model, so a user could configure one, get a clean run, and never learn a different model reviewed their plan. Now warns explicitly. Two hardening fixes: - The slug is concatenated into artifact paths, so LANE_SLUG_RE is enforced in the resolver rather than inherited from a validator that does not run on this path — the module documents itself as the overlay-manifest trust boundary, so it should not depend on someone else having checked. - normalizeHost mangled a scheme-less value: new URL('localhost:11434') parses with an empty hostname, so it became 'localhost://11434' and was compared and requested as if real. An empty hostname now means not-a-URL. Also documents the one gap that cannot be closed here: the antigravity watermark is keyed by workspace, so two concurrent reviews of the same repo share a transcript. agy exposes no per-invocation id to filter on, so the handler now states which half of its never-stale guarantee actually holds. Refs #2782 * test(#2799): retarget the remaining eight review.md-asserting suites The remote runner found 37 failures the local sweep missed (it hit the shell's two-minute cap before reaching these). All eight extract per-CLI bash from review.md that this phase deletes; each protects a real invariant, so each is retargeted onto the plan, the runner or the handler rather than removed. Three real defects surfaced by doing so: - effort args never reached ANY lane. model-resolver.cjs exports no resolveExecution, so effortFor silently returned [] every time. Restored by calling the same bounded resolve-execution query the bash legs used — and NOT with --raw, which prints the resolved effort rather than the picked field, so claude got 'low' instead of '--effort low'. - the timeout guidance lost 'a silent empty output is a timeout kill, not a crash' — the operator note that exists because of the Codex 0xc0000142 misdiagnosis. Restored. - the opencode handler dropped EMPTY assistant text parts. The shipped jq was , and only substitutes for false/null — an empty string is truthy in jq and contributed a blank line. Found by a property test shrinking to ['', '']. The opencode property suite no longer spawns jq at all, which deletes the #2099 hang mechanism it was architected around rather than mitigating it. Refs #2782 * fix(#2799): register the two new generated modules, and untrack them The remote runner caught build output committed to git. Both new modules compile from src/*.cts into gsd-core/bin/lib/*.cjs, and every sibling generated that way is gitignored and eslint-ignored (ADR-457) - including Phase 1's own review-lane-descriptor.cjs. Mine were neither, so repo-invariants' "each bin/lib/*.cjs is linted xor ignored according to migration state" failed. Registered both in .gitignore and eslint.config.mjs alongside the Phase 1 module, and dropped them from the index. Nothing about the shipped behaviour changes; the artifacts are rebuilt by build:lib. This is the new-.cts-module registration ripple, and it is the one part of it I had not completed - the CONTEXT.md glossary and the inventory manifest were already done. Refs #2782 * chore(#2799): backfill changeset pr number to 2861 * chore(#2799): backfill changeset pr number to 2861 --------- Co-authored-by: Test <test@example.com> |
||
|
|
97e9fd50bd |
fix(#2752): make tool_input.path authoritative over model-controlled file_path in gsd-phase-boundary.sh (#2860)
* test(#2752): path is authoritative over model-controlled file_path in phase-boundary hook Rewrite the #2304 precedence test (which pinned the buggy file_path-wins behavior with an impossible no-tool_name {file_path,path} payload) to assert the correct precedence with realistic Kimi-shaped payloads: a real .planning/ write with a decoy file_path must still fire the reminder (suppression repro), and a write elsewhere with a decoy .planning/ file_path must NOT fabricate one (fabrication repro). Update the parity vocabulary alarm to the new expression. * fix(#2752): make tool_input.path authoritative over model-controlled file_path in gsd-phase-boundary.sh The hook consulted file_path first, path second. kimi-cli executes on path and sends path only; file_path on a Kimi payload is always model-supplied. So a model-supplied decoy file_path could suppress the reminder for a real .planning/ write or fabricate one for a file never touched. Flip the precedence so path is authoritative and file_path is the fallback (Claude Code emits file_path and no path, so the fallback must remain). Mirrors the #2595 JS-guard fix. * chore(#2752): changeset fragment * chore(#2752): clarify comment/changeset — JS guards use upstream normalization, shell hook applies precedence directly (review minor 1) * chore(#2752): backfill changeset PR number (2860) --------- Co-authored-by: Test <test@example.com> |
||
|
|
aa19e3478c |
fix(#2854): pin the emitted gate to the base the tree was merged with (#2859)
* test(#2854): failing-first coverage for CI baseline export provenance Extracts the export decision out of main() behind injected IO so it is unit-testable, preserving today's export-whenever-present semantics, and adds the matrix that proves those semantics are wrong. The PR lane restores the emitted baseline keyed on the PR's recorded base sha while the gate resolves the base ref live, so the two drift whenever next advances mid-flight. The restore was published straight to GSD_EMITTED_BASELINE, where a mismatch is fatal, turning a recoverable cache into a hard failure on diffs that touched nothing related. Also renames the stale-env fixture from 'from-cache-restore.json' to an operator-pin name: that fixture asserted the exact conflation this bug is, documenting the defect as intended behavior. Refs #2854 * fix(#2854): validate a restored baseline before publishing it as an operator pin GSD_EMITTED_BASELINE is an operator pin: resolveBaseline() treats a mismatch there as a hard stop, because the operator said "use this one". CI published its cache restore to that same variable whenever the file merely existed, so a restore keyed on the PR's recorded base sha - while the gate resolves the base ref live - turned a recoverable cache into a fatal error whenever next advanced mid-flight. Required tests went red on diffs that touched nothing related, and named a test file the contributor never opened. The export step is the boundary, so it is the boundary that validates. It now publishes only a baseline already valid for the sha under test, judged by validateBaseline so the staleness rule keeps one definition. Anything refused is left to be found via the cache path, where a mismatch degrades to the in-job build exactly as ADR-2719 SS5 specifies. The operator hard stop is untouched, and the fast path still hits on a current cache. Also reports the sources actually reached rather than asserting all three ran: the failure message claimed an in-job build it had returned before calling, sending contributors after a rebuild that never happened. Fixes #2854 * fix(#2854): pin the emitted gate to the base the tree was actually merged with The differential compared a tree built on one commit against a baseline at a different one. "Rebase check" merges pull_request.base.sha, pinned by #2472 so all 12 matrix jobs agree on one tree, but resolveBase() fell through to origin/next, which fetch-depth 0 leaves at the live tip. Nothing set GSD_EMITTED_BASE, so whenever next advanced mid-flight the two disagreed. The cached baseline, keyed on base.sha, was correct for that tree and was rejected as STALE by a target that was not. Required tests went red on diffs that touched nothing related, naming a test file the contributor never opened. The near miss is the worse half: had resolution gotten past the baseline step, a baseline at the live tip would have attributed commits merged to next in between to the PR under test. The hard stop was shielding us from a wrong answer, so making it fall through would have made this worse. Pins GSD_EMITTED_BASE to the same expression as CI_REBASE_BASE_SHA in every rebase-merged job, with a parity test asserting the two cannot diverge. That parity check immediately caught a third lane, test-inert, that merges a pinned base and had been missed. Fixes #2854 * fix(#2854): keep the export step self-contained across the package boundary scripts/ ships in the npm tarball and tests/ does not, so requiring the validator across that boundary is MODULE_NOT_FOUND in a published install. The export step now reads the same GSD_EMITTED_BASE pin the gate resolves through, and applies a cheap self-contained precondition; validateBaseline remains the sole authority and still runs downstream on whatever is published, so there is no second opinion to drift. Reading the pin rather than re-deriving a base is the point: a second, divergent base lookup is exactly what caused this bug. The same hazard pre-exists in scripts/gen-emitted-baseline.cjs, which ships and requires three tests/ modules. Filed as #2858 rather than folded in: fixing it means relocating the shared helpers out of tests/ and updating every consumer, which would bury this change. Refs #2854 * fix(#2854): stop announcing the restored cache through the operator-pin door Two independent reviewers found the same blocker in the previous approach. Validating before publishing to GSD_EMITTED_BASELINE only narrowed the hole: the precondition gated on sha equality alone, so a document with a MATCHING sha but a wrong schema version or malformed manifests was still announced as an operator pin and still hard-stopped downstream. That reproduces this bug's own class, triggered by malformation instead of staleness. The step was never load-bearing. The cache restores to .gsd-cache/emitted-baseline.json, which is resolveBaseline's DEFAULT_CACHE_PATH and is read whether or not anything announces it. Publishing the same file to the pin door could only ever convert recoverable into fatal, so the step and its script are deleted rather than made cleverer. Every failure mode now degrades to the in-job build by construction, and validateBaseline is once again the only thing that judges a baseline. Coverage moves to where the behaviour lives: stale sha, wrong schema version, manifests array/absent, non-object documents, unreadable file, and the 39/40/41 hex boundary all assert degradation via the cache path. Adds the empty-pin case a reviewer flagged as untested - the pin is job-level env, so on push events it renders as an empty string, and only baseRefCandidates' truthy check keeps it out of the candidate list. Fixes #2854 --------- Co-authored-by: Test <test@example.com> |
||
|
|
b36e3b7e1f |
fix(#2751): normalize bare gsd-tools command-position calls to gsd_run in shipped source (#2851)
* test(#2751): regression guard — no command-position bare gsd-tools calls Agents/workflows instructed bare `gsd-tools <verb>` invocations that fail with 'command not found' on a shim-only install (#725 fixed only the Codex conversion pipeline; the Claude-facing source shipped them verbatim). Adds a source-text guard (allow-test-rule: source-text-is-the-product) scanning agents/*.md + gsd-core/workflows/*.md for the operative shape `gsd-tools <verb> <arg>`, excluding command -v probes / resolver definitions, with a documented PROSE_ALLOWLIST for descriptive mentions that name the command without instructing literal invocation. A stale-allowlist check ensures entries stay real. RED first; source fix lands next commit. * fix(#2751): normalize bare gsd-tools calls to gsd_run in Claude-facing source The 12 command-position bare `gsd-tools <verb>` instructions across agents/ and gsd-core/workflows/ failed with 'command not found' on a shim-only install (no gsd-tools binary on PATH). #725 fixed this only for the Codex install-conversion pipeline; the Claude-facing SOURCE shipped the bare calls verbatim, and new ones kept accumulating (new-project.md:114 landed 12 days AFTER #725 closed). Rewrite each operative site to the portable `gsd_run` resolver that the same files already define (3-18x each) — a pure command-position token swap preserving all arguments, flags, --files, and surrounding prose. Every runtime now benefits from one source change instead of each needing its own converter. Touched sites (12): gsd-intel-updater (validate/snapshot/extract-exports), gsd-code-fixer (query commit), gsd-planner (learnings.query), gsd-project- researcher (websearch/research-plan/classify-confidence), gsd-phase-researcher (websearch/research-plan/classify-confidence), new-project (project-instruction- file / commit --files), new-milestone (commit --files). Preserves command -v gsd-tools probes, resolver-snippet definitions, and the 4 descriptive prose mentions that NAME the command without instructing invocation. RED @ 1bb12ba1. * chore(#2751): allow-test-rule issue ref + changeset fragment Add the (#2751) tracking ref to the source-text-is-the-product annotation per ADR-456, and the .changeset Fixed fragment (pr:0, backfilled post-PR). * fix(#2751): convert remaining command-position bare gsd-tools calls (verify-summary, windows, worktree, smart-entry, quick-tasks-append) Isolated adversarial review (Step 4) found the first pass missed genuine command-position bare calls because the regression test's hand-maintained 6-verb list silently false-passed verify-summary (the 'verify' branch matched the prefix then died on the hyphen) and omitted windows/worktree/smart-entry/ quick-tasks-append entirely. Convert these 8 additional operative sites across new-project.md, new-milestone.md, ship.md, execute-phase.md, progress.md, smart-entry.md, quick.md. * chore(#2751): backfill changeset PR number (2851) * fix(#2751): normalize allowlist paths to forward slashes — Windows path-separator false-flag The PROSE_ALLOWLIST is keyed by file:line using forward-slash paths, but path.relative() returns backslash separators on Windows, so the allowlist lookup failed and the 6 descriptive mentions were flagged as offenders on the windows-latest CI lane. Normalize rel to forward slashes before the lookup so the allowlist matches identically on every OS. --------- Co-authored-by: Test <test@example.com> |
||
|
|
0408276791 |
chore(#2797): federate reviewer config keys off the central schema (#2841)
* chore(#2797): federate reviewer config keys off the central schema Phase 4 of epic #2782 (ADR-2782 D9, config half). Runs AFTER 5a per the ADR's swap amendment: a federated config slice lives inside a capabilities/<id>/capability.json, and three of the five key families had no capability directory until 5a created them. Four key families move to the lanes that use them; the central-schema removal and the federated addition land in this one commit because the exclusivity invariant fails the build on a key present in both. review.max_prompt_tokens, review.default_reviewers and review.reviewer_instances describe policy ACROSS lanes and stay central. Two things the issue did not name, both found while building it: 1. THE EXCLUSIVITY GATE WAS BLIND TO PATTERNS. It compared federated keys against manifest.validKeys only, and two of the four families (review.models.<slug>, review.max_prompt_tokens_per_reviewer.<slug>) were pattern-backed. That is not cosmetic: isCentralConfigKey consults those patterns and mergeFederatedConfig skips every key for which it returns true, so declaring a slice while the pattern survived would have shipped an INERT slice behind a green gate — the exact half-migrated shape the invariant exists to prevent. The gate now loads the patterns from the same manifest the runtime reads. 2. AN UNSET PER-LANE BUDGET NOW RESOLVES TO 0, NOT NOT-FOUND, because a federated key always resolves to its declared default. The three fallback guards in review.md checked only empty-or-"null", so a user who set the GLOBAL review.max_prompt_tokens would have silently lost trimming on the HTTP lanes. The guards now treat 0 as unset. D9 says review.models.<slug> is owned by "the lane whose slug it names". That is false for one lane: the shipped key is review.models.agy while the slug is antigravity. Ownership follows the lane; the key name is preserved, because renaming would break every config that sets it. Existing tests updated rather than left asserting the old world: config-get on a cleared federated key yields empty instead of not-found (what #2046 actually protects — never persisting the literal "null" — is unchanged and still asserted); the config-schema dynamic pattern representative moves to reviewer_instances; the prototype-pollution guard case moves to a surviving dynamic prefix so alert #26 keeps its coverage, with a new case asserting the old key is now rejected earlier; and Phase 2's harvest-widening inertness assertion becomes an ownership assertion, since Phase 4 is what consumes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2797): use a -1 sentinel so an explicit per-lane budget of 0 survives A federated config key always resolves to its declared default, so an unset per-lane prompt budget needed a value the workflow could treat as 'not configured'. The first cut used 0 — which is wrong: 0 is already a LEGITIMATE per-lane budget meaning 'do not trim this lane' (the early-return guard in prepare_trimmed_prompt_for_reviewer). Treating it as unset would have silently switched a user who deliberately disabled trimming for one lane onto the global budget. The sentinel is now -1, which is not a valid token budget, so all three states stay distinguishable: unset falls back to global, an explicit 0 disables trimming for that lane, and an explicit N is used. Locked by three CLI round-trip tests. Surfaced by the isolated security reviewer before it crashed mid-run; verified independently against the shipped trim guard rather than taken on trust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2797): update central-registration assertions and stay under the review.md cap The remote runner caught both; my local sweep missed the files. 1. tests/plan-review-convergence.test.cjs asserted the three local-server host keys are in VALID_CONFIG_KEYS. They are federated to their lane capabilities now, and the exclusivity invariant forbids a key living in both places. What #2306-local actually protects is that config-set ACCEPTS them, so that is what is asserted — via isValidConfigKey, the predicate config-set itself uses, which spans central and federated. A second assertion pins federated ownership, so a silent reversion back to the central schema fails too. 2. review.md exceeded the LARGE tier hard cap (62583 > 61440). That cap is a red line, not a budget to raise. The three per-lane budget guard comments were near-identical; condensed to one terse line each. 61371 bytes, 69 to spare. Real extraction to workflows/review/modes/ is Phase 5b/6 work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2797): fail closed on a broken config-schema manifest; reconcile stale docs Isolated security review findings. MAJOR — loadCentralConfigPatterns failed OPEN. It swallowed a JSON parse error and returned [], while its sibling loadCentralConfigKeys, reading the SAME file, writes to stderr and throws ExitError(1) on that identical failure class. Fail-open here defeats the gate this function exists to feed: with zero patterns, validateCrossCapability's pattern-collision check silently passes and an inert federated slice ships green. It was masked in the one production call site only because loadCentralConfigKeys runs first against the same path — a coincidence of ordering, not a guarantee, and this function is exported and called standalone. The two now share a contract: ENOENT is the legitimate absent case, anything else throws loudly. A single unparseable PATTERN is still skipped, which degrades to "checked less" rather than blocking every build. The branch had zero coverage; it now has two tests (malformed JSON, EISDIR). MINOR — docs/CONFIGURATION.md still listed review.models.qwen and review.models.cursor as settable, ~770 lines below this PR's own new Ownership section. Those lanes take no model flag, so they declare no model key and config-set now rejects them. Rows removed; the missing review.models.agy row added; the per-reviewer budget row corrected to name only the lanes that own a budget key, and to document that a per-lane 0 disables trimming for that lane. Also fixes a shadowed "raw" binding introduced by the fail-closed change, which made the generator unrequirable — caught immediately by its own --check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2797): backfill changeset pr number to 2841 * fix(#2452): make the base-ref mutation test hermetic against leaked GIT_* env tests/mutation-workflow-base-ref.test.cjs fails on PR branches while next stays green, and it is currently blocking at least three unrelated PRs (#2841, #2832, #2827) with: error: invalid object 100644 <sha> for 'base-N.txt' error: Error building trees The existing loop comment attributes this to `git add .` rehashing O(n^2) blobs "before the object write had landed" and works around it by staging one path per iteration. That is not the cause: sequential execFileSync calls cannot race each other's object writes, and the failure persisted after that change — it simply moved to a lower commit index. The cause is that the git() helper inherited the runner's environment. A leaked GIT_INDEX_FILE makes `git add` write into a DIFFERENT repository's index; GIT_OBJECT_DIRECTORY / GIT_ALTERNATE_OBJECT_DIRECTORIES send the blob to another object store; GIT_DIR / GIT_WORK_TREE redirect the whole operation. In every case `git commit` then cannot resolve a blob it just staged, which is precisely the error above. Verified by negative control: with GIT_DIR exported, this test fails on the unfixed helper (the git commands operate on the wrong repository entirely); with the helper stripping GIT_* it passes. The single-path staging is kept — it is genuinely less work — but it is no longer load bearing. Found while shipping #2797. Fixed in place rather than deferred: it is a defect surfaced during the work, and it is blocking other contributors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2452): build the base-advance commits empty, removing the lost-object class The base-ref guard has been failing in CI with: error: invalid object 100644 <sha> for 'base-N.txt' error: Error building trees It is currently red on at least three unrelated PRs (#2841, #2832, #2827) while next stays green. Two theories have now been tried and neither held. #1881 blamed `git add .` rehashing O(n^2) blobs and switched to staging one path per iteration; the failure moved from commit 32 to commit 25 and carried on. The preceding commit here made the git helper hermetic against leaked GIT_* environment — that IS a real vulnerability (with GIT_DIR exported the helper operates on the wrong repository entirely, proven by negative control) but it produces a different error than CI reports, so it is not demonstrably the cause either. Neither trigger reproduces off-CI, so this stops guessing at the trigger and removes the failure CLASS instead. The loop needs base-branch DEPTH and nothing else: no assertion reads these commits' contents, and base-side files cannot appear in `origin/base...HEAD` regardless. `--allow-empty` writes no blob and no tree, so there is no object for the index to reference and lose. It is also far less work than 60 write+hash+index cycles. The guard still proves its mechanism: the test asserts that a --depth=1 base fetch FAILS and a full fetch resolves, so a broken topology would surface immediately rather than passing vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Test <test@example.com> |
||
|
|
4f6935e29b |
fix(#2717): write CommonJS marker for cursor/windsurf/codex staged .js hooks (#2846)
* test(#2717): CommonJS marker for cursor/windsurf/codex staged .js hooks Cursor/windsurf (skipSharedHooksInstall) and codex (!isCodex gate) stage .js hook scripts via dedicated paths that bypass installSharedHooksBundle — the only writer of the {"type":"commonjs"} marker. Under a config root declaring {"type":"module"}, Node loaded those scripts as ESM and every require() failed with 'require is not defined', silently disabling the runtime's hooks. Adds regression tests (RED first, fix lands next commit): - parametrized cursor/windsurf/codex install asserts hooks/package.json exists with exactly GSD's marker content; - end-to-end: a cursor require()-using hook loads under a planted ESM-typed config root without the require-is-not-defined error; - the ensureCommonJsMarker / removeCommonJsMarkerIfGsdOwned contract: GSD markers are removed on uninstall, user-authored package.json is never touched. * fix(#2717): write CommonJS marker for cursor/windsurf/codex staged .js hooks The {"type":"commonjs"} marker lived only inside installSharedHooksBundle, which cursor/windsurf (skipSharedHooksInstall) and codex (!isCodex gate) never reach. Their .js hooks are staged by dedicated paths, so under a config root declaring {"type":"module"} Node loaded them as ESM and every require() failed with 'require is not defined', silently disabling those runtimes' hooks. Decouple the marker write into a shared helper so any code path that stages .js hooks can ensure it lands in the SAME directory as the scripts: - src/runtime-hooks-surface.cts: add ensureCommonJsMarker(dir) + removeCommonJsMarkerIfGsdOwned(dir) (byte-identical content to installSharedHooksBundle's marker; preserves a user-authored package.json on both write and uninstall). Call ensureCommonJsMarker(hooksDir) from writeCursorHooksJson + writeWindsurfHooksJson; call removeCommonJsMarkerIfGsdOwned on their matching remove paths. Export both. - bin/install.js: call hooksSurface.ensureCommonJsMarker after the codex hook copy; call hooksSurface.removeCommonJsMarkerIfGsdOwned in the generic hooks-removal loop (safe no-op where no marker exists). No change to which runtimes receive the shared bundle, the !isCodex gate, skipSharedHooksInstall, or kimi/kimi-code/cline/copilot/trae/zcode (all unchanged — audit in the diagnosis). RED @ dbb7d2bb (6 failures: 3 missing markers + the ESM require error + missing helpers); GREEN pending. * docs(#2717): changeset fragment (pr:0, backfilled post-PR) * chore(#2717): regen codex/cursor/windsurf install-tree fixtures + attribution ack The fix adds hooks/package.json to those three runtimes' install trees (the new CommonJS marker), so the golden install-tree fixtures gain one path each (regenerated via npm run gen:install-tree). emitted-attribution (ADR-2719) flags the 3 emitted hooks/package.json paths under the hooks-built rule; acknowledge them. Also drops 5 spent ack entries left by now-merged PRs (#2694 code-review.md/code-review-fix.md, #2695 worker/registry, #2794 review.md) — they are stale on this branch (base already carries them). * fix(#2717): codex ESM-root behavioral test + hooks-built provenance for package.json Two review-driven follow-ups on the #2717 fix: - Adversarial review noted the ESM-root behavioral test covered only cursor; refactor it into a helper and add a codex case (the !isCodex-gated path most likely to regress, whose marker write lives in bin/install.js). gsd-check-update.js require()s at module load, so it surfaces the ESM failure immediately. - emitted-provenance flagged hooks/package.json as 'attributed source does not exist' — the marker is code-derived (a fixed literal emitted by ensureCommonJsMarker at install time), not built from a tracked source. Route the hooks-built rule's sources/transforms for package.json to the surface source file, mirroring the existing .cmd-shim sub-family. * chore(#2717): drop now-redundant hooks/package.json attribution ack The hooks-built provenance routing (prior commit) now self-attributes the emitted hooks/package.json to src/runtime-hooks-surface.cts, which IS in this diff — so the attribution is self-explaining and the emitted-drift-ack entry became stale. Delete the (now-empty) ack file per ADR-2719's empty-file rule. * docs(changeset): backfill #2717 PR number to 2846 |
||
|
|
b12d4df03b |
fix(#2694): normalize CRLF before frontmatter-boundary match in code-review workflows (#2839)
* test(#2694): CRLF frontmatter boundary regression for code-review workflows The code-review / code-review-fix workflows embed inline node -e one-liners whose frontmatter boundary regex used a literal \n, silently returning null on CRLF-saved SUMMARY.md / REVIEW.md / REVIEW-FIX.md artifacts and dropping every file in that summary (acceptance: per-artifact, no warning when the phase aggregate stays non-zero). Adds: - behavioral CRLF==LF boundary extraction tests (replica of the shipped one-liner's boundary step), proving the buggy literal-\n returns null on CRLF while the fixed normalize-then-match yields a byte-identical body; - a structural-regression-guard (allow-test-rule: structural-regression-guard) that reads the two shipped workflow files and asserts every boundary site normalizes \r\n -> \n before matching, so a revert of the fix is caught. * fix(#2694): normalize CRLF before frontmatter-boundary match in code-review workflows The code-review and code-review-fix workflows embed nine inline node -e one-liners that extract YAML frontmatter via a boundary regex content.match(/^---\n([\s\S]*?)\n---/) The literal \n defeated any CRLF-saved artifact (\r between --- and the line terminator), so SUMMARY.md / REVIEW.md / REVIEW-FIX.md saved with CRLF endings silently contributed zero files (or 'unknown' status / 'invalid') with no per-artifact warning. The Tier-3 git-diff fallback only fires when the aggregate across all summaries is zero, so a single CRLF summary among LF summaries produced no signal at all. Normalize \r\n -> \n once before the existing boundary match at all nine sites (code-review.md x3, code-review-fix.md x6). Byte-identical to the LF path; mirrors the canonical src/frontmatter.cts extractFrontmatter intent (CRLF == LF at the boundary); zero risk of \r leaking into field values consumed by the inner JS or the shell grep/cut pipeline. RED @ 94d0213 (3 failures, structural guard caught the shipped-text bug, both linux-node22+24 lanes).GREEN pending. * chore(#2694): acknowledge code-review workflow growth + changeset fragment emitted-attribution (ADR-2719) reports the byte growth from the CRLF-normalize insertion in code-review.md (+69) and code-review-fix.md (+138); both are the intended #2694 fix. Adds the .changeset Fixed fragment (pr:0, backfilled post-PR). * test(#2694): mixed CRLF/LF phase yields the union of both artifacts (criterion 2) The spec-axis review flagged that acceptance criterion 2 (a phase with a mix of CRLF-affected and unaffected artifacts no longer silently drops the CRLF artifact's contribution) was only transitively satisfied. Adds an explicit mixed-phase test replicating the full shipped Tier-2 extractor (boundary + inner key_files parse) across one LF and one CRLF SUMMARY.md, asserting the union of both — plus a RED proof showing the buggy boundary drops the CRLF artifact silently (aggregate non-zero, so the Tier-3 eq-zero fallback never fired). Locks the silent-partial-masking behavior the triage named as the more serious half of the defect. * docs(changeset): backfill #2694 PR number to 2839 * fix(#2694): make the CRLF regression test itself CRLF-lint-clean CI lint-tests caught that the new test tripped local/no-crlf-fragile-split: - the frontmatter boundary regex replicas (fixed + buggy) were RegExpLiterals with a bare \n; the rule flags frontmatter-shape regexes unconditionally. Build them via new RegExp(...) (byte-identical .source to the shipped literal) so the faithful replica is not a lint violation — the buggy replica MUST keep the literal \n, that is the bug it demonstrates. - the structural guard's src.split('\n') on the readFileSync'd workflow file was genuinely CRLF-fragile; use /\r?\n/ per the rule's canonical fix. - the allow-test-rule annotation gains its (#2694) tracking ref per ADR-456. lint:ci now exit 0 (incl. lint-allow-test-rule-refs, lint-emitted-drift-ack, lint-fix-has-regression-test: PASS). |
||
|
|
1fc21cdee0 |
fix(#2716): route non-user-facing conventional types to an Internal bucket, omit from release notes (#2838)
* test(#2716): failing-first regression for non-user-facing types → Internal bucket * fix(#2716): route non-user-facing conventional types to an Internal bucket, omit from release notes * fix(#2716): update SAMPLE_BODY/Discord/property tests for Internal bucket; fix stale CONTRIBUTING sentence (review) * test(#2716): relax Discord Enhancement assertion (enhance: prefix not stripped by cleanBullet) * docs(changeset): #2716 non-user-facing types omitted from release notes * docs(changeset): backfill #2716 PR number to 2838 |
||
|
|
6a9babda69 |
chore(#2798): declare the eleven reviewer lanes as manifest data (#2837)
* chore(#2798): declare the eleven reviewer lanes as manifest data Phase 5a of epic #2782, delivering ADR-2782 D9 (roster half) and D3. - Five reviewers GSD never installs into become lane-only role:reviewer capabilities with no runtime body, no runtimeCompat and no install surface: gemini, coderabbit, ollama, lm-studio, llama-cpp. Before this they had no descriptor at all and lived as a hardcoded NON_RUNTIME_REVIEWER_SLUGS tail, which is now deleted outright. - The six hosts that are ALSO reviewers gain a reviewer body alongside their runtime body. Their runtime bodies are byte-identical to next -- verified per capability against the git blob, not asserted -- so no install behaviour moves. - KNOWN_REVIEWER_SLUGS derives from declared bodies via an exported deriveReviewerSlugs(registry). hostBehaviors.reviewerCli survives as a derived legacy alias for one release; where a capability carries both, the body wins and the slug appears once. Alias removal is Phase 7 (#2801). THE KEYSTONE: the roster is the SAME ELEVEN SLUGS as before -- antigravity, claude, coderabbit, codex, cursor, gemini, llama_cpp, lm_studio, ollama, opencode, qwen. This phase changes HOW the roster is derived, not WHO is in it, and the test asserts that literal list rather than a count. kimi-code is deliberately NOT declared here. It is net-new with no invoke_reviewers leg, so declaring it now would make it selectable but not invocable -- present in --all, selected, emitting an empty section for the whole 5a-to-5b window -- and would break Phase 1's parity assertion. It lands in 5b alongside the iteration that can run it. Legacy kimi (the Python CLI) is not a reviewer at all and gains nothing. The highest-value test is declaredManifestLanesMatchThePhase1Descriptor: it deep-compares all eleven declared bodies against REVIEWER_LANES field-by-field, including probe and invoke sub-fields. All eleven are byte-identical, key order included. The epic's premise is that the manifest and the core descriptor describe the same lane with NO translation layer, and Phase 2's review already caught one divergence that every other test missed. Two ADR corrections folded in, as Phases 1-3 each did: 1. PHASE ORDER. The ADR runs Phase 4 (federated config) before 5a and #2798 claims a dependency on 4. That is inverted and makes Phase 4 unsatisfiable: D9 assigns review.<host>_host to lane capabilities that do not exist until THIS phase creates them, and a federated config slice must live inside capabilities/<id>/capability.json. Real graph: Phase 2 -> 5a -> 4. 2. #2798's INVENTORY acceptance item is vacuous. The inventory catalogs bin/lib/*.cjs modules, not capability directories -- antigravity, opencode and qwen appear zero times in it -- and gen-inventory-manifest --check passes with the five new dirs and no edit. Also corrected a stale line in Phase 2's own ADR amendment: it recorded the slug pattern as /^[a-z][a-z0-9_-]*$/, but Phase 2's security review widened the shipped pattern to /^[a-z0-9][a-z0-9_-]*$/ to match Phase 1's exported LANE_SLUG_RE. The prose had not followed the code. Closes #2798 * fix(#2798): catalogue reviewer capabilities in the generated matrix The capability matrix rendered exactly two tables, feature and runtime, via renderTable(caps, role) filtering on c.role === role. ADR-2782 D3 added a THIRD role, so every role:"reviewer" capability was silently dropped from the first-party catalogue. The drift guard did not catch it, and could not: --check compares generated output against the committed file, and both omitted the five lanes identically, so it reported "up to date" while five shipped capabilities were invisible in the one document that is supposed to list what ships. A guard blind to an entire role is not guarding. This phase is what exposed it -- it ships the first role:"reviewer" capabilities -- so it is fixed here rather than deferred (CLAUDE.md: a defect found while working is fixed in the current change, which overrides one-concern-per-PR). Verified red-before-green: with a lane row deleted from the matrix, --check now exits 1; restored, it exits 0. Before this fix the lanes were absent entirely, so there was nothing for the guard to compare. Phase 6 (#2800) still owns enriching the matrix with lane-specific detail (slug/flag/transport columns) and the locale parity gate. This is the narrower fix: the capabilities APPEAR at all. * fix(#2798): close two hardening gaps and record three limits durably Isolated security review (5 targets, no blockers) reproduced two gaps in the new deriveReviewerSlugs. Both are unreachable through the checked-in registry -- it is generated, JSON-sourced and code-reviewed -- but the function is EXPORTED for reuse and carries no other validation, so it must not depend on its caller. - A whitespace-only slug passed the length>0 test verbatim and occupied a roster entry it could never match. Slugs are now trimmed before the emptiness test. A blank body correctly falls through to the legacy alias rather than DROPPING the lane, which would have been worse than the blank slug. - KNOWN_REVIEWER_SLUGS is computed at require() time, so an uncaught throw there breaks import for EVERY consumer rather than degrading selection. It is now guarded, yielding an empty roster on a malformed registry. That is a visible degradation, not a silent one: under D4 an explicitly requested reviewer that is unavailable is an ERROR, so /gsd:review --claude against an empty roster fails loudly. This also removes an asymmetry -- the sibling capability-trust module documents its collectors as TOTAL and wraps them for exactly this reason. Also records three findings that previously existed ONLY in squash-merged PR bodies, which is not a durable record: - ADR-2782 D5 gains an implementation note explaining why the resolved host is deliberately EXCLUDED from the disclosure signature. Rule 1 says consent binds the resolved host; the loader has no config resolver, so folding it in would make the loader and lifecycle compute different signatures for one manifest and re-prompt forever. The binding is split: signature covers the SHA-pinned manifest fields, the consent record stores the resolved host, and Phase 5b re-resolves at invocation -- which is where rule 4 already puts the check. A reader comparing rule 1 to the code would otherwise conclude it is unimplemented. - CONTEXT.md's capability-trust entry still described THREE executable surfaces. Phase 3 added the fourth and made that false; corrected here, since it is drift this epic introduced rather than Phase 6's new-glossary-term work. - stableJson documents the NaN/Infinity/undefined -> null signature collision and why it is unreachable (JSON grammar has no such literal, so JSON.parse throws first). Reachability rests entirely on the ingest path staying JSON.parse-only, so the note lives where someone would break it. * chore(#2798): backfill changeset pr number to 2837 |
||
|
|
982e83f0f7 |
docs(#2705): single-source the legacy ADR range; classify 0174/0656 as mis-padded modern (#2836)
* test(#2705): failing-first regression for single-sourced ADR legacy range * docs(#2705): single-source the legacy ADR range; classify 0174/0656 as mis-padded modern * fix(#2705): align README prose with test vocabulary (mis-padded modern; backtick-tolerant range regex) (review) * docs(changeset): #2705 single-source legacy ADR range * docs(changeset): backfill #2705 PR number to 2836 * chore: trigger clean CI run (prior run cancelled by rapid backfill push) |
||
|
|
8fc244b754 |
fix(#2702): workstream config-get inherits absent keys from root config (#2833)
* test(#2702): failing-first regression for workstream config-get root inheritance * fix(#2702): workstream config-get inherits absent keys from root config * fix(#2702): root inheritance wins over --default; gate on GSD_WORKSTREAM not path (review) * test(#2702): make GSD_PROJECT test exit-safe (--default sentinel, no error path) * docs(changeset): #2702 workstream config-get root inheritance * docs(changeset): backfill #2702 PR number to 2833 |
||
|
|
6229f0e55c |
fix(#2701): reject NUL-corrupted plan/state artifacts at the validator entry points (#2829)
* test(#2701): failing-first regression for NUL-corrupted plan/state validators * fix(#2701): reject NUL-corrupted plan/state artifacts at the validator entry points * fix(#2701): seed STATE.md in test (writeState); add NUL-path guards to validate/verify for parity (review) * docs(changeset): #2701 validators reject NUL-corrupted artifacts * docs(changeset): backfill #2701 PR number to 2829 |
||
|
|
69dbf28ca7 |
feat(#2796): reviewer lane as a fourth trust-disclosure class (#2826)
* feat(#2796): reviewer lane as a fourth trust-disclosure class Phase 3 of epic #2782, delivering ADR-2782 D5. A reviewer lane is piped the plan text, requirements, research findings and CONTEXT.md decisions, and its output is read back into REVIEWS.md -- an egress channel for the most sensitive artifacts GSD produces. Making lanes pluggable WITHOUT a disclosure class would open a data-exfiltration path behind a manifest field, which is why this gates the feature rather than following it. - discloseExecutableSurfaces was cyclomatic 51 / cognitive 99 / 110 lines with risk_level critical. Rather than grow it, it is now a short orchestrator over four extracted collectors (hooks, commands, mcp -- behaviour-preserving -- plus the new lane collector), each independently testable. That is also what makes the 80% mutation threshold survivable: 51 branches in one function cannot be mutation-covered by whole-function tests. - A spawn lane discloses its binary AND its full declared args, in rendered and raw form. Binary-only disclosure would be insufficient and not hypothetically: a lane declaring python3 with innocuous args could later change them to ['-c', '<program>'] without the binary changing. That is the bug class #1459 already fixed for MCP servers. - An openai-http lane has no binary, so it discloses the destination host and the config key naming it. A localhost destination is disclosed and distinguished from a remote one. Both forms name the egress payload classes. THE CONSTRAINT THAT SHAPED THE DESIGN: the lane element is appended to the disclosure signature ONLY when at least one lane is declared. signatureForManifest is the consent key both the loader and the lifecycle compare, so appending unconditionally would have changed every installed capability's signature and re-prompted every user for every capability on their next upgrade -- for a feature they do not use. Two pre-change goldens are asserted byte-for-byte as the tripwire. The resolved host is deliberately NOT in the signature. The loader has no config resolver, so including it would make the loader and the lifecycle compute different signatures for the same manifest and produce a permanent false-mismatch loop. It is disclosed and recorded instead; Phase 5b re-resolves and compares at invocation, which is D5 rule 4's own placement. reviewsSection and timeoutFloorMs are also excluded from the signature: a cosmetic change must not force re-consent, because a prompt carrying no security information is how users learn to click through. A lane's binary is NOT existence-checked against the staged bundle. It is a PATH tool, never a bundle artifact; treating it like a hook script would add every lane to missingArtifacts and block every lane install. Two defects fixed beyond the fourth class: - isLocalHostValue mis-parsed a scheme-less host: new URL('localhost:1234') does NOT throw, it reads 'localhost' as the URL scheme and yields an empty hostname, so a bare host:port would have been reported as non-local. Now falls back on an empty hostname rather than only on a caught throw. - The orchestrator's safeCollect closes a PRE-EXISTING totality gap in the other three classes: a null manifest, or one with a throwing getter or Proxy trap, previously threw out of disclosure -- which runs on an UNVALIDATED manifest at install time. No well-formed input changes; all 51 existing trust tests pass. Closes #2796 * fix(#2796): close four disclosure gaps found by the isolated security review All four were REPRODUCED by execution against the shipped module, and all four passed the existing 41-test suite while live -- each exists because the matrix did not think to ask. B (MEDIUM, reachable via plain JSON). Non-string argv members were folded into the consent SIGNATURE but dropped from the human-facing text, because the summary rendered the string-filtered args rather than the raw declared array. A manifest declaring args ['--json', 7, {mode:'exfiltrate-everything'}, true] printed as '--json' alone -- the host still receives the rest, so the user consented to a surface never shown. That directly contradicts this design's own Kerckhoffs claim that nothing about a lane is hidden. The summary now renders the raw array, with non-strings shown in a visible form, and never throws on a circular or BigInt member. F (MEDIUM, reachable). The [local] flag is design-load-bearing, and it was dropped for every loopback form except the dotted quad and the bare hostname. Bracketed IPv6 was mangled by splitting on the address's own colons ([::1]:8080 became '['), and legacy IPv4 encodings were not recognised at all. A browser, curl and the OS resolver all treat 127.1, 2130706433, 0x7f000001 and 0177.0.0.1 as loopback. isLocalHostValue now handles bracketed and bare IPv6, IPv4-mapped loopback, and inet_aton shorthand/decimal/hex/octal. The dangerous direction was already clean and is now pinned by tests: localhost.evil.com, http://user@localhost@evil.com and friends stay REMOTE. C (LOW). An empty reviewer body flipped hasExecutable true and perturbed the disclosure signature, producing a re-consent prompt whose only content was '(no binary declared)'. A prompt carrying no security information is the click-through-training harm this design explicitly refuses for reviewsSection and timeoutFloorMs; refusing it there and permitting it here was inconsistent. A body declaring nothing recognised is no longer a lane. The test is deliberately broad -- any ONE recognised field suffices -- because requiring specifically a binary, or specifically a slug, would let a lane declaring only the other slip through unconsented, which is the far worse failure. Pinned in both directions. D (LOW-MEDIUM). Disclosure runs BEFORE validation, so a mis-cased or unrecognised transport reaches this code. Keying on an exact string sent a lane that plainly declares a hostConfigKey down the spawn branch, printing '(no binary declared)' for a lane egressing to a live remote host, and left its resolvedHost blank -- which reads as 'no destination', the precise thing the design forbids. Both the collector and the summary now branch on the declared SHAPE, so such a lane discloses its key and either a resolved host or the explicit unresolved marker. Two further findings were reproduced but confirmed NOT reachable through the real pipeline and are recorded as known limits rather than fixed: a selective-throw Proxy blanking a whole lane, and NaN/Infinity/undefined colliding to 'null' in a signature. Every production manifest reaches disclosure through readManifestBounded's strict JSON.parse, which cannot produce a Proxy, a getter, a BigInt, a circular reference, NaN or Infinity. The 0/-0 sub-case IS reachable via valid JSON but is inert -- String(0) === String(-0), so a spawned process receives identical argv. 9 regression tests added (50 total in this file, up from 41). * chore(#2796): backfill changeset pr number to 2826 |
||
|
|
ddcf459c81 |
fix(#2697): skip the context-monitor spawn in-process when context_warnings disabled (#2824)
* test(#2697): failing-first regression for context-monitor spawn not hoisted behind toggle * fix(#2697): skip the context-monitor spawn in-process when context_warnings disabled * test(#2697): patch spawnSync before require (plugin destructures at load time) * test(#2697): establish session before asserting on context-monitor spawn (review) * docs(changeset): #2697 context-monitor spawn skipped when context_warnings disabled * docs(changeset): backfill #2697 PR number to 2824 |
||
|
|
46e84d5e39 |
chore(#2795): reviewer manifest body + registry harvest, validation, forward-compat (#2823)
* chore(#2795): reviewer manifest body + registry harvest, validation, forward-compat Phase 2 of epic #2782 under ADR-2782. Delivers D1, D2, D3, D7, D8 and the four Phase-1 vocabulary amendments (A1-A4). - VALID_ROLES gains "reviewer"; the reviewer body is admissible on role:runtime (a host that is also a reviewer keeps one manifest) and on the new role:reviewer (a lane that is not an install target). A reviewer body on role:feature is an error: declaring one is an assertion of lane-ness. - validateReviewerBody + validateLaneProbe + validateLaneInvoke: nine closed enums, a transport discriminator selecting mutually-exclusive invoke sub-shapes, bounded probes (D7), and outputArg required-iff outputChannel is file-arg and forbidden otherwise. - Absent-safe (D4.1): only `undefined` is absent. null/{}/[]/false/0 are malformed assertions and error. 39 of 39 shipped capabilities depend on this. - collectReviewerWarnings: an unknown field inside the body warns, never errors, so a forward-built manifest degrades visibly instead of failing the build. - D8 uniqueness (slug / flags / reviewsSection) lives in validateCrossCapability, so it is enforced at build time over first-party AND at load time over the merged first-party union overlay set, with first-party-wins falling out of the loader's existing ordering rather than a new provenance check. - Config harvest widened past the role==="feature" branch in both the generator and the ownership loop. The often-cited cause of the stranded reviewer config keys -- the runtime body forbidding feature-only fields -- is not the mechanism: `config` is not in FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME. The cause is two harvest sites that never read it. Verified inert: no shipped capability declares config on a non-feature role, and the generated registry is unchanged. Three ADR corrections are folded in (Phase 1 set the precedent of amending in-phase): the misattributed config-stranding cause, D3's inverted profile-membership claim, and the specified capability folder names for lm_studio / llama_cpp, which would have failed the id kebab-case invariant. Closes #2795 * chore(#2795): collapse nine enum checks into one validateEnumField helper Standards-axis review findings, both applied: - Duplicated Code: the enum-membership + enumerate-the-members error shape repeated near-verbatim at nine call sites. Routing them through one helper makes "the error names its valid members" structural rather than a convention repeated nine times, where it would drift. That property is load-bearing until Phase 6 ships the prose reference, because these errors are currently the only documentation of the vocabulary. - Speculative Generality: the isReservedName() pre-check on every enum field was inert. A VALID_* set never contains __proto__/constructor/prototype, so membership alone already rejects them, and "must be one of: ..." is more actionable than "is a reserved name". The literal guards remain where they do real work -- the key-derived write sites in the registry generator and the claim() accumulator. The reserved-name test now asserts all three reserved names are rejected via enum membership, rather than one name via a branch that no longer exists. * fix(#2795): align lane slug grammar with Phase 1 and wire the load-time diagnostic channel Spec-axis review findings, both applied. (1) The slug grammar had diverged from Phase 1's core descriptor. Phase 1 exports LANE_SLUG_RE = /^[a-z0-9][a-z0-9_-]*$/ (leading digit permitted); the manifest validator required a leading LETTER. A slug the core descriptor accepts -- a model-named lane such as 4o-mini -- would have been rejected by the manifest validator, which is exactly the translation layer ADR-2782 exists to delete. It was inert only because all eleven shipped slugs begin with a letter, so nothing else would have caught it until a third party shipped such a lane. The grammar cannot be reduced to one definition: Phase 1's module compiles to gitignored build output, and capability-validator.cjs is a committed plain .cjs that must load on a fresh worktree before build:lib has ever run. That makes this the repo's DEFECT.GENERATIVE-FIX class, so the duplication now carries a parity assertion -- laneSlugGrammarMatchesPhase1Descriptor -- which compares both the source grammar and the accept/reject verdict for a shared input set, and fails if the two ever drift again. (2) collectReviewerWarnings had exactly one caller: the build-time generator, which only ever sees first-party in-repo manifests. The real third-party overlay loader never called it and ValidatorModule did not declare it, so ADR-2782 D4.3 -- an unknown field inside a reviewer body is ignored WITH A WARNING -- surfaced nowhere at runtime, which is precisely the case D4.3 exists for. loadRegistry now collects those diagnostics on the accept path, behind a typeof-guard (an older built validator without the function still loads) and a try/catch (ADR-1244 D2's never-crash contract outranks a diagnostic). They land in a NEW OverlayMeta.diagnostics field rather than OverlayMeta.warnings, because warnings records capabilities that were SKIPPED and a consumer treating every entry as inactive would mislabel a working lane. Covered end-to-end by overlayLaneWithUnknownFieldIsAcceptedAndDiagnosed, which drives a real global-scope overlay through loadRegistry and asserts the lane is accepted, produces no skip warning, and yields a diagnostic naming the field. * fix(#2795): make the reviewer validators honour their documented totality contract Isolated adversarial review finding (MAJOR), reproduced by execution. validateReviewerBody documents itself as "TOTAL: returns an array of error strings for ANY input and never throws", and the overlay loader contracts every validator to RETURN errors -- #1461 OVL-1 records a validator that THREW and would have crashed every consumer of loadRegistry. The contract was false at ten sites: JSON.stringify throws on a BigInt and on a circular structure, and every enum/scalar rejection path interpolated the rejected value into its own rejection message. Reading the value could throw too, before any message was built, via a throwing getter or a Proxy get/ownKeys trap. Not reachable through a capability.json today -- every ingestion path is a plain JSON.parse of file text, which cannot express any of those shapes. Fixed anyway: the contract is stated on an EXPORTED function, and a caller must not have to re-derive today's reachability analysis to know whether it holds. Two layers, because serialization safety alone is insufficient: - describeValue() renders any value without throwing, so messages stay useful (a BigInt now reads "got: 10n" rather than degrading to a generic fallback). - A structural try/catch around validateReviewerBody and collectReviewerWarnings makes the guarantee absolute rather than argued, covering read-time throws that fire before any message exists. The same review found the property test guarding this contract was FALSE CONFIDENCE, which is the more important half. fc.anything() at default constraints emits no BigInt, no circular reference, no getter and no Proxy -- 20,000 sampled draws produced zero of each -- so the test was named for a contract its generator could not reach. Even withBigInt is insufficient under whole-value fuzzing, because the defect needs an exotic value in a specifically NAMED field and random key names never land on one. The property is now field-targeted across all twelve reviewer fields, and a companion test enumerates the shapes fast-check cannot generate at all (BigInt, circular, throwing getter, symbol, function, null-prototype) across scalar positions, array-element positions, and read-time traps. Verified red-before-green: with the fix reverted both property tests fail; with it restored all 119 pass. * chore(#2795): backfill changeset pr number to 2823 |
||
|
|
cfdfdf0b4c |
fix(#2695): deliver the complete four-file codex hook set for every profile (#2822)
* test(#2695): failing-first regression for codex hook worker/registry omission * fix(#2695): deliver the complete four-file codex hook set for every profile * test(#2695): regenerate codex install-tree + acknowledge emitted hook ripple * test(#2695): pin core config.toml/hooks.json wiring + fix agent-extension guard (review) * docs(changeset): backfill #2695 PR number to 2822 * fix(#2695): SessionStart wiring assertion is extension-portable (Windows .cmd shim) |
||
|
|
8b44a0da43 |
chore(#2794): single-source the reviewer invocation contract + parity assertion (#2820)
* chore(#2794): single-source the reviewer invocation contract Phase 1 of epic #2782 (ADR-2782). Introduces one core descriptor table as the declared contract for all 11 cross-AI reviewer lanes, and the DEFECT.GENERATIVE-FIX parity assertion the roster has never had. The lane contract lived in three unrelated surfaces — the roster, ~640 lines of hand-authored per-CLI bash in invoke_reviewers, and the write_reviews section headings — so cross-cutting fixes landed per-leg (#2494 and #2605 were the same empty-output defect filed twice). - src/review-lane-descriptor.cts: frozen table declaring per lane the slug, flags, probe, invoke shape, timeout floor, empty-output policy, REVIEWS.md section, evidence class, required binaries, prompt-budget key and handler. Field names track ADR-2782 D1/D2/D6/D7 verbatim so Phase 2 harvests the shape with no translation layer. It declares; it does not execute — invoke_reviewers iterates in Phase 5b. - checkReviewerLaneParity: bidirectional parity across descriptor, roster, invoke_reviewers legs and write_reviews sections. Forward-only would miss the failure it exists to catch (#2718 added a leg, #2781 was the drift). ADR-1517 instance headings are exempt per D8. - Legs carry an explicit <!-- reviewer-lane: slug --> marker; five non-lane bold labels share the bold-then-fence shape a heuristic matcher would key on. - ADR-2782 D4: an explicitly-flagged reviewer that cannot run is now an error in both the core module and the workflow prose that mirrors it. A code-only change would be unobservable — the module has no production caller; the workflow narrates the policy. Discovery paths (--all, review.default_reviewers) stay lenient. - Fixes the qwen leg, the last one discarding stderr to /dev/null. Two ADR-2782 D2 vocabulary widenings were forced by surveying the shipped legs: promptChannel 'none' (CodeRabbit is fed no prompt) and outputChannel 'file-arg' (Codex writes via -o and discards stdout, #1698). Both are additive and closed; Phase 2 owns the validator. Closes #2690 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2794): make the parity checker total and pin the lane slug grammar Findings from the orthogonal review passes. Spec axis — the module claimed its vocabulary tracked ADR-2782 D1/D2 "verbatim" while diverging in three undisclosed ways, which is the translation layer Phase 2 was supposed to be spared: - `transport` moves from `invoke.transport` to the LANE level, a sibling of `probe`/`invoke`, exactly as D1's manifest example places it. The nested form read better as a TS discriminated union; the union is now discriminated at the lane level instead, which costs nothing. - The header and the CONTEXT.md glossary now enumerate all FOUR widenings (adding `outputArg` and `flags[]`), not two. Standards axis — CLAUDE.md requires a fast-check property test for a parser, and `checkReviewerLaneParity` parses markdown for markers and headings. Adding one found two real defects that the hand-written matrix missed: - NOT TOTAL: a malformed descriptor entry threw on `lane.flags` iteration, contradicting the module's own "never throws" claim. Every field is now narrowed from `unknown` at the trust boundary and reported as MALFORMED_LANE / INVALID_SLUG. This matters because Phase 2 feeds this function third-party overlay data, and a parity gate that crashes is indistinguishable from one never run. - SILENT GRAMMAR MISMATCH: LEG_MARKER_RE captures only [a-z0-9_-], so a slug outside that class was unmatchable — its marker could be present and correct and the scan would still report LEG_MARKER_MISSING forever. LANE_SLUG_RE now pins the grammar and a violating slug is reported INVALID_SLUG. A loud named violation beats a silent miss. Generators are document-shaped, not writer-seeded (CONTRIBUTING #2371): seeding from the module's own matchers could only produce documents those matchers already recognize. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2794): register the new bin/lib module in the ESLint ignore list The remote runner caught this; lint:ci did not, because the invariant lives in the test suite rather than the lint chain: tests/repo-invariants.test.cjs "each bin/lib/*.cjs is linted xor ignored according to migration state" -> tsc-generated bin/lib modules not yet added to ESLint ignore list: review-lane-descriptor.cjs Adding a src/*.cts module ripples to six surfaces (.gitignore, the ESLint ignore list, docs/INVENTORY-MANIFEST.json, the CONTEXT.md glossary, the capability/inventory manifests, and any size baseline). The other five were covered; this was the miss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#2794): amend ADR-2782 D1/D2/D8 with the vocabulary Phase 1 surfaced Building the Phase 1 descriptor table against all eleven shipped legs is the first time every lane's contract was written in one place, and it surfaced four cases the ADR's original survey did not cover. Amending the design lock rather than diverging from it, so Phase 2 (#2795) implements the manifest validator against the amended vocabulary instead of rediscovering the gaps. All four are additive widenings of closed enums; no decision reverses: - D2 promptChannel gains `none` — coderabbit is fed no prompt at all, it reviews the working-tree diff. - D2 outputChannel gains `file-arg` — the ADR called a file-writing lane a shape a real CLI *could* take; codex already is one, writing via -o/--output-last-message and discarding stdout (#1698). - D2 gains `outputArg`, required iff file-arg — knowing the review lands in a file is useless without the argument naming it. - D1 `flag` becomes `flags[]` and D8's uniqueness flattens across lanes — antigravity is selected by both --antigravity and --agy, which a single-valued field cannot express. This is the same evidence path that produced the openai-http transport: the vocabulary widens on a lane that exists, under review, never on speculation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2794): backfill changeset pr number to 2820 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9624167eec |
fix(#2810): accept the documented effortSurface axis on EoS registry entries (#2813)
* fix(#2810): accept the documented effortSurface axis on EoS registry entries The EoS registry schema required an exact eight-key `interactions.axes` object, while `docs/registries/README.md` and `CONTEXT.md` both documented nine keys including `effortSurface`. An entry that faithfully mirrored its upstream descriptor's `effortSurface` key was rejected outright. `effortSurface` reached the runtime-descriptor vocabulary through ADR-1239 amendment #2481 (`HOST_INTEGRATION_AXES`), but the registry's hand-maintained copy of that vocabulary never picked it up. The runtime-descriptor surface is guarded by tests/host-integration-validator-parity.test.cjs; the registry copy had no equivalent guard, which is what let the two drift. Accept `effortSurface` as an OPTIONAL ninth axis validated against the canonical ['argv','none'] rather than a required one: registry entries mirror their upstream registry/eos-entry.json byte-for-byte, so requiring it would retroactively invalidate every entry published before the amendment. Adds tests/registry-axes-parity.test.cjs, which asserts that every key shared between the registry vocabulary and HOST_INTEGRATION_AXES has an identical enum array, plus limit-1/limit/limit+1 boundary coverage on the axes key set. Closes #2810 * test(#2810): fail when a canonical axis is added but never mirrored The enum-equality assertion compares only keys the registry and HOST_INTEGRATION_AXES already share, so it is blind to the exact drift that produced #2810: a new canonical axis appears and the registry copy is never told. Verified by simulation — mutating an enum is caught, adding a new canonical key is not. Assert instead that every HOST_INTEGRATION_AXES key is either modeled by the registry or named in an explicit NOT_MODELLED allowlist (subagentToolkit and isolation, both dispatch sub-fields the registry collapses into its free-form dispatch summary). Adding a canonical axis now fails until someone decides which bucket it belongs in. The allowlist is itself guarded against going stale. Refs #2810 * fix(#2810): harden the axis value lookup with the CodeQL barrier pattern Both orthogonal reviews flagged the same line: `AXES[key] !== undefined` is not an own-property test, and the bracket reads are shaped like a prototype-pollution sink even though the unknown-key gate above provably makes them unreachable. Switch the presence test to `Object.hasOwn` and add the repo's inline literal guards (`capability-state.cts:146-155`, "Prototype-pollution guard (inline literal, CodeQL barrier)"), which CodeQL can follow where it cannot follow the `.includes()` filter that actually does the work. Behavior is unchanged — re-verified all five axes key-count shapes plus a genuine own `__proto__` property built through JSON.parse (the shape a third-party registry PR would submit): it is rejected as an unknown key and Object.prototype is untouched. Refs #2810 * chore(#2810): backfill changeset PR number |
||
|
|
46bae2f9ff |
fix(#2624): write the .gsd-source marker before staging reads it (#2811)
* test(#2624): failing-first regression for stale .gsd-source marker read-before-write
Adds failing-first regression proving a Claude-global upgrade currently lets
staging read a stale prior-version .gsd-source marker before install() rewrites
it. Pre-seeds a stale marker pointing at a still-existing fake source, spies on
findInstallSourceRoot to capture which source staging resolves, and asserts
staging never resolves the stale path. Also covers fresh-install and ghost-marker
negative space.
* fix(#2624): write the .gsd-source marker before staging reads it
A Claude-global upgrade silently installed skill content from the PREVIOUS
version. findInstallSourceRoot prefers <configDir>/.gsd-source, and install()
used to rewrite that marker AFTER staging had already read it — so on an
upgrade the marker still pointed at the prior version's source (an npx cache
dir that still exists on disk) and every converted skill was generated from
the OLD commands/gsd, with generateManifest then recording the stale content's
hash as correct.
Extract the marker write into _writeGsdSourceMarker(runtime, targetDir, src,
isGlobal) and call it BEFORE the staging pass (before the _isSkillsRuntime
branch), preserving the original sourceMarkerFile && isGlobal guard, the
half-published-package existsSync guard, and the non-fatal write-failure warn.
This closes the read-before-write hole for every findInstallSourceRoot
consumer (skills, commands, /gsd-surface, capability-state) in one move.
Long-standing (marker write added in
|
||
|
|
7f13ee5373 |
enhance(#2793): ADR-2782 — reviewer lane becomes a declared capability surface (#2809)
* docs(#2793): add ADR-2782 — reviewer lane capability surface Design lock for epic #2782. Declares a reviewer lane as capability data rather than a core patch across three unrelated surfaces. Key decisions: - D2 transport discriminator (spawn | openai-http) — a survey of all twelve lanes found three that are HTTP endpoints with no binary, which invalidated the single-invoke-shape draft. - D4 the reviewer body is optional and absent-safe at every layer. - D5 a fourth executable-surface disclosure class covering the lane binary or host AND its egress payload classes. - D6 handler is a closed first-party enum, upholding ADR-1016; the consequence — third-party lanes are data-only — is stated plainly. - D7 probe kinds wider than existence, and every probe bounded. Amends ADR-857, ADR-894, ADR-1016, ADR-1244. Also records the D7/D8-extended-by-ADR-1244 marker on ADR-857 that ADR-1244 D8 promised but never added. Closes #2793 * docs(#2793): address orthogonal review findings on ADR-2782 Two blockers from the isolated adversarial pass: - D5 disclosed the spawn binary but not its args, reopening the #1459 bug class already fixed for MCP servers (binary python3 + args -c <program>). args are now disclosed and signature-bound. - hostConfigKey resolves from .planning/config.json, which is mutable after consent with no integrity check, so a lane consented against localhost could be silently redirected to a remote host by an ordinary PR. The resolved host is now consent-bound and re-verified on the invocation path; a mismatch blocks the lane. Majors and spec gaps: - D4 gains an explicit-selection carve-out. Absent-safe governs discovery, never a lane the user named; the current selector records that as info, which Phase 1 now corrects. - D4 gains a warning delivery channel. - D6 enumerates the handler closed-enum members; a closed enum whose membership is left to the implementing phase is not closed. - D6 records aider and plandex as concrete lanes the vocabulary cannot express, rather than claiming sufficiency it did not verify. - D2 gains evidenceClass, requiresBinaries, promptBudgetKey for per-lane divergence that was only prose, and motivates the one-member outputChannel enum. - reviewer.requires renamed requiresBinaries — it collided with the envelope requires (capability deps) at a different nesting depth. - Antigravity two-level timeout: Context cited it then dropped it; now explicitly delegated to the handler. - D9 gains a per-key ownership table, including three keys that stay central because they are policy across lanes, not lane properties. - Phase table maps every decision D1-D9 to a delivering phase; D6 handler modules and D5 invocation-time re-verification were previously unclaimed. - American English per house style. |
||
|
|
84bfef0818 |
docs(#2533): refresh gsd-cursor EoS metadata (#2792)
Co-authored-by: clezcoding <clezcoding@users.noreply.github.com> |
||
|
|
1e3c995e6f |
fix(#2789): scope the emitted-drift ack to the diff that introduced it (#2803)
* fix(#2789): scope the emitted-drift ack to the diff that introduced it Every input to `diffEmitted` is base-relative -- `baseline` vs `current`, `changedPaths` from `git diff base...HEAD` -- except the ack set, which was read absolutely, from the working tree only. A differential machine consulting a non-differential input. So `staleAcks` asks exactly one question, "did a delta consume you?", and that cannot distinguish an ack that never explained anything (an authoring mistake) from one whose ripple is now absorbed into the base (the ack's SUCCESS condition). After merge an ack is in the second state but reports as the first. The trigger is ordinary. Actions sets GITHUB_BASE_REF on pull_request events only, so a push to `next` falls through to origin/next -- the very commit under test. Both sides build identical content, no deltas remain, and every live ack is reported stale. PR #2768 acked a deliberate 40866 -> 42020 byte growth, was green on its own lane, and reddened `next` the moment it merged. It also reds every PR branching off the poisoned base, and since publish-emitted-baseline is gated on the test job, it blocked baseline publication too. Give the ack the base side it was missing. `diffEmitted` now takes `baseAck` -- the same document at the base ref, via `readAckFileAtRef`. An entry already present there is SPENT: it may no longer consume a delta and is never reported stale, only surfaced as `spentAcks` for tidying. An entry new or reworded in this diff stays live, and if nothing consumes it that genuinely fails, with blame on the author who just wrote it. This closes a hazard the IMPLEMENTATION named but could not prevent -- a leftover ack silently pre-clearing the next ripple on its path. (ADR-2719 §3 asserted only that TOUCHING the file is the alarm; its residual-risk list never covered pre-clearing, and §3 now carries an amendment.) Verified against the two-PR laundering sequence -- land an innocuous ack, then change the artifact -- which passed silently before and now fails on both the hash pass and the size ratchet. Three things the design has to get right, each of which was wrong first: - A read failure on the base document THROWS; only absence-at-the-ref returns null. Returning null on error LOOKS armed (every entry stays live) but a live entry's defining power is that it CONSUMES a delta, so null is armed on the staleness axis and DISARMED on consumption -- silently the whole pre-#2789 gate. `git show` cannot tell absence from fault, so absence is established with `ls-tree`. - Re-arming a spent ack costs actual PROSE. Internal whitespace and the zero-width family collapse, and `runtime` is not compared: a doubled space, an invisible character, or a decorative field would otherwise re-arm an ack whose justification still describes the previous ripple, showing a reviewer nothing. - `baseAck` is REQUIRED once an ack declares entries -- omission is an error, not a silent "inherit nothing" -- so a dropped argument fails loudly instead of quietly restoring this bug with the suite green. Because a corrupt document ON THE BASE is expensive (the loud base-side failure reds every ack-carrying PR), scripts/lint-emitted-drift-ack.cjs blocks one from landing. It is standalone rather than importing parseAck -- scripts/ ships in the npm package and tests/ does not -- so a parity test runs both surfaces over one corpus and fails on divergence; it caught one immediately, a `null` document, now classed as policy rather than schema. Deadlock is separately foreclosed: a tree carrying no ack never reads the base, so the PR that DELETES a corrupt file still lands. `readAckFileAtRef` takes an injected git runner so all four branches are tested deterministically; it never executes in the remote runner, where the real-tree test skips for want of a base ref. It also refuses an option-shaped ref, since execFileSync's array form stops shell metacharacters but not git's own option parsing. Rejected: skipping the differential when base == HEAD. It treats the symptom, costs real coverage on the push-to-next lane, and does nothing about the downstream PRs the same flaw was reddening. Deletes the now-spent tests/emitted-drift-ack.json, and updates the CONTEXT.md canon and ADR-2719 §3: presence is no longer the alarm -- a LIVE entry is, and a spent one is inert. Closes #2789 * chore(#2789): backfill changeset PR number |
||
|
|
d626dbc6e3 |
fix(#1883): distinguish a permission/IO error from genuine emptiness in dir scans (#2802)
* test(#1883): failing-first regression for findContextMdIn / listMilestoneArchiveDirs swallowing EACCES
Adds failing-first regression tests proving an unreadable dir is currently
swallowed as empty/null instead of surfacing the permission error. Covers
EACCES, EIO, the unchanged ENOENT empty path, the array fast-path, and both
CONTEXT.md forms. listMilestoneArchiveDirs is exercised in-process via a new
_listMilestoneArchiveDirs test seam (the validate command runs in a subprocess,
so an fs monkeypatch in the test process cannot reach it).
* fix(#1883): distinguish a permission/IO error from genuine emptiness in dir scans
findContextMdIn (src/planning-workspace.cts) and listMilestoneArchiveDirs
(src/verify.cts) catch-alled every readdirSync error into the empty marker
(null / []), conflating a genuine ENOENT ('nothing there') with an EACCES/EIO
failure ('can't read this'). An unreadable phase dir was silently reported as
'no CONTEXT.md' (discuss/plan gates wrongly skipped context) and an unreadable
milestones/ dir as 'no archives' (active-milestone resolution / archived-phase
filtering misbehaved).
Narrow each catch to ENOENT only — keep the long-standing null/[] contract for
genuine absence (Hyrum: empty path unchanged) and re-throw every other error so
it propagates to the caller's existing try/catch. All six findContextMdIn
callers either pass a pre-read string[] (no readdir) or sit inside a try block
that already handles readdir failures; the two listMilestoneArchiveDirs callers
live in the validate command path where errors reach the command error handler.
Exposes a _listMilestoneArchiveDirs test seam so the permission-error path can
be unit-tested in-process (the validate command runs in a subprocess, so an fs
monkeypatch in the test process cannot reach the private helper).
* fix(tests): delete stale emitted-drift ack for gsd-phase-researcher.md
Pre-existing base-branch defect, not part of #1883: commit
|
||
|
|
5296ff152d |
docs(#2533): list gsd-cursor EoS integration (#2581)
* docs: add gsd-cursor to the EoS registry Appends the gsd-cursor EoS entry to docs/registries/eos.json and regenerates docs/registries/eos-registry.md. Discussion: open-gsd/gsd-core#2578. * docs(#2533): regenerate eos-registry.md (id-sorted) + add changeset Addresses review on #2581: the generator sorts entries by id, so gsd-cursor (< gsd-omp) must appear first in both the table and the detail sections. Also adds the required .changeset Added fragment (mirrors precedent #2448). * docs(#2533): regenerate eos-registry.md via gen-registry.cjs (markdown escaping) The hand-written markdown left ( ) and _ unescaped; scripts/gen-registry.cjs escapes them (\(...\), model\_profile\_overrides), so gen-registry.cjs --check (part of lint:ci / the lint-tests job) failed. This is the generator's exact output; node scripts/gen-registry.cjs --check now passes. * docs(#2533): regenerate eos-registry.md for corrected compat floor Regenerated via scripts/gen-registry.cjs after the enginesGsd fix. node scripts/gen-registry.cjs --check passes (exit 0). * docs(#2533): correct gsd-cursor enginesGsd floor to >=1.8.0 (real release) The >=1.39.0 floor referenced a legacy-lineage version number that no current @opengsd/gsd-core release has ever reached (latest is 1.8.0). Per trek-e's review, corrected to >=1.8.0 — the current release, which ships the model_profile_overrides.<runtime> config key gsd-cursor uses. * docs(#2533): regenerate eos-registry.md (enginesGsd >=1.0.0, install v1.0.1) * docs(#2533): mirror gsd-cursor enginesGsd >=1.0.0 + repoint install to v1.0.1 Per review: enginesGsd must mirror what the package declares. Corrected engines.gsd upstream in gsd-cursor to >=1.0.0 and cut v1.0.1; this mirrors that range and pins install at the new tag. |
||
|
|
1b41083220 |
enhance(#2151): probe interactive-control for loading and error states (#2575)
* feat(#2151): probe interactive-control for loading + error states The ui-consideration-probe taxonomy mapped interactive-control to only one consideration (long-text), so a control-only UI surface (e.g. a theme toggle) lifted no loading or error consideration — the verifier never asked what a control shows while its action is in flight or when it fails, and a spec omitting those states could PASS. Add 'interactive-control' to the loading and error entries' elements in UI_TAXONOMY so control-only surfaces are probed for in-flight and failure states. empty is deliberately excluded (a control is not data-bearing; empty would be Goodhart noise). No new categories, no cue-map change, no probe-core change — a widening within the closed shape-rooted 8 (ADR-550), an independently-versionable predicate- generator adapter (ADR-857). Regenerated the reference-doc coverage table and the 19 golden-install- parity fixtures (reference-doc hash). Regression test added first (RED: interactive-control yielded only long-text; GREEN: now error+loading+long-text). Closes #2151 * feat(#2151): add changeset for interactive-control loading/error coverage --------- Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
a8b40fa53f |
fix(#2547): fail closed on crashing and path-shadowing Kimi payloads (#2595)
* fix(#2547): fail closed on a malformed Kimi edit list in normalizeKimiPayload
`normalizeKimiPayload` rebuilt old_string/new_string with
`String(e.old ?? '')`. `??` guards the value, not the dereference, so a
nullish entry in a Kimi `edit` list threw a TypeError at the top of the
handler, before any tool dispatch. Each guard's outer
`catch { process.exit(0) }` swallowed that crash and emitted the same exit
code as "nothing to report" — turning a should-BLOCK call into a silent
allow.
Two hard blocks were bypassable:
* gsd-worktree-path-guard's cross-git-root write block (#260) — a
StrReplaceFile write whose path resolves to a different git root is
correctly blocked with a well-formed edit list, and silently allowed
with `edit: [null]`.
* gsd-workflow-guard's force-add block on agent-* branches — a Shell
payload carrying a spurious `edit: [null]` field walks past it. The
Bash path never reads `edit`; the field only has to be present to
trigger the crash.
Fixed with `e?.old` / `e?.new`, landed identically in all five copies so
tests/kimi-guard-normalization-parity.test.cjs's byte-identity assertion
still holds.
The crash boundary is nullish specifically, not "non-object": `('x').old`
and `(7).old` are legal reads yielding undefined, so string/number entries
never threw. Both are kept as controls proving the fix did not change
their behaviour.
Regression coverage is folded into the owning suites per CONTRIBUTING.md
(no new bug-* files). Negative-controlled: the nullish cases exit 0
against pre-fix guards and exit 2 after, with positive controls (the
equivalent well-formed payload blocks) and negative controls (in-worktree
writes and benign commands still pass) alongside.
Refs #2547
* test(#2547): exercise the production Kimi payload shape in read-guard tests
The `#2304: Kimi tool vocabulary engages the read guard` cases send
payloads with no `session_id`, and runHook injects none. A live Kimi turn
always carries one — kimi-cli's hooks/events.py `_base()` sets it
unconditionally, and soul/kimisoul.py calls `set_session_id()` at the top
of every turn before tool dispatch, so the ContextVar's `default=""` never
reaches a tool call.
gsd-read-guard treats any non-empty `data.session_id` as "Claude Code
already enforces read-before-edit, skip" (#2520). So the advisory those
tests assert fires only for a shape production never sends: the tests were
green, and the guard was dormant on Kimi. A sibling #2520 case in the same
file asserts the skip when `session_id` IS present — both passed, and the
production shape hits the skip.
Two changes, test-validity only:
* Retitle the #2304 block to say what it proves — the tool VOCABULARY is
normalized through to the Write/Edit branch — with a comment warning
not to read it as production evidence.
* Add a #2547 block asserting behaviour against the production shape
(session_id populated), including a case that pins the delta directly:
the same payload fires without session_id and is silent with it.
The #2547 block characterizes a known gap; it does not endorse it.
Redesigning how the guard discriminates runtimes is explicitly out of
scope for #2547. If a later change makes the advisory fire on Kimi these
tests are supposed to fail — update them then rather than dropping the
coverage.
Refs #2547
* docs(#2547): scope the Kimi guard-engagement claim to what Kimi enforces
#2518 engaged the guards' Kimi matchers and the release notes describe the
result as "All seven guard hooks now engage on Kimi", singling out the
prompt-injection read scanner as "the security-relevant guard" taken "from
silently dormant to engaged". That is not achievable for the scanner at
the emit layer.
gsd-read-injection-scanner.js is a PostToolUse hook, and kimi-cli's
dispatch never inspects PostToolUse hook results: src/kimi_cli/soul/
toolset.py awaits PreToolUse and honours `result.action == "block"`, but
fires PostToolUse via asyncio.create_task() and returns the ToolResult
without awaiting it — the done_callback only retrieves the task's own
exception. So no output shape the scanner emits can block or flag a Kimi
tool call, and `security.injection_blocking` cannot take effect there.
Reshaping the scanner's output would not change this; the enforcement gap
is in kimi-cli's PostToolUse handling, which is out of scope here.
This corrects the claim rather than the code — there is no gsd-core emit
fix that would make it true:
* .changeset/2304-kimi-guard-tool-name.md — the fragment is unreleased,
so it would otherwise ship this as a CHANGELOG security claim.
Headline narrowed to "normalize Kimi's payload shape" and a scope
paragraph added naming what actually blocks on Kimi (the two
PreToolUse blocks) versus what cannot.
* docs/migration/kimi-to-kimi-code.md — the scanner was listed under
"Every GSD `PreToolUse` guard"; it is PostToolUse. Corrected, and the
"What about the dormant guards?" section now splits enforceable from
not-enforceable instead of saying Phase 0 "fixed all seven".
* hooks/gsd-read-injection-scanner.js — the same scope note in the
file's own Kimi rationale comment, where the next contributor to touch
the normalization will actually read it. Comment only; the shared
normalization block is untouched and byte-identity still holds.
Refs #2547
* chore(#2547): regenerate golden install-parity fixtures for the guard fix
The golden install-parity fixtures record a content hash per installed
file, so changing the five guard hooks changes their hashes across every
runtime's fixture. Regenerated with the full sweep (build, gen:golden,
size:baseline) rather than a single generator — running gen:golden alone
leaves tests/workflow-size-baseline.json stale and loses CI jobs to a
regeneration that looked complete.
The size baselines came out unchanged (no workflow or agent bodies
touched) and the hash delta is confined to exactly the five guards:
gsd-prompt-guard, gsd-read-guard, gsd-read-injection-scanner,
gsd-workflow-guard, gsd-worktree-path-guard.
Refs #2547
* fix(#2547): guard the String() coercion in normalizeKimiPayload too
Found by adversarial review of the first commit, then reproduced against
pristine next: `e?.old` closes the nullish dereference but leaves a second
route to the same crash-to-allow.
`{"toString": null}` is valid JSON, and coercing it throws
`TypeError: Cannot convert object to primitive value` — so an edit entry
that IS a well-formed object still crashes normalization, still lands in
the outer `catch { process.exit(0) }`, and still downgrades a should-BLOCK
call to a silent allow. Confirmed on both hard blocks:
{"tool_name":"Shell","tool_input":{
"command":"git add -f secret.env",
"edit":[{"old":{"toString":null},"new":"x"}]}} -> exit 0 (was)
{"tool_name":"StrReplaceFile","tool_input":{
"path":"<main-repo>/src/index.ts",
"edit":[{"old":{"toString":null},"new":"x"}]}} -> exit 0 (was)
Both exit 2 now.
The coercion is wrapped rather than replaced with a `typeof === 'string'`
test on purpose. Degrading only the non-coercible entry keeps
stringification identical for every value that CAN coerce — numbers,
arrays, plain objects — which matters because gsd-prompt-guard scans
new_string for injection patterns, and a `typeof` test would silently stop
scanning content that reaches that scan today (e.g. `new: ["ignore all
previous instructions"]` currently stringifies and is scanned). Verified:
zero behaviour change across string, number, bool, null, array-of-strings,
nested array, plain object and `__proto__`-keyed input; only the throwing
case changes, from crash to ''.
Regression cases are negative-controlled against the previous commit: the
four new coercion-trap tests fail with only the `e?.old` fix in place and
pass with this one.
Refs #2547
* chore(#2547): cover the String() coercion vector in the changeset
The release note described only the nullish-dereference route. Both routes
reach the same fail-open, so both belong in the changelog entry, along with
why the coercion is wrapped rather than type-tested.
Refs #2547
* chore(#2547): point the changeset fragment at the real PR number
The fragment has to exist before `gh pr create` runs, so it carried the
issue number as a placeholder. Corrected to 2595 now that the PR is open.
Refs #2547
* fix(#2547): make Kimi's `path` authoritative over a model-supplied `file_path`
normalizeKimiPayload copied Kimi's `path` into `file_path` only when
`file_path === undefined`, so any `file_path` the model chose to include won
outright. Every guard reads `file_path`; kimi-cli executes on `path`. The guard
therefore inspected one file while the write landed on another.
This bypass needs no crash. A payload pairing a cross-root `path` with a
spurious `file_path: ""` left gsd-worktree-path-guard reading an empty string
and exiting 0, while the identical write without the extra key blocked — the
same cross-root write the #260 block exists to catch. The shadowing also
preserved a non-string `file_path` (`[]`), which threw inside that guard's
path.isAbsolute() and reached its outer `catch { process.exit(0) }`: the same
crash-to-allow the rest of #2547 closes, reached through the guard's own read
rather than through normalization.
Reachability is not speculative. kimi-cli's soul/toolset.py json-parses the
model's raw tool arguments and passes that dict verbatim as tool_input to
PreToolUse, performing typed validation only later inside tool.call() — after
the hook has already decided. So the model controls extra keys in tool_input at
the moment the guard runs. kimi-cli's file tools carry no `file_path` field at
all (src/kimi_cli/tools/file/write.py, replace.py), so a `file_path` in a Kimi
payload is always model-supplied.
`path` now wins outright. Overwriting can only ever narrow what a guard inspects
to the path that will actually be written, so it cannot under-block.
Normalization returns early for non-Kimi tool names, so the native Claude Code
contract (file_path governs) is untouched.
Landed identically across all five inlined copies; the byte-identity assertion
in tests/kimi-guard-normalization-parity.test.cjs enforces that.
* test(#2547): cover the file_path-shadowing bypass in the #260 guard suite
Four cases, each exiting 0 (bypass) against the pre-fix guards: a spurious
empty-string file_path, an in-worktree decoy file_path, and non-string
file_path values (array and object) that additionally crashed
path.isAbsolute() into the outer catch.
Two controls that are not bypass cases and matter as much:
- an in-worktree write carrying a cross-root DECOY file_path must still exit
0. Pre-fix this blocked, because the decoy won; the guard now follows the
path kimi-cli executes on in both directions, so the fix narrows what is
inspected without over-blocking.
- a native Claude Edit (no `path` field) must still block on file_path alone.
normalizeKimiPayload returns early for non-Kimi tool names, and this pins
that the non-Kimi contract did not move. It passes both pre- and post-fix
by design.
Negative-controlled: run against the pre-fix hooks, the four bypass cases and
the decoy control fail, and the native-Claude control passes.
* test(#2547): back the totality claim with property tests over fc.anything()
This PR claims the fix "makes normalization total over the inputs JSON can
express" — a for-all guarantee — while the tests backing it are example-based,
each shape added reactively after a crash was found by hand (the String()
coercion trap was itself found by adversarial review after the first commit
shipped). Example-based tests cannot substantiate a for-all claim; they record
the counterexamples someone happened to think of.
Four properties over fc.anything(), which is exactly the JSON-expressible
domain the claim names:
(a) totality over any tool_input
(b) totality over any edit list — the crash surface both #2547 fixes targeted
(c) `path` always wins over any model-supplied `file_path` (the review blocker
invariant: a guard reading file_path can never be aimed at a file other
than the one kimi-cli writes)
(d) a non-Kimi tool_name passes through untouched — the native Claude contract
normalizeKimiPayload is inlined per hook with no runtime binding, so there is
nothing to require. The block is extracted from hook source and evaluated via
the SAME extraction contract kimi-guard-normalization-parity.test.cjs uses, so
a source edit that breaks one breaks both instead of silently testing a stale
block. An extraction floor test fails loudly if the extraction yields a no-op.
Non-vacuous, and checked rather than assumed: against pristine pre-#2547 `next`,
(a), (b) and (c) all FAIL and (d) passes. (a) needed the fix that makes it
meaningful — a bare fc.anything() for tool_input passed even against the live
defect, because arbitrary generation essentially never invents the `edit` key
the crash lives behind, so the generator is biased onto the keys normalization
actually reads and unioned back with unbiased input.
* chore(#2547): cover the shadowing vector in the changeset and regen goldens
Golden install-parity churn is hash-only, on exactly the five hook files this
round changed. gsd-phase-boundary.sh is deliberately unchanged.
* test(#2547): make the property test able to kill the coercion mutant
Review Major 1: the generative test added to stop the NEXT counterexample
could not kill the one it was written for. Reproduced the reviewer's matrix
independently — against the shipped generator, a mutant reverting `editText`
to the unguarded `String(v ?? '')` passed all four properties.
Cause, confirmed by measurement: the edit-array ENTRIES were bare
`fc.anything()`, which essentially never invents an `old`/`new` key, so
`e?.old` was always undefined and `String(undefined ?? '')` never coerced
anything. That is the same vacuity the file's own comment describes one level
up, reproduced one level down.
The review's prescribed fix — bias the entry onto `{old, new}` — is necessary
but NOT sufficient, and this is the part worth recording: measured over 20,000
draws, bare `fc.anything()` yields a non-coercible value 3 times (0.015%). At
`numRuns: 200` an `old` key holding a hostile value essentially never
co-occurs, and the mutant survives the entry bias too. Both levels need
biasing — the entry onto the keys normalization reads, and the VALUE onto the
shape that actually throws.
`{"toString": <non-function>}` is that shape and stays inside the
"JSON-expressible" domain the claim names (JSON.parse produces it verbatim);
`fc.anything({withNullPrototype: true})` would also kill the mutant but widens
the domain past what the PR asserts, so it is not used.
Verified: M1 now dies at every seed tried (1/7/42/99/4242/31337, failing
within 3-31 cases) while HEAD stays green at all of them.
Also closes three coverage gaps the review listed as nits — properties (e)
totality over any JSON value as the WHOLE payload, (f) the tool_output →
tool_response mapping (including that an existing tool_response is not
clobbered), and (g) an empty edit list reconstructing nothing.
Property (e) required a one-line fix in the normalizer itself: `JSON.parse
('null')` is null, and null/primitive payloads threw on the `data.tool_name`
read — falsifying the "total over the inputs JSON can express" claim. Harmless
in practice (the throw landed in the same fail-open catch as the exit 0 it now
takes deliberately), but the claim should be true as stated. Landed
byte-identically across all five copies.
Refs #2547
* fix(#2547): make old_string/new_string authoritative, not merely defaulted
Review Major 2: the shadowing class the prior round's BLOCKER closed for
`file_path` survived one field over. `old_string`/`new_string` were still
reconstructed only `if (input.<field> === undefined)`, so a model-supplied
value won.
The argument for making `path` authoritative applies verbatim here.
kimi-cli's StrReplaceFile schema is `path` + `edit` only
(src/kimi_cli/tools/file/replace.py @ 4a550ef) and carries no
`old_string`/`new_string` at all, so either key appearing in a Kimi payload is
always model-supplied — exactly like `file_path`.
Verified end-to-end against the reviewer's payload: a cross-root write
carrying `new_string: ""` alongside an injected `edit[].new` left
gsd-prompt-guard reading '' and returning at its `if (!content)` guard, so the
injection advisory never fired and the reconstructed content was never
scanned. `new_string: null` behaved identically. Negative-controlled: both
produce empty output against pre-fix source and fire the advisory after.
Chose unconditional reconstruction over the offered `typeof` alternative
deliberately. A type test closes `""`/`null` but leaves the interesting case
open — a benign NON-EMPTY decoy (`new_string: "chore: tidy"`) shadows just as
effectively and passes any type test. The new suite includes that case
specifically; it is what discriminates between the two candidate fixes.
Also pins the kimi-cli SHA in the authoritative-path comment, as requested —
it cited file names with no version while the issue pins 4a550ef.
Landed byte-identically across all five inlined copies; the parity test's
byte-identity assertion holds.
Refs #2547
* fix(#2547): close the non-string file_path crash-to-allow at every read site
Review Major 3: the crash-to-allow was closed only as a side effect of `path`
masking the bad value, while the changeset read as though it were closed
outright. Confirmed both of the review's reachability claims: `[]`/`{}`/`42`
are truthy, survive the `!rawFilePath` early-out, and throw inside
path.isAbsolute() into the outer `catch { process.exit(0) }`; and normalization
returns early for native Claude Code payloads (KIMI_TOOL_NAMES has no 'Edit'
entry), so `{"tool_name":"Edit","tool_input":{"file_path":[]}}` reached it
untouched — this guard's original #260 surface.
Reproduced on a real fixture: string cross-root path exits 2, the identical
payload with `[]` or `{}` exits 0.
Swept the class rather than the instance. Five more untyped read sites across
four other hooks, each one line from a type-strict or method-dependent call.
Census of what each can actually do:
gsd-worktree-path-guard.js:173 BLOCKS -> live bypass (the review's finding)
gsd-prompt-guard.js:128 scanner -> silenced the injection scan, the
same outcome as Major 2 by another
route; verified empirically
gsd-workflow-guard.js:206 advisory only (its exit-2 is the Bash
force-add path, which reads
`command`, not `file_path`)
gsd-read-guard.js:141 advisory only
gsd-read-injection-scanner.js:213 advisory only
gsd-windsurf-pre-write.js:75 ALREADY TYPED — the shape adopted here
All six now read typed. The workflow-guard site keeps its truthiness fallback
(`(typeof x === 'string' && x) || ...`) because a bare type test would let an
empty `file_path` shortcut the `path` fallback.
Also declares one swept hit NOT fixed: `gsd-workflow-guard.js:175` reads
`command` untyped on a genuinely blocking path. Same shape, but not
exploitable — unlike file_path/path there is no second field carrying the
executable value, so a non-string command cannot smuggle a real `git add -f`
past the block. Left alone rather than widen this PR into the Bash path.
The regression gate is a SOURCE-level invariant, not a behavioural one, and
that is deliberate: the fixed read and the crashing read are black-box
identical — both end at exit 0, one via the catch and one via the early-out.
A test asserting exit 0 on a non-string payload passes against the unfixed
code, which is the same false-green the review flagged in the existing
`['non-string file_path (array)', []]` cases. Repeating it one level up would
be no better. tests/kimi-guard-typed-payload-reads.test.cjs fails if any hook
regresses to an untyped read (negative-controlled: it reports all five
pre-fix sites with correct file:line).
The behavioural cases requested — non-string file_path with NO `path` key —
are added to worktree-safety.test.cjs and labelled honestly as documenting the
explicit fail-open rather than detecting a revert.
Also states the relative-path premise (review Minor 5) at the early-out that
depends on it: "always safe" holds only while every runtime reaching there
resolves relative paths against the tool CWD. Claude Code satisfies it by
requiring absolute paths; kimi-cli's resolution behaviour is NOT verified here
and is recorded as an unverified premise rather than an asserted bypass.
Refs #2547
* docs(#2547): correct the changeset's closed-claim and fold the misattributed note
Review Major 3 also flagged the fragment: it said the non-string vector "threw
inside that guard's path.isAbsolute() ... `path` now wins outright", which
reads as closed when it was closed only conditionally. Rewritten to state what
is now true — closed unconditionally at all six read sites — and extended with
the Major 2 finding.
Review Minor 6 (the #2547 scope note living in a `pr: 2518` fragment) turns out
to understate the problem. Rendering the changelog and re-parsing it shows the
note is not merely misattributed — it is DROPPED. serializeChangelog emits each
fragment as a single `- ` bullet, and parseChangelog terminates a bullet at the
first non-continuation line, so everything after a blank line is lost on
re-parse. Audited all 44 fragments: exactly one was lossy —
2304-kimi-guard-tool-name.md, losing 656 of 1730 characters, i.e. precisely
that second paragraph. Folding it into this PR's fragment fixes the
attribution and the silent loss together; all 44 now round-trip losslessly.
That same mechanism is why the remaining nit — reformat this fragment's
~2,000-character paragraph for readability — is NOT applied. A paragraph break
or a bullet list would silently truncate the entry at the first blank line
(verified for both). The single-paragraph form is load-bearing under the
current serializer, not an authoring preference. Worth its own issue; noted in
the PR thread rather than worked around here.
Refs #2547
* chore(#2547): regenerate golden install parity after rebase onto next
Rebased onto `next` @
|
||
|
|
7e8f6a6d7d |
enhance(#2572): run the verify-summary artifact check against phase SUMMARYs (#2685)
* enhance(#2572): run the verify-summary artifact check against phase SUMMARYs (W025) The artifact<->git check has existed since the beginning but was only ever pointed at .planning/research/SUMMARY.md (new-project.md:1145, new-milestone.md:425). Phase summaries -- the ones that actually claim "I created these files" -- were never checked. - extract verifySummaryCore from cmdVerifySummary: same checks, lifted out of the output() wrapper so callers consume {passed, checks, errors} directly instead of shelling out and re-parsing JSON; cmdVerifySummary is now a thin adapter over it - validate.health gains advisory W025 per phase SUMMARY with missing files Advisory only: appends to warnings[], never touches status escalation beyond the channel's own warning semantics, the repair set, or readVerificationStatus. Resolves both open questions from triage: (a) commits_exist is deliberately NOT surfaced -- its hash pattern matches any hex-shaped token in prose, too loose to show a user; (b) a phase carries N per-plan summaries plus a legacy bare SUMMARY.md, so all of them are checked via the repo-wide filter. * chore(#2572): add changeset fragment * feat(#2572): move the SUMMARY artifact check to phase completion Responds to the #2685 review. Three substantive changes. Seam (Blocker 2). The check now runs in cmdPhaseComplete, the seam the issue body cited (src/phase.cts:~1745), not validate.health. That channel does exist: cmdPhaseComplete declares warnings[], populates it from the UAT/VERIFICATION pre-scan, and emits it. The cycle objection raised against the earlier deviation holds for state.cts only -- verify.cts has no transitive import path to phase.cts, so phase.cts -> verify.cjs adds no cycle (verified over every src/*.cts). Moving it also retires the retroactive firing across all historical phases: this fires once, at completion, for the phase being completed. Extraction (Blocker 1). Pattern 2 now excludes [ and ] from its path class. The SUMMARY templates prescribe a YAML flow sequence (key-files.created: [a.ts, b.ts]) and the label matches case-insensitively, so the class previously captured the literal [ and produced a candidate that can never exist on disk -- firing on healthy projects built from the templates GSD itself ships. Stripping frontmatter was the other offered remedy; measured across all three shipped templates it is a no-op on top of the exclusion, so it is not carried. Consequence named in-code: the key-files block still is not read, which needs a real frontmatter parse. Also narrowed to the noise classes confirmed in review -- globs, bare hostnames, and paths resolving outside the project are skipped rather than reported, and the containment guard the old comment claimed now actually exists. Budget (Majors 1 and 3). verifySummaryCore takes a checkCommits option; phase completion passes false, so the discarded git cat-file probes are not spawned at all. It also passes Infinity, so every referenced file is reported instead of the first two -- a summary listing twelve files of which nine are missing now says nine, not zero. The verb keeps its historical 2-file default. Tests (Major 2). The vacuous fixtures are gone with the health block. The replacements use /-bearing paths that genuinely extract, and each fix was mutation-checked: un-anchoring pattern 2, dropping the glob, hostname or containment filter, forcing commit checking on, and re-capping at 2 each fail at least one test. * docs(#2572): describe the phase-completion SUMMARY artifact check The W025 text under /gsd-health is withdrawn with the health seam; the check is documented where it now runs, under `phase complete` in docs/CLI-TOOLS.md. Both the docs and the changeset previously overclaimed: they said a referenced file not on disk is warned about, while at most two candidates per SUMMARY were ever examined. The cap is gone at this seam, so the claim now holds -- and the text states the limits that remain, rather than leaving them to be discovered: the key-files frontmatter block is not read, commit hashes are not resolved, and globs, URLs, bare hostnames and out-of-project paths are skipped rather than reported. --------- Co-authored-by: CI Rebase Check <ci@gsd-redux> |
||
|
|
6932fb16d7 |
enhance(#1699): require read-and-cite provenance for in-repo discrete values (#2768)
* test(#1699): failing-first contract tests for in-repo value provenance
Nine assertions on the deployed gsd-phase-researcher contract: the discrete-value taxonomy, the same-session Read requirement, path-AND-line-range citation, grep-alone exclusion, the verbatim quote and paraphrase ban, the quote-is-the-checkable-artifact guard, [ASSUMED] routing for unquoted skeleton values, a no-regression guard on the pre-existing package name provenance rule, and a single-definition-site guard.
Eight of the nine fail against the unmodified agent on origin/next; the ninth is the no-regression invariant and passes in both states, which is the intended enhancement shape.
Added to tests/research-agent-profiles.test.cjs rather than a new file: that file already carries the allow-test-rule exemption <runtime-contract-is-the-product> research agent .md content is the governed surface, so no new allowlist entry and no change to the per-module test-file count.
* enhance(#1699): require read-and-cite provenance for in-repo discrete values
The claim-provenance system governed external facts (npm registry, official docs, Context7, package-name provenance). For an in-repo discrete value -- an enum, schema or type union, error code, status constant, or filesystem path -- [VERIFIED] could be earned from training memory or a bare codebase grep, which proves a string occurs, not that the definition was read.
A drifted value passes into RESEARCH.md, is lifted by the planner into PLAN.md's <interfaces> context block, and is trusted by the executor, where it fails at parse()/typecheck as a mid-execution deviation -- the most expensive place to discover it.
The rule lands beside its structural sibling, the package name provenance rule, since both say existence is not verification. The verbatim quote is named as the load-bearing artifact: a citation with no quote does not earn the tag, however precise the line range looks. That keeps the rule falsifiable against the file rather than a self-report, which is the Goodhart guard.
Scope note: the quote goes in RESEARCH.md beside the claim, NOT in an <interfaces> block. <interfaces> appears zero times in this agent on next -- it is planner-side, defined at gsd-core/references/planner-interface-context.md:15 as a PLAN.md structure. The issue text and triage both said <interfaces>; instructing the researcher to populate a block it does not emit would be undefined.
Defined once at the definition site; the source-hierarchy recap is deliberately untouched, since restating it is the paraphrase-drift mode META.RULE.brief-no-paraphrase names.
* test(#1699): regenerate agent size baseline and golden parity fixtures
Regenerated via npm run size:baseline and npm run gen:golden, never hand-edited. agent-size-baseline gsd-phase-researcher.md 40866 to 42020 (LARGE tier, cap 49152, 7132 bytes headroom remaining, per ADR-1610's per-file baseline guard). 18 of 19 golden fixtures updated; pi.json is unchanged because the pi runtime ships zero agents.
* chore(#1699): add changeset for the in-repo value citation rule
User-facing behavior change in gsd-phase-researcher, so a Changed fragment is required. pr: 2768.
* test(#1699): acknowledge the gsd-phase-researcher growth for the emitted-drift gate
CI test (ubuntu-latest, 22) failed on emitted-attribution.test.cjs: gsd-phase-researcher.md grew 1154 bytes (40866 -> 42020) without an acknowledgment. The differential emitted-attribution gate landed on next in
|
||
|
|
e276cc7f00 |
enhance(#2778): make the size-ratchet failure name its own remedy (#2780)
* fix(#2778): exempt intentionally-absent paths from the glossary gate check-glossary-refs asserts that every backticked tests/ token in CONTEXT.md resolves on disk. tests/emitted-drift-ack.json (ADR-2719 section 3) is absent on a healthy next BY DESIGN — it appears only inside a PR that needs it, which is what makes touching it the alarm. It passed before only by accident of backtick pairing: CONTEXT.md's RULESET entries are themselves backtick-wrapped and contain backticks, so the token happened to fall outside a code span. Any edit that shifted the parity exposed it. A gate that passes by luck is not passing. The exemption is exact, not a prefix hole: a sibling missing tests/ path still fails, and a test locks that. * feat(#2778): make the size-ratchet failure name its own remedy The growth branch stated a requirement and withheld the means of satisfying it: no ack file named, no schema, no key format, and no do-not-regenerate line — so the likeliest guess was to hunt for a baseline that #2724 deleted. Observed live on #2543. All remediation now comes from one frozen REMEDIATION export whose example document is rendered from ACK_VERSION, so the taught schema cannot drift from the schema parseAck accepts. A round-trip test feeds the printed document back through parseAck. The report is now built as a typed IR (buildReport) that formatReport renders, so tests assert on structure rather than prose, per CONTRIBUTING.md's raw-text-matching rule. Two defects found and fixed inline while building: - diffEmitted's validation early-return omitted newFileCapExceeded while formatReport reads its length, so the branch that reports a failed git diff threw a TypeError instead of naming the problem. - Printing one complete ack document per failing branch made each read as the whole file, so pasting the second over the first silently lost an acknowledgment. One document now covers the whole report. Closes #2778 * chore(#2778): backfill changeset pr number to 2780 |
||
|
|
0997d4f443 |
fix(#2620): inject the reference DispatchLogger on the live dispatch seam when observability is enabled (#2621)
* fix(#2620): inject the reference DispatchLogger on the live dispatch seam when observability is enabled The Command Routing Hub defaulted to createNoOpLogger and no caller ever injected createDefaultLogger, so GSD_AUDIT=1 wrote nothing and failed dispatches emitted no structured JSON to stderr — contradicting ADR-0174 §5/§6, CONTEXT.md's Dispatch Observability Module contract, and docs/CONFIGURATION.md. Inject the reference logger at both live createHub() sites, gated on the existing opt-in signal (newly exported isAuditEnabled). When observability is off no logger is injected, so the Hub keeps its no-op fallback and default output stays byte-for-byte identical. Enabling stderr-on-error unconditionally adds a second line to the --json-errors envelope that callers parse as exactly one JSON line, so that is deferred to its own increment under #2619. * chore(#2620): add changeset for the dispatch logger wiring fix * test(#2620): cover the phase seam and drop try/finally from the adapter test Two review findings from the #2621 round-1 review. The fix wires the logger at BOTH live createHub() seams, but only cjs-command-router-adapter was exercised. Adds a fail-first regression test for src/phase-command-router.cts:258 — verified RED against a tree with that hunk reverted (1 fail, exact assertion) and GREEN with it restored — plus a negative pin that no trace file appears when GSD_AUDIT is unset. The negative case passes pre-fix and is a pin, not fail-first. CONTRIBUTING.md:344 forbids try/finally inside test bodies; the new adapter test used it. Converted to the Pattern-2 t.after() form, switched to the centralized createTempDir helper, and removed the now-unused os require. * chore(#2620): scope the changeset to the activation path that actually ships The fragment claimed config.audit.enabled activates the audit trail. It cannot: both seams call isAuditEnabled() with zero arguments, so the config branch in _isAuditEnabled is unreachable from production, and src/config-schema.cts registers no audit key at all — a user setting it would be silently dropped. That string ships in the user-facing CHANGELOG. Scoped to GSD_AUDIT=1, which is what actually works. The missing schema key stays a disclosed deferred sub-defect on #2620. Also adds the (#2620) issue backlink the other fragments carry. * docs(#2620): correct the fork-leaked issue reference in the wiring comments Four files cited this fix as #26, the issue number from the fork where the change was first written. Upstream #26 is an unrelated closed SDK issue, and next already uses #26 with that meaning in src/validate.cts:17,29,42 and src/config.cts:474, so these references pointed somewhere real and wrong rather than merely dangling. Baked into permanent doc comments, they reach users compiled via the ADR-457 build-at-publish path. The changeset and tests/phase-command-router.test.cjs already cited #2620; this brings the remaining four files into line. Comment-only, no behaviour change. build:lib produces no generated drift. The rename is scoped to these four files so the pre-existing SDK #26 references in validate.cts, config.cts, health-validation.test.cjs and config.test.cjs are deliberately left untouched. --------- Co-authored-by: CI Rebase Check <ci@gsd-redux> |
||
|
|
16e59d0db5 |
fix(#2691): repair seven dangling references in the ADR corpus and contributor docs (#2692)
* fix(#2691): repair five dangling references in the ADR corpus and contributor docs
Found by the 2026-07-24 ADR corpus audit; each mechanism re-reproduced live
against next @
|
||
|
|
3274db2757 |
fix(#2526): remove gsd-ui-auditor's uncallable Playwright-MCP block (#2594)
* fix(#2526): drop gsd-ui-auditor's uncallable Playwright-MCP block The agent declares `tools: Read, Write, Bash, Grep, Glob, Skill` — no `mcp__*` grant of any kind — while its body presented a `<playwright_mcp_approach>` block as the *preferred* capture path. That branch was unreachable by construction: the availability check had a fixed answer, the three `mcp__playwright__*` calls could never dispatch, and the "when Playwright-MCP is NOT available" fallback was the only branch that ever ran — 39 lines of instruction loaded on every /gsd-ui-review spawn that also invited the model to claim a capture path it could not take. Remove the dead block, leaving the CLI screenshot path as the sole documented approach. Guard the class in tests/mcp-tool-inheritance.test.cjs, which already owns agent MCP-grant parity: the new block generalizes the #1284 researcher check from two agents and one dispatch table to every agents/*.md and its whole body — no agent may document an `mcp__<server>__*` namespace absent from its own `tools:` declaration. Frontmatter is read through the canonical parser (gsd-core/bin/lib/frontmatter.cjs) rather than a hand-rolled scan, so inline CSV, block sequences, flow arrays, quoted scalars and full-line comments are handled by construction; inline comments inside a scalar survive that parser, so they are stripped explicitly. The check is server-level by design, ignores prose metavariables like `mcp__X__*`, matches hyphenated server ids, and carries a discovery guard plus negative controls for every documented boundary so it cannot decay into a vacuous pass. The session-level Playwright-MCP pass in gsd-core/workflows/ui-review.md is deliberately untouched — workflow files carry no fixed allowlist, so their availability check is genuinely runtime-detected and honest. Fixes #2526 * chore(#2526): set changeset fragment pr to 2594 The fragment shipped with the documented `pr: 0` placeholder because the PR number does not exist until the PR is opened, and scripts/changeset/parse.cjs rejects `pr <= 0`. Now that the PR is open, set the real number so changeset-lint passes. * test(#2526): cover the two-char server-id boundary of the metavariable exclusion The length-1 "prose metavariable" exclusion was tested at length=1 and at real ids (>=3 chars), but never at length=2 — the limit+1 boundary where a server id starts being recognized. Review finding on #2594: `mcp__ab__foo` in a body with no grant must flag `['ab']`. * fix(#2526): treat a bare mcp__* grant as covering every server `grantedServers()` stripped `mcp__*` to the empty string and dropped it via `if (server)`, so an allowlist that grants every MCP server read as granting none — and the guard then fired against a body the grant plainly covered. That is the one input shape that inverts the check, turning it against a correct agent rather than merely missing a bad one. A `/^mcp__\*+$/` token now sets a GRANT_ALL sentinel that short-circuits `ungrantedServers()`. The sentinel `*` is outside REFERENCE_RE's character class, so no body reference can collide with it. A bare `mcp__` with no wildcard stays a typo rather than a grant and keeps failing closed. No agent uses the `mcp__*` spelling today, so this was latent rather than live. Two negative controls pin both halves. * fix(#2526): scan the frontmatter description for MCP references too `ungrantedServers()` scanned `stripFrontmatter(content)` only, so an `mcp__foo__bar` reference in the `description` field escaped the check. That field ships with the agent and the dispatcher reads it, which makes a dead reference there exactly as dead as one in the body. Only `description` is added to the scanned surface, never the whole frontmatter: `tools:` is the grant list itself, so scanning it would let every allowlist satisfy itself and turn the guard vacuous. A negative control pins that boundary alongside the new positive case. All 34 per-agent tests still pass with the wider surface, so no live agent verdict changes — this was latent. * test(#2526): give multi-character placeholders a convention the checker knows The metavariable exclusion is `length === 1`, so the natural placeholders `mcp__SRV__*` and `mcp__SERVER__*` were flagged as real references — and the failure message then offered an author two remedies ("grant the namespace or drop the block") that both misread what they wrote. Adopts the angle-bracket half of the suggested fix: `mcp__<SERVER>__*` is the sanctioned multi-character placeholder, exempt by construction because `<` is outside the reference pattern's character class. This pins an existing property rather than adding a special case. Declines the all-caps half. An all-caps exemption would be a false NEGATIVE for any real server spelled in caps, and a guard that misses a dead reference fails in exactly the direction this check exists to prevent. The bare-caps form keeps firing; the message now names the convention as a third remedy. Also corrects "grants neither" in that message, which was wrong for any count other than two. * test(#2526): pin the zero-length server id, completing the boundary triple `mcp____foo` yields `[]`, but for a different reason than the length-1 case: it is unrepresentable by `/mcp__([A-Za-z0-9_-]+?)__/g` since `+?` requires at least one character, so the pattern skips it before the metavariable exclusion is ever consulted. Pinning limit-1 completes the 0/1/2 boundary rule on its own terms and records which mechanism owns the case. * docs(#2526): correct every drifted AGENTS.md Tools row, not just the one The review asked for the one-line `gsd-ui-auditor` correction (missing `Skill`). Sweeping the defect class first — every `**Tools**` row in docs/AGENTS.md against its agent's `tools:` frontmatter — found it was 26 of 34 rows, so the one-line framing was the reviewer's premise rather than the population. Breakdown of the 26: * 21 omitted `Skill`, 6 omitted `Edit` (overlapping) — under-promises, the same drift class as #2526 but in the harmless direction. * 8 wrote `mcp (context7)` as shorthand while frontmatter granted up to 8 servers (firecrawl, exa, tavily, ref, jina, perplexity, both context7s). * 1 was actively wrong: gsd-debug-session-manager documented `Task`, a tool name that no longer exists — the #2526 shape at the doc layer, naming a capability that cannot dispatch. Every row is now the frontmatter `tools:` value verbatim, which is also what makes the parity guard in the following commit non-brittle. The diff is 26 insertions / 26 deletions, all Tools rows. * test(#2526): guard AGENTS.md Tools rows against agent frontmatter The 26 corrected rows in the previous commit were free to drift because nothing asserted the role card and the frontmatter agreed — the same reason the #2526 block itself survived. Correcting them without an invariant just resets the clock. Lands in agent-classification-parity.test.cjs rather than a new file: that suite already owns docs/AGENTS.md as a contract surface, already carries the `allow-test-rule` exemption for treating the doc as the product, and file count is the unit of CI overhead (docs/TESTING-SUITES.md). Compares the row to the frontmatter value VERBATIM, not as a set — a set comparison would keep accepting the "mcp (context7)" shorthand that hid eight grants behind one, which is the under-documentation half of the drift. Carries the same discovery guard #2526's own check uses: a section with a granted `tools:` but no **Tools** row fails loudly, so deleting a row cannot silently retire its assertion. Both halves are negative-controlled — against the pre-fix doc it fails naming 26 rows (gsd-ui-auditor:339 among them), and with a row deleted it fails on the missing-row assertion. * chore(#2526): note the AGENTS.md drift correction in the changeset The role cards are user-visible, and 26 of them documented a tool set the agent did not have. Type, `pr: 2594`, and the trailing `(#2526)` are unchanged. * fix(#2526): use a CRLF-safe split in the AGENTS.md Tools-row guard `lint-tests` (npm run lint:ci) rejected `rawAgentsMd.split('\n')` under the repo's local/no-crlf-fragile-split rule: Windows autocrlf yields CRLF, so a trailing \r rides into the parsed line. Switched to `/\r?\n/`. Caught by CI on the round-3 push before the response comment went out. * docs(#2526): correct the drift tallies stated in c5a9607e Re-derived the census programmatically from the pre-fix doc instead of by eye. The 26-of-34 headline was right; the breakdown was not. Skill omitted 21 -> 22 Edit omitted 6 -> 7 "mcp (context7)" shorthand 8 rows -> 7 rows The 8 was conflating two things: 8 rows omitted MCP grants entirely, but only 7 of them used the "mcp (context7)" shorthand — gsd-executor listed no MCP at all. Also names the one `Agent` omission (gsd-debug-session-manager, the row that still read `Task`). c5a9607e's message keeps the wrong numbers rather than rewriting a pushed branch mid-review; the test comment and changeset are the durable statements and both are corrected here. --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
1c1af70a4b |
refactor(#2724): delete the committed golden fixtures and size baselines (#2767)
* test(#2724): delete golden-install-parity fixtures, test, and generator Removes the 19 committed path->hash manifests, the two per-file size baselines, tests/golden-install-parity.test.cjs, and scripts/gen-golden-install-parity-zcode.cjs. These were pure functions of the source tree (ADR-2719); the differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the sole gate for emitted-artifact propagation. tests/fixtures/install-tree/*.json and tests/golden-install-tree.test.cjs are unchanged (ADR-2719 section 7 exception). Follow-up commits fix the resulting bookkeeping: scripts/ci-test-scope.cjs's existence guard, .gitattributes, package.json scripts, the emitted-provenance totality guard's IO, the differential check's baseline acquisition, CI wiring to publish/restore the baseline artifact, and docs. * refactor(#2724): make the differential attribution check self-sufficient Three fixes required to delete the golden fixtures without breaking CI: - scripts/ci-test-scope.cjs: remove tests/golden-install-parity.test.cjs from the three rules that named it. #2759's missingRuleTestFiles guard hard-throws at module load if a rule names a test file absent from disk, which would break the changes job on every PR the moment the fixture-deletion commit landed. - tests/helpers/emitted-provenance.cjs: loadManifests() read the committed golden fixture directory. With that directory deleted at every future ref, this would throw at module load forever, taking the Phase 2 totality guard down with it. Rebuilt from real installer spawns (MANIFEST_FAMILIES + runMinimalInstall + buildParityManifest), the same shape emitted-runtime.cjs's currentManifests() already uses. - tests/emitted-attribution.test.cjs / tests/helpers/emitted-runtime.cjs: the real-tree test's baseline acquisition swaps from baselineManifestsAtRef(base) (git show at a ref that no longer carries fixtures) to resolveBaseline()'s documented precedence: env, then the on-disk cache, then an in-job build. The build fallback (buildBaselineAtRef, new) checks out base into a throwaway git worktree and runs the new scripts/gen-emitted-baseline.cjs there -- no npm ci needed, since bin/install.js and the test helper shells are Node-builtins-only. That script also publishes the baseline artifact from CI's push-to-next job (wired in a follow-up commit). * refactor(#2724): retire the merge-driver bridge and per-file size baselines The Phase 1 bridge (#2721) is retired now that the artifacts it guarded are deleted: scripts/git-merge-regen-driver.cjs, its test, and the 'setup:merge-driver' npm script are removed, and the .gitattributes merge=gsd-regen/linguist-generated block for the three deleted-path globs is dropped. tests/fixtures/install-tree/*.json keeps its normal merge behavior, unchanged (ADR-2719 section 7). scripts/update-size-baseline.cjs and its test are removed: their sole purpose was regenerating tests/workflow-size-baseline.json and tests/agent-size-baseline.json, both deleted. The 'size:baseline' npm script and its step in 'regen:derived' go with it. The per-file baseline describe blocks in tests/workflow-size-budget.test.cjs and tests/agent-size-budget.test.cjs are removed for the same reason; the independent loose-tier hard caps are untouched. The differential attribution check's size ratchet (tests/emitted-diff.cjs, already shipped in #2723) is the replacement anti-creep mechanism. 'npm run gen:golden' is replaced by 'npm run gen:install-tree', which keeps regenerating tests/fixtures/install-tree/*.json (the one artifact family ADR-2719 section 7 keeps committed); tests/golden-install-tree.test.cjs's error messages point at the new command name. tests/golden-parity-single-source.test.cjs's anti-divergence guard (#2266) is retargeted from the two deleted golden-parity consumers to their two replacements (tests/helpers/emitted-runtime.cjs and tests/helpers/emitted-provenance.cjs), which import buildParityManifest the same way — the divergence risk the guard exists for is unchanged. Also wires CI: a new publish-emitted-baseline job runs scripts/gen-emitted-baseline.cjs after a push to next and caches the result keyed on the sha; the test and test-full jobs restore that cache on pull_request events, keyed on the PR's base sha, and export GSD_EMITTED_BASELINE for tests/emitted-attribution.test.cjs's real-tree test to pick up. * docs(#2724): flip ADR-2719 to Accepted and update contributor docs Status: Proposed -> Accepted. Regenerated docs/adr/README.md index. CONTRIBUTING.md, docs/TESTING-SUITES.md, and CONTEXT.md (RULESET. EMITTED_ATTRIBUTION, RULESET.WORKFLOW_SIZE_BUDGET, RULESET. AGENT_SIZE_BUDGET, and the Emitted Artifact Provenance glossary entry) no longer point at the deleted golden-install-parity fixtures, size baselines, gen:golden, UPDATE_GOLDEN, or the setup:merge-driver / git-merge-regen-driver.cjs bridge. Editing shipped content now requires zero manual fixture regeneration, documented against the differential attribution check instead of the deleted commands. * docs(#2724): add changeset for removed golden-parity commands * fix(#2724): drop stale scripts/update-size-baseline.cjs glossary ref check-glossary-refs.cjs verifies every backtick-wrapped scripts/*.cjs token in CONTEXT.md resolves to a real file. The RULESET. EMITTED_ATTRIBUTION rewrite named the deleted script inside backticks, which the checker reads as a live reference, not historical prose. * test(#2724): retarget ci-test-scope tests off the deleted golden test tests/ci-test-scope.test.cjs asserted specific RULES entries select tests/golden-install-parity.test.cjs, and that every rule selecting it also selects both emitted gates. Both premises broke when the golden test was deleted (#2724): the deleted filename never re-appears in targeted_tests, and there was no longer a third file for the gates to travel alongside. Retargeted the two selection describe blocks to assert tests/emitted-provenance.test.cjs directly (the drift guard the golden gate's rules were retargeted to), and simplified the third block to assert the two emitted gates always travel together, without reference to the golden filename. * docs(#2724): repoint two contributor how-to guides at the differential check Both guides told contributors to regenerate a baseline against tests/golden-install-parity.test.cjs, which #2724 deletes. Repointed at the differential attribution check (tests/emitted-attribution.test.cjs, ADR-2719), which needs no manual regeneration step. * fix(#2724): repair phase6-capstone-conformance's deleted-baseline read An independent orthogonal review caught a real regression this branch introduced into a test file the branch's diff never touched: tests/phase6-capstone-conformance.test.cjs read tests/workflow-size-baseline.json (deleted earlier in this branch) with no fallback, so the whole suite would throw ENOENT the moment this branch landed. The test's actual intent — prove the host-loop workflow files are real, tracked, non-empty docs — is preserved by asserting the live byte count via the same shared counter (scripts/workflow-size.cjs) the size guards already use, instead of a committed snapshot. Also, from the same review: a stale doc comment in scripts/workflow-size.cjs still named the deleted scripts/update-size-baseline.cjs as a consumer, and buildBaselineAtRef's cleanup in tests/helpers/emitted-runtime.cjs left two fs.rmSync calls unguarded against masking the primary result/error, inconsistent with the try/catch already wrapping the git cleanup beside them. Both fixed. A doc comment was added to baselineFamilyNamesAtRef explaining why it (and its siblings) are kept despite having no production caller post-cutover — they still answer real questions about refs that predate the cutover. * fix(#2724): repair three real regressions found by remote verification 1. tests/emitted-provenance.test.cjs's two hostile-input tests (non-object manifest, unreadable fixture) drove loadManifests(tmp) and monkeypatched fs.readFileSync, both premised on the deleted fixture-directory read this branch already replaced with real installer spawns -- the negative assertions silently stopped firing. loadManifests() now accepts injected {families, install, build, clean} (defaulting to production values), giving the tests a real seam to drive a bad build result and a build failure through the ACTUAL loader instead of a reimplementation, and added coverage that clean() still runs on both paths. 2. .github/workflows/test.yml's two 'Export GSD_EMITTED_BASELINE' steps hardcoded shell: bash, which is wrong on windows-latest (native pwsh) and on test-full's macos-latest legs (native zsh per that job's own matrix) -- the repo's H1 shell policy (tests/policy-shell-pinning .test.cjs) caught it. Replaced the inline bash script with scripts/ci-export-emitted-baseline-env.cjs, a plain Node script: a bare 'node <path>' command line has no shell-specific syntax, so it runs correctly under bash, zsh, and pwsh without a shell override. tests/phase6-capstone-conformance.test.cjs's deleted-baseline read (caught by the same remote run, at a commit prior to this one) was already fixed in d0c3b1242 and is not touched here; verified still passing after these changes. * fix(#2724): revive ADR-1610's new-file size cap inside the differential An isolated review caught a real regression: deleting tests/workflow-size-baseline.json silently dropped NEW_FILE_CAP (ADR-1610 Decision point 3, the Codex project_doc_max_bytes anchor) with no successor. tests/helpers/emitted-diff.cjs's size ratchet already 'continue's past any file absent from sizeBaseline -- exactly the files this cap exists to bound -- so a brand-new workflow file sized 32,769-40,960 bytes passed CI clean and shipped, then risked silent truncation at the Codex anchor at runtime. ADR-1610 is Accepted and never referenced anywhere in this branch. Fix: NEW_FILE_CAP=32768 revived inside emitted-diff.cjs's own size-ratchet loop, keyed off the SAME hasOwnProperty(sizeBaseline, name) signal the growth check already computes -- 'new' is exactly 'present in sizeCurrent, absent from sizeBaseline'. Not ack-able, matching the tier hard caps it sits beside: the fix is extraction, not an acknowledgment entry. Documented, disclosed narrowing: the pure differential module cannot see XL_WORKFLOWS/LARGE_WORKFLOWS tiering (tests/workflow-size-budget.test.cjs's classification), so a legitimately large new file must extract rather than tier in, one release earlier than an existing file would need to. ADR-1610 itself is left unamended -- this restores its decision rather than re-litigating it. Also fixes a stale comment plus a redundant real 19-installer-spawn assertion left over from the pre-injection-seam version of tests/emitted-provenance.test.cjs's build-failure test, and annotates 3 of 4 stale golden-fixture citations in docs/reference/host-integration-capability-matrix.md as superseded (the 4th is an accurate historical PR narrative, left alone). * fix(#2724): repair three red CI defects on the golden-fixture cutover Windows-only provenance false attribution (defect A): the `hooks-built` provenance rule attributed `hooks/<name>.cmd` to itself. Those shims are Windows-only installer output (ensureCodexHooksJsonSessionStart / ensureCodexHooksJsonEvent, both in src/runtime-hooks-surface.cts) wrapping the same-named `.js` hook — no `.cmd` file is ever tracked in the repo, so the self-attribution resolved to a path that exists on no platform. Only windows-latest ever emits the key, so this only failed there. Fixed by special-casing `.cmd` inside the SAME `hooks-built` rule (not a dedicated rule) — a dedicated rule would match zero paths, and therefore report as a dead rule, on every non-Windows lane of the same totality guard. `sources` already supported per-match functions; `transforms` is extended to support the same shape so the attribution can vary by match within one rule. Baseline bootstrap was structurally impossible (defect B): `buildBaselineAtRef` ran `scripts/gen-emitted-baseline.cjs` from INSIDE the base-ref worktree, but that script is new in this PR and therefore absent at any base ref that predates it — every call failed closed with "Cannot find module". Fixed by running the PR checkout's own generator against the worktree via a new `--dir` parameter, decoupling "which copy of the script runs" from "which tree it measures" (`currentManifests`/`currentSizes` gained a `repoRoot` override, threaded down to `runMinimalInstall`'s new `installScript` override). This is not just a bootstrap fix: a differential needs ONE measurement schema applied to both sides, or the two stop being comparable the moment that schema evolves — running each side's own copy would silently reintroduce that risk. Verified locally end-to-end against real origin/next: resolves a valid {version, sha, manifests, sizes} artifact with the correct sha and no leaked worktree. Changeset placeholder (defect C): `pr: 0` -> `pr: 2767`, which is what let docs-lint evaluate the fragment for the first time; it already passes (docs/TESTING-SUITES.md and friends already document the removed scripts). Also fixed while in this file: an eslint no-unused-vars warning surfaced by the changed lint run (unused `cleanup` import in tests/emitted-provenance.test.cjs). Added regression coverage for both A and B: a cross-platform spot-check that drives the real hooks-built rule against `.cmd` keys directly (not through a real Windows install), and a real-tree test that drives buildBaselineAtRef against a base ref verified (via git cat-file) to lack the generator, both skipping honestly rather than false-passing when their precondition does not hold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * fix(#2724): repair false .cmd byte-provenance and a permanently-skipping regression test Two isolated-review findings on PR #2767: - `hooks-built`'s `.cmd` branch attributed the Windows shim's bytes to the wrapped `hooks/<name>.js` script, asserting a byte-provenance link that does not exist — traced against buildCodexHookWindowsShimIR (src/runtime-hooks-surface.cts), only the script's NAME (a literal in that same file) flows into the .cmd bytes, never its content. Point `sources` at HOOKS_WINDOWS_SHIM_SRC instead, matching the code-derived convention used elsewhere in the table. Since `sources` is checked before `transforms` in the differential, the wrong mapping silently excused any .cmd byte movement caused by editing the wrapped .js file. - The `buildBaselineAtRef` regression test skipped unless a resolvable base ref still lacked scripts/gen-emitted-baseline.cjs — true only until this PR merges, after which every base ref carries the file and the test skips forever with zero ongoing coverage. Rebuilt hermetically: synthesize the missing-generator condition in-place via git plumbing (a throwaway commit, child of HEAD, with just that one file removed from a scratch index), never touching the real working tree, HEAD, or index, and never depending on ambient history or remotes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * fix(#2724): tolerate the remote runner's dubious-ownership git mount in the emitted baseline path The runner container mounts the repo at a path owned by a different uid than the process running the suite, so git's dubious-ownership protection refuses every git operation there. GitHub Actions never hits this because actions/checkout registers the workspace as safe automatically; this runner's container does not. buildBaselineAtRef is the production build-fallback the sole remaining emitted gate depends on (resolveBaseline's in-job-build leg), not just a test helper, so the fix is in the shared git() wrapper (emitted-runtime.cjs) that every caller — resolveChangedPaths, resolveBase, buildBaselineAtRef's worktree add/remove/prune, and the hermetic regression test added in the prior commit — funnels through, plus gen-emitted-baseline.cjs's own rev-parse (now reusing that same wrapper instead of a second execFileSync, so the fix has one source of truth). Each call declares -c safe.directory=<the exact directory it already operates on>, never the * wildcard. Audited every other helper on this surface (emitted-diff.cjs, emitted-baseline.cjs, install-shared.cjs) for the same gap: none of them shell out to git at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d04592de58 |
fix(#2758): select the emitted differential wherever golden-parity runs (#2759)
* fix(#2758): select the emitted differential wherever golden-parity runs Add tests/emitted-provenance.test.cjs and tests/emitted-attribution.test.cjs to every scripts/ci-test-scope.cjs rule that already selects tests/golden-install-parity.test.cjs, so the ADR-2719 dual-run differential travels with the golden on the targeted CI lane instead of being selected by no rule at all. Add an independent module-load totality guard (missingRuleTestFiles) that throws when any rule names a test file absent from disk -- the guard that would have caught the post-Phase-4-cutover hole. It immediately surfaced three pre-existing phantom entries left behind by consolidation epic #1969 (bug-3588/bug-10/bug-3683 filenames folded into other suites months ago but never removed from the rule table); fixed in the same change rather than deferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * fix(#2758): trim unused exports and normalize the path require style Code-review (Standards axis) flagged two judgement-call smells: exporting classify/isInertCi with no caller (Speculative Generality), and requiring path with a node: prefix while the file's other core requires do not (inconsistent style within one file). Both addressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
44707c2c5e |
fix(#2757): attribute transform-driven emitted changes and reclassify agents-verbatim (#2760)
* fix(#2757): let derived provenance rules attribute a transform change The Phase 2 provenance table (#2722) could only explain a moved emitted path via its `sources`, so a `kind: 'derived'` artifact whose TRANSFORM code changed (not its source) was unattributable by construction: 16 emitted agents/*.toml moved by PR #2566's runtime-artifact-conversion.cts change, with zero agents/*.md in the diff. Adds an optional per-rule `transforms: string[]`. A moved emitted path now attributes via its source OR its transform, reusing sourceSatisfiedBy so exact/prefix matching stays identical for both. Declared narrowly on agents-toml-derived and agents-verbatim (the two families verified to route through the same conversion pipeline) as src/runtime-artifact-conversion.cts, src/install-effort-resolver.cts, and src/model-catalog.cts — never bin/install.js, which would be a blanket escape hatch spanning every installer concern. Also corrects agents-verbatim from kind:'identity' to kind:'derived': measured against origin/next's own fixtures, the same agents/<name>.md hashes differently per runtime (e.g. codex vs claude), which a true verbatim copy cannot do. Verified empirically by installing every runtime and diffing against the raw repo source — none reproduce it byte-for-byte; every one rewrites frontmatter and the hardcoded .claude/ self-reference, and claude additionally gets an effort: line injected. A new assertNoIdentityTransforms invariant rejects an identity rule that declares a non-empty transforms list. Closes #2757 * docs(#2757): flag PR #2566's transform-role files for later verification src/agent-tools-contract.cts (added by #2566, absent on next) and src/agent-install-check.cts (modified +112/-1 by #2566, read-only today) were reviewed for prospective inclusion in AGENT_TRANSFORM_SRCS. Excluded for now: the nonexistent file would fail this fix's own "every declared transform path exists" hygiene test, and the modified file's future role cannot be verified without the unmerged PR's diff. Documents the reasoning, the interim ack-file safety net, and the verification method to apply once these files stabilize, so the next PR touching them has a pre-scoped one-line fix rather than a silent gap. Related to #2757 |
||
|
|
9cbb48afa1 |
fix(#2753): scan every key occurrence when asserting a documented default (#2756)
* fix(#2753): scan every key occurrence when asserting a documented default The settings-default assertion located each key with indexOf and checked only a 400-char window after the FIRST match. Its stated contract is "the workflow documents the default for this key"; what it actually asserted was "the first mention of this key is followed by the default" - an ordering assumption that was never part of the contract and that breaks the moment a workflow names a key in prose before its settings-table entry. PR #2558 does exactly that: a shared language directive puts response_language at index 6 while the table documents null at 7185, so the gate failed a document that was correct, and the failure message showed the prose window rather than the cause. Extracted findDocumentedDefault, which scans every occurrence and reports how many it examined. Widening the window to the whole file was rejected - it would pass on any unrelated occurrence of the token - as was parsing the settings table, which would couple the check to table markup. The negative case still fails: a key that no occurrence documents is a failure, now with the occurrence count so a genuine miss stays distinguishable from this false negative. * fix(#2753): guard the empty needle and de-vacuum the newline test Isolated review found a real hang: String#indexOf('', pos) clamps to str.length rather than returning -1, so an empty key made the scan loop stabilize at the end of the document and spin forever. Unreachable from SPEC_FIELDS today, but the docstring claimed termination while reasoning only about self-overlap. Guarded, with the clamping behavior named so the guard is not tidied away later. The newline test asserted only that CRLF and LF agree, which a constant stub satisfies. It now pins the absolute verdict on both, plus a document neither style can rescue. Added the property test the review noted was missing: windowSize is a budget limit, so the verdict is asserted to be exactly "key + gap + default fits the window" over disjoint alphabets. It would have caught the empty-key hang on its own. * fix(#2753): assert examined-occurrence count, not the document's total The no-regression test asserted occurrences === 2 because the fixture mentions the key twice. The scan short-circuits on the first documenting window, so exactly one occurrence is examined - the assertion was describing the fixture rather than the function. Documented the semantic properly instead of just correcting the number: occurrences is how many were EXAMINED before deciding, so on success it is the 1-based position of the match and on failure the document's full count. The asymmetry is deliberate - the failure path is where the number must be trustworthy, since "examined N, none documented it" is what separates a genuine miss from the first-occurrence false negative this function removes. |
||
|
|
0f60266042 |
fix(#2723): reconcile emitted manifest families as a set, not a count (#2750)
* fix(#2723): reconcile emitted manifest families as a set, not a shared count EXPECTED_MANIFEST_COUNT was a single literal 19 asserted against both the baseline (built at the base ref) and the current tree (built at PR HEAD). Those sides legitimately differ by one family whenever a PR adds or removes a runtime, so no value satisfied both: 19 rejected the current side, 20 rejected the baseline side. Every runtime-adding PR was hard-blocked. Replace the shared literal with three independent signals - the derived family set, the recorded fixture set, and the families present at the base ref - reconciled as sets in both directions. A family may appear or vanish only when the diff plausibly touches the runtime registry, and the failure names the family rather than a count. An absolute floor catches the uniformly shrunken universe a same-count self-check passes vacuously. Found by tracing #2005 (Qoder runtime) through the gate during the ADR-2719 dual-run window. * fix(#2723): read the baseline family set from the ref, not HEAD's registry Review found three defects in the first cut. Blocker: baselineManifestsAtRef enumerated MANIFEST_FAMILIES, which is imported at module load and therefore describes PR HEAD. A runtime REMOVED by the PR is already absent from that list, so the base ref was never asked for it, the baseline silently omitted a family that genuinely existed, and the dropped-family check could never fire in production - while its unit tests passed, because they inject the baseline directly. Enumerate from the ref with git ls-tree instead. Also: narrow the registry-signal set to the two surfaces that actually define the family set, since every extra path widens what excuses an unattributed delta; drop the ack bypass, which was a one-sided escape hatch making removals easier to wave through than additions; and gate the derived/fixtures inputs so malformed values return a verdict rather than an unhandled TypeError. * fix(#2723): filter prototype-shaped family names read from git output baselineFamilyNamesAtRef derives object keys from git ls-tree output rather than a trusted constant, so a fixture committed as __proto__.json would turn the manifests[name] assignment into a prototype write. Compared inline rather than through a Set, which is the form the prototype-pollution analysis recognizes. * fix(#2723): require an exact capability path depth and make the ref test hermetic The remote runner went red on both linux lanes with two real defects. The capability signal matched by prefix+suffix, so 'capabilities/capability.json' (no runtime segment) and 'capabilities/a/b/capability.json' (wrong depth) both attributed a family change and would have excused an unattributed delta. Anchored to an exact single-segment pattern. The ref-derivation test reached for this repo's root commit, which is not stable: the remote runner shallow-clones, so rev-list --max-parents=0 returns the grafted boundary carrying every fixture, and this repo has two root commits locally anyway. It now builds its own git repo containing a family absent from the current registry - the real discriminator, and one the root-commit version could never assert. Lint then caught a third: the test called t.after() without declaring t. * fix(#2723): stop asserting ref enumeration against the ambient checkout The remote runner returned [] for the repo's own HEAD while every hermetic temp-repo assertion in the same test passed. That is this function's documented behavior when git cannot read the ref - the runner works from a shallow clone under a bind-mounted workdir - so the assertion was testing the checkout rather than the code. Dropped it. The temp repo already proves the property that matters, and proves it more strongly: it contains a family absent from the current registry, which a registry-derived implementation could never report. The ambient path stays covered by the real-tree test, which skips explicitly when no base ref is resolvable. A git failure is not silently permissive downstream: baselineManifestsAtRef returns null on an empty family set and the real-tree test asserts the baseline is non-empty. |
||
|
|
9138271b5f |
test(#2723): differential emitted-attribution check, dual-run beside the golden (#2737)
* test(#2723): differential emitted-attribution check, dual-run beside the golden Phase 3 of #2719. The conservation law itself, running BESIDE golden-install-parity.test.cjs -- both green, fixtures untouched. Every emitted path whose hash moved between next HEAD and PR HEAD must be attributable, through the Phase 2 table, to a path the PR actually changed. Unattributable deltas fail with the paths NAMED. The only way through is a committed acknowledgment, never a flag -- a contributor facing a red gate sets a flag, which is what UPDATE_GOLDEN=1 is today. The central decision is that the law is a PURE function (no fs, git, installer, or clock), with I/O confined to a separate resolver. The naive one-big-integration-test shape would need ~38 installer spawns per assertion, so #2723's four failing-first criteria would not in practice have been written -- which is exactly how a phase ships promised-but-not-built. Pure, they are millisecond table tests, and the Stryker gate can actually bite. Buckets are conserved: every moved path lands in exactly one of attributed | unattributable | acked, property-tested at 400 runs. A path the provenance table cannot resolve surfaces as an error, never a silent skip. Asymmetries that are deliberate, each with a test: - an ADDED emitted key is a ripple too, not just a modified one - synthesized paths are exempt; code-derived ones are NOT (Phase 2 refused to mark them exempt precisely because exempt means permanently blind) - shrinkage needs no ack; growth does. Gating shrinkage would punish exactly what the size ratchet wants - a STALE ack is a hard failure -- an ack outliving its ripple pre-clears the next one on that path - a failed `git diff` is an explicit error, never an empty changedPaths set; reading it as "nothing changed" would make everything unattributable and produce a failure storm that reads like a real finding - prefix sources are SEGMENT-aware, so `agents/` does not attribute `agentsfoo/x.md` Baseline is cached, not committed, keyed on the next sha. A stale key is refused rather than used: absence fails loudly and gets fixed, whereas staleness produces a confident wrong answer. An explicitly pointed-at GSD_EMITTED_BASELINE that is stale is a hard stop; a stale cache falls through to the in-job build. No baseline-unavailable path returns -- ADR-2719 section 6 names that trap, since in node:test a bare return is a PASS. Both the conservation property and the staleness gate were mutation-verified (injecting a swallowed key fails 9 tests; disabling the staleness comparison fails 5). Fixtures, generators, the merge driver and the ADR status are untouched -- those are Phase 4 (#2724). Refs #2719 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * test(#2723): compute stale acks once, after the size pass Self-review defect found while the reviewers were running. `staleAcks` was computed twice: once between the hash pass and the size pass, then again after. Only the second value was returned, so the first was dead code -- and the dead one was placed where it would have been WRONG. An acknowledgment can be consumed by either a hash move or a size growth. Computing staleness before the size pass reports a legitimate growth ack as stale, which is a false failure that pushes a contributor to delete the very ack that is doing its job. Now computed once, after both passes, with a regression test. Verified by mutation: restoring the early computation fails 2 tests. Refs #2719 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * test(#2723): wire the attribution check to the real tree, not just synthetic input An isolated reviewer caught that the first cut was INTERFACE-ONLY: nothing read the ack file from disk, nothing shelled git, nothing built real manifests. Every test was true of hand-built inputs and none of the repo, so the acceptance criterion "both this check and golden-install-parity green on the same tree" was trivially true rather than meaningfully true. That is the promised-but-not-built failure this epic keeps finding in its predecessors, recurring one phase later for the wiring itself. Taken, not argued. Adds tests/helpers/emitted-runtime.cjs -- the only module that touches git, disk, or the installer -- and an integration test that runs the same pure law against reality: - CURRENT side: 19 real installer spawns via runMinimalInstall + buildParityManifest, the same machinery the golden harness uses. - BASELINE side: `git show origin/next:<fixture>`. That is next's RECORDED emitted state and it costs nothing. Deliberately NOT the working-tree fixtures, which are whatever this PR's author regenerated -- comparing against those would be vacuous. Phase 4 deletes the fixtures and swaps in resolveBaseline's cache path, already implemented and tested. - changed paths from real `git diff --name-only origin/next...HEAD`, with the git subprocess bounded at 30s per CLAUDE.md's unbounded-subprocess rule. - the real tests/emitted-drift-ack.json (absent is legal; present-but-empty or unparseable throws rather than being read as absent). Verified it can actually fail: an uncommitted edit to a shipped workflow moves emitted output but never appears in the committed diff, and the check names all 18 affected emitted paths with the message format ADR-2719 §1 specifies. Restores clean. Also from review: - readAckFile now has a real test exercising the SUT across absent / valid / empty / unparseable / unreadable. The previous test asserted fs behaviour rather than SUT behaviour, because no SUT ack-reading path existed yet. - formatReport's sampleLimit gains true limit-1/limit/limit+1 coverage at 19/20/21. A test was previously NAMED "(limit+1)" while testing no numeric limit at all, which is worse than no coverage because it reads as covered. Windows uses an explicit t.skip (install output is platform-specific there, mirroring the golden harness) -- never a bare return, which node:test scores as a PASS. Refs #2719 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * test(#2723): cover the claude-local manifest family in the real-tree check Isolated adversarial review, MAJOR. The real-tree wiring enumerated Object.keys(RUNTIME_META) -- 18 entries -- while the emitted manifest set has 19 families. The 19th is claude-local: claude is the reference host and the only runtime with a distinct LOCAL "legacy flat-commands" layout (commands/gsd-*.md + agents/gsd-*.md at project scope), which golden-install-parity.test.cjs guards with a hand-coded test outside its RUNTIME_META loop (#2086). The family was dropped from BOTH sides, so the test's own self-check (current.length === baseline.length) passed vacuously at 18 === 18. A PR changing Claude's local-scope output would have failed the golden while this check reported ok -- and that disagreement is precisely what the dual-run window is designed to surface as a provenance-table hole. A wiring omission masquerading as one is the worst available failure here, because it would have been read as evidence about Phase 2 rather than a bug in Phase 3. Fixed by deriving MANIFEST_FAMILIES explicitly (18 global + claude-local at local scope) instead of inferring the set from RUNTIME_META. The self-check is also repaired: it now asserts both sides against the INDEPENDENT EXPECTED_MANIFEST_COUNT from the Phase 2 table, and asserts claude-local specifically. Comparing the two sides to each other can never catch a family missing from both -- the assertion has to come from outside. Verified by mutation: removing claude-local again fails the test. Also from the same review: - sourceSatisfiedBy returns the matched source string, so an empty-string source would return '' and the caller's `if (hit)` would silently discard a real match. Unreachable today (every rule source is a non-empty template) but a footgun for the next rule author; now `!== null`. - the purity fixture used a single-element changedPaths array, so an in-place sort would have been invisible. Now three elements in unsorted order. Refs #2719 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * fix(#2723): resolve the base ref tolerantly instead of hard-requiring origin/next The first matrix run failed on both linux lanes: differential attribution over the real tree cannot resolve origin/next (Command failed: git rev-parse origin/next) Not a flake, and not an environment excuse -- a real defect in this diff. The gsd-test runner shallow-clones and merges base+head, so no origin/* remote- tracking refs exist in the container. My own fail-loud path fired correctly; what was wrong was hard-depending on that ref existing. GitHub Actions has the same shape by default, which is exactly why changeset-required.yml carries an explicit `git fetch origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}"`. Now resolved through an ordered candidate list -- GSD_EMITTED_BASE (explicit lane override), then origin/$GITHUB_BASE_REF and $GITHUB_BASE_REF, then origin/next and next -- de-duplicated, each verified with `rev-parse --verify <ref>^{commit}`. When NO candidate resolves the test takes an explicit t.skip() naming every ref it tried and stating that the gate did not run here. That is the ADR-2719 section 6 distinction: t.skip is REPORTED as skipped, whereas a bare return is scored as a PASS. Hard-failing was the other option and is wrong -- it would make the suite permanently red wherever a base ref cannot exist by construction, which is a statement about the checkout, not a propagation finding. The candidate ordering is pinned by a unit test rather than left implicit, since the ordering IS the fix. Refs #2719 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |