agents/gsd-planner.md was 49,316 chars after the initial PR; the
planner-decomposition <48K test was passing on main at 49,150 chars (just under
the 49152 limit). My addition pushed it over.
Restructure: instead of teaching the planner agent to read .last-build-status.json
directly, fold the auto-build state into graphifyStatus()'s existing `stale: true`
signal. The planner's existing rule ("if stale: true, treat as approximate") fires
correctly for failed and in-flight auto-builds — no new planner-side prompt content
needed. The full state is exposed under `last_build_auto_update` for callers that
want exit_code / duration_ms / commit-sha context.
- get-shit-done/bin/lib/graphify.cjs: graphifyStatus() reads
.planning/graphs/.last-build-status.json; OR-folds status in {failed, running}
into the existing stale signal; exposes last_build_auto_update field
- agents/gsd-planner.md: revert the auto-update awareness paragraph (49,524 → 49,150)
- agents/gsd-phase-researcher.md: revert the parallel paragraph for consistency
- get-shit-done/references/planner-graphify-auto-update.md: rewrite to document
the graphifyStatus seam instead of planner-side prompt instructions
- tests/feat-3347-graphify-auto-update-config.test.cjs: 4 new graphifyStatus
tests pinning the failed/running/ok/missing matrix
- tests/feat-3347-graphify-auto-update-hook.test.cjs: bump per-spawn timeout
5s → 30s and wait-deadline 5s → 15s to absorb cold-start latency under
parallel-test-file load (full suite runs many *.test.cjs concurrently)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs/CONFIGURATION.md references the bundled hook by its file path
(hooks/gsd-graphify-update.sh). The docs-parity regex captures
/gsd-graphify-update from the path component and looks it up in the
live command registry, where it does not (and should not) exist —
it's a hook script, not a slash command. Add the slug alongside the
other hook-path slugs (statusline, context-monitor, update-banner).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5.0 of the CJS↔SDK hard-seam migration (parent #3524).
Foundational PR. Ships ONLY the synchronous primitive on the SDK
runtime bridge plus pinning tests. Per-family CJS router migrations
(state.*, verify.*, init.*, phase.*, phases.*, validate.*,
roadmap.*, frontmatter.*, config.*) become follow-up enhancements
that each reuse this primitive.
## What landed
- sdk/src/runtime-bridge-sync/index.ts (155 lines) — public API.
Exports executeForCjs(input: RuntimeBridgeExecuteInput):
RuntimeBridgeSyncResult. Synchronous; lazily creates the
synckit sync function on first call.
- sdk/src/runtime-bridge-sync/worker.ts (167 lines) — synckit
worker. Constructs a native-only QueryRuntimeBridge (with
allowFallbackToSubprocess: false), awaits its async execute,
catches GSDToolsError / GSDError, maps classification to the
six ADR-0001 canonical error kinds plus exit code.
- sdk/src/runtime-bridge-sync/index.test.ts (197 lines, 8 vitest
pinning fixtures) — success path; unknown_command;
native_failure (shape); validation_error (shape); shape
invariants; idempotency.
- tests/runtime-bridge-sync-smoke.test.cjs (99 lines, 4
node:test cases) — proves the primitive works from CJS
callers via require().
## Decisions
1. Synchronous-bridging mechanism: synckit. Disqualified:
- deasync: stagnant (68 open issues, single maintainer,
last release Nov 2025), private Node API (process.binding('uv')),
untested on Node 22, documented deadlocks with modern
Promise chains.
- Sync-native SDK refactor: technically infeasible —
acquireStateLock in state-mutation.ts uses await setTimeout
for retry backoff; making that fully sync requires either
Atomics.wait (which IS synckit), busy-loop (degrades
responsiveness), or breaking 100+ SDK consumers.
Synckit (v0.11.12) is pure JS, actively maintained (last
push today), stable public APIs (Atomics.wait +
SharedArrayBuffer), Node 22 compatible, no native compile.
2. Native-only transport inside the worker. The sync bridge
uses allowFallbackToSubprocess: false. Unknown commands
surface as unknown_command instead of spawning gsd-sdk.
Keeps the worker self-contained and predictable.
3. Worker path resolution. resolveWorkerPath() navigates ../..
from the loaded module URL to land at
dist/runtime-bridge-sync/worker.js — works under both
vitest (loads src) and CJS consumers (load dist).
4. GSDError.Blocked → validation_error. ADR-0001's 6-kind
taxonomy has no `blocked` kind; Blocked classification is
mapped onto validation_error since the operational shape
matches (prerequisite missing).
## Numbers
- 8 SDK vitest pinning tests pass.
- 4 CJS smoke tests pass.
- Full suite: 9286/9286 pass (baseline 9282; +4 from the
new smoke test cases).
- SDK vitest unit: 1860/1860 pass.
- Performance: 80ms first-call cold latency (Worker startup +
bridge construction); 0.1ms steady-state per-call latency
(10-call average after warmup). Well within budget for CJS
dispatcher overhead.
## Canonical error kind coverage
- unknown_command: covered with pinning fixture
- native_failure: shape coverage (handler that throws)
- validation_error: shape coverage (GSDError.Validation +
GSDError.Blocked)
- internal_error: shape coverage only (eliciting TypeError
reliably from a registered handler requires elaborate fixture)
- native_timeout: NOT pinned (no registered handler genuinely
times out; classification logic present in worker)
- fallback_failure: NOT pinned (subprocess fallback disabled
by design in sync bridge)
The classification logic is in the worker regardless; per-family
migration PRs will exercise the unpinned kinds incidentally.
## Wiring
- sdk/package.json: synckit ^0.11.12 added as runtime
dependency (not devDependency — it's required at runtime
whenever a CJS caller invokes executeForCjs).
- sdk/package-lock.json: regenerated.
- CONTEXT.md: new "Sync Runtime Bridge Module" entry added
after Dispatch Policy Module. Existing "CJS Command Router
Adapter Module" entry amended with one sentence pointing at
the primitive and the per-family migration roadmap.
No generator/freshness check needed for this phase — the
primitive IS the SDK (not a generated CJS mirror).
Closes#3555.
Phase 4 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the `findProjectRoot` duplication that lived at
bin/lib/core.cjs:74-140 and sdk/src/query/helpers.ts:497-590,
the drift carrier behind historical bugs #1362 and #2561.
- sdk/src/project-root/index.ts — source of truth (120 lines,
pure-with-sync-fs). Exports findProjectRoot(startDir: string)
and FIND_PROJECT_ROOT_MAX_DEPTH constant.
- sdk/src/project-root/index.test.ts — 13 vitest pinning fixtures
covering all four heuristics, the #1362 guard, malformed
config fallback, empty sub_repos, deep nesting, and depth-limit
enforcement.
- sdk/scripts/gen-project-root.mjs — generator. Captures
function body via Function.prototype.toString() from compiled
sdk/dist/. Emits CJS preamble for destructured node:fs /
node:path / node:os imports.
- sdk/scripts/check-project-root-fresh.mjs — freshness check.
Imports the generator function directly (Phase 3's cleaner
pattern).
- get-shit-done/bin/lib/project-root.generated.cjs — generator-
emitted CJS mirror.
- tests/project-root-generator.test.cjs — 11 parity assertions
comparing SDK source and generated CJS for every fixture.
- sdk/src/query/helpers.ts: -127 lines. The 94-line inline
findProjectRoot plus the FIND_PROJECT_ROOT_MAX_DEPTH constant
(originally at line 471) replaced by a single re-export:
`export { findProjectRoot } from '../project-root/index.js';`
Removed unused `parse as parsePath` import.
- get-shit-done/bin/lib/core.cjs: -83 lines net. The 67-line
inline findProjectRoot replaced by a single
`require('./project-root.generated.cjs')`. The detectSubRepos
helper at lines 40-56 stays (used by loadConfig migration).
- sdk/package.json: gen:project-root + check:project-root-fresh
scripts.
- package.json: proxy for the freshness check.
- .githooks/pre-commit: drift block.
- .github/workflows/test.yml: drift check step after the
state-document drift step.
- CONTEXT.md: Project-Root Resolution Module entry.
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json:
+1 module count, +1 row.
- Full suite: 9226/9226 pass (baseline 9215 + 11 new parity
fixtures).
- SDK vitest: 1804/1804 pass.
- Reader shrink: -127 SDK + -83 CJS = 210 lines of duplication
deleted across the two Readers. New shared Module is 120 lines.
1. Depth limit canonicalization. CJS findProjectRoot previously
had no explicit walk-up bound (walked until dir === root or
homedir). The new Module uses FIND_PROJECT_ROOT_MAX_DEPTH = 10,
matching the SDK's pre-existing value. Only affects paths
nested more than 10 levels deep from a .planning/ root — a
pathological case in practice. None of the existing 22 CJS
findProjectRoot tests covered this; the new parity test does.
2. platformReadSync → readFileSync. The old CJS findProjectRoot
used the platformReadSync wrapper from
shell-command-projection.cjs for reading .planning/config.json,
which returns null on read failure. The Module uses raw
readFileSync, which throws — caught by the surrounding
try/catch that already swallowed errors. Functionally
equivalent for the existing code path; no test exercises the
null-return semantic.
Closes#3553.
Phase 3 of the CJS↔SDK hard-seam migration (parent #3524).
Introduces the Builder/Reader pattern for paired Modules with
mixed pure-and-I/O concerns — the template for Phase 4 and
follow-up enhancements that migrate other paired Modules.
Phase 1 and Phase 2 migrated Modules where both sides used
character-equivalent logic. Phase 3 introduces the case where
the pure logic is shareable but the I/O is legitimately per-side.
The Builder/Reader split resolves this:
- The Builder is pure — accepts pre-collected data
(BuilderInputs struct), returns the typed projection. One
source of truth; one generator-emitted CJS mirror. Drift
is structurally impossible.
- The Readers are per-side hand-authored Adapters that do the
fs reads in their native idiom (currently both sync; either
side can go async later without touching the Builder), then
delegate to the Builder.
- sdk/src/workstream-inventory/builder.ts — Builder source.
170 lines. Pure. Exports buildWorkstreamInventory(inputs),
isCompletedInventory(status), plus the three typed inventory
interfaces (WorkstreamPhaseInventory, WorkstreamInventory,
WorkstreamInventoryList).
- sdk/src/workstream-inventory/builder.test.ts — 18 vitest
pinning fixtures across all status branches, progress-percent
clamping, active-marker projection, and isCompletedInventory
classifier.
- sdk/scripts/gen-workstream-inventory-builder.mjs — generator.
Captures function bodies via Function.prototype.toString();
emits with the standard GENERATED FILE banner. Includes a
small `const relative = path.relative;` preamble in the
output to handle ESM destructured imports in the compiled
source.
- sdk/scripts/check-workstream-inventory-builder-fresh.mjs —
freshness check. Imports the generator function directly
(rather than duplicating logic) — a cleaner pattern than
Phase 1/2's approach.
- get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs —
generator-emitted CJS mirror.
- tests/workstream-inventory-builder-generator.test.cjs — 16
parity assertions confirming CJS-generated output ==
SDK source output for every fixture.
- bin/lib/workstream-inventory.cjs: 159 → 132 lines.
Projection logic gone. `inspectWorkstream` and
`listWorkstreamInventories` collect BuilderInputs via the
existing sync fs functions and delegate to the Builder.
`isCompletedInventory` re-exported from the Builder (its
signature changed from object→string, but no external
callers exist so the change is safe).
- sdk/src/query/workstream-inventory.ts: 196 → 143 lines.
Same shape, sync fs (the SDK was already sync — surprise from
recon). Types re-exported from the Builder.
- sdk/package.json: gen:workstream-inventory-builder and
check:workstream-inventory-builder-fresh scripts.
- package.json: proxy for the freshness check.
- .githooks/pre-commit: drift block.
- .github/workflows/test.yml: drift check step.
- CONTEXT.md: amended "Workstream Inventory Module" entry
to document the Builder/Reader split.
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json:
+1 module count, +1 row for the generated builder.
- Full suite: 9229/9229 pass (baseline 9215 + 14 net new from
the parity assertions).
- Vitest: 18 Builder fixtures pass.
- Reader shrink: -27 lines on CJS, -53 lines on SDK.
- Net diff (modified files only): +68 / -133 = 65-line
reduction. New files (Builder, generator, freshness check,
parity test) add ~600 lines of new structured code.
1. `isCompletedInventory` signature changed from
isCompletedInventory(inventory: object) to
isCompletedInventory(status: string). Original CJS exported
the object form but no external caller passed an object —
they all passed inventory.status. Verified by grep before
committing.
2. Generator preamble. The compiled ESM uses
`import { relative } from 'node:path'`, making `relative`
a free variable in `buildWorkstreamInventory`. The generator
emits `const relative = path.relative;` so the captured
function body works in CJS.
3. Freshness check imports the generator. The freshness check
imports the generator's buildWorkstreamInventoryBuilderCjs()
function directly rather than duplicating generation logic.
Cleaner than Phase 1/2; future generators should follow this.
Shareable via the Builder/Reader pattern in future enhancements:
- frontmatter (pure YAML/markdown parsing)
- plan-scan (pure PLAN.md structure parsing)
- decisions (pure decision-record parsing)
- secrets (regex-based detection in text)
- uat (UAT-criteria parsing)
Structural divergence — different approach needed:
- state — sync vs async file ops; mutation paths differ.
- workstream — lifecycle ops; per-side API surface differs.
- phase, roadmap, init, profile-output, template — large
surfaces; each its own potential enhancement.
None of these is in scope for Phase 3.
Closes#3544.
The SDK side of the parity test asserts the source shape of
sdk/src/query/config-schema.ts (must re-export from
../configuration/index.js; must NOT contain inline `new Set([...])`
literals). Runtime/IR comparison cannot distinguish a re-export from
a redeclared Set with identical contents — only source inspection
catches drift back to inline literals.
Adds the documented `// allow-test-rule:` annotation explaining why
the three `src.includes()` calls are structurally necessary. Test
behavior unchanged; all 6 tests still pass; lint-no-source-grep now
reports 0 violations across 514 test files.
Phase 2 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the structural drift surface that produced bug class
After this phase, neither bin/lib/ nor sdk/src/ defines
CONFIG_DEFAULTS, VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, or the
four legacy-key normalizations inline. All come from one canonical
source: the Configuration Module (sdk/src/configuration/index.ts)
+ two JSON manifests (sdk/shared/config-{defaults,schema}.manifest.json).
The CJS mirror is generator-emitted (get-shit-done/bin/lib/configuration.generated.cjs)
with a CI freshness check (sdk/scripts/check-configuration-fresh.mjs).
- sdk/shared/config-defaults.manifest.json — canonical nested defaults,
union of CJS + SDK keys (includes security_*, post_planning_gaps,
agent_skills, mode, every git/workflow/hooks sub-section).
- sdk/shared/config-schema.manifest.json — VALID_CONFIG_KEYS array,
RUNTIME_STATE_KEYS array, DYNAMIC_KEY_PATTERNS array with source
strings (regex reconstructed at runtime).
- sdk/src/configuration/index.ts — source of truth. Exports
loadConfig (pure read), normalizeLegacyKeys (pure, idempotent,
returns Normalization[]), mergeDefaults (deep-merge), migrateOnDisk
(explicit opt-in disk writeback), plus CONFIG_DEFAULTS,
VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS.
- sdk/src/configuration/index.test.ts — 29 vitest pinning tests.
- sdk/scripts/gen-configuration.mjs — generator (Function.prototype.toString()
inspection of compiled SDK dist, plus brace-balanced text scan for
internal helpers, matching the Phase 1 pattern).
- sdk/scripts/check-configuration-fresh.mjs — CI freshness gate.
- tests/configuration-generator.test.cjs — 27 parity assertions
(CJS-generated == SDK source).
- tests/configuration-migrate-config.test.cjs — 3 cases for the new
gsd-tools migrate-config subcommand.
- bin/lib/core.cjs: CONFIG_DEFAULTS literal now sources values from
CANONICAL_CONFIG_DEFAULTS (the manifest), with a thin flat
projection at the load boundary to preserve the existing
flat-shape return contract for the ~21 CJS test files and 100+
consumers. All four legacy-key migration blocks (branching_strategy,
sub_repos, multiRepo, depth — historically lines 351-358, 388-397,
401-408, 416-423) collapse to a single normalizeLegacyKeys call
in each code path. The inline platformWriteSync writeback stays
for now to preserve sync loadConfig semantics; the new async
migrateOnDisk is reachable via gsd-tools migrate-config.
- bin/lib/config-schema.cjs: 135 → 31 lines. Re-exports from the
generated Module.
- bin/lib/config.cjs: adds cmdMigrateConfig handler (calls
migrateOnDisk on the explicit user-driven path).
- bin/gsd-tools.cjs: wires migrate-config into command dispatch.
- sdk/src/config.ts: re-exports CONFIG_DEFAULTS and mergeDefaults
from the Module. loadConfig now calls normalizeLegacyKeys before
mergeDefaults (replaces the inline branching_strategy graft).
- sdk/src/query/config-schema.ts: 160 → 36 lines. Re-exports from
the Module.
- tests/config-schema-sdk-parity.test.cjs: refactored from
"CJS Set equals SDK Set" (trivially true post-migration) to
"both sides source from the manifest" — structural plus runtime
invariant.
- Four other tests that text-grepped source files for valid keys
(plan-review-convergence, bug-3212, bug-2492, feat-3210) are
updated to use runtime VALID_CONFIG_KEYS.has() or manifest JSON
lookups.
- CONTEXT.md: new Configuration Module entry with full Interface
contract.
- Root package.json: check:configuration-fresh proxy script.
- sdk/package.json: gen:configuration + check:configuration-fresh.
- .githooks/pre-commit: configuration drift block.
- .github/workflows/test.yml: configuration drift step after the
alias drift check.
- 9201 CJS tests pass (baseline pre-cycle: 9195; +6 net new tests
across migrate-config + parity refactor)
- 1872 SDK vitest tests pass
- 29 Configuration Module vitest fixtures
- 27 CJS/SDK parity fixtures
- Net diff: +388 / −519 = 131-line reduction across the seven cycles,
despite adding the new Module, manifests, generator, freshness
check, and two new test files.
1. SDK CONFIG_DEFAULTS now includes manifest-canonical keys
(resolve_model_ids: false, context_window: 200000, phase_naming,
claude_md_path, git.create_tag, workflow.security_*,
workflow.code_review_*, planning.*, hooks.workflow_guard, ship.*).
Consumers accessing via [key: string]: unknown index get
the manifest default instead of undefined.
2. SDK mergeDefaults is now proper recursive deep-merge instead of
spread-per-section. Overlay { workflow: { research: false } }
now preserves sibling workflow keys; previously it replaced
the entire workflow section with only research + the section's
defaults. Semantically identical for the common case;
strictly better for partial nested overrides.
3. New gsd-tools migrate-config CLI subcommand for the explicit,
opt-in on-disk migration path.
Closes#3536.
* fix(3537): route every phase-number ROADMAP regex through phaseMarkdownRegexSource
v1.42.1 added the padding-tolerant `phaseMarkdownRegexSource()` helper but
wired it into only 1 of 8 call sites that build phase-number regexes against
ROADMAP/STATE prose. The other 7 used raw `escapeRegex(phaseNum)` or partial
`0*${escapeRegex(...)}` (tolerated extra padding, not missing), so when
skills passed the resolved padded form (`02.7`) against un-padded ROADMAP
prose (`### Phase 2.7:`, `- [ ] **Phase 2.7:**`), the verbs silently no-op'd
while reporting success.
This consolidates every phase-number ROADMAP/STATE regex through the
canonical helper:
- Promote `phaseMarkdownRegexSource` from `roadmap.cjs` to `core.cjs` so
`phase.cjs` and `core.cjs` itself can consume it (no circular dep —
both already import `core.cjs`).
- Wire the helper into the 7 remaining sites:
- `core.cjs:getRoadmapPhaseInternal` (replaces hand-rolled `isNumeric`
branch that only padded integers, not decimals).
- `roadmap.cjs:cmdRoadmapGetPhase` (searchPhaseInContent escapedPhase).
- `roadmap.cjs:cmdRoadmapAnalyze` checkbox lookup.
- `roadmap.cjs:cmdRoadmapAnnotateDependencies` phase header lookup.
- `phase.cjs:cmdPhaseNextDecimal` ROADMAP prose scan.
- `phase.cjs:cmdPhaseInsert` target anchor + decimal scan + header.
- `phase.cjs:cmdPhaseComplete` (3 regexes: checkbox, plan-count,
REQUIREMENTS extraction).
Adds `tests/bug-3537-padded-id-against-unpadded-roadmap.test.cjs` — a
parity-style regression matching CONTEXT.md DEFECT.GENERATIVE-FIX: for
each user-facing verb, asserts that the padded form (`02.7`) and the
un-padded form (`2.7`) produce identical ROADMAP.md against an identical
fixture. Includes one control case (`update-plan-progress`, already wired
in 1.42.1) to prove the parity assertion is non-vacuous.
Closes#3537
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): add changeset fragment (pr: placeholder, amended post-create)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): pin changeset pr: field to #3538
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(3530): STATE.md Document Module via generator (Phase 1 of #3524)
Phase 1 of the CJS↔SDK hard-seam migration (parent #3524).
Converts the hand-synced state-document.cjs/state-document.ts pair
into a generator-driven seam, modeled on the existing
command-aliases.generated.* precedent.
What landed:
- sdk/src/query/state-document.ts is the source of truth.
- sdk/scripts/gen-state-document.ts emits
get-shit-done/bin/lib/state-document.generated.cjs from the
compiled SDK dist via Function.prototype.toString() inspection
for the 7 public exports and 3 internal helpers.
- sdk/scripts/check-state-document-fresh.mjs is the CI freshness
gate; pre-commit hook also runs it when relevant files change.
- get-shit-done/bin/lib/state-document.cjs is reduced to a one-line
re-export from state-document.generated.cjs so existing callers
(state.cjs, workstream-inventory.cjs, init.cjs) need no changes.
- New CI step in .github/workflows/test.yml after the existing alias
drift check.
- sdk/package.json: gen:state-document, check:state-document-fresh
scripts. tsx added as devDep.
- Root package.json: proxy script for the freshness check.
- CONTEXT.md: one-sentence amendment on STATE.md Document Module
recording the source-of-truth file path.
Tests:
- sdk/src/query/state-document.test.ts: 34 vitest fixtures across
the 7 public exports (TDD pinning safety net).
- tests/state-document-generator.test.cjs: 31 node:test parity
assertions comparing SDK source vs generated CJS for every
fixture.
- Full suite: 9177/9177 pass (baseline was 9146; +31 new tests).
One subtle behavior change worth flagging: the old hand-written
state-document.cjs used String(str) coercion inside escapeRegex,
which the SDK source does not. The generator faithfully matches
the SDK (the source of truth per ADR-3524), so the new CJS no
longer coerces non-string input to string before regex-escaping.
No current caller passes non-string input, so no observable
regression in the test suite. Flagged in the PR body for
reviewers.
Closes#3530.
* fix(3530): address state-document review findings
Six failing tests covering all Done-when criteria from #3523:
1. No warning emitted for top-level branching_strategy
2. Value still surfaced via git.branching_strategy after loadConfig
3. Double-emission capped to single-emission per process
4-5. On-disk migration (option 3): write-back + no-clobber guard
6. CJS↔SDK contract: both agree on legacy-shape fixture
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Simulates orchestrator-leaked CWD in the post-merge cleanup loop and
asserts that PROJECT_ROOT is resolved via `git -C "$WT" rev-parse
--git-common-dir` before any bare git command, that a missing root
causes a logged skip/continue, and that the existing pre-merge deletion
guard (#1756) and STATE.md/ROADMAP.md backup/restore remain in-place
after the CWD pin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds bug-3516-reapply-patches-gsd-update-filter.test.cjs — 7 tests that
assert all four exclusion patterns (gsd:update, gsd-update, GSD update,
gsd-install) are present in the git-enhanced two-way merge filter inside
get-shit-done/workflows/reapply-patches.md.
Two tests fail before the fix: 'filter excludes renamed gsd-update commits'
and 'all four expected exclusion patterns are present in the filter'.
Fixes two root causes behind bug #3517:
1. Idempotency: completed_phases was blindly incremented (parseInt + 1),
causing phase.complete N run twice to double-count (4 → 5 → 6).
Now derives from ROADMAP progress table Complete-row count, making
the operation idempotent.
2. Field coverage: eight STATE.md fields were left stale after phase
completion. Now updates in the same atomic lock section:
- frontmatter: stopped_at, last_updated, total_plans, completed_plans
- body: Current focus, Status line, By Phase table row
completed_plans = count of *-SUMMARY.md files across all phase dirs
total_plans = sum of M/N plan counts from ROADMAP progress table
percent = recomputed from fresh derived counts
Closes#3517
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract composeStatusline() helper from duplicated inline template logic in
runStatusline() and renderStatusline(). Both call sites now route through the
helper, which accepts a position param ('end' | 'front', default 'end').
- 'end' (default) preserves byte-identical output to v1.38.x and earlier
- 'front' renders ctx immediately after model name, before the first │
- Invalid values silently coerce to 'end' at runtime (belt-and-suspenders;
config-set rejects invalid values upfront via enum validator)
Adds statusline.context_position to VALID_CONFIG_KEYS in both CJS and TS
schemas, enum validator in config.cjs, docs row in CONFIGURATION.md,
and a changeset. Closes#2937.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On machines where os.tmpdir() returns a path with a space (e.g.
/Volumes/Mini Me/tmp), runGsdTools() string args were whitespace-split by
the helper tokeniser, truncating paths at the first space. Switch all
calls that embed a dynamic path into the argument list to the array form
of runGsdTools() so execFileSync receives each path as a single argv slot.
Fixes#3509
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds boolean config key `git.create_tag` (default: true, fully backcompat)
so projects with their own release flow can disable GSD's automatic
`git tag -a v[X.Y]` on milestone completion. Also adds tag-collision
pre-check to prevent silent failure on re-run. Closes#3086
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
migrateCodexHooksMapFormat re-emitted the raw `[hooks.<X>]` path segment as
the leaf TOML key of the new `[[hooks.<EVENT>]]` block. When the legacy
table key was a `<file>:<event>:<line>:<col>` location identifier and the
real event lived in an `event = "..."` body field, the migration emitted a
header like `[[hooks."C:\\Users\\helen\\.codex\\config.toml:session_start:0:0"]]`
that Codex 0.124.0+ refuses to load — causing `npx get-shit-done-cc@latest`
to abort the Codex runtime install on Windows configs that pre-date AoT.
Mirror the flat-AoT branch in the map-format and stale-namespaced-AoT
branches: when the section body declares `event = "..."`, that name wins
as the leaf key and `event` is excluded from the re-emitted handler body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The check at tests/path-replacement.test.cjs:163 used a naive
content.includes(normalizedHomedir) to detect resolved homedir leaks in
installed .md files. When os.homedir() is short (e.g. /root inside a
Docker container), the substring false-matches inside ordinary tokens
such as `</root_cause_analysis>` in agents/gsd-debug-session-manager.md,
producing spurious failures with no actual path leak.
Real path leaks are always followed by a path separator, so require
`normalizedHomedir + '/'` instead. Extracted the predicate into a
testable `containsResolvedHomedir` helper and added regression tests
covering the /root case, a genuine /home/alice leak, /root followed by
an actual separator, and the $HOME placeholder short-circuit.
Fixes#3503
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `has_git` boolean returned by `init new-project` and `init ingest-docs`
was derived from a shallow `pathExists(cwd, '.git')` check, so a subdirectory
of an existing repo reported `has_git: false`. The workflow then ran
`git init`, creating a nested `.git` inside the outer worktree and silently
diverting subsequent `gsd-sdk commit` calls into the nested repo.
Replace the shallow check with `git rev-parse --is-inside-work-tree`
semantics in both CJS (`get-shit-done/bin/lib/init.cjs`) and TS
(`sdk/src/query/init.ts`, `sdk/src/query/init-complex.ts`) handlers via a new
shared `gitWorktreeInfoInternal` helper, and expose `git_worktree_root` +
`in_nested_subdir` so the workflows can refuse `git init` inside an existing
worktree and warn that planning files will track to the outer repo.
Regression test: `tests/bug-3491-nested-git-worktree.test.cjs`.
Fixes#3491
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-invoking `state complete-phase --phase <N>` on a phase that was
already marked complete in STATE.md silently rolled STATE.md back to
that phase's moment-of-completion — clobbering Status, Last Activity,
Last Activity Description, and the ## Current Position body. The bug
fired whenever a follow-up phase had been inserted (or the next phase
had begun) and a downstream workflow re-ran complete-phase on the
already-closed phase. Damage was silent: handler reported
{"updated":["Status","Last Activity","Current Position"]} and no error.
Root cause: cmdStateCompletePhase wrote unconditionally — it never
consulted STATE.md to detect that the requested phase had been
superseded. The handler is a legacy-bridge fallback (no native SDK
registration), so the SDK CLI fell through to gsd-tools.cjs.
Fix: add an idempotency guard at the top of cmdStateCompletePhase.
If STATE.md's canonical Current Phase field already names a phase
distinct from the one we are being asked to mark complete, return a
no-op payload ({updated:[], phase:"<N>", idempotent:true, note:"phase
already superseded; no-op"}) without writing to STATE.md.
The guard is conservative — it only fires when Current Phase is set
and differs from the resolved target. First-time completion (Current
Phase == target, or Current Phase absent) is unaffected, so the four
existing complete-phase test cases (#2761, #3063) continue to pass.
Regression test: tests/bug-3489-complete-phase-idempotent.test.cjs
- re-running complete-phase --phase 02.2 with Current Phase=02.2.1
in STATE.md leaves the file byte-identical and reports idempotent:true
- normal first-time completion is NOT flagged idempotent
Scope: handler-level idempotency only. Does not address the related
stopped_at filename-sort ordering issue called out in the bug report
(filed under suggested fix#2) or porting to the native registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>