* 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>
* perf: convert discuss-phase @file imports to lazy per-branch reads
Replace eager @file directives in <execution_context> with on-demand
Read calls gated behind mode routing. discuss-phase-assumptions.md is
now only read when DISCUSS_MODE=assumptions; discuss-phase.md is only
read for the default discuss mode; discuss-phase-power.md and
templates/context.md are removed from the entry point entirely
(power mode is handled inside discuss-phase.md's lazy mode dispatch;
context.md is loaded at the write_context step).
Reduces tokens loaded at skill entry from ~13k to near zero.
Closes#2606
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(discuss-phase): use contiguous 'Read and execute' phrase in process block
The test at tests/discuss-mode.test.cjs:45 asserts that the <process>
block contains 'Read and execute' as a literal substring. The prior
wording split the instruction across two lines (Read(...) / Then execute),
so the substring match failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(discuss-phase): restore discuss-phase-power reference in process block
---------
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>
* feat: post-merge build & test gate — Build step, iOS/Xcode, serial mode
Step 5.6 of execute-phase is extended per #2720:
- Renamed from "Post-merge test gate (parallel mode only)" to "Post-merge build & test gate"
- Gate now runs in both parallel mode (after worktree merge) and serial mode (after last plan)
- Added Step A: Build gate resolving BUILD_CMD from workflow.build_command config key, then auto-detecting via priority: config override → Xcode (.xcodeproj) → Makefile build: → Justfile → Cargo/Go/Python/npm. Xcode uses xcodebuild -list -json to get first scheme, then xcodebuild build -scheme ... -destination 'platform=iOS Simulator,name=iPhone 16'. Build failure increments WAVE_FAILURE_COUNT.
- Added Xcode/iOS detection to Step B (Test gate): when *.xcodeproj present and no workflow.test_command configured, uses xcodebuild test instead of the previous "no test runner detected" skip. Scheme reused from Step A when available.
- Documented workflow.build_command and workflow.test_command in docs/CONFIGURATION.md (table + JSON schema)
Closes#2720
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(execute-phase): extract Step 5.6 body to post-merge-gate.md sub-file
Moves the build-detection logic and xcodebuild commands from the inline
Step 5.6 body into execute-phase/steps/post-merge-gate.md, replacing it
with a single Read() reference. Reduces execute-phase.md from 1755 to
1647 lines, satisfying the ≤1700 XL-tier budget enforced by
tests/workflow-size-budget.test.cjs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a concrete single-phase walkthrough (webhook validator project)
showing ROADMAP.md, CONTEXT.md, PLAN.md, SUMMARY.md, and STATE.md
excerpts and how each command consumes the previous step's output.
Also adds links to the walkthrough from README.md's nav bar and
How It Works section.
Closes#2359
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>
Adds ORCHESTRATOR RULE blockquotes immediately after every Task() spawn
in 26 GSD workflow files, instructing the parent orchestrator to stop
working on the task while the subagent is active. This prevents the
parallel-work anti-pattern on Codex runtime where the parent continues
reading files and producing duplicate/conflicting output after spawning.
Rules are placed inline at each spawn point (not as generic headers)
so they are adjacent to and unambiguously associated with each Task()
call. Background Task() spawns get a variant noting not to return to
the spawning context until the subagent reports back.
Closes#2729
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: validate LM Studio model identity in review workflow
Capture the full API response before extracting content, then compare
the top-level `.model` field against the configured LM_STUDIO_MODEL.
Emits a warning to stderr if LM Studio served a different model than
requested, while still proceeding with the review response.
Closes#2721
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(review): skip LM Studio review file when content is empty instead of writing error text
Also applies the same fix to llama.cpp which had the identical pattern of writing
a literal error string into the review temp file when content was empty/null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When building nested config sections (workflow, git, hooks, agent_skills,
features), the deep merge was missing globalDefaults for those sections,
causing user values from ~/.gsd/defaults.json to be silently dropped.
Added globalDefaults spread at the correct precedence level (hardcoded <
globalDefaults < userChoices) for all five nested keys, and added three
test cases verifying the merge works end-to-end via HOME env var override.
Closes#2673
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>
The previous guard `if (after.trim().length > 0)` incorrectly triggered
when `after` contained only footer text (e.g. `---\n*Last updated*`).
In that case `after.replace(pattern, replacement)` is a no-op and the
function returned unchanged content instead of falling through to the
slow path that searches inside the last `<details>` block.
Fix: capture the replaced string first, then only take the fast path
when the replacement actually changed `after`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a case where the active milestone is the last <details> block but
footer text (--- / *Last updated*) follows </details>, triggering the
fast-path to replace in the footer instead of inside the block.
Closes#2743
- 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
resolveModel ignored _workstream, unlike configGet/configPath which both
forward it to planningPaths/loadConfig. Different workstreams may have
different model_profile settings.
Addresses coderabbit finding on PR #2742.
Filesystem fallback regex /^(\d+)-/ missed directories like CK-45-foundation
when project_code is configured. Updated to /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/i.
Addresses coderabbit finding on PR #2737.
Applies the same [ \t]* + section-boundary lookahead fix that was applied
to planCountPattern in phase-lifecycle.ts. roadmap.update-plan-progress
shared the same corruption vector via \s* crossing newlines.
Addresses coderabbit finding on PR #2736.
- 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
All 18+ query handlers accepted _workstream but never forwarded it to
planningPaths/loadConfig/getMilestoneInfo. Remove _ prefix and pass
workstream to all internal helper calls so --ws flag actually scopes
path resolution.
Affected handlers: initNewProject, initProgress, initManager, configGet,
configPath, configSet, configSetModelProfile, configNewProject,
configEnsureSection, validateHealth, commit, checkCommit, commitToSubrepo.
Also fixes validate.ts to use paths.* fields from planningPaths instead
of hardcoded join(projectDir, '.planning') paths.
Closes#2731
Demonstrates that initProgress and initManager ignore the workstream
parameter, reading from root .planning/ instead of the workstream
subdirectory.
Closes#2731
[[agents]] sequence format (introduced in #2645) is rejected by
codex-cli 0.124.0 with "invalid type: sequence, expected struct AgentsToml".
Revert to [agents.<name>] struct format which is correct for 0.120.0+.
stripCodexGsdAgentSections already handles both formats for self-healing
configs written by previous GSD versions using [[agents]].
Closes#2727
replaceInCurrentMilestone's lastIndexOf('</details>') heuristic fails
when the active milestone itself is wrapped in a <details> block — the
after-slice is empty so the replacement is silently dropped.
Fix detects this case (after.trim().length === 0) and falls back to
locating the last complete <details>…</details> span and applying the
replacement only inside it, leaving all earlier archived-milestone
blocks untouched.
Closes#2641