* 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
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
milestoneComplete was imported in decomposed-handlers.test.ts but had zero
test coverage. The original defect (6f79b1d) called phasesArchive([], ...)
instead of forwarding the positional version arg; the wrapping try/catch
swallowed the GSDError into { completed: false, reason: String(err) },
masking a programming error as a legitimate negative answer.
Add five Vitest tests that lock in the correct contract:
- positional version arg is extracted from args[0] and echoed in response
- missing version throws GSDError (not masked as completed: false)
- --archive-phases flag is processed
- --name flag sets milestone name
- response shape has version/date/phases/milestones_updated fields
Closes#2644
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
phaseAdd's phase-number regex only matched heading format (## Phase N:),
missing bullet checklist (- [x] Phase N:) and bold (**Phase N:**) entries.
When zero regex matches, newPhaseId defaulted to 1.
Fix: broaden regex to match all three formats, and add filesystem fallback
scanning .planning/phases/ when ROADMAP scan finds nothing.
Closes#2726
\s* after **Plans:** matches newlines, causing [^\n]+ to consume the first
plan checkbox when the **Plans:** field has no value on the same line.
Additionally, the lazy [\s\S]*? could cross section boundaries when the
current section had no **Plans:** value, corrupting a later section.
Fix 1: replace \s* with [ \t]* to restrict post-colon match to horizontal
whitespace only.
Fix 2: replace [\s\S]*? with (?:(?!\n#{2,4})[\s\S])*? to prevent the
pattern from crossing into a new section heading.
Closes#2728