CodeRabbit surfaced one outstanding inline finding plus an outside-diff
finding and a finer point on two already-remediated sites; all addressed:
1. (inline, Minor) runtime-slash.cjs:31 — a degenerate input like `/gsd:`,
`gsd:`, or `gsd-` normalizes to empty and the previous fallback returned
the original colon-form input, reintroducing the deprecated shape the
module exists to suppress. Now returns `''` (empty string) so callers
see "no command" instead of an unroutable string. Also catches
whitespace-only inputs the same way. New unit tests pin the contract.
2. (inline, Major) drift.cjs library purity — the earlier remediation
passed `projectDir` into `detectDrift` and re-resolved runtime inside the
library. CodeRabbit (correctly) flagged this as breaking the module's
pure-library contract. detectDrift now accepts `input.runtime` directly;
verify.cmdVerifyCodebaseDrift resolves the runtime once and passes the
literal name in. drift.cjs no longer reads env or config at all.
3. (duplicate inline, Minor) gsd2-import.cjs:475 — when
`gsd2-import --path <dir>` targets a project that isn't the process
cwd, the preview command was formatted for the wrong runtime.
buildPreview now receives the resolved `projectDir` (the same one
used to find the .gsd/ root) instead of the raw `cwd`.
4. (outside-diff, Minor) tests/copilot-install.test.cjs:1-5 — the
`allow-test-rule: integration-test-input` rationale block specifically
named verify.cjs as the fixture, but #3584 changed that test to use a
synthetic input. Comment now describes the real shape of the file's
readFileSync usage (commands/, agents/, install.js source inputs to
the installer/converter functions under test) and notes the synthetic
substitution for the bin/lib path.
Full suite: 9366/9366 pass (+1 new test). Lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review surfaced five issues in the initial slash-formatter PR — three MED
and two LOW. All five are addressed:
1. (MED) formatter corrupted argument tails under codex runtime. Splitting on
the first whitespace and lowercasing only the command token preserves
path-like arguments (`Map-Codebase --paths C:\\Users\\Me\\Project`) and
case-sensitive flag values. (`runtime-slash.cjs`)
2. (MED) profile-output `cmdGenerateDevPreferences` emitted a hardcoded
`command_name: '/gsd-dev-preferences'`. Replaced with
`formatGsdSlash('dev-preferences', resolveRuntime(cwd))` so the structured
result honors codex/skills runtime distinction. (`profile-output.cjs`)
3. (MED) `drift.detectDrift` → `buildMessage` called `resolveRuntime(null)`,
ignoring a project's `.planning/config.json` `runtime` setting when
`GSD_RUNTIME` env var was absent. Threaded `projectDir` through
`detectDrift({projectDir})` → `buildMessage(..., projectDir)` →
`resolveRuntime(projectDir)`. `verify.cmdVerifyCodebaseDrift` now passes
`cwd` into the detect call. (`drift.cjs`, `verify.cjs`)
4. (LOW) `gsd2-import.buildPreview` had the same env-vs-config issue. Threaded
`cwd` through `buildPreview(..., projectDir)` for parity. (`gsd2-import.cjs`)
5. (LOW) The codex emitter test only asserted absence of `/gsd:` — a regression
to `/gsd-` (skills) form in codex output would have passed undetected.
Added positive assertions that every gsd-referencing fix string under
`GSD_RUNTIME=codex` contains `$gsd-` and contains neither `/gsd-` nor
`/gsd:`. Also added formatter unit tests pinning the argument-tail
preservation contract for both hyphen and codex runtimes.
Full suite: 9365/9365 pass (+2 new). Lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce `runtime-slash.cjs` as the single source of truth for emitting
GSD slash-command references in user-facing runtime output and persisted
artifacts. `formatGsdSlash(commandName, runtime)` produces `/gsd-<cmd>`
for skills-based runtimes (Claude/Cursor/OpenCode/Kilo/etc.) and
`$gsd-<cmd>` for Codex. The deprecated `/gsd:<cmd>` colon form is never
emitted — pasting a recommended-action command into Claude Code now
routes correctly instead of failing with `Unknown command`.
Wired into the high-impact emitters identified in #3584:
- `init.cjs` `cmdInitManager` recommended_actions[].command (the
original failure path in the bug report) plus the no-ROADMAP /
no-STATE error hints.
- `phase.cjs` `cmdPhaseAdd`, `cmdPhaseAddBatch`, `cmdPhaseInsert` —
ROADMAP.md `Plans:` references now persist the routable form
instead of the legacy colon form.
- `verify.cjs` `cmdValidateHealth` — every fix-hint addIssue() call
(E001/E002/E003/E004/E005, W002/W003/W008/W009/W011/W016/W018) and
the persisted STATE.md regenerate / MILESTONES.md backfill notes.
- `milestone.cjs` `cmdMilestoneComplete` — Operator Next Steps tail
rewrite.
- `validate-command-router.cjs` — `validate context` recommendation
strings for WARNING/CRITICAL utilization bands.
- `workstream.cjs` — missing .planning hint.
- `profile-output.cjs` — `generate-claude-md` workflow enforcement
block, project/skills fallbacks, profile placeholder, and the
dev-preferences refresh hints.
- `drift.cjs`, `gsd2-import.cjs`, `commands.cjs scaffold context` —
remaining one-off persisted references.
Runtime detection: `resolveRuntime(projectDir)` reads
`process.env.GSD_RUNTIME` first, then a side-effect-free direct read of
`.planning/config.json` (NOT `loadConfig`, which would normalize legacy
keys and re-write the file just to read the runtime name).
Tests:
- `tests/bug-3584-runtime-slash-formatter.test.cjs` — 22 unit tests
covering the pure formatter and resolver (hyphen vs codex, prefix
normalization, defensive returns, env/config/default chain).
- `tests/bug-3584-runtime-slash-emitters.test.cjs` — 6 integration
tests exercising `init manager`, `phase add` (via the structured
`roadmap get-phase` payload to avoid raw-text matching on the
on-disk artifact), `validate health`, `validate context`, and the
codex variant.
- Existing tests updated to assert the new contract: validate-context
recommendations, claude-md workflow block, milestone complete
Operator Next Steps. Copilot-install engine-conversion test now
asserts against a synthetic input since bin/lib/*.cjs no longer
contains literal `/gsd:` references for the install-time converter
to rewrite.
INVENTORY.md and INVENTORY-MANIFEST.json updated for the new module
(64 CLI modules shipped, +1).
Fixes#3584
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
* test: red — bounded git subprocess + structured worktree warnings (#3281)
Regression tests for #3281: worktree-related git subprocess calls have no
timeout bound, and timeout/error outcomes are not surfaced as structured signals.
Failing assertions:
- planWorktreePrune / listLinkedWorktreePaths / snapshotWorktreeInventory must
return reason=git_timed_out (not generic git_list_failed) when execGit returns
timedOut:true — enables callers to distinguish timeout from auth failure
- executeWorktreePrunePlan must include timedOut:true in result when the git
prune call itself times out
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): bounded git subprocess + structured warning surfacing (#3281)
Root cause (PRED.k014): execGit / execGitDefault called spawnSync with no
timeout, so `git worktree list --porcelain` against a hung/locked repo
blocked the parent process indefinitely. Downstream callers in core.cjs
and verify.cjs then swallowed any resulting failure silently via
catch { /* intentionally empty */ } (PRED.k302).
Fix:
- worktree-safety.cjs: execGitDefault now passes timeout:10000 to spawnSync.
Detects SIGTERM+ETIMEDOUT and returns { timedOut:true } in the result shape.
readWorktreeList maps timedOut:true -> reason:'git_timed_out' (distinct from
generic git_list_failed) so callers can emit a structured warning.
executeWorktreePrunePlan propagates timedOut:true as a first-class result field.
- core.cjs: execGit receives the same timeout+timedOut treatment (PRED.k014
uniform-fix discipline). pruneOrphanedWorktrees now emits a [gsd-tools]
WARNING to stderr when the git prune call times out instead of silent-catch.
- verify.cjs: Check 11 branches on worktreeHealth.ok to surface W018 warning
when the worktree list times out, instead of silent-catch on ok:false.
Backward-compatible: exitCode/stdout/stderr continue to work for all existing
callers; timedOut and error are additive new fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3283 for #3281
* fix(verify): rename W020 for worktree-timeout warning to avoid W018 collision
W018 is already used for milestone archive drift (Check 12). The new
worktree-health-degraded timeout warning was assigned W018, causing
warning-code ambiguity in triage. Rename to W020 (next available code).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(workstream): normalize migrate-name to valid slug
* docs(context): record workstream migrate-name slug invariant
* fix(catalog-cjs): balanced fallback for unknown profile (CR finding A)
profiles[profile] could return undefined for any profile key absent from
the catalog entry, causing downstream callers like formatAgentToModelMapAsTable
to crash on .length. Add ?? profiles.balanced fallback to match the SDK adapter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(sdk): anchor path resolution on import.meta.url not cwd (CR finding B)
resolve(process.cwd(), '..') breaks when Vitest is invoked from the repo root
because cwd is already the repo root and '..' goes one level above. Replace
with a file-relative path using fileURLToPath(new URL('../../../', import.meta.url))
anchored at the test file's location (sdk/src/query/).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: derive Group B runtime list from catalog (CR finding C)
Hardcoded ['kilo', 'cline', ...] throws TypeError if a runtime name is
removed from the catalog. Derive group B dynamically via
Object.keys(catalog.runtimeTierDefaults).filter(r => !r.opus) so the
test never goes stale and auto-covers future Group B additions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(workflow): add hermes to Step B runtime options (CR finding D)
hermes appears in the Group A built-in defaults table but was missing from
the AskUserQuestion options in Step B, forcing users to manually type it via
'Other (Group B or custom)'. Add explicit hermes entry for UI consistency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(config): refresh dynamic_routing tier table; fix stale L671 (findings E+F)
Finding E: tier table was missing 6 heavy-tier agents and 15 standard/light
agents added by this PR. Updated all three rows to match catalog routingTier
assignments (33 agents total).
Finding F: removed stale '18 of 31' claim and agent enumeration; replaced
with accurate note that all 33 agents have explicit catalog entries. Updated
authoritative source pointers to model-catalog.cjs / model-catalog.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(core): add profile-fallback unit tests for quality and budget (CR nitpick G)
The PR introduced quality→opus and budget→haiku unknown-agent fallbacks but
only balanced→sonnet and inherit→inherit were tested. Add two tests covering
the remaining two branches to complete coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* adr: define planning workspace and worktree seam
* refactor(worktree): extract worktree safety policy module
* refactor(workstream): extract active workstream pointer store seam
* test(worktree): cover policy branch paths and persist seam guardrails
* refactor(worktree): centralize health inventory seam for W017
* fix(workspace): align SDK project path policy with CJS planningDir
* refactor(query): unify SDK planning path projection seam
* refactor(init): route workspace projection through planningPaths seam
* docs(adr): add SDK architecture and planning path ADRs
* refactor(worktree): deepen name, pointer, inventory, and config seams
* docs(config): harmonize claude-opus-4-6 to 4-7 in resolve_model_ids example (CR finding 2)
* fix(sdk): return undefined for model_profile='inherit' sentinel (CR finding 3)
* docs(adr): renumber conflicting 0003-sdk-package-seam-module to 0007, update seam-map reference (CR finding 4)
* fix(workstream): align CJS and SDK name validation to accept dots, guard path traversal via includes('..') (CR finding 5)
* fix(sdk): guard writeActiveWorkstream against non-existent workstream directory, k014/k031 parity (CR finding 6)
* chore(changeset): add #3269 changeset (CR finding 1 — proper changeset for this PR)
* docs(inventory): register 3 new CLI modules in INVENTORY.md/MANIFEST (active-workstream-store, workstream-name-policy, worktree-safety)
* fix(sdk): use relPlanningPath(workstream) in planningPaths, fix setActiveWorkstream/getActiveWorkstream name errors in workstream.ts
* fix(sdk): validate GSD_WORKSTREAM in planningPaths before use (#3269 regression)
planningPaths() called resolveWorkspaceContext() which returned GSD_WORKSTREAM
raw (no validation). An invalid value like '../evil' was used as effectiveWorkstream,
constructing a bad path; roadmapAnalyze() caught the ENOENT and returned a
no-phase_count error object instead of the root ROADMAP result.
Fix: validate envCtx.workstream with validateWorkstreamName() in planningPaths()
before accepting it as effectiveWorkstream. Invalid env → null → root .planning/
fallback, preserving the bug-2791 contract: invalid GSD_WORKSTREAM is silently
ignored and falls back to the root context (phase_count: 0 for empty root ROADMAP).
The bug-2791 regression test now passes. No other call sites read GSD_WORKSTREAM
without validation: query-runtime-context.ts already validates; cli.ts already
validates; context-engine.ts takes a caller-validated workstream parameter.
Closes#3268 (regression introduced by #3269 workstream-name-policy work).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#3164
Validators and find-phase hardcoded phasesDir = .planning/phases/, so projects
using the milestone-archive layout (.planning/milestones/v*-phases/) had an empty
diskPhases set, triggering W006 for every active phase and find-phase returning
found:false.
Add collectDiskPhases(planBase) helper that scans both flat layout and all
.planning/milestones/v*-phases/ subdirs. Wire it into cmdValidateConsistency,
cmdValidateHealth (both the Check 4 validPhases set and Check 8 diskPhases),
and refactor cmdFindPhase to iterate candidate search dirs so it also searches
milestone-archive dirs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All workflow, command, reference, template, and tool-output files that
surfaced /gsd:<cmd> as a user-typed slash command have been updated to
use /gsd-<cmd>, matching the Claude Code skill directory name.
Closes#2697
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2633): use ROADMAP.md as authority for current-milestone phase counts
initMilestoneOp (SDK + CJS) derives phase_count and completed_phases from
the current milestone section of ROADMAP.md instead of counting on-disk
`.planning/phases/` directories. After `phases clear` at the start of a new
milestone the on-disk set is a subset of the roadmap, causing premature
`all_phases_complete: true`.
validateHealth W002 now unions ROADMAP.md phase declarations (all milestones
— current, shipped, backlog) with on-disk dirs when checking STATE.md phase
refs. Eliminates false positives for future-phase refs in the current
milestone and history-phase refs from shipped milestones.
Falls back to legacy on-disk counting when ROADMAP.md is missing or
unparseable so no-roadmap fixtures still work.
Adds vitest regressions for both handlers; all 66 SDK + 118 CJS tests pass.
* fix(#2633): preserve full phase tokens in W002 + completion lookup
CodeRabbit flagged that the parseInt-based normalization collapses distinct
phase IDs (3, 3A, 3.1) into the same integer bucket, masking real
STATE/ROADMAP mismatches and miscounting completions in milestones with
inserted/sub-phases.
Index disk dirs and validate STATE.md refs by canonical full phase token —
strip leading zeros from the integer head only, preserve [A-Z] suffix and
dotted segments, and accept just the leading-zero variant of the integer
prefix as a tolerated alias. 3A and 3 never share a bucket.
Also widens the disk and STATE.md regexes to accept [A-Z]? suffix tokens.
* feat: auto-remap codebase after significant phase execution (#2003)
Adds a post-phase structural drift detector that compares the committed tree
against `.planning/codebase/STRUCTURE.md` and either warns or auto-remaps
the affected subtrees when drift exceeds a configurable threshold.
## Summary
- New `bin/lib/drift.cjs` — pure detector covering four drift categories:
new directories outside mapped paths, new barrel exports at
`(packages|apps)/*/src/index.*`, new migration files, and new route
modules. Prioritizes the most-specific category per file.
- New `verify codebase-drift` CLI subcommand + SDK handler, registered as
`gsd-sdk query verify.codebase-drift`.
- New `codebase_drift_gate` step in `execute-phase` between
`schema_drift_gate` and `verify_phase_goal`. Non-blocking by contract —
any error logs and the phase continues.
- Two new config keys: `workflow.drift_threshold` (int, default 3) and
`workflow.drift_action` (`warn` | `auto-remap`, default `warn`), with
enum/integer validation in `config-set`.
- `gsd-codebase-mapper` learns an optional `--paths <p1,p2,...>` scope hint
for incremental remapping; agent/workflow docs updated.
- `last_mapped_commit` lives in YAML frontmatter on each
`.planning/codebase/*.md` file; `readMappedCommit`/`writeMappedCommit`
round-trip helpers ship in `drift.cjs`.
## Tests
- 55 new tests in `tests/drift-detection.test.cjs` covering:
classification, threshold gating at 2/3/4 elements, warn vs. auto-remap
routing, affected-path scoping, `--paths` sanitization (traversal,
absolute, shell metacharacter rejection), frontmatter round-trip,
defensive paths (missing STRUCTURE.md, malformed input, non-git repos),
CLI JSON output, and documentation parity.
- Full suite: 5044 pass / 0 fail.
## Documentation
- `docs/CONFIGURATION.md` — rows for both new keys.
- `docs/ARCHITECTURE.md` — section on the post-execute drift gate.
- `docs/AGENTS.md` — `--paths` flag on `gsd-codebase-mapper`.
- `docs/USER-GUIDE.md` — user-facing behavior note + toggle commands.
- `docs/FEATURES.md` — new 27a section with REQ-DRIFT-01..06.
- `docs/INVENTORY.md` + `docs/INVENTORY-MANIFEST.json` — drift.cjs listed.
- `get-shit-done/workflows/execute-phase.md` — `codebase_drift_gate` step.
- `get-shit-done/workflows/map-codebase.md` — `parse_paths_flag` step.
- `agents/gsd-codebase-mapper.md` — `--paths` directive under parse_focus.
## Design decisions
- **Frontmatter over sidecar JSON** for `last_mapped_commit`: keeps the
baseline attached to the file, survives git moves, survives per-doc
regeneration, no extra file lifecycle.
- **Substring match against STRUCTURE.md** for `isPathMapped`: the map is
free-form markdown, not a structured manifest; any mention of a path
prefix counts as "mapped territory". Cheap, no parser, zero false
negatives on reasonable maps.
- **Category priority migration > route > barrel > new_dir** so a file
matching multiple rules counts exactly once at the most specific level.
- **Empty-tree SHA fallback** (`4b825dc6…`) when `last_mapped_commit` is
absent — semantically correct (no baseline means everything is drift)
and deterministic across repos.
- **Four layers of non-blocking** — detector try/catch, CLI try/catch, SDK
handler try/catch, and workflow `|| echo` shell fallback. Any single
layer failing still returns a valid skipped result.
- **SDK handler delegates to `gsd-tools.cjs`** rather than re-porting the
detector to TypeScript, keeping drift logic in one canonical place.
Closes#2003
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(mapper): tag --paths fenced block as text (CodeRabbit MD040)
Comment 3127255172.
* docs(config): use /gsd- dash command syntax in drift_action row (CodeRabbit)
Comment 3127255180. Matches the convention used by every other command
reference in docs/CONFIGURATION.md.
* fix(execute-phase): initialize AGENT_SKILLS_MAPPER + tag fenced blocks
Two CodeRabbit findings on the auto-remap branch of the drift gate:
- 3127255186 (must-fix): the mapper Task prompt referenced
${AGENT_SKILLS_MAPPER} but only AGENT_SKILLS (for gsd-executor) is
loaded at init_context (line 72). Without this fix the literal
placeholder string would leak into the spawned mapper's prompt.
Add an explicit gsd-sdk query agent-skills gsd-codebase-mapper step
right before the Task spawn.
- 3127255183: tag the warn-message and Task() fenced code blocks as
text to satisfy markdownlint MD040.
* docs(map-codebase): wire PATH_SCOPE_HINT through every mapper prompt
CodeRabbit (review id 4158286952, comment 3127255190) flagged that the
parse_paths_flag step defined incremental-remap semantics but did not
inject a normalized variable into the spawn_agents and sequential_mapping
mapper prompts, so incremental remap could silently regress to a
whole-repo scan.
- Define SCOPED_PATHS / PATH_SCOPE_HINT in parse_paths_flag.
- Inject ${PATH_SCOPE_HINT} into all four spawn_agents Task prompts.
- Document the same scope contract for sequential_mapping mode.
* fix(drift): writeMappedCommit tolerates missing target file
CodeRabbit (review id 4158286952, drift.cjs:349-355 nitpick) noted that
readMappedCommit returns null on ENOENT but writeMappedCommit threw — an
asymmetry that breaks first-time stamping of a freshly produced doc that
the caller has not yet written.
- Catch ENOENT on the read; treat absent file as empty content.
- Add a regression test that calls writeMappedCommit on a non-existent
path and asserts the file is created with correct frontmatter.
Test was authored to fail before the fix (ENOENT) and passes after.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commands are now installed as commands/gsd/<name>.md and invoked as
/gsd:<name> in Claude Code. The old hyphen form /gsd-<name> was still
hardcoded in hundreds of places across workflows, references, templates,
lib modules, and command files — causing "Unknown command" errors
whenever GSD suggested a command to the user.
Replace all /gsd-<cmd> occurrences where <cmd> is a known command name
(derived at runtime from commands/gsd/*.md) using a targeted Node.js
script. Agent names, tool names (gsd-sdk, gsd-tools), directory names,
and path fragments are not touched.
Adds regression test tests/bug-2543-gsd-slash-namespace.test.cjs that
enforces zero legacy occurrences going forward. Removes inverted
tests/stale-colon-refs.test.cjs (bug #1748) which enforced the now-obsolete
hyphen form; the new bug-2543 test supersedes it. Updates 5 assertion
tests that hardcoded the old hyphen form to accept the new colon form.
Closes#2543
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds artifacts.cjs with canonical .planning/ root file names, W019 warning
in gsd-health that flags unrecognized .md files at the .planning/ root, and
templates/README.md as the authoritative artifact index for agents and humans.
Closes#2448
Adds W018 warning when .planning/milestones/vX.Y-ROADMAP.md snapshots
exist without a corresponding entry in MILESTONES.md. Introduces
--backfill flag to synthesize missing entries from snapshot titles.
Closes#2446
Add W017 warning to cmdValidateHealth that detects linked git worktrees that are stale (older than 1 hour, likely from crashed agents) or orphaned (path no longer exists on disk). Parses git worktree list --porcelain output, skips the main worktree, and provides actionable fix suggestions. Gracefully degrades if git worktree is unavailable.
Closes#2167
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cmdValidateHealth read the phases directory four separate times for
checks 6 (naming), 7 (orphaned plans), 7b (validation artifacts), and
8 (roadmap cross-reference). Hoist the directory listing into a single
readdirSync call with a shared Map of per-phase file lists.
Reduces syscalls from ~3N+1 to N+1 where N is the number of phase
directories.
Closes#1973
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
W006 (Phase in ROADMAP.md but no directory on disk) fired for every phase
listed in ROADMAP.md that lacked a phase directory, including future phases
that haven't been started yet. This produced false DEGRADED health status
on any project with more than one phase planned.
Fix: before emitting W006, check the ROADMAP summary list for a
'- [ ] **Phase N:**' unchecked checkbox. Phases explicitly marked as not
yet started are intentionally absent from disk -- skip W006 for them.
Phases with a checked checkbox ([x]) or with no summary entry still
trigger W006 as before.
Adds two regression tests: one verifying W006 is suppressed for unchecked
phases, and one verifying W006 still fires for checked phases with no disk
directory.
* 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)
* test: add stale /gsd: colon reference regression guard
Fixes#1748
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace 39 stale /gsd: colon references with /gsd- hyphen format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#1707
Extracts config defaults from loadConfig() into an exported
CONFIG_DEFAULTS constant in core.cjs. config.cjs and verify.cjs
now reference CONFIG_DEFAULTS instead of duplicating values,
preventing future divergence.
* fix: replace /gsd: command format with /gsd- skill format in all suggestions
All next-step suggestions shown to users were still using the old colon
format (/gsd:xxx) which cannot be copy-pasted as skills. Migrated all
occurrences across agents/, commands/, get-shit-done/, docs/, README files,
bin/install.js (hardcoded defaults for claude runtime), and
get-shit-done/bin/lib/*.cjs (generate-claude-md templates and error messages).
Updated tests to assert new hyphen format instead of old colon format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate remaining /gsd: format to /gsd- in hooks, workflows, and sdk
Addresses remaining user-facing occurrences missed in the initial migration:
- hooks/: fix 4 user-facing messages (pause-work, update, fast, quick)
and 2 comments in gsd-workflow-guard.js
- get-shit-done/workflows/: fix 21 Skill() literal calls that Claude
executes directly (installer does not transform workflow content)
- sdk/prompt-sanitizer.ts: update regex to strip /gsd- format in addition
to legacy /gsd: format; update JSDoc comment
- tests/: update autonomous-ui-steps, prompt-sanitizer to assert new format
Note: commands/gsd/*.md frontmatter (name: gsd:xxx) intentionally unchanged
— installer derives skillName from directory path, not the name field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plan-phase): preserve --chain flag in auto-advance sync and handle ui-phase gate in chain mode
Bug 1: step 15 sync-flag check only guarded against --auto, causing
_auto_chain_active to be cleared when plan-phase is invoked without
--auto in ARGUMENTS even though a --chain pipeline was active. Added
--chain to the guard condition, matching discuss-phase behaviour.
Bug 2: UI Design Contract gate (step 5.6) always exited the workflow
when UI-SPEC was missing, breaking the discuss --chain pipeline
silently. When _auto_chain_active is true, the gate now auto-invokes
gsd-ui-phase --auto via Skill() and continues to step 6 without
prompting. Manual invocations retain the existing AskUserQuestion flow.
* fix: remove <sub>/clear</sub> pattern and duplicate old-format command in discuss-phase.md
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add W011: detect when STATE.md says current phase is N but ROADMAP.md shows it as [x] complete (can happen after crash mid-phase-complete). Add W012-W015: validate branching_strategy against known values, context_window as positive integer, phase_branch_template has {phase} placeholder, milestone_branch_template has {milestone} placeholder.
Add stateReplaceFieldWithFallback diagnostic: warn when neither primary nor fallback field name matches in STATE.md (surfaces template drift from external edits).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GSD agents silently skip database schema push — verification passes but
production breaks because TypeScript types come from config, not the live
database. This adds two layers of protection:
1. Plan-phase template detection (step 5.7): When the planner detects
schema-relevant file patterns in the phase scope, it injects a mandatory
[BLOCKING] schema push task into the plan with the appropriate push
command for the detected ORM.
2. Post-execution drift detection gate: After execution completes but
before verification marks success, scans for schema-relevant file
changes and checks if a push command was executed. Blocks verification
with actionable guidance if drift is detected.
Supports Payload CMS, Prisma, Drizzle, Supabase, and TypeORM.
Override with GSD_SKIP_SCHEMA_CHECK=true.
Fixes#1381
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When GSD agents are not installed in .claude/agents/, Task(subagent_type="gsd-*")
silently falls back to general-purpose, losing specialized instructions, structured
outputs, and verification protocols.
What changed:
- Added checkAgentsInstalled() to core.cjs that validates all expected agents exist on disk
- All init commands now include agents_installed and missing_agents in their output
- Health check (validate health) reports W010 when agents are missing or incomplete
- New validate agents subcommand for standalone agent installation diagnostics
Fixes#1371
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enable multiple Claude Code instances to work on the same codebase
simultaneously by scoping .planning/ state into workstreams.
Core changes:
- planningDir(cwd, ws?) and planningPaths(cwd, ws?) are now workstream-aware
via GSD_WORKSTREAM env var (auto-detected from --ws flag or active-workstream file)
- All bin/lib modules use planningDir(cwd) for scoped paths (STATE.md, ROADMAP.md,
phases/, REQUIREMENTS.md) and planningRoot(cwd) for shared paths (milestones/,
PROJECT.md, config.json, codebase/)
- New workstream.cjs module: create, list, status, complete, set, get, progress
- gsd-tools.cjs: --ws flag parsing with priority chain
(--ws > GSD_WORKSTREAM env > active-workstream file > flat mode)
- Collision detection: transition.md checks for other active workstreams before
suggesting next-milestone continuation (prevents WS A from trampling WS B)
- ${GSD_WS} routing propagation across all 9 workflow files ensures workstream
scope chains automatically through the workflow lifecycle
New files:
- get-shit-done/bin/lib/workstream.cjs (CRUD + collision detection)
- get-shit-done/commands/gsd/workstreams.md (slash command)
- get-shit-done/references/workstream-flag.md (documentation)
- tests/workstream.test.cjs (20 tests covering CRUD, env var routing, --ws flag)
All 1062 tests passing (1042 existing + 20 new workstream tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stripShippedMilestones() uses a negative heuristic: strip all <details>
blocks, assume what remains is the current milestone. This breaks when
agents accidentally wrap the current milestone in <details> for
collapsibility — all downstream consumers then see an empty milestone.
Observed failure: cmdPhaseComplete() returns is_last_phase: true and
next_phase: null for non-final phases because the current milestone's
phases were stripped along with shipped ones.
Added extractCurrentMilestone(content, cwd) — a positive lookup that:
1. Reads the current milestone version from STATE.md frontmatter
2. Falls back to 🚧 in-progress marker in ROADMAP.md
3. Finds the section heading matching that version
4. Returns only that section's content
5. Falls back to stripShippedMilestones() if version can't be determined
Updated 12 call sites across 6 files to use extractCurrentMilestone:
- core.cjs: getRoadmapPhaseInternal(), getMilestonePhaseFilter()
- phase.cjs: cmdPhaseAdd(), cmdPhaseInsert(), cmdPhaseComplete() (2 sites)
- roadmap.cjs: cmdRoadmapGetPhase(), cmdRoadmapAnalyze()
- commands.cjs: stats/progress display
- verify.cjs: phase verification (2 sites)
- init.cjs: project initialization
Kept stripShippedMilestones() for:
- getMilestoneInfo() — determines the version itself, can't use positive lookup
- replaceInCurrentMilestone() — write operations, conservative boundary
- extractCurrentMilestone() fallback — when no version available
All 755 tests pass.
Fixes#1145
Two fixes to verify.cjs:
1. Add CWD guard to cmdValidateHealth — detects when CWD is the home
directory (likely accidental) and returns error E010 before running
checks that would read the wrong .planning/ directory.
2. Import and apply stripShippedMilestones to both cmdValidateConsistency
and cmdValidateHealth (Check 8) — prevents false warnings when
archived milestones reuse phase numbers.
This PR subsumes #1071 (strip archived milestones) to avoid merge
conflicts on the same import line.
Addresses patch 2 from #899, fixes#1060
Two bugs caused workflow.research to silently reset to false:
1. new-milestone.md unconditionally overwrote workflow.research via
config-set after asking about research — coupling a per-milestone
decision to a persistent user preference. Now the research question
is per-invocation only; persistent config changes via /gsd:settings.
2. verify.cjs health repair (createConfig/resetConfig) used flat keys
(research, plan_checker, verifier) instead of the canonical nested
workflow object from config.cjs, also missing branch templates,
nyquist_validation, and brave_search fields.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: change nyquist_validation default to true and harden absent-key skip conditions
new-project.md never wrote the key, so agents reading config directly
treated absent as falsy. Changed all agent skip conditions from "is false"
to "explicitly set to false; absent = enabled". Default changed from false
to true in core.cjs, config.cjs, and templates/config.json.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enforce VALIDATION.md creation with verification gate and Check 8e
Step 5.5 was narrative markdown that Claude skipped under context pressure.
Now MANDATORY with Write tool requirement and file-existence verification.
Step 7.5 gates planner spawn on VALIDATION.md presence. Check 8e blocks
Dimension 8 if file missing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add W008/W009 health checks and addNyquistKey repair for Nyquist drift detection
W008 warns when workflow.nyquist_validation key is absent from config.json
(agents may skip validation). W009 warns when RESEARCH.md has Validation
Architecture section but no VALIDATION.md file exists. addNyquistKey repair
adds the missing key with default true value.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add /gsd:validate-phase command and gsd-nyquist-auditor agent
Retroactively applies Nyquist validation to already-executed phases.
Works mid-milestone and post-milestone. Detects existing test coverage,
maps gaps to phase requirements, writes missing tests, debugs failing
ones, and produces {phase}-VALIDATION.md from existing artifacts.
Handles three states: VALIDATION.md exists (audit + update), no
VALIDATION.md (reconstruct from PLAN.md + SUMMARY.md), phase not yet
executed (exit cleanly with guidance).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: audit-milestone reports Nyquist compliance gaps across phases
Adds Nyquist coverage table to audit-milestone output when
workflow.nyquist_validation is true. Identifies phases missing
VALIDATION.md or with nyquist_compliant: false/partial.
Routes to /gsd:validate-phase for resolution. Updates USER-GUIDE
with retroactive validation documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: compress Nyquist prompts to match GSD meta-prompt density conventions
Auditor agent: deleted philosophy section (35 lines), compressed execution
flow 60%, removed redundant constraints. Workflow: cut purpose bloat,
collapsed state narrative, compressed auditor spawn template. Command:
removed redundant process section. Plan-phase Steps 5.5/7.5: replaced
hedging language with directives. Audit-milestone Step 5.5: collapsed
sub-steps into inline instructions. Net: -376 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Phase number parsing only matched single-decimal (e.g. 03.2) but crashed
on multi-level decimals (e.g. 03.2.1). Requirement IDs with regex
metacharacters (parentheses, dots) were interpolated raw into RegExp
constructors, causing SyntaxError crashes.
- Add escapeRegex() utility for safe regex interpolation
- Update normalizePhaseName/comparePhaseNum for multi-level decimals
- Replace all .replace('.', '\\.') with escapeRegex() across modules
- Escape reqId before regex interpolation in cmdPhaseComplete
- Update all phase dir matching regexes from (?:\.\d+)? to (?:\.\d+)*
- Add regression test for phase complete 03.2.1
Closes#621
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>