* 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>
* fix(config): route CRUD through planningDir to honor GSD_PROJECT
PR #1484 added planningDir(cwd) and the GSD_PROJECT env var so a workspace
can host multiple projects under .planning/{project}/. loadConfig() in
core.cjs (line 256) was migrated at the time, but the four CRUD entry points
in config.cjs and the planningPaths() helper in core.cjs were left resolving
against planningRoot(cwd).
The result was a silent split-brain in any multi-project workspace:
- cmdConfigGet, setConfigValue, ensureConfigFile, cmdConfigNewProject
all wrote to and read from .planning/config.json
- loadConfig read from .planning/{GSD_PROJECT}/config.json
So `gsd-tools config-get workflow.discuss_mode` returned "unset" even when
the value was correctly stored in the project-routed file, because the
reader and writer pointed at different paths.
planningPaths() carried a comment that "Shared paths (project, config)
always resolve to the root .planning/" which described the original intent,
but loadConfig() already contradicted that intent for config.json. project
and config now both resolve through planningDir() so the contract matches
the only function that successfully read config.json in the multi-project
case.
Single-project users (no GSD_PROJECT set) are unaffected: planningRoot()
and planningDir() return the same path when no project is configured.
Verification: in a workspace with .planning/projectA/config.json and
GSD_PROJECT=projectA, `gsd-tools config-get workflow.discuss_mode` now
returns the value instead of "Error: Key not found". Backward compat
verified by running the same command without GSD_PROJECT in a
single-project layout.
Affected sites:
- get-shit-done/bin/lib/config.cjs cmdConfigNewProject (line 199)
- get-shit-done/bin/lib/config.cjs ensureConfigFile (line 244)
- get-shit-done/bin/lib/config.cjs setConfigValue (line 294)
- get-shit-done/bin/lib/config.cjs cmdConfigGet (line 367)
- get-shit-done/bin/lib/core.cjs planningPaths.config (line 706)
- get-shit-done/bin/lib/core.cjs planningPaths.project (line 705)
* fix(template): emit project-aware references in template fill plan
The template fill plan body hardcoded `@.planning/PROJECT.md`,
`@.planning/ROADMAP.md`, and `@.planning/STATE.md` references. In a
multi-project workspace these resolve to nothing because the actual
project, roadmap, and state files live under .planning/{GSD_PROJECT}/.
`gsd-tools verify references` reports them as missing on every PLAN.md
generated by template fill in any GSD_PROJECT-routed workspace.
Fix: route the references through planningDir(cwd), normalize via the
existing toPosixPath helper for cross-platform path consistency, and
embed them as `@<relative-path>` matching the phase-relative reference
pattern used elsewhere in the file.
Single-project users (no GSD_PROJECT set) get exactly the same output
as before because planningDir() falls back to .planning/ when no project
is active.
Affected site: get-shit-done/bin/lib/template.cjs cmdTemplateFill plan
branch (lines 142-145, the @.planning/ refs in the Context section).
* fix(verify): planningDir for cmdValidateHealth and regenerateState
cmdValidateHealth resolved projectPath and configPath via planningRoot(cwd)
while ROADMAP/STATE/phases/requirements went through planningDir(cwd). The
inconsistency reported "missing PROJECT.md" and "missing config.json" in
multi-project layouts even when the project-routed copies existed and the
config CRUD writers (now also routed by the previous commit in this PR)
were writing to them.
regenerateState (the /gsd:health --repair STATE.md regeneration path)
hardcoded `See: .planning/PROJECT.md` in the generated body, which fails
the same reference check it just regenerated for in any GSD_PROJECT-routed
workspace.
Fix: route both sites through planningDir(cwd). For regenerateState, derive
a POSIX-style relative reference from the resolved path so the reference
matches verify references' resolution rules. Also dropped the planningRoot
import from verify.cjs since it is no longer used after this change.
Single-project users (no GSD_PROJECT set) get the same paths as before:
planningDir() falls back to .planning/ when no project is configured.
Affected sites:
- get-shit-done/bin/lib/verify.cjs cmdValidateHealth (lines 536-541)
- get-shit-done/bin/lib/verify.cjs regenerateState repair (line 865)
- get-shit-done/bin/lib/verify.cjs core.cjs import (line 8, dropped unused
planningRoot)
Three files emit path.join or path.relative results directly into
JSON output without normalizing backslashes on Windows. Wrap these
with toPosixPath (already used in core.cjs and init.cjs) so agents
receive consistent forward-slash paths on all platforms.
Files changed:
- phase.cjs: wrap directory path in cmdPhasesFind
- commands.cjs: wrap todo path in cmdListTodos, relPath in cmdScaffold
- template.cjs: wrap relPath in cmdTemplateFill (both exists and
created branches)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Break the 5324-line monolith into focused modules:
- core.cjs: shared utilities, constants, internal helpers
- frontmatter.cjs: YAML frontmatter parsing/serialization/CRUD
- state.cjs: STATE.md operations + progression engine
- phase.cjs: phase CRUD, query, and lifecycle
- roadmap.cjs: roadmap parsing and updates
- verify.cjs: verification suite + consistency/health validation
- config.cjs: config ensure/set/get
- template.cjs: template selection and fill
- milestone.cjs: milestone + requirements lifecycle
- commands.cjs: standalone utility commands
- init.cjs: compound init commands for workflow bootstrapping
gsd-tools.cjs is now a thin CLI router (~550 lines including
JSDoc) that imports from lib/ modules. All 81 tests pass.
Also updates package.json test script to point to tests/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>