Adds a new `phase_status` field to the `init.plan-phase` SDK + CJS query
output and a §1.5 "Closed-Phase Gate" in workflows/plan-phase.md that
short-circuits on closed phases instead of silently replanning over
shipped code.
## What was broken
`gsd-sdk query init.plan-phase <N>` returned the same "ready to plan"
payload for a closed phase (REQUIREMENTS Met, VERIFICATION.md status:
passed, ROADMAP flipped) as for an open one. No field signaled closure,
so `/gsd:plan-phase --reviews` happily replanned over closed phases —
risking documentation drift on already-shipped code.
## Fix
- Export `determinePhaseStatus` from `commands.cjs` (already present, was
module-private).
- Both `cmdInitPlanPhase` (CJS) and `initPlanPhase` (TS SDK) now compute
`phase_status` from plan/summary counts + VERIFICATION.md status using
the existing `determinePhaseStatus` helper — the project-wide phase
lifecycle vocabulary (Pending | Planned | In Progress | Executed |
Complete | Needs Review). No directory yet → Pending.
- Workflow `plan-phase.md` adds §1.5 "Closed-Phase Gate":
- `phase_status == "Complete"` with `--reviews` → hard-stop, no
override (replanning a closed phase via review feedback is never
legitimate; concerns belong in a follow-up phase or new issue).
- `phase_status == "Complete"` without `--force` → exit with a clear
notice pointing at VERIFICATION.md.
- `phase_status == "Complete"` with `--force` → continue with a
transcript banner so the deliberate replan is visible.
`Executed` and `Needs Review` are intentionally not gated — those mean
planning finished but verification did not pass, and replanning is the
correct next step.
## Tests
- SDK: 4 new `phase_status` cases in init.test.ts covering Pending /
Planned / Executed / Complete transitions.
- Existing init.plan-phase golden parity test continues to pass (the
`researcher_model: '' vs sonnet` drift in that test predates this
change and is unrelated).
- Full Mac+Docker suite: 9323 / 9323 passed (Mac), 9318 / 9323 passed
(Docker, 5 skipped).
Fixes#3569
Phase 4 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the `findProjectRoot` duplication that lived at
bin/lib/core.cjs:74-140 and sdk/src/query/helpers.ts:497-590,
the drift carrier behind historical bugs #1362 and #2561.
- sdk/src/project-root/index.ts — source of truth (120 lines,
pure-with-sync-fs). Exports findProjectRoot(startDir: string)
and FIND_PROJECT_ROOT_MAX_DEPTH constant.
- sdk/src/project-root/index.test.ts — 13 vitest pinning fixtures
covering all four heuristics, the #1362 guard, malformed
config fallback, empty sub_repos, deep nesting, and depth-limit
enforcement.
- sdk/scripts/gen-project-root.mjs — generator. Captures
function body via Function.prototype.toString() from compiled
sdk/dist/. Emits CJS preamble for destructured node:fs /
node:path / node:os imports.
- sdk/scripts/check-project-root-fresh.mjs — freshness check.
Imports the generator function directly (Phase 3's cleaner
pattern).
- get-shit-done/bin/lib/project-root.generated.cjs — generator-
emitted CJS mirror.
- tests/project-root-generator.test.cjs — 11 parity assertions
comparing SDK source and generated CJS for every fixture.
- sdk/src/query/helpers.ts: -127 lines. The 94-line inline
findProjectRoot plus the FIND_PROJECT_ROOT_MAX_DEPTH constant
(originally at line 471) replaced by a single re-export:
`export { findProjectRoot } from '../project-root/index.js';`
Removed unused `parse as parsePath` import.
- get-shit-done/bin/lib/core.cjs: -83 lines net. The 67-line
inline findProjectRoot replaced by a single
`require('./project-root.generated.cjs')`. The detectSubRepos
helper at lines 40-56 stays (used by loadConfig migration).
- sdk/package.json: gen:project-root + check:project-root-fresh
scripts.
- package.json: proxy for the freshness check.
- .githooks/pre-commit: drift block.
- .github/workflows/test.yml: drift check step after the
state-document drift step.
- CONTEXT.md: Project-Root Resolution Module entry.
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json:
+1 module count, +1 row.
- Full suite: 9226/9226 pass (baseline 9215 + 11 new parity
fixtures).
- SDK vitest: 1804/1804 pass.
- Reader shrink: -127 SDK + -83 CJS = 210 lines of duplication
deleted across the two Readers. New shared Module is 120 lines.
1. Depth limit canonicalization. CJS findProjectRoot previously
had no explicit walk-up bound (walked until dir === root or
homedir). The new Module uses FIND_PROJECT_ROOT_MAX_DEPTH = 10,
matching the SDK's pre-existing value. Only affects paths
nested more than 10 levels deep from a .planning/ root — a
pathological case in practice. None of the existing 22 CJS
findProjectRoot tests covered this; the new parity test does.
2. platformReadSync → readFileSync. The old CJS findProjectRoot
used the platformReadSync wrapper from
shell-command-projection.cjs for reading .planning/config.json,
which returns null on read failure. The Module uses raw
readFileSync, which throws — caught by the surrounding
try/catch that already swallowed errors. Functionally
equivalent for the existing code path; no test exercises the
null-return semantic.
Closes#3553.
Phase 3 of the CJS↔SDK hard-seam migration (parent #3524).
Introduces the Builder/Reader pattern for paired Modules with
mixed pure-and-I/O concerns — the template for Phase 4 and
follow-up enhancements that migrate other paired Modules.
Phase 1 and Phase 2 migrated Modules where both sides used
character-equivalent logic. Phase 3 introduces the case where
the pure logic is shareable but the I/O is legitimately per-side.
The Builder/Reader split resolves this:
- The Builder is pure — accepts pre-collected data
(BuilderInputs struct), returns the typed projection. One
source of truth; one generator-emitted CJS mirror. Drift
is structurally impossible.
- The Readers are per-side hand-authored Adapters that do the
fs reads in their native idiom (currently both sync; either
side can go async later without touching the Builder), then
delegate to the Builder.
- sdk/src/workstream-inventory/builder.ts — Builder source.
170 lines. Pure. Exports buildWorkstreamInventory(inputs),
isCompletedInventory(status), plus the three typed inventory
interfaces (WorkstreamPhaseInventory, WorkstreamInventory,
WorkstreamInventoryList).
- sdk/src/workstream-inventory/builder.test.ts — 18 vitest
pinning fixtures across all status branches, progress-percent
clamping, active-marker projection, and isCompletedInventory
classifier.
- sdk/scripts/gen-workstream-inventory-builder.mjs — generator.
Captures function bodies via Function.prototype.toString();
emits with the standard GENERATED FILE banner. Includes a
small `const relative = path.relative;` preamble in the
output to handle ESM destructured imports in the compiled
source.
- sdk/scripts/check-workstream-inventory-builder-fresh.mjs —
freshness check. Imports the generator function directly
(rather than duplicating logic) — a cleaner pattern than
Phase 1/2's approach.
- get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs —
generator-emitted CJS mirror.
- tests/workstream-inventory-builder-generator.test.cjs — 16
parity assertions confirming CJS-generated output ==
SDK source output for every fixture.
- bin/lib/workstream-inventory.cjs: 159 → 132 lines.
Projection logic gone. `inspectWorkstream` and
`listWorkstreamInventories` collect BuilderInputs via the
existing sync fs functions and delegate to the Builder.
`isCompletedInventory` re-exported from the Builder (its
signature changed from object→string, but no external
callers exist so the change is safe).
- sdk/src/query/workstream-inventory.ts: 196 → 143 lines.
Same shape, sync fs (the SDK was already sync — surprise from
recon). Types re-exported from the Builder.
- sdk/package.json: gen:workstream-inventory-builder and
check:workstream-inventory-builder-fresh scripts.
- package.json: proxy for the freshness check.
- .githooks/pre-commit: drift block.
- .github/workflows/test.yml: drift check step.
- CONTEXT.md: amended "Workstream Inventory Module" entry
to document the Builder/Reader split.
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json:
+1 module count, +1 row for the generated builder.
- Full suite: 9229/9229 pass (baseline 9215 + 14 net new from
the parity assertions).
- Vitest: 18 Builder fixtures pass.
- Reader shrink: -27 lines on CJS, -53 lines on SDK.
- Net diff (modified files only): +68 / -133 = 65-line
reduction. New files (Builder, generator, freshness check,
parity test) add ~600 lines of new structured code.
1. `isCompletedInventory` signature changed from
isCompletedInventory(inventory: object) to
isCompletedInventory(status: string). Original CJS exported
the object form but no external caller passed an object —
they all passed inventory.status. Verified by grep before
committing.
2. Generator preamble. The compiled ESM uses
`import { relative } from 'node:path'`, making `relative`
a free variable in `buildWorkstreamInventory`. The generator
emits `const relative = path.relative;` so the captured
function body works in CJS.
3. Freshness check imports the generator. The freshness check
imports the generator's buildWorkstreamInventoryBuilderCjs()
function directly rather than duplicating generation logic.
Cleaner than Phase 1/2; future generators should follow this.
Shareable via the Builder/Reader pattern in future enhancements:
- frontmatter (pure YAML/markdown parsing)
- plan-scan (pure PLAN.md structure parsing)
- decisions (pure decision-record parsing)
- secrets (regex-based detection in text)
- uat (UAT-criteria parsing)
Structural divergence — different approach needed:
- state — sync vs async file ops; mutation paths differ.
- workstream — lifecycle ops; per-side API surface differs.
- phase, roadmap, init, profile-output, template — large
surfaces; each its own potential enhancement.
None of these is in scope for Phase 3.
Closes#3544.
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.
* 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
Closes#3522. When --respect-staged is passed the git add loop is skipped
entirely so per-hunk staging from git add -p is preserved. Default behavior
(full re-stage) is unchanged. The #3061 pathspec invariant holds under both
modes. Nothing-staged within scope returns { committed: false, reason:
'nothing staged' } without error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
The DAG resolver in phase-plan-index only matched full-stem
('03-01-auth-hardening') and canonical-prefix ('03-01') forms when
resolving `depends_on` references, so plans in decimal phases
(e.g. `99.9-test`, `02.2-cross-repo`) declaring short-form
`depends_on: [01]` had their edges silently dropped. Dependents
collapsed into wave 1 and the SDK emitted a misleading
"declared wave: N but depends_on DAG places it in wave 1" warning
that pointed at the wave declaration rather than the broken reference.
Add a tertiary short-form index keyed on the trailing `-NN` of each
plan's canonical ID — derived per plan via `lastIndexOf('-')` so it
handles integer, letter-suffixed, and decimal phase IDs uniformly.
Emit a dedicated `Plan X: unresolved depends_on reference 'NN' — no
matching plan in phase` warning whenever a dep fails all three lookup
forms, so a dropped edge can no longer hide behind the wave-mismatch
warning.
Regression coverage added in `sdk/src/query/phase.test.ts` for the
decimal-phase short-form case, the integer-phase short-form case, and
the unresolved-reference warning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a roadmap places shared phase-detail bodies under a non-version-prefixed
`## Phase Details` heading AFTER a `### 📋 vX.Y+ (Planned)` sibling in document
order, the entire Phase Details section fell outside the slice returned by
`extractCurrentMilestone`. `gsd-sdk query phase.insert N` then reported
"Phase N not found in ROADMAP.md" even though `### Phase N:` was unambiguously
present.
PR #2455 (closing #2422) added a same-version `continue` branch that already
handles the version-prefixed variant (e.g. `## v2.0 Phase Details`). This fix
extends the same intent to the generic-label variant: after the initial
boundary scan, look for a literal `^#{1,3}\s+Phase\s+Details\b` heading past
`sectionEnd` and, if found, append the Phase Details block (up to the next
real milestone boundary — version-bearing or milestone-emoji-bearing heading —
or EOF) to the returned slice. The intervening planned-milestone content is
skipped so it does not leak into the active-milestone view.
Bounded to a single append so a malformed roadmap can't loop. Only matches the
literal `Phase Details` label (canonical per GSD ROADMAP template); anything
else continues to terminate the slice. Does not regress the v2.0/v2.1+
version-prefixed handling shipped by #2455 (existing `bug-2422` test still
passes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(execute-phase): classify quota/rate-limit failures across runtimes (#3095)
Dispatched executor subagents that die from provider quota or rate-limit
errors currently look identical to a crashed agent to the orchestrator —
so step 7's recovery prompt offers "retry now" when the right action is
"wait for reset and resume". This adds a runtime-agnostic classifier and
wires execute-phase step 7 to it.
- `agent.classify-failure` SDK query returns
`{class: 'quota-exceeded' | 'classify-handoff-bug' | 'unknown-failure',
sentinel?, retryAfterSeconds?}`. Sentinels cover Claude Code
(`usage limit`, `429`), Copilot CLI (`rate_limit`,
`user_weekly_rate_limited`), Codex (`usage_limit_reached`,
`too many requests`), and Gemini (`RESOURCE_EXHAUSTED`,
`exceeded your`).
- `execute-phase.md` step 7 now branches on the class. Quota-exceeded
presents a wait-for-reset prompt and points at the safe-resume gate
landing in #3212 instead of re-dispatching a fresh executor.
- `docs/research/provider-rate-limit-signals.md` records the proactive
(header / SDK event) signals each provider exposes and the upstream
Claude Code / Copilot / Codex issues blocking hook-side detection —
the forward path once host runtimes surface them.
Resume-from-partial-worktree and context-load metrics from the original
report are deliberately out of scope; they overlap #3212's
`state.verify-against-disk` work already in flight.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(execute): render quota retry hint and refresh alias artifacts
* fix(workflow): restore slash namespace and execute-phase size budget
---------
Co-authored-by: Claude Opus 4.7 (1M context) <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
* fix(phase): parse remove --force regardless of position
* chore(changeset): add fragment for phase-remove flag fix
* fix(phase): fail when phase.remove target is missing
* feat(sdk): deepen compatibility seam for legacy profile path
* chore(changeset): set PR number for #3419
* test(sdk): make query seam wiring assertion behavioral
* test(3309): red — workflow.human_verify_mode contract
New behavioral test file covers:
- workflow.human_verify_mode is a recognized config key (VALID_CONFIG_KEYS)
- defaults to 'mid-flight' (preserves current behavior)
- config-set / config-get round-trips for both values
- persists in config.json as string
- planner agent file references the flag with canonical wording, couples
end-of-phase mode with the rule that checkpoint:human-verify is not
emitted, and documents the <verify><human-check> deferred-item shape
- verifier agent file references harvesting <verify><human-check> blocks
- references/checkpoints.md documents the cost-control alternative
Source-text assertions on agent .md files are exempted via
allow-test-rule: source-text-is-the-product — those files ARE the
runtime contract loaded by AI runtimes, so asserting their wording is
the only way to verify the agents will respect the flag.
Fails 10/11 against current source. Will pass after the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(3309): add workflow.human_verify_mode = end-of-phase opt-out
Each mid-flight checkpoint:human-verify halt costs a full executor
cold-start (CLAUDE.md, MEMORY.md, STATE.md, plan re-read on every
respawn) because subagent context is discarded across the pause. A plan
with N human-verify checkpoints pays the cold-start cost N+1 times. The
reporter (rentanything-nb) measured this at "tens of thousands of tokens"
per round-trip and "hundreds of thousands per week."
This adds workflow.human_verify_mode (default 'mid-flight') with an
'end-of-phase' value that:
- instructs gsd-planner to NOT emit <task type="checkpoint:human-verify">
tasks; verification details go into a <verify><human-check> sub-block
on the relevant auto task instead
- instructs gsd-verifier (Step 8) to harvest those <verify><human-check>
blocks at end-of-phase and merge them into its own human-verification
list
- the existing human_needed → HUMAN-UAT.md flow in execute-phase.md is
the single sink — no new file/writer is created
checkpoint:decision and checkpoint:human-action are unaffected — those
gate the work itself, not post-hoc verification.
Surfaces touched:
- bin/lib/config-schema.cjs, bin/lib/config.cjs — register key + default
- sdk/src/config.ts, sdk/src/query/config-schema.ts — SDK parity
- agents/gsd-planner.md — slim Detection section + reference link
- agents/gsd-verifier.md — Step 8 harvest instruction
- get-shit-done/references/planner-human-verify-mode.md — full rules,
loaded conditionally to keep planner.md under its size budget
- get-shit-done/references/checkpoints.md — surface the alternative
- docs/CONFIGURATION.md — config table row
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json — track new reference
Tag name <human-check> chosen instead of <human> to avoid the
prompt-injection scan pattern that flags <system|assistant|human> tags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3309): align changeset pr: to actual PR number
The pr: field was authored as 3319 (a guess at the next number) before
the PR was opened. Actual PR is #3325.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(3309): flip workflow.human_verify_mode default to end-of-phase
Per maintainer direction on PR #3325, end-of-phase is the new project
default. Mid-flight checkpoint:human-verify halts cost a full executor
cold-start (CLAUDE.md, MEMORY.md, STATE.md, plan re-read on respawn) per
round-trip — reported at "tens of thousands of tokens" per round-trip,
"hundreds of thousands per week" on real projects. The cost-control
mode is what new projects should get out of the box.
mid-flight remains a one-line opt-back-in via:
gsd config-set workflow.human_verify_mode mid-flight
Behavior change for existing projects: the new default takes effect
when .planning/config.json is rewritten (config-set, fresh project).
Existing in-flight PLAN.md files with checkpoint:human-verify tasks
continue to work in either mode — the flag only changes what the
planner emits next time it runs.
Surfaces updated:
- bin/lib/config.cjs, sdk/src/config.ts — default flipped
- sdk/src/config.ts docstring — describes new default + opt-back-in
- agents/gsd-planner.md — Detection section explains new default
- references/planner-human-verify-mode.md — reordered modes; added
guidance on when to opt back into mid-flight
- references/checkpoints.md — surface the default flip and the why
- docs/CONFIGURATION.md — table row reflects new default + reason
- tests/feat-3309-human-verify-mode.test.cjs — default test asserts
end-of-phase
- .changeset/fierce-geese-march.md — describes the default flip and
the migration semantics
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address human verify mode review
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(3317): red — SDK detect-custom-files must scan skills/
Mirrors tests/bug-2942-detect-custom-skills.test.cjs on the SDK side.
The SDK's GSD_MANAGED_DIRS array omits 'skills', so user-added skills
under <config-dir>/skills/<name>/ are never returned and get destroyed
on /gsd-update. New vitest covers:
- detects custom skill at skills/<name>/SKILL.md
- does not flag manifest-tracked skill as custom
- still detects custom files under get-shit-done/workflows/ (regression)
- custom_count matches custom_files.length across multiple skills
Fails 2/4 against current SDK source. Will pass after fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3317): SDK detect-custom-files now scans skills/ (parity with CJS)
The SDK port of detect-custom-files declared GSD_MANAGED_DIRS without
'skills', while the canonical bin/gsd-tools.cjs port (which the SDK
docstring explicitly cites as its source) had the entry. Because
update.md prefers gsd-sdk over the CJS shim, real-world users with the
SDK installed never had their custom skills detected — the installer's
'Installed N skills to skills/' step then wiped any non-manifest skill
without backing it up to gsd-user-files-backup/. Real-world incident:
skills/gsd-roadmap/SKILL.md (fully user-owned) destroyed during the
1.40.0 → 1.41.0 update, recoverable only via Time Machine.
One-line fix adds 'skills' to the SDK's GSD_MANAGED_DIRS, matching the
CJS source the port was supposed to mirror. The 4 vitest cases added in
the prior commit now all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: correct changeset pr number
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: tighten sdk-first architecture seams
Refs #3312
* refactor: finish state document seam cleanup
Refs #3312
* test: harden minimal install cleanup assertion
* ci: support sdk-scoped package lock
* fix(3316): restore root package-lock.json and align changeset pr ref
Reverts dec57a83 ("ci: support sdk-scoped package lock") and restores
the root package-lock.json that c249d34d deleted. The deletion was the
wrong direction:
- The root package.json declares its own runtime and dev deps
(@anthropic-ai/claude-agent-sdk, ws, c8). Without a root lockfile,
`npm install --no-package-lock` resolves whatever satisfies semver at
install time — CI today and CI in six months can install different
transitive trees, defeating reproducibility.
- The lockfile has been part of every release on this repo (long
history on main); removing it loses the npm audit / Dependabot
target without compensating benefit.
- The CI workaround pattern (cache-dependency-path: sdk/package-lock.json
+ `npm install --no-package-lock`) papered over the symptom rather
than fix the cause.
Also fix the changeset pr: from 3312 (issue) to 3316 (PR). CONTEXT.md
flags this exact failure mode as a recurring CodeRabbit finding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address coderabbit review findings
* fix: close remaining coderabbit threads
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(3251): red — assert 14 missing commands in command-aliases.generated.cjs
Parametrized test covering all 14 commands from issue #3251. Requires the
CJS manifest and asserts structurally (never greps source). Currently fails
because NON_FAMILY_COMMAND_ALIASES is not exported and all 14 are missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(3251): add 14 missing commands to command-aliases.generated.cjs
Adds NON_FAMILY_COMMAND_ALIASES export to command-aliases.generated.cjs and
extends sdk/src/query/command-manifest.non-family.ts with the 10 commands that
were registered in static catalogs but absent from the manifest source-of-truth:
- check.decision-coverage-plan / check.decision-coverage-verify
- frontmatter.get
- phase.mvp-mode
- progress.bar
- stats.json
- task.is-behavior-adding
- todo.match-phase
- uat.render-checkpoint
- workstream.list
The other 4 (frontmatter.set, learnings.copy, milestone.complete,
requirements.mark-complete) were already in non-family.ts but unexported.
Generator (sdk/scripts/gen-command-aliases.ts) now produces both the TS and CJS
artifacts including the non-family section, sorted by canonical for determinism.
Freshness check (sdk/scripts/check-command-aliases-fresh.mjs) verifies TS and CJS
non-family parity against the manifest source-of-truth.
Fixes#3251
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3251): add changeset and CHANGELOG entry for PR #3305
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(gen-command-aliases): preserve TS type interfaces and annotations on regen (#3251)
- Add FamilyCommandAlias and NonFamilyCommandAlias interface declarations to tsBody so regen never strips them
- Replace JSON.stringify with single-line compact serialisers for TS output (matches committed file format)
- Apply typed annotations (readonly FamilyCommandAlias[] / readonly NonFamilyCommandAlias[]) to all exported TS constants
- CJS path unchanged: remains untyped pure-JS with JSON.stringify multi-line format
- Regenerate sdk/src/query/command-aliases.generated.ts to sync with updated generator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: drop redundant CHANGELOG.md edit (use .changeset/ fragment per CONTRIBUTING.md)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: phase-dir prefix parity across creation paths (#3287 RED)
Add failing tests asserting that init.phase-op and init.plan-phase
expose expected_phase_dir with the project_code prefix when the phase
directory does not yet exist — matching the prefix applied by phase.add.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(phase): unify phase-dir naming via shared getPhaseDirName helper (#3287)
Both init.phase-op (discuss-phase workflow) and init.plan-phase
(plan-phase workflow) now compute expected_phase_dir — the canonical
directory name including the project_code prefix when set — and expose
it in their JSON bundle.
Workflow fallback mkdir calls are updated to use ${expected_phase_dir}
instead of constructing the path from padded_phase + phase_slug, which
was missing the project_code prefix.
This eliminates the two-headed naming convention where phase.add/insert
produced XR-01-foundation/ while the first-touch paths produced
01-foundation/ for the same project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(phase): apply project_code prefix in scaffold phase-dir (#3287)
Audit finding: phase.scaffold (CJS commands.cjs + SDK phase-lifecycle.ts)
also constructed phase dir names without project_code prefix.
Both implementations now read config.project_code and apply the same
prefix logic as phase.add/phase.insert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3292 for #3287
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(changelog): add entry for #3287 phase-dir prefix parity fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>