fc3fde05ee405ccfd7ea6a24e6973304477c65a1
2409 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fc3fde05ee |
feat(#2646): surface unresolved deferred-items.md at milestone close (#2983)
* feat(#2646): surface unresolved deferred-items.md at milestone close auditOpenArtifacts scanned eight categories; deferred-items.md was not among them. #2287 made that file readable at the PHASE boundary (audit-uat, /gsd-progress check 7), but one boundary up it stayed invisible — and phase directories archive to milestones/vX.Y-phases/ by default (#1871), so an out-of-scope discovery a phase agent correctly recorded rather than fixed left the live tree at milestone close having never reached the [R]/[A]/[C] prompt that exists to catch exactly this. Adds deferred_items as a ninth scanner plus its count, its items entry and its report section. The workflow needed no change: complete-milestone branches on "any section with count > 0", so the new category flows through the existing prompt. The resolved/unresolved predicate is NOT reimplemented. uat.cjs already exports parseDeferredItems, which owns the parsing rule (entries under a `## Deferred Items` level-2 heading, else the whole file fail-safe; RESOLVED only on an explicit case-insensitive `status: resolved` field). The scanner requires it lazily, inside the scan, so audit-command-router's property that a route never loads the module it does not need is preserved. Two readers of one file sharing one predicate is the point — duplicating the inequality is how they drift into disagreeing about what "open" means. Regression test proves fail-first: 9 of its 10 cases go red against the pre-change tree. The tenth is the deliberate no-regression boundary (a clean tree emits no section) and is green both ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TQU48ETJjEmGLJjA6hdQ4 * docs(#2646): document the pre-close artifact audit and its nine categories The /gsd-complete-milestone entry did not mention the audit at all, so the gate that can stop a close was undocumented — and this change adds a category to it. Tabulates all nine with their source artifact and what makes each one "open", plus the [R]/[A]/[C] outcomes. Also disambiguates the one genuinely confusing thing: the per-phase deferred-items.md scanned here is NOT the `## Deferred Items` section the [A] path writes into STATE.md. Same name, different artifact, opposite ends of the flow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TQU48ETJjEmGLJjA6hdQ4 * chore(#2646): backfill changeset pr number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TQU48ETJjEmGLJjA6hdQ4 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
c61dd49d95 |
enhance(#2255): blocking catastrophic-shrink guard for curated .planning/ writes (#2301)
* feat(#2255): blocking catastrophic-shrink guard for .planning writes Adds hooks/gsd-write-guard.js, a PreToolUse hook that hard-blocks (decision: 'block', exit 2) a whole-file Write collapsing a curated .planning/ artifact (ROADMAP.md, .planning/milestones/*-ROADMAP.md, STATE.md) below 40% of its on-disk line count. Files under 40 lines are exempt; GSD_ALLOW_PLANNING_SHRINK=1 (named in the block message) bypasses for legitimate milestone resets. Fix 3 of #973 — the only defense independent of per-agent tool config. Registered on the Claude plugin surface (hooks.json), settings-json runtimes (runtime-hooks-surface.cts, self-contained pattern), Kimi spec, and the OpenCode/Kilo plugin buses. Golden install fixtures and INVENTORY regenerated; regression tests negative-controlled (16/16 RED with the hook absent, 16/16 GREEN with it present). * chore(#2255): backfill changeset pr number to 2301 * enhance(#2255): address review — fail-closed reads, typed block output, registration, property test Review fixes for trek-e's CHANGES_REQUESTED on PR #2301: - Blocker 2: register gsd-write-guard.js in BUNDLED_GSD_HOOK_FILES (no-shipping-drift test). - Blocker 3: update the always-on hook enumerations in ADR-766 and CONTEXT.md from six to seven. - Major 4: fail CLOSED on non-ENOENT read errors — only a missing file (new-file Write) passes; EACCES/EISDIR/ELOOP/etc now block, with a typed readError field and the override still honored. Tested, with a negative control against the pre-fix hook. - Major 5: fast-check property test for the SHRINK_RATIO/FLOOR_LINES budget contract (blocked ⟺ newLines < oldLines*SHRINK_RATIO above the floor; sub-floor always exempt), boundary examples pinned. - Major 6: block output now carries typed oldLines/newLines/ overrideEnvVar fields; tests assert on those instead of regexing the free-form reason string. - Minor: CURATED_PATTERNS are case-insensitive (case-insensitive-FS bypass on macOS/Windows); limit+1 boundary tests added for both the floor and the ratio. * enhance(#2255): engage the write guard on Kimi's native payload shape The guard shipped with Claude-vocabulary checks (tool_name 'Write', tool_input.file_path), which #2304 showed leaves a guard dormant on Kimi: the [[hooks]] matcher is registered pre-translated but kimi-cli forwards its native payload verbatim — tool_name 'WriteFile' (bare or module-qualified) and tool_input.path per its tool schemas (src/kimi_cli/tools/file/write.py). The guard matched, saw an unknown name, and exited 0. Apply the same per-guard normalization PR #2326 gives the three sibling guards (name + field mapping, inlined — hook scripts stage as standalone files), and write the block reason to stderr as well as stdout JSON: Kimi feeds stderr, not stdout, back to the model on exit 2, so a stdout-only reason blocks without telling the model why or naming the documented override. Regression tests pipe Kimi-shaped payloads (engage, qualified-name, stderr-reason) plus exemption pins (StrReplaceFile stays out of scope by design; non-curated paths pass) — verified red against the pre-fix guard, green after. * enhance(#2255): rebase onto next; regenerate golden-parity fixtures * enhance(#2255): wire the escape hatch into complete-milestone's reorganize step Review Blocker 1: the guard hard-blocked /gsd:complete-milestone's ROADMAP reorganize — the tree's only legitimate milestone reset and the exact caller GSD_ALLOW_PLANNING_SHRINK was built for. The reorganize step now performs the rewrite through a shell write with the hatch set on the command (a hook inherits the runtime env, so a bare Write cannot carry a per-step override), and a binding test derives the env var name from the guard's typed output and asserts (a) the workflow step sets it and (b) the guard passes the identical catastrophic payload under it — so the next complete-milestone.md edit cannot silently re-break the wiring. * enhance(#2255): drop dead Edit-class mapping from normalizeKimiPayload Review Major 1: StrReplaceFile -> 'Edit' and the old_string/new_string reconstruction were unreachable-by-effect — the guard exits 0 for any tool_name !== 'Write', so nothing ever read the fields they set, leaving guaranteed-surviving mutants against the Stryker bar. The map now carries only WriteFile -> 'Write'; the StrReplaceFile exemption test message states the fall-through it actually exercises. * enhance(#2255): review minors — American spellings; writeSync before exit(2) Minor 1: normalised/normalise -> American house style. Minor 2: the two block paths wrote stdout+stderr via async pipe writes then exit(2) — async-on-Windows, unflushed at exit; fs.writeSync(1/2, ...) makes the block payload durable. * enhance(#2255): assert stderr equals the typed reason, not raw prose Minor 3: the last raw-text match in the suite pinned override-name prose on stderr. The contract is "stderr carries the reason Kimi feeds back" — now asserted as stderr non-empty and byte-equal to the parsed stdout.reason. * enhance(#2255): bind the write-guard's Kimi normalization into the parity test Review Major 2: the guard's normalizeKimiPayload is a 4th inlined copy with nothing binding it. This extends PR #2326's kimi-guard-normalization-parity test (same path and helpers, authored as a superset so either merge order resolves cleanly): sibling byte-parity is existence-gated zero-or-all — trivially green until #2326 lands, full-strength after — and the write-guard copy is bound semantically (map is the value-inverse of convertKimiToolName; the Kimi name for Write must map, or the guard is dormant on Kimi; the path -> file_path half must be present). Byte-parity is deliberately not asserted for this copy: it legitimately omits the Edit-class mapping (Major 1 — dead code in a Write-only guard). * enhance(#2255): refresh golden-parity fixtures for revised guard + workflow * chore(#2255): regenerate golden fixtures after rebase onto next The committed fixture hashes were generated against a tree predating next's latest 11 commits, which independently modified the same install-parity surface. Rebased onto next and regenerated with `npm run gen:golden`. Verified: against upstream/next the regenerated fixtures differ by exactly this PR's own entries -- hooks/gsd-write-guard.js (new), hooks/managed-hooks-registry.cjs, plugins/gsd-core.js, and gsd-core/workflows/complete-milestone.md. No unrelated drift. * fix(#2255): regenerate workflow size baseline for complete-milestone `complete-milestone.md` grew 31071 -> 32061 (+990) when the round-2 review fix bound GSD_ALLOW_PLANNING_SHRINK=1 into the reorganize step, but tests/workflow-size-baseline.json was never regenerated. The per-file workflow baseline test (issue #1074) failed on ubuntu-latest/22 and both macOS shard 1/3 jobs. The growth is justified: it is the escape-hatch binding requested in review round 2 (the guard must not hard-block the tree's only legitimate milestone reset), not incidental bloat. Regenerated via `npm run size:baseline`; the diff is exactly the one entry. * chore(#2255): regenerate golden fixtures and size baseline after rebase onto next * enhance(#2255): bind the shrink escape hatch mechanically — single-use sentinel the guard consumes Round-5 M1: the per-step `GSD_ALLOW_PLANNING_SHRINK=1 tee` prefix was inert (no PreToolUse hook exists on Bash in this family; the write succeeded by dodging the guard, not by the override firing) and the protection was prose. The hatch is now a transport code consults: complete-milestone's reorganize step arms `.planning/.gsd-allow-shrink` with the target's path, keeps the Write tool as the sanctioned path, and the guard — at the block point only — verifies the sentinel is fresh (15 min) and names the pending target, then CONSUMES it and allows that one write. Path-bound + single-use + freshness keep it from becoming a standing unlock. The env var remains as the interactive transport, where it can actually reach the hook. Regression tests written first (negative control: 3 failed pre-fix): the armed-sentinel Write passes and consumes; stale does not exempt; a token for a different file neither exempts nor is consumed; the binding test now takes the sentinel name from the guard's typed output (overrideSentinel), asserts the step arms it, and asserts the step no longer routes the rewrite around Write via a shell pipe. Also in this commit, same file: - m2: block emission is exception-safe — emitBlock() wraps both writeSync sites in their own try/catch that still exits 2, so an EPIPE can no longer convert fail-closed into the outer catch's fail-open. - Header discloses the two reviewed design limits (cumulative sequential shrink; lexical match vs symlinked paths) per round-5 scoping. * docs(#2255): document the sentinel transport across guard surfaces; changeset ends with the (#2255) parenthetical (m4) USER-GUIDE bullet, INVENTORY row (en + ja/ko/pt/zh), the runtime-hooks-surface registration comment, and the changeset now describe both hatches — the single-use sentinel for workflow steps and the env var for interactive use — instead of implying a per-step env can reach a hook. The changeset's trailing `Resolves #2255.` prose becomes the `(#2255)` parenthetical the repo's fragments use (round-5 m4). * chore(#2255): regenerate derived families on the rebased tree (full sweep) Full generator sweep after rebasing onto next @ the body-parser-patched lockfile: build, gen-inventory-manifest, gen:golden, size:baseline. Every regen delta verified to be either a PR-owned entry (gsd-write-guard.js, complete-milestone.md, INVENTORY/USER-GUIDE) or exact convergence to next's committed value for entries our arbitrary-side conflict resolution had left stale (all 18 runtime fixtures checked mechanically). * test(#2255): use helpers.cleanup for sentinel teardown, not raw fs.rmSync The repo's local/no-raw-rmsync-in-tests rule exists for the Windows-EBUSY retry budget; the sentinel disarm now rides it like every other teardown. * chore(#2255): regenerate derived families after rebase onto next Full sweep on the rebased tree (build -> gen-inventory-manifest -> gen:golden -> size:baseline). Every delta is either a PR-owned entry (hooks/gsd-write-guard.js, its registration surfaces hooks/managed-hooks-registry.cjs and the two plugin buses, gsd-core/workflows/complete-milestone.md) or exact convergence to next's committed value across all 18 runtime fixtures. * chore(#2255): regenerate derived families after rebase onto next @ |
||
|
|
cc3ee301a7 |
fix(#2544): stage the CommonJS marker in GSD-owned dirs, not the config root (#2593)
* fix(#2544): stage the CommonJS marker in GSD-owned dirs, not the config root installSharedHooksBundle wrote `{"type":"commonjs"}` over <configRoot>/package.json unconditionally — no existence check, no merge, no backup — on every install and every /gsd-update re-install. On the 11 affected runtimes that file is often user-owned; on OpenCode and Kilo it is the documented place to declare local-plugin npm dependencies, so a user's name/type/dependencies/scripts were destroyed on each run. The uninstall path already read the file and unlinked it only on an exact content match. That asymmetry was the defect: the discipline existed in the codebase, it just was not applied on the write side. Move the marker into the directories GSD creates and fills with its own .js files — hooks/ (all shared-hooks runtimes, incl. Kimi's own root) and the nativePlugin dir (plugins/ for OpenCode+Kilo, extensions/ for pi) — and stop writing the config root entirely. New src/commonjs-marker.cts owns the marker string plus one ownership predicate (absent / gsd-owned / foreign, fail-closed on an unreadable file) shared by ensureCommonJsMarker and removeCommonJsMarker, so install and uninstall cannot drift apart again. Nothing else depended on the config-root marker: package identity is baked at build time (#378/#498) and version resolution prefers gsd-core/VERSION and already tolerates a missing root package.json (#1383) — Codex has installed without one all along. A package.json in plugins/ or extensions/ is inert to plugin discovery, which globs *.{ts,js} only (see installer-migration 006). Uninstall retires the pre-fix config-root marker, so upgrading users are cleaned up on removal, and still never touches a file it did not write. * fix(#2544): point the changeset fragment at the filed PR The fragment's `pr:` field is only knowable after `gh pr create` returns. * fix(#2544): register commonjs-marker.cjs in the tsc-generated ESLint ignore set bin/lib/commonjs-marker.cjs is tsc output (src/commonjs-marker.cts is the linted source), so it belongs in the ADR-457 ignore list like its siblings. Clears the lint-tests no-var failure and the repo-invariants "linted xor ignored" migration-state test. * fix(#2544): pin the kimi CommonJS marker to hooks/, not the ~/.kimi root The UPGRADE 1 test still asserted the pre-#2544 marker location (~/.kimi/package.json). The marker now lives inside ~/.kimi/hooks — the directory GSD itself creates — matching the updated golden-install-parity and install-tree fixtures. Also asserts the root marker is NOT written. * fix(#2544): make the CommonJS marker write path non-fatal Review round 2, Major 3 + Minor 1 + the stagedHooks nit. ensureCommonJsMarker rethrew any non-EEXIST write error and neither call site caught it, so EACCES on a read-only hooks/, EROFS, or ENOSPC aborted the whole install with a raw stack trace. Every other marker interaction in the module is best-effort — removeCommonJsMarker swallows unlink failures, classifyMarker swallows read failures — and this was the write path, i.e. the one most likely to fail on a locked-down config dir. It now returns a new 'failed' outcome and both call sites warn and continue. Sibling found while sweeping for the same defect class: fs.mkdirSync sat OUTSIDE the try block, so an unwritable parent threw past the guard entirely. Creating the directory is the same environmental hazard as writing into it, so it moved inside. Also in this file: - The hooks marker is now gated on `stagedHooks && hooksOk`, not stagedHooks alone. stagedHooks is computed from the SOURCE listing before the copy loop, so it stays true when the copies land but verifyInstalled() then fails — marking a hooks/ GSD did not successfully populate claims an ownership the install did not earn. - The uninstall rmdir of the native plugin dir is gated on GSD having actually removed something from it. Hoisting it out of the adapter-exists guard (so the marker-only case could prune) had silently widened it into deleting a user-created but empty plugins/ or extensions/ dir — the same "don't touch territory GSD didn't fill" principle this issue is about, inverted. - Kimi's pre-#2544 marker at its native hook root (~/.kimi) is retired at the same call site that writes its replacement. That path is outside kimi's configDir, so installer-migration 007 structurally cannot reach it. * fix(#2544): retire the stale config-root marker via installer-migration 007 Review round 2, Major 1 — the PR's headline claim was false for existing installs. Upgraders kept BOTH markers: the new one under hooks/ and the stale {"type":"commonjs"} at the config root, so their config root stayed pinned to CommonJS and their dependency manifest stayed gone until they uninstalled. The migration is unusual in one way, and it is the part worth reviewing: the config-root marker was never recorded in gsd-file-manifest.json (writeManifest records hooks/, agents/, commands/, scripts/ and the native plugin, never a root package.json), so classifyArtifact answers 'unknown' for it and the planner's own guard downgrades a remove-managed on an 'unknown' classification to preserve-user. 007 therefore supplies the "purpose-built detector for an old GSD-owned shape" that docs/installer-migrations.md#remove-managed sanctions — exact content match, the same predicate removeCommonJsMarker has always used — and declares the resulting classification on the action. A package.json with any other content is left untouched, and there is deliberately no backup-and-remove branch: a non-matching file here is not a patched GSD artifact, it is somebody else's file. Scope is all runtimes. The `runtimes` field is OMITTED rather than `[]`: validateStringArray requires the field to be non-empty WHEN PRESENT, while the runtime filter treats an empty array as "all" — so `runtimes: []` throws at plan time and the migration never runs. The metadata test pins this. Kimi is a deliberate carve-out, named in the migration's own header: its marker lived at ~/.kimi, outside kimi's configDir, and migration relPaths are structurally confined to configDir. It is retired by the installer instead. Registration: shipped-migrations table, .gitignore for the emitted .cjs, the EXPECTED_CHECKSUMS baseline, and the ESLint ignore set. That last one is not copied from migration 006 by rote — 006 needs no entry because it imports nothing, while 007 imports node builtins, so tsc emits its __importDefault helper and the `var` in it trips no-var. This is the same lint gate that made round 1 red. * test(#2544): fault-injection and multi-runtime marker coverage Review round 2, Major 2 + Minors 4 and 5. Major 2 — CONTRIBUTING.md:514-531 is mandatory for install/uninstall flows and the suite had no fs monkeypatching at all. Every branch now covered is one whose doc comment claims it as the module's safety posture: - classifyMarker non-ENOENT lstat error -> 'foreign' (the fail-closed rule), with an ENOENT control alongside it so the test discriminates rather than just asserting one side - classifyMarker readFileSync throw -> 'foreign' (present-but-unreadable never downgrades to the permissive answer) — the fixture's bytes are exactly GSD's marker, so the test fails if the code ever answers on content it could not read - a DIRECTORY at the marker path (CONTRIBUTING:521; the symlink case was already covered with a real symlink, the directory case needs no injection at all) - the ensureCommonJsMarker TOCTOU EEXIST branch — the entire reason for flag:'wx' - the new 'failed' outcome, for both writeFileSync (EACCES/EROFS/ENOSPC) and the mkdirSync that used to sit outside the guard - removeCommonJsMarker unlink throw -> false These save and restore fs methods in `finally` rather than using chmod 0o000, which does not fault under root and would pass vacuously in root Docker and CI. Minor 4 — uninstall was driven for opencode only. pi's extensions/ and both kimi locations now have behavioral coverage, install and uninstall, each paired with a user-authored-file case proving GSD leaves it alone. Minor 5 — the stagedHooks gate had no assertion behind its stated reason. A pre-existing, GSD-untouched hooks/ directory is now driven through a runtime that declares skipSharedHooksInstall and asserted to stay marker-free, with its user content intact. Also regression-tests the uninstall rmdir gate from the previous commit: an empty plugin dir GSD removed nothing from must survive. * docs(#2544): correct stale marker prose, register the module, document the trade-off Review round 2, Minors 2, 3 and 6. Minor 2 — six files asserted the installed ROOT ships the synthetic marker. None was load-bearing (all three walk-up consumers are VERSION-first with try/catch and the marker never carried a `version`), but ADR-457:52 is the rationale for keeping a generated module, so a future reader would mis-derive the constraint from it. Each site is corrected to what is now true: the installed tree carries no package.json with a .name at all, because the only ones GSD stages are {"type":"commonjs"} markers and they now live in GSD's own directories. Two of the six needed more than a location swap. hooks/gsd-check-update-worker.js and the platform-gate test both described `require('../package.json').name` resolving to undefined; post-#2544 that require does not resolve at all, so the history is kept accurate and the present-tense claim corrected rather than just moved. And src/runtime-artifact-conversion.cts described the no-root-package.json case as Codex-only — it is now every runtime, which strengthens that comment's own argument for lazy resolution. The generated .cjs sibling needs no edit: it is gitignored build output, not a tracked file. Minor 3 — src/commonjs-marker.cts had no CONTEXT.md entry, unlike every peer module, and CONTEXT.md is the #2 co-change partner of bin/install.js. Added, including the fail-closed posture and the never-throws contract. Minor 6 — the plugins//extensions/ marker shadows the config root for all .js siblings, so an OpenCode/Kilo user's ESM plugin/*.js stays broken. That is exactly what #2544's Fix section prescribed and it is disclosed in the PR body, but the PR body is not documentation. It now lives in the OpenCode section of docs/how-to/install-on-your-runtime.md, stated as a real constraint rather than a pure improvement, with the .ts mitigation and a fallback for ESM plugins. * test(#2544): attribute the CommonJS marker in the emitted-provenance rules The differential emitted-attribution gate (#2723, landed on `next` after this branch was cut) went red on the macOS shards once this PR rebased onto it. Two distinct causes, both real gaps rather than noise: 1. `plugins/package.json` and `extensions/package.json` matched NO rule — the `native-plugin` rule covers `*.{js,cjs,mjs}` only, so the marker read as an unattributed emitted family. 2. `hooks/package.json` fell through to `hooks-built`, which attributes an emitted `hooks/<X>` to a repo source `hooks/<X>`. There is no `hooks/package.json` in the repo, so it resolved to a nonexistent path. Cause 2 is exactly the failure already documented three lines above it for Copilot's `gsd-session.json` — "a code literal, not a built script" — so the fix follows that precedent rather than inventing one: `package.json` is excluded from `hooks-built` the same way, and a dedicated `commonjs-marker` rule attributes the family across all four roots it can appear in (both hooks roots plus `plugins`/`extensions`) to the sources that actually emit it. Deliberately a RULE, not an entry in tests/emitted-drift-ack.json. An ack is for a one-off ripple and goes stale by design — the gate fails a stale ack precisely so it cannot pre-clear the next change on that path. These markers are a permanent part of the emitted tree from #2544 onward, so they need standing attribution. Verified by reproducing the CI failure locally with GSD_EMITTED_BASE: 3 provenance errors + 12 unattributed paths before, 35/35 green after. * fix(#2544): route the #2717 hooks-surface marker helpers through commonjs-marker #2717 landed a second copy of ensureCommonJsMarker/removeCommonJsMarkerIfGsdOwned in src/runtime-hooks-surface.cts for the runtimes that stage .js hooks via dedicated paths (cursor/windsurf/codex). That copy had drifted from this PR's module on the two properties that matter: - ownership probe: `fs.existsSync` FOLLOWS symlinks and reports false for a DANGLING one, so a dangling package.json symlink classified as absent and the write went straight through it. Demonstrated: against the pre-fix copy, ensureCommonJsMarker() on a hooks/ dir holding a dangling package.json symlink returns true and creates {"type":"commonjs"} OUTSIDE that directory. - create: a plain writeFileSync leaves the classify->write window open, where commonjs-marker creates with flag:'wx' (O_EXCL). Both helpers now delegate to src/commonjs-marker.cts, which is what this PR's own docstring already claimed was the single place these rules are enforced. Exported signatures are unchanged (still boolean), so bin/install.js and the #2717 tests are unaffected. The new subtest is the only coverage that fails if the duplicate is ever reintroduced — the two implementations agree on every non-adversarial input, so the existing suites pass against both. * test(#2544): pin the stagedHooks gate on zcode, not windsurf The Minor-5 coverage picked windsurf because hostBehaviors.skipSharedHooksInstall kept it out of the shared hooks bundle, so GSD staged nothing into hooks/ and the marker was correctly absent. #2717 changed that premise: cursor/windsurf/codex now stage their .js hooks via dedicated paths and get the marker beside those scripts. Measured on this tree, windsurf stages 2 .js hooks and receives a marker — so the assertion was pinning behaviour that is now wrong, not the gate it was written for. ZCode is the durable choice: per #1821 it has hooksSurface:'none' AND no plugin surface to spawn hooks, so GSD stages no .js there by either route (measured: 0 staged, no marker). The property under test is unchanged — a user-created hooks/ directory GSD never fills stays marker-free. * test(#2544): use the shared cleanup helper in the migration test Addresses the review's Major 1. The suppression's stated reason — "no helpers import available" — was not correct: tests/helpers.cjs exports cleanup, and the other test file added in this same PR imports it (tests/commonjs-marker.test.cjs). The local reimplementation dropped two protections that are live on this repo's windows-latest lane: the CWD guard (Windows cannot remove a directory that is the current working directory) and the 20 x 250ms retry budget that absorbs the deferred-scan handle Windows Defender holds on newly-written files. Local function and suppression both removed; local/no-raw-rmsync-in-tests now passes without one. * test(#2544): expect hooks/package.json for the #2717 runtimes The fresh-install contract table predates #2717, which stages cursor/windsurf/ codex .js hooks via dedicated paths and writes the CommonJS marker beside them. All three therefore now receive hooks/package.json legitimately. Measured on this tree: codex stages 3 .js hooks, cursor 6, windsurf 2 — each with the marker; cline/copilot/trae/zcode stage none and get none, so their contracts are unchanged. * fix(#2544): gate the #2717 marker writes on having staged something The three dedicated marker writers #2717 added ran unconditionally. Each one mkdirs hooks/ up front and stages its scripts conditionally on the source existing, so with an absent or empty hook source they created a directory, filled it with nothing, and marked it as GSD's anyway. That is the same write-into-someone-else's-territory this issue is about, and installSharedHooksBundle already guards the identical case with `stagedHooks`. The dedicated paths now carry the matching gate: - cursor / windsurf: `installedScripts.size > 0` - codex: a new `codexStagedHooks` flag. The enclosing guard only proves that hooks/dist EXISTS; it says nothing about whether any CODEX_HOOKS_TO_COPY entry landed. Covered for cursor and windsurf by driving each writer against a src tree whose hooks/ dir is empty. The codex leg is defensive and deliberately uncovered: its trigger state needs a package tree where hooks/dist exists but holds none of the allowlist, which is not constructible from a real checkout. * test(#2544): scope the commonjs-marker sources per root The rule declared one flat source list for every marker root, so `extensions/package.json` was attributed to runtime-hooks-surface.cts (which never writes there) and `.kimi/hooks/package.json` to install-engine.cts. That is not merely untidy. emitted-diff.cjs accepts the FIRST satisfied source, so a flat list containing bin/install.js let any change anywhere in that 13k-line file authorise marker drift for every root — the blanket escape hatch this file's own agents-verbatim comment refuses for exactly the same reason. Sources are now derived per root from ctx.rel. Note the rule ctx is `{ rel, runtime }` and carries no `root`, so keying on ctx.root would have sent every path down one branch silently. * test(#2544): state precisely what the zcode assertion pins The comment claimed the test pinned installSharedHooksBundle's `stagedHooks` gate. It does not, and neither did the windsurf version it replaced: zcode declares skipSharedHooksInstall, so the outer guard skips that helper entirely and the gate is never evaluated. The test passes on the runtime exclusion. What it does pin — the outcome a pre-existing, GSD-untouched hooks/ stays marker-free — is still worth having, and is what the review asked for. The two `staging zero hook scripts` tests are the ones that pin a real staged-nothing gate. Comment corrected rather than left implying coverage that is not there. --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
b62589b73f |
fix(#2840): exclude runtime from defaults.json spread into project config (#2985)
* test(#2840): add regression for runtime poisoning from defaults.json * fix(#2840): exclude runtime from defaults.json spread into project config runtime is host-specific (written by whichever installer ran last). On a machine with 2+ runtimes, it poisons every new project config — e.g. a Codex install's runtime:'codex' leaks into Claude Code projects. Now excluded from the userDefaults spread, mirroring the resolve_model_ids guard (#2297). * chore(#2840): add changeset fragment * fix(#2840): add new test file to lint-test-file-count allowlist * chore(#2840): backfill changeset PR number 2985 --------- Co-authored-by: sim <sim@local> |
||
|
|
33fd203ccd |
test(#2966): loop QA walk — drive real scenarios across all five loop steps (#2976)
* test(#2966): loop QA walk — drive real scenarios across all five loop steps Adds a headless walk that carries accumulating project state across discuss -> plan -> execute -> verify -> ship against one temp project, layered over the existing tests/helpers.cjs runGsdTools substrate. Findings carry severity. A violation breaks a stated contract and fails the build; a smell is legal under today's implementation but structurally questionable, is recorded, and never reddens CI. Without that split an oracle set derived from current behavior can only ever confirm current behavior -- the harness could not say "this works and is still wrong". The end-to-end test asserts the walk produces at least one smell: a QA harness that reports nothing on a first run against a real engine is far more likely mis-specified than the engine is perfect. It deliberately does not pin smell ids or counts, which would re-freeze current behavior. First run against the real engine: 0 violations, 3 smell classes -- init returns agents_dir outside the project tree; smart-entry emits prose unconditionally so routing cannot be asserted; state-snapshot reports a missing STATE.md through a payload key with exit 0. Also fixes tests/fixtures/index.cjs: createFixture with git:true and planning:false staged nothing, so the commit failed with "nothing to commit". That combination was unreachable until greenfield needed it. Extends RULESET.TESTS.feedback-loop-convergence from estimation to the loop itself. Design lock: docs/adr/2966-loop-qa-walk.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): wire fault injection, make perturbations discriminating Independent review found tests/qa/mutations.cjs entirely unwired: 462 lines exercised only by their own unit tests, with no mutation hook in the scenario DSL and no scenario applying one, while the module header and the ADR described fault injection in the present tense. Dead code documented as live. Adds a `mutate` step field, three perturbation scenarios, and a wiring detector: a self-test scenario whose expectations are known-false and which MUST fail. The previous anti-vacuity check asserted only that the walk produced a smell, which passes on well-known engine behavior regardless of whether the harness wiring works. First perturbation attempt produced zero signal -- progress does not structurally parse ROADMAP.md, so a corrupted roadmap sailed through. A perturbation that cannot fail is the same defect in a new costume. Probes now target roadmap get-phase, and each mutated step runs a clean baseline first so `mutationObserved` records whether the corruption changed anything at all. Also clears four review findings: classify() returned PROSE for exit-0 with empty stdout; `warnings` was structurally unpopulatable on the success path (execFileSync discards it) and is now documented as error-path-only; read-only-idempotence passed vacuously when asked to check idempotence without the data to check it; the ADR miscounted the oracles. Discrimination matrix across 8 mutations x 6 commands: bom, duplicate-phase-id and escaped-pipes are absorbed silently by every probed surface, and progress / smart-entry / roadmap validate never reacted to any mutation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): add path-containment guard for scenario-supplied targets Security review found scenario-supplied paths joined to the temp project with no containment check. step.mutate.target and agent.write keys were validated only as non-empty strings, so a target of ../../../../etc/hosts reached fs.unlinkSync / fs.writeFileSync / fs.symlinkSync outside the project. The symlink mutation was worst: it read the traversed file, wrote a sibling copy, deleted the original and symlinked it back. Not exploitable today -- all shipped scenarios target .planning/ROADMAP.md and scenarios are repo-committed, not runtime input. Fixed anyway: it is a live primitive any future scenario or copied helper can reach. Adds tests/qa/paths.cjs with resolveWithin(): rejects absolute paths, NUL bytes and empty input, normalizes separators unconditionally, and requires containment by path segment so a sibling like <base>-evil is not treated as inside. Non-existent targets resolve via nearest existing ancestor rather than falling back to a lexical compare. Scenario load now rejects traversing or absolute targets up front. oracles.cjs previously carried its own copy of the containment logic; both now share paths.cjs, since a duplicated containment check is exactly the divergence class this repo calls out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): complete trajectory corpus, report emission, boundary-aware oracle Adds the remaining trajectories and drives all 11 mutations end-to-end. 20 scenarios, 72 steps, 0 violations, 25 smells. Adds qa-report.json with per-step verdicts and a copy-pasteable repro command, plus --keep / GSD_QA_KEEP=1 to preserve a failing tree. A repro line for a tree that was not preserved is marked NOT RUNNABLE rather than emitting a command pointing at a deleted directory. monotonic-progress is now boundary-aware. Two scenarios had been trimmed to stop the oracle complaining at a milestone rollover, which destroys the signal the trajectory exists to produce. Evidence: counters legitimately reset to zero at milestone complete, but the payload milestone_version lags until a new ROADMAP.md is written. So the oracle now scopes by milestone plus workstream, keeps a same-scope decrease as a violation, and records a boundary crossing as a smell. Both scenarios walk the real boundary again. Standards review fixes: oracle findings now carry a structured subject so tests assert on typed fields instead of substring-matching the free-form detail string, resolveWithin throws a typed EPATHESCAPE error, and the absolute-path predicate scenario.cjs had re-implemented now comes from paths.cjs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): fix silently-vacuous fixtures and guard the class Every fixture carried its #2371 provenance comment BEFORE the frontmatter block, and extractFrontmatter returns {} when anything precedes the opening ---. So every scenario reading status/phase/name was operating on an empty object and reporting green. Nine fixtures repositioned; the comment stays, it just moves below the closing ---. Both UAT fixtures lacked a parser-recognized result block, so evaluateUatPassed saw checks.length===0 and could never return passed:true. The uat-fail-then-remediate scenario could not have proven a remediation. Its expect block only inspected blockers, which is empty before AND after, which is why the corpus never noticed. Both fixtures now carry real result blocks and the scenario asserts passed and no_uat_artifacts on each side of the flip. The actual deliverable is the guard: a fixture-integrity block asserting every fixture with a frontmatter shape parses to a non-empty object, that every fixture carries its provenance marker, and that the two UAT fixtures produce opposite verdicts through the real evaluateUatPassed. The first guard written required --- at byte 0, which would never have fired on the regression it exists to prevent; it was rewritten and proven by deliberately re-breaking a fixture. No engine defect here. no_uat_artifacts means no parsed check items, not no UAT files, and it was reporting correctly on fixtures that had none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): make the walk report — smell ratchet, baseline, CI job The harness computed smells into a gitignored qa-report.json that nothing read. In CI it surfaced nothing at all: violations failed the build, but the half of the tool that says "this works and is still wrong" was inert. A QA tool nobody hears is decoration. Adds a ratchet on the same idiom this repo already uses three times over (the regression-test-name allowlist, the emitted-drift acks, the size baseline): a committed smell-baseline.json, per-PR acknowledgment fragments under tests/qa/smell-acks/, and a ratchet script wired into CI. The design invariant is preserved exactly. A smell still never fails a build on its own merits. What fails is an UNACKNOWLEDGED NEW smell -- the absence of a decision -- leaving an author two honest exits: fix it, or record a fragment with a real reason. An empty reason is rejected. The baseline is shrink-only, so a fixed smell must prune its entry. Violations remain unacknowledgeable. Fingerprints are composed only from stable fields (oracle id, scenario, argv, subject discriminator) -- never temp paths, timestamps or counts. Verified byte-identical across two runs in separate temp dirs; an unstable fingerprint would have false-positived every CI run. CI gains a qa-loop-walk job that runs the suite and the ratchet, uploads the report with `if: always()` (it matters most when it failed), and renders a summary a reviewer reads without downloading anything. Also fixes the report runner invoking main() unconditionally on require, so importing it double-ran every scenario and clobbered its own output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): every smell terminates in a defect or a fixed detector The baseline accepted a smell with a free-text reason. That is a mechanism for designing smells in -- an allowlist nobody revisits. The harness is brand new, so nothing it found is inherited legacy; every finding is a FIRST finding. Each must now terminate in exactly one of two states: REAL -> an assigned defect, entry carries the issue number FALSE POSITIVE -> the detector is wrong and gets fixed, never baselined There is no third "accepted with a good explanation" state, so the ratchet now requires a positive-integer `issue` on every entry. A reason may remain as a human note but can never substitute. `--update` refuses to invent issue numbers: a new smell is written with `issue: null` and a TODO, and the next plain run rejects it, forcing triage rather than accumulation. Working the 21 existing entries through that rule found 16 were my own detectors being wrong: value-hygiene (10) flagged $.agents_dir, a field whose entire contract is to point at the install tree outside any project. Fixed with a leaf-key allowlist of contractually-external fields, verified as the only such key in the init payload. Genuinely unexpected out-of-project paths still smell. monotonic-progress (6) fired on legitimate boundary crossings -- milestone v1.0 to v2.0, workstream beta to alpha -- and on one payload carrying no scope fields at all, where a change cannot even be known. Scope changes now reset silently and scope-less observations are skipped. The same-scope decrease remains a violation; that is the real invariant and is regression- guarded. The five survivors are real and now tracked: soft-error-exit-zero (#2980), untyped-success (#2979). Baseline 25 -> 5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): keep the ratchet out of the tarball, unpin the qa CI job The remote matrix returned failed -- 3 unique failures, identical on node22 and node24, both root causes in this branch's own diff. The ratchet lives under scripts/, which ships in the npm tarball, and it requires three modules under tests/, which does not. In a published install it is MODULE_NOT_FOUND at load. This is exactly the class the #2858 guard was added to catch, and it caught it. Fixed the way #2858 fixed the same shape for its own repo-only CI script: a targeted files[] negation, so the ratchet stays in the repo for CI and out of the tarball. Not solved by moving or inlining the required modules -- the ratchet must keep using the same code the harness uses, or the two drift. Verified both directions: the script is no longer in the pack list, and build-hooks.js, fix-slash-commands.cjs and gen-capability-registry.cjs are all still shipped. Over-negating there would have broken installs, since bin/install.js requires them. The qa-loop-walk job also carried CI_REBASE_BASE_SHA copied from a neighbouring job without the paired GSD_EMITTED_BASE, which the #2854 invariant forbids by name: diverging them makes the differential compare a tree against a baseline from a different commit. The job runs only the qa suite and the ratchet and invokes no emitted-attribution test, so it needs no rebase-pinned base at all -- the step was removed rather than paired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2966): stop monotonic-progress going blind on scope-less payloads The full remote suite caught a false NEGATIVE I introduced while fixing a false positive. Silencing the boundary-crossing noise had made the oracle skip ANY observation lacking milestone fields -- so a minimal payload like {total_summaries: n} produced no violation at all, and the oracle stopped catching the exact defect it exists to catch. For a QA tool that is strictly worse than the noise it replaced. Scope is only indeterminate when the two observations DISAGREE about having it: both scoped, same scope, decrease -> VIOLATION both scoped, different scope -> reset silently NEITHER scoped, decrease -> VIOLATION (the regression) mixed -> skip the comparison Implementing the mixed case surfaced a second blind spot: advancing the reference point on a skipped pair lets a scope-less observation sitting between two same-scope ones mask a real decrease. Mixed now leaves the reference untouched. All four branches carry explicit coverage; only one did before, which is why this shipped. The self-test that failed was right and the code was wrong, so the code moved. Corpus behavior is unchanged: still 5 smells, 0 new, 0 stale, 0 violations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
628648d63a |
chore(#2931): cap emitted per-runtime bytes and single-source windsurf (#2984)
* fix(#2931): preserve protected regions and cap emitted per-runtime bytes Route every runtime brand swap through applyClaudeCodeBrandSwap so "Claude Code" survives verbatim inside <runtime_compatibility> regions (#2284b). The fix existed only in bin/install.js's local copies; the src/*.cts exports still used a naive replace, so binding install.js to the single source -- as this phase does for the Windsurf family -- would have silently regressed those runtimes. A table-driven parity guard now covers all nine brand-swapping converters. De-duplicate the Windsurf converter family: delete the six local copies in bin/install.js and bind the four exported ones by reference, guarded by reference-identity assertions (the ADR-1508/#1675 pattern). The two unexported helpers and an unused tool table go with them. Replace the Windsurf 12,000-byte throw with description truncation, matching the bound its sibling skill converter already applied. The throw could only fire on an ~11.7 KB frontmatter description: the largest emitted workflow is 311 bytes. Truncation makes the cap unreachable by construction and leaves 12,000 in exactly one place, eliminating the dual-surface duplication rather than testing for it. Add the emitted-byte cap gate: buildEmittedSizes captures LF- and <HOME>-normalized bytes from the walk buildParityManifest already performs, and evaluateEmittedCaps asserts them against a per-runtime cap table with dead-rule detection. buildParityManifest's return shape is deliberately unchanged -- diffEmitted compares its values with ===, so making them objects would report all 8,529 emitted paths as moved. A regression test pins the values as strings. Add a deterministic trim-safety gate over composeWithinBudget's omitted/shrunk/floored/isolatePrefix metadata, with an anti-vacuity rule, replacing the model-graded eval gate the issue described. * docs(#2931): correct ADR-1671 windsurf premise and trim-safety contract * fix(#2931): bound the windsurf command name and single-source the brand swap Review findings from the orthogonal passes, all fixed inline. The claim that removing the 12,000-byte throw left total emission "bounded by construction" was false. The #1615 regex constrains the character class but not the length, and commandName is interpolated three times into the emitted workflow: a 20,000-character name emitted 60,162 bytes silently. Add WINDSURF_COMMAND_NAME_MAX=128 as a separate, clearly-labelled size control that THROWS -- commandName is the @-ref path target, so truncating it would point the workflow at a file that does not exist (DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED). The #1615 security regex is untouched and still runs first. 128 is generous: the longest shipped name is gsd-plan-review-convergence at 27. Harmonize convertClaudeCommandToWindsurfSkill onto the code-point-safe truncation helper. It still used a UTF-16 slice(0,177) -- the exact surrogate-splitting bug the helper was written to avoid, in the very sibling the helper's comment cites as its model. Bounds are unchanged, so output is byte-identical for every shipped command (descriptions max out at 99 chars). Export applyClaudeCodeBrandSwap and bind it in bin/install.js, deleting the local copy. Adding it to the .cts left two unlinked implementations of identical logic -- the drift class this change exists to remove. Verified byte-identical across eight fixtures and five sequential calls before merging, and guarded by a reference-identity assertion. Convert three try/finally test bodies to t.after (CONTRIBUTING.md:344), add fast-check property coverage for the trim-safety contract, and use fc.pre instead of a bare return in a property callback. * test(#2931): fix three test-authoring bugs the remote matrix caught The remote runner returned 8 unique failures on 6f15cdeb8. All three causes were in the test files, not the modules under test -- local harnesses exercise the modules directly, so nothing executed the test bodies until the matrix did. `{ __proto__: [...] }` in an object literal sets the prototype instead of an own key, so the JSON round-trip erased it and the cap table never saw a reserved runtime key. The production rejection was already correct; the test could not reach it. Use a computed key. Two cap fixtures tripped orthogonal error paths rather than the paths they name: one declared windsurf in the cap table but omitted it from sizes (UNKNOWN_RUNTIME), the other left the sole windsurf pattern matching nothing (a genuine dead rule). Both now include a compliant artifact so the intended branch is what is asserted. The dead-rule and unknown-runtime contracts are deliberate and unchanged. `const { root } = makeSyntheticConfig({ ... `${root}` })` referenced `root` from inside its own initializer -- a temporal dead zone error. makeSyntheticConfig now optionally takes a (root) => files factory. Also raise the npm pack --dry-run bound 60s -> 120s in the shipped- scripts packaging test. That failure is NOT from this branch: the file is byte-identical to next, a fresh tsc measures 1.98s there vs 2.14s here, and the run recorded 60,637ms against a 60,000ms bound -- a timeout under 28,948-test parallel contention, not a slowdown. Fixed rather than deferred because a bound that tight is fragile regardless of which branch trips it. * chore(#2931): backfill changeset pr number to 2984 --------- Co-authored-by: sim <sim@local> |
||
|
|
4df6d884b3 |
fix(#2641): treat absent capture_artifacts as enabled (schema default) (#2982)
* test(#2641): add regression for mempalace-capture gate default inversion * fix(#2641): treat absent capture_artifacts as enabled (schema default) The gate used `capture_artifacts !== true` which treated absent (undefined) as disabled — inverted from the capability registry's declared default of true. Changed to `capture_artifacts === false` (disabled only on explicit false), matching the sibling gsd-mempalace-recall skill's correct pattern. * chore(#2641): add changeset fragment * chore(#2641): backfill changeset PR number 2982 --------- Co-authored-by: sim <sim@local> |
||
|
|
6c96b13cfe |
fix(#2639): warn when local is ahead of origin before forking phase branch (#2981)
* test(#2639): add regression for local-ahead-of-origin warning in handle_branching execute-phase.md's handle_branching forks from origin/$DEFAULT_BRANCH. When local is ahead (unpushed commits), the phase branch silently misses them. The test asserts the workflow checks for local-ahead-of-origin and warns. * fix(#2639): warn when local is ahead of origin before forking phase branch handle_branching forks the phase branch from origin/$DEFAULT_BRANCH. When local $DEFAULT_BRANCH is ahead (unpushed commits like plan/research docs), the fork silently misses those commits. Now a loud WARNING is printed to stderr naming the commit count and advising the user, matching the existing uncommitted-changes warning pattern. * chore(#2639): add changeset fragment * fix(#2639): condense warning under ADR-857 cap + merge emitted-drift ack gsd-test gate caught: (1) execute-phase.md exceeded the 93600-byte Phase 6 ceiling — condensed the WARNING from 3 echo lines to 1. (2) emitted-attribution flagged the growth without an ack — merged into the existing #2930 ack fragment (execute-phase.md was already acked there; can't have two acks for the same path). * fix(#2639): condense warning further to clear the 93400 comfortable-margin gate The ADR-857 Phase 6 test has two assertions: <93600 (hard ceiling) and <=93400 (comfortable margin). Condensed from 3 lines to 2 to fit under 93400 (now 93369). * chore(#2639): backfill changeset PR number 2981 --------- Co-authored-by: sim <sim@local> |
||
|
|
34633fa4ec |
fix(#2893): preserve prose below the JSON ledger on windows append/waive/fixed (#2975)
* test(#2893): add regression for append destroying prose below JSON ledger writeLedgerAtomic overwrites the entire file with renderLedger(ledger), dropping any prose below the JSON closing fence. The test creates a WINDOWS.md with prose sections below the ledger, appends an entry, and asserts the prose survives. * fix(#2893): preserve prose below the JSON ledger on append/waive/fixed writeLedgerAtomic was overwriting the entire WINDOWS.md with renderLedger(ledger), which reconstructs only the frontmatter + header + table + JSON block — silently destroying any prose a user wrote below the JSON closing fence. Now the writer reads the existing file before overwriting, extracts content after the closing fence, and appends it to the rendered ledger. First-write (no existing file) proceeds normally with no prose to preserve. * fix(#2893): address review — correct fence search + idempotency test BLOCKER from isolated adversarial review: indexOf(JSON_FENCE_CLOSE) matched the OPENING fence ('json' starts with ''), duplicating the entire JSON body as prose on every write. Now searches for the closing fence starting AFTER the opening fence, mirroring parseJsonBlock. Test hardened: non-empty initial ledger, second append (idempotency — prose appears exactly once, exactly one JSON fence open), parseLedger round-trip. * chore(#2893): add changeset fragment * chore(#2893): backfill changeset PR number 2975 --------- Co-authored-by: sim <sim@local> |
||
|
|
640eaee16e |
chore(#2930): fragmentize execute-phase.md and prove per-runtime composed emission (#2972)
* feat(#2930): fragmentize plan-phase.md workflow into per-runtime-composed sections Adds src/workflow-fragments.cts (in-file <!-- gsd:section --> marker parser/composer, ADR-1671 epic #1671 Phase 3), wires it into bin/install.js's copyWithPathReplacement emission path, and pilots the marker grammar on gsd-core/workflows/plan-phase.md. Bookkeeping ripple for the new src/*.cts module: .gitignore, eslint.config.mjs, docs/INVENTORY.md + docs/INVENTORY-MANIFEST.json, and a CONTEXT.md glossary entry. Amends ADR-1671 with open questions 1 and 2 resolutions and records the closed when= applicability grammar. Adds docs/reference/workflow-fragments.md and an ARCHITECTURE.md section documenting the marker authoring model. * fix(#2930): put allow-test-rule issue ref on the same line as the marker lint-allow-test-rule-refs.cjs requires the #NNN issue reference on the same source line as `allow-test-rule:`; it was one line below and read as an unreferenced novel exemption. * docs(#2930): link the orphaned gate-predicates reference from the docs index Found while adding the workflow-fragments reference doc: docs/reference/gate-predicates.md shipped without an entry in docs/README.md, so it was unreachable from the docs index. Fixed inline rather than deferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#2930): scope composition to workflows, add typed failure reasons Review findings from two orthogonal passes: - Scope composeWorkflow to gsd-core/workflows/ only. It previously ran on every .md the installer copied, so a future agent/command/reference doc documenting the marker syntax with an unfenced example would have been mis-parsed and silently stripped — a lossy drop the phase forbids. - Add a frozen REASON enum; failures attach a typed .reason and tests assert on it instead of matching free-form message text (CONTRIBUTING.md:635-694). - Derive the property generator's when= values from WHEN_VOCABULARY instead of duplicating them (DEFECT.GENERATIVE-FIX). - Add adversarial parser fixtures: Unicode headings, NUL, U+FFFD, BOM, fence-within-fence, tilde and indented fences, lone-CR marker line. - Document why --mvp is structurally unmarkable: its content is interleaved, not sectioned, so the whole-line grammar cannot reach it. Also fixes two stale tests on this branch, each reproduced on the unmodified tree before correction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#2930): retarget the pilot from plan-phase to execute-phase The full remote matrix went red on both Linux lanes. Root cause was ours: tests/phase6-capstone-conformance.test.cjs holds a PRE_PHASE6 ceiling of 94519 bytes for plan-phase.md, asserting an ADR-857 Phase-6 completion property. That is a third size gate beyond the tier caps and the differential ratchet, and it left plan-phase.md just 36 bytes of headroom rather than the 3821 computed from the XL cap. The 330 marker bytes overran it by 294. Raising the ceiling is not an option: it is a red line certifying another ADR's completion. plan-phase.md is reverted to byte-identical origin/next and the pilot moves to execute-phase.md, which has 728 bytes of headroom under its own ceiling and lands at 93147 with 3 marker pairs. The vocabulary narrows to the atoms actually used: always, flag:--wave, state:gap-closure-phase, state:has-prior-phases. Recorded in the ADR: every branch the epic names lives in plan-phase.md, which cannot be fragmentized until caps move from source to emitted bytes. That is direct evidence for the epic's premise and may reorder phases 3-4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(#2930): backfill changeset PR number (#2972) * fix(#2930): make the emission install tests portable on Windows The windows-latest lane went red on three tests in the new install suite; Linux was green. Both causes were in the test harness, not the module. Root normalization: the opencode converter always embeds the install root forward-slashed, but the tests stripped it with the native-separator string from mkdtemp. On Windows that never matched, so the root leaked through unstripped — and because the real and stub install roots have different prefix lengths, that length difference landed directly in the byte-delta assertion (344 observed vs 275 expected). Normalize both text and root to one separator form before stripping. @-ref resolution: the helper stripped only the @~/ and @$HOME/ forms, so a Windows absolute ref (@C:/Users/...) fell through and was joined onto the root, producing ...\@C:\Users\... Strip the @ first, then detect absoluteness from the token's own shape (POSIX, drive-letter, or UNC) with no platform branching, so every OS takes the same path. Neither assertion was weakened; the exact-equality byte check is the point of the test and still holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(#2930): document every REASON member and guard the doc/enum parity Code review found the reference doc's 'Fails closed' list covering 10 of the 11 frozen REASON members — MALFORMED_ATTRIBUTES (parseAttrs rejects malformed key="value" syntax) had no bullet, and it is distinct from UNRECOGNIZED_ATTRIBUTE, which is valid syntax with an unknown key. Two parallel surfaces sharing one constant with nothing asserting they agree is the DEFECT.GENERATIVE-FIX class, so the same commit adds the parity assertion: the test derives the enum side from the built module and the doc side by parsing the reference page, keyed on the reason IDENTIFIER rather than prose so a reworded bullet does not break it, and reports set differences in both directions by name. Proven non-vacuous: removing the MALFORMED_ATTRIBUTES bullet turns the suite red naming that exact member; restoring it returns 44/44. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f0bb0787c9 |
fix(#2640): report truthful state_updated + keep progress frontmatter in sync after phase remove (#2974)
* test(#2640): add regression for state_updated false positive + stale progress Three cases: (1) state_updated reflects actual content change, not just file existence; (2) progress.total_phases resync'd even when the body lacks 'Total Phases:' (the no-op guard was skipping syncStateFrontmatter); (3) state_updated is false when STATE.md doesn't exist. * fix(#2640): report truthful state_updated + force frontmatter resync Two defects in cmdPhaseRemove: 1. state_updated was fs.existsSync(statePath) — trivially true, since the file existed before and readModifyWriteStateMd never deletes it. Now captures the boolean return from readModifyWriteStateMd (changed from void to boolean: true when content was written, false on no-op). 2. progress.* frontmatter stayed stale when the body lacked 'Total Phases:' or 'of N' — readModifyWriteStateMd's no-op guard (#948) skipped syncStateFrontmatter when the body transform was unchanged. Now the transform forces a body diff when a phase was actually removed, so the guard passes and syncStateFrontmatter rebuilds progress.* from the post-deletion disk/ROADMAP state. * fix(#2640): address review — gate forced-diff on targetDir, strengthen assertions Two MAJOR findings from isolated adversarial review: 1. Forced-diff injected a spurious 'Total Phases:' line even when no directory was removed (targetDir === null). Now gated on targetDir !== null. 2. Test #2 asserted 'not 3' instead of '2' — would pass for any wrong count. Now asserts exact value. Test #1 strengthened to assert body Total Phases and frontmatter total_phases both equal 1. * chore(#2640): add changeset fragment * chore(#2640): backfill changeset PR number 2974 --------- Co-authored-by: sim <sim@local> |
||
|
|
000a322489 |
fix(#2941): hint at global: prefix when bare skill name matches a global skill (#2973)
* test(#2941): add regression for bare-skill-name global: hint When a bare skill name matches an existing global skill, the skip warning must hint at the global: prefix. When no global skill matches, the original 'Skill not found' warning is unchanged. * fix(#2941): hint at global: prefix when a bare skill name matches a global skill When buildAgentSkillsBlock skips a bare skill name that doesn't exist as a project-relative path, check if it matches an existing global skill. If so, append a hint to the warning: 'a global skill named X exists; use global:X to reference it'. When no global skill matches, the warning is unchanged. getGlobalSkillDir and getGlobalSkillsBase are already imported in this module for the global: branch. The hint is guarded on globalSkillsBase being non-null (runtimes without a skills directory don't support the prefix). * chore(#2941): add changeset fragment * chore(#2941): backfill changeset PR number 2973 --------- Co-authored-by: sim <sim@local> |
||
|
|
608be0e7cf |
fix(#2913): distinguish empty cherry-pick from genuine conflict in hotfix create (#2970)
* test(#2913): add regression for hotfix empty-cherry-pick discrimination Two layers: (1) real-git test proving the discrimination logic (check for unmerged paths → skip if empty, abort if conflict) is correct; (2) source-text assertions proving the logic and summary heading are in release.yml. Before the fix: release.yml treats any non-zero cherry-pick exit as a conflict, so an already-applied commit (empty pick) aborts the entire hotfix create. * fix(#2913): distinguish empty cherry-pick from genuine conflict git cherry-pick exits non-zero for BOTH genuine conflicts AND empty picks (the change is already present by content). The hotfix create job treated every non-zero exit as a conflict, so an already-applied commit (notably the structural 'chore: sync next package version' that follows every release finalize) aborted the entire run. Now the error handler checks for unmerged paths (git diff --diff-filter=U): - No unmerged paths → already applied by content → skip, record, continue. - Unmerged paths present → genuine conflict → existing abort/push/exit-1 behavior and operator guidance, unchanged. The job summary now has a separate 'Skipped (already applied by content)' heading, distinct from 'Skipped (feat/refactor/etc)'. * fix(#2913): address review — post-skip continuation test + conflict observability Two findings from isolated adversarial review: 1. MINOR (test gap): no test proved the sequencer is clean after --skip, so a regression switching --skip to --quit would pass green. Added a second cherry-pick after the skip asserting it succeeds. 2. MINOR (observability): SKIPPED_EMPTY was dropped on the conflict-exit path — already-applied commits before a genuine conflict were silently lost from the summary. Now the conflict summary emits them under a dedicated heading. * chore(#2913): add changeset fragment * chore(#2913): backfill changeset PR number 2970 --------- Co-authored-by: sim <sim@local> |
||
|
|
d3305fc3a5 |
fix(#2858): exclude gen-emitted-baseline.cjs from npm tarball + class-extinction guard (#2968)
* test(#2858): add class-extinction guard for shipped-script require boundary Every shipped scripts/**/*.cjs must be require-able using only shipped paths. The guard resolves the tarball file list via npm pack --dry-run --json (not a hardcoded list) and statically checks each require() call against the shipped set. A script requiring ../tests/** (which does not ship) is a violation. * fix(#2858): exclude gen-emitted-baseline.cjs from the npm tarball scripts/gen-emitted-baseline.cjs is repo-only CI tooling (CI workflows + test fixtures spawn it from a checkout). It requires three modules from tests/, which does not ship — so in a published install it is MODULE_NOT_FOUND at load time. Add a targeted files[] negation (!scripts/gen-emitted-baseline.cjs) so the script stays in the repo for CI use but does not ship. Other scripts that ship and are required by bin/install.js (build-hooks.js, fix-slash-commands.cjs, gen-capability-registry.cjs) are unaffected. The class-extinction guard test in tests/packaging-shipped-scripts-require-only-shipped.test.cjs ensures no shipped script can require outside the shipped tree going forward. * test(#2858): widen guard to .js + strip block comments (review fixes) Two findings from isolated adversarial review: 1. MAJOR: the guard only checked .cjs files, but scripts/build-hooks.js ships and is required by bin/install.js — a .js file with a broken require would bypass the guard. Widened filter to /\.(cjs|js)$/. 2. MODERATE: the static parser could false-positive on require() calls inside /* */ block comments or inline // comments. Now strips both before matching. * chore(#2858): add changeset fragment * chore(#2858): backfill changeset PR number 2968 * fix(#2858): add issue ref to allow-test-rule exemption (ADR-456) CI lint-tests caught: the allow-test-rule comment needs a 'see #NNN' ref per ADR-456. Added '(see #2858)' to the integration-test-input exemption. --------- Co-authored-by: sim <sim@local> |
||
|
|
1db7dcd9bf |
fix(#2849): strip trailing hyphen after 60-char slug truncation (#2967)
* test(#2849): add failing regression for trailing-hyphen-after-truncation The strip ran before .substring(0, 60), so a cut landing on a separator produced a slug ending in '-'. Four cases: the exact issue repro (59 a's + space + tail), a boundary landing before a separator, leading-hyphen survival, and a long-Cyrillic transliteration+truncation case. * fix(#2849): strip trailing hyphen after 60-char truncation generateSlugInternal ran the ^-+|-+$ hyphen strip BEFORE .substring(0, 60), so a title whose 60-character cut landed on a separator yielded a slug ending in '-' — the very thing the strip step exists to prevent. Reorder so the strip runs after truncation. Truncation cannot introduce a leading hyphen, so the full ^-+|-+$ pass last is equivalent for leading hyphens and fixes the trailing-hyphen-after-truncation case. Latin-script output for titles ≤ 60 chars is byte-identical; only titles whose truncation boundary lands on a separator change (from broken to clean). * test(#2849): add all-separator collapses-to-empty boundary case Surfaced by isolated adversarial review: pin the contract that input which is entirely separators ('!!!', '!'.repeat(70)) reduces to '' — not null, not a stray hyphen — both short and past the 60-char truncation. * chore(#2849): add changeset fragment pr:0 placeholder; will backfill the real PR number after the PR exists. * chore(#2849): backfill changeset PR number 2967 --------- Co-authored-by: sim <sim@local> |
||
|
|
0bb7525a62 |
fix(#2943): rename get-library-docs -> query-docs; correct the ctx7 fallback rationale (#2963)
* test(#2943): parity guard against the nonexistent get-library-docs tool Second context7 naming drift after #2017 (which guarded the plugin-marketplace PREFIX). #2017's guard only checks tools: frontmatter lines, not prose bodies — which is where the broken tool NAME (get-library-docs) lived. The context7 MCP server registers only resolve-library-id and query-docs; get-library-docs is a stale copy from upstream's own README. Scans the shipped prose surface (agents/, gsd-core/references|workflows/, commands/gsd/, skills/) and fails if any artifact instructs an agent to call mcp__context7__get-library-docs. Excludes tests/ (a fixture may use the name as a negative input) and CHANGELOG/RELEASE-NOTES-LEGACY (history). Fails-first: 4 offenders today (gsd-executor.md:29, research-documentation-lookup.md:5, discovery-phase.md:68 & :104). * fix(#2943): rename get-library-docs to query-docs and correct the ctx7 fallback rationale The context7 MCP server registers only resolve-library-id and query-docs (verified against upstream packages/mcp/src/index.ts); get-library-docs is a stale name copied from upstream's own README. Four shipped prose sites instructed agents to call a tool the server does not register, so every research path that loaded the canonical reference either errored, fell through to the ctx7 CLI branch, or fabricated a result. - research-documentation-lookup.md, gsd-executor.md, discovery-phase.md (x2): get-library-docs -> query-docs, params context7CompatibleLibraryId/topic -> libraryId/query (the registered contract). - Same files' ctx7 CLI fallback rationale: the cited cause (anthropics/claude-code#13898 'strips MCP tools from agents with a tools: frontmatter restriction') was wrong on two counts — #13898 is closed and was never about tools: frontmatter. Rewritten to describe the real mechanism (custom subagents cannot see project-scoped .mcp.json; they only inherit user-scoped ~/.claude/mcp.json). The fallback itself is kept. - discovery-phase.md 'mode: code/info' dropped — query-docs takes libraryId + query only; the code-vs-concepts intent is now expressed via the query text. resolve-library-id is unchanged (still registered upstream). CHANGELOG and RELEASE-NOTES-LEGACY citations are historical record, left as-is. * chore(#2943): add changeset fragment (pr:0 placeholder) * test(#2943): widen parity-guard scan surface to docs/ (isolated-review finding) The isolated adversarial review flagged that SCAN_DIRS omitted docs/, which ships docs/AGENTS.md — agent-consumed prose carrying 8 mcp__context7__* refs. No false negative today (it uses only the wildcard), but a future banned-name addition there would slip through, recreating the exact drift this guard exists to prevent. Add docs/ to the scan surface, with an EXCLUDED_FILES set for historical record (docs/RELEASE-NOTES-LEGACY.md, CHANGELOG.md) that must not be rewritten to satisfy the guard. * fix(#2943): update shifted PROSE_ALLOWLIST line + acknowledge gsd-executor.md growth The gsd-test gate caught two real consequences of the rationale rewrite in agents/gsd-executor.md (the +2-line corrected mechanism description shifted line numbers below it): 1. tests/no-bare-gsd-tools-command-position.test.cjs: the legitimate 'gsd-tools query commit' descriptive mention moved from line 791 -> 793. Update the PROSE_ALLOWLIST entry to the new line (the mention is unchanged, just relocated by my edit above it). Without this the gate reports both a stale allowlist entry (791) and a new offender (793) for the same mention. 2. tests/emitted-drift-acks/2943-context7-tool-name.json: gsd-executor.md grew 95 bytes (the accurate mechanism rationale is longer than the wrong one-line #13898 attribution it replaces). Acknowledge the growth with the reason. Both are mandated by the gate, not optional. The rename itself (get-library-docs -> query-docs) is byte-neutral-ish; only the rationale rewrite grew the file. * chore(#2943): backfill changeset PR number 2963 --------- Co-authored-by: sim <sim@local> |
||
|
|
73418c516f |
fix(#2956): scope Phase extraction to ## Current Position (3rd gen of #2444/#2567) (#2961)
* test(#2956): fail-first regressions for Phase scoped to ## Current Position Third generation of #2444 / #2567. Stopped At / Paused At were scoped to ## Session; Phase (canonically in ## Current Position per templates/state.md) was left unscoped, so a historical Phase: / **Phase:** line in an archive section overwrites current_phase on every write. Since current_phase is routing input for gsd-progress / --next, the rewind routes work to the wrong phase. Six failing-first regressions + one round-trip: - shape B: bold **Phase:** 19 archive BELOW the section - shape C: plain archive Phase: 19 ABOVE the section - bootstrap h3 ### Current Position variant - CRLF variant - Phase token in decisions prose (over-broad-fix guard) - Paused At read-path parity with the write seam (## Session) - write-then-read round trip stays at 22 (read/write agreement) Folded into tests/state.test.cjs (lint:regression-test-names bans a new tests/bug-NNNN-*.test.cjs file). * fix(#2956): scope Phase extraction to ## Current Position at both seams Third generation of #2444 / #2567. Stopped At / Paused At were scoped to ## Session by those fixes; Phase (canonically in ## Current Position per templates/state.md) was left unscoped, so a historical Phase: / **Phase:** line in an archive section silently overwrote current_phase on every write. Because current_phase is routing input for gsd-progress / --next, the rewind routes work to the wrong phase. Fix mirrors the proven #2444 seam exactly: - new matchCurrentPositionSection helper (collectSection-based, CRLF-tolerant, level-flexible for the bootstrap ### Current Position h3 variant), sited next to matchSessionSection. - read path (cmdStateSnapshot): extract Phase from matchCurrentPositionSection ?? body. Also scope Paused At to matchSessionSection ?? body so the read seam agrees with the write seam (which already scoped Paused At to ## Session). - write path (buildStateFrontmatter): extract Phase from matchCurrentPositionSection ?? bodyContent. stateExtractField itself is untouched (its bold/plain precedence is load-bearing for other fields — the #3265 test depends on it), and preferNewerLastActivity is untouched (Last Activity has no canonical section; its date-direction guard is deliberate). Fall back to full-body when no ## Current Position section exists so files without the heading keep current behaviour. * chore(#2956): add changeset fragment (pr:0 placeholder, backfill after PR) * test(#2956): make round-trip test actually trigger the write-path resync The write-then-read round-trip test used 'state update Status "Executing"' on a fixture with no Status field, so the update was a no-op (updated:false) and no frontmatter resync ran through buildStateFrontmatter — the assertion on the written frontmatter then failed not because the fix is wrong, but because no write happened. Add a **Status:** field so the update performs a real field update (updated:true) and forces the resync. Verified locally: pre-fix this writes current_phase:19 (the archive value); post-fix it writes 22. The code fix is correct (5 of 7 RED tests passed; the 2 failures were this defective test). This is the 'fix the bad test' half of the TDD-loop rule. * chore(#2956): backfill changeset PR number 2961 --------- Co-authored-by: sim <sim@local> |
||
|
|
9ac0dfad58 |
chore(#2929): generalize prompt-budget into the shared context-composer seam (#2958)
* test(#2929): capture prompt-budget parity corpus pre-refactor Phase 2 of epic #1671 generalizes prompt-budget's trim ladder into a shared context-composer seam. Its success condition is that review-prompt output does not change, and the only authority on "did not change" is the behavior that shipped before the refactor. Capture that behavior now, while it is still the live implementation. 47 characterization cases, every `expected` value computed by executing the current implementation rather than hand-authored — the independence CONTRIBUTING.md "Fixture provenance (#2371)" asks for. A corpus is only worth what it can detect, so this one was validated by mutation rather than assumed. Five deliberate defects were injected and each must be caught by at least one case: - the note reserve deducted unconditionally instead of only under pressure - the pressure test relaxed from `>` to `>=` - a no-op head-shrink still setting the shrunk flag - the per-plan floor dropped from the proportional share - drop order reversed Two of those exposed real holes in the first cut of this corpus, and the cases that close them exist because of it: - `>=` was caught by NOTHING. At exact cap the only trimmable fragment was a floored plan group, and the 1024-char floor absorbed the entire trim, so the mutation was byte-invisible. A3b/A3c put a droppable at exactly the cap, which makes the strict inequality observable as context kept vs omitted. - No case reached proportional-truncate at all — B6 and B7 both hard-failed the min-set pre-check first, leaving planTruncationPct at 0 across every case and the floor semantics entirely unexercised. Rebudgeted to 700 and 1100 so the min-set fits and the truncate step is actually reached; they now record 40.20% and 48.80%. The A4/A10 families sweep the pressure boundary from both sides, which is where this function has regressed before: CONTEXT.md's LEARNING.prompt-budget.boundary-gap records PR #3708 shipping two regressions that only fired when the baseline sat inside the NOTE_RESERVE_TOKENS band, because the suite paired a trivially-fitting budget with a trivially-overflowing one and never sampled between them. A4 pins that nothing is trimmed from the cap down to 81 tokens under it; A10 pins that pressure fires at +1. Together with A3b/A3c they satisfy row (d) of RULESET.TESTS.boundary-coverage.fixtures. Two facts the corpus establishes that the design notes had wrong: - "" and null sections are NOT distinguished. applyBudget uses truthy checks throughout, so an empty-string section is treated as absent: not rendered, not dropped, never recorded in `omitted`. B13b pins this while the ladder is actively trimming, where only the non-empty `research` is dropped. - Sizing matters. B12/B13 were first written at a budget where both hard-failed the min-set check and returned "", so comparing them compared two empty strings and proved nothing. Committed as its own commit, ahead of the refactor, and regenerated against the pre-refactor implementation, so the oracle is demonstrably independent of the change it will adjudicate. Refs #2929 * refactor(#2929): extract the context-composer seam from prompt-budget Epic #1671 needs prompt-budget's budget-trimming logic for a second consumer — per-runtime artifact emission — but it is walled inside the cross-AI review pipeline. Lift it into a shared seam so later phases can call it, without changing what the review pipeline emits. ADR-1671 specifies the composer as "priority + binary-search cutoff to a per-runtime budget". Read against the code it generalizes, that contract cannot express the thing being generalized. applyBudget is not a cutoff: it is a fixed five-step ladder in which each section carries its own shrink strategy, and only three of its eight sections are ever dropped. PROJECT.md is head-shrunk to N lines; plans are proportionally tail-truncated with a per-plan 1024-byte floor; instructions and roadmap are never touched at all. A cutoff composer sorts by priority and discards the tail — it has no way to say "shrink this one", "truncate that one but never below 1 KB each", or "these three are the only droppables, in this order". Building to the literal contract and routing prompt-budget through it would have silently changed review-prompt output, which is the one outcome this phase forbids. So shrink strategies are the core abstraction here, and cutoff becomes one strategy among them — the right one for per-runtime emission in Phases 3-4, not for this ladder. That is an elaboration of the ADR's intent, not a departure from it, and ADR-1671 is updated to say so. Three decisions worth stating: - The composer DECIDES; the caller RENDERS. composeWithinBudget returns a plan of surviving fragments and never a string. assemblePrompt's rendering is prompt-shaped (`## Roadmap`, `### <file>`, the note in position two), and owning it in the composer would force emission to adopt prompt-shaped rendering. The split is what lets one seam serve both consumers. - The budget unit is INJECTED via `measure(text)`. prompt-budget passes its chars/4 estimator; emission will pass a byte counter, which ADR-1671 requires for emission caps. The existing code converts a token budget to a character budget with a hardcoded `* 4`; that assumption is now an explicit `charsPerUnit` inverse, which is precisely what a byte unit needs in order to reuse this. - The entry point is `composeWithinBudget`, not `applyBudget`. That name already exists twice — src/prompt-budget.cts and src/graphify.cts, the latter being an unrelated graph-edge budget. A third would make every symbol search in this repo ambiguous, and it already misresolves: preflight and impact queries for "applyBudget" return graphify's. Behavior is unchanged and proven so: all 47 characterization cases reproduce byte-identically, and the corpus is mutation-validated rather than merely green (see the preceding commit). prompt-budget.cts drops from 436 to 343 lines and from eighteen mutable accumulators to two, both inside a helper copied verbatim. estimateTokens deliberately stays in prompt-budget and keeps its exact math: src/phase-estimation.cts re-exports it as measureTokens, and CONTEXT.md pins plan estimates and recorded actuals to that same scale, so moving or changing it would silently break the calibration loop. Refs #2929 * docs(#2929): document the context-composer seam and amend ADR-1671 Adds the INVENTORY row, the CONTEXT.md glossary entry (a PR gate for new domain modules), and a mutation-matrix entry for the new module. The ADR amendment is the substantive part. ADR-1671 specified the composer as "priority + binary-search cutoff to a per-runtime budget". Implementing Phase 2 established that a cutoff alone cannot express the function the platform generalizes, so the ADR now records shrink strategies as the core abstraction with cutoff as one strategy among them, reserved for per-runtime emission in Phases 3-4. Recording it in the ADR matters because Phases 3-6 are planned against that contract and would otherwise be planned against a mechanism that does not work. The mutation-matrix entry is not bookkeeping. Stryker scores per module against a named .cjs, so relocating the ladder out of prompt-budget.cjs would leave the extracted code unmeasured while prompt-budget's own score floated free of the logic it used to cover. context-composer gets its own entry at the same floor. Refs #2929 * test(#2929): pin the effectiveBudget rounding mode in the parity corpus An isolated correctness review found a real blind spot: mutating `Math.floor` to `Math.round` in the effectiveBudget calculation failed ZERO of the 47 corpus cases. Every (budget, safetyMarginPct) pair in the generator happened to produce a whole number, so floor, round and ceil all agreed and the rounding mode was entirely unpinned by a corpus whose whole job is to pin observable behavior. Three cases fix that by straddling the .5 boundary: A11 95 * 0.90 = 85.5 floor 85, round 86 -> the two disagree A12 97 * 0.90 = 87.3 floor and round agree; ceil (88) does not A13 93 * 0.85 = 79.05 same guard at a non-multiple-of-10 margin, so the margin arithmetic is exercised and not just the budget A11 alone catches the round mutation; all three catch ceil. Regenerated against the pre-refactor implementation (`git show 9557f8552:src/prompt-budget.cts`), so the expanded corpus keeps the independence property the original capture had. The corpus is now mutation-validated against seven injected defects, every one caught: unconditional note reserve, `>` relaxed to `>=`, no-op head-shrink setting its flag, the truncate floor ignored, drop order reversed, and both rounding-mode changes. Refs #2929 * feat(#2929): flexReserve floors and the byte-stable isolate prefix Two of issue #2929's "Done when" items were unimplemented rather than deferred, and an isolated review flagged them alongside my own audit. Both are part of ADR-1671's composer contract, so shipping the seam without them would have left Phases 3-4 building against a contract that does not exist yet. flexReserve is a per-fragment floor in measure units that every strategy must respect, which is what makes it different from the pre-existing floorChars: that one is a chars-denominated detail of proportional-truncate alone and is retained unchanged. A floored fragment is never dropped, is never head-shrunk below its floor, and raises its own proportional cap. A fragment already smaller than its floor is untouchable outright. Metadata gains `floored`, listing the ids whose floor actually prevented a trim — a guarantee no caller can observe is a guarantee no test can hold you to. isolate marks the byte-stable canonical prefix the ADR calls for: never trimmed, never dropped, but still counted, because a prefix excluded from accounting would silently under-count real context. Metadata gains `isolatePrefix` so a caller can hash or assert on the exact bytes. Declaring an isolate fragment after a non-isolate one throws: a prefix that is not at the front is not a prefix, and accepting it would make the cross-runtime stability claim meaningless. Adds tests/context-composer.test.cjs for the exact new semantics and tests/context-composer.property.test.cjs for the five invariants, including the budget-monotonicity property the issue names explicitly. Both are registered in the mutation matrix, since coverage does not migrate with relocated code. prompt-budget uses neither feature, and its output is unchanged: all 50 corpus cases still reproduce byte-identically. Refs #2929 * chore(#2929): allowlist the prompt-budget parity suite The parity corpus needs its own test file and that makes prompt-budget a three-file module against a limit of two. The lint offers consolidation or an allowlist entry with justification; the entry is the right call here. Consolidation would mean folding the characterization suite into prompt-budget.test.cjs, which is the one thing that should not happen to it. The parity suite is a distinct concern with a distinct lifecycle: it is generated rather than hand-written, it is named by scripts/mutation-matrix.cjs as its own scoring target, and its failure means something categorically different from a unit-test failure — not "this behavior is wrong" but "observable output moved". Burying it inside a general unit file would obscure exactly that signal. The allowlist is an identity ratchet, so this entry pins today's three exact filenames: adding a fourth still fails, and dropping back to two requires removing the entry. Refs #2929 * fix(#2929): register the new module with two gates it was missing The remote matrix caught three defects that no local check could, because the local runner is blocked in this repo and these suites had therefore never executed. Eight failures, identical on node22 and node24, so nothing environment-shaped. Two are the new-module ripple. A net-new src/*.cts lands in six places and this change had reached four of them — .gitignore, INVENTORY, the manifest, and the CONTEXT.md glossary — while missing the ESLint ignore list (tsc OUTPUTS must not be linted; repo-invariants asserts linted-xor-ignored) and the mutation ratchet baseline (a deliberate review-visible mirror of the matrix floors, which every COVERED module must carry). Both are now registered, the ratchet at the same floor of 66 the matrix declares. The third was a test asserting an outcome it had made impossible. It set budget:1 alongside a 400-char required fragment, so the group budget came out at -99 and the proportional-truncate step was skipped entirely — the deliberate "non-positive group budget is skipped, never clamped" rule inherited from the original ladder. Nothing was trimmed, and the test then asserted a truncation. Rebudgeted so the step actually runs, with the arithmetic written out in a comment so the next reader does not have to re-derive why 120 rather than 80. Fixing that surfaced a genuine bug in the composer. `floored` is documented as recording fragments whose flexReserve prevented a trim that would otherwise have happened, but the push sat in the else-branch of "content did not change", so it only fired when nothing was trimmed at all. A fragment truncated to a reserve-raised cap has also had a trim prevented — 40 characters' worth in the test above — and was silently absent from the field that exists to make the guarantee observable. The condition was already right; it was in the wrong branch. Now recorded on both paths: a drop prevented outright, and a truncation capped higher than the share alone would have allowed. Parity is unaffected — prompt-budget never sets flexReserve, so the branch is unreachable from every corpus path, and all 50 cases still match. Refs #2929 * chore(#2929): backfill changeset PR number (#2958) * chore(#2929): correct the corpus case count in the changeset fragment --------- Co-authored-by: sim <sim@local> |
||
|
|
f6257f3745 |
ci(#2952): shard the full test lane instead of widening its cap (#2960)
* ci(#2952): budget CI job timeouts by headroom over measured cost `origin/next` was red. The only failing check was `Required tests`, and its sole cause was `test (ubuntu-latest, 24)` reported `cancelled` — GitHub's conclusion for a job that exceeds its `timeout-minutes`, not a button press. That job's `ubuntu-latest / 24` entry is the only `scope: full` matrix entry: it runs the whole unit suite under c8 coverage, then the scripts/ coverage floor, integration, security, install and slow, serially on one runner. On next@5a0a9f097 it ran 15m16s against `timeout-minutes: 15` and was axed 23s into `npm run test:slow`. Projected to a completed slow step (27s on the last green run) the lane costs ~15m20s. Confirmed hypothesis: the budget, not the suite. The lane had been riding the ceiling all day — 12m03s, 11m34s, 11m50s, 14m25s, 14m51s — and crossed on three of the last four full-lane runs ( |
||
|
|
f0ff23635e |
fix(#2602): discover project-local Codex agents (#2623)
* fix(#2602): discover project-local Codex agents - Select an existing local Codex agents directory before global fallback - Prove init reports the canonical local installation through compiled CJS * test(#2602): lock Codex agent precedence - Cover override, local authority, global fallback, and runtime compatibility - Exercise installed state through the compiled resolver * fix(#2602): resolve local Codex agent skills - Pass the canonical project root to the non-Claude persona fallback - Cover nested-Codex fallback and Claude compatibility through the CLI * test(#2602): cover local Codex validation status - Assert emitted validate and health commands use the project-local install - Preserve empty local-directory authority beside complete global agents * fix(#2602): align validation with local Codex discovery - Pass the resolved runtime and project root to health W010 - Resolve the validate-agents runtime before checking installation status * test(#2602): cover local Codex docs status - Assert docs-init reports an authoritative empty local install as unhealthy * fix(#2602): align docs with local Codex discovery - Pass the resolved runtime and canonical project root to the shared agent checker * fix(#2602): honor agent-skills runtime override - Resolve agent-skills fallback runtime through the canonical project resolver - Cover conflicting config and GSD_RUNTIME values through the emitted CLI * fix(#2602): ignore non-directory local agents paths - Treat only a local Codex agents directory as authoritative - Cover regular-file fallback through the emitted install checker * chore(#2602): add changelog fragment - record the user-visible local Codex agent discovery fix for PR #2623 * fix(#2602): align local agent discovery with runtime policy - Resolve Codex's local config directory through the canonical runtime policy - Use test-managed cleanup for local-agent discovery coverage * fix(#2602): discover local agents across runtimes - Prefer manifest-backed project-local installs for non-Claude runtimes - Respect runtime-specific local install roots and preserve global fallback behavior - Cover native, partial, cross-runtime, and project-root local discovery * fix(#2602): preserve agent discovery fallback - Fall back globally when local-install probes fail - Document and test symlink rejection - Align the changeset with repository format * fix(#2602): reuse local directory policy - Resolve runtimes without local config through the canonical sentinel - Document the manifest gate and refresh the context index --------- Co-authored-by: Daniel E. <daniel.e@teachingstrategies.com> Co-authored-by: Rezolv <dave@sienkowski.com> |
||
|
|
7b204ad2ac |
enhance(#2530): extend UAT checkpoint frame language pack (9 more languages) (#2564)
* feat: extend UAT checkpoint frame language pack (9 more languages) response_language is a free-form config value, but CHECKPOINT_FRAMES only covered 9 languages — any other configured language silently fell back to the English frame. Add Dutch, Polish, Russian, Ukrainian, Turkish, Hindi, Arabic, Vietnamese, and Indonesian frames plus their aliases, with a regression test asserting each resolves instead of falling back. Follow-up to #2402 (PR #2457). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: add changeset for #2527 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(#2530): list UAT checkpoint frame languages in CONFIGURATION.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(#2530): point changeset fragment at PR #2557 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#2530): address Unicode language-pack review * fix: address checkpoint language review * fix: count spacing combining marks in checkpoint width * test: verify checkpoint aliases structurally * fix: isolate RTL checkpoint frames * fix: isolate RTL checkpoint frames correctly * test(#2530): assert checkpoint aliases neither collide nor go unreachable Review Minor #1. A duplicate alias key was invisible to the existing catalog tests: the runtime object is well-formed after JS collapses the literal, the self-alias assertion still holds, and the losing language just stops resolving. tsc catches the byte-equal case (TS1117), but not the two that survive compilation — an alias whose NFC-lowercase form already belongs to another language, and an alias not in lookup form at all, which resolveCheckpointFrame() can never produce. The check reads the source literal rather than the object, since the object no longer records what was written. Both assertions are independently load-bearing: an NFD twin of an existing alias trips the collision check, an uppercase alias trips the unreachability check. Review Minor #2: changeset retyped Changed -> Added. Nine wholly new supported response_language values are an addition under Keep a Changelog, not a modification of existing behavior. * test(#2530): check alias collisions on the catalog, not its source --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> Co-authored-by: Rezolv <dave@sienkowski.com> |
||
|
|
f092c6da85 |
fix(#2649): diagnose-issues + execute-plan run worktree.base-check before worktree dispatch (#2955)
* test(#2649): failing-first — diagnose-issues + execute-plan must run base-check before worktree dispatch * fix(#2649): diagnose-issues + execute-plan run worktree.base-check before dispatch diagnose-issues.md spawn_agents and execute-plan.md Pattern A spawned worktree-isolated subagents (gsd-debugger / gsd-executor) without the pre-dispatch worktree.base-check gate that execute-phase (#683/#1369) and quick (#1941) already run. Claude Code's isolation="worktree" forks from origin/HEAD, not live local HEAD; without the gate, the documented GSD steady state (commit every step locally, push only on request) hits the verify-only worktree_branch_check guard's exit-42 halt mid-investigation with no auto-degrade. Mirror the quick.md #1941 pattern: before dispatch, run `gsd_run query worktree.base-check --pick shouldDegrade`; if true, print its message + a #2649 warning to stderr and set USE_WORKTREES=false (sequential main-tree dispatch). The verify-only guard stays as a backstop in both cases. Per the triage and #2649 acceptance criterion 5, execute-plan.md's Pattern A (identified as a second site with the identical gap) is fixed in the SAME change — same bug class, same one-line gate, two workflow files — rather than filed as a separate follow-up. * fix(#2649): ack the diagnose-issues + execute-plan growth (per-PR fragment) The two workflow files grew vs next (diagnose-issues.md +1381, execute-plan.md +905) adding the #2649 base-check gate. emitted-attribution requires an ack; this is a per-PR fragment under tests/emitted-drift-acks/ (#2914 mechanism, replacing the legacy shared emitted-drift-ack.json). * test(#2649): tighten base-check ordering assertion + guard backstop survival Address code-review minors: - the ordering assertion was a loose disjunction that passed even if the base-check moved AFTER the dispatch; tighten to assert base-check < Agent() (the real invariant). - add a test that the verify-only <worktree_branch_check> backstop remains embedded in the Agent() prompt (acceptance criterion 4 — the base-check is a pre-dispatch degrade, the guard is a post-fork fail-closed backstop; both layers must survive). * changeset(#2649): diagnose-issues + execute-plan auto-degrade on stale worktree base * changeset(#2649): backfill PR number 2955 --------- Co-authored-by: sim <sim@users.noreply.github.com> |
||
|
|
388837219d |
fix(#2648): phase.complete refuses when non-retired plans lack summaries (fail-closed coverage gate) (#2953)
* test(#2648): failing-first — phase complete must refuse when plans lack summaries Adds findUnsummarizedPlans to core-utils (mirrors countMatchedSummaries but returns the unmatched plan files) and a PHASE_PLAN_COVERAGE_INCOMPLETE error reason, plus a 3-case regression block in phase.test.cjs. The gate itself is NOT yet wired into cmdPhaseComplete (reverted for the RED run), so the 'blocks completion' and 'superseded does not block' cases must FAIL (the pre-fix code completes silently). * fix(#2648): phase.complete refuses when non-retired plans lack summaries cmdPhaseComplete gated only on a single *-VERIFICATION.md status, so a phase could close complete while an arbitrary number of its plans had no completion record (confirmed incident: 6/30 plans unexecuted incl. the phase's entire final UI scope, every signal green). Add a fail-closed plan-coverage gate that refuses completion when any plan lacks a matching *-SUMMARY.md, naming the missing plans, UNLESS the plan is retired via machine-readable status: superseded frontmatter (#2349) — closing the Goodhart hole (delete a SUMMARY to raise the %) without regressing the lock/recovery pattern. Uses scanPhasePlans (superseded-AWARE) + new findUnsummarizedPlans helper so the gate, the count, and the named list can never disagree. Evaluated before the verification-gate transaction so a refusal fails fast without mutating ROADMAP/STATE. milestone.complete's parallel gap is explicitly out of scope (separate seam, separate PR). * fix(#2648): test fixtures — give #1752 phase plans summaries + STATE.md in coverage fixture The plan-coverage gate (#2648) correctly blocks phase completion when a plan lacks a SUMMARY. Two test fixtures needed updating to reflect the new contract: - #1752 (total_phases-decrement cascade): its 8 phase dirs each had a PLAN.md with no SUMMARY. The test's concern is the total_phases cascade, not plan coverage, so add a matching SUMMARY to each to keep the phase fully-covered and isolate the #1752 behavior. - the #2648 coverage-gate fixture: write STATE.md (createTempProject scaffolds .planning/phases but not STATE.md) so the 'ROADMAP/STATE unchanged on refusal' assertions have a file to read. * fix(#2648): security — fail closed on unreadable plan dir + sanitize msg + surface superseded Address the security-review blocker (B1) and hardening (M1/m2): - B1 (blocker): the gate failed OPEN when scanPhasePlans could not read the phase dir (it swallows readdirSync errors → empty plan set → gate sees zero unsummarized plans → passes). A coverage gate that passes when it cannot read the plans re-opens the #2648 hole under any I/O failure. Now readdirSync the dir explicitly and fail closed (PHASE_PLAN_COVERAGE_INCOMPLETE) on a throw; a readable empty dir still passes (legitimately complete empty phase). - m2: sanitize plan filenames (strip C0 controls / DEL) before interpolating into the error message — they come raw from readdirSync and could spoof the terminal in plain-error mode. - M1: surface the count of plans excluded as status: superseded so a reviewer can audit which work was declared retired (the marker is a committable, review-time-trusted bypass; keep it visible). - Add a 4th regression case: unreadable plan dir (ENOTDIR via a file, not chmod 0o000 which root bypasses) must fail closed. * test(#2648): drop unreachable B1 case — no root-safe unreadable-dir repro The B1 fail-closed-on-unreadable-dir defense stays in src/phase.cts (cheap + correct), but it cannot be unit-tested cross-platform: any condition that makes the phase dir unreadable to the gate's readdirSync ALSO fails findPhaseInternal upstream ('Phase N not found') before the gate runs, and chmod 0o000 is forbidden (root bypasses it in root CI). Document the gap in the test file; remove the case that asserted a reason the upstream error pre-empts. * style(#2648): drop unnecessary type assertions flagged by lint:ci scanPhasePlans returns typed string[] arrays, so the `as string[]` casts on coverageScan.planFiles/summaryFiles were redundant (@typescript-eslint/ no-unnecessary-type-assertion). Compute supersededCount from typed lengths; only the phaseInfo['plans'] cast remains (it is genuinely unknown). * changeset(#2648): phase.complete refuses when plans lack summaries * changeset(#2648): backfill PR number 2953 --------- Co-authored-by: sim <sim@users.noreply.github.com> |
||
|
|
5a0a9f0972 |
fix(#2944): remove the catastrophic-backtracking regex from the ADR-1671 example (#2950)
* fix(#2944): remove the catastrophic-backtracking regex from the example The non-shipping Option-E reference example carried its own copy of the predicate-id regex, which nested a dot-containing character class inside a dot-prefixed repeat. A run of N consecutive dots therefore had exponentially many partitions. Measured on next before this change: 30 dots 54ms, 35 66ms, 40 807ms — so roughly 55-60 dots hangs for hours. Not exploitable where it sits: the example is outside tsconfig.build.json, outside the npm package files list, outside the installer and outside tests, so no build step or CI job parses anything with it. Fixed because the entire point of a reference example is that people copy it forward, and ADR-1671 presents this one as the pattern for the platform. Ports the linear per-segment validation that #2928 gave the production module, so the two copies agree: both parse the real CONTEXT.md to 415 predicates across 20 classes with 0 duplicates. Doubled-dot ids are now rejected here too, matching production, and the grammar comment records it. Also refreshes the example's committed index, which #2928 made stale when it removed the duplicate predicate from CONTEXT.md. Closes #2944 * test(#2944): guard predicate-index sync and example/production parity Two regression tests for the two defects in this PR. Index sync: asserts the committed docs/CONTEXT-INDEX.json equals a fresh parse of CONTEXT.md, naming any diverging predicate ids. The merge race that reddened next was invisible to both PRs involved and only surfaced on the next PR to run lint:ci; this puts the same check inside the suite, which runs on every PR, and a mutation test proves the assertion is not vacuous. Example/production parity: asserts both copies of the parser report the same count, classes and duplicates for the real CONTEXT.md, and agree verdict-for- verdict over a table of id shapes. The divergence WAS the bug — production went linear-time while the example kept the backtracking regex, with nothing asserting they agreed. Also pins the example rejecting a 60-dot id, with the clean rejection as the binding assertion and wall-clock only as a smoke check. Notes a real tension rather than hiding it: ADR-1671 says the example sits outside tests/, and this imports it. The ADR's intent is that the example is not compiled, packaged or installed — not that it may silently rot. A parity guard does not ship it. The file states this so a reviewer can object. * fix(#2944): address both isolated review passes Two independent reviewers (correctness and security axes, neither the author). Security found nothing — it measured linearity to 100k chars across dots, hyphens, underscores and mixed classes, and showed prototype pollution is structurally unreachable because the first-segment pattern forbids lowercase and underscore-leading ids. The correctness pass found three blockers, all real. Blocker: the parity test violated ADR-1671 verbatim. The ADR lists FOUR exclusions for the reference example, the fourth being the CI test suite, and the test imported it from tests/ while its own justification comment cited only three -- constructing a rationale around the exclusion it broke. Moved to scripts/lint-example-parser-parity.cjs wired into lint:ci; a lint script is not the test suite, so the exclusion stands. The test file keeps only the docs/CONTEXT-INDEX.json sync check. Blocker: the mutation test leaked its temp dir. Its callback took no `t`, so a failing assertion skipped the bare cleanup call. Now registered via t.after(), matching the convention adr-index-gate.test.cjs documents. Blocker: the example's own committed index carries the identical merge-race staleness this PR fixes for the production one, and nothing guarded it. Deliberately NOT fixed by wiring the example's --check into CI: that artifact bakes line numbers, so it re-drifts on any unrelated CONTEXT.md line shift -- exactly ADR-1671 open question 4 -- and would make CI routinely red. The new lint asserts the line-INDEPENDENT facts instead: count, class map, duplicate set, and every (id, value) pair. Proven non-vacuous both ways: mutating a value fails and names the id, mutating only a line number passes. Major: a real divergence the parity claim would have missed. Production rejects values containing an embedded CR, LF, U+2028 or U+2029; the example did not, so a value with an embedded lone CR was rejected by one copy and accepted by the other. Ported, and now covered by the parity table. Also, found while verifying rather than reported: malformed diagnostics covered only empty values. A doubled-dot id, a space in an id, and a lowercase-leading id were all dropped silently. That contradicts the module's own intent -- a typo should be diagnosable, and a space in an id is a likely one -- and predicates are contractually cited, so a silently vanished predicate is the failure mode that matters. Each rejection class now carries a named reason in both copies, while ordinary inline code still yields none. Trues up counts my own change staled: the example README and ADR-1671's prototype figures said 416 and 393/18 against a real 415/20/0. Closes #2944 * chore(#2944): backfill changeset PR number 2950 --------- Co-authored-by: sim <sim@local> |
||
|
|
07603df8f2 |
fix(#2647): code-fixer worktree under .claude/worktrees/, not a hardcoded /tmp path (#2942)
* test(#2647): failing-first — fixer worktree path must be repo-relative not /tmp * fix(#2647): place code-fixer worktree under .claude/worktrees/, not /tmp The gsd-code-fixer agent hand-rolled its worktree at a hardcoded `/tmp/sv-${padded_phase}-reviewfix-XXXXXX` mktemp path. On Windows/Git Bash that landed OUTSIDE the project tree — outside the agent session's permission allowlist, so every Read inside the worktree prompted (~25/run) — and mktemp's MAX_PATH-avoidance substitute produced an un-removable `C:/mvwtNN` path. Place the worktree repo-relative under `.claude/worktrees/` (the same dir the harness-managed executor worktrees use: gitignored via `.claude/`, inside the session's permission scope), with a $$-PID + epoch suffix for concurrency uniqueness (replacing mktemp's XXXXXX). $main_repo is resolved the same way the cleanup tail already resolves it. Three sites updated: setup_worktree bash, concrete-steps prose, critical_rules. The #2990 `-b "$reviewfix_branch"` invariant is preserved (the folded test asserts it). Failing-first regression added to the #2990 suite in tests/agent-frontmatter.test.cjs. * test(#2647): update #2686 path assertion to expect .claude/worktrees/, not /tmp The #2686 regression test encoded the worktree location as a hardcoded `/tmp/sv-` path (matching sibling GSD agents at the time). #2647 showed that breaks Windows/Git Bash (worktree outside the project tree → permission prompts; mktemp MAX_PATH substitute un-removable). Update the #2686 path assertion to require the repo-relative `.claude/worktrees/` location and forbid `/tmp/sv-`. The #2686 isolation + cleanup assertions are unchanged. * fix(#2647): word-boundary wt= parse + ack the fixer growth vs next Two follow-ups to the #2647 GREEN run: - parseWtAssignments matched `prior_wt=` (no word boundary), polluting the set and tripping the repo-relative + concurrency-unique assertions. Anchor on (?:^|\s)wt= so only the real worktree-path assignment is captured. - emitted-attribution: gsd-code-fixer.md grew 1875 bytes vs origin/next. Update the emitted-drift-ack entry to attribute the #2647 worktree-path change (supersedes the prior #2825 attribution, whose growth is already in next). * fix(#2647): address review — validate padded_phase at the sink + tighten test Code-review + security-review both APPROVED with one actionable minor: padded_phase is interpolated into a worktree PATH and a git BRANCH NAME, but was only validated by the orchestrator (code-review-fix.md), not at the agent sink. The agent prompt is a literal bash contract any caller can spawn, so add a `[[ =~ ^[0-9]+(\.[0-9]+)?$ ]]` self-defense check rejecting traversal/shell metachars (defense-in-depth; not a present vuln — the only caller validates). Also tighten the concurrency-uniqueness test to require BOTH $$ AND $(date +%s) (either-alone was too lax per review). Update the emitted-drift-ack reason to cover the added validation growth. * changeset(#2647): code-fixer worktree under .claude/worktrees not /tmp * changeset(#2647): backfill PR number 2942 * chore(#2938): regenerate stale docs/CONTEXT-INDEX.json on next #2938 (#2928) updated the CONTEXT.md RULESET prose for the new per-PR emitted-drift-ack fragment mechanism (#2914) but shipped a CONTEXT-INDEX.json generated from the OLD prose. lint:generated-sync fails on every PR that rebases onto next after #2938 (the regen produces a 3-line diff bringing three RULESET entries — AGENT_SIZE_BUDGET, EMITTED_ATTRIBUTION, WORKFLOW_SIZE_BUDGET — in sync with the prose already on next). Mechanical regen via `node scripts/gen-context-index.cjs --write`; idempotent; surfaced by the #2647 rebase. No behavioral change. --------- Co-authored-by: sim <sim@users.noreply.github.com> |
||
|
|
05b170e448 |
chore(#2928): productionize the CONTEXT.md predicate fact-store and gate it in CI (#2938)
* feat(#2928): port CONTEXT.md predicate fact-store into the src seam Productionizes the ADR-1671 Option-E reference example as a real module: src/context-predicates.cts (parser + selector + index builder) compiled to gsd-core/bin/lib/, plus scripts/gen-context-index.cjs following the repo's --check/--write drift-guard idiom and wired into lint:generated-sync. Parser behavior is deliberately prototype-equivalent in this commit so the next commit's regression matrix binds to the real defects rather than to a missing module. Two locked design deviations from the prototype: - duplicates carry a count, not line numbers - the committed index carries no line field at all, resolving ADR-1671 open question 4: an artifact without line numbers cannot drift on a line shift, so promoting --check to a CI gate does not make it routinely red Also reconciles the one remaining duplicate predicate ID (RULESET.WORKFLOW_MARKDOWN.FENCES was declared twice; the non-MD040 wording is removed) so the gate can land fail-closed on duplicates. Refs #1671 * test(#2928): failing-first matrix for the predicate fact-store Adds the regression matrix from the phase test plan: parser declaration forms, fence and comment regions, ID/value grammar boundaries at limit-1/limit/limit+1, CRLF fidelity, duplicate detection, the drift-guard CLI, the selector query surface, and four document-shaped fast-check properties. Seven rows are RED for behavioral reasons against the ported parser: indented-bare, star-list, plus-list and numbered-list declaration forms are dropped; a tilde fence and a four-backtick fence containing a shorter fence are not skipped; and a multi-line HTML comment is parsed as live. Eleven selector rows are RED because the query surface is not wired yet. Negative fixtures come from real repo documents that predate the grammar (CONTEXT.md, CONTRIBUTING.md's fenced env-assignment examples) per the fixture-provenance rule, and the property generators are document-shaped rather than seeded from our own serializer. Refs #1671 * fix(#2928): consume the shared fence scanner, relocate the index, wire the selector Drives the failing-first matrix green. Parser: replaces the ported naive triple-backtick toggle with the shared markdown-sectionizer fence engine. scanFencedBlocks and FencedBlockRecord gain an export keyword — the only change to that module, which has 71 upstream dependents — because it already returns line-indexed spans, which is exactly what a line-reporting parser needs. It also already documents itself as the second copy of the fence state machine pending consolidation; adding a third copy here would have been the generative-fix divergence this repo warns about. A parity suite now pins predicate fence-skipping against that scanner across eight fence shapes. HTML-comment skipping stays local because the sectionizer has no comment scanner. Declaration forms widen to indented-bare, star, plus and numbered list items. Index location: docs/CONTEXT-INDEX.json, not a module under bin/lib. The remote matrix run caught the original choice — a committed .cjs there ships ~120KB of CONTEXT.md prose into a runtime module, and two content guards fired truthfully on it (a leaked .claude install path, and four hardcoded package-name literals). Neither guard was allowlisted; the artifact moved instead, mirroring docs/INVENTORY-MANIFEST.json. Nothing at runtime needs to require it — it is a drift-detection artifact, so the selector parses CONTEXT.md live and is always current. Generator: adds a frozen REASON enum and --check --json so the gate's outcome is asserted structurally instead of by matching prose, and --context-path/--index-path so tests drive the real CLI against a temp tree with no filesystem monkeypatching. Selector: gsd_run query context-predicates with --class/--prefix/--contains, structured output carrying a matched count, own-property guards, and no project-root resolution. Registering it exposed that the query dispatch table and the usage string had drifted: a new parity test found 20 routed commands missing from the usage list, all added here rather than deferred. Refs #1671 * test(#2928): lock the newly-public scanFencedBlocks contract Exporting scanFencedBlocks made it public API for the first time, so it needs its own contract test independent of the consumer that motivated the export. Memtrace's co-change analysis flagged the gap: this suite changes together with markdown-sectionizer.cts 8 times in 90 days and was absent from the diff. Covers the documented rules: 0-based indices, -1 for an unterminated fence, the same-char/>=length/no-trailing-text closer rule, a shorter fence inside a longer one staying content, CommonMark 4.5 backtick-in-info-string, and <=3-space indent tolerance. Refs #1671 * fix(#2928): address both isolated review passes Two independent reviewers (correctness axis and security axis, neither the author) found seven findings. All are fixed here with regression tests; none deferred. BLOCKER — comment-blind fence scanning caused silent, permanent predicate loss. The HTML-comment scan and the fence scan ran as two independent passes, and the fence scanner is comment-blind, so a fence delimiter inside an HTML comment with no later close read as an unterminated fence and skipped every remaining line to EOF. Worse, the drift-guard could not catch it: it diffs against a baseline produced by the same corrupted parse. The two constructs now interleave in a single pass so each suppresses the other's boundary detection while active, covered in both directions. The parity suite still binds this scanner to markdown-sectionizer's for comment-free documents, so the two cannot diverge unnoticed. BLOCKER — the selector was not consumed anywhere, leaving the phase's acceptance criterion unmet. Now wired into the pre-work predicate-citation step in contributor-standards, which is the repo's actual brief-assembly path; no code-level brief assembler exists to wire into. MAJOR — ReDoS with an unauthenticated CI-hang exploit. The predicate-id regex nested a dot-containing character class inside a dot-prefixed repeat, so N consecutive dots had exponentially many partitions: 40 dots took 565ms and growth was exponential. CI runs this parser over a pull request's own CONTEXT.md, so any contributor could have hung a shared runner with one line. Replaced with linear per-segment validation. Doubled-dot ids are now rejected; the real document contains none. MAJOR — the duplicate-id gate had only ever been proven on synthetic fixtures. A test now re-inserts the exact line this branch removed and asserts the real generator names it. MAJOR — --check together with --write silently let write win, turning the gate into a writer; a missing path value resolved to the cwd and leaked an EISDIR stack trace. Both are now clean usage errors. MINOR — the hoisted skip-list was exported as a live mutable Set; replaced with a read-only predicate. MINOR — flag-shaped selector values were unmatchable; the inline --flag=value form now provides the escape hatch. Refs #1671 * chore(#2928): backfill changeset PR number 2938 --------- Co-authored-by: sim <sim@local> |
||
|
|
c043f2946c |
fix(#2914): per-PR ack fragments instead of one shared mutable file (#2923)
* fix(#2914): never persist a spent emitted-drift ack on next tests/emitted-drift-ack.json held 34 spent #2834 entries merged via #2900. Every entry is scoped to the diff that introduced it (#2789), so once merged to next it is at the base by definition -- spent and inert. Its presence is still load-bearing though: each PR rewrites the paths map wholesale, making a persistent base copy a shared cell. Five of six conflicting PRs in the open queue collided on this file and nothing else. Deletes the stale document and adds a push-to-next guard asserting it stays absent. The guard is deliberately NOT wired into lint:ci -- a PR-lane check against the base is the #2768 shape #2789 exists to end. Closes #2914 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2914): backfill changeset pr number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2914): per-PR ack fragments instead of one shared mutable file The emitted-drift acknowledgment lived in a single tests/emitted-drift-ack.json whose paths map every PR rewrote wholesale. That is a shared mutable cell: any two PRs needing an ack edit the same lines and conflict. Five of six conflicting PRs in the open queue collided on this file and nothing else. Acks now live as per-PR fragments under tests/emitted-drift-acks/, the same shape .changeset/ already uses to solve this exact problem. Two PRs pick different filenames, so they cannot collide, and fragments lingering on next are harmless rather than toxic. The legacy file's 35 entries are MIGRATED into a fragment, not deleted. An earlier delete-only attempt failed verification twice: the ratchet lost the spec-phase.md acknowledgment from #2779 and reported a 10-byte growth with no ack. Relocating preserves every acknowledgment. The legacy single file is still READ (unioned with the fragments) because five open PRs carry it; dropping support would break all of them. A duplicate path key across sources is a hard error, never last-wins. The push-to-next guard is retargeted accordingly: it now asserts only that the legacy SHARED file never reappears on next. Fragments may persist harmlessly. Closes #2914 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d2d2f7c088 |
fix(#2848): non-Latin titles no longer produce empty slugs (Cyrillic transliteration) (#2934)
* test(#2848): add failing-first regression for non-Latin slug transliteration generateSlugInternal and slugify both strip non-ASCII chars with no transliteration step, so an all-Cyrillic title reduces to an empty slug. 12-row matrix: Cyrillic regression (both impls), Latin negative control, multi-letter mappings, soft/hard sign drops, Ukrainian extras, null contract, CJK unaffected, mixed scripts, truncation parity, slugify's distinct no-truncate contract. * fix(#2848): transliterate Cyrillic titles to ASCII before slug strip Both generateSlugInternal (src/core-utils.cts) and slugify (src/gsd2-import.cts) stripped non-ASCII with no transliteration, so an all-Cyrillic title reduced to an empty slug, producing unnamed phase directories (01-) and empty milestone_slug init JSON. Add a shared transliterateForSlug primitive (core-utils) covering Russian + the reported Ukrainian/Belarusian extras (і ї є ґ ў), with multi-letter mappings (ж→zh ч→ch ш→sh щ→sch ю→yu я→ya) and dropped soft/hard signs (ъ ь). It runs BEFORE the existing ASCII filter, so Latin-script text hits zero map entries and is byte-for-byte unchanged (negative control). slugify consumes the shared primitive, preserving its distinct single hyphen-strip + no-truncation contract. CJK/unmapped scripts keep the existing strip-to-ASCII behavior. Also corrects two test assertions to match the chosen й→y mapping and the б→b (not bie) transliteration. * changeset(#2848): Fixed — non-Latin slug transliteration * changeset(#2848): backfill PR number 2934 --------- Co-authored-by: sim <sim@local> |
||
|
|
76b7d73039 |
fix(#2733): route gate-passed spec-phase paths into the probe steps (#2779)
* fix(#2733): route gate-passed spec-phase paths into the probe steps All four gate-passed transitions in spec-phase.md said "Jump to Step 6", textually bypassing the mandatory Step 5.5 edge-completeness and Step 5.6 prohibition-completeness probes. Steps 5.5/5.6 were spliced between Step 5 and Step 6 by two later feature commits and the pre-existing jumps were never re-pointed, so no jump instruction in the file reached Step 5.5 at all and both probes were unreachable dead prose. Re-point the four gate-passed jumps (lines 129, 162, 168, 170) to Step 5.5. Control then flows 5.5 -> 5.6 -> 6 as the probes' own preconditions prescribe. The max-rounds "write anyway" bypasses and the probes' own "proceed to Step 6" exits are deliberately unchanged. Add tests/spec-phase-probe-reachability.test.cjs, which derives the mandatory probe steps from the file's own headings rather than hardcoding 5.5/5.6, so a future spliced-in probe step is covered without editing the test. It also locks the two coupled constraints: the max-rounds bypass must not be redirected into a probe, and each probe must keep its own onward exit. The existing probe contract tests are untouched and still pass; both scope from the "## Step 5.5"/"## Step 5.6" heading onward and were structurally incapable of observing the upstream jump text. * chore(changeset): Fixed fragment for #2779 (spec-phase probe reachability) * fix(#2733): route Step 5.5's own soft gate into Step 5.6 Round-1 review blocker. The four upstream gate-passed jumps were re-pointed to Step 5.5, but Step 5.5's own terminal soft gate at :305 still read "proceed to Step 6" - so the COMMON path (all applicable edges resolved) skipped the prohibition-completeness probe outright. Same defect class as the four this PR already fixed, on the success path of the very step being fixed: the SPEC shipped with an empty Prohibitions section instead of an empty Edge Coverage one. Its sibling at :393 is byte-identical yet correct, because Step 6 genuinely follows Step 5.6. Position, not phrasing, is the discriminator. The guard could not see it: the transition matcher keyed only on the literal "Jump to Step", and :305 says "proceed to Step". Widened it to a verb alternation (jump/proceed/continue/go/return/skip + "to Step N", case-insensitive) and renamed it TRANSITION_RE to match what it now models. This makes the file's own docstring promise - that a future spliced-in probe is covered without editing the test - true for a step whose exit is worded differently. Verified no false positives: the two pre-existing "continue to Step 3/4" transitions are upstream of both probes but target pre-probe steps, and the max-rounds bypass block contains no step transitions at all. Fail-first verified before fixing :305 - with the widened matcher against the unfixed workflow the guard fails naming exactly "spec-phase.md:305 jumps to Step 6, skipping mandatory Step 5.6", 4 pass / 1 fail; after the fix, 5/5. The two sibling probe contract tests stay 16/16. Also from review: - STEP_HEADING_RE gains an explicit \r? before $. Without it, on a CRLF checkout `.` stops before the \r and the unanchored $ fails to match, yielding ZERO steps and vacuously passing every assertion in the file. Not live today (.gitattributes forces eol=lf) but this repo has a recurring CRLF-regex bug class, so the guard no longer leans on it. - allow-test-rule category corrected to source-text-is-the-product; the previous runtime-contract-is-the-product is not one of the six recognized categories (CONTRIBUTING.md:609-619). - changeset body given the documented bold-lead-in form. - emitted-drift ack reason updated: +8 -> +10 bytes across five transitions (31987 -> 31997), DEFAULT tier, cap 40960. --------- Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
d49a7d0c4d |
fix(#2853): roadmap.update-plan-progress preserves hand-written annotations (#2916)
* test(#2853): add failing-first regression for plan-progress annotation preservation The count-bump regex's trailing [^\n]+ swallowed the whole Plans line and the replacement wrote back only the regenerated count, deleting any hand-written annotation after it. 8-row matrix covers bold/plain forms, bare template form, executed path, CRLF, and idempotency. * fix(#2853): preserve hand-written annotations in roadmap plan-progress bump The count-bump regex's trailing [^\n]+ swallowed the entire Plans line and the replacement wrote back only the regenerated count, deleting any hand-written prose after the count (e.g. a gap-closure annotation). The verb owns the count token only. Capture the existing count token ($2) and the trailing line text ($3), and rebuild the line as <label><new count><surviving text>. Trailing text is preserved ONLY when a real count token preceded it, so the fresh-template bracketed placeholder (`[Number of plans…]`) is still replaced cleanly rather than glued after the count (pre-#2853 behaviour on the template path preserved). CRLF \r is preserved via [^\r\n]. Widens replaceInCurrentMilestone to accept a replacement callback (needed to branch on whether the count group matched). The bare Plans: checklist header is still skipped — the lazy match lands on the summary line first and a count-less bare header yields no count to anchor preservation to. * changeset(#2853): backfill PR number 2916 --------- Co-authored-by: Test <test@example.com> Co-authored-by: sim <sim@local> |
||
|
|
90771ddf02 |
enh(#2904): add a reviewer entry type so third-party reviewer lanes are discoverable (#2912)
* feat(#2904): add a `reviewer` entry type so third-party reviewer lanes are discoverable ADR-2782 made a reviewer lane installable by a third party, but neither discoverability catalog could hold one. The Community Capability Registry requires a non-empty `loopExtensionPoints` and forbids a lane from declaring any hook kind, so a `role: "reviewer"` entry is unsatisfiable by construction; the EoS Registry is for ADR-1239 host integrations, which a lane is not. Adds a third catalog — `docs/registries/reviewers.json` → `docs/registries/reviewer-registry.md` — whose `interactions` describes the lane: slug, flags, transport, evidenceClass, reviewsSection, requiresBinaries, configKeys, runtimeCompat. The lane vocabulary is a hand-written mirror of `capability-validator.cjs` (the same pattern as `AXES` mirroring `HOST_INTEGRATION_AXES`), with parity enforced by tests/registry-reviewer-parity.test.cjs. `slug` deliberately uses the runtime `LANE_SLUG_RE` grammar rather than the registry's kebab-only `id` rule, so real lanes (`lm_studio`, `4o-mini`) are not rejected. Two binary type branches became three-way Map dispatch. Both now fail loudly on an unrecognized type instead of silently treating it as a capability — `renderMarkdown` in particular writes a committed catalog file, so a silent wrong-title render was the worst failure mode available. Also fixed while here: `gen-registry.cjs` parsed source JSON with no error handling, so a malformed or non-array `capabilities.json` surfaced as a raw SyntaxError/TypeError instead of an actionable CLI error. Closes #2904 * fix(#2904): bound and sanitize untrusted registry `interactions` strings Review findings from the pre-PR passes. Security (isolated pass): `interactions` string fields reached the generated, committed Markdown catalog with no control-character check and no length bound. A `reviewsSection` carrying ESC and a `requiresBinaries` element carrying NUL plus 5000 characters validated clean and landed verbatim in the rendered page — `mdInline` escapes Markdown metacharacters and collapses CRLF, but nothing else. The identical gap already existed on the capability type's `configKeys`/`requires`/`runtimeCompat`/`produces`/`consumes`, so it is fixed there too rather than inherited into a third type. `hasDisallowedControlChar` is lifted to module scope so exactly one implementation exists, and a shared `validateStringArrayField` enforces control-character rejection, a 200-character element cap and a 50-element array cap for both types. Correctness (standards pass): `renderMarkdown`'s per-entry summary builder was still an if/else-if chain whose final `else` was the capability branch — the one per-type dispatch point this change had not converted, and the same silent fallthrough it removes elsewhere. It now lives in `RENDER_META` alongside the title, so a fourth type cannot silently inherit capability's rendering. All three types' rendered output is byte-identical to before the refactor. Also corrects a test comment that still claimed the reviewer suites were failing-first against an unmodified module. * chore(#2904): backfill changeset PR number (#2912) |
||
|
|
42f4f184c0 |
fix(#2844): verify-summary ignores future/prose path mentions; resolves project root (#2910)
* fix(#2844): verify-summary binds file-claim extraction to a creation-claim context verify-summary's Pattern 1 matched any backticked path-like token with no context check, so a prose mention of a future deliverable (`shared/types.ts` in a 'next phase will add…' sentence) was checked for existence and its absence failed the verdict on a healthy phase. #2685 added shape filtering but no context check. - src/verify.cts: both extraction patterns now require a claim label on the line (Created/Modified/Added/Updated/Edited/key-files). A bare prose mention no longer matches; genuine labeled claims still do. - gsd-core/bin/gsd-tools.cjs: remove 'verify-summary' from SKIP_ROOT_RESOLUTION so relative claim paths resolve against the project root, not the raw cwd (subdirectory invocation no longer manufactures missing files). Regression tests: prose mention not treated as a claim; prose-only SUMMARY passes; absent claimed file still fails. * chore(#2844): backfill changeset PR 2910 --------- Co-authored-by: Test <test@example.com> |
||
|
|
39dbe5e0f5 |
fix(#2843): findProjectRoot stops at git-repo boundaries, not just MAX_DEPTH (#2909)
* fix(#2843): findProjectRoot stops at git-repo boundaries, not just MAX_DEPTH findProjectRoot's heuristic (3) used isInsideGitRepo(parent), which only checked 'does SOME .git exist between start and the ancestor' — it never verified the .git was co-located with / bounded the trusted .planning/. A nested child repo (own .git, no .planning) under an ancestor GSD project satisfied the check, so resolution silently crossed into the ancestor project (wrong identity, exit 0). Add nearestGitRoot(from, upTo) (fs-walk, no spawn) and use it in heuristics (3) and (4): if the caller is inside its own nested repo whose root is strictly below the candidate ancestor, do not return that ancestor. The plain-descendant (#1414), co-located .git+.planning, and sub_repos/multiRepo cases are unchanged. Regression test: a nested child .git under an ancestor .planning no longer resolves to the ancestor; the co-located single-repo case still resolves. * chore(#2843): backfill changeset PR 2909 --------- Co-authored-by: Test <test@example.com> |
||
|
|
5d0fd4dc53 |
fix(#2825): gsd-code-fixer honors workflow.use_worktrees; never rm -rf a reparse point (#2905)
* fix(#2825): gsd-code-fixer honors workflow.use_worktrees; never rm -rf a reparse point gsd-code-fixer was the only writer that hand-rolled a git worktree inside the agent prompt and the only one that never read workflow.use_worktrees. With the setting explicitly false, --fix still created worktrees; the fresh worktree had no node_modules, so runs improvised a teardown whose `rm -rf` followed a Windows junction into the REAL node_modules (silent data loss, 3x observed). Defect 1: gate setup_worktree + its cleanup tail on workflow.use_worktrees (same gsd_run query config-get read the four sibling workflows use). When false: edit/commit in the main checkout (wt='.', no temp branch, no sentinel, no cleanup). Defect 2: forbid rm -rf on a possible reparse point in the spec — never fall through to a destructive remove; on failure, stop and surface the error. Defect 3: REVIEW-FIX records where verification ran (main checkout vs worktree). The transactional worktree path (#2839/#2990/#2686) is unchanged when worktrees are enabled. Docs-parity guards in tests/code-review.test.cjs bind the spec to the fix. * chore(#2825): backfill changeset PR 2905 * fix(#2825): drop stale agents/gsd-code-fixer.toml ack + spent entries (emitted-attribution) gsd-test failed: agents/gsd-code-fixer.toml is a STALE ack here — that TOML was changed by #2834 (now in next), not this PR. The 4 base-carried acks (autonomous/discuss-phase-assumptions/next/plan-phase) are spent/inert. --------- Co-authored-by: Test <test@example.com> |
||
|
|
00c859fae5 |
fix(#2834): write defaults.json before agent TOML generation on clean Codex install (#2900)
* fix(#2834): write defaults.json before agent TOML generation on clean Codex install Extracted writeNonClaudeDefaults(runtime) and called it BEFORE installCodexConfig so the runtime-aware model resolver has resolve_model_ids=omit + runtime=codex in ~/.gsd/defaults.json before agent TOMLs are generated. Pre-fix, a clean first Codex install generated TOMLs with no model fields (the resolver didn't know the runtime); a second run fixed it. The original inline defaults-write block (which ran AFTER agent generation) is replaced by the earlier function call (idempotent). * chore(#2834): changeset fragment * fix+test(#2834): acknowledge codex TOML drift (emitted-attribution) + fix test comment window The emitted-attribution gate flags 19 codex agent TOMLs that now carry model-routing fields (the fix's correct effect) but can't link them to a .md or src/ change (the fix is in bin/install.js ordering). Acknowledge the drift in emitted-drift-ack.json. Fix the test's comment-detection window (300 chars to capture the #2834 rationale). * fix(#2834): ack remaining 15 codex TOML drift paths * chore(#2834): backfill changeset PR number (2900) * chore(#2834): ack code-review.md growth from concurrent merge (rebase pickup) * fix(#2834): remove stale code-review.md ack (emitted-attribution failure) CI failed: 'differential attribution over the real tree' — the code-review.md ack added in 6e0b4b3b8 ('ack code-review.md growth from concurrent merge') is STALE: this PR's diff does not touch code-review.md (only bin/install.js + tests + changeset), so the ack explains growth that isn't here. The base already absorbed the concurrent code-review.md growth; the ack is inert here and the gate flags it as stale. Remove it. --------- Co-authored-by: Test <test@example.com> |
||
|
|
9f567a1627 |
fix(#2788): recover Gaps Found rows; mark-complete no longer false-succeeds on a rejected row (#2902)
* fix(#2788): recover Gaps Found rows; mark-complete no longer false-succeeds on a rejected row Two coupled defects in the requirement traceability state machine: Defect 1 (terminal state): requirements revert-phase (the gaps_found response) left a row at 'Gaps Found' with no inverse — neither mark-complete's /^pending$/i guard nor the phase-complete reconcile's /^(?:pending|in progress)$/i accepted it, so a single failed verification stranded every requirement permanently and blocked the milestone. Widen both guards to accept 'gaps found' so a genuinely-satisfied stranded row reaches Complete again. Defect 2 (false success): mark-complete ORed checkboxHit || tableHit for 'updated', so on a Gaps Found row it flipped the checkbox but could not move the row, yet reported updated:true. When a traceability table has a row for an ID, gate 'updated' on the row moving (tableHit) — a checkbox-only flip on a table-bearing file no longer lies. The #2140 table_unmatched path (no row for the ID) is preserved. * chore(#2788): backfill changeset PR 2902 --------- Co-authored-by: Test <test@example.com> |
||
|
|
79ed181ec0 |
fix(#2667): run-with-timeout mediates .cmd/.bat spawns on Windows (CVE-2024-27980); fallow pre-pass names failure kind (#2897)
* fix(#2667): mediate .cmd/.bat/.exe spawns on Windows; split fallow pre-pass failure diagnostic run-with-timeout spawned .cmd/.bat/.exe commands without shell:true on Windows, tripping Node's CVE-2024-27980 EINVAL (April 2024 security hardening). The fallow structural pre-pass then no-op'd silently — a hard execution failure read the same as 'optional dependency absent'. (A) gsd-core/bin/gsd-tools.cjs runWithTimeout: gate shell:true on (win32 && command ends in .cmd/.bat/.exe). Narrow by design — never fires for the 7 `bash -c` callers (command is `bash`, no such suffix), so the recorded no-shell-for-argv-array security contract (DEFECT.UNBOUNDED-SUBPROCESS) is preserved; cmdArgs stays an array. POSIX untouched. (B) code-review.md fallow pre-pass: name the failure KIND (timeout / spawn failure / crash / not-found) so a Windows .cmd spawn failure is not mistaken for an absent binary. Regression test in tests/run-with-timeout.test.cjs gated to win32 (.cmd/.bat/.exe shims run with exit 0 + non-empty stdout; pre-fix EINVAL → exit 125/empty). POSIX negative-space test guards the unchanged bash -c callers. * chore(#2667): changeset fragment * chore(#2667): backfill changeset PR 2897 + correct body (cmd.exe array, not shell:true) * fix(#2667): exclude .exe from the win32 spawn-mediation gate; ack code-review.md growth CI caught two failures on the first push: 1. windows-24: 'exits 124 when the wall-clock budget is exceeded' regressed. The gate matched .exe, so the HANG command (node.exe -e 'setTimeout(...)') was wrapped in 'cmd.exe /c node.exe ...' — the wrapped child escaped the timeout cap's process-group reap (exit 124 never fired; hit the 30s harness backstop) AND cmd.exe risked mis-parsing the -e script arg. .exe is INTENTIONALLY excluded now: real PE executables spawn fine directly; only .cmd/.bat are the CVE-2024-27980 EINVAL cases. The .exe test becomes a negative-space test (node.exe spawned directly, exit 0). 2. ubuntu-22: emitted-attribution — code-review.md grew 1177 bytes from the #2667 fallow pre-pass failure-KIND case statement; acknowledge it. --------- Co-authored-by: Test <test@example.com> |
||
|
|
6e0bc50142 |
fix(#2666): code-review scopes root-level + extensionless build files, cross-checks against git diff (#2895)
* test(#2666): add regression + docs-parity guards for code-review file scoper The Tier-2 SUMMARY.md extractor dropped every repository-root file (no `/`) and every extensionless build file (Dockerfile/Makefile/etc.) via an AND-joined predicate. Adds behavioral tests against the pure-function mirror plus docs-parity structural guards that bind the shipped workflow .md to the fix. RED: the docs-parity guards fail against the pre-fix shipped predicate. * fix(#2666): accept root-level + extensionless build files in code-review scope; intersect-and-warn Two coordinated edits to gsd-core/workflows/code-review.md compute_file_scope: (A) Tier-2 SUMMARY extractor: replace the AND-joined predicate `/\\//.test(raw) && /\\.[A-Za-z0-9]+$/.test(raw)` (which required BOTH a directory separator AND a trailing extension, silently dropping every root-level file and every extensionless build file) with a relaxed predicate that accepts any path with a trailing extension OR a known extensionless build basename (Dockerfile/Containerfile/Makefile/Justfile/Procfile). (B) Tier-3: convert the eq-zero git-diff gate into an intersect-and-warn — whenever a reliable diff base is available, cross-check the SUMMARY scope against `git diff --name-only` and warn about (then add) any changed files the SUMMARY extractor did not surface. Portable (bash 3.2, no associative arrays) so a partial SUMMARY result can no longer silently ship an incomplete review scope. * chore(#2666): changeset fragment * fix(#2666): use exact whole-line matching (grep -Fxq) in Tier-3 cross-check Adversarial review found the unanchored `case "$IN_SCOPE" in *"$file"$\\n*` substring membership test would false-match: a root-level `Dockerfile` in the diff substring-matches an already-scoped `docker/Dockerfile`, silently skipping it — reintroducing the exact class of silent-scope-loss bug this PR fixes. Switch to `grep -Fxq` (exact whole-line match). Add docs-parity guard for exact matching + the basename-collision regression. * fix(#2666): resolve gsd-test failures — paraphrase predicate in comment, ack code-review.md growth gsd-test caught 3 issues on 7469c3f22: 1. The docs-parity guard fired on the .md COMMENT which restated the buggy predicate verbatim — paraphrase the comment so it no longer contains the exact string the guard detects. 2. Cascade subtest failure from #1. 3. emitted-attribution: code-review.md grew 2814 bytes — acknowledge the deliberate #2666 growth in tests/emitted-drift-ack.json. * chore(#2666): backfill changeset PR number 2895 --------- Co-authored-by: Test <test@example.com> |
||
|
|
f093738412 |
fix(#2891): normalize emitted version against the measured tree, not the measuring repo (#2894)
* fix(#2891): normalize emitted version against the measured tree, not the measuring repo buildParityManifest normalized the install-time {{GSD_VERSION}} stamp using PKG_VERSION, bound at module load from the MEASURING repo's package.json. Since #2767, currentManifests({repoRoot}) measures a DIFFERENT checkout, so during a release cut the baseline worktree (origin/next, 1.8.0) was normalized with the current tree's version (1.9.0) and its literal 1.8.0 stamp survived into the hash. All 364 emitted hook paths diverged and the differential attribution gate hard-failed every finalize/rc run. Normalize against the version of the tree that PRODUCED the emitted output: buildParityManifest takes an explicit pkgVersion, and currentManifests resolves it from the measured tree via a new fail-closed measuredPackageVersion(). * chore(#2891): backfill changeset pr number (#2894) --------- Co-authored-by: Test <test@example.com> |
||
|
|
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> |