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>
* refactor(shell-projection): remove deprecated wrappers + finalize ADRs (Phase 4, #3468)
Final phase of the shell-command-projection expansion. Removes the legacy
core.cjs wrappers (`atomicWriteFileSync`, `safeReadFile`, `normalizeMd`)
now that every call site lives behind the seam, plus three Phase-3
stragglers (`graphify.cjs`, `template.cjs`, dead import in
`profile-pipeline.cjs`).
Documentation:
- ADR-0009: addendum noting Phase 1–4 scope expansion (subprocess +
file I/O ownership), supersession of "does not execute" constraint,
and resolution of open Q4.
- ADR-0010: status changed to Superseded by ADR-0009 with explanation.
- CONTEXT.md "Shell Command Projection Module" entry already current
from Phase 1 — no edit needed.
Tests:
- `tests/atomic-write.test.cjs` deleted — wrapper it tested is gone;
`atomic-write-coverage.test.cjs` (Phase 3) covers platformWriteSync.
- `tests/core.test.cjs::safeReadFile` + `::normalizeMd` describes
deleted — wrappers are gone.
- `tests/concurrency-safety.test.cjs` normalizeMd suite (behavioral /
perf / snapshot) repointed via 2-line shim at the seam's
`normalizeContent` — full regression coverage preserved.
Test result: 9059/9041/18 — exact pre-Phase-4 baseline. All 18
failures are pre-existing path-with-spaces local-env issues.
Closes#3468
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate remaining raw fs.writeFileSync sites (Phase 4, #3468)
Sweeps the 7 raw fs.writeFileSync call sites that bypassed the seam through Phase 3,
folding them into platformWriteSync. Net -14 lines: deletes the local writeFileAtomicSync
helper in installer-migrations.cjs and collapses surface.cjs's manual tmp+rename into a
single seam call.
Sites migrated:
- drift.cjs (1) — frontmatter write
- learnings.cjs (1) — learning record JSON write
- install-profiles.cjs (1) — profile marker write (collapsed redundant mkdir)
- gsd2-import.cjs (1) — imported file write (collapsed redundant mkdir)
- surface.cjs (1) — surface state write (replaced manual tmp+rename block)
- installer-migrations.cjs (3) — journal init/finalize + rewrite-json action;
deleted private writeFileAtomicSync helper and its three call sites
Two sites intentionally retained outside the seam:
- planning-workspace.cjs:241 — workspace lock (wx-flag atomic-create; previously excluded by Phase 3)
- installer-migrations.cjs:220 — install migration lock (fd write into wx-opened handle)
- writeInstallState (installer-migrations.cjs) — strict atomic contract for install state;
the seam's fallback-to-direct-write on rename failure would silently violate the
invariant that install state must never be left half-written. Inline tmp+rename with
rethrow keeps the original guarantee.
Tests: 9059 / 9041 / 18 — exactly the pre-Phase-4 baseline; 18 failures are the
pre-existing path-with-spaces local-env issues, identical files as before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(installer-migrations): use strict atomic write for rollback install-state restore
The rollback path was restoring INSTALL_STATE via platformWriteSync, which falls
back to a direct write on rename failure and would silently violate the
half-written invariant that the install-state contract guarantees elsewhere.
Extracts the strict tmp+rename logic from writeInstallState into a shared
atomicWriteInstallState(configDir, content) helper and routes both
writeInstallState and rollbackAppliedMigrationResult through it. Preserves the
existing null-handling (rmSync when previousInstallStateBytes === null) and
existing failure-collection (failures.push on caught errors).
Byte-faithful restore: previousInstallStateBytes is written as-is (no JSON
parse round-trip), preserving the exact prior file contents on restore.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate roadmap.cjs writes to platformWriteSync (#3467)
2 atomicWriteFileSync calls → platformWriteSync. The seam owns markdown
normalization, so the explicit utf-8 encoding arg is no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate config.cjs writes to platformWriteSync (#3467)
- 3 atomicWriteFileSync calls → platformWriteSync
- 1 raw fs.writeFileSync (depth→granularity migration) → platformWriteSync
- 2 fs.mkdirSync(planningBase, { recursive: true }) → platformEnsureDir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate docs.cjs reads to platformReadSync (#3467)
6 try { fs.readFileSync } catch {} patterns → platformReadSync(path) with
explicit null guards. detectProjectType now reads package.json once and
shares it across has_cli_bin/is_monorepo/has_tests checks. JSON.parse is
still wrapped in a try (parsing is a separate failure mode from missing file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate audit.cjs reads to platformReadSync (#3467)
8 try { fs.readFileSync(safeFilePath, 'utf-8') } catch { continue } patterns
→ const content = platformReadSync(safeFilePath); if (content === null) continue;
The single safeSum case (where catch set status='unreadable' rather than
continue) maps to an if/else that preserves the same semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate planning-workspace.cjs to platform* seam (#3467)
- 2 try { fs.readFileSync } catch {} → platformReadSync (null on missing)
- 2 fs.writeFileSync (workstream pointer writes) → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
The .lock file write at withPlanningLock is intentionally NOT migrated.
That call uses { flag: 'wx' } for atomic exclusive-create, which is the
correct lock-acquisition primitive. platformWriteSync's atomic-rename
pattern would silently overwrite an existing lock file and break the
locking guarantee.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate milestone.cjs writes to platform* seam (#3467)
- 5 atomicWriteFileSync calls → platformWriteSync (4 dropped normalizeMd
wrapper; seam handles .md normalization automatically)
- 2 raw fs.writeFileSync (archive ROADMAP.md / REQUIREMENTS.md) → platformWriteSync
- 2 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Dropped normalizeMd import (only used as write pre-call here)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate intel.cjs to platform* seam (#3467)
- 7 fs.readFileSync (existsSync+readFileSync patterns and try/catch) → platformReadSync
- 2 fs.writeFileSync → platformWriteSync
- 1 fs.mkdirSync(intelPath, { recursive: true }) → platformEnsureDir
- Consolidated dual-check (existsSync + readFileSync) into single platformReadSync
call returning null on missing file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate workstream.cjs to platform* seam (#3467)
- 5 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- 1 fs.writeFileSync (STATE.md initial scaffold) → platformWriteSync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate init.cjs reads/writes to platform* seam (#3467)
- 11 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 1 fs.writeFileSync (skill-manifest.json) → platformWriteSync
Three bare fs.readFileSync calls remain (ROADMAP/STATE reads in code paths
where the file is required to exist) — these are not "Done when" violations
(no try/catch wrapping, no inline existsSync guard).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate commands.cjs reads/writes to platform* seam (#3467)
- 6 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 2 fs.writeFileSync → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Removed unused safeReadFile import (zero call sites in this file)
Three bare fs.readFileSync calls remain (sourcePath at line 752, fullPath at
443, roadmapPath in cmdAuditOpen) — preceded by existsSync guards or in code
paths where file presence is required; not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate profile-output.cjs to platform* seam (#3467)
- 6 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync
as safeReadFile in the import — same semantics, zero call-site changes
- 3 try/JSON.parse(readFileSync) patterns → platformReadSync + try/JSON.parse
- 1 existsSync+readFileSync pattern (claude.md update) → platformReadSync
- 5 fs.writeFileSync → platformWriteSync
- 4 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
Two bare fs.readFileSync calls remain (template reads where file must exist
or fail loudly) — not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate state.cjs to platform* seam (#3467)
- 4 atomicWriteFileSync calls → platformWriteSync (3 dropped normalizeMd
wrapper; seam handles .md normalization)
- 4 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 1 fs.writeFileSync (WAITING.json) → platformWriteSync
- 1 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Dropped normalizeMd and atomicWriteFileSync imports (only used as write
pre-calls here)
Bare fs.readFileSync calls remain in code paths where STATE.md is required
to exist (statePath reads in cmd handlers, dry-run prune) — not "Done when"
violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate core.cjs to platform* seam (#3467)
- 7 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 3 fs.writeFileSync (config writes + large-payload temp file) → platformWriteSync
- 1 fs.mkdirSync (GSD_TEMP_DIR) → platformEnsureDir
Three fs calls remain — they are the internal implementations of the
safeReadFile and atomicWriteFileSync wrappers that core.cjs exports for
backward compatibility. The wrappers are scheduled for removal in Phase 4
(#3468) and will not be migrated here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate phase.cjs writes to platform* seam (#3467)
- 6 atomicWriteFileSync calls → platformWriteSync
- 3 fs.writeFileSync(path.join(dirPath, '.gitkeep'), '') → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
Bare fs.readFileSync calls remain for roadmapPath/planPath reads where the
file is required to exist; these are not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate verify.cjs to platform* seam (#3467)
- 8 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync
as safeReadFile in the import — same semantics, zero call-site changes
- 1 existsSync+readFileSync inline ternary → safeReadFile (returns null)
- 5 fs.writeFileSync (config writes + milestones writes) → platformWriteSync
Bare fs.readFileSync calls remain for code paths where the file is required
to exist (roadmap/state/config full reads); these are not "Done when"
violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate frontmatter.cjs + update atomic-write test (#3467)
- frontmatter.cjs: 2 atomicWriteFileSync calls → platformWriteSync. The
legacy normalizeMd wrapper is dropped because the seam handles markdown
normalization. safeReadFile preserved by aliasing platformReadSync.
- atomic-write-coverage.test.cjs: update the #1972 structural invariant
to assert on platformWriteSync. platformWriteSync uses the same
tmp-file + atomic-rename primitive that atomicWriteFileSync did — the
no-partial-write guarantee is preserved across the migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add entry for shell-projection Phase 3 migration (#3467)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(coderabbit): disable ESLint tool (repo uses custom lint scripts)
CodeRabbit's review surface emits a "skipped: no ESLint configuration"
warning because the repo doesn't ship ESLint config. The repo
intentionally does not use ESLint — it ships its own targeted lint
scripts (scripts/lint-no-source-grep.cjs, npm run lint:tests) that
enforce repo-specific test-quality invariants. Adding ESLint config
purely to satisfy CR would add an external dependency
(CONTRIBUTING.md: "No external dependencies in core") and overlap
with the existing custom lint surface.
Disable the ESLint tool in CR's tools config so the skip warning
stops appearing on every PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate planning-workspace.cjs tty probe to probeTty seam (#3466)
Replaces direct execFileSync('tty') with probeTty() from the shell-projection
seam. Removes try/catch — probeTty() returns null on error/non-tty/win32.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate commands.cjs to execGit seam (#3466)
- Replaces execSync('git diff --cached --name-only') with execGit array call
- Migrates 14 existing execGit(cwd, args) callers from core.cjs's local
wrapper to the seam's execGit(args, { cwd }) signature
- Drops execGit from the core.cjs destructure to resolve naming collision
Drops try/catch around git diff — execGit returns exitCode without throwing,
so the no-staged-files / not-a-git-repo case is detected by exitCode !== 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate check-latest-version.cjs to execNpm seam (#3466)
Routes the default-spawn path through execNpm — execNpm owns the win32
shell-flag policy. The injection point remains spawnSync-shaped for test
compatibility; an internal adapter translates { exitCode } → { status }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate init.cjs git calls to execGit seam (#3466)
Replaces 3 execSync calls with execGit array-args:
- detectChildRepos: git status --porcelain
- cmdInitNewWorkspace: git --version (worktree availability probe)
- cmdRemoveWorkspace: git status --porcelain
Drops 3 try/catch blocks — execGit returns exitCode without throwing, so
best-effort handling becomes a clean exitCode === 0 check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate core.cjs to execGit seam delegation (#3466)
- Removes direct require('child_process') from core.cjs
- Replaces execFileSync('git check-ignore') with seam's execGit
- Local execGit wrapper now a thin adapter delegating to seam — keeps the
legacy (cwd, args) positional signature and derived timedOut field for
the verify.cjs and worktree-safety.cjs consumers that are out of Phase 2
scope (the wrapper proper would only be removed once those consumers
migrate, tracked separately)
Extends the seam's _spawnResult to expose signal and error fields so callers
can compute timedOut without bypassing the seam. The Phase 1 test suite
asserts on required field presence only, so the extension is
backward-compatible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate graphify.cjs to execTool seam (#3466)
- execGraphify: spawnSync('graphify', ...) → execTool with env passthrough,
preserving the ENOENT/TIMEOUT/EXIT_NONZERO typed reason mapping using the
seam's signal/error fields
- checkGraphifyInstalled: spawnSync('graphify', ['--help']) → execTool
- checkGraphifyVersion strategy 1: graphify --version via execTool
- checkGraphifyVersion strategy 2: python3 importlib.metadata via execTool
Adds env option to execTool — graphify needs PYTHONUNBUFFERED=1 to drain
buffered stdout on long-running operations.
Changes seam internals to access spawnSync/execFileSync via the non-destructured
childProcess module reference. Destructured imports capture references at load
time and are un-mockable by mock.method(childProcess, 'spawnSync', ...) — which
breaks all the graphify subprocess tests. Non-destructured access restores
mockability without changing public behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(shell-projection): execGit defaults to non-interactive git env (#3466)
Bakes GIT_TERMINAL_PROMPT=0 and GCM_INTERACTIVE=never into execGit's default
env. Without these, a credential prompt or terminal-input probe blocks the
git subprocess indefinitely until our 10s timeout kills it — surfacing as
a generic timeout instead of the actual auth-prompt cause.
These were previously set ad-hoc in worktree-safety.cjs's local execGitDefault
wrapper. Moving them to the seam makes them the consistent default for every
git call across the codebase. Callers can override via opts.env.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): remove core.cjs local execGit wrapper; migrate verify + worktree-safety (#3466)
Completes the Phase 2 "remove local execGit wrapper" criterion. All callers
now use the shell-projection seam's execGit(args, opts) signature directly.
- core.cjs: delete the local execGit wrapper and the execGit export. The
isGitIgnored seam check now calls execGit from the seam. Worktree-safety
function calls drop their execGit DI passthrough — worktree-safety's
internal execGitDefault now delegates to the seam and adds timedOut.
- verify.cjs: 6 callers migrate from execGit(cwd, args) to
execGit(args, { cwd }). Imports execGit from the seam directly. The
inspectWorktreeHealth DI passes the seam's execGit (worktree-safety now
matches that shape).
- worktree-safety.cjs: local execGitDefault becomes a thin adapter over
the seam — no more direct spawnSync. 11 internal callers migrate to the
new shape. DI contract for tests changes from (cwd, args) → (args, opts).
- graphify.cjs: 2 remaining execGit callers migrate from core.cjs (now
removed) to the seam directly.
- test mocks updated in 3 worktree-safety test files to match the new
(args, opts) DI shape — most mocks were shape-agnostic and required no
changes; only those that destructured cwd/args needed updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add entry for shell-projection Phase 2 migration (#3466)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(pr3476): address CodeRabbit review findings
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(state): prevent backreference expansion in state mutations
* chore(changeset): set pr field for #3463
* test(state): use structured STATE parser in bug-3454 regression