fb2f25101024c1fbaf43a5285acdfaf5d1588dd7
53 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
835dd6ab44 |
test(3596): adversarial security/prompt-injection abuse suite (#3654)
* test(3596): adversarial security/prompt-injection abuse suite Adds `tests/security-prompt-injection.test.cjs` and a fixtures directory at `tests/fixtures/adversarial/security/` covering the attack classes enumerated in #3596: - Command substitution / backticks / heredoc payloads in workstream names — sentinel-file probes prove no shell is spawned, slugifier neutralises the input. - Path traversal through `--ws` and slash-bearing workstream names — rejected with structured `--json-errors` payload, no stack trace, no filesystem mutation outside the project root. - Fake `<system>` / `[SYSTEM]` / `<<SYS>>` / `[INST]` boundary tags — sanitizeForPrompt neutralises every form; structural negative property locked across all six styles in one place. - Zero-width / bidi-override codepoints — stripped per the documented codepoint set; asserted via codePoint inspection, not regex literals. - Hostile read of CONTEXT.md / PLAN.md / ROADMAP.md fixtures — `gsd-read-injection-scanner.js` surfaces the advisory; excluded paths and non-Read tools stay silent; malformed JSON does not crash the hook. - Hostile write of `.planning/` files — `gsd-prompt-guard.js` emits a `PreToolUse` advisory; non-Write/Edit tools stay silent. - Fake `ghp_*` / `sk-*` env tokens — never echoed in CLI stdout or stderr under hostile inputs; covered under `// allow-test-rule: structural-regression-guard` because the only way to assert byte-level absence is `.includes(token)` against the captured streams. - `validatePath`, `validateShellArg`, `validatePhaseNumber`, `validateFieldName` — focused negative-input contract pins. Pinned behavior gaps (documented, NOT fixed in this PR): - `<instructions>` is intentionally whitelisted by both the scanner and the sanitiser (GSD's own prompt scaffolding). Two REGRESSION GUARD tests lock that contract. - The current `scanForInjection` does NOT flag malicious markdown links (javascript:/data:/embedded-credentials URLs). PINNED with negative-proof so any future scope extension fails the assertion and forces a deliberate update to the acceptance map. - `prompt-builder.ts` does not yet wrap plan/context markdown in an "untrusted data" envelope. That seam lives on the TS side and is covered by `sdk/src/prompt-builder.test.ts`; out of scope for a CJS test file. Mentioned in the file header. Verification: - `node --test tests/security-prompt-injection.test.cjs` → 73 tests pass. - `node scripts/lint-no-source-grep.cjs` → 0 violations across 546 test files (one `allow-test-rule: structural-regression-guard` annotation on this file for the token-absence assertions). - `node scripts/run-tests.cjs` → 9730 tests pass, 0 fail. Refs #3596 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3596): allow adversarial fixtures in scan + harden graphify status parse * fix(3596): skip adversarial security fixtures in secret scan --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ae63cbe557 |
feat(3575): Phase 6 — CJS↔SDK seam migration end-to-end complete (#3524) (#3577)
* feat(3575): Phase 6 enforcement hardening + retrospective (#3524 feature-complete) Phase 6 of the CJS↔SDK hard-seam migration (parent #3524). Final phase per the PRD. After this lands the migration is feature-complete: shared Modules from Phases 1-4 are in place, the runtime-bridge primitive from Phase 5.0 is wired with the state.* family proof in Phase 5.1 (PR #3574), and Phase 6 hardens the seam against future drift via lint, CODEOWNERS, and retrospective documentation. ## What landed - scripts/lint-shared-module-handsync.cjs (274 lines) — the drift-prevention gate. Scans bin/lib/*.cjs and looks for same-named sdk/src/<name>.ts or sdk/src/query/<name>.ts (excluding generated artifacts). Pairs not on the allowlist fail the lint with a clear message: either add to allowlist with justification, or migrate to a shared Module. Supports --root, --allowlist, --cjs-dir, --sdk-src, --warn-all flags for testability. - scripts/shared-module-handsync-allowlist.json (148 lines) — two categories: - cooperatingSiblings (14 pairs) — legitimate Readers/Adapters that consume shared Modules or run structurally-different runtime paths. - migrateMeBacklog (8 pairs) — known drift anti-patterns that ARE on main today (config, decisions, intel, model-catalog, plan-scan, schema-detect, secrets, workstream-name-policy). Lint warns but does not fail on these; documented in the retrospective as candidate Shared Module migrations. - tests/lint-shared-module-handsync.test.cjs (285 lines, 11 cases) — proves the lint catches new drift, honors the allowlist, and exits 0 on the current tree. - .github/workflows/test.yml — new "Shared Module hand-sync drift check" step after the freshness checks. - .github/CODEOWNERS — appended 11 architecture-owned path rules for source-of-truth files (Shared Module dirs, manifest JSONs, runtime bridge, lint script, allowlist). Existing blanket rule preserved. - docs/agents/cjs-sdk-seam.md (280 lines) — full retrospective + guide: - Migration overview table linking Phases 1-6 with PR numbers. - 15 historical drift bugs (#1535 ... #3523) each mapped to the Phase 6 enforcement layer that would have blocked them. - "Guide: Adding a new Shared Module" — step-by-step using Phase 1 (state-document) as the worked example. - "Guide: Adding a new canonical command" — step-by-step using Phase 5.1 (state.update) as the worked example. - "Open follow-ups" listing the 8 MIGRATE_ME pairs, per-family Phase 5.2+ candidates pending maintainer authorization, sync bridge workstream support, and Phase 5.1's parity divergences. - CONTRIBUTING.md — short cross-reference paragraph in the Architecture & Domain Standards section. ## Audit findings All 5 freshness checks from Phases 0-4 are already wired in CI: command-aliases, state-document, configuration, workstream-inventory-builder, project-root. Phase 6 adds the 6th (hand-sync drift check) for total enforcement coverage. ## Numbers - Full CJS suite: 9335/9335 pass (baseline 9323 + 11 new lint tests + 1 cooperating). - Lint passes on current tree: 14 cooperating siblings + 8 backlog pairs accounted for, 0 unauthorized drift pairs. - Lint exits 1 (fails CI) on an intentional new hand-synced pair added to a fixture — verified by the test suite. Closes #3575. Closes the structural drift surface of #3524. * chore(3577): add changeset fragment for Phase 6 * feat(3575): Phase 6 end-to-end completion — CJS↔SDK seam migration done Per maintainer correction: Phase 6 is THE final phase and must complete the migration end-to-end. This commit absorbs Phase 5.1's work (state.* router + worker fix), finishes the remaining per-family router migrations, completes all five resolvable Shared Module extractions, resolves the parity divergences, lands native workstream support in the sync bridge, and ships the lint + CODEOWNERS + retrospective from the original Phase 6 scope. After this commit the CJS↔SDK seam migration started in #3524 is feature-complete. No follow-up "Phase 5.x" or "Phase 7" should be needed — the only documented carve-outs are three pairs that intentionally cannot be migrated (config CLI handlers, intel async wrapper, model-catalog already on the shared-JSON pattern). Cherry-picked state.* from Phase 5.1 (PR #3574 absorbed). Migrated verify.*, init.*, phase.*, phases.*, validate.*, roadmap.* via the same executeForCjs delegation pattern. Migrated the inline gsd-tools.cjs cases for frontmatter.*, config-* CLI, and non-family commands (generate-slug, current-timestamp, find-phase, docs-init) with shared _dispatchNonFamily helper + _tryLoadSdkBridge loader. CJS-native carve-outs documented: config-path, migrate-config, detect-custom-files (no SDK counterpart yet); state.complete-phase (no SDK counterpart yet); validate.context (CJS-only inline logic with no clean SDK port); phases.archive (SDK-only). - plan-scan (Module-via-generator from sdk/src/query/plan-scan.ts) - secrets (Module-via-generator) - schema-detect (Module-via-generator) - decisions (Module-via-generator; SDK regex aligned to CJS alphanumeric IDs to preserve project compatibility) - workstream-name-policy (Module-via-generator; SDK extended with hasInvalidPathSegment and isValidActiveWorkstreamName that CJS callers depend on) Each ships with: SDK source-of-truth, generator at sdk/scripts/gen-<name>.mjs, freshness check at sdk/scripts/check-<name>-fresh.mjs, parity test at tests/<name>-generator.test.cjs, CJS shim at get-shit-done/bin/lib/<name>.cjs, scripts in sdk and root package.json, pre-commit drift block, CI workflow step, CODEOWNERS rule, INVENTORY.md row. - config (config.cjs vs sdk/src/config.ts) — CJS file is CLI-handler surface (cmdConfigGet/Set/etc.); SDK file is loadConfig wrapper (already migrated in Phase 2). Zero logical overlap. Classified as CJS-CLI-ONLY in the allowlist. - intel (intel.cjs vs sdk/src/query/intel.ts) — SDK is the async QueryHandler wrapper of the CJS module; intentional split per the SDK file's own docstring. Classified as cooperating-sibling. - model-catalog (model-catalog.cjs vs sdk/src/model-catalog.ts) — both already consume sdk/shared/model-catalog.json (ADR-0003). No constants duplicated. Classified as ADAPTER-OVER-MODULE. - state.record-metric: SDK aligned to CJS auto-create of ## Performance Metrics section when absent. Parity assertion now exact equality. - state.prune: SDK aligned to CJS disk-based phase counting via stateExtractField. Parity assertion now exact equality. SDK unit tests updated to match. GSDTransport.shouldUseNative no longer forces subprocess when request.workstream is set — the Phase 5.0 worker fix already threaded workstream through dispatchNative + registry.dispatch, making the subprocess force unnecessary. state-command-router.cjs's workstream fallback guard removed. cjs-sdk-seam.md and the regression test updated to document the resolution. Unchanged from the previous commit on this branch. The lint now reports 22 cooperating siblings, 0 backlog pairs. The retrospective section "Open follow-ups" is reduced to the three intentional carve-outs above; the four stale subsections (8 MIGRATE_ME pairs, per-family Phase 5.x candidates, workstream support, parity divergences) are gone because they're all resolved in this commit. - Full CJS suite: 9441/9441 pass (baseline pre-Phase-6 was 9323; +118 from the Phase 6 work — 11 lint tests + 12 state-router parity + 6 verify parity + 3 phase parity + 1 roadmap parity + 24 plan-scan parity + 20 secrets parity + 18 schema-detect parity + 15 decisions parity + 19 workstream-name-policy parity). - SDK vitest unit: 1863/1863 pass. - Hand-sync lint: 22 cooperating siblings, 0 backlog pairs. - All freshness checks: fresh. Closes #3575. Closes the migration the CJS↔SDK seam was designed to eliminate (#3524). * fix(3575): lint-shared-module-handsync emits typed JSON; tests assert on IR The lint-no-source-grep CI step rejected the original Phase 6 test file (tests/lint-shared-module-handsync.test.cjs) because it substring-matched on .stdout/.stderr from the lint script output — prohibited per CONTRIBUTING.md "Raw Text Matching on Test Outputs". Fix: add --json mode to the production lint script and assert on typed IR fields. ## Changes scripts/lint-shared-module-handsync.cjs: - New --json flag. When set: - Success: emits { ok: true, cooperatingCount, backlogCount, warnings } - Unauthorized pairs: emits { ok: false, reason: 'unauthorized_pairs', errors: [{ relCjs, tsPaths }], warnings, cooperatingCount } - Missing CJS/SDK dir: emits { ok: false, reason: 'cjs_dir_missing' | 'sdk_src_missing', path } - Default (human-readable) output unchanged. - Warnings section is suppressed in --json mode (still surfaced in the IR's `warnings` field for tests to inspect). tests/lint-shared-module-handsync.test.cjs: - runLintJson() helper replaces runLint(), invoking the script with --json and parsing the IR. - Every assertion now reads typed fields (payload.ok, payload.reason, payload.errors, payload.warnings, payload.cooperatingCount) instead of substring-matching stdout/stderr. - Test count unchanged at 9 cases across 3 describe blocks. - All pass. ## Verification - node scripts/lint-no-source-grep.cjs → exit 0, 529 test files checked, 0 violations (was: 1 violation in this test file). - node --test tests/lint-shared-module-handsync.test.cjs → 9/9 pass. - node scripts/lint-shared-module-handsync.cjs → unchanged human-readable output, 22 cooperating siblings, 0 backlog pairs. - node scripts/run-tests.cjs → 9449/9449 pass. Addresses CI failure on PR #3577 (Phase 6 of #3524). * fix(3575): address CodeRabbit review on PR #3577 Six findings resolved: 1. scripts/lint-shared-module-handsync.cjs — allowlist matching now pair-aware. Keys composite ${cjs}::${ts} instead of cjs-only, so an entry covering one (cjs, ts) pair no longer silently passes a sibling at a different ts path with the same module name. Header doc-comment also corrected: removed the stale claim about GSD_LINT_CHANGED_FILES filtering (no such code existed). 2. sdk/src/gsd-transport.ts — removed dead 'workstream_forced' member from the TransportDecision.reason union (no longer assigned after Phase 5.0 workstream-native refactor). 3. sdk/src/gsd-transport.ts — removed stale workstream interpolation from the subprocess-reason Error message; the field is no longer load-bearing for that decision path. 4. All eight generator scripts (sdk/scripts/gen-*.mjs and gen-state-document.ts) — replaced the manual entry-point check that used `new URL(process.argv[1], 'file://')`. On Windows that misparses `C:\…\gen-*.mjs` as scheme "c:" and breaks the check. Replaced with the cross-platform-safe direct comparison `fileURLToPath(import.meta.url) === process.argv[1]`. (Not using `import.meta.main` — that's only stable in Node 24+ and the project supports Node 22+.) 5. docs/agents/cjs-sdk-seam.md — added explicit `text` language specifier to the four file-path fenced blocks (lines 157, 165, 173, 181). Closing fences correctly remain bare. Verification - node scripts/lint-no-source-grep.cjs → 0 violations - node scripts/lint-shared-module-handsync.cjs → 22 cooperating siblings, 0 backlog (counts unchanged after pair-aware refactor) - node scripts/lint-shared-module-handsync.cjs --json → typed IR unchanged - All 9 generator freshness checks → fresh - node scripts/run-tests.cjs → 9449/9449 pass - sdk vitest src/gsd-transport.test.ts → 10/10 pass Tests for pair-aware matching: the existing 9 cases in tests/lint-shared-module-handsync.test.cjs already build fixture allowlist entries with both `cjs` and `ts` fields, so they implicitly exercise the new pair-aware lookup; all 9 pass. * fix(3575): address second CodeRabbit review on PR #3577 Five new findings resolved. 1. Shared SDK bridge loader (`get-shit-done/bin/lib/cjs-sdk-bridge.cjs`) Eliminates seven-fold duplication of `tryLoadSdk` / `_executeForCjs` that lived verbatim in every `*-command-router.cjs` plus a near-identical variant in `gsd-tools.cjs`. The new module exposes `tryLoadSdk()`, `getExecuteForCjs()`, and `getSdkModule()` (the last for routers that pull additional named exports, e.g. state's `formatStateLoadRawStdout`). All eight call sites refactored to consume it. As a side benefit `gsd-tools.cjs` no longer imports from the private `@gsd-build/sdk/dist/runtime-bridge-sync/index.js` subpath; everyone now uses the public package entry consistently. 2. `phase remove` accepts zero positional args (#3577 review) `phase remove --force` previously passed validation with no phase number and invoked `cmdPhaseRemove(cwd, undefined, ...)`. Tightened to `positional.length !== 1` and added the early `return` so the handler never receives an undefined phase id. 3. Decisions parser regex hardened (#3577 review) `D-[A-Za-z0-9_-]+` allowed malformed IDs like `D--foo` and `D-_bar`. Tightened to `D-[A-Za-z0-9][A-Za-z0-9_-]*` so the first character after `D-` must be alphanumeric; internal `_`/`-` still permitted. Decisions generated CJS mirror regenerated. 4. plan-scan-generator test no longer uses hardcoded `/tmp` paths `/tmp/__gsd_test_nonexistent_dir_xyz__` and `/tmp/__nonexistent_gsd_test__` could collide with prior runs on shared CI runners. Replaced with `uniqueMissingPath()` helper that synthesizes `os.tmpdir()/<prefix>-<pid>-<ms>-<random>` and force-removes the path before returning. 5. lint-shared-module-handsync test now validates pair-aware TS matching Added `rejects pair when TS path differs from allowlist entry` — a regression guard that creates an on-disk pair at `sdk/src/query/<name>.ts` but allowlists the (cjs, sdk/src/<name>.ts) shape. The lint must reject because the (cjs, ts) tuple does not match. Demonstrates the pair-aware matching added in the previous commit and locks it in. ## Wiring `cjs-sdk-bridge.cjs` added to `docs/INVENTORY.md` (count 68→69) and `docs/INVENTORY-MANIFEST.json` regenerated. ## Verification - node scripts/lint-no-source-grep.cjs → 0 violations (529 files) - node scripts/lint-shared-module-handsync.cjs → 22 cooperating, 0 backlog - node scripts/run-tests.cjs → 9452/9452 pass (was 9449 + 1 lint-test + 1 changed plan-scan path test) - node sdk/scripts/check-decisions-fresh.mjs → fresh - sdk vitest src/query/decisions.test.ts → 15/15 pass * docs(3575): correct PR/issue refs in cjs-sdk-seam.md CodeRabbit caught two stale references that conflated the issue number (#3575) with the PR number (#3577). Phase 6 ships as PR #3577 closing issue #3575. Migration overview table row and the Final Completion Summary updated accordingly. * fix(3575): cjs-sdk-bridge actually loads the SDK (was dead-code since Phase 5.0) ## The bug `cjs-sdk-bridge.cjs:tryLoadSdk()` resolved `require('@gsd-build/sdk')`, but that package name is not installed in the root `node_modules` (the SDK lives as `./sdk/` — a sibling workspace, not a dependency) and the SDK's public entry doesn't re-export `executeForCjs` or `formatStateLoadRawStdout` anyway. `tryLoadSdk()` always returned false, the `_loadFailed = true` cache made every subsequent call return false for the lifetime of the process, and every CJS router silently fell through to the CJS handler. The pattern shipped in Phase 5.0 (PR #3558, merged) via `require('@gsd-build/sdk/dist/runtime-bridge-sync/index.js')` and was inherited into the routers via `require('@gsd-build/sdk')` in Phase 5.1 (PR #3574, merged). Both subpaths/imports failed in the same way. CI passed for the whole CJS↔SDK migration because the CJS fallback handlers kept running — meaning the entire claimed "state.* delegation" never actually executed via the SDK in any shipped run. This is exactly the silent-drift class the Phase 6 lint and retrospective are supposed to prevent. Catching it here closes the loop. ## The fix Resolve the bundled SDK by **package-relative filesystem path**: <root>/sdk/dist/runtime-bridge-sync/index.js <root>/sdk/dist/query/state-project-load.js The `files` array in `package.json` keeps `sdk/dist` at the same relative location inside the published tarball, so the path works in both dev and post-install. The two-file split is necessary because `formatStateLoadRawStdout` lives in the state handler, not the runtime-bridge entry. ## Integration test `tests/cjs-sdk-bridge-integration.test.cjs` proves four things and locks the load-success invariant so this regression cannot recur: 1. tryLoadSdk() returns true on the current checkout 2. getExecuteForCjs() returns a function (not null) 3. getFormatStateLoadRawStdout() returns a function (not null) 4. executeForCjs() actually dispatches a canonical registry command (generate-slug) and returns an ok:true result — proving real SDK execution, not a silent CJS-fallback ## State-router formatter wiring The state command router was reaching into `getSdkModule()` to pluck `formatStateLoadRawStdout`. Replaced with the explicit `getFormatStateLoadRawStdout()` getter so the bridge module owns all SDK-export resolution. ## state.load --raw output mode While the bridge was broken, the state.load --raw test happened to pass via CJS fallback. The first SDK execution exposed a contract mismatch: passing `mode: 'raw'` to the bridge tells the SDK to pre-render result.data to a JSON string, but the router was also calling `formatStateLoadRawStdout(result.data)` to project to key=value lines — the formatter saw a string and no-op'd. Fix: when a CJS-side rawFormatter is supplied, the router requests `mode: 'json'` from the bridge (always get typed data) and runs the formatter itself. When no rawFormatter, the user's --raw flag flows through to the bridge as usual. ## Surfaced pre-existing parity gaps (NOT yet fixed) With the bridge now actually executing the SDK, 8 `tests/state.test.cjs` cases reveal pre-existing CJS↔SDK behavioral drift that Phase 5.1's "104/104 pass" report could not see because the SDK was never running: - `state load returns error when STATE.md missing` - `state get returns error when STATE.md missing` - `state update returns error when STATE.md missing` - `state update reports field not found` - `state patch / record-metric / update-progress / resolve-blocker / record-session — error when STATE.md missing` - `add-decision --summary-file` / `add-blocker --text-file` (file-input path rejected by SDK security check) Each is a real CJS↔SDK divergence that needs explicit alignment in the SDK handler. Listed here so the next commit can address them honestly rather than letting the broken bridge mask them again. * fix(3575): align SDK with CJS contract — bridge-exposed divergences The Phase 5.1 bridge fix (0fc60b0c) made executeForCjs() actually load and dispatch. With routers now hitting the SDK in normal layouts, six CJS↔SDK behavioral divergences became visible. This commit aligns the SDK to match the canonical CJS contract test-by-test. ROUTER CHANGES (mode: raw → mode: json) All 7 CJS routers were passing `mode: raw ? 'raw' : 'json'`. With the bridge active, `mode: 'raw'` makes the bridge pre-render result.data to a JSON string, which CJS output() then re-stringifies — producing a JSON string of a JSON string. Routers now always request typed JSON; CJS output() handles user- facing rendering. Affected: gsd-tools, init, phase, phases, roadmap, state, validate, verify routers. SDK STATE MUTATION HANDLERS (sdk/src/query/state-mutation.ts) state.update / record-metric / update-progress / resolve-blocker / record- session no longer auto-create STATE.md via readModifyWriteStateMd. CJS errors out when STATE.md is missing; SDK now does the same via an upfront existsSync check returning {updated: false, reason: 'STATE.md not found'}. Also fixes: • resolve-blocker semantic: SDK returned resolved:false when no blocker line matched. CJS returns resolved:true whenever the Blockers section exists. Aligned. • readTextArgOrFile path validation: rejected /var/folders paths on macOS because /var → /private/var is a symlink. Now resolves both base and target via realpathSync before the prefix check. STATE.MD STOPPED_AT SCOPING (sdk/src/query/state.ts) buildStateFrontmatter extracted `Stopped At` from the entire body; CJS scopes it to the ## Session section. Bug-2444 parity restored — the field no longer bleeds in from unrelated sections of STATE.md. PHASE_DIR_COUNT MILESTONE FILTER (sdk/src/query/init.ts) initNewMilestone counted every directory under phases/ regardless of which milestone it belonged to. CJS uses getMilestonePhaseFilter to count only current-milestone phase dirs. Bug-2445 parity restored. ARCHIVED PHASE GUARD (sdk/src/query/init.ts) shouldDropArchivedPhaseMatch had an extra `archivedTag === milestone.version` escape hatch that doesn't exist in CJS. CJS unconditionally drops the archived match when the phase appears in the current ROADMAP. Removed the escape hatch — fixes the bug #2391 regression where `init plan-phase 03` returned the archived v1.0 phase instead of the current ROADMAP phase. PADDING-TOLERANT ROADMAP PHASE LOOKUP (sdk/src/query/roadmap.ts) searchPhaseInContent used `escapeRegex(phaseNum)` as the phase-number fragment — `03` failed to match `Phase 3:` headings. CJS uses phaseMarkdownRegexSource which emits `0*<integer>` for padding tolerance. Restored same helper inline in roadmap.ts. Fixes bug #2391 / #3537 parity in zero-padded phase lookups. STATE COMMAND ROUTER STATE.MD-MISSING ERROR SURFACE (get-shit-done/bin/lib/state-command-router.cjs) state.get must surface "STATE.md not found" as an error (matching CJS exit behavior); other state mutations must surface {updated: false, reason: ...} as data. Added EXIT_ON_STATE_MD_MISSING discriminator with STATE_MD_MISSING_ MESSAGE constant. VERIFICATION • init.test.cjs — 93/93 pass (was 91/2 fail) • state.test.cjs — 104/104 pass (was 95/9 fail) • core.test.cjs — pass • roadmap.test.cjs — pass • cjs-sdk-bridge-integration.test.cjs — 4/4 pass (bridge load locked in) The 13 phase.test.cjs failures (next-decimal 999.x backlog skip, add-batch JSON validation, insert dry-run rejection, find-phase non-canonical warnings) are pre-existing SDK gaps from the broken-bridge era and will be addressed in a follow-up commit on this same PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): align SDK phase handlers with CJS (wave 2 — phase.test.cjs) The bridge-fix (0fc60b0c) exposed 13 more CJS↔SDK behavioral divergences inside the phase command family. All are now aligned to the canonical CJS contract, with per-test verification. phase.ts: • Centralised isCanonicalPlanFile / looksLikePlanFile / describeNonCanonical Plans helpers mirroring phase.cjs:17–52. Exported for reuse from phase-lifecycle.ts (phasesList) so the warning shape never drifts between read sites. • searchPhaseInDir now emits result.warning (singular) with the canonical message when a plan-shaped file would be skipped by the canonical filter. Bug #2893 parity for find-phase. • phasePlanIndex moved its non-canonical warning to the singular result.warning field (was a generic entry in result.warnings) so consumers see the same field name and message format as find-phase / phases-list. Other diagnostics (unresolved deps, wave-declaration mismatches) still flow through the warnings array unchanged. • Added PhaseInfo.warning to the type. getPhaseFileStats now also returns allFiles so the caller can compute the diagnostic without re-reading the directory. phase-lifecycle.ts: • phasesList (phases list --type plans) emits per-dir prefixed warnings matching phase.cjs:120 (`${dir}: ${describeNonCanonicalPlans(...)}`). • phaseAdd now matches the CJS router contract for arg parsing: accepts --raw (ignored), --dry-run, --id <value>; rejects every other --flag with "phase add does not support <flag>"; rejects dangling --id with "--id requires a value"; joins all positional tokens with space so `phase add User Dashboard` produces description "User Dashboard". customId comes from --id, never from positional[1]. • phaseInsert now mirrors phaseAdd's arg parsing: rejects --dry-run with "does not support --dry-run", strips --raw, joins positional.slice(1) for the description. Also reports the bug-3098 placeholder error ("Phase N exists in roadmap summary but is missing a detail section") when the ROADMAP has only a checklist entry but no detail section. • phaseAddBatch dangling --descriptions or --descriptions followed by another flag now surface "--descriptions must be a JSON array" instead of silently falling through to positional parsing or throwing "--descriptions must be a valid JSON array". • renameIntegerPhases now skips backlog phases (dirInt >= 999) — bug-2434 parity. Without this, removing phase 3 in a project with 999.1-backlog-* on disk would rename the backlog dir to 998.1-backlog-*. • updateRoadmapAfterPhaseRemoval rewritten to mirror phase.cjs:880-922 exactly: 5 targeted regex passes (not a loop), driven by three decrement helpers (decrementRoadmapPhaseNumber, decrementRoadmapPhase Token, decrementRoadmapPaddedPhaseNumber) that guard against `num >= 999`. The padded-prefix replace uses negative lookbehind/ lookahead to skip YYYY-MM-DD substrings. Fixes: - bug-2435: integer phase remove no longer corrupts dates in ROADMAP (e.g. `(Shipped: 2025-04-15)` is left alone when removing phase 4). - bug-3355: integer phase remove no longer renumbers the same phase more than once (loop overlap removed). - Backlog phases stay frozen during renumbering. • phaseComplete next-phase scan skips backlog dirs (999.x). Without this, `phase complete 2` in a project with 999.1-backlog/ on disk would emit next_phase: '999.1' even though Phase 3 exists in ROADMAP.md. Bug #2129 parity. VERIFICATION (per-test, targeted runs — full suite not exercised due to prior 89GB OOM with concurrent runs): • phase.test.cjs — 108/108 pass (was 13 fail) • init.test.cjs — 93/93 pass (no regression) • state.test.cjs — 104/104 pass (no regression) • validate.test.cjs — pass (no regression) • verify.test.cjs — pass (no regression) • core.test.cjs — pass (no regression) • roadmap.test.cjs — pass (no regression) • cjs-sdk-bridge-integration.test.cjs — 4/4 pass (bridge intact) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): align SDK roadmap-mutation helpers with CJS — bug-2005 Three CJS↔SDK divergences in the phase.complete write path were hiding behind the broken bridge: 1. replaceInCurrentMilestone (sdk/src/query/phase-roadmap-mutation.ts) The SDK port carried an extra fallback that doesn't exist in the CJS (core.cjs:1013-1022): if the "after last </details>" slice didn't match the pattern, the SDK silently retried inside the last <details> block. That fallback corrupts the current milestone when it is itself wrapped in <details open>...</details> and there's no content after the close tag — the supposed-to-be-skipped scope is the only place the match exists. Aligned to CJS: split at the last </details>, replace only in the after-slice, return. No fallback. Documented with a "do not re-add" warning since this fallback has been added back twice in prior porting passes. 2. phase complete checkbox update (sdk/src/query/phase-lifecycle.ts) The SDK was scoping the `- [ ] Phase N:` → `- [x] Phase N:` replacement through replaceInCurrentMilestone. The CJS (phase.cjs:1057) uses a direct roadmapContent.replace(...) call. When the current milestone is wrapped in <details>, the scoped variant never reaches the checkbox; direct replace finds it. Aligned with CJS. 3. phase complete plan-count update (sdk/src/query/phase-lifecycle.ts) Same pattern — the SDK was scoping the `**Plans:** X/Y` update through replaceInCurrentMilestone. CJS (phase.cjs:1080) uses direct replace. Aligned. VERIFICATION • bug-2005-phase-complete-details.test.cjs — 2/2 pass (was 1 fail) • phase.test.cjs — 108/108 pass (no regression) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): align SDK with CJS — add-decision DWIM + frontmatter paths Two more CJS↔SDK divergences exposed by the bridge fix: state.add-decision / state.add-blocker DWIM (sdk/src/query/state-mutation.ts) CJS state.cjs:481-498 + 532-548 auto-create the canonical Decisions / Blockers section when it's absent from STATE.md. The SDK was returning `{added: false, reason: '<Section> section not found in STATE.md'}` even when STATE.md was writable. Bug #3286 (parity for both verbs): • If section header pattern matches → append entry (existing path). • If section is absent → scaffold `## Decisions` (or `### Blockers`) and append the entry, then set `created: true` on the result. Matches the begin-phase / advance-plan DWIM behavior. Callers can now treat `state add-decision` as idempotent — first call creates the scaffold, subsequent calls append to it. frontmatter get/set/merge/validate (helpers.ts + frontmatter.ts + frontmatter-mutation.ts) CJS frontmatter.cjs:323/340/354/369 resolves user paths with the simple `path.isAbsolute(p) ? p : path.join(cwd, p)`. The SDK port had promoted this to `resolvePathUnderProject` which adds a real-path prefix check against the project root. That check rejects absolute paths outside the project — including macOS tmpdir paths whose names contain spaces, the exact regression cited in bug #3509. Frontmatter verbs are deliberately path-flexible in CJS because they're called against external files (plan paths from other repos, scratch markdown, tmpdir fixtures). Introduced `resolveFrontmatterPath()` mirroring the CJS one-liner. The project-scoped `resolvePathUnderProject()` is unchanged — still used for template output, decision artifacts, etc. VERIFICATION • bug-3286-state-write-routing.test.cjs — 13/13 pass (was 6 fail) • bug-3509-path-spaces.test.cjs — 6/6 pass (was 3 fail) • phase.test.cjs / init.test.cjs / state.test.cjs / validate.test.cjs / verify.test.cjs / core.test.cjs / roadmap.test.cjs — all pass (no regression — 566 total tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): route SDK state handlers through scanPhasePlans — bug-3257 The SDK port of buildStateFrontmatter / stateValidate / stateSync was using a naive top-level filter (`files.filter(/-PLAN\.md$/i)`) instead of the canonical scanPhasePlans helper. The naive filter undercounts every phase that uses the nested layout `phases/NN-name/plans/<NN>-PLAN-MM-slug.md`, which is the default the planner agent produces. CJS routes all three sites through scanPhasePlans (state.cjs:408, 824, 1427). scanPhasePlans is already a Shared Module — generated CJS at plan-scan.generated.cjs from sdk/src/query/plan-scan.ts. The fix is just to consume it. CHANGES • buildStateFrontmatter (sdk/src/query/state.ts): replaced the inline `-PLAN.md` / `-SUMMARY.md` regex filters with scanPhasePlans; use the helper's `completed` flag for diskCompletedPhases. • stateValidate (sdk/src/query/state-mutation.ts): same swap on the current-phase plan-count drift check. • stateSync (sdk/src/query/state-mutation.ts): same swap on the rollup loop. Also routes the Progress percent through computeProgressPercent(completedPlans, totalPlans, diskCompletedPhases, syncTotalPhases) so the min(plan_fraction, phase_fraction) cap from bug #3242 Bug B is applied — without this, sync emitted 60% when the real progress was capped at 50% by phase-fraction. VERIFICATION • bug-3257-nested-plans-undercount.test.cjs — 14/14 pass (was 12 fail) • phase.test.cjs / init.test.cjs / state.test.cjs / validate.test.cjs / verify.test.cjs / core.test.cjs / roadmap.test.cjs / bug-3286 / bug-2005 / bug-3509 — all pass (no regression — 580 total). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(3575): phase 6 CJS↔SDK seam behavioral contracts — TDD-found worker bug Adds tests/phase-6-cjs-sdk-seam-contracts.test.cjs — a behavioral contract suite for everything Phase 6 of #3524 introduced. Written under the issue #3592 test rewrite discipline: • No source-grep on .cjs files • No assert.match / .includes on free-form child-process stdout/stderr • Every assertion is on a parsed JSON object, a filesystem fact, an exit code, or a frozen enum value (SYNC_ERROR_KIND, BRIDGE_EXPORTS, TRANSPORT_MODE) • Helpers come from tests/helpers.cjs (runGsdTools, createTempProject, cleanup) — no inline fs.mkdtempSync • Fixture content built with array.join('\n'), never template literals • beforeEach/afterEach for shared setup; no try/finally inside tests COVERAGE 1. Bridge module surface — exports lock against BRIDGE_EXPORTS 2. Bridge load lifecycle — tryLoadSdk, getters return cached refs, pre-load returns null 3. executeForCjs RuntimeBridgeSyncResult shape — ok:true vs ok:false discriminated union; mode:"json" never double-stringifies 4. CLI family-router dispatch — one structured-JSON assertion per family (roadmap, phase, phases, state, init, validate, find-phase) 5. mode:"json" regression guard — stdout parses to object, not to JSON-encoded string (the Wave-1 double-stringify bug shape) 6. GSD_WORKSTREAM gate — SDK path and CJS fallback produce identical structured fields for the same fixture 7. Validation error taxonomy — empty arg → ok:false + errorKind: SYNC_ERROR_KIND.VALIDATION_ERROR 8. phase.add filesystem facts — directory exists, ROADMAP file grew (asserted via fs.statSync, never by reading content back) TDD-FOUND BUG (RED → GREEN) Suite §7 (validation_error taxonomy) failed in the RED phase: expected: 'validation_error' actual: 'native_failure' Root cause in sdk/src/runtime-bridge-sync/worker.ts: when an SDK handler throws a GSDError(Validation), the native direct adapter wraps it in a GSDToolsError via createNativeFailureError, preserving the original on `.cause`. classifyError only checked for TypeError causes — every GSDError cause fell through to `native_failure`, breaking the documented SyncErrorKind contract. Fix: classifyError now unwraps the cause once. When the cause is a GSDError with ErrorClassification.Validation or .Blocked, the result is errorKind: 'validation_error' (exit 10) — matching the direct branch a few lines below for unwrapped GSDError. VERIFICATION (per-test, before and after the worker fix) • Phase 6 contract suite — 21/21 pass (was 20/1 fail at RED) • phase.test.cjs — 108/108 pass • init.test.cjs — 93/93 pass • state.test.cjs — 104/104 pass • validate.test.cjs / verify.test.cjs / core.test.cjs / roadmap.test.cjs — all pass • cjs-sdk-bridge-integration.test.cjs — 4/4 pass (bridge intact) • npm run lint:tests — 0 violations (no source-grep) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): SDK config-get/set parity + reason-code propagation — bugs #2943 #3086 #3212 Three CJS↔SDK divergences in config dispatch exposed when Phase 6 routes `config-get` / `config-set` through `executeForCjs`: 1. SDK config-get was missing the SCHEMA_DEFAULTS map. CJS config.cjs:505-510 hard-codes documented defaults for `context_window` (200000), `executor.stall_detect_interval_minutes` (5), `executor.stall_threshold_minutes` (10), `git.create_tag` (true). When a config.json omits the key, CJS returns the documented default with exit 0. SDK threw `Key not found` for all four — every skill that reads `context_window`, executor stall thresholds, or the tag toggle broke under SDK dispatch. Ported the table verbatim into sdk/src/query/config-query.ts and consult it at every "not found" exit point (matching the three CJS branches: missing file, traversal collapse, terminal undefined). 2. SDK config-set was missing the `git.create_tag` boolean-only guard. CJS rejects `config-set git.create_tag maybe` because the schema is boolean. SDK silently accepted it and wrote "maybe" to disk under Phase 6 dispatch. Added the matching guard + the missing `workflow.post_planning_gaps` boolean guard. 3. SDK errors lost their structured reason code at the bridge boundary. `--json-errors` callers expect `reason: 'config_key_not_found'` etc. from a frozen `ERROR_REASON` taxonomy; the bridge dispatcher in gsd-tools.cjs was calling `error(message)` without the second argument, so every SDK-routed error surfaced as `reason: 'unknown'`. Fix is end-to-end: • config handlers tag the GSDError with `.reason = 'config_*'`. • worker.ts:classifyError reads `.reason` off the cause (or off the direct error) and forwards it via `errorDetails.reason`. • `_dispatchNonFamily` in gsd-tools.cjs passes that reason as the second arg to `error()` when present. • Also added the `--raw` scalar pass-through here, so `output(data, raw, String(data))` is called for primitive results — without it, `config-get context_window --raw` emitted the JSON shape '200000\n' which happens to match but breaks any primitive whose JSON encoding differs from its String() form (booleans for example, where the CJS produces `true` while the SDK-routed path was producing `true` — same here, but the structural guarantee was wrong before). VERIFICATION (per-test) • bug-2943-config-get-context-window-default.test.cjs — 5/5 pass • bug-3086-git-create-tag-config-gate.test.cjs — 4/4 pass • bug-3212-execute-phase-stall-safe-resume.test.cjs — 7/7 pass • Phase 6 contract suite — 21/21 pass • phase/init/state/core/roadmap/validate/verify — all pass (570 total) • Full bug-* suite: 24 fail → 17 fail (7 fixed in this commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): SDK milestone-archive layout discovery — bug #3164 Two CJS↔SDK divergences in phase discovery and validation surfaced when projects moved to the milestone-archive layout (`.planning/milestones/v<version>-phases/<phase>/`) instead of the flat `.planning/phases/<phase>/`. 1. SDK findPhase had no `searched_directories` field on the not-found payload. CJS surfaces this for diagnostics. Added: track every directory probed (the active `.planning/phases/` plus each archive root) and include the relative paths in the not-found payload. Bug #3164 — #find-phase tests. 2. SDK validateConsistency only scanned `.planning/phases/`. CJS `cmdValidateConsistency` (verify.cjs:467) walks every active phase root via `collectPhaseRoots(planBase)` — the flat dir plus the active milestone archive resolved from STATE.md. Without parity, every roadmap phase on a milestone-archive-layout project emitted W006 ("no directory on disk") even though the phases were present in the archive. Ported the helper trio (listMilestoneArchiveDirs, getActiveMilestoneArchiveDir, collectPhaseRoots) verbatim from verify.cjs:400-444 and rewrote validateConsistency's disk-phase scan + per-phase plan scan to iterate `phaseRoots`. Warning labels now include the archive prefix so users can tell which root surfaced the issue. Also accepts prefixed archive dir names (`CK-64-...`) as phase 64 via the `(?:[A-Z]{1,6}-)?` group at the head of PHASE_TOKEN_FROM_DIR_RE — same regex CJS uses. VERIFICATION (per-test) • bug-3164-milestone-archive-layout.test.cjs — 8/8 pass • Phase 6 contract suite — 21/21 pass • phase/init/state/validate/verify/core/roadmap — 570 pass • Full bug-* suite: 17 fail → 12 fail (5 fixed in this commit; cumulative 12 fixed since Wave 6 start). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): padded phase IDs match unpadded ROADMAP prose — bug #3537 Three failures in bug-3537-padded-id-against-unpadded-roadmap: 1. roadmap.get-phase returned `phase_number` verbatim from the user input — `02.7` produced `"phase_number": "02.7"` while `2.7` produced `"phase_number": "2.7"` on the same fixture, so a parity compare of the two stdouts fails. Fixed by promoting the matched phase token in `searchPhaseInContent` to a capture group and returning that as the canonical `phase_number`. Same fix in the checklist-fallback branch so the malformed-roadmap diagnostic carries the as-written form too. 2. phase.complete built every ROADMAP-prose regex from `escapeRegex(phaseNum)` instead of the padding-tolerant `phaseMarkdownRegexSource(phaseNum)`. Calling `phase complete 02.7` against the un-padded heading `### Phase 2.7:` matched nothing — checkbox didn't flip, plan count stayed at `0/1`, table row stayed `Planned`. Promoted `phaseMarkdownRegexSource` to an exported helper in roadmap.ts and wired it into phaseComplete's roadmap mutation block. 3. roadmap.annotate-dependencies infinite-looped through the bridge. The SDK handler delegates to `spawnSync(gsd-tools.cjs roadmap annotate-dependencies …)`; the child re-entered the roadmap router; the router re-dispatched through executeForCjs; synckit spawned the same SDK worker; that worker spawned gsd-tools.cjs again; … Recursion hit the 15s timeout and the test reported `code=null`. Fixed with a `GSD_SDK_NESTED=1` env-var guard: the SDK handler sets it when spawning the child, and the CJS roadmap router refuses SDK dispatch when it sees the flag. VERIFICATION (per-test) • bug-3537-padded-id-against-unpadded-roadmap.test.cjs — 6/6 pass • Phase 6 contract suite — 21/21 pass • phase/init/state/validate/verify/core/roadmap — 570 pass • Full bug-* suite: 12 fail → 7 fail (5 fixed in this commit; cumulative 17 fixed across the wave-6/7/8 sequence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): final-7 SDK parity — bugs #2787 #2268 #2526 Closes out the bug-suite tail. Three independent fixes against three independent regressions surfaced when Phase 6 routed read-only and mutation paths through the SDK. 1. extractCurrentMilestone truncated at heading-like lines inside fenced code blocks — bug #2787. The `^#{1,N}\\s+...vX.Y` scan ran with the `/m` flag, which matches `^` at every newline, including newlines inside ``` and ~~~ fences. A snippet like ```bash # Ops runbook — v1.0 compat ``` placed between Phase 2 and Phase 3 of a v1.1 milestone shortened the milestone slice and made phases 3, 4 invisible to roadmap.analyze / roadmap.get-phase. Added `isInsideFencedCodeBlock(content, offset)` — a GFM-aware walker that toggles a `fenceChar` cursor on each fence boundary (backticks and tildes; closing fences require the matching character and no info string — so ```js inside ```text does NOT close). The nextMilestoneRegex loop now skips any match that falls inside an open fence. 2. init.manager only marked the FIRST undiscussed phase as `is_next_to_discuss` — bug #2268. Two and five-phase fixtures both proved the regression: parallel-discuss capacity was lost, recommended_actions emitted at most one discuss action even when callers were free to take several. Replaced the sliding- window loop with an unconditional `phase.is_next_to_discuss = (status === 'empty' || status === 'no_directory')`. 3. phase.complete didn't surface "REQ-IDs found in body but missing from Traceability table" warnings — bug #2526. CJS phase.cjs:1140-1167 scans REQUIREMENTS.md for `**REQ-ID**` references in the body, intersects against the IDs that actually appear in the Traceability section table, and warns about the diff. The SDK port only ran the per-roadmap-REQ checkbox update and never emitted the body-scan warning. Added the missing scan + warning push; also routed the writeFile through a `reqContentChanged` flag so we only write when at least one substitution actually fired (parity with the implicit "every checkbox already complete" no-write CJS branch). VERIFICATION • bug-2787-milestone-fenced-block-truncation.test.cjs — 4/4 pass • bug-2268-parallel-discuss.test.cjs — 4/4 pass • bug-2526-phase-complete-req-discovery.test.cjs — 3/3 pass • Phase 6 contract suite — 21/21 pass • Major suites (phase/init/state/validate/verify/core/roadmap) — 570 pass • **Full bug-* suite: 2397/2397 pass — ZERO failures.** • Combined run (major + bug-*): 2967/2967 pass — zero failures. Cumulative since the bridge-fix landing (PR #3577): 12 sub-test regressions surfaced + every one resolved. Phase 6 is now byte-for- byte CJS-parity across every command family verified by the test suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): preserve codex runtime command shape after router migration * test(3575): pin agent-install-validation init tests to GSD_AGENTS_DIR PR #3577 routed init.execute-phase and init.plan-phase through executeForCjs to the SDK handlers. The SDK side's resolveAgentsDir (sdk/src/query/helpers.ts) honors GSD_AGENTS_DIR or falls back to <runtimeConfigDir>/agents; it does not walk up from cwd to find <repo>/agents/ like the CJS-era code did. The two init-suite tests that asserted agents_installed=true relied on that implicit walk and only passed on dev machines where ~/.claude/agents/ already had the 33 agents installed — Linux CI runners have neither. Match the pattern every passing sibling in this file already uses: pass { GSD_AGENTS_DIR: REPO_AGENTS_DIR } through runGsdTools so the SDK resolver points at the repo's agents/ dir explicitly. No production code change. Refs sdk/src/query/QUERY-HANDLERS.md ("subprocess vs in-process path resolution") and CONTEXT.md DEFECT.PORT-DRIFT.cjs-sdk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3577): Phase 6 config-* SDK port parity carve-outs Restored the legacy contract for four CLI tests broken by the Phase 6 router migration: 1. `config-ensure-section` was bound to the new SDK `configEnsureSection` handler which requires `args[0]=sectionName`. Every real CLI caller uses the no-arg form expecting full default config.json creation. Reverted the dispatch case to call `config.cmdConfigEnsureSection` directly (matches the precedent in 7d5dfa9d for `codex` runtime). 2. SDK `configNewProject` `commit_docs` and `parallelization` defaults set to `true`/`true` (was `false`/`1`) — aligned with `sdk/shared/config-defaults.manifest.json` and the CJS `buildNewProjectConfig` `hardcoded` block. 3. SDK `configNewProject` returns the project-rooted relative path `.planning/config.json` instead of the absolute `paths.config`, matching the CJS `ensureConfigFile` output shape. 4. SDK error vocabulary aligned with CJS: `Unknown config key: <key>` (no surrounding quotes), and config-get's malformed-JSON message leads with `Failed to read config.json:` so legacy substring assertions in `tests/config.test.cjs` keep matching. Local: 132/132 across `tests/{config,agent-skills,ai-evals}.test.cjs`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3631): family routers forward --raw to SDK bridge as mode:'raw' #3577 routed every family subcommand through the SDK bridge with a hardcoded mode:'json'. With --raw set, the bridge returned the typed JSON IR and routers called `output(result.data)` — bypassing output()'s rawValue branch. Shell consumers expecting scalar tokens (`gsd-tools phase next-decimal --raw 1` → `1.1`) received the JSON- stringified IR instead. Each `*-command-router.cjs` SDK dispatch path now requests `mode: raw ? 'raw' : 'json'` from the bridge. The sync-bridge worker is wired to `formatNativeRaw = formatQueryRawOutput` so the bridge returns the per-command scalar projection. Routers route the formatted string through `output(null, true, str)` (rawValue branch) so it lands on stdout verbatim. formatQueryRawOutput extended for the two commands covered by the issue acceptance criteria — phase.next-decimal (→ data.next) and roadmap.get-phase (→ data.section). Other registered raw projections (state.load, commit, config-set, state.begin-phase) are unaffected; the default `safeStringify` branch still applies to unprojected commands. state-command-router already had a dispatchViaSdk helper that selected mode based on a rawFormatter. The trailing fallthrough `output(result.data)` when no rawFormatter was present is the same regression and was patched to use the rawValue branch under --raw. Regression test `tests/bug-3631-router-raw-flag.test.cjs` exercises end-to-end: - `phase next-decimal --raw 1` emits a scalar phase token (not JSON). - `roadmap get-phase --raw 2` emits the section text (not JSON). The fix targets `feat/3575-enforcement-hardening` (PR #3577, open) — not origin/main as the issue body asserted. The #3577 regression lives on that branch and the fix needs to land there before merge. Fixes #3631 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(3631): force CJS dispatch path in router unit tests via GSD_WORKSTREAM phases-command-router.test.cjs and roadmap-command-router.test.cjs mock the CJS-side `phase`/`milestone`/`roadmap` handlers and assert they are called with the parsed args. Since #3577 the router prefers SDK dispatch when sdk/dist is present — the mocks are then bypassed and the SDK side fails because the test cwd `/tmp/proj` has no `.planning/` fixture. The router already gates SDK dispatch on `process.env.GSD_WORKSTREAM` being unset (workstream-scoped requests fall through to CJS). Setting GSD_WORKSTREAM in before()/after() deterministically routes through the CJS handlers the tests were written against, without weakening the assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3632): report each ts sibling independently in lint-shared-module-handsync The cooperatingPairs lookup ran inside `.some()` over all ts candidates for a given cjs. When two ts siblings shared the same basename (e.g. `sdk/src/foo.ts` and `sdk/src/query/foo.ts`) and only one pair was allowlisted, `.some()` short-circuited and the unallowlisted sibling silently passed through CI. Classify each ts sibling independently against the allowlist so partially- allowlisted multi-sibling drift surfaces. Added regression test `reports unallowlisted ts sibling when another ts sibling for the same cjs IS allowlisted (#3632)`. Real-tree lint output unchanged on `feat/3575-enforcement-hardening`: 22 cooperating siblings, 0 unauthorized, 0 backlog pairs. Fixes #3632 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3577): ADR/PRD compliance + SDK port completeness for Phase 6 Multiple ADR/PRD violations in the Phase 6 cutover surfaced during gsd-test-summary docker runs. Root causes traced to docs/adr/ 3524-cjs-sdk-hard-seam.md §3 (out-of-seam module list) and docs/prd/3524-cjs-sdk-hard-seam.md L160 (CJS-only verbs must not route through the SDK runtime bridge), plus port-drift bugs the ADR was specifically written to prevent (DEFECT.PORT-DRIFT.cjs-sdk). Out-of-seam Module bindings removed from SDK catalog/manifests: - verify.codebase-drift (drift is CJS-only; the SDK stub used execFileSync back to gsd-tools, recursing infinitely with the Phase 6 router rewrite — forked hundreds of node procs on the 64 GiB plex2 docker host before manual kill) - intel.* (8 verbs: diff, snapshot, validate, status, query, extract-exports, patch-meta, update — intel is CJS-only per ADR) Both already have direct-CJS dispatch in gsd-tools.cjs (case 'intel') and verify-command-router.cjs (`'codebase-drift':` now calls verify.cmdVerifyCodebaseDrift without going via sdkHandler). config-ensure-section cutover restored via catalog rebind: - 'config-ensure-section' in command-static-catalog-foundation.ts rebound from configEnsureSection (single-section semantics, requires args[0]=sectionName the CLI never passes) to configNewProject (whose no-args branch produces the full default config.json — matches the legacy ensureConfigFile contract). - gsd-tools.cjs `case 'config-ensure-section'` restored to its Phase 6 _dispatchNonFamily form (no CJS fallback — the SDK handler now does the right thing). configNewProject defaults from canonical manifest: - Replaced the hardcoded duplicate `defaults` block with a derivation from CONFIG_DEFAULTS (sdk/src/configuration/index.ts, sourced from sdk/shared/config-defaults.manifest.json). The duplicate had drifted — omitted workflow.{ai_integration_phase, tdd_mode, human_verify_mode, pattern_mapper, plan_bounce*, auto_prune_state, subagent_timeout, security_*, post_planning_gaps}, git.create_tag, claude_md_path, planning.*, graphify.*, mode, resolve_model_ids, context_window — every one of which had a test asserting the post-init value. SDK configSet value-validation port (CJS cmdConfigSet parity): - workflow.drift_action enum (warn|auto-remap) - workflow.drift_threshold positive-integer - workflow.human_verify_mode enum (mid-flight|end-of-phase) - statusline.context_position enum (front|end) - code_quality.fallow.scope enum (phase|repo) - code_quality.fallow.profile enum (minimal|standard|strict) - review.default_reviewers array shape + slug regex + lowercase-unique normalisation (matches bin/lib/review-reviewer-selection.cjs normalizeConfiguredDefaultReviewers, with the normalised value persisted to disk) Init/roadmap/phase/workspace/frontmatter handler fixes: - initExecutePhase + initPlanPhase parse --tdd boolean override - initMapCodebase reads workflow.subagent_timeout with 300000 default per manifest - roadmapAnalyze surfaces `mode` per phase (parity with roadmapGetPhase) - phaseComplete auto-prunes STATE.md when workflow.auto_prune_state is true (port of bin/lib/phase.cjs:1378-1390; #2087) - initRemoveWorkspace throws GSDError on no-name and workspace-not-found instead of returning {data:{error}} which the CLI output path treated as success - frontmatterGet parses --field <name> in addition to positional args[1] Local: 150/150 across the failing-cluster test files (review-default-reviewers-config, subagent-timeout, pattern-mapper, tdd-mode, drift-detection, roadmap-mode-field, workspace, phase-complete-auto-prune, frontmatter-cli). Docker gsd-test-summary re-run in progress for full validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3577): clear 12 ubuntu-only regressions surfaced by gsd-test-summary Docker test pass 3 (holodeck) surfaced 12 real bugs after the earlier ADR/PRD-compliance commit (cf4dd0cb). Every one is a SDK-side bug — fix-forward, not "pre-existing": bug-3599 (2 subtests) — roadmap.get-phase project-code-prefix lookup: Ported phaseMarkdownRegexSourceExact from CJS (core.cjs:704-708) so `PROJ-42` queries try the exact escaped form FIRST before falling back to the padding-tolerant numeric. searchPhaseInContent now does two-pass lookup. Without this, `roadmap get-phase PROJ-42` returned not-found even when ROADMAP contains `### Phase PROJ-42:`, and bare `42` queries cross-matched the PROJ-42 heading. roadmap-mode-field (1) — roadmapAnalyze surfaces `mode` per phase: Extracts the same `**Mode:**` field that roadmapGetPhase already parses (CONTEXT.md "MVP Mode" glossary). Without this, downstream consumers reading roadmap.analyze output couldn't tell which phases were MVP-mode. bug-3601 (2 subtests) — phase.remove preserves peer-depth decimals: Ported the depth-aware end-of-section regex from CJS phase.cjs (named capture `(?<h>#{2,4})` + `\k<h>(?!#)` backreference). Now removing `### Phase 2:` stops at `### Phase 2.1:` (same depth, peer decimal) while continuing past `#### Phase 27.1:` (child depth). bug-3602 (1 subtest) — phase.remove renumbers slugged plan refs: Extended the padded-plan-reference pattern with optional kebab-case slug segments `(?:-[A-Za-z][A-Za-z0-9-]*)*` between NN-NN and the PLAN/SUMMARY suffix, matching CJS phase.cjs:#3602 fix. Without this, `07-01-cherry-pick-foundation-PLAN.md` references stayed at `07-01-` after Phase 7 was removed, while the file on disk was already `06-01-...`. config.test (1) — config-get git.base_branch returns "Key not found": configNewProject now filters out manifest keys legacy CJS init does NOT materialize: top-level `resolve_model_ids`, `context_window`, `mode`, `planning`, `graphify`; nested `git.base_branch`. These have their own resolution paths (origin/HEAD auto-detect for base_branch, feature opt-in for planning/graphify) and materializing the manifest defaults would suppress them. Manifest stays the schema source of truth per ADR §6; init shape stays minimal per legacy CJS contract. gsd-sdk-query-registry-integration (1) — agents/gsd-intel-updater.md references retargeted from `gsd-sdk query intel.*` to `gsd-tools intel <subcommand>`. intel is out-of-seam per ADR §3 / PRD L160 ("CJS-only Module handlers ... keep their in-process CJS implementations"). Removing the SDK catalog entries (cf4dd0cb) made the SDK route invalid; the agent now correctly invokes the CJS handler via gsd-tools, which routes through Shell Command Projection for cross-platform formatting. Local: 79/79 across the failing test files. Docker re-run in progress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3577): regenerate command-aliases + retarget workflow drift-gate CI ubuntu-24 surfaced two remaining ADR-compliance gaps after the previous push: 1. `sdk/src/query/command-aliases.generated.{ts,cjs}` still listed verify.codebase-drift + intel.{snapshot,patch-meta} from before the manifest-side removal. Ran `npx tsx sdk/scripts/gen-command-aliases.ts` to regenerate; both files now match the manifest source of truth. Closes the `command-seam-coverage.test.ts` "missing registry canonical verify.codebase-drift" failure (its assertion is correct — the SDK does NOT register codebase-drift, so the alias entry must not be present either). 2. `get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md` invoked `gsd-sdk query verify.codebase-drift` — drift is out-of-seam (CJS-only) per ADR §3 / PRD L160, so there is no SDK handler to route through. Retargeted to `gsd-tools verify codebase-drift` which dispatches direct to bin/lib/drift.cjs (the canonical implementation) via the CJS router. Closes the `gsd-sdk-query-registry-integration.test.cjs` failure. Local: docker gsd-test-summary 11383/0 on plex2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3575): raise Node heap for coverage in CI matrix --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: ci <ci@gsd-build> |
||
|
|
2e4fe65816 |
fix(3579): ship graphify hook + lib/ helper through build-hooks + install (#3640)
* fix(3579): ship graphify hook + lib/ helper through build-hooks + install `scripts/build-hooks.js` `HOOKS_TO_COPY` did not include `gsd-graphify-update.sh` (added in #3347 / PR #3557), so it never landed in `hooks/dist/` and `bin/install.js` — which `readdirSync`s the dist — never copied it to `~/.claude/hooks/`. The hook's detached rebuild helper at `hooks/lib/gsd-graphify-rebuild.sh` was also silently dropped because both build-hooks.js (flat allowlist) and bin/install.js (readdir + isFile filter) only walked top-level files. The published tarball gap (Gap 3 in the issue body) is not reproduced on origin/main — `npm pack --dry-run --json` shows both source files are present today. Only Gaps 1 and 2 are in scope. Changes: - Add `gsd-graphify-update.sh` to `HOOKS_TO_COPY`. - Add `HOOKS_SUBDIRS_TO_COPY = ['lib']` and copy whitelisted hook subdirectories (`hooks/<dir>/*` → `hooks/dist/<dir>/*`) in build-hooks.js, with the same syntax-check + atomic-rename path the top-level loop uses. - `bin/install.js`: when copying `hooks/dist/`, recurse one level into any directory entry so subdir files (e.g. `lib/gsd-graphify-rebuild.sh`) land at the mirrored target path the hook's REBUILD_SCRIPT lookup expects. Top-level if/else structure for files is unchanged. Regression test `tests/bug-3579-graphify-hook-publish.test.cjs`: - Drift guard: every top-level `hooks/*.sh` must appear in `HOOKS_TO_COPY`. Generalizes beyond graphify so the next .sh hook added cannot regress. - After build: `hooks/dist/gsd-graphify-update.sh` AND `hooks/dist/lib/gsd-graphify-rebuild.sh` exist. - After install: both files land at the target, and no "Missing expected hook: gsd-graphify-update.sh" warning is emitted. Fixes #3579 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(3579): replace source-grep drift guard with filesystem-behavior assertion The Gap-1 drift guard read scripts/build-hooks.js as text and regex-parsed the HOOKS_TO_COPY literal, which tripped lint-no-source-grep and is brittle under refactors. Replace with a behavior-based assertion: run the build, then for every top-level hooks/*.sh assert hooks/dist/<name> exists. Strictly stronger — catches both the original allowlist gap and any future regression that silently drops a hook for any other reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
05316369ae |
fix(3583): normalize retired colon-form commands in generated Claude/Qwen/Hermes SKILL.md bodies (#3629)
* Add first-class grok runtime support (maps to ~/.agents); wire installer, runtime-homes.cjs and sync-skills; update Grok Build engine in local ~/.agents to latest; record session progress in discussion doc * Normalize gsd colon references to hyphen in generated Claude SKILL.md bodies using the shared transformer. Fixes #3583. * Refine #3583 implementation after review: cache command names, improve tests, clean up comments * Harden gsd colon-to-hyphen transformer with bidirectional word boundaries and body-only regression guard * Track quick-wins batch status and local session notes for #3583/#3579 handoff * Port installer robustness (hoist copyLibDir + selective Codex hooks) from 3579 to make Codex tests pass on this branch. Fixes ReferenceError and prevents extra hook pollution in Codex installs. * Restore #3583 transformer wiring and Codex .sh GSD_VERSION branch lost in 50ff8f17 port Commit 50ff8f17 ('Port installer robustness from #3579') accidentally reverted: - the top-level require of transformContentToHyphen/readGsdCommandNames - the body normalization inside convertClaudeCommandToClaudeSkill - the Codex hook loop's .sh branch with {{GSD_VERSION}} substitution These were the actual #3583 fix and the Codex half of the #2136 invariant. Failing tests fixed: bug-2808-skill-hyphen-name, claude-skills-migration #3583 case, bug-2136 Codex .sh substitution. * Exempt 'sync-skills' slug from docs-parity check (skill dir name in path references) gsd-sync-skills is an installed Claude skill name and a workflow file but not a registered slash command. The docs-parity regex catches /gsd-sync-skills from filesystem path references like ~/.agents/skills/gsd-sync-skills/ in docs/discussions/grok-build-support-2026-05.md. Adding to INTERNAL_COMPONENT_SLUGS matches the existing exemption pattern for 'statusline', 'workspaces', 'graphify-update', etc. * Restrict hooks/lib/ install to hook-enabled runtimes and managed allowlist Codex/Copilot/Cursor/Windsurf/Trae/Cline already skip the hooks block but were still copying hooks/lib/ helpers, contradicting the downstream Codex comment. Gate the call on the same runtime check and pass GSD_HOOK_LIB_FILES so install scope matches the uninstall/manifest scope. |
||
|
|
7f8b5701bf |
Enforce documentation updates via lint:docs + PR templates (#3651)
* Enforce documentation updates via lint:docs + PR templates (#3213) New scripts/lint-docs-required.cjs + Docs Required CI workflow fail any PR whose changeset fragment is typed Added / Changed / Deprecated / Removed without modifying at least one file under docs/. Mirrors scripts/changeset/lint.cjs: pure evaluateLint({ changedFiles, fragments, labels }) returning { ok, reason, triggering } over a frozen LINT_REASON enum; CLI wrapper reads the PR diff and parses each touched changeset fragment via the existing parseFragment helper. Escape hatches: - no-docs PR label (global) - per-fragment <!-- docs-exempt: <reason> --> marker, all triggering fragments must carry it for the PR to pass Fixed and Security fragments do not trigger the lint — bug fixes restore documented behavior, they do not introduce new behavior to document. PR templates (enhancement.md, feature.md) gain a Documentation checklist section pointing at the which-doc-to-update matrix. CONTRIBUTING.md adds a Documentation Updates section codifying that matrix, the English-canonical language policy for docs/ and the root README, and the two opt-out routes. Closes #3213 * Address Codex review: fail-closed on malformed fragments and strip docs-exempt marker from rendered release notes (#3213) Two P2 issues caught by `codex review --base main`: 1) Malformed fragments could silently bypass docs enforcement. parseFragment would return ok:false on a triggering Added fragment with bad frontmatter and readFragmentsFromDisk dropped it, so evaluateLint saw no triggering fragments and passed. The changeset-required lint only checks fragment _presence_ not _validity_, so the assumed fallback did not catch it. Fix: readFragmentsFromDisk now returns { fragments, malformed }; evaluateLint accepts a malformed param and emits a new FAIL_MALFORMED_FRAGMENT verdict that outranks every OK path (including the no-docs label) — a parse failure must be fixed before docs lint can decide anything else. 2) The per-fragment <!-- docs-exempt: reason --> marker lived in the fragment body, so the existing changelog (serializeChangelog) and GitHub release-notes (formatBullet) serializers published it verbatim. Worse, both serializers append `(#NNNN)` to the body's last line — with the marker as the trailing line, the PR suffix attached to the hidden comment instead of the visible bullet. Fix: parseFragment now extracts the marker into a typed `docsExempt` field and strips it from `body`, so all downstream renderers produce clean output without remembering to strip. The regex is anchored to its own line (^...$ with m flag) so inline mentions of the marker syntax in documentation (e.g. inside backticks) cannot accidentally exempt a fragment. Bounded character class [^\n>] keeps the regex linear-time. Test additions: - tests/lint-docs-required.test.cjs: FAIL_MALFORMED_FRAGMENT coverage, end-to-end "Added fragment with bad pr → malformed → fail-closed" regression test, updated readFragmentsFromDisk return-shape assertions, isExemptFragment now checks the typed docsExempt field rather than body content. - tests/changeset-parse.test.cjs: extractDocsExempt extraction cases (with/ without reason, case-insensitive, EMPTY_BODY when body is only a marker), inline-mention false-positive guard, real-marker-wins-when-also-inline test. - tests/changeset-new.test.cjs: fragment shape now includes docsExempt: null. CONTRIBUTING.md updated to clarify the "on its own line" requirement and the parse-time stripping behavior. The bootstrap fragment cleaned up so its body no longer contains a literal marker example that would have triggered the false-positive case. Full suite: 9696/9696 pass. * CRLF-safe docs-exempt marker stripping (Codex review pass 2, #3213) Second `codex review --commit` pass caught a CRLF regression in the docs-exempt extraction added in the previous commit. Repro: a Windows-authored fragment ---\r\ntype: Added\r\npr: 1\r\n---\r\nFeature.\r\n\r\n<!-- docs-exempt: x -->\r\n would parse to body `Feature.\r\n\r\n\r` because: - The previous trailing-newline slice trimmed only `\n`, leaving `\r`. - DOCS_EXEMPT_RE was anchored with `$` only — in multiline mode `$` matches before `\n` but does not consume `\r`, so the marker line's trailing `\r` was left behind after replace. - The cleanup regex stripped trailing `\n` but not `\r`. Net effect: serializeChangelog emitted - Feature.\r \r \r (#1) — the `(#1)` PR suffix landed on a blank line instead of attached to the visible bullet. Same bug surfaces in github-release-notes formatBullet. Fix: - DOCS_EXEMPT_RE: add `\r?` before `$` so the regex consumes the CR of a CRLF terminator. Switch reason character class from `[^\n>]` to `[^\r\n>]` so CRLF-authored reasons don't carry a trailing `\r`. - extractDocsExempt cleanup: `[ \t\r]+$/gm` strips trailing `\r` on each line; `(?:\r?\n){3,}` collapses CRLF triple-blank-lines; `[\r\n]+$` strips every trailing line terminator (LF or CR). - parseFragment trailing-newline slice: CRLF-aware — strips `\r\n` (2 chars) before falling through to single `\n`. Tests: two CRLF regression cases in tests/changeset-parse.test.cjs — Codex's exact repro (end-to-end through serializeChangelog) plus the no-marker CRLF passthrough case. Full suite: 9698/9698 pass. * CRLF regression test asserts on parseChangelog IR not rendered text (Codex review pass 3, #3213) Third `codex review` pass caught that the CRLF regression test added in the previous commit asserted on serializeChangelog's rendered Markdown via `out.split('\n')` + `assert.match`. That violates CONTRIBUTING.md's "Prohibited: Raw Text Matching on Test Outputs" rule and the documented serializer contract in `serialize.cjs`: > tests assert via round-trip (parse(serialize(ir))) > rather than by inspecting serialized text Replace the regex check with the established `parseChangelog(out)` round-trip and assert on the structured `{ body: 'Feature.', pr: 1 }` bullet. This is also a stronger regression check than the substring match: Codex's own probe in the review session confirmed the pre-fix buggy body shape (`Feature.\r\n\r\n\r`) breaks parseChangelog's bullet regex entirely (returns `bullets: []`), so the round-trip catches the exact failure mode end-to-end. Full suite: 9698/9698 pass. * Address CodeRabbit findings: anchor link + require non-empty docs-exempt reason (#3213) CodeRabbit's review on the PR caught two actionable issues, both quick wins. Anchor link in PR templates pointed to a heading that does not exist. The CONTRIBUTING.md heading "Documentation Updates — Update the Relevant Docs" contains an em-dash, which GitHub strips entirely when generating anchor slugs (it does NOT collapse to a hyphen). The actual anchor is #documentation-updates-update-the-relevant-docs (single hyphen between every word), not #documentation-updates--update-the-relevant-docs (double hyphen where the em-dash was). Both feature.md and enhancement.md fixed. The docs-exempt marker matched a bare `<!-- docs-exempt -->` with no reason, which defeats the entire purpose of the escape hatch — the marker exists to leave an audit trail explaining WHY a PR is exempt. Without a reason it is a silent bypass. Fix: DOCS_EXEMPT_RE now requires both the colon AND a non-whitespace first reason character. Bare `<!-- docs-exempt -->`, empty `<!-- docs-exempt: -->`, and whitespace-only `<!-- docs-exempt: -->` are all rejected as if the marker were not present (`docsExempt: null`). The lint then falls through to its normal docs-required / no-docs-label checks. `isExemptFragment` in the lint module tightened too — defense-in-depth: even if a caller constructs a fragment with `docsExempt: ''` directly, it does not count as exempt. The predicate now requires `typeof === 'string'` and non-empty after trim. Tests: - changeset-parse.test.cjs: three new explicit-rejection cases (bare marker, empty reason, whitespace-only reason). Existing DOCS_EXEMPT_RE shape test extended with negative assertions for the same three forms. - lint-docs-required.test.cjs: prior "empty reason still exempt" test inverted — empty/whitespace docsExempt now produces FAIL_DOCS_MISSING. isExemptFragment helper test extended with the same negative cases. - CONTRIBUTING.md: clarified that the reason is required and non-empty. Skipped CodeRabbit's third finding ("use `npm run lint:docs` in CI workflow instead of `node scripts/lint-docs-required.cjs`") — the existing changeset-required.yml uses the direct-node form for the equivalent changeset lint, so the new docs-required.yml is convention-consistent. Switching one without the other would create drift, and switching both is out of scope for #3213. Bootstrap fragment continues to extract cleanly under the stricter regex (verified — `docsExempt` field still contains the full bootstrap reason). Full suite: 9701/9701 pass. |
||
|
|
90eb9e0f8b |
Merge pull request #3607 from gsd-build/feat/3592-test-rewrite-text-existence-checks-into-
test: rewrite alias coverage as behavioral contract |
||
|
|
0efe4f0612 |
fix(3621): release-sdk hotfix cherry-picks test-fixture commits (#3623)
* fix(3621): cherry-pick test-fixture commits in hotfix runs
The release-sdk hotfix loop excluded test-fixture updates that align CI
with a cherry-picked production fix, leaving the hotfix branch with new
production behavior and stale test assertions. Broke v1.42.3 CI (run
25949422676) when fix(3562) was cherry-picked but its bundled test
correction in docs(3562) commit
|
||
|
|
5a8610e0c2 | test: forbid cwd in PR checkers | ||
|
|
d0f916728b |
feat(skill-surface): install-time profiles + runtime /gsd:surface (#3408) (#3456)
* feat(skill-deps): add requires: frontmatter to all 51 skills with cross-skill references Mechanical migration from docs/research/data/2026-05-12-skill-audit.json. Every skill whose body references another GSD skill now declares those dependencies in `requires:` YAML frontmatter (flow-style array). Notable: discuss-phase, plan-phase, and execute-phase all reference `phase`, which confirms the latent gap in MINIMAL_SKILL_ALLOWLIST — `phase` is pulled by the core loop but was never in the allowlist. The profile closure model (ADR-0010 Phase 1) resolves this automatically. Closes part of #3408. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(skill-surface-budget): add PROFILES map, resolveProfile, loadSkillsManifest, staging, marker IO Implements the Skill Surface Budget Module core (ADR-0010, Phase 1): - PROFILES Object.freeze map: core (6 skills), standard (~13), full ('*') - loadSkillsManifest: parses requires: frontmatter from commands/gsd/*.md into a Map<stem, string[]> without external YAML dep - resolveProfile({modes, manifest}): computes transitive closure over the requires: graph; composable (modes=['core','audit'] unions closures) - stageSkillsForProfile / stageAgentsForProfile: filesystem staging with same exit-cleanup machinery as the legacy stageSkillsForMode - readActiveProfile / writeActiveProfile: .gsd-profile marker round-trip - Back-compat shims preserved: MINIMAL_SKILL_ALLOWLIST, isMinimalMode, shouldInstallSkill (overloaded), stageSkillsForMode — all legacy tests pass The phase latent bug is now resolved by closure: discuss-phase, plan-phase, and execute-phase all require phase, so any profile including any of them automatically includes phase via transitive closure. Tests: 22 manifest+resolve, 9 stage, 10 marker (41 new tests, all green). Back-compat anchor: 80/80 passing. Closes part of #3408. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(skill-surface-budget): add lint-skill-deps.cjs CI gate and fix 19 missed requires: entries Two lint checks (scripts/lint-skill-deps.cjs): a) Frontmatter-body consistency: skill body references must appear in requires: b) Profile closure: every requires: dep of any profile skill must be in closure Running the lint revealed 19 body references missed by the audit JSON (the audit used static analysis; some bodies have conditional references). Fixed: complete-milestone: +audit-milestone, discuss-phase, plan-phase, execute-phase, new-milestone fast: +quick health: +thread map-codebase: +new-project, plan-phase new-milestone, new-project, review, ultraplan-phase: +plan-phase ship: +verify-work sketch, spike: +new-project verify-work: +execute-phase workstreams: +new-milestone, resume-work Wired into package.json as lint:skill-deps and added to pretest. 8 fixture-based tests: all green. Closes part of #3408. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(skill-surface-budget): wire --profile= arg, profile marker write/read in bin/install.js - Add --profile=<name> / --profile=<n1>,<n2> arg parsing (composable). Mutually exclusive with --minimal / --core-only (aliases for --profile=core). Default (no flag): full. - Import readActiveProfile / writeActiveProfile from install-profiles.cjs. - After writeManifest: persist active profile to .gsd-profile marker. - gsd update path: if no --profile flag given, read existing .gsd-profile marker so non-full profiles are not silently re-expanded to full (ADR-0010). - Update --help block to document --profile= with per-tier token costs. New test: install-minimal-backcompat.test.cjs (6 tests): - PROFILES.core === MINIMAL_SKILL_ALLOWLIST (contract) - --minimal writes .gsd-profile marker "core" - --profile=core, --profile=standard write correct markers - default install writes marker "full" Closes part of #3408. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(changeset): add feat-3408-skill-profiles changelog fragment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(install-profiles): derive agents from skill body refs and wire into resolveProfile Deviation 1 of ADR-0010 phase 1b: tiered profiles (core, standard) now produce a non-empty agents Set instead of always returning empty. resolveProfile() scans each skill body for gsd-* agent name references (via new parseCallsAgents()), stores them in _calls_agents_<stem> manifest entries, and unions them across the resolved skill closure. stageAgentsForProfile() already checked resolvedProfile.agents — it now gets real data so tiered profiles install the correct subset of agents instead of zero. Closes #3408 (partial — Deviation 1 only) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(install): honor .gsd-profile marker on update, add resolveEffectiveProfile/mostRestrictiveProfile Deviation 2 of ADR-0010 phase 1b: the marker written during installation is now actually honored when re-running without explicit flags (e.g. gsd update). The dead-end logging block is replaced by resolveEffectiveProfile(), which picks the marker profile over 'full' when no explicit --profile= flag was given. The resolved profile is piped through to all 13 stageSkillsForMode dispatch sites (now _stageSkills) so updates install only the previously-chosen skill subset. --minimal retains its back-compat behavior (strict 6-skill allowlist, no closure) while writing 'core' to the marker. mostRestrictiveProfile() is exported for callers that need to reconcile disagreeing markers across runtimes (smallest skill set wins). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(surface): add CLUSTERS data + state IO module Add clusters.cjs with 10 named skill groups covering all 66 skills (verified by surface-clusters.test.cjs). Add surface.cjs with readSurface/ writeSurface atomic IO, resolveSurface, applySurface, and listSurface. Tests: 17 passing (11 state IO + 6 cluster integrity). Closes #3408 * docs(adr): add ADR-0011 Skill Surface Budget Module (Phase 1 accepted, Phase 2 amendment) Records the install-time profile staging decision (Phase 1, landed) and the runtime /gsd:surface cluster-toggle decision (Phase 2, in flight) as an amendment. Updates the ADR README index. Closes #3408 * docs(install-profiles): update module docblock for Phase 2 and ADR-0011 Corrects the ADR reference from 0010 to 0011, documents the three-profile model and back-compat aliases, adds resolveEffectiveProfile precedence rule, and notes the companion surface.cjs Phase 2 engine. * docs(context): add Skill Surface Budget Module canonical entry Adds the Domain terms entry for the Skill Surface Budget Module covering both Phase 1 (install-time profiles, .gsd-profile marker) and Phase 2 (runtime /gsd:surface cluster toggles, clusters.cjs, .gsd-surface.json), per ADR-0011 Consequences requirement. * feat(surface): add resolveSurface and applySurface engine + tests Tests cover: profile → surface equivalence, cluster disable/enable, explicitAdds transitive closure, applySurface file sync (add missing, remove superseded, preserve non-gsd files), listSurface token cost. 16 new tests passing. * docs(readme): document --profile= flag and /gsd:surface command Brief user-facing mention of install profiles (core/standard/full) and the /gsd:surface slash command in the Commands table. Points to ADR-0011 for details. * feat(surface): add /gsd:surface slash command runbook New skill: gsd:surface — runtime profile/cluster toggle without reinstall. Sub-commands: list, status, profile <name>, disable/enable <cluster>, reset. Persists state to .gsd-surface.json (independent of .gsd-profile). Description 96 chars (≤100 limit). lint:descriptions + lint:skill-deps: 0 violations. * feat(surface): add changeset fragment for /gsd:surface runtime toggle * feat(surface): add surface skill stem to utility cluster surface.md is a new skill; add it to the utility cluster so the surface-clusters.test.cjs coverage invariant stays satisfied. * docs(adr): fix ADR references to 0011 and record Phase 2 as shipped ADR-0010 number was already claimed by the file-operation-engine ADR; this ADR landed as 0011-skill-surface-budget-module.md. Update inline ADR references in clusters.cjs, surface.cjs, install-profiles.cjs, and the Phase 2 changeset to ADR-0011. Update the ADR Status section to record Phase 2 artifacts as shipped on this branch rather than "in progress". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(research): port skill-surface-budget memo and audit data ADR-0011 references docs/research/2026-05-12-skill-surface-budget.md and docs/research/data/2026-05-12-skill-audit.json, which only existed in the research worktree. Port both onto this branch so the ADR's References section resolves and reviewers can read the cluster taxonomy (§3.2), dependency topology (§3.1), and option grading (§4) that justify Phase 1 and Phase 2 decisions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(registration): register surface/clusters in INVENTORY, COMMANDS, and help.md - surface.md: convert allowed-tools from inline YAML array to block style (was parsed as a single tool name "[Read, Write, Bash]" by test harness) - docs/INVENTORY.md: add CLI module rows for clusters.cjs and surface.cjs; add Commands row for /gsd-surface; bump CLI Modules count 55→57, Commands 66→67 - docs/INVENTORY-MANIFEST.json: add entries for clusters.cjs, surface.cjs, and /gsd-surface (filename-based command key) - docs/COMMANDS.md: add ### `/gsd-surface` heading in Configuration Commands - get-shit-done/workflows/help.md: add /gsd:surface entry in Configuration section Fixes registration failures introduced by Phase 2 of #3408. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(surface,docs): scrub .claude leakage and escape hypothetical slash tokens Two PR regressions introduced earlier on this branch: 1. surface.cjs JSDoc comments contained the canonical paths (~/.claude/commands/gsd, ~/.claude/agents) as example values, which the cline-install leak regex (~\/\.claude\/(?:get-shit-done|commands|agents |hooks)) flagged as install-time path leaks. Reworded the docblocks to describe runtime-resolved paths without literal ~/.claude tokens. 2. The ported research memo proposed hypothetical Option C dispatchers using slash syntax (/gsd:milestone, /gsd:research). The docs-parity-live-registry test enforces that every slash-command token in docs/ resolves to a real command. Rewrote the Option C sketch without the slash prefix and added a clarifying note that the dispatchers are illustrative, not shipped. Targeted tests now pass: tests/cline-install.test.cjs and tests/docs-parity-live-registry.test.cjs both green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: remove raw output/source grep in lint tests * fix: close coderabbit profile and requires issues * test: align surface token-cost assertion wording * fix(install): align core profile alias and defer profile marker write --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
a60e05c714 |
fix(claude): restore namespaced /gsd:<command> references (#3452)
* fix(claude): restore namespaced /gsd:<command> references * test(claude): align slash-command expectations to /gsd: form * test(claude): align generated command references to /gsd: * test(claude): finish /gsd: namespace expectation updates |
||
|
|
0bcc9146a4 |
feat(shell-projection): add shim wrapper drift guard (#3448)
* feat(shell-projection): add shim wrapper drift guard * chore(changeset): add PR 3448 changelog fragment |
||
|
|
39e0113bcb | fix(pr-template-policy): avoid false positive on valid enhancement template banner (#3423) | ||
|
|
dd49abd32f |
Add PR template format gate (#3412)
* Add PR template format gate * Address CodeRabbit PR template gate feedback |
||
|
|
a51fc86a18 |
feat: generate release notes from changeset slugs (#3383)
* feat: generate release notes from changeset slugs * fix: harden release note generator inputs * fix: address release note review nits |
||
|
|
c4d3fe62a5 |
fix(install): require persistent SDK reachability before reporting ready (#3231) (#3249)
* test: reproduce false GSD SDK ready signals on Linux (#3231) * fix(install): require persistent SDK reachability before reporting ready (#3231) * changeset: pr=3249 for #3231 * fix(install): filter _npx from login-shell PATH probe (CR finding 1) Apply filterNpxFromPath() to the getUserShellPath() result before passing it to isGsdSdkOnPath(), mirroring the same filtering already applied to process.env.PATH. Without this, a transient _npx entry in the login-shell PATH can falsely satisfy the cross-shell reachability check and reintroduce the false-ready condition this PR fixes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(test): unconditional legacy-shim replacement assertion (CR finding 2) Replace readFileSync+includes source-grep check with isLegacyGsdSdkShim() and add an else branch asserting that when sdkReady is false, a warning/error was emitted. Previously the sdkReady===false path had no assertion at all, allowing the test to pass without verifying any postcondition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: replace text-grep assertions with structured ones (CR finding 2 + nitpick) Finding 2: restructure the legacy-shim replacement assertion to branch on isLegacyGsdSdkShim() state (a behavioral fact) rather than console output, and add an unconditional postcondition for both branches. Nitpick 3 (4 locations): - lines 149-153: replace /GSD SDK ready/.test(combined) with isGsdSdkOnPath(filterNpxFromPath(PATH)) === false - lines 167-169, 185-189: split filterNpxFromPath result into segments array and use array.includes() instead of string.includes() on the raw PATH string - lines 375-377: replace /GSD SDK ready/.test(combined) with fs.existsSync(shimPath) + isGsdSdkOnPath(filterNpxFromPath(localBin)) All 8 tests pass. lint-no-source-grep: 0 violations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(build-hooks): per-PID staging dir eliminates concurrent-cleanup TOCTOU race When multiple test before() hooks spawned build-hooks.js concurrently (--test-concurrency=4), a race existed: Process A would finish all copies, call rmdirSync('.dist-staging/') in cleanup, then Process B — still in its copy loop — would call copyFileSync(src, '.dist-staging/hook.pid.ts') and get ENOENT because the staging directory was gone. On macOS/Linux, copyFileSync reports the SOURCE path in ENOENT errors when the destination directory is missing, making the failure appear to be a missing source file (hooks/gsd-statusline.js) rather than a missing destination directory. This misled the diagnosis. Fix: make STAGE_DIR per-PID ('.dist-staging-<pid>/') so each builder owns its own staging directory. No other process touches it, eliminating all contention on staging-dir creation and cleanup. Update .gitignore to match the new 'hooks/.dist-staging-*/' glob. Reproduces as: CI test matrix (macos-24, ubuntu-22, ubuntu-24) all failing with ENOENT on hooks/gsd-statusline.js in bug-2136 before() hook. The new test file added in this PR (bug-3231) shifts the concurrency schedule just enough to expose the race on every CI run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: assert on captured console output, not tautological PATH state (CR finding) The two discarded `captureConsole()` return values in the bug-3231 test were flagged by CodeRabbit as tautological assertions. Fix: - Test 1 (transient _npx PATH): capture stdout/stderr and assert the installer does NOT emit "GSD SDK ready" (the false-positive the PR fixes), and that it does emit some diagnostic output instead. - Test 3 (clean install): capture stdout/stderr and assert the installer DOES emit "GSD SDK ready" after successfully self-linking into a persistent PATH dir — confirming the positive path works correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
f4c4ec6211 |
docs(build-hooks): correct staging-dir cleanup comment
The previous comment claimed "rmdir-on-non-empty is a no-op" — that is factually wrong. fs.rmdirSync throws ENOTEMPTY on non-empty directories. The actual race-safety mechanism is: 1. fs.readdirSync(STAGE_DIR) -> leftovers 2. fs.rmdirSync(STAGE_DIR) only when leftovers.length === 0 3. Outer try/catch swallows TOCTOU ENOTEMPTY (peer added a file between readdir and rmdir) and ENOENT (peer already cleaned up). Comment now references the leftovers variable and both fs calls so a future reader can map narrative to code without reverse-engineering it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c47c2c5def |
fix(build-hooks): handle Windows EPERM/EBUSY on rename, fall back to copy
POSIX rename(2) atomically replaces dest even when readers hold open handles. Windows MoveFileEx (which fs.renameSync uses with MOVEFILE_REPLACE_EXISTING) cannot — it throws EPERM/EBUSY when another process has the destination open. Concurrent install.js readers and antivirus scanners are realistic triggers; both release within ms. renameAtomicWithRetry() preserves the bare renameSync call on POSIX (no overhead) and on Windows retries up to 4 times with 10/30/90/270ms backoff, then falls back to copyFileSync + unlinkSync. If even copy fails because dest is hard-locked, log a non-fatal warning and leave the prior dest in place — a subsequent build retries from a fresh state. The build no longer crashes on Windows transient locking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c4f11db5e9 |
fix(build-hooks): atomic rename to prevent race with concurrent install reads
scripts/build-hooks.js used fs.copyFileSync (truncate-then-write, non-atomic). Under --test-concurrency=4, multiple builder invocations raced; a parallel install.js subprocess could readFileSync between truncate and write and observe an empty file, then write that emptiness into the install target. Surfaced as the release-blocking bug-2136-sh-hook-version part 4 failure on main even though the same SHA passed every install-smoke matrix entry. Fix: stage outputs to hooks/.dist-staging/ then fs.renameSync into hooks/dist/. POSIX rename(2) is atomic, so concurrent readers always observe a complete file. The existing bug-2136 part 4 test locks the post-fix invariant. Failing run: https://github.com/gsd-build/get-shit-done/actions/runs/25472202941/job/74738276687 Closes #3214 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d44fcee013 |
Merge pull request #3110 from patrickclery/fix/3100-search-dirs-colon-leaks
fix: replace stale /gsd: references in agents/, sdk/src/, and .clinerules |
||
|
|
96d2556209 |
fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> |
||
|
|
a411e08e88 |
fix(coderabbit): resolve all 12 findings on PR #3152
MAJOR (security/correctness): - commands/gsd/debug.md: add Write to allowed-tools (session file creation requires it — workflow explicitly says 'use Write tool, never heredoc') - workflows/debug.md: add SLUG sanitization guard to steps 1b+1c (status/ continue subcommands used raw user input in file paths — path traversal) - workflows/thread.md: sanitize $ARGUMENTS in RESUME mode before file path construction (was bypassing the sanitization guard in CLOSE/STATUS modes) MINOR (consistency/correctness): - docs/INVENTORY-MANIFEST.json: remove stale top-level 'workflows' array (duplicate of families.workflows introduced in earlier update) - commands/gsd/resume-work.md: normalize process to 'Execute end-to-end.' - commands/gsd/settings.md: normalize process to 'Execute end-to-end.' - commands/gsd/update.md: normalize otherwise branch to 'execute end-to-end.' - docs/adr/0002: add Status: Accepted + Date header (ADR convention) - workflows/extract-learnings.md: rename step extract_learnings → extract-learnings - tests/extract-learnings.test.cjs: tighten step-name assertion to exact name ARCHITECTURE: - scripts/command-contract-helpers.cjs: extract CANONICAL_TOOLS, parseFrontmatter, executionContextRefs as shared module — single source of truth consumed by both lint script and test suite (prevents silent lint/test disagreement) - scripts/lint-command-contract.cjs: require() helpers instead of duplicating - tests/command-contract.test.cjs: require() helpers; move readFileSync calls inside test() callbacks (registration-time throws surface as named failures) |
||
|
|
81f9534b5a |
feat(adr-0002): command contract validation module + prose @-ref cleanup + workflow extraction
ADR-0002: commands/gsd/*.md contract now enforced at two layers: LINT (scripts/lint-command-contract.cjs — new CI step): - name: present, starts with gsd: or gsd- - description: non-empty - allowed-tools: non-empty, all entries canonical - execution_context @-refs: resolve on disk, no trailing prose on same line - handles both @~/ and $HOME/ path prefixes TEST (tests/command-contract.test.cjs — 361 assertions): - Behavioral contract for all 65 command files - Replaces scattered coverage in enh-2790 + bug-3135 - Per-command per-rule test — one failure names the exact file + rule CI (.github/workflows/test.yml): - 'Lint — command contract (ADR-0002)' step added to lint-tests job PROSE @-REF CLEANUP (39 command files, ~900 tokens/invocation recovered): - Removed redundant @~/.claude/get-shit-done/... paths from <process> prose - execution_context block is now the single authoritative load declaration - Routing commands (sketch, spike, update, pause-work, etc.) keep routing instructions; only the inert path token is stripped WORKFLOW EXTRACTION (debug.md + thread.md, ~15,000 chars / ~3,750 tokens): - get-shit-done/workflows/debug.md: full process extracted from commands/gsd/debug.md - get-shit-done/workflows/thread.md: full process extracted from commands/gsd/thread.md - Command files reduced to frontmatter + objective + execution_context + context - debug.md: 9,603 → 1,703 chars; thread.md: 7,868 → 585 chars RENAME: - get-shit-done/workflows/extract_learnings.md → extract-learnings.md (aligns with hyphen convention of all other workflow files) DOCS: - docs/INVENTORY.md: count 85→87, new rows, rename row, fix add-todo --backlog attribution - docs/INVENTORY-MANIFEST.json: +debug.md +thread.md +extract-learnings.md -extract_learnings.md Closes ADR-0002 implementation. |
||
|
|
f9c1f01971 |
fix: extend fix-slash-commands SEARCH_DIRS to agents/, sdk/src/, .clinerules
scripts/fix-slash-commands.cjs SEARCH_DIRS did not cover agents/, sdk/src/, or top-level files, so 9 colon-form references survived in 6 files. The hit at agents/gsd-codebase-mapper.md:105 propagated into ~/.claude/agents/ at install time (the fixer is not wired into install) and produced unrunnable /gsd:<cmd> suggestions in agent output on non-Gemini runtimes. This commit includes Pass 1 (the 9 line edits) AND Pass 2 (extending the fixer's SEARCH_DIRS so future regressions are auto-rewritten and caught by the bug-2543 guard, which mirrors that list). The standalone bug-3100 test added in the prior revision is removed in favor of the bug-2543 guard's extended scan, per CONTRIBUTING.md test standards (no source-grep tests on non-.md files). Refs #3100 |
||
|
|
95d2bc20f8 |
feat(hooks): opt-in SessionStart update banner for non-statusline users (#2795) (#3035)
* feat(hooks): opt-in SessionStart update banner for non-statusline users (#2795) When a user declines (or keeps a non-GSD) statusline at install time, the installer now offers an opt-in SessionStart banner that surfaces GSD update availability. The banner reads the existing ~/.cache/gsd/gsd-update-check.json cache (written by gsd-check-update-worker.js) and emits a single systemMessage line only when update_available is true: GSD update available: <installed> → <latest>. Run /gsd-update. It is silent when up-to-date and rate-limits "check failed" diagnostics to once per 24h via a sentinel file so a corrupt cache doesn't nag every session. Removed cleanly by `npx get-shit-done-cc --uninstall` which strips both the script and the SessionStart entry. The banner is never offered when GSD's statusline is being installed (statusline already surfaces update info, so re-prompting would be noise). Implementation: - hooks/gsd-update-banner.js — pure functions buildBannerOutput, shouldSuppressFailureWarning, readCache; thin main() wires them. - bin/install.js — handleUpdateBanner() prompt, parseUpdateBannerInput(), buildUpdateBannerHookEntry(), buildUpdateBannerPromptText(); chained into installAllRuntimes() so finalize() receives both flags. updateBannerCommand computed alongside the other JS-hook commands; finishInstall() registers the SessionStart entry only when shouldInstallBanner === true and the hook file is present at the target. - Hook ships in scripts/build-hooks.js HOOKS_TO_COPY, listed in MANAGED_HOOKS for stale-detection in gsd-check-update-worker.js, in the uninstall hook-removal lists in install.js, and in the rewriteLegacyManagedNodeHookCommands allowlist. Tests: - tests/feat-2795-update-banner.test.cjs — 22 tests, structural-IR assertions on parsed JSON envelopes (no raw-text matching). Covers pure-function branches (cache present/absent, parseError, rate-limit suppression, missing version fields), end-to-end hook invocation against fixture cache states, and install.js wiring (prompt text, input parsing, hook entry shape). - tests/trae-install.test.cjs — updated install() return-shape assertion to include updateBannerCommand: null for the no-settings runtime. - 6881/6881 tests pass. Docs (bundled in same commit per the bundle-docs-with-code skill): - docs/USER-GUIDE.md — new "Surface GSD Update Notifications Without GSD's Statusline" task section with opt-in/opt-out instructions. - docs/FEATURES.md — REQ-HOOK-08 added; "Update Banner" subsection under the Hook System feature with cache flow + removal path. - docs/INVENTORY.md — hook count 11 → 12, new row for gsd-update-banner.js. - docs/INVENTORY-MANIFEST.json — regenerated. Closes #2795 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(install): gate banner prompt on actual installability (CR #3035) CodeRabbit findings on PR #3035: - bin/install.js (Major): continueAfterStatusline gated banner prompt on the raw `shouldInstallStatusline` flag from handleStatusline. But finishInstall later silently skips the statusline write on local installs unless --force-statusline is set (#2248). Two consequences: 1. Interactive local Claude/Gemini installs got neither a statusline nor a banner offer. 2. Codex/Cursor/Copilot/Windsurf/Trae/Cline-only installs (where every result.updateBannerCommand is null) still got prompted even though the choice was silently ignored. Fix: derive willInstallStatusline = shouldInstallStatusline && (isGlobal || forceStatusline), and gate the banner prompt on a canInstallBanner precondition computed from results[].updateBannerCommand. Pass the raw shouldInstallStatusline through to finalize unchanged so per-runtime statusline gating in finishInstall is unaffected. - tests/feat-2795-update-banner.test.cjs (Minor): rate-limit suppression test parsed r1.stdout without first asserting r1.status === 0. Other e2e tests in this file (lines 210, 241) do this. A non-zero exit would surface as a cryptic SyntaxError instead of a status assertion failure. Fix applied verbatim. 6881/6881 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4277f7d7e8 |
fix(#2994): move verify-reapply-patches.cjs to get-shit-done/bin/ so it ships to user installs (#3000)
* fix(#2994): move verify-reapply-patches.cjs to get-shit-done/bin/ so installer ships it scripts/verify-reapply-patches.cjs (added in #2972 to close the verified-yes-without-checking gap from #2969) shipped in the npm tarball but never reached user installs: bin/install.js copies get-shit-done/ recursively but does not copy the top-level scripts/ directory. Effect: every fresh install hit `Cannot find module …/scripts/verify-reapply-patches.cjs` on Step 5 of /gsd-reapply-patches. The whole point of moving verification out of LLM-driven prose into a deterministic script is undone if the script does not resolve at runtime. Fix: move the script to get-shit-done/bin/verify-reapply-patches.cjs (same pattern as gsd-tools.cjs and other runtime bin scripts that the installer ships) and update reapply-patches.md Step 5 to invoke ${GSD_HOME}/get-shit-done/bin/verify-reapply-patches.cjs. Tests: - bug-2969 SCRIPT path updated to the new location - New bug-2994-verify-reapply-patches-installed-path.test.cjs parses reapply-patches.md into structured invocation records and asserts every node ${GSD_HOME}/... reference lives under get-shit-done/ (the installed tree). Catches future regressions where someone moves a runtime-needed script back to scripts/. Closes #2994 * chore(#2994): add changeset fragment for PR #3000 * chore(#2994): add changeset fragment for PR #3000 * docs(#2994): update verifier-script-location comment to reflect new path (CR) CodeRabbit on PR #3000: the parenthetical at line 278 still said the script ships under scripts/, but this PR moved it to get-shit-done/bin/. Updated the prose to reference the new location and the installer target path. * chore(#3000): drop direct CHANGELOG.md edit; release entry now lives in .changeset/ The changeset-fragment workflow (#2975) renders fragments into CHANGELOG.md at release time. Direct edits to [Unreleased] on each PR caused merge conflicts on every concurrent PR. This commit restores CHANGELOG.md to match origin/main; the release entry for this fix is preserved in the .changeset/*.md fragment(s) on this branch, which the release workflow consolidates. |
||
|
|
c2ada7e799 |
feat(#2995): post-install path audit for workflow-invoked scripts (#2996)
* feat(#2995): post-install path audit for workflow-invoked scripts Catches the gap class surfaced by #2994: a workflow references a script via ${GSD_HOME}/<path> that ships in the npm tarball but is not copied to the user's config dir at install time. Unit tests don't catch it because they resolve the script via path.join(__dirname, '..', 'scripts', …) — the source layout, not the deployed layout. Implementation built TDD per #2995, vertical slices with structured-IR assertions: scripts/audit-workflow-script-paths.cjs - Pure auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes }) returns { ok, findings: [{ workflow, path, kind }] } via the AUDIT_FINDING enum. - Two finding kinds: MISSING_FROM_REPO (typo / file deleted) and NOT_INSTALLED (#2994 class — first segment outside installed prefixes). - Tolerates ${GSD_HOME:-...} default-fallback syntax. tests/bug-2995-post-install-script-paths.test.cjs - 9 tests across 3 suites: • Pure-function pass and per-finding-kind detection (5 tests on synthetic fixtures). • Real workflow audit (2 tests asserting the actual repo's get-shit-done/workflows/ has no NEW gaps and KNOWN_GAPS stays consistent with audit findings). • Enum shape lock + extractReferences edge cases. - All assertions on typed AUDIT_FINDING enum / structured records; zero raw text matching. - KNOWN_GAPS is a Set keyed on `workflow|path|kind` strings; currently contains the #2994 entry. The companion test fails if a KNOWN_GAPS entry no longer matches a real finding (forces the allow-list to shrink as gaps fix). The audit immediately catches #2994's gap on `reapply-patches.md`. The allow-list contains exactly that entry; new gaps fail CI; #2994's fix will remove the entry as part of the same PR. Closes #2995 Refs #2994 * chore(#2995): add changeset fragment for PR #2996 * chore(#2995): add changeset fragment for PR #2996 * fix(#2995): emit both NOT_INSTALLED + MISSING_FROM_REPO; clean up fixture leak (CR) CodeRabbit on PR #2996 found two issues: 1. (Low value) auditWorkflowScriptPaths short-circuited on NOT_INSTALLED, masking MISSING_FROM_REPO for the same ref. Removed the `continue` so both findings emit in one run; added a regression test. 2. (Low value) bug-2995 test created tmpRoot in before() but never wrote into it; per-fixture mkdtempSync dirs leaked. Rooted fixture repos under tmpRoot so the after() cleanup actually frees them. |
||
|
|
918f987a19 |
feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes() (#2985)
* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes() The base lint (scripts/lint-no-source-grep.cjs) only catches readFileSync(...).<text-method>() chained directly. The much more common var-binding form escapes it: const src = fs.readFileSync(p, 'utf8'); // 50 lines later if (src.includes('foo')) {} // ← still grep, lint missed it Scan of the test suite found ~141 files using this pattern. Implementation built TDD per #2982 with structured-IR assertions: scripts/lint-no-source-grep-extras.cjs - detectVarBindingViolations(src) — pure detector, two passes: pass 1 collects vars bound from readFileSync, pass 2 finds any <var>.<includes|startsWith|endsWith|match|search>( on those vars. - detectWrappedAssertOkMatch(src) — flags assert.ok(<expr>.match(...)) which escapes the assert.match rule. - VIOLATION enum exposes stable codes for tests to assert on. scripts/lint-no-source-grep.cjs - Wires the new detectors into the existing per-file check; one additional violation row per file with the first 3 sample tokens. tests/bug-2982-lint-var-binding.test.cjs - 13 tests, all assertions on typed VIOLATION enum / structured records. Covers all 5 text-match methods, multi-var, no-bind, string literal (must NOT trigger), wrapped assert.ok(.match), and assert.match (must NOT double-flag). Migration backlog (#2974 expanded scope): - 42 files annotated `// allow-test-rule: source-text-is-the-product` (legitimate — they read .md/.json/.yml files whose deployed text IS the product) - 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]` (read .cjs/.js source — clear migration debt) - 95 files annotated `pending-migration-to-typed-ir [#2974]` with `Per-file review may reclassify as source-text-is-the-product during migration` (mixed — manual review under #2974) After this lands the lint reports 0 violations on main; new violations in PRs surface immediately. Closes #2982 Refs #2974 * test(#2982): fix truncated test name per CR The label ended with a bare '(' from a copy-paste mishap. Now reads 'does NOT flag .matchAll(...) — matchAll is not match, so assert.ok(.matchAll(...)) is not flagged'. * chore(#2982): add changeset fragment for PR #2985 * chore(#2982): add changeset fragment for PR #2985 |
||
|
|
9d5db87249 |
feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts (#2978)
* feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts Two PRs that both edit `### Fixed` in CHANGELOG.md always conflict on merge. Recently bit on #2960/#2972 in the same session — fix-the-conflict-and-rebase tax. Replace the shared-file model with per-PR fragment files that never share lines. Implementation built TDD per #2975, vertical slices with structured-IR assertions throughout: scripts/changeset/parse.cjs - fragment text → typed record + frozen FRAGMENT_ERROR enum (8 tests) scripts/changeset/render.cjs - fragments → structured IR with Keep-a-Changelog section ordering (2 tests) scripts/changeset/serialize.cjs - IR ↔ markdown round-trip pair (parse(serialize(ir)) === ir, 3 tests) scripts/changeset/cli.cjs - file-I/O wrapper with --json mode; reads .changeset/, folds into CHANGELOG.md, deletes consumed fragments. Idempotent. (1 test) scripts/changeset/lint.cjs - pure verdict (changedFiles, labels) → { ok, reason } via LINT_REASON enum. Honors `no-changelog` label. (5 tests) scripts/changeset/new.cjs - fragment scaffolder with random adjective-noun-noun filename. Tests assert via parseFragment round-trip. (3 tests) Total: 22 tests, all assertions on typed structured fields. No regex on text, no String#includes on file content. Lint clean across 356 test files. Supporting: .changeset/README.md - format spec + workflow docs .changeset/eager-hawks-rally.md - dogfood fragment for THIS PR (will be the first thing the new release tool consumes) .github/workflows/changeset-required.yml - CI: every PR runs lint.cjs package.json - npm run changeset, changelog:render, lint:changeset CONTRIBUTING.md - new "CHANGELOG Entries — Drop a Fragment" section between PR Guidelines and Testing Standards Closes #2975 * fix(#2975): address CodeRabbit findings on changeset workflow 7 valid findings (4 Major, 3 Minor); all addressed: scripts/changeset/parse.cjs - Preserve fragment body verbatim. Previously body.trim() ate intentional leading whitespace (code blocks, etc.); now trim() is used only for the emptiness check, and a single trailing newline is stripped (the editor-added one) so well-formed fragments round-trip byte-for-byte. Added a regression test asserting a code-block-leading body is preserved. scripts/changeset/cli.cjs - Validate flag values during argument parsing. parseArgs now returns { ok, opts | error }; rejects `--repo` etc. with no following value or with another flag as the value. main() surfaces the error message before exiting 2. - Handle post-write fragment-deletion failures. After CHANGELOG.md is written, any unlink failure is captured into a structured deleteFailures list with reason 'fail_fragment_delete'; cmdRender returns exitCode=1 with the partial-failure detail instead of leaving the changelog updated and fragments behind (which would cause double-consumption on rerun). scripts/changeset/lint.cjs - Treat CHANGELOG.md as a linted user-facing path. Direct edits to CHANGELOG.md (the bypass route around the new workflow) now fail the lint with FAIL_MISSING_FRAGMENT. Added a regression test for that case. - Use cp.execFileSync instead of cp.execSync for the git diff call. Eliminates the shell-interpolation surface on GITHUB_BASE_REF; git's own arg parser remains the validator. scripts/changeset/new.cjs - Atomic fragment creation. existsSync() + writeFileSync was racy under concurrent invocations. Now writeFileSync uses { flag: 'wx' } which fails EEXIST on collision; the random-name retry loop catches EEXIST and re-rolls. Throws explicitly after 16 attempts rather than silently overwriting. .changeset/README.md - Add language tag `md` to the format example fence (markdownlint MD040). All 25 changeset tests pass; lint clean (356 test files, 0 violations). * fix(#2975): sanitize --type and validate flag values in new.cjs (CR fixes) Two CR findings on scripts/changeset/new.cjs: 1. (Minor) `type` was embedded in frontmatter without sanitization. A newline in the value (e.g. `--type 'Fixed\ntype: Added'`) would corrupt the fragment. scaffoldFragment now validates `type` against the Keep-a-Changelog ALLOWED_TYPES set BEFORE writing — same set parse.cjs uses on consume. Throws with a typed error referencing the allowed values; tests cover the newline case + 4 other non-allowed values. 2. (Minor) `--repo` (and other value-taking flags) without a value silently set opts.repo to undefined, which produced a cryptic ERR_INVALID_ARG_TYPE deep inside path.join. parseArgs now mirrors the cli.cjs convention: returns { ok, opts | error }, validates that the next token exists and is not itself another flag, and surfaces a precise "missing value for --repo" message before exit. Added 3 tests: missing-trailing-value, flag-as-value, well-formed. 29 tests pass across the changeset suite (4 new regression tests). |
||
|
|
fb92d1e596 |
fix(#2983): classifier exit-code discipline, base-tag staging, drop vestigial merge-back (#2984)
* fix(#2983): classifier exit-code discipline, base-tag staging, drop vestigial merge-back Three issues surfaced by CodeRabbit's post-merge review of #2981 plus a production failure on the v1.39.1 release run. (1) Overloaded classifier exit code scripts/diff-touches-shipped-paths.cjs reused exit 1 for both the legitimate "no shipped paths" result and Node's default exit on uncaught throw, so any classifier failure (corrupt package.json, EPERM, etc.) was indistinguishable from a normal skip — the workflow's `if ! ... ; then skip` idiom would silently drop the commit. Distinct exit codes now: 0 shipped — at least one path is in the npm `files` whitelist 1 not shipped — CI / test / docs / planning only 2 classifier error — workflow MUST fail-fast uncaughtException + unhandledRejection + try/catch around fs/JSON parsing all route to exit 2 with stderr context. (2) Classifier missing at the base tag (CRITICAL) `Prepare hotfix branch` runs `git checkout -b "$BRANCH" "$BASE_TAG"` BEFORE the cherry-pick loop, replacing the working tree with the base tag's contents. Base tags predating #2980 (notably v1.39.0, the most likely next hotfix base) don't have scripts/diff-touches-shipped-paths.cjs at all — `node <missing>` exits non-zero — `if !` skips every commit — empty hotfix branch published. Strictly worse than the original #2980 push-rejection, which at least failed loudly. Stage the classifier from the dispatched ref's working tree into $RUNNER_TEMP at the top of the run script (before any working-tree- mutating git command). The cherry-pick loop now references $CLASSIFIER (staged) instead of the in-tree path. Sanity guards: refuse to start if scripts/diff-touches-shipped-paths.cjs is missing in the dispatched ref, refuse to proceed if cp didn't materialize $CLASSIFIER. The cherry-pick loop captures node's exit via ${PIPESTATUS[1]} and dispatches via explicit case: 0 proceed with cherry-pick 1 skip into NON_SHIPPED_SKIPPED * emit ::error:: + exit "$CLASSIFIER_RC" (3) Drop the merge-back PR step Auto-cherry-pick only picks commits already on main (`git cherry HEAD origin/main` outputs the unmerged ones; we filter fix:/chore: from main). By construction every code commit on the hotfix branch is already on main. The only hotfix-branch-only commit is `chore: bump version to X.Y.Z for hotfix`, which either no-ops against main or rewinds main's in-progress version. The merge-back PR was vestigial. It also failed in production on run 25232968975 with `GitHub Actions is not permitted to create or approve pull requests (createPullRequest)` — org policy blocks PR creation from the workflow's GH_TOKEN. Even without that block, the PR would have nothing useful to merge. Step removed. The `pull-requests: write` permission granted solely for the merge-back step has been dropped from the release job (least-privilege). Regression coverage tests/bug-2983-classifier-exit-codes-and-base-tag-staging.test.cjs adds 12 assertions across two describe blocks: - 5 classifier behavioral: exit 0/1 preserved, exit 2 on missing package.json, exit 2 on malformed JSON, exit-code constants exported. - 7 workflow contract: classifier staged before checkout, target is $RUNNER_TEMP, missing-source guard, missing-staged guard, PIPESTATUS-based dispatch, error branch fails workflow, loop uses staged path (not in-tree). tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs updated where it asserted the pre-#2983 `if ! ... ; then` shape: now accepts the post-#2983 case-dispatch form. The test still proves the classifier participates; bug-2983 enforces the specific shape. Run summary references for the curious reviewer: - Run 25232010071 — original #2980 trigger (workflow-file push rejection) - Run 25232968975 — failed merge-back step that prompted the "is this even useful?" question that drove the removal Closes #2983 * fix(#2983): address CodeRabbit findings on PR #2984 Two findings, both real, both fixed. (1) [Critical] PIPESTATUS capture clobbered by `|| true` Pre-fix shape: git diff-tree ... | node "$CLASSIFIER" || true CLASSIFIER_RC="${PIPESTATUS[1]}" When the classifier exits 1 ("not shipped" — common case) or 2 (error), `|| true` triggers the right-hand side. `true` is a one-command "pipeline" that overwrites PIPESTATUS to (0). ${PIPESTATUS[1]} on the next line is therefore unset (or stale under set -u). The case dispatch then matched the empty string — falling into `*)` and failing the workflow on every non-shipped commit, OR matching `0)` after some shells default-init unset to 0 and silently picking commits that don't ship. Local repro confirms the issue: $ bash -c 'set -euo pipefail; false | sh -c "exit 7" || true; \ echo "PIPESTATUS: ${PIPESTATUS[*]}"; \ echo "[1]: ${PIPESTATUS[1]:-<unset>}"' PIPESTATUS: 0 [1]: <unset> Fix: bracket the pipeline in `set +e`/`set -e`, snapshot PIPESTATUS into a local array on the very next line, then dispatch on the snapshot: set +e git diff-tree ... | node "$CLASSIFIER" PIPE_RC=("${PIPESTATUS[@]}") set -e DIFFTREE_RC="${PIPE_RC[0]}" CLASSIFIER_RC="${PIPE_RC[1]}" The snapshot must happen on the first line after the pipeline; any intervening simple command resets PIPESTATUS. The array form is invariant against that. Bonus from the new shape: $DIFFTREE_RC is now also captured. git diff-tree is unlikely to fail on a known-good $SHA, but if it does, we no longer feed partial/empty input to the classifier and call it "not shipped." A non-zero DIFFTREE_RC emits ::error::git diff-tree failed and exits. (2) [Minor] Stale "Merge-back PR opened against main" summary line The hotfix run summary still printed: echo "- Merge-back PR opened against main" But the merge-back step itself was removed in the previous commit on this branch. Operators reading the summary would expect a PR that doesn't exist. Replaced with explicit non-action text: echo "- No merge-back PR (auto-picked commits are already on main)" Test coverage bug-2983 test file gains 3 assertions: - PIPE_RC array-snapshot pattern is required (regex matches the exact `PIPE_RC=("${PIPESTATUS[@]}")` form). - The `pipeline || true; ${PIPESTATUS[1]}` antipattern is explicitly forbidden via assert.doesNotMatch. - DIFFTREE_RC is captured from PIPE_RC[0] and a non-zero value triggers ::error::git diff-tree failed. - Run summary forbids `Merge-back PR opened against main` and requires the new non-action sentence. bug-2964 test's loop-anchor window bumped 6 KB → 8 KB to accommodate the additional pre-pick scaffolding (the test's own comment had already anticipated this kind of growth, citing prior precedents from #2970 and #2980). Mark CodeRabbit comments resolved post-commit. Refs CR finding ids 3175253571, 3175253578 on PR #2984. |
||
|
|
7424271aa0 |
fix(#2980): hotfix cherry-pick only picks commits that change what ships (#2981)
* fix(#2980): pre-skip workflow-file cherry-picks in release-sdk hotfix loop The default GITHUB_TOKEN issued to the release-sdk run lacks the `workflow` scope, so the prepare job's `git push origin "$BRANCH"` is rejected by GitHub when any cherry-picked commit modifies a file under `.github/workflows/`: ! [remote rejected] hotfix/X.YY.Z -> hotfix/X.YY.Z (refusing to allow a GitHub App to create or update workflow ... without `workflows` permission) Pre-#2980 behavior: the auto_cherry_pick loop happily picked workflow-file commits, then the trailing push exploded with no clear signal which commit was the culprit. v1.39.1 hit this on PR #2977 (run 25232010071) — earlier release-sdk fixes (#2965, #2967, #2970) had been skipped on conflict so their workflow-file changes never reached the push step, masking the bug; #2977 was the first workflow-file commit to apply cleanly and the push immediately exploded. Fix: pre-pick guard in the cherry-pick loop. Inspect each candidate commit's file list via `git diff-tree --no-commit-id --name-only -r` BEFORE attempting the pick. If any path matches `^\.github/workflows/`, skip the commit, emit a `::warning::` annotation naming the dropped commit, and append to a new `WORKFLOW_SKIPPED` bucket. The run summary surfaces this bucket in its own section, distinct from `CONFLICT_SKIPPED` (real merge conflicts) and `POLICY_SKIPPED` (feat/refactor exclusions), so operators reviewing the run never confuse the remediation paths. The loud-warning piece is non-negotiable: silent drops were explicitly rejected as a failure mode during the option-1/2/3 tradeoff discussion. If a workflow-file fix genuinely needs to ship in a hotfix, the operator applies it manually on the hotfix branch using a token with `workflow` scope, or lands it on main and re-cuts the release. Regression covered by tests/bug-2980-skip-workflow-file-cherrypicks.test.cjs (5 assertions: pre-pick guard exists, uses `git diff-tree`, emits `::warning::`, lands in dedicated bucket, surfaces in summary). The bug-2964 test's 4 KB window after the cherry-pick-loop anchor was nudged to 6 KB to accommodate the new pre-pick scaffolding — the test's own comment had already anticipated this kind of growth (citing #2970's merge-commit pre-skip as prior precedent). Closes #2980 * refactor(#2980): replace workflow-file pre-skip with shipped-paths filter The previous commit on this branch caught only the .github/workflows/* subset of the bug, treating the symptom (push rejection on workflow-file changes) rather than the root cause (the fix:/chore: filter is too broad — it picks any commit with that conventional-commit type even when the diff cannot affect the published npm package). CI-only fixes (release-sdk.yml itself, hotfix tooling, test-only commits) shouldn't flow through hotfix runs at all — they cannot change what `npm install get-shit-done-cc@X.YY.Z` produces. The .github/workflows/* push rejection is just the loudest of these "shouldn't have been picked" cases; tests/, docs/, .planning/ commits get picked silently with the same lack of effect on consumers. Replace the workflow-file pre-skip with a shipped-paths filter: - New scripts/diff-touches-shipped-paths.cjs reads package.json `files`, plus package.json itself (always-shipped per `npm pack` semantics), and exits 0 iff any input path is in the shipped set. Lockfile is not shipped (npm pack excludes it unless explicitly in `files`). - Workflow loop now pipes `git diff-tree --no-commit-id --name-only -r` through the classifier; on exit 1 the commit is skipped and appended to a new NON_SHIPPED_SKIPPED bucket (replaces WORKFLOW_SKIPPED). - Run summary surfaces NON_SHIPPED_SKIPPED as informational — no ::warning:: annotation. A non-shipping commit cannot affect the package, so a yellow alert would imply remediation is possible and would mislead operators. The classifier in a separate .cjs file (rather than inline bash heredoc) is so its rules — directory-prefix vs exact-match, package.json-always-shipped, lockfile-not-shipped — are unit-testable in tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs (11 new assertions: 4 static workflow + 6 classifier behavioral + 1 mixed- diff edge case). Why this dissolves the original push-rejection bug: workflow files aren't in `files`, so workflow-only commits are skipped pre-pick. The push step never sees them. If a workflow-file fix genuinely needs to ship in a hotfix release (extremely rare — the hotfix workflow is read from main's ref, not the hotfix branch's), the operator applies it manually using a token with `workflow` scope. The pre-skip puts that requirement in the run summary explicitly. Closes #2980 |
||
|
|
ef43f5161f |
fix(#2969): deterministic Step 5 verification gate for /gsd-reapply-patches (#2972)
* fix(#2969): deterministic Step 5 verification gate for /gsd-reapply-patches The prior Step 5 "Hunk Verification Gate" was prescribed correctly in the workflow text — but executed laxly by the LLM, which filled in `verified: yes` without actually checking content presence. The reporter observed three distinct files (skills/gsd-discuss-phase/SKILL.md, skills/gsd-autonomous/ SKILL.md, get-shit-done/workflows/new-project.md) where archives contained substantive user-added blocks that did not survive into the merged result, yet the gate reported clean. Move verification from LLM-driven prose into a deterministic Node script the workflow calls. The script can't be shortcut. Changes: - scripts/verify-reapply-patches.cjs (new): pure Node, no external deps. For each file in the patches dir, computes user-added significant lines as the line-set diff between backup and pristine baseline (when available; falls back to "every significant backup line" when no pristine — over-broad but the safe direction for this bug class). Asserts each line appears literally in the merged installed file via String.prototype.includes. Filters trivial lines (length < 12 chars, pure punctuation, decorative comments) so harmless drift doesn't trigger false failures. Exits 0 on pass, 1 on any miss with per-file diagnostic, 2 on usage error. Supports --json for workflow consumption. - get-shit-done/workflows/reapply-patches.md: rewrite Step 5 to call the script and parse its JSON output. The Step 4 Hunk Verification Table remains as advisory Claude-readable summary, but the gate is now the script's exit code. - tests/bug-2969-verify-reapply-patches.test.cjs (new): 6 tests covering (a) pass when every line survives, (b) fail when a line is missing, (c) fail when the merged file is deleted entirely, (d) --json structured report shape, (e) backup-meta.json is correctly skipped as metadata, (f) no-pristine-dir fallback exercises the safe over-broad path. All pass. Out of scope: the manifest-baseline tightening described in #2969 Failure 1 (saveLocalPatches comparing against the wrong baseline so prior silent wipes poison subsequent updates). That's a separate, bigger architectural change involving pristine-content infrastructure; this PR addresses the gate fidelity half so users at least see the diagnostic when content goes missing. Closes #2969 (partial — Failure 2 only) * fix(#2969): preserve #1999 Hunk Verification Table assertions alongside new script gate CI failure on PR #2972 surfaced that tests/reapply-patches.test.cjs (the #1999 contract) asserts Step 5 references: - "Hunk Verification Table" - `verified: no` failure condition - explicit STOP/halt/abort directive - "table absent / missing" halt path My initial Step 5 rewrite for #2969 substituted the deterministic script for the table-based gate entirely, stripping those references. The script is the strictly stronger gate, but the existing #1999 test enforces the table-based safety net as a defense-in-depth contract. Restore both gates as a layered Step 5: - 5a (binding): deterministic verifier script — script gate, exits non-zero on any miss, cannot be shortcut by the LLM - 5b (advisory): Hunk Verification Table review — preserved as redundant safety net for the case where the script has a bug or the pristine baseline is unavailable Both gates must pass. Verified: tests/reapply-patches.test.cjs (5 tests in the #1999 suite) and tests/bug-2969-verify-reapply-patches.test.cjs (6 tests in the #2969 suite) all pass — 21/21 total in this fixture. * fix(#2969): address CodeRabbit findings on workflow + script Five CR findings on PR #2972, all valid; addressed in this commit: 1. (Major) Stderr was merged into VERIFY_OUTPUT via `2>&1`, so any Node warning, deprecation notice, or stack trace would corrupt the JSON parse downstream. Capture stdout only; stderr remains on the controlling terminal for operator visibility. 2. (Major) verifyFile() crashed with EISDIR/EACCES instead of producing a structured diagnostic when the installed path was a directory or unreadable. Wrap statSync/readFileSync in try/catch and emit a per-file fail row; the whole-run gate continues with structured output. Added test case asserting the directory-at-installed-path case fails with `not a regular file` diagnostic instead of crashing. 3. (Minor) PRISTINE_FLAG built as a single string + unquoted expansion would split paths with spaces. Switched to a bash array (VERIFY_ARGS) that preserves whitespace through expansion. 4. (Minor) Fenced code block missing language tag (markdownlint MD040). Added `text` tag to the error message block. 5. (Minor) Usage comment said pristine fallback was "backup-meta lookup" but the actual code path falls back to significant-line checks from backup content. Corrected the comment to match implementation. Verified all 21 tests in tests/reapply-patches.test.cjs (#1999 contract) + tests/bug-2969-verify-reapply-patches.test.cjs (now 7 tests with the new directory case) pass. * test(#2969): structured JSON assertions, no substring matching on script output Replace every assert.match(r.stdout, /pattern/) call with structured assertions on the parsed JSON report from the script's own --json mode. The script's --json contract IS the structured shape we test against — the test author should never depend on the human-readable formatter output, just as no test should depend on substring presence in source. Changes: - All 7 tests now run the verifier with --json (via a runVerifier() helper) and parse the resulting JSON document into { status, report, stderr }. Diagnostic stderr is preserved as a separate channel for debug output but is not used for assertions. - Each previously substring-matched diagnostic ("Failures: 1", "not a regular file", "installed file missing after merge", file path, dropped line) is now a deepEqual / equal / Array.includes against typed report fields: report.failures, report.results[i].status, report.results[i].reason, report.results[i].file, report.results[i].missing[]. - Added an explicit "documented shape" test asserting the JSON output has exactly the keys { file, missing, reason, status } per result — locks the public contract of the --json mode. - DRY'd up fixture reset into a resetFixture() helper since every test starts with a fresh patches/installed/pristine triple. Linter: scripts/lint-no-source-grep.cjs reports 0 violations across 348 test files. Combined run of bug-2969-...test.cjs (7 tests) + reapply-patches.test.cjs (5 tests in the #1999 suite) all pass — 22/22 in the relevant fixture. * fix(#2969): typed REASON enum + raw-text-matching rule shipped repo-wide This commit closes the loop on the no-source-grep discipline: 1. scripts/verify-reapply-patches.cjs: - Frozen REASON enum exposes the diagnostic surface as stable codes: OK_NO_USER_LINES_VS_PRISTINE, OK_NO_SIGNIFICANT_BACKUP_LINES, FAIL_INSTALLED_MISSING, FAIL_INSTALLED_NOT_REGULAR_FILE, FAIL_READ_ERROR, FAIL_USER_LINES_MISSING. - Each result.reason is now a code from this enum, not free text. Tests assert via REASON.X equality, not regex on prose. - REASON exported from module.exports. 2. tests/bug-2969-verify-reapply-patches.test.cjs: - Full rewrite. Every assertion on typed structured fields: report.results[0].status === 'fail', report.results[0].reason === REASON.FAIL_INSTALLED_NOT_REGULAR_FILE, report.results[0].missing.includes(droppedLine) (Array set membership, not String substring). - Locks the REASON enum surface via Object.keys(REASON).sort() deepEqual. - Locks the JSON report shape via Object.keys(report).sort() deepEqual. - Zero regex, zero String#includes, zero startsWith/endsWith on text. 3. CONTRIBUTING.md: - New section "Prohibited: Raw Text Matching on Test Outputs" with concrete BAD/GOOD examples (substring on file content; assert.match on stdout; "structured parser" hiding string ops; regex on free-form reason fields). - The rule statement: "Tests assert on typed structured values. If the code under test produces text, the code under test must also expose a structured intermediate representation, and the test must assert on that IR — never on the rendered text." - Required structured-surface table: file IR, --json mode, frozen enum, fs facts. - "Hiding grep behind a function is still grep" callout — the parser-wrapper anti-pattern. - New `pre-existing-text-matching` exemption category for the 8 grandfathered files. Marked Transitional; new tests cannot use it. 4. scripts/lint-no-source-grep.cjs: - Three new patterns enforced (in addition to the existing .cjs-source readFileSync rule): - assert.match/doesNotMatch on .stdout/.stderr - .stdout/.stderr.<includes|startsWith|endsWith>( - readFileSync(...).<includes|startsWith|endsWith>( - Aggregated violations per file (multiple findings now report together). - Updated diagnostic message references both CONTRIBUTING.md sections. 5. 8 pre-existing tests annotated with `// allow-test-rule: pre-existing-text-matching` so the lint passes on this commit; each carries the prose "Tracked for migration to typed-IR assertions; do not copy this pattern." Files: bug-2649, bug-2687, bug-2796, bug-2838, bug-2943, graphify, hooks-opt-in, security-scan. Verification: lint 0 violations across 348 test files; full suite passes. * fix(#2969): rename exemption category to pending-migration-to-typed-ir + cite tracking issue Per maintainer feedback: 1. "Grandfathered" / "legacy" framing is wrong — both terms imply permanent or condoned exemption. The 8 files are tracked for correction, not exempted. 2. Each annotated file must cite the tracking issue so the migration work is auditable. Changes: - CONTRIBUTING.md: rename exemption category from `pre-existing-text-matching` to `pending-migration-to-typed-ir`. Update prose to "Tracked for correction, not exempted" and require each annotation to cite the open migration issue (e.g. `// allow-test-rule: pending-migration-to-typed-ir [#NNNN]`). - 8 test files: update annotation to cite #2974 (the tracking issue opened for migrating these files to typed-IR assertions). |
||
|
|
4d394a249d |
fix(commands): normalize gsd slash namespace drift (#2858)
* fix(commands): normalize gsd slash namespace drift * fix(#2855): address CodeRabbit findings on namespace drift PR Three CR findings, all valid: 1. autonomous.md line 783 still had `gsd:discuss-phase` (the PR's own normalization missed this line). Switched to `gsd-discuss-phase` and updated the matching test in autonomous-interactive.test.cjs that was asserting the now-retired colon form. 2. tests/bug-2543-gsd-slash-namespace.test.cjs source-grepped the fix-slash-commands.cjs script with .includes() rather than driving its transform behaviour. Refactored fix-slash-commands.cjs to export a pure transformContent(src, cmdNames) function, kept the CLI behaviour unchanged via require.main, and replaced the source-grep block with five behavioural cases: rewrite, multi-occurrence, idempotence on canonical input, no-op on gsd-sdk/gsd-tools, and word-boundary safety. 3. tests/bug-2808-skill-hyphen-name.test.cjs matched `name:` anywhere in SKILL.md; a stray name: in the body could satisfy the assertion. Scoped the lookup to the YAML frontmatter block via the suggested diff (parse the leading --- ... --- region first, then find name: inside it). Full suite: 5854/5854 passing. * fix(#2855): address remaining CodeRabbit findings on PR #2858 Three structural concerns flagged on the namespace-drift fix PR: 1. scripts/fix-slash-commands.cjs:24 — `buildPattern([])` compiled `/gsd:()(?=[^a-zA-Z0-9_-]|$)/g`. The empty capture group still matches any `/gsd:` token followed by a non-word boundary (whitespace, EOL, punctuation), rewriting it to a stray `/gsd-`. Verified live: `transformContent("/gsd:", [])` → `"/gsd-"`. Added a guard returning null from `buildPattern` on empty input and updated `transformContent` and `processDir` to no-op when the pattern is null. 2. tests/autonomous-interactive.test.cjs:44-47 — assertion was `content.includes('gsd-discuss-phase') && content.includes('INTERACTIVE')`, which would false-pass on any unrelated co-occurrence (e.g. `INTERACTIVE=""` initialization plus a stray `gsd-discuss-phase` prose mention). Replaced with a structural extraction: locate the `**If \`INTERACTIVE\` is set:**` branch, bound it by the next `**If` / `<step>` boundary, and assert the `Skill(skill="gsd-discuss-phase", ...)` invocation lives inside that region. Tolerates whitespace around `(`, `skill`, and `=`. 3. tests/bug-2808-skill-hyphen-name.test.cjs:104 — colon-call regex was `Skill\(skill=...` and missed valid formatting like `Skill(skill = "gsd:cmd")` or `Skill( skill = ...)`. Loosened to `Skill\(\s*skill\s*=\s*...` so reformatting drift can't slip past the namespace guard. Verification: 5854/5854 pass on `npm test` from the rebased branch. * fix(#2855): drop pre-validation filter that hid namespace drift CR finding on tests/bug-2808-skill-hyphen-name.test.cjs:128: the test collected generated skill directories with `.filter(entry => entry.isDirectory() && entry.name.startsWith('gsd-'))`, then validated namespace invariants over that filtered list. Anything that violated the prefix invariant — `gsd:extract-learnings` (colon form), `extract_learnings` without prefix, `Gsd-foo` mis-cased — would silently disappear from the iteration and the test would falsely pass. Drop the `startsWith('gsd-')` filter so every generated directory shows up. Add explicit assertions before the existing per-skill loop: - directory list is non-empty (catches a broken converter that produces nothing) - every directory begins with `gsd-` - every directory contains no `:` - every directory contains no `_` Re-audited the full PR diff for the same anti-pattern: only this one site filtered before validating the namespace; bug-2643 and commands-doc-parity also use `readdirSync().filter()` but only by file extension, which is correct. 5854/5854 on `npm test`. * fix(#2855): address remaining CR findings (1 active + 2 nitpicks) Three findings on PR #2858, all the same root cause: input narrowing before validation lets drift slip past the guards. 1. tests/bug-2808-...:104 (active) — `colonCallRe` captured local names with `[a-z0-9-]+`, which excluded the underscore. A drift like `Skill(skill="gsd:extract_learnings")` (deprecated colon syntax with the old underscore filename) silently slid through. Broadened the capture to `[^'"\s)]+` so any malformed local name is surfaced; surrounding pattern (whitespace tolerance, escape support, flags) unchanged. 2. tests/bug-2643-...:43 (nitpick) — `extractSkillNamesHyphen` and `extractSkillNamesColon` had the same over-strict capture plus relied on a single regex over raw bytes, which the project test- rigor memory bans (`feedback_no_source_grep_tests.md`). Replaced with `extractSkillCalls(content)` — a small structural extractor that walks `Skill(` openers, locates each call's matching `)`, parses the body's `skill = "..."` keyword argument with permissive whitespace + quoting + escape handling, and returns `{ name, raw }` records. The two namespace-form helpers become thin filters over the structured output. Tightened the body class to `[^'"\\]+` so a trailing escape `\` before the closing quote (as in `Skill(skill=\"gsd-foo\", …)` written inside another string context) doesn't get included in the captured name. 3. tests/bug-2543-...:44 (nitpick) — `DOC_SEARCH_FILES` was a hand- curated 7-entry array. Every doc added in the future would silently weaken drift detection until someone remembered to extend the list. Replaced with `discoverDocSearchFiles(ROOT)`: globs every `.md` under `docs/` and adds `README.md` if present. New docs are picked up automatically. Re-audited the diff surface for similar narrowings; no other sites filter or constrain before validating namespace invariants. 5854/5854 on `npm test`. * fix(#2855): recurse docs/ tree so localized translations are scanned too CR finding: discoverDocSearchFiles() stopped at docs/*.md, leaving localized translation trees (docs/ja-JP/, docs/zh-CN/, docs/ko-KR/, docs/pt-BR/) and other nested doc collections (docs/skills/, docs/superpowers/) invisible to the namespace-drift invariant. Verified the gap: docs/ has 6 nested directories with ~30 .md files that the previous top-level-only scan was skipping. None contain /gsd: references today, but a future translation update or new doc subdir could leak drift. Switch to an iterative stack walk so every .md under docs/ is scanned regardless of depth. Stack form (rather than recursion) avoids the risk of running into the call-stack limit on deep doc trees. 5854/5854 on `npm test`. --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
e81592878e |
feat(#2789): trim skill description anti-patterns; enforce 100-char budget (#2823)
* feat(#2789): trim skill description anti-patterns; enforce 100-char budget - Trim descriptions in all commands/gsd/*.md files over 100 chars - Remove flag documentation from descriptions (belongs in argument-hint) - Remove Triggers: keyword stuffing - Add scripts/lint-descriptions.cjs — fails on descriptions > 100 chars - Add npm script: lint:descriptions - Add tests/enh-2789-description-budget.test.cjs Closes #2789 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(#2789): add CHANGELOG entry for description budget lint * docs(#2789): update COMMANDS.md descriptions; add skill description standards note Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
aeef87de7f |
docs(test-standards): enforce no-source-grep rule with CI linter + CONTRIBUTING.md (#2700)
* docs(test-standards): enforce no-source-grep rule with CI linter + update CONTRIBUTING.md
Adds scripts/lint-no-source-grep.cjs — a static linter that detects readFileSync
on .cjs source files in tests without an allow-test-rule annotation. Wires it
into CI as a new lint-tests job in test.yml and as npm run lint:tests.
Resolves all 9 existing violations across the test suite:
- Rewrites workspace routing tests (3) as behavioral runGsdTools calls that
verify each command is router-recognized (exit != "Unknown init workflow")
- Adds allow-test-rule annotations with explanatory comments to 7 legitimate
structural tests: architectural invariants (locking, orphan-worktree),
structural regression guards (milestone-regex-global), docs-parity
(config-field-docs), integration-test-input (copilot-install), and
structural-implementation-guards (bug-1891, discuss-mode)
Updates CONTRIBUTING.md Testing Standards section with:
- "Prohibited: Source-Grep Tests" section with the before/after pattern,
root cause analysis of why it breaks (commit
|
||
|
|
259c1d07d3 |
fix(#2647): guard tarball ships sdk/dist so gsd-sdk query works (#2671)
v1.38.3 shipped without sdk/dist/ because the outer `files` whitelist and `prepublishOnly` chain had drifted. The `gsd-sdk` bin shim then fell through to a stale @gsd-build/sdk@0.1.0 (pre-`query`), breaking every workflow that called `gsd-sdk query <noun>` on fresh installs. Current package.json already restores `sdk/dist` + `build:sdk` prepublish; this PR locks the fix in with: - tests/bug-2647-outer-tarball-sdk-dist.test.cjs — asserts `files` includes `sdk/dist`, `prepublishOnly` invokes `build:sdk`, the shim resolves sdk/dist/cli.js, `npm pack --dry-run` lists sdk/dist/cli.js, and the built CLI exposes a `query` subcommand. - scripts/verify-tarball-sdk-dist.sh — packs, extracts, installs prod deps, and runs `node sdk/dist/cli.js query --help` against the real tarball output. - .github/workflows/release.yml — runs the verify script in both next and stable release jobs before `npm publish`. Partial fix for #2649 (same root cause on the sibling sdk package). Fixes #2647 |
||
|
|
73c1af5168 |
fix(#2543): replace legacy /gsd-<cmd> syntax with /gsd:<cmd> across all source files (#2595)
Commands are now installed as commands/gsd/<name>.md and invoked as /gsd:<name> in Claude Code. The old hyphen form /gsd-<name> was still hardcoded in hundreds of places across workflows, references, templates, lib modules, and command files — causing "Unknown command" errors whenever GSD suggested a command to the user. Replace all /gsd-<cmd> occurrences where <cmd> is a known command name (derived at runtime from commands/gsd/*.md) using a targeted Node.js script. Agent names, tool names (gsd-sdk, gsd-tools), directory names, and path fragments are not touched. Adds regression test tests/bug-2543-gsd-slash-namespace.test.cjs that enforces zero legacy occurrences going forward. Removes inverted tests/stale-colon-refs.test.cjs (bug #1748) which enforced the now-obsolete hyphen form; the new bug-2543 test supersedes it. Updates 5 assertion tests that hardcoded the old hyphen form to accept the new colon form. Closes #2543 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
62eaa8dd7b |
docs: close doc drift vectors — bidirectional parity, manifest, schema-driven config (#2479)
Option A — ghost-entry guard (INVENTORY ⊆ actual): tests/inventory-source-parity.test.cjs parses every declared row in INVENTORY.md and asserts the source file exists. Catches deletions and renames that leave ghost entries behind. Option B — auto-generated structural manifest: scripts/gen-inventory-manifest.cjs walks all six family dirs and emits docs/INVENTORY-MANIFEST.json. tests/inventory-manifest-sync.test.cjs fails CI when a new surface ships without a manifest update, surfacing exactly which entries are missing. Option C — schema-driven config validation + docs parity: get-shit-done/bin/lib/config-schema.cjs extracted from config.cjs as the single source of truth for VALID_CONFIG_KEYS and dynamic patterns. config.cjs now imports from it. tests/config-schema-docs-parity.test.cjs asserts every exact-match key appears in docs/CONFIGURATION.md, surfacing 14 previously undocumented keys (planning.sub_repos, workflow.ai_integration_phase, git.base_branch, learnings.max_inject, and 10 others) — all now documented in their appropriate sections. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
13a96ee994 |
fix(build): include gsd-read-injection-scanner in hooks/dist (#2406)
The scanner was added in #2201 but never added to the HOOKS_TO_COPY allowlist in scripts/build-hooks.js, so it never landed in hooks/dist/. install.js reads from hooks/dist/, so every install on 1.37.0/1.37.1 emitted "Skipped read injection scanner hook — not found at target" and the read-time prompt-injection scanner was silently disabled. - Add gsd-read-injection-scanner.js to HOOKS_TO_COPY - Add it to EXPECTED_ALL_HOOKS regression test in install-hooks-copy Fixes #2406 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c35997fb0b |
feat(hooks): add gsd-read-injection-scanner PostToolUse hook (#2201) (#2328)
* feat: add /gsd-spec-phase — Socratic spec refinement with ambiguity scoring (#2213) Introduces `/gsd-spec-phase <phase>` as an optional pre-step before discuss-phase. Clarifies WHAT a phase delivers (requirements, boundaries, acceptance criteria) with quantitative ambiguity scoring before discuss-phase handles HOW to implement. - `commands/gsd/spec-phase.md` — slash command routing to workflow - `get-shit-done/workflows/spec-phase.md` — full Socratic interview loop (up to 6 rounds, 5 rotating perspectives: Researcher, Simplifier, Boundary Keeper, Failure Analyst, Seed Closer) with weighted 4-dimension ambiguity gate (≤ 0.20 to write SPEC.md) - `get-shit-done/templates/spec.md` — SPEC.md template with falsifiable requirements (Current/Target/Acceptance per requirement), Boundaries, Acceptance Criteria, Ambiguity Report, and Interview Log; includes two full worked examples - `get-shit-done/workflows/discuss-phase.md` — new `check_spec` step detects `{padded_phase}-SPEC.md` at startup; displays "Found SPEC.md — N requirements locked. Focusing on implementation decisions."; `analyze_phase` respects `spec_loaded` flag to skip "what/why" gray areas; `write_context` emits `<spec_lock>` section with boundary summary and canonical ref to SPEC.md - `docs/ARCHITECTURE.md` — update command/workflow counts (74→75, 71→72) Closes #2213 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(hooks): add gsd-read-injection-scanner PostToolUse hook (#2201) Adds a new PostToolUse hook that scans content returned by the Read tool for prompt injection patterns, including four summarisation-specific patterns (retention-directive, permanence-claim, etc.) that survive context compression. Defense-in-depth for long GSD sessions where the context summariser cannot distinguish user instructions from content read from external files. - Advisory-only (warns without blocking), consistent with gsd-prompt-guard.js - LOW severity for 1-2 patterns, HIGH for 3+ - Inlined pattern library (hook independence) - Exclusion list: .planning/, REVIEW.md, CHECKPOINT, security docs, hook sources - Wired in install.js as PostToolUse matcher: Read, timeout: 5s - Added to MANAGED_HOOKS for staleness detection - 19 tests covering all 13 acceptance criteria (SCAN-01–07, EXCL-01–06, EDGE-01–06) Closes #2201 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ci): add read-injection-scanner files to prompt-injection-scan allowlist Test payloads in tests/read-injection-scanner.test.cjs and inlined patterns in hooks/gsd-read-injection-scanner.js legitimately contain injection strings. Add both to the CI script allowlist to prevent false-positive failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(test): assert exitCode, stdout, and signal explicitly in EDGE-05 Addresses CodeRabbit feedback: the success path discarded the return value so a malformed-JSON input that produced stdout would still pass. Now captures and asserts all three observable properties. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
50f61bfd9a |
fix(hooks): complete stale-hooks false-positive fix — stamp .sh version headers + fix detector regex (#2224)
* fix(hooks): stamp gsd-hook-version in .sh hooks and fix stale detection regex (#2136, #2206) Three-part fix for the persistent "⚠ stale hooks — run /gsd-update" false positive that appeared on every session after a fresh install. Root cause: the stale-hook detector (gsd-check-update.js) could only match the JS comment syntax // in its version regex — never the bash # syntax used in .sh hooks. And the bash hooks had no version header at all, so they always landed in the "unknown / stale" branch regardless. Neither partial fix (PR #2207 regex only, PR #2215 install stamping only) was sufficient alone: - Regex fix without install stamping: hooks install with literal "{{GSD_VERSION}}", the {{-guard silently skips them, bash hook staleness permanently undetectable after future updates. - Install stamping without regex fix: hooks are stamped correctly with "# gsd-hook-version: 1.36.0" but the detector's // regex can't read it; still falls to the unknown/stale branch on every session. Fix: 1. Add "# gsd-hook-version: {{GSD_VERSION}}" header to gsd-phase-boundary.sh, gsd-session-state.sh, gsd-validate-commit.sh 2. Extend install.js (both bundled and Codex paths) to substitute {{GSD_VERSION}} in .sh files at install time (same as .js hooks) 3. Extend gsd-check-update.js versionMatch regex to handle bash "#" comment syntax: /(?:\/\/|#) gsd-hook-version:\s*(.+)/ Tests: 11 new assertions across 5 describe blocks covering all three fix parts independently plus an E2E install+detect round-trip. 3885/3885 pass. Approach credit: PR #2207 (j2h4u / Maxim Brashenko) for the regex fix; PR #2215 (nitsan2dots) for the install.js substitution approach. Closes #2136, #2206, #2209, #2210, #2212 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(hooks): extract check-update worker to dedicated file, eliminating template-literal regex escaping Move stale-hook detection logic from inline `node -e '<template literal>'` subprocess to a standalone gsd-check-update-worker.js. Benefits: - Regex is plain JS with no double-escaping (root cause of the (?:\\/\\/|#) confusion) - Worker is independently testable and can be read directly by tests - Uses execFileSync (array args) to satisfy security hook that blocks execSync - MANAGED_HOOKS now includes gsd-check-update-worker.js itself Update tests to read worker file instead of main hook for regex/configDir assertions. All 3886 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
2703422be8 |
refactor(tests): standardize to node:assert/strict and t.after() per CONTRIBUTING.md (#1675)
* refactor(tests): standardize to node:assert/strict and t.after() per CONTRIBUTING.md
- Replace require('node:assert') with require('node:assert/strict') across
all 73 test files to enforce strict equality (no type coercion)
- Replace try/finally cleanup blocks with t.after() hooks in core.test.cjs
and hooks-opt-in.test.cjs per the test lifecycle standards
- Utility functions in codex-config and security-scan retain try/finally
as that is appropriate for per-function resource guards, not lifecycle hooks
Closes #1674
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(tests): add --test-concurrency=4 to test runner for parallel file execution
Node.js --test-concurrency controls how many test files run as parallel child
processes. Set to 4 by default, configurable via TEST_CONCURRENCY env var.
Fixes tests at a known level rather than inheriting os.availableParallelism()
which varies across CI environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): allowlist verify.test.cjs in prompt-injection scanner
tests/verify.test.cjs uses <human>...</human> as GSD phase task-type
XML (meaning "a human should verify this step"), which matches the
scanner's fake-message-boundary pattern for LLM APIs. This is a
false positive — add it to the allowlist alongside the other test files
that legitimately contain injection-adjacent patterns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
ca6a273685 |
fix: remove marketing text from runtime prompt, fix #1656 and #1657 (#1672)
* chore: ignore .worktrees directory Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(install): remove marketing taglines from runtime selection prompt Closes #1654 The runtime selection menu had promotional copy appended to some entries ("open source, the #1 AI coding platform on OpenRouter", "open source, free models"). Replaced with just the name and path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(kilo): update test to assert marketing tagline is removed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): use process.execPath so tests pass in shells without node on PATH Three test patterns called bare `node` via shell, which fails in Claude Code sessions where `node` is not on PATH: - helpers.cjs string branch: execSync(`node ...`) → execFileSync(process.execPath) with a shell-style tokenizer that handles quoted args and inner-quote stripping - hooks-opt-in.test.cjs: spawnSync('bash', ...) for hooks that call `node` internally → spawnHook() wrapper that injects process.execPath dir into PATH - concurrency-safety.test.cjs: exec(`node ...`) for concurrent patch test → exec(`"${process.execPath}" ...`) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve #1656 and #1657 — bash hooks missing from dist, SDK install prompt #1656: Community bash hooks (gsd-session-state.sh, gsd-validate-commit.sh, gsd-phase-boundary.sh) were never included in HOOKS_TO_COPY in build-hooks.js, so hooks/dist/ never contained them and the installer could not copy them to user machines. Fixed by adding the three .sh files to the copy array with chmod +x preservation and skipping JS syntax validation for shell scripts. #1657: promptSdk() called installSdk() which ran `npm install -g @gsd-build/sdk` — a package that does not exist on npm, causing visible errors during interactive installs. Removed promptSdk(), installSdk(), --sdk flag, and all call sites. Regression tests in tests/bugs-1656-1657.test.cjs guard both fixes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: sort runtime list alphabetically after Claude Code - Claude Code stays pinned at position 1 - Remaining 10 runtimes sorted A-Z: Antigravity(2), Augment(3), Codex(4), Copilot(5), Cursor(6), Gemini(7), Kilo(8), OpenCode(9), Trae(10), Windsurf(11) - Updated runtimeMap, allRuntimes, and prompt display in promptRuntime() - Updated multi-runtime-select, kilo-install, copilot-install tests to match Also fix #1656 regression test: run build-hooks.js in before() hook so hooks/dist/ is populated on CI (directory is gitignored; build runs via prepublishOnly before publish, not during npm ci). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
9d626de5fa |
fix(hooks): add read-before-edit guard for non-Claude runtimes (#1645)
* fix(hooks): add read-before-edit guidance for non-Claude runtimes When models that don't natively enforce read-before-edit hit the guard, the error message now includes explicit instruction to Read first. This prevents infinite retry loops that burn through usage. Closes #1628 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(build): register gsd-read-guard.js in HOOKS_TO_COPY and harden tests The hook was missing from scripts/build-hooks.js, so global installs would never receive the hook file in hooks/dist/. Also adds tests for build registration, install uninstall list, and non-string file_path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
98f05d43b8 |
fix: security scan self-detection and Windows test compatibility
- Add base64-scan.sh and secret-scan.sh to prompt injection scanner allowlist (scanner was flagging its own pattern strings) - Skip executable bit check on Windows (no Unix permissions) - Skip bash script execution tests on Windows (requires Git Bash) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
feec5a37a2 |
ci(security): add prompt injection, base64, and secret scanning
Add CI security pipeline to catch prompt injection attacks, base64-obfuscated payloads, leaked secrets, and .planning/ directory commits in PRs. This is critical for get-shit-done because the entire codebase is markdown prompts — a prompt injection in a workflow file IS the attack surface. New files: - scripts/prompt-injection-scan.sh: scans for instruction override, role manipulation, system boundary injection, DAN/jailbreak, and tool call injection patterns in changed files - scripts/base64-scan.sh: extracts base64 blobs >= 40 chars, decodes them, and checks decoded content against injection patterns (skips data URIs and binary content) - scripts/secret-scan.sh: detects AWS keys, OpenAI/Anthropic keys, GitHub PATs, Stripe keys, private key headers, and generic credential patterns - .github/workflows/security-scan.yml: runs all three scans plus a .planning/ directory check on every PR - .base64scanignore / .secretscanignore: per-repo false positive allowlists - tests/security-scan.test.cjs: 51 tests covering script existence, pattern matching, false positive avoidance, and workflow structure All scripts support --diff (CI), --file, and --dir modes. Cross-platform (macOS + Linux). SHA-pinned actions. Environment variables used for github context in run blocks (no direct interpolation). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
62db008570 |
security: add prompt injection guards, path traversal prevention, and input validation
Defense-in-depth security hardening for a codebase where markdown files become LLM system prompts. Adds centralized security module, PreToolUse hook for injection detection, and CI-ready codebase scan. New files: - security.cjs: path traversal prevention, prompt injection scanner/sanitizer, safe JSON parsing, field name validation, shell arg validation - gsd-prompt-guard.js: PreToolUse hook scans .planning/ writes for injection - security.test.cjs: 62 unit tests for all security functions - prompt-injection-scan.test.cjs: CI scan of all agent/workflow/command files Hardened code paths: - readTextArgOrFile: path traversal guard (--prd, --text-file) - cmdStateUpdate/Patch: field name validation prevents regex injection - cmdCommit: sanitizeForPrompt strips invisible chars from commit messages - gsd-tools --fields: safeJsonParse wraps unprotected JSON.parse - cmdFrontmatterGet/Set: null byte rejection - cmdVerifyPathExists: null byte rejection - install.js: registers prompt guard hook, updates uninstaller Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
a6ba3e268e |
feat: PreToolUse workflow guard hook for rogue edit prevention (#678) (#1197)
New opt-in PreToolUse hook that warns when Claude edits files outside a GSD workflow context (no active /gsd: command or subagent). Soft guard — advises, does not block. The edit proceeds but Claude sees a reminder to use /gsd:fast or /gsd:quick for state tracking. Enable: set hooks.workflow_guard: true in .planning/config.json Default: disabled (false) Allows without warning: - .planning/ files (GSD state management) - Config files (.gitignore, .env, CLAUDE.md, settings.json) - Subagent contexts (executor, planner, etc.) Includes 3s stdin timeout guard and silent fail-safe. Closes #678 |
||
|
|
14c1dd845b |
fix(build): add syntax validation to hook build script (#1165)
Prevents shipping hooks with JavaScript SyntaxError (like the duplicate const cwd declaration that caused PostToolUse errors for all users in v1.25.1). The build script now validates each hook file's syntax via vm.Script before copying to dist/. If any hook has a SyntaxError, the build fails with a clear error message and exits non-zero, blocking npm publish. Refs #1107, #1109, #1125, #1161 |
||
|
|
02a5319777 |
fix(ci): propagate coverage env in cross-platform test runner
The run-tests.cjs child process now inherits NODE_V8_COVERAGE from the parent so c8 collects coverage data. Also restores npm scripts to use the cross-platform runner for both test and test:coverage commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
ccb8ae1d18 |
fix(ci): cross-platform test runner for Windows glob expansion
npm scripts pass `tests/*.test.cjs` to node/c8 as a literal string on Windows (PowerShell/cmd don't expand globs). Adding `shell: bash` to CI steps doesn't help because c8 spawns node as a child process using the system shell. Use a Node script to enumerate test files cross-platform. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |