* fix(#2773): emit correct Codex 0.124.0+ two-level nested hooks schema
Codex 0.124.0's stable spec requires:
[[hooks.SessionStart]] ← event entry (optional matcher)
[[hooks.SessionStart.hooks]] ← handler sub-table
type = "command"
command = "node ..."
Previous GSD versions wrote the flat [[hooks]] + event = "SessionStart"
form (#2637) or a single-block [[hooks.SessionStart]] without the nested
.hooks sub-table (#2760). Both are rejected by Codex 0.124.0+ at launch.
Changes:
bin/install.js
- Hook block emission now always writes the two-level nested AoT form.
- migrateCodexHooksMapFormat extended to also migrate flat [[hooks]]
array-of-tables entries (event = "..." key → [[hooks.<EVENT>]] form).
Flat [[hooks]] and [[hooks.<EVENT>]] are mutually exclusive TOML types;
any pre-existing flat entries must be promoted before GSD appends its
own namespaced hooks.
- Migrated flat AoT blocks are inserted BEFORE the GSD marker so they
stay in the "user" portion of the file and survive stripGsdFromCodexConfig.
- stripCodexGsd* regexes cover all four historical block shapes.
- validateCodexConfigSchema no longer rejects flat [[hooks]] at the root
level (removing the false-positive that blocked install when users had
their own AfterCommand hooks). The validator still enforces the nested
[[hooks.<EVENT>.hooks]] shape for entries that have a .hooks sub-table.
tests/
- bug-2760-codex-install-defensive.test.cjs: 29/29 passing.
Added 5 new regression cases for fresh install, upgrade from each
legacy shape, idempotent reinstall, and user hook preservation.
- codex-config.test.cjs: 106/106 passing.
All migration tests updated to assert [[hooks.<TYPE>.hooks]] sub-table
(command now in handler level, not event-entry level).
New tests: flat [[hooks]] migration (SessionStart, AfterCommand),
install+uninstall preserves non-GSD AfterCommand hook.
Closes#2773
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address CodeRabbit review + CI regression in bug-2698-crlf-install
CI regression (#2698 tests):
Strip GSD-managed hook blocks BEFORE running migrateCodexHooksMapFormat.
The previous order let migration convert the stale [[hooks]] + event =
"SessionStart" + gsd-update-check.js block to [[hooks.SessionStart]] form
before Shape 1 strip regex could match it; Shape 1 only matches the flat
[[hooks]] form, so the stale block survived reinstall. Swapping to
strip-then-migrate ensures only user-authored hooks reach the migration step.
Shape 3/4 regexes also extended to match both gsd-check-update.js and the
legacy gsd-update-check.js filename so no variant slips through.
CodeRabbit actionable (major):
migrateCodexHooksMapFormat now accepts single-quoted TOML event values
(event = 'SessionStart') in the flat [[hooks]] filter and event-name
extractor. TOML spec allows single-quoted literal strings; double-quote-only
regexes silently skipped them, leaving the block unmigrated and triggering
the hard-fail validator.
CodeRabbit nitpicks:
tests/codex-config.test.cjs: replace indexOf('[[hooks.AfterCommand]]')
ordering check with parseTomlToObject structural assertions (no-source-grep
rule).
tests/bug-2760-codex-install-defensive.test.cjs: replace three
content.match(/…/g).length raw-text counts with parseTomlToObject structural
assertions for single-handler and single-event-entry invariants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address CodeRabbit review #2 — extractFlatHookEventName helper + type assertions
- bin/install.js: consolidate TOML_QUOTED_STRING + TOML_EVENT_CAPTURE into a
single extractFlatHookEventName() helper that rejects empty-string event values
(event = "" or event = ''); previously two independent regexes had to be kept
in sync and neither guarded against a blank event name producing a [[hooks.]]
header
- tests/bug-2760-codex-install-defensive.test.cjs: add comments explaining why
the e.command fallback is retained in both allSessionStartCommands and
afterToolCommands collectors — migration only upgrades [hooks.TYPE] map-format
sections, not existing [[hooks.TYPE]] namespaced AoT entries authored with
command at event-entry level; removing the fallback causes false failures for
preserved user entries
- tests/codex-config.test.cjs: add type = "command" assertions to all migration
tests that verify .command but were missing .type checks; buildNestedBlock
injects type = "command" when the source body has no explicit type key, so
every migrated handler must carry it per the Codex 0.124.0+ schema
138 tests pass, 0 fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: CR round 3 + proactive audit — TOML quoting, stale AoT migration, strict validator
Three real issues from CodeRabbit round 3, plus the collateral improvements they
enable:
bin/install.js — tomlBareKey() helper (#2773 CR6a)
buildNestedBlock interpolated the raw event name into [[hooks.${type}]] and
[[hooks.${type}.hooks]] headers without TOML escaping. An event name containing
spaces or punctuation (e.g. "Before Tool") would produce invalid TOML that
parseTomlToObject would subsequently reject. Added tomlBareKey() — wraps the
key in double-quoted TOML strings when it contains non-bare-key characters
([A-Za-z0-9_-]).
bin/install.js — staleNamespacedAotSections migration path (#2773 CR6b)
migrateCodexHooksMapFormat handled [hooks.TYPE] (map-format) and flat [[hooks]]
with event = "..." but ignored [[hooks.TYPE]] AoT entries that carried handler
fields (command, type, timeout, statusMessage) at event-entry level without a
nested [[hooks.TYPE.hooks]] sub-table. This is the pre-#2773 single-block shape
that Codex 0.124.0+ rejects. Added staleNamespacedAotSections as the third
migration category: detected by STALE_HANDLER_FIELD_PATTERN + absence of a
[[hooks.TYPE.hooks]] sub-table in the same file; promoted to the two-level
nested form by buildNestedBlock. Matcher-only entries (no handler fields) are
intentionally skipped.
bin/install.js — validator now rejects event-level handler fields (#2773 CR6c)
With migration covering the stale AoT shape, validateCodexConfigSchema can be
strict: entries that have handler fields at event-entry level but no .hooks
sub-array return ok: false instead of silently passing. Matcher-only entries
(no handler fields and no .hooks) remain valid as event filters.
tests/codex-config.test.cjs — four new migration tests + missing type assertion
Four tests cover the new stale AoT migration path: single-entry promotion,
already-nested entry is left untouched (no double-wrap), multiple event types,
and matcher-only entry is skipped. Added the missing type = "command" assertion
to the CRLF migration test (the one miss from CR round 2).
tests/bug-2760-codex-install-defensive.test.cjs — strict .hooks-only collectors
With stale AoT entries now migrated, the entry.command fallbacks in
allSessionStartCommands and afterToolCommands are dead code. Replaced with
strict entry.hooks-only collection guarded by an every(Array.isArray(e.hooks))
pre-assertion, so any future regression that leaves handler fields at event
level produces an explicit test failure rather than silently collecting them.
142 tests pass, 0 fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: CR round 4 — segment-safe quoted-key detection + structural test assertions
bin/install.js — getTomlTableSections now exposes segments (#2773 CR7a)
The staleNamespacedAotSections filter used section.path.split('.').length > 2
to skip [[hooks.TYPE.hooks]] sub-table entries. That check misclassifies quoted
event names containing dots: [[hooks."before.tool"]] has path hooks.before.tool
(3 dot-parts) but only 2 true parsed segments, so it was incorrectly excluded
from migration. Fixed by adding segments to the getTomlTableSections return
shape (already available on record.tableHeader.segments) and replacing the
split-based check with section.segments.length !== 2, which uses the true
parsed key count regardless of dots inside quoted names.
tests/codex-config.test.cjs — replace raw-equality assertions (#2773 CR7b)
The two new no-op migration tests (already-nested and matcher-only) used
assert.strictEqual(result, content) — raw string equality that conflicts with
the repo no-source-grep testing standard. Replaced with structural assertions
using parseTomlToObject: the already-nested test verifies the handler stays
under .hooks[0] and no double-wrap occurs; the matcher-only test verifies the
matcher key is preserved and no .hooks sub-array is added.
142 tests pass, 0 fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: CR round 5 — parseHooksBody key parser, empty-handler guard, segment-safe legacyMap filter, stronger test assertions
- parseHooksBody: replace /^([\w.]+)\s*=/ regex with parseTomlKey() so
hyphenated keys (status-message) and quoted keys are not silently dropped
- buildNestedBlock: guard against handlerEntries.length === 0 — do not
synthesise [[hooks.TYPE.hooks]] with type="command" but no command for
matcher-only or otherwise handler-empty stale sections
- legacyMapSections filter: use section.segments.length === 2 (same fix
applied to staleNamespacedAotSections in round 4) to prevent [hooks.X.Y]
3-segment tables from being misclassified as event entries
- tests: add regression test for [[hooks."before.tool"]] quoted-dot event
names; strengthen command path assertion to exact absolute path comparison
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2760): defensive Codex install — strip legacy agents blocks, default hooks to AoT, validate post-write schema
Three defects, three defensive fixes shipped together. Issue reporter
never returned with the requested diagnostic backup, but four additional
users have since confirmed the same Codex breakage and ZakAnun confirmed
manual cleanup is the only working workaround — defensive triple ships
without the original backup grep, justified by the corroborating reports.
Fix 1 (defect 3 — confirmed real). The Codex hooks emit path always
appended a top-level `[[hooks]]` AoT block, which collides with users
who already use the namespaced AoT form `[[hooks.SessionStart]]`. New
helper `hasUserNamespacedAotHooks()` detects the user's preferred shape
on parse and the install emits the GSD-managed hook in that same shape
when present. Default for fresh configs stays at top-level `[[hooks]]`
so status-quo behavior is preserved.
Fix 2 (defects 1+2 — defensive). `stripLeakedGsdCodexSections()` (the
install-time stripper) now always purges bare `[agents]` single-bracket
tables and `[[agents]]` sequence tables regardless of GSD marker
presence — both forms are invalid in current Codex schema and produce
"invalid type: ..., expected struct AgentsToml". Previously gated on
GSD-name lookup which missed marker-stripped configs and third-party
authored entries. The uninstall-time stripper (`stripCodexGsdAgentSections`)
keeps its old conservative behavior so user-authored entries survive
uninstall.
Fix 3 (defensive). Post-write schema validation parses the bytes about
to be committed and asserts no bare `[agents]`, no `[[agents]]`, and no
bare `[hooks.<Event>]` tables remain. On failure the install restores
the pre-install backup of config.toml and aborts loudly so the user is
never left with a Codex CLI that refuses to load. Pre-install snapshot
is captured before installCodexConfig runs (not after) so restore
returns the file to its true pre-GSD state.
Tests added (10 new, 1 updated):
- bug-2760-codex-install-defensive.test.cjs (10 new tests across 4
describes: hooks AoT preservation, strip robustness for both
[agents] and [[agents]] without marker, schema validator behavior,
abort+restore via test seam)
- codex-config.test.cjs "case 2 ..." updated to reflect new defensive
bare-[agents] purge
Full suite: 5747 pass / 0 fail.
Closes#2760
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#2760): normalize Codex hooks emit field name across migration and managed paths
The migrateCodexHooksMapFormat path emitted `type = "<TYPE>"` for legacy
[hooks.TYPE] sections, while the GSD-managed Codex install emitted
`event = "SessionStart"` — same target [[hooks]] schema, two different
field names. Codex currently tolerates both via permissive parsing, but
the moment one path tightens this becomes a silent #2760-class regression.
Normalize both call sites on `event` (the existing GSD-managed
convention). Update migration emit, docstring, and existing migration
assertions to match. Add a parity regression test that drives both code
paths and asserts the [[hooks]] field key is identical.
* test(#2153): fix test isolation by building hooks/dist on demand
The "Codex install copies hook file (#2153)" regression depends on
hooks/dist/ being populated, but that directory is gitignored and only
built by `npm run build:hooks`. The npm pretest chain runs `build:sdk`
but not `build:hooks`, so when this file is run in isolation
(`node --test tests/codex-config.test.cjs`) the hook copy step skips
silently and the regression test fails on a stale-environment artifact
rather than a real bug.
Add a top-level before() hook that runs scripts/build-hooks.js when
hooks/dist/ is missing or empty. Matches the pattern already used by
bug-1834-sh-hooks-installed and other install integration tests, so the
suite passes regardless of runner ordering or which tests are targeted.
* fix(#2760): structural TOML validation, atomic writes, and behavioral test rewrites
Addresses CodeRabbit review on PR #2785 plus source-grep violations the
maintainer flagged in the regression test.
Fix 1 (CR 3149606220) — validateCodexConfigSchema now parses the TOML into
a structured object first via the new parseTomlToObject helper, then
runs schema-shape checks against both the parsed structure and the table
section headers. Malformed TOML with valid-looking headers no longer
slips past validation.
Fix 2 (CR 3149606224) — Replaced the four source-grep assertions in
tests/bug-2760-codex-install-defensive.test.cjs (lines 109, 125, 169,
201) with structural assertions against the parsed TOML object via the
exported parseTomlToObject helper. Tests now verify behavior (the file
parses and contains the expected structure) instead of literal byte
patterns. Robust to formatting changes — exactly what the regex-loosening
suggestion was reaching for, done correctly. Confirmed clean by
`npm run lint:tests` (0 violations).
Fix 3 (CR 3149606234) — The describe block that mutates
installModule.__codexSchemaValidator now runs with concurrency: false
so the test seam mutation cannot leak into sibling suites that also
call runCodexInstall.
Fix 4 (CR outside-diff) — Approach (b): atomic temp-file + renameSync.
Added atomicWriteFileSync helper used by mergeCodexConfig and the final
hooks-write. A mid-write failure leaves the .tmp-<pid>-<n> sibling
behind (cleaned up immediately) and never truncates the original
config.toml. Paired with try/catch wrapping around the entire
post-snapshot mutation sequence so any unexpected throw also triggers
restoreCodexSnapshot. Two layers of defense: atomic write prevents the
corruption window, snapshot restore handles non-atomic write paths.
Added behavioral test for fix 4: stubs fs.renameSync to throw on the
configPath rename, asserts the on-disk bytes match the pre-install
snapshot byte-for-byte, asserts the parsed structure is still the
user's [model] section (no half-written GSD agents block), and asserts
no stray .tmp-* files remain. Marked concurrency: false because it
monkey-patches a global.
Test results: 5749/5749 pass, 0 fail. lint:tests clean.
* test(#2760): TOML-parse based assertions for bare-agents purge and hook-field parity (CodeRabbit follow-up)
* fix(#2760): treat write failures as fatal, strip legacy hooks before guard, tighten TOML parser (CR4)
CR4 finding 1 (MAJOR) — Write failures silently succeeded. The inner catch
around atomicWriteFileSync restored the snapshot then re-threw, but the outer
catch only matched 'post-write Codex schema validation failed' and downgraded
everything else to a warn-and-continue. Install finished with "Done!" while
Codex had no GSD agents configured. Fix: wrap writeErr with a `post-write
Codex install failed:` prefix and broaden the outer guard to `.startsWith(
'post-write')` so both schema-validation and write failures abort install.
CR4 finding 2 (MAJOR) — Legacy flat [[hooks]] block prevented namespaced AoT
upgrade. The `!configContent.includes('gsd-check-update')` guard short-
circuited the new namespaced emit when an existing install had the legacy
flat [[hooks]] block, leaving users stuck in the mixed layout this fix is
designed to eliminate. Fix: strip ALL existing managed gsd-check-update
hook blocks (top-level [[hooks]] AND namespaced [[hooks.SessionStart]])
BEFORE evaluating the includes guard, so every install converges on the
right shape regardless of prior state.
CR4 finding 3 (MAJOR) — Homegrown TOML parser silently accepted malformed
input. parseTomlValue happily consumed the `0` prefix of `timeout = 0.5`
and parseTomlToObject did not verify the full RHS was consumed, so
`key = "x" junk` and date/time literals slipped through. Per CONTRIBUTING
("No external dependencies in core"), option (b) was chosen over adding
@iarna/toml: (a) parseTomlValue rejects any integer immediately followed
by `.`, `e`, `E`, `:`, `-`, `T`, or `Z` (floats / dates / times); (b)
parseTomlToObject scans from parsed.end to the next newline and throws
`trailing bytes after value` if anything other than whitespace + optional
`# comment` is present.
* test(#2760): add CR4 regression tests + scope GSD_TEST_MODE + rename rename-fault test
CR4 finding 5 (NIT) — GSD_TEST_MODE leak. Saved previous value, set '1' for
the require, then restored (delete if undefined). No more test-only env var
leaking to siblings in the same node process.
CR4 finding 4 (NIT) — Renamed the existing fix-4 test from 'fs.writeFileSync'
to 'fs.renameSync' (the only call actually faulted) and added a sibling test
that stubs fs.writeFileSync to throw on the .tmp- target — exercising the
pre-rename branch of atomicWriteFileSync that was previously untested. Both
serialize via concurrency: false on the existing describe block.
CR4 finding 1 (MAJOR test) — New behavioral test asserts install throws with
a `post-write Codex install failed` message AND never prints "Done!" when
the hook-block atomic rename fails. Captures stdout via console.log stub,
asserts byte equality of restored snapshot. Faults only the rename whose
temp source contains gsd-check-update so earlier mergeCodexConfig writes
are not collateral damage.
CR4 finding 2 (MAJOR test) — New TOML-parsed behavioral test for the
legacy-hook upgrade path: pre-install has [[hooks.SessionStart]] (user) +
legacy flat [[hooks]] managed gsd-check-update entry; post-install must
have hooks.SessionStart as Array-of-tables with both user hook and GSD
entry, and no top-level [[hooks]] AoT remaining. Also asserts exactly one
gsd-check-update entry (no duplicates).
CR4 finding 3 (MAJOR test) — parseTomlToObject regression suite: rejects
floats (timeout = 0.5), dates (created = 1979-05-27), trailing garbage
(key = "x" junk), and accepts trailing whitespace + # comment.
* fix(#2760): CR5 — pre-write fatal, TOML duplicate-key/header rejection, namespaced AoT migration
Address all five CodeRabbit round-5 findings on PR #2785:
Finding 1 (MAJOR) — Pre-write failures in the Codex hook configuration
catch (around bin/install.js:7002) used to fall through to console.warn
even though restoreCodexSnapshot() had already run. This produced "Done!"
output with no Codex hooks configured. Now wraps the original error with
a "(pre-write)" prefix and rethrows so install aborts loudly. Same defect
class as CR4 finding 1, different layer.
Finding 2 (MAJOR) — parseTomlToObject silently reused existing tables and
overwrote duplicate keys. Real TOML 1.0 rejects:
- duplicate scalar key in same table ([a]\nx=1\nx=2)
- re-declared [a] header (two [a] sections)
- [[arr]] then [arr] for same path (shape mismatch)
Tracks pathShape, declaredHeaders, and per-table-instance key sets;
throws "duplicate or shape-mismatched table header at <path>" or
"duplicate key <name> in <path>".
Finding 3 (MAJOR) — migrateCodexHooksMapFormat used to emit flat
[[hooks]]\nevent="<TYPE>", which produced mixed flat+namespaced layouts
when the user already had [[hooks.<OTHER>]] entries. Now emits
[[hooks.<TYPE>]] directly (the namespace IS the event); managed-emit
detector hasUserNamespacedAotHooks fires correctly so the install
converges on a single namespaced layout regardless of pre-existing state.
Finding 4 (NIT) — tests/bug-2760-codex-install-defensive.test.cjs
rename-failure test tightened from "throw OR warn acceptable" to
assert.equal(threw, true), locking the contract Finding 1 establishes.
Finding 5 (NIT) — bug-2760 test suite snapshots and restores fs.renameSync
defensively in beforeEach/afterEach (symmetric with fs.writeFileSync),
removing the fragile per-test try/finally. Second test in the same
suite cleaned up to drop its try/finally.
Updates tests/codex-config.test.cjs to assert the new namespaced AoT
migration shape via parseTomlToObject (no source-grep). Existing field-
parity test reframed as shape-parity since both paths now emit
namespaced.
Tests: 5764 pass (+8 new). lint:tests: 0 violations.
* docs(#2760): add CHANGELOG entry for Codex install defensive triple
Adds the [Unreleased] Fixed entry for the Codex install fix landed in this
PR — defensive strip of legacy [agents]/[[agents]] blocks, namespaced AoT
hook detection across all events, atomic write + rollback, strict TOML
validation rejecting duplicate keys/repeated headers/trailing bytes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#2769): tolerate Requirements header with colon inside bold delimiters
extractReqIds in sdk/src/query/init.ts and the legacy init.cjs port only
matched `**Requirements**:` (colon outside bold), so phases declared with
the equally-valid markdown form `**Requirements:**` (colon inside bold,
which is what the project's own templates emit) returned phase_req_ids:
null for both `init plan-phase` and `init execute-phase`.
The mirror-image bug in `phase complete`'s REQUIREMENTS.md traceability
sweep at get-shit-done/bin/lib/phase.cjs:871 only matched the inside-bold
form, silently skipping the REQ-ID checkbox flips for any roadmap that
used the outside-bold form. Both parsers now share the same canonical
regex that accepts all three rendered-identical variants:
**Requirements:** (colon inside bold)
**Requirements**: (colon outside bold)
**Requirements** : (space before outside colon)
Tests:
- tests/init.test.cjs — parameterized over the three header variants for
both init plan-phase and init execute-phase (6 new behavioral cases).
- sdk/src/query/init.test.ts — describe.each over the same variants
exercising initPlanPhase through the SDK.
- tests/bug-2769-requirements-header-variants.test.cjs — phase complete
flips REQ-001 in REQUIREMENTS.md across all three header variants.
Closes#2769
* refactor(#2769): centralize REQUIREMENTS_HEADER_RE constant per CodeRabbit
* fix(#2767): pass paths via --files to gsd-sdk query commit + lint guard
Workflows, agents, commands, and references passed file paths positionally
to `gsd-sdk query commit`, which silently appended them to the commit
subject and triggered the `.planning/` wholesale-stage fallback in
sdk/src/query/commit.ts:136. Regression of #733/#798.
Inserted `--files` before the path list at every site (81 invocations
across 50 files). Added tests/bug-2767-gsd-sdk-commit-files-flag.test.cjs
as a permanent lint that scans every shipped .md file and asserts each
`gsd-sdk query commit[-to-subrepo]` invocation either uses `--files` or
carries no path arguments.
Closes#2767
* test(#2767): replace source-grep with behavioral SDK test
The original test walked every shipped .md file and regex-tokenized
`gsd-sdk query commit` invocations to assert `--files` was present.
CONTRIBUTING.md prohibits this source-grep pattern.
Rewrite as behavioral SDK tests against `sdk/dist/cli.js` over a real
tmp git project (createTempGitProject helper). Cover both the
well-formed (`--files <paths>`) form — clean subject, exactly-staged
files, .planning/ left untouched — and the buggy positional form,
asserting the documented misbehavior (paths leak into subject + the
`.planning/` wholesale-stage fallback at commit.ts:136). Also asserts
`commit-to-subrepo` rejects when `--files` is omitted (commit.ts:258).
The doc-lint is retained as a supplementary defense-in-depth guard
since agent-prompt markdown invocations cannot be exercised end-to-end
— but it is no longer the primary contract.
* docs(#2767): correct contradictory --files guidance in zh-CN/en docs + fix test docstring
* fix(#2770): coerce non-string truths to preserve cross-cutting constraints
`cmdRoadmapAnnotateDependencies` skipped non-string truth entries via
`if (typeof t !== 'string') continue`. That avoided the TypeError reported
in #2770 but silently dropped legitimate constraints — numeric YAML scalars
(`- 3`) and kv-shaped truths from parseMustHavesBlock's continuation-kv
path (#2757) — from the cross-cutting analysis, leaving ROADMAP.md
under-annotated.
Replace the skip-guard with a `coerceTruthToString` helper that:
* passes strings through
* `String()`-coerces numbers, booleans, bigints
* extracts a string field (title, text, name, rule, path, provides) from
object-shaped items
Composes cleanly with #2757 (objects from kv continuation lines now
contribute their title rather than being dropped) and the existing
`splitInlineArray` quote-aware parser.
Tests: tests/bug-2770-annotate-deps-int-coerce.test.cjs
- numeric scalar truth shared across plans surfaces as constraint
- kv-shaped truth surfaces via title field
- bare-int depends_on regression guards on extractFrontmatter
Full suite: 5678 pass, 0 fail.
Closes#2770
* test(#2770): use array join() for multi-line fixtures per CONTRIBUTING
* refactor(#2770): cache trim() and avoid no-op truthCounts.set in aggregation
* fix(#2772): only disable worktree isolation when planned paths touch submodules
The previous guard in execute-phase.md and quick.md unconditionally set
USE_WORKTREES=false whenever .gitmodules existed, penalising every plan in
a submodule project even when no plan touched a submodule path.
Replace with submodule-path parsing + per-plan path intersection:
- Parse SUBMODULE_PATHS once from .gitmodules via
`git config --file .gitmodules --get-regexp '^submodule\..*\.path$'`.
- In execute-phase.md, intersect SUBMODULE_PATHS with each plan's
files_modified frontmatter; disable worktree isolation only for plans
with non-empty intersection. Fall back to safe-disable for that plan
when files_modified is missing/unparseable, with a log line explaining
why.
- In quick.md (no pre-declared paths), keep submodule-path parsing and
document a fail-loud commit-time guard so the executor aborts only when
it actually stages a submodule path.
Add tests/bug-2772-gitmodules-path-intersection.test.cjs covering both
files: no unconditional disable, submodule paths are parsed, intersection
logic exists in execute-phase, fallback path is documented.
Full suite: 5680 / 5680 pass.
Closes#2772
* test(#2772): replace source-grep with behavioral test of submodule path intersection
* fix(#2772): wire USE_WORKTREES_FOR_PLAN into dispatch + fix glob matcher + add quick.md commit guard
Address CodeRabbit review on PR #2779 — the original fix computed
USE_WORKTREES_FOR_PLAN but never read it, so the per-plan submodule
intersection was dead code. Dispatch sites still branched on the
project-level USE_WORKTREES.
Changes:
1. execute-phase.md (CRITICAL — dispatch wiring): Move per-plan
computation into execute_waves as sub-step 2.5, run it for each plan
before its dispatch, and gate all four dispatch sites on
USE_WORKTREES_FOR_PLAN: worktree-mode header, sequential-mode header,
"worktrees disabled" sequential rule, and post-wave cleanup. Document
PLAN_FILES extraction via jq from the phase-plan-index JSON. Track
WAVE_WORKTREE_PLANS so post-wave cleanup only runs when at least one
plan in the wave actually used worktrees.
2. Per-plan gate matcher (MAJOR — glob safety): Strip leading "./" and
trailing "/" from both submodule and planned paths. Match
bidirectionally (pf inside sm AND sm inside pf). Handle globby
planned paths like "vendor/**/*.c" by extracting the literal prefix
before the first glob metachar and re-checking. Wrap the iteration
in set -f / set +f so glob expansion does not corrupt patterns.
Extracted the gate (~92 lines) into
workflows/execute-phase/steps/per-plan-worktree-gate.md to keep
execute-phase.md under the 1700-line XL budget.
3. quick.md (CRITICAL — fail-loud guard): Inject SUBMODULE_PATHS into
the executor Task prompt and add a <submodule_commit_guard> bash
block the executor must run before every git commit. The guard
inspects staged paths via `git diff --cached --name-only`, normalizes
paths, and aborts with a clear ABORT message + recovery instruction
("re-run with workflow.use_worktrees=false") when any staged path
falls inside a submodule.
4. tests/bug-2772-gitmodules-path-intersection.test.cjs: 25 tests total.
Updated GATE_SNIPPET to match the new bash matcher. Added
normalization tests (./ prefix, trailing /, glob "vendor/**/*.c",
parent directory, ./ in .gitmodules). Added workflow-markdown
wiring assertions for all 4 dispatch sites + per-plan gate file
extraction. Added quick.md guard tests: prompt injection assertion +
behavioral fixture-repo tests that stage a submodule path and assert
the guard exits non-zero with the ABORT message.
Test count: 5701 pass / 0 fail (was 5698/1 before).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#2774): inclusion-based worktree cleanup to protect workspace .git
The cleanup blocks in execute-phase.md and quick.md used an exclusion
filter (`grep -v "$(pwd)$"`) to skip the current worktree before calling
`git worktree remove --force` on everything else. The exclusion fails
whenever the current workspace is itself a worktree of an upstream repo:
- multi-workspace setups where `git worktree list` reports the registry
path as a different absolute path than `$(pwd)`
- the cross-drive Windows case where the registry reports `E:/...` while
`$(pwd)` resolves to `C:/...` — the equality test never holds, every
other worktree (including the workspace itself) is removed, and the
workspace's `.git` pointer file is destroyed.
Switches both cleanup blocks to an inclusion-based filter that targets
only agent-spawned worktrees under `.claude/worktrees/agent-`, the
namespace Claude Code's `isolation="worktree"` always uses for executor
worktrees. The workspace path can never collide with that prefix.
Adds tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs covering:
- both workflow files use the inclusion filter
- neither falls back to the broken `grep -v "$(pwd)$"` guard
- end-to-end simulation of porcelain output with workspace + agent
worktrees yields only the agent worktree
Closes#2774
* test(#2774): replace source-grep with behavioral test of cleanup pipeline
* fix(#2774): whitespace-safe worktree iteration with while/read
CodeRabbit review on PR #2778 flagged that `for WT in $WORKTREES` splits
on whitespace. Any agent worktree path containing a space (e.g. a workspace
under '/Users/dev/My Workspace/') would be torn into broken half-paths,
`git -C` would fail on each fragment, and the executor branch would never
be deleted.
Switch both cleanup blocks (quick.md and execute-phase.md) to:
while IFS= read -r WT; do
[ -z "$WT" ] && continue
...
done < <(git worktree list --porcelain | grep ... | sed ...)
Process substitution feeds the pipeline output line-by-line — IFS= and -r
preserve every byte of the path including embedded spaces.
Also rename the misleading `makeBareTempGitRepo` helper to
`makeTempUpstreamRepo` (it does not pass --bare; it inits a normal repo
with an initial commit so worktree-add works).
Add two new behavioral tests:
- discovery pipeline yields whitespace paths intact on a single line
- the actual while/read loop iterates each whitespace-bearing path
exactly once (would fail with the previous `for WT in` form)
Tests: 5681 pass, 0 fail.
* fix(#2775): verify gsd-sdk on PATH before reporting SDK ready
`npx get-shit-done-cc@latest` printed `✓ GSD SDK ready` even though
`gsd-sdk` was not callable. Root cause: npx only links the package's
primary bin (`get-shit-done-cc`); secondary bins like `gsd-sdk` are not
materialized into a PATH directory. The installer asserted the weaker
invariant "sdk/dist/cli.js exists on disk" and treated it as proof of
the stronger invariant "command -v gsd-sdk resolves" — they aren't the
same.
Fix tightens the gate in installSdkIfNeeded:
1. After confirming the dist is present, walk PATH for an executable
`gsd-sdk` shim (isGsdSdkOnPath, no spawn).
2. If absent, attempt to materialize the shim via symlink at
`~/.local/bin/gsd-sdk` (or the first HOME-rooted PATH dir we can
write to), falling back to a copy on filesystems that reject
symlinks (trySelfLinkGsdSdk).
3. Re-probe PATH after linking. Only print `✓ GSD SDK ready` when the
probe succeeds; otherwise emit a clear ⚠ + remediation.
Also strips the misleading "or `npx get-shit-done-cc`" clause from the
shim header (it never linked the secondary bin).
Closes#2775
* test(#2775): use centralized helpers from helpers.cjs per CONTRIBUTING
* fix(#2775): wrapper script in symlink fallback to preserve __dirname resolution
CodeRabbit follow-up on PR #2777. The previous symlink-fallback in
trySelfLinkGsdSdk used fs.copyFileSync(shimSrc, target), but
bin/gsd-sdk.js resolves the CLI via path.resolve(__dirname, '..',
'sdk', 'dist', 'cli.js'). After a copy, __dirname becomes the link
directory (e.g. ~/.local/bin), so the resolved CLI path was broken
(~/.local/sdk/dist/cli.js) — and isGsdSdkOnPath() only checked file
existence + execute bit, so the success line still printed over a
broken install.
Replace the copy with a tiny wrapper script that require()s the real
shim by absolute path. This preserves __dirname inside bin/gsd-sdk.js
because the require runs against shimSrc's own location.
Also fixes the PATH restoration nit in the regression test (was
coercing undefined to the string "undefined" if PATH was unset).
Adds a behavioral fallback test that mocks fs.symlinkSync to throw,
exercises the fallback path, and asserts the resulting target is a
require()-wrapper (not a verbatim copy) and is executable.
* fix(#2775): PATH-backed dir ordering + tighten captureConsole + drop tautological assertion (CodeRabbit follow-up)
* fix(#2771): unify user-owned-artifacts list to suppress false patches warning
USER-PROFILE.md was both preserved across reinstalls (correctly) AND tracked in
gsd-file-manifest.json (incorrectly). On the next install, saveLocalPatches()
hashed the on-disk file, found it differed from the stale manifest hash (because
/gsd-profile-user --refresh regenerated it), and reported it as a "locally
modified GSD file" — a spurious warning every time the profile refreshed.
A file is either distribution (manifest-tracked, diff'd against manifest) or
user artifact (preserved across installs, never diff'd). Never both. This
extracts USER_OWNED_ARTIFACTS as a single source of truth, referenced by both
the preserveUserArtifacts call site and writeManifest, so the invariant cannot
drift again.
Adds a regression test that exercises the full reproduction path: install,
create USER-PROFILE.md, reinstall, refresh USER-PROFILE.md, reinstall, assert
no patch backup and no warning text.
Closes#2771
* test(#2771): use centralized helpers from helpers.cjs per CONTRIBUTING
* fix(#2771): normalize legacy USER_OWNED_ARTIFACTS entries from manifest + tighten test
* refactor(state): drop unused args param and lift currentPhase in cmdStateCompletePhase
Two cleanup items surfaced by CodeRabbit review of PR #2759:
1. cmdStateCompletePhase(cwd, args, raw) — args is never read inside the
function. All sibling state subcommands use the leaner (cwd, raw) shape.
Remove the unused parameter and update the dispatch call in gsd-tools.cjs.
2. output() at line 1754 called fs.readFileSync(statePath) after
readModifyWriteStateMd had already released the lock, re-extracting
Current Phase via an extra fs read. The closure already computed
currentPhase at line 1704; lifting resolvedPhase into outer scope and
capturing it in the callback eliminates the post-lock read and closes the
small race window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#2761): apply CodeRabbit nitpicks with regression tests
Two CodeRabbit nitpicks from PR #2761 review, each landed with a
regression test so a future refactor can't unwind them.
1. tests/dispatcher.test.cjs — pin the enumerated subcommand list:
the 'state unknown subcommand errors' test now also asserts that
the dispatcher's error string includes 'complete-phase'. Without
this, a future reformat of the available-subcommands enumeration
could silently drop entries and the existing
'Unknown state subcommand' substring check would still pass.
2. get-shit-done/bin/lib/state.cjs — tighten the Phase fallback in
cmdStateCompletePhase: when STATE.md is missing the canonical
'**Current Phase:**' field and the only phase signal is the
decorated body line under '## Current Position' (e.g.
'Phase: 01 (Foo) — EXECUTING'), the previous fallback returned
the entire decorated string, producing messy downstream output:
Status: Phase 01 (Foo) — EXECUTING complete
Phase: 01 (Foo) — EXECUTING — COMPLETE
The fallback now strips everything past the leading
numeric/decimal token via /^\\s*([\\w.-]+)/ so degraded inputs
produce clean output identical to the canonical path.
3. tests/state.test.cjs — two new tests in a dedicated describe block:
- decorated Phase line writes clean Phase identifier
- canonical Current Phase wins over Current Position decoration
Both run real `gsd state complete-phase` against synthetic
STATE.md fixtures and assert on the rendered Status field.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#2762): add --minimal install profile to cut cold-start token cost
Eager system-prompt load from 86 gsd-* skill descriptions plus 33
subagent descriptions costs ~12k tokens per turn even in directories
with no .planning/. Frontier models (Sonnet 4.6 / Opus 4.7) with 200K-1M
context don't feel it; local LLMs with 32K-128K do.
--minimal (alias --core-only) installs only the main GSD loop:
new-project, discuss-phase, plan-phase, execute-phase, plus help/update.
Zero gsd-* subagents are written. Re-running gsd update without
--minimal expands to the full surface. Default install behavior is
unchanged.
DRY: a single stageSkillsForMode() helper filters the source dir; all
13 runtime-specific copy fns are unchanged because they recurse the
staged dir. Allowlist + helpers live in get-shit-done/bin/lib/install-
profiles.cjs as the single source of truth.
Manifest now records mode: 'minimal' | 'full' so future commands can
detect install profile.
Tested end-to-end: --minimal yields 6 skill folders + 0 agents; default
yields 86 + 33 (unchanged).
* docs(#2762): document --minimal install in README
Adds a collapsible 'Minimal Install' section under Getting Started
covering: who it's for (local LLMs, token-billed APIs), what you get
(6 skills, 0 subagents, ~700 token floor vs ~12k), and the critical
caveat that re-installing without --minimal restores the full surface
and erases the savings. Includes a comparison table, the manifest
inspection one-liner, and the use-case decision matrix.
* fix(#2762): address CodeRabbit review + CI failures
CodeRabbit findings:
1. Temp dir leak (Minor): stageSkillsForMode created tmp dirs that were
never cleaned up. Added a module-level Set tracking every staged dir
plus a process.on('exit') handler that rm -rf's them. Also wrap the
copy loop in try/catch to remove a partially-populated tmp dir on
mid-flight failure. Verified end-to-end: 0 leaked dirs in /tmp after
a real install.
2. Codex full -> minimal stale state (Major): a previous full Codex
install left agents/gsd-*.toml files plus [agents.gsd-*] sections in
config.toml. The original cleanup only removed .md files, so a switch
to --minimal would leave Codex still advertising the full agent
surface. Cleanup now also handles .toml under isCodex, and minimal
mode strips GSD sections from config.toml via the existing
stripGsdFromCodexConfig helper (same path used by --uninstall).
3. Nitpick — Codex downgrade regression test: added a spawnSync-based
end-to-end test that fakes a previous full install (stale gsd-*.md +
gsd-*.toml + GSD-marked config.toml + a user-owned agent/setting),
runs install.js --codex --minimal, and asserts stale GSD files +
sections are gone while user content is preserved.
CI failures (inventory parity):
- docs/INVENTORY.md CLI Modules table now lists install-profiles.cjs
with the correct headline count (30 -> 31).
- docs/INVENTORY-MANIFEST.json regenerated via gen-inventory-manifest.cjs.
Test count: 149 pass (was 116 in last commit; +14 new install-minimal +
all previously-failing inventory tests now green).
* test(#2762): expand install-minimal test coverage for future-proofing
Each new test pins a specific guarantee that closes off a future
regression class — turning every CodeRabbit finding (including the
nitpicky one) into a permanent guard.
cleanupStagedSkills suite (+3 tests):
- 'full mode does not register a staged dir' — catches a future
regression where someone forgets the early-return in stageSkillsForMode
and starts polluting STAGED_DIRS in default installs.
- 'exit handler registers exactly once across many calls' — catches
removal of the exitHandlerRegistered guard. install.js has 13
dispatch sites, so a missing guard would attach 13 listeners.
- 'mid-copy failure removes partial staged dir and re-throws' —
intercepts fs.copyFileSync to throw mid-loop and asserts the staged
dir count in /tmp is unchanged after the throw. Pins the exact
CodeRabbit-flagged leak.
Claude full -> minimal downgrade (+1 test):
- Mirrors the Codex downgrade test for the .md-only path that the
other 12 runtimes share. Asserts user-owned agents are preserved.
Manifest mode round-trip (+3 tests):
- Default install -> mode: 'full' with >6 skills and >0 agents
- --minimal -> mode: 'minimal' with exactly 6 skills and 0 agents
- --core-only alias produces identical manifest to --minimal
Allowlist scope guards (+3 tests):
- Every main-loop command IS in allowlist (positive)
- Off-loop commands (autonomous, ship, do, progress, next, fast,
quick, debug, code-review, verify-work) are NOT (guards against
silent scope creep — future contributor adds 'autonomous' to core
and the floor erodes)
- Unknown mode strings fall through to full behavior — pre-emptive
guard for future 'compact'/'tier2' modes that might forget to
update the predicate.
Total: 25 tests in this file (was 15), 159/159 passing across the
install + inventory suites.
* fix(#2762): clean up staged tmp dirs on SIGINT/SIGTERM/SIGHUP
CodeRabbit follow-up review on c727bf5f flagged that process.on('exit')
does not fire on signal-driven termination. An installer is exactly
the kind of process users abort mid-run with Ctrl+C, so without
explicit signal handlers the staged tmp dirs in STAGED_DIRS would be
left behind until the OS reaps tmpdir.
Fix: ensureExitCleanup now also registers process.once handlers for
SIGINT, SIGTERM, SIGHUP. Each handler runs cleanupStagedSkills then
re-raises the same signal via process.kill(pid, sig) so the OS-default
handler takes over and the parent shell sees the correct exit code
(130 for SIGINT, etc.) — CI scripts and interactive users see the
abort the way they expect.
Test: spawns a child that stages a tmp dir then blocks; parent
captures the staged path from stdout, sends SIGINT, asserts (a) the
staged dir is gone after child exit, (b) child exits via the signal
not via code 0. Skipped on Windows (signal semantics differ; the
natural-exit cleanup test covers the Windows CI matrix).
Total: 26 tests in install-minimal.test.cjs (was 25).
parseMustHavesBlock dispatched on `includes(':')` to detect key-value pairs,
but unquoted YAML strings like `GET /foo/:id resolves...` and
`Class::Method is idempotent` also contain colons. When the KV regex failed
to match, `current` was left as `{}` (the empty object initialized before
the branch), which then caused `t.trim()` in roadmap.cjs to throw
`TypeError: t.trim is not a function`.
Two fixes:
- frontmatter.cjs: tighten the KV regex to require at least one space after
the colon (`\s+` instead of `\s*`), matching YAML convention. When the
regex still fails to match, fall back to treating the item as a plain
string instead of leaving `current` as `{}`.
- roadmap.cjs: add `typeof t !== 'string'` guard before `.trim()` as a
cheap safety net against any future parser anomaly.
Closes#2757
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: parseMustHavesBlock quoted strings + gsd state complete-phase
Bug #2734: parseMustHavesBlock dropped quoted truths containing ':' because
fully-quoted strings like `"App-side UUIDv4: generated locally"` fell into
the kv-parse branch, the regex failed (value starts with '"'), and current
stayed as empty {}. Fix: detect fully-quoted strings before the ':' check
and extract them directly. Two regression tests added to frontmatter.test.cjs.
Bug #2735: `gsd state complete-phase` subcommand was missing — unknown
subcommands fell through to cmdStateLoad. Added cmdStateCompletePhase to
state.cjs (updates Status, Last Activity, and Current Position to COMPLETE),
exported it, and wired it into the case 'state': dispatch in gsd-tools.cjs.
Closes#2734Closes#2735
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(state): unknown subcommand returns explicit error instead of silent fallthrough
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#2612
- Add gemini, qwen, opencode, and copilot entries to RUNTIME_PROFILE_MAP in core.cjs
- Group B runtimes (kilo, cline, cursor, windsurf, augment, trae, codebuddy, antigravity) intentionally have no built-in map and fall through to the existing unknown-runtime fallback
- Add 40 new tests to tests/issue-2517-runtime-aware-profiles.test.cjs covering each new runtime's three tiers, Group B fall-through, and partial override merge semantics
- Add Section 7 "Runtime Model Tiers" to settings-advanced.md with interactive UI to view and override built-in tier defaults per runtime
- Update docs/CONFIGURATION.md built-in tier table to include all four new runtimes
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new slash command that lets developers modify any field of an
existing phase in ROADMAP.md without affecting phase number or position.
- commands/gsd/edit-phase.md: command file with --force flag support
- get-shit-done/workflows/edit-phase.md: full workflow with status guard,
depends_on validation, diff+confirmation, and STATE.md update
- tests/edit-phase.test.cjs: 32 tests covering all acceptance criteria
- docs/INVENTORY.md, INVENTORY-MANIFEST.json, COMMANDS.md: registered
Closes#2617
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two tests to review-model-config.test.cjs:
- isValidConfigKey accepts review.models.claude (schema validation)
- round-trip: config-set then config-get for review.models.claude
The dynamic key pattern (^review\.models\.[a-zA-Z0-9_-]+$), the workflow
model-read logic in review.md, and the CONFIGURATION.md docs were already
in place. Only the claude-specific test coverage was missing.
Closes#2688
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Codex 0.124.0 changed the required config.toml hooks format from the old
map-style ([hooks.shell]) to array-of-tables ([[hooks]]). Old GSD installs
that wrote the legacy format now cause a startup parse error on upgrade.
Add migrateCodexHooksMapFormat() which detects non-array [hooks] and
[hooks.TYPE] sections and rewrites them to [[hooks]] entries with an
injected type = "TYPE" key. The migration runs at the start of every Codex
install so affected configs self-heal on the next `gsd install --codex`.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(config): bump MODEL_ALIAS_MAP and RUNTIME_PROFILE_MAP to claude-opus-4-7
Opus 4.7 shipped Q1 2026 but MODEL_ALIAS_MAP and RUNTIME_PROFILE_MAP.claude.opus
were still pinned to claude-opus-4-6. Users with resolve_model_ids: true received
stale model IDs in logs and agent-tool calls.
Also adds a resolve_model_ids: true test suite — this path had zero coverage,
which is why the stale ID survived undetected.
Closes#2712
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(config): derive RUNTIME_PROFILE_MAP.claude from MODEL_ALIAS_MAP (coderabbit)
RUNTIME_PROFILE_MAP.claude was duplicating model IDs that MODEL_ALIAS_MAP
already owns. Future model bumps now only require updating MODEL_ALIAS_MAP.
Also fixes stale test assertion (claude-opus-4-6 → claude-opus-4-7).
* fix(tests): update stale claude-opus-4-6 refs to claude-opus-4-7; DRY: derive RUNTIME_PROFILE_MAP.claude from MODEL_ALIAS_MAP
- Update 3 hardcoded `claude-opus-4-6` assertions in tests/issue-2517-runtime-aware-profiles.test.cjs to `claude-opus-4-7`
- Update comment on line 128 that referenced the old model ID
- Replace manual per-tier expansion of RUNTIME_PROFILE_MAP.claude with Object.fromEntries so future alias bumps only require updating MODEL_ALIAS_MAP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Success path: add explicit python3Calls.length === 0 assertion so "no
fallback" is stated directly rather than implied by calls.length === 1
- Fallback path: add explicit calls[0].cmd === 'graphify' assertion so
"graphify precedes python3" is verified by name, not just argument
- Add _deepMergeConfig() with correct null-override semantics
- loadConfig() reads root config.json when GSD_WORKSTREAM is set, then
deep-merges with workstream config (workstream wins on conflict)
- Workstream without config.json falls back to root config entirely
- Migrations and disk writes operate on fileData (on-disk content) only,
never on the merged result, to prevent workstream pollution
- Fixes null-override bug from PR #2717: explicit null in workstream now
correctly overrides root value instead of falling back to root
- Tests: inherit root model_overrides, workstream override, nested
workflow.* deep merge, explicit null override, missing workstream config
Closes#2714
graphify . --update was removed in favor of graphify update . in v0.4.x.
Also improves version detection to try `graphify --version` before
falling back to python3 importlib query.
Closes#2732
* fix(#2722): forensics gh commands pin --repo gsd-build/get-shit-done
gh issue create and gh label list both defaulted to the repo inferred
from $PWD, causing issues to be submitted to the user's current project
instead of this repo. Added --repo gsd-build/get-shit-done to both
commands. Added two regression tests covering both gh calls.
Closes#2722
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2723): scope forensics tests to specific gh commands, not whole file
CodeRabbit found that the gh issue create test searched the whole
workflow file, so it would pass even if gh issue create lacked --repo
(because gh label list already contains the repo string elsewhere).
Also replaced the brittle 200-char slice in the label-list test with
a regex. Both tests now use assert.match() with command-scoped regexes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
377a6d2 deleted sdk/prompts/agents/ and sdk/prompts/workflows/ (13 files)
but did not update 3 test files that reference them, causing ENOENT
failures on every CI run (main and all PRs) since that commit.
Removed:
- sdk/prompts/agents variants describe block (enh-2427-sycophancy-hardening)
- PLAN_PHASE_SDK_PATH constant and headless plan-phase test (post-planning-gaps-2493)
- sdk/prompts/workflows/verify-phase.md describe block (verifier-deferred-items)
The underlying behaviour is covered by the existing main agent/workflow
tests; the SDK variant tests are moot now that the SDK loads installed
files instead of bundled stripped-down copies.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#2500): enrich gsd-codebase-mapper arch-focus ARCHITECTURE.md template
The codebase mapper's arch-focus template was a sparse structural inventory.
After major refactors, the research/ARCHITECTURE.md (created at /gsd-new-project
and never refreshable) went stale while the refreshable codebase version lacked
the visual richness that makes architecture docs useful for planning.
Add to the ARCHITECTURE.md template:
- <!-- refreshed: {date} --> marker at the top (maintainer request)
- ASCII system overview diagram with component boxes and flow arrows
- Component responsibility table (Component / Responsibility / File)
- Primary request path traces with numbered steps and code references
- Architectural constraints section (threading, global state, circular imports)
- Anti-patterns section with codebase-specific patterns and correct alternatives
All existing sections (Pattern Overview, Layers, Key Abstractions, Entry Points,
Error Handling, Cross-Cutting Concerns) are preserved.
7 new tests in tests/enh-2500-codebase-mapper-arch-rich-format.test.cjs verify
each required section is present in the deployed template.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2500): resolve CodeRabbit review findings
- Add 'text' language tag to bare ASCII diagram fenced block (markdownlint MD040)
- Tighten data flow test: require '### Primary Request Path' heading, 3+
numbered steps, and file:line reference pattern — prevents loose-match
false positives
- Tighten constraints test: require '## Architectural Constraints' heading
AND Threading / Global state / Circular imports tokens — prevents broad
keyword matches masking regressions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#2306): plan-review-convergence v2 — CYCLE_SUMMARY contract, config gate, local model reviewers
Fixes the false-stall detection bug in the plan→review→replan convergence
loop. REVIEWS.md accumulates history across cycles so raw grep inflated
HIGH counts; HIGH count now comes from a per-cycle CYCLE_SUMMARY contract
emitted in the review agent's return message.
Key changes:
- workflow.plan_review_convergence config gate (disabled by default, same
pattern as workflow.code_review / workflow.nyquist_validation)
- Review agent prompt defines CYCLE_SUMMARY: current_high=<N> contract with
PARTIALLY RESOLVED / FULLY RESOLVED counting rules
- Orchestrator aborts on absent/malformed CYCLE_SUMMARY (distinguishes both)
- Warns when HIGH_COUNT > 0 but ## Current HIGH Concerns section is missing
- Stall detection and --ws forwarding preserved and tested
- Local model reviewers: --ollama, --lm-studio, --llama-cpp flags added to
convergence workflow and review workflow; all three use OpenAI-compatible
/v1/chat/completions endpoint with jq --rawfile for safe JSON encoding
- review.ollama_host / review.lm_studio_host / review.llama_cpp_host config
keys registered and documented (default to localhost:11434/1234/8080)
- review.models.ollama / .lm_studio / .llama_cpp model-name config support
- 58 tests (up from 29 in PR #2339), all passing
Closes#2306Closes#2339
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): sync sdk/src/query/config-schema.ts with CJS schema (#2306)
Add workflow.plan_review_convergence, review.ollama_host,
review.lm_studio_host, and review.llama_cpp_host to the SDK-side
TypeScript mirror — required by the CJS↔SDK parity test (#2653).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2306): resolve CodeRabbit review findings
- Anchor HIGH_COUNT extraction with head -1 to prevent multi-match when
agent return message contains multiple CYCLE_SUMMARY lines (e.g. quoted
back from prompt context)
- Replace hardcoded reviewers list in REVIEWS.md frontmatter template with
runtime-derived placeholder — the static list did not reflect which
reviewers were actually invoked
- Broaden workflow.plan_review_convergence docs to include local reviewers
(Ollama, LM Studio, llama.cpp) alongside cloud reviewers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): restore reviewers frontmatter list with runtime note
The cursor-reviewer.test.cjs (and equivalent per-reviewer tests) assert
that each supported reviewer appears on the reviewers: line — these are
wiring tests that catch when a new reviewer is added to invocation but
not to the REVIEWS.md template. Replacing the list with a placeholder
broke those tests.
Restore the full static list and add an inline comment clarifying that
the actual committed frontmatter should be filtered to only the reviewers
invoked that run — satisfying both the per-reviewer tests and the
CodeRabbit correctness note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#2698 — The two separate LF/CRLF .replace() calls in the Codex hooks
migration could not handle mixed line endings (e.g. header in LF, body in
CRLF), leaving stale gsd-update-check blocks after reinstall. Consolidated to
a single \r?\n-aware regex with gm flags that handles LF, CRLF, and mixed
content in one pass.
Fixes#2678 — installSdkIfNeeded() called process.exit(1) unconditionally when
sdk/dist/cli.js was missing, even during --local installs where users cannot
write to global node_modules. Added isLocal option: when true, prints a warning
and returns instead of exiting.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2686): review-fix agent now uses git worktree for isolation
The gsd-code-fixer agent operated directly against the main working tree,
racing any concurrent foreground session for HEAD, the index, and on-disk
files. Added a setup_worktree step (git worktree add /tmp/sv-N-reviewfix
HEAD) as the first action before any file operations, with unconditional
git worktree remove cleanup on exit. Mirrors the pattern used by all other
GSD per-issue agents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2686): address CodeRabbit review — mktemp unique path, branch-aware worktree, tighten test assertions
- Use mktemp -d for unique worktree path (prevents concurrent-run collision)
- Resolve branch via git branch --show-current before worktree add (prevents detached HEAD)
- Error-and-exit on worktree add failure instead of force-removing shared path
- Test: use .exec().index for checkout position (not indexOf on match string)
- Test: match gsd-sdk query commit as well as git commit for ordering assertion
- Test: tighten /tmp path assertion to require actual /tmp/sv- assignment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#2692): add behavioral --wave N test, annotate source-text assertions
Adds two behavioral tests for wave filtering via phase-plan-index:
- Verifies plans with wave frontmatter are correctly grouped by wave number
- Verifies plans with no wave field default to wave 1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2684,#2676): milestone.complete version validation + parallel milestone phase routing
#2684: Confirms milestone.complete correctly validates and uses its version
argument end-to-end. The inline archive path in milestoneComplete already
forwarded version correctly; regression tests lock in that contract.
#2676: phase.complete applied getMilestonePhaseFilter unconditionally, using
STATE.md's primary milestone to scope the candidate set. When the completed
phase belongs to a parallel (secondary) milestone, the filter excluded all
phases from that milestone, leaving an empty candidate set and incorrectly
returning is_last_phase: true / next_phase: null.
Fix: before applying the milestone filter in Step E, check whether the
completed phase itself appears in the filtered set. If not, skip the filter
for both the directory scan and the ROADMAP.md fallback so phases from the
secondary milestone remain visible for next-phase detection.
Closes#2684Closes#2676
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
VALID_PROFILES was derived solely from Object.keys(MODEL_PROFILES['gsd-planner']),
which only contained the named tiers (quality/balanced/budget/adaptive). The
cmdConfigSetModelProfile validator rejected 'inherit' even though the runtime has
supported it since #1829. Fix: append 'inherit' to VALID_PROFILES and handle it
in getAgentToModelMapForProfile so the agent→model table shows 'inherit' instead
of undefined.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two behavioral tests for wave filtering via phase-plan-index:
- Verifies plans with wave frontmatter are correctly grouped by wave number
- Verifies plans with no wave field default to wave 1
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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 990c3e64), and CI reference
- allow-test-rule exemption table (6 recognized categories with when-to-use)
- "CI Test Quality Checks" table showing lint-tests job and local run command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve CodeRabbit findings on PR #2700
- CONTRIBUTING.md: "four recognized categories" → "six" (table has 6 rows)
- workspace.test.cjs: use positional args in routing tests (no --name flag)
- lint-no-source-grep.cjs: add source-dir guard to READ_WITH_INLINE_CJS_RE
(mirrors CJS_PATH_CONST_RE's protection against false positives on temp files)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): tighten allow-test-rule and add recursive test discovery
- ALLOW_ANNOTATION now requires at least one non-whitespace char after the
colon so bare '// allow-test-rule:' cannot bypass the lint gate
- findTestFiles() recurses into subdirectories so nested *.test.cjs files
are covered if the tests/ tree ever grows subdirs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
All workflow, command, reference, template, and tool-output files that
surfaced /gsd:<cmd> as a user-typed slash command have been updated to
use /gsd-<cmd>, matching the Claude Code skill directory name.
Closes#2697
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: destroy 9 config-schema.cjs/core.cjs source-grep tests, add behavioral config-set tests (#2691, #2693)
Replace source-grep theater with config-set behavioral tests:
- execute-phase-wave: config-set workflow.use_worktrees replaces VALID_CONFIG_KEYS grep
- inline-plan-threshold: delete redundant source-grep (behavioral test at L36 already covered it)
- plan-bounce: config-set for plan_bounce / plan_bounce_script / plan_bounce_passes replaces 3 key-presence greps
- code-review: config-set for code_review / code_review_depth replaces 2 greps; removes CONFIG_PATH constant
- thinking-partner: config-set features.thinking_partner replaces two greps (config-schema.cjs AND core.cjs)
Behavioral tests survive refactors (no path constants, no file reads). The config-schema.cjs →
core.cjs migration commit 990c3e64 happened because these tests groped source paths.
Add allow-test-rule: source-text-is-the-product annotations to legitimate product-content tests:
autonomous-allowed-tools, agent-frontmatter, agent-skills-awareness, bug-2334, bug-2346,
execute-phase-wave (MD reads), plan-bounce (workflow reads). Annotations explain WHY text
inspection is the right level of testing for AI instruction files.
Closes#2691Closes#2693
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: address CodeRabbit findings on #2696
- agent-frontmatter.test.cjs: move allow-test-rule annotation from block comment
to standalone // line comment so rule scanners can detect it
- thinking-partner.test.cjs: strengthen config-set test with config-get read-back
assertion to verify the value was persisted, not just accepted (exit 0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: tighten thinking_partner config assertion per CodeRabbit (#2696)
Replace config-get output substring check (includes('true') false-positive
risk) with a direct JSON read of .planning/config.json, asserting the
exact persisted value via strictEqual. This also validates the config file
was created, catching silent key-acceptance without persistence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2661): unconditional plan-checkbox sync in execute-plan
Checkpoint A in execute-plan.md was wrapped in a "Skip in parallel mode"
guard that also short-circuited the parallelization-without-worktrees
case. With `parallelization: true, use_worktrees: false`, only
Checkpoint C (phase.complete) then remained, and any interruption
between the final SUMMARY write and phase complete left ROADMAP.md
plan checkboxes stale.
Remove the guard: `roadmap update-plan-progress` is idempotent and
atomically serialized via readModifyWriteRoadmapMd's lockfile, so
concurrent invocations from parallel plans converge safely.
Checkpoint B (worktree-merge post-step) and Checkpoint C
(phase.complete) become redundant after A is unconditional; their
removal is deferred to a follow-up per the RCA.
Closes#2661
* fix(#2661): gate ROADMAP sync on use_worktrees=false to preserve single-writer contract
Adversarial review of PR #2682 found that unconditionally removing the
IS_WORKTREE guard violates the single-writer contract for shared
ROADMAP.md established by commit dcb50396 (PR #1486). The lockfile only
serializes within a single working tree; separate worktrees have
separate ROADMAP.md files that diverge.
Restore the worktree guard but document its intent explicitly: the
in-handler sync runs only when use_worktrees=false (the actual #2661
reproducer). Worktree mode relies on the orchestrator's post-merge
update at execute-phase.md lines 815-834, which is the documented
single-writer for shared tracking files.
Update tests to assert both branches of the gate:
- use_worktrees: false mode runs the sync (the #2661 case)
- use_worktrees: true mode does NOT run the in-handler sync
- handler-level idempotence and lockfile contention tests retained,
scope clarified to within-tree concurrency only
* fix(#2660): capture prose after label in extractOneLinerFromBody
The regex `\*\*([^*]+)\*\*` matched the first bold span, so for the new
SUMMARY template `**One-liner:** Real prose here.` it captured the label
`One-liner:` instead of the prose. MILESTONES.md then wrote bullets like
`- One-liner:` with no content.
Handle both template forms:
- Labeled: `**One-liner:** prose` → prose
- Bare: `**prose**` → prose (legacy)
Empty prose after a label returns null so no bogus bullets are emitted.
Note: existing MILESTONES.md entries generated under the bug are not
regenerated here — that is a follow-up.
Closes#2660
* fix(#2660): normalize CRLF before one-liner extraction
Windows-authored SUMMARY files use CRLF line endings; the LF-only regex
in extractOneLinerFromBody would fail to match. Normalize \r\n and \r
to \n before stripping frontmatter and matching the one-liner pattern.
Adds test case (h) covering CRLF input.
* fix(#2659): qualify bare output() calls in audit-open handler
The audit-open dispatch case in bin/gsd-tools.cjs previously called bare
output() on both --json and text branches, which crashed with
ReferenceError: output is not defined. The core module is imported as
`const core`, so every other case uses core.output(). HEAD already
qualifies the calls correctly; this commit adds a regression test that
invokes `audit-open` and `audit-open --json` through runGsdTools and
asserts a clean exit plus non-empty stdout (and an explicit check that
the failure mode is not ReferenceError). The test fails on any revision
where either call reverts to bare output().
Closes#2659
* test(#2659): assert valid JSON output in --json mode
CodeRabbit nit: tighten --json regression coverage by parsing stdout
and asserting the result is a JSON object/array, not just non-empty.
* fix(#2636): surface gsd-sdk query failures and add workflow↔handler parity check
Root cause: workflows invoked `gsd-sdk query agent-skills <slug>` with a
trailing `2>/dev/null`, swallowing stderr and exit code. When the installed
`@gsd-build/sdk` npm was stale (pre-query), the call resolved to an empty
string and `agent_skills.<slug>` config was never injected into spawn
prompts — silently. The handler exists on main (sdk/src/query/skills.ts),
so this is a publish-drift + silent-fallback bug, not a missing handler.
Fix:
- Remove bare `2>/dev/null` from every `gsd-sdk query agent-skills …`
invocation in workflows so SDK failures surface to stderr.
- Apply the same rule to other no-fallback calls (audit-open, write-profile,
generate-* profile handlers, frontmatter.get in commands). Best-effort
cleanup calls (config-set workflow._auto_chain_active false) keep
exit-code forgiveness via `|| true` but no longer suppress stderr.
Parity tests:
- New: tests/bug-2636-gsd-sdk-query-silent-swallow.test.cjs — fails if any
`gsd-sdk query agent-skills … 2>/dev/null` is reintroduced.
- Existing: tests/gsd-sdk-query-registry-integration.test.cjs already
asserts every workflow noun resolves to a registered handler; confirmed
passing post-change.
Note: npm republish of @gsd-build/sdk is a separate release concern and is
not included in this PR.
* fix(#2636): address review — restore broken markdown fences and shell syntax
The previous commit's mass removal of '2>/dev/null' suffixes also
collapsed adjacent closing code fences and 'fi' tokens onto the
command line, producing malformed markdown blocks and 'truefi' /
'true fi' shell syntax errors in the workflows.
Repaired sites:
- commands/gsd/quick.md, thread.md (frontmatter.get fences)
- workflows/complete-milestone.md (audit-open fence)
- workflows/profile-user.md (write-profile + generate-* fences)
- workflows/verify-work.md (audit-open --json fence)
- workflows/execute-phase.md (truefi -> true / fi)
- workflows/plan-phase.md, discuss-phase-assumptions.md,
discuss-phase/modes/chain.md (true fi -> true / fi)
All 5450 tests pass.
* fix(#2645): emit [[agents]] array-of-tables in Codex config.toml
Codex ≥0.116 rejects `[agents.<name>]` map tables with `invalid type:
map, expected a sequence`. Switch generateCodexConfigBlock to emit
`[[agents]]` array-of-tables with an explicit `name` field per entry.
Strip + merge paths now self-heal on reinstall — both the legacy
`[agents.gsd-*]` map shape (pre-#2645 configs) and the new
`[[agents]]` with `name = "gsd-*"` shape are recognized and replaced,
while user-authored `[[agents]]` entries are preserved.
Fixes#2645
* fix(#2645): use TOML-aware parser to strip managed [[agents]] sections
CodeRabbit flagged that the prior regex-based stripper for [[agents]]
array-of-tables only matched headers at column 0 and stopped at any line
beginning with `[`. An indented [[agents]] header would not terminate the
preceding match, so a managed `gsd-*` block could absorb a following
user-authored agent and silently delete it.
Replace the ad-hoc regex with the existing TOML-aware section parser
(getTomlTableSections + removeContentRanges) so section boundaries are
authoritative regardless of indentation. Same logic applies to legacy
[agents.gsd-*] map sections.
Add a comprehensive mixed-shape test covering multiple GSD entries (both
legacy map and new array-of-tables, double- and single-quoted names)
interleaved with multiple user-authored agents in both shapes — verifies
all GSD entries are stripped and every user entry is preserved.
installCodexConfig() applied a narrow path-only regex pass before
generateCodexAgentToml(), skipping the convertClaudeToCodexMarkdown() +
neutralizeAgentReferences(..., 'AGENTS.md') pipeline used on the .md emit
path. Result: emitted Codex agent TOMLs carried stale Claude-specific
references (CLAUDE.md, .claude/skills/, .claude/commands/, .claude/agents/,
.claudeignore, bare "Claude" agent-name mentions).
Route the TOML path through convertClaudeToCodexMarkdown and extend that
pipeline to cover bare .claude/<subdir>/ references and .claudeignore
(both previously unhandled on the .md path too). The $HOME/.claude/
get-shit-done prefix substitution still runs first so the absolute Codex
install path is preserved before the generic .claude → .codex rewrite.
Regression test: tests/issue-2639-codex-toml-neutralization.test.cjs —
drives installCodexConfig against a fixture containing every flagged
marker and asserts the emitted TOML contains zero CLAUDE.md / .claude/
/ .claudeignore occurrences and that Claude Code / Claude Opus product
names survive.
Fixes#2639
Flat-skills installs write SKILL.md files under gsd-<cmd>/ dirs, but
Claude Code resolves skills by their frontmatter `name:`, not directory
name. PR #2595 normalized every `/gsd-<cmd>` to `/gsd:<cmd>` across
workflows — including inside `Skill(skill="...")` args — but the
installer still emitted `name: gsd-<cmd>`, so every Skill() call on a
flat-skills install resolved to nothing.
Fix: emit `name: gsd:<cmd>` (colon form) in
`convertClaudeCommandToClaudeSkill`. Keep the hyphen-form directory
name for Windows path safety.
Codex stays on hyphen form: its adapter invokes skills as `$gsd-<cmd>`
(shell-var syntax) and a colon would terminate the variable name.
`convertClaudeCommandToCodexSkill` uses `yamlQuote(skillName)` directly
and is untouched.
- Extract `skillFrontmatterName(dirName)` helper (exported for tests).
- Update claude-skills-migration and qwen-skills-migration assertions
that encoded the old hyphen emission.
- Add `tests/bug-2643-skill-frontmatter-name.test.cjs` asserting every
`Skill(skill="gsd:<cmd>")` reference in workflows resolves to an
emitted frontmatter name.
Full suite: 5452/5452 passing.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
loadConfig's multiRepo migration and filesystem-sync writers targeted the
top-level parsed.sub_repos, but KNOWN_TOP_LEVEL (the unknown-key validator's
allowlist) only recognizes planning.sub_repos (canonical per #2561). Each
migration/sync therefore persisted a key the next loadConfig call warned was
unknown.
Redirect both writers to parsed.planning.sub_repos, ensuring parsed.planning
is initialized first. Also self-heal legacy/buggy installs by stripping any
stale top-level sub_repos on load, preserving its value as the
planning.sub_repos seed if that slot is empty.
Tests cover: (a) canonical planning.sub_repos emits no warning, (b) multiRepo
migration writes to planning.sub_repos with no top-level residue,
(c) filesystem sync relocates to planning.sub_repos, (d) stale top-level
sub_repos from older buggy installs is stripped on load.
Closes#2638
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