agents/gsd-planner.md was 49,316 chars after the initial PR; the
planner-decomposition <48K test was passing on main at 49,150 chars (just under
the 49152 limit). My addition pushed it over.
Restructure: instead of teaching the planner agent to read .last-build-status.json
directly, fold the auto-build state into graphifyStatus()'s existing `stale: true`
signal. The planner's existing rule ("if stale: true, treat as approximate") fires
correctly for failed and in-flight auto-builds — no new planner-side prompt content
needed. The full state is exposed under `last_build_auto_update` for callers that
want exit_code / duration_ms / commit-sha context.
- get-shit-done/bin/lib/graphify.cjs: graphifyStatus() reads
.planning/graphs/.last-build-status.json; OR-folds status in {failed, running}
into the existing stale signal; exposes last_build_auto_update field
- agents/gsd-planner.md: revert the auto-update awareness paragraph (49,524 → 49,150)
- agents/gsd-phase-researcher.md: revert the parallel paragraph for consistency
- get-shit-done/references/planner-graphify-auto-update.md: rewrite to document
the graphifyStatus seam instead of planner-side prompt instructions
- tests/feat-3347-graphify-auto-update-config.test.cjs: 4 new graphifyStatus
tests pinning the failed/running/ok/missing matrix
- tests/feat-3347-graphify-auto-update-hook.test.cjs: bump per-spawn timeout
5s → 30s and wait-deadline 5s → 15s to absorb cold-start latency under
parallel-test-file load (full suite runs many *.test.cjs concurrently)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/CONFIGURATION.md: document graphify.auto_update key (config-schema-docs-parity)
- hooks/gsd-check-update-worker.js: add gsd-graphify-update.sh to MANAGED_HOOKS (managed-hooks)
- agents/gsd-planner.md + gsd-phase-researcher.md: slim auto-update awareness block to
a one-line @-reference; extract full instructions to a new reference file
- get-shit-done/references/planner-graphify-auto-update.md: new reference with the
status-file schema, the four annotation cases (running/failed/ok-current/ok-stale),
and interaction with the existing stale-mtime annotation
- docs/INVENTORY.md: References (60 → 61 shipped) + row for new reference; regenerate
docs/INVENTORY-MANIFEST.json via gen-inventory-manifest.cjs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
* fix(3537): route every phase-number ROADMAP regex through phaseMarkdownRegexSource
v1.42.1 added the padding-tolerant `phaseMarkdownRegexSource()` helper but
wired it into only 1 of 8 call sites that build phase-number regexes against
ROADMAP/STATE prose. The other 7 used raw `escapeRegex(phaseNum)` or partial
`0*${escapeRegex(...)}` (tolerated extra padding, not missing), so when
skills passed the resolved padded form (`02.7`) against un-padded ROADMAP
prose (`### Phase 2.7:`, `- [ ] **Phase 2.7:**`), the verbs silently no-op'd
while reporting success.
This consolidates every phase-number ROADMAP/STATE regex through the
canonical helper:
- Promote `phaseMarkdownRegexSource` from `roadmap.cjs` to `core.cjs` so
`phase.cjs` and `core.cjs` itself can consume it (no circular dep —
both already import `core.cjs`).
- Wire the helper into the 7 remaining sites:
- `core.cjs:getRoadmapPhaseInternal` (replaces hand-rolled `isNumeric`
branch that only padded integers, not decimals).
- `roadmap.cjs:cmdRoadmapGetPhase` (searchPhaseInContent escapedPhase).
- `roadmap.cjs:cmdRoadmapAnalyze` checkbox lookup.
- `roadmap.cjs:cmdRoadmapAnnotateDependencies` phase header lookup.
- `phase.cjs:cmdPhaseNextDecimal` ROADMAP prose scan.
- `phase.cjs:cmdPhaseInsert` target anchor + decimal scan + header.
- `phase.cjs:cmdPhaseComplete` (3 regexes: checkbox, plan-count,
REQUIREMENTS extraction).
Adds `tests/bug-3537-padded-id-against-unpadded-roadmap.test.cjs` — a
parity-style regression matching CONTEXT.md DEFECT.GENERATIVE-FIX: for
each user-facing verb, asserts that the padded form (`02.7`) and the
un-padded form (`2.7`) produce identical ROADMAP.md against an identical
fixture. Includes one control case (`update-plan-progress`, already wired
in 1.42.1) to prove the parity assertion is non-vacuous.
Closes#3537
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): add changeset fragment (pr: placeholder, amended post-create)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): pin changeset pr: field to #3538
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(3530): STATE.md Document Module via generator (Phase 1 of #3524)
Phase 1 of the CJS↔SDK hard-seam migration (parent #3524).
Converts the hand-synced state-document.cjs/state-document.ts pair
into a generator-driven seam, modeled on the existing
command-aliases.generated.* precedent.
What landed:
- sdk/src/query/state-document.ts is the source of truth.
- sdk/scripts/gen-state-document.ts emits
get-shit-done/bin/lib/state-document.generated.cjs from the
compiled SDK dist via Function.prototype.toString() inspection
for the 7 public exports and 3 internal helpers.
- sdk/scripts/check-state-document-fresh.mjs is the CI freshness
gate; pre-commit hook also runs it when relevant files change.
- get-shit-done/bin/lib/state-document.cjs is reduced to a one-line
re-export from state-document.generated.cjs so existing callers
(state.cjs, workstream-inventory.cjs, init.cjs) need no changes.
- New CI step in .github/workflows/test.yml after the existing alias
drift check.
- sdk/package.json: gen:state-document, check:state-document-fresh
scripts. tsx added as devDep.
- Root package.json: proxy script for the freshness check.
- CONTEXT.md: one-sentence amendment on STATE.md Document Module
recording the source-of-truth file path.
Tests:
- sdk/src/query/state-document.test.ts: 34 vitest fixtures across
the 7 public exports (TDD pinning safety net).
- tests/state-document-generator.test.cjs: 31 node:test parity
assertions comparing SDK source vs generated CJS for every
fixture.
- Full suite: 9177/9177 pass (baseline was 9146; +31 new tests).
One subtle behavior change worth flagging: the old hand-written
state-document.cjs used String(str) coercion inside escapeRegex,
which the SDK source does not. The generator faithfully matches
the SDK (the source of truth per ADR-3524), so the new CJS no
longer coerces non-string input to string before regex-escaping.
No current caller passes non-string input, so no observable
regression in the test suite. Flagged in the PR body for
reviewers.
Closes#3530.
* fix(3530): address state-document review findings
Three-part fix for the false "unknown config key(s)" warning fired for
top-level `branching_strategy` in .planning/config.json:
1. On-disk migration (option 3, mirroring multiRepo → planning.sub_repos):
When loadConfig reads a config.json with top-level `branching_strategy`
set and `git.branching_strategy` unset, it grafts the value into
`git.branching_strategy` and deletes the top-level key, then persists.
If `git.branching_strategy` is already set, the nested value wins
(matches SDK mergeDefaults precedence, PR #3116).
2. KNOWN_TOP_LEVEL safety net: 'branching_strategy' added to the deprecated-
keys bucket so the warning never fires even on the first read of a root
config that feeds a workstream merge (where `parsed` may still carry it).
3. Double-emission guard: a module-level `_warnedUnknownConfigKeys` Set
deduplicates the unknown-key warning across multiple loadConfig calls
within a single CLI invocation (init phase-op N called it twice).
Closes#3523
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The post-merge worktree-cleanup loop in quick.md issued bare `git diff`,
`git merge`, and related commands relying on CWD = project root. An LLM
orchestrator that reformats bash across separate tool calls can leak CWD
into a worktree, causing the merge to silently no-op.
At the top of each iteration body, resolve PROJECT_ROOT via
`git -C "$WT" rev-parse --git-common-dir` and `cd "$PROJECT_ROOT"`.
If the root cannot be resolved or reached, log a skip message and
continue to the next manifest entry. All existing guards (pre-merge
deletion guard #1756, STATE.md/ROADMAP.md backup/restore, resurrection
guard #2501/#3195) remain intact after the CWD pin.
Closes#3521 (bug 1 — CWD safety only; bug 2 / resurrection guard was
already fixed in a6beac40 / PR #3201 and is pending reporter retest).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The grep -v alternation in the git-enhanced two-way merge step was missing
the 'gsd-update' arm after the slash-command rename from /gsd:update to
/gsd-update. Commits authored by the current update flow fell through the
filter and were misclassified as user customizations, causing spurious merge
prompts during /gsd-update --reapply.
Adds 'gsd-update' between 'gsd:update' and 'GSD update'. The legacy
'gsd:update' arm is preserved for back-compat; 'GSD update' and 'gsd-install'
exclusions are unchanged.
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>
Re-invoking `state complete-phase --phase <N>` on a phase that was
already marked complete in STATE.md silently rolled STATE.md back to
that phase's moment-of-completion — clobbering Status, Last Activity,
Last Activity Description, and the ## Current Position body. The bug
fired whenever a follow-up phase had been inserted (or the next phase
had begun) and a downstream workflow re-ran complete-phase on the
already-closed phase. Damage was silent: handler reported
{"updated":["Status","Last Activity","Current Position"]} and no error.
Root cause: cmdStateCompletePhase wrote unconditionally — it never
consulted STATE.md to detect that the requested phase had been
superseded. The handler is a legacy-bridge fallback (no native SDK
registration), so the SDK CLI fell through to gsd-tools.cjs.
Fix: add an idempotency guard at the top of cmdStateCompletePhase.
If STATE.md's canonical Current Phase field already names a phase
distinct from the one we are being asked to mark complete, return a
no-op payload ({updated:[], phase:"<N>", idempotent:true, note:"phase
already superseded; no-op"}) without writing to STATE.md.
The guard is conservative — it only fires when Current Phase is set
and differs from the resolved target. First-time completion (Current
Phase == target, or Current Phase absent) is unaffected, so the four
existing complete-phase test cases (#2761, #3063) continue to pass.
Regression test: tests/bug-3489-complete-phase-idempotent.test.cjs
- re-running complete-phase --phase 02.2 with Current Phase=02.2.1
in STATE.md leaves the file byte-identical and reports idempotent:true
- normal first-time completion is NOT flagged idempotent
Scope: handler-level idempotency only. Does not address the related
stopped_at filename-sort ordering issue called out in the bug report
(filed under suggested fix#2) or porting to the native registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
* refactor(shell-projection): remove deprecated wrappers + finalize ADRs (Phase 4, #3468)
Final phase of the shell-command-projection expansion. Removes the legacy
core.cjs wrappers (`atomicWriteFileSync`, `safeReadFile`, `normalizeMd`)
now that every call site lives behind the seam, plus three Phase-3
stragglers (`graphify.cjs`, `template.cjs`, dead import in
`profile-pipeline.cjs`).
Documentation:
- ADR-0009: addendum noting Phase 1–4 scope expansion (subprocess +
file I/O ownership), supersession of "does not execute" constraint,
and resolution of open Q4.
- ADR-0010: status changed to Superseded by ADR-0009 with explanation.
- CONTEXT.md "Shell Command Projection Module" entry already current
from Phase 1 — no edit needed.
Tests:
- `tests/atomic-write.test.cjs` deleted — wrapper it tested is gone;
`atomic-write-coverage.test.cjs` (Phase 3) covers platformWriteSync.
- `tests/core.test.cjs::safeReadFile` + `::normalizeMd` describes
deleted — wrappers are gone.
- `tests/concurrency-safety.test.cjs` normalizeMd suite (behavioral /
perf / snapshot) repointed via 2-line shim at the seam's
`normalizeContent` — full regression coverage preserved.
Test result: 9059/9041/18 — exact pre-Phase-4 baseline. All 18
failures are pre-existing path-with-spaces local-env issues.
Closes#3468
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate remaining raw fs.writeFileSync sites (Phase 4, #3468)
Sweeps the 7 raw fs.writeFileSync call sites that bypassed the seam through Phase 3,
folding them into platformWriteSync. Net -14 lines: deletes the local writeFileAtomicSync
helper in installer-migrations.cjs and collapses surface.cjs's manual tmp+rename into a
single seam call.
Sites migrated:
- drift.cjs (1) — frontmatter write
- learnings.cjs (1) — learning record JSON write
- install-profiles.cjs (1) — profile marker write (collapsed redundant mkdir)
- gsd2-import.cjs (1) — imported file write (collapsed redundant mkdir)
- surface.cjs (1) — surface state write (replaced manual tmp+rename block)
- installer-migrations.cjs (3) — journal init/finalize + rewrite-json action;
deleted private writeFileAtomicSync helper and its three call sites
Two sites intentionally retained outside the seam:
- planning-workspace.cjs:241 — workspace lock (wx-flag atomic-create; previously excluded by Phase 3)
- installer-migrations.cjs:220 — install migration lock (fd write into wx-opened handle)
- writeInstallState (installer-migrations.cjs) — strict atomic contract for install state;
the seam's fallback-to-direct-write on rename failure would silently violate the
invariant that install state must never be left half-written. Inline tmp+rename with
rethrow keeps the original guarantee.
Tests: 9059 / 9041 / 18 — exactly the pre-Phase-4 baseline; 18 failures are the
pre-existing path-with-spaces local-env issues, identical files as before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(installer-migrations): use strict atomic write for rollback install-state restore
The rollback path was restoring INSTALL_STATE via platformWriteSync, which falls
back to a direct write on rename failure and would silently violate the
half-written invariant that the install-state contract guarantees elsewhere.
Extracts the strict tmp+rename logic from writeInstallState into a shared
atomicWriteInstallState(configDir, content) helper and routes both
writeInstallState and rollbackAppliedMigrationResult through it. Preserves the
existing null-handling (rmSync when previousInstallStateBytes === null) and
existing failure-collection (failures.push on caught errors).
Byte-faithful restore: previousInstallStateBytes is written as-is (no JSON
parse round-trip), preserving the exact prior file contents on restore.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate roadmap.cjs writes to platformWriteSync (#3467)
2 atomicWriteFileSync calls → platformWriteSync. The seam owns markdown
normalization, so the explicit utf-8 encoding arg is no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate config.cjs writes to platformWriteSync (#3467)
- 3 atomicWriteFileSync calls → platformWriteSync
- 1 raw fs.writeFileSync (depth→granularity migration) → platformWriteSync
- 2 fs.mkdirSync(planningBase, { recursive: true }) → platformEnsureDir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate docs.cjs reads to platformReadSync (#3467)
6 try { fs.readFileSync } catch {} patterns → platformReadSync(path) with
explicit null guards. detectProjectType now reads package.json once and
shares it across has_cli_bin/is_monorepo/has_tests checks. JSON.parse is
still wrapped in a try (parsing is a separate failure mode from missing file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate audit.cjs reads to platformReadSync (#3467)
8 try { fs.readFileSync(safeFilePath, 'utf-8') } catch { continue } patterns
→ const content = platformReadSync(safeFilePath); if (content === null) continue;
The single safeSum case (where catch set status='unreadable' rather than
continue) maps to an if/else that preserves the same semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate planning-workspace.cjs to platform* seam (#3467)
- 2 try { fs.readFileSync } catch {} → platformReadSync (null on missing)
- 2 fs.writeFileSync (workstream pointer writes) → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
The .lock file write at withPlanningLock is intentionally NOT migrated.
That call uses { flag: 'wx' } for atomic exclusive-create, which is the
correct lock-acquisition primitive. platformWriteSync's atomic-rename
pattern would silently overwrite an existing lock file and break the
locking guarantee.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate milestone.cjs writes to platform* seam (#3467)
- 5 atomicWriteFileSync calls → platformWriteSync (4 dropped normalizeMd
wrapper; seam handles .md normalization automatically)
- 2 raw fs.writeFileSync (archive ROADMAP.md / REQUIREMENTS.md) → platformWriteSync
- 2 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Dropped normalizeMd import (only used as write pre-call here)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate intel.cjs to platform* seam (#3467)
- 7 fs.readFileSync (existsSync+readFileSync patterns and try/catch) → platformReadSync
- 2 fs.writeFileSync → platformWriteSync
- 1 fs.mkdirSync(intelPath, { recursive: true }) → platformEnsureDir
- Consolidated dual-check (existsSync + readFileSync) into single platformReadSync
call returning null on missing file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate workstream.cjs to platform* seam (#3467)
- 5 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- 1 fs.writeFileSync (STATE.md initial scaffold) → platformWriteSync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate init.cjs reads/writes to platform* seam (#3467)
- 11 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 1 fs.writeFileSync (skill-manifest.json) → platformWriteSync
Three bare fs.readFileSync calls remain (ROADMAP/STATE reads in code paths
where the file is required to exist) — these are not "Done when" violations
(no try/catch wrapping, no inline existsSync guard).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate commands.cjs reads/writes to platform* seam (#3467)
- 6 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 2 fs.writeFileSync → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Removed unused safeReadFile import (zero call sites in this file)
Three bare fs.readFileSync calls remain (sourcePath at line 752, fullPath at
443, roadmapPath in cmdAuditOpen) — preceded by existsSync guards or in code
paths where file presence is required; not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate profile-output.cjs to platform* seam (#3467)
- 6 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync
as safeReadFile in the import — same semantics, zero call-site changes
- 3 try/JSON.parse(readFileSync) patterns → platformReadSync + try/JSON.parse
- 1 existsSync+readFileSync pattern (claude.md update) → platformReadSync
- 5 fs.writeFileSync → platformWriteSync
- 4 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
Two bare fs.readFileSync calls remain (template reads where file must exist
or fail loudly) — not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate state.cjs to platform* seam (#3467)
- 4 atomicWriteFileSync calls → platformWriteSync (3 dropped normalizeMd
wrapper; seam handles .md normalization)
- 4 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 1 fs.writeFileSync (WAITING.json) → platformWriteSync
- 1 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Dropped normalizeMd and atomicWriteFileSync imports (only used as write
pre-calls here)
Bare fs.readFileSync calls remain in code paths where STATE.md is required
to exist (statePath reads in cmd handlers, dry-run prune) — not "Done when"
violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate core.cjs to platform* seam (#3467)
- 7 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 3 fs.writeFileSync (config writes + large-payload temp file) → platformWriteSync
- 1 fs.mkdirSync (GSD_TEMP_DIR) → platformEnsureDir
Three fs calls remain — they are the internal implementations of the
safeReadFile and atomicWriteFileSync wrappers that core.cjs exports for
backward compatibility. The wrappers are scheduled for removal in Phase 4
(#3468) and will not be migrated here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate phase.cjs writes to platform* seam (#3467)
- 6 atomicWriteFileSync calls → platformWriteSync
- 3 fs.writeFileSync(path.join(dirPath, '.gitkeep'), '') → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
Bare fs.readFileSync calls remain for roadmapPath/planPath reads where the
file is required to exist; these are not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate verify.cjs to platform* seam (#3467)
- 8 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync
as safeReadFile in the import — same semantics, zero call-site changes
- 1 existsSync+readFileSync inline ternary → safeReadFile (returns null)
- 5 fs.writeFileSync (config writes + milestones writes) → platformWriteSync
Bare fs.readFileSync calls remain for code paths where the file is required
to exist (roadmap/state/config full reads); these are not "Done when"
violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate frontmatter.cjs + update atomic-write test (#3467)
- frontmatter.cjs: 2 atomicWriteFileSync calls → platformWriteSync. The
legacy normalizeMd wrapper is dropped because the seam handles markdown
normalization. safeReadFile preserved by aliasing platformReadSync.
- atomic-write-coverage.test.cjs: update the #1972 structural invariant
to assert on platformWriteSync. platformWriteSync uses the same
tmp-file + atomic-rename primitive that atomicWriteFileSync did — the
no-partial-write guarantee is preserved across the migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add entry for shell-projection Phase 3 migration (#3467)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(coderabbit): disable ESLint tool (repo uses custom lint scripts)
CodeRabbit's review surface emits a "skipped: no ESLint configuration"
warning because the repo doesn't ship ESLint config. The repo
intentionally does not use ESLint — it ships its own targeted lint
scripts (scripts/lint-no-source-grep.cjs, npm run lint:tests) that
enforce repo-specific test-quality invariants. Adding ESLint config
purely to satisfy CR would add an external dependency
(CONTRIBUTING.md: "No external dependencies in core") and overlap
with the existing custom lint surface.
Disable the ESLint tool in CR's tools config so the skip warning
stops appearing on every PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate planning-workspace.cjs tty probe to probeTty seam (#3466)
Replaces direct execFileSync('tty') with probeTty() from the shell-projection
seam. Removes try/catch — probeTty() returns null on error/non-tty/win32.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate commands.cjs to execGit seam (#3466)
- Replaces execSync('git diff --cached --name-only') with execGit array call
- Migrates 14 existing execGit(cwd, args) callers from core.cjs's local
wrapper to the seam's execGit(args, { cwd }) signature
- Drops execGit from the core.cjs destructure to resolve naming collision
Drops try/catch around git diff — execGit returns exitCode without throwing,
so the no-staged-files / not-a-git-repo case is detected by exitCode !== 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate check-latest-version.cjs to execNpm seam (#3466)
Routes the default-spawn path through execNpm — execNpm owns the win32
shell-flag policy. The injection point remains spawnSync-shaped for test
compatibility; an internal adapter translates { exitCode } → { status }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate init.cjs git calls to execGit seam (#3466)
Replaces 3 execSync calls with execGit array-args:
- detectChildRepos: git status --porcelain
- cmdInitNewWorkspace: git --version (worktree availability probe)
- cmdRemoveWorkspace: git status --porcelain
Drops 3 try/catch blocks — execGit returns exitCode without throwing, so
best-effort handling becomes a clean exitCode === 0 check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate core.cjs to execGit seam delegation (#3466)
- Removes direct require('child_process') from core.cjs
- Replaces execFileSync('git check-ignore') with seam's execGit
- Local execGit wrapper now a thin adapter delegating to seam — keeps the
legacy (cwd, args) positional signature and derived timedOut field for
the verify.cjs and worktree-safety.cjs consumers that are out of Phase 2
scope (the wrapper proper would only be removed once those consumers
migrate, tracked separately)
Extends the seam's _spawnResult to expose signal and error fields so callers
can compute timedOut without bypassing the seam. The Phase 1 test suite
asserts on required field presence only, so the extension is
backward-compatible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate graphify.cjs to execTool seam (#3466)
- execGraphify: spawnSync('graphify', ...) → execTool with env passthrough,
preserving the ENOENT/TIMEOUT/EXIT_NONZERO typed reason mapping using the
seam's signal/error fields
- checkGraphifyInstalled: spawnSync('graphify', ['--help']) → execTool
- checkGraphifyVersion strategy 1: graphify --version via execTool
- checkGraphifyVersion strategy 2: python3 importlib.metadata via execTool
Adds env option to execTool — graphify needs PYTHONUNBUFFERED=1 to drain
buffered stdout on long-running operations.
Changes seam internals to access spawnSync/execFileSync via the non-destructured
childProcess module reference. Destructured imports capture references at load
time and are un-mockable by mock.method(childProcess, 'spawnSync', ...) — which
breaks all the graphify subprocess tests. Non-destructured access restores
mockability without changing public behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(shell-projection): execGit defaults to non-interactive git env (#3466)
Bakes GIT_TERMINAL_PROMPT=0 and GCM_INTERACTIVE=never into execGit's default
env. Without these, a credential prompt or terminal-input probe blocks the
git subprocess indefinitely until our 10s timeout kills it — surfacing as
a generic timeout instead of the actual auth-prompt cause.
These were previously set ad-hoc in worktree-safety.cjs's local execGitDefault
wrapper. Moving them to the seam makes them the consistent default for every
git call across the codebase. Callers can override via opts.env.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): remove core.cjs local execGit wrapper; migrate verify + worktree-safety (#3466)
Completes the Phase 2 "remove local execGit wrapper" criterion. All callers
now use the shell-projection seam's execGit(args, opts) signature directly.
- core.cjs: delete the local execGit wrapper and the execGit export. The
isGitIgnored seam check now calls execGit from the seam. Worktree-safety
function calls drop their execGit DI passthrough — worktree-safety's
internal execGitDefault now delegates to the seam and adds timedOut.
- verify.cjs: 6 callers migrate from execGit(cwd, args) to
execGit(args, { cwd }). Imports execGit from the seam directly. The
inspectWorktreeHealth DI passes the seam's execGit (worktree-safety now
matches that shape).
- worktree-safety.cjs: local execGitDefault becomes a thin adapter over
the seam — no more direct spawnSync. 11 internal callers migrate to the
new shape. DI contract for tests changes from (cwd, args) → (args, opts).
- graphify.cjs: 2 remaining execGit callers migrate from core.cjs (now
removed) to the seam directly.
- test mocks updated in 3 worktree-safety test files to match the new
(args, opts) DI shape — most mocks were shape-agnostic and required no
changes; only those that destructured cwd/args needed updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add entry for shell-projection Phase 2 migration (#3466)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(pr3476): address CodeRabbit review findings
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(state): prevent backreference expansion in state mutations
* chore(changeset): set pr field for #3463
* test(state): use structured STATE parser in bug-3454 regression
* feat(skill-deps): add requires: frontmatter to all 51 skills with cross-skill references
Mechanical migration from docs/research/data/2026-05-12-skill-audit.json.
Every skill whose body references another GSD skill now declares those
dependencies in `requires:` YAML frontmatter (flow-style array).
Notable: discuss-phase, plan-phase, and execute-phase all reference `phase`,
which confirms the latent gap in MINIMAL_SKILL_ALLOWLIST — `phase` is pulled
by the core loop but was never in the allowlist. The profile closure model
(ADR-0010 Phase 1) resolves this automatically.
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(skill-surface-budget): add PROFILES map, resolveProfile, loadSkillsManifest, staging, marker IO
Implements the Skill Surface Budget Module core (ADR-0010, Phase 1):
- PROFILES Object.freeze map: core (6 skills), standard (~13), full ('*')
- loadSkillsManifest: parses requires: frontmatter from commands/gsd/*.md
into a Map<stem, string[]> without external YAML dep
- resolveProfile({modes, manifest}): computes transitive closure over the
requires: graph; composable (modes=['core','audit'] unions closures)
- stageSkillsForProfile / stageAgentsForProfile: filesystem staging with
same exit-cleanup machinery as the legacy stageSkillsForMode
- readActiveProfile / writeActiveProfile: .gsd-profile marker round-trip
- Back-compat shims preserved: MINIMAL_SKILL_ALLOWLIST, isMinimalMode,
shouldInstallSkill (overloaded), stageSkillsForMode — all legacy tests pass
The phase latent bug is now resolved by closure: discuss-phase, plan-phase,
and execute-phase all require phase, so any profile including any of them
automatically includes phase via transitive closure.
Tests: 22 manifest+resolve, 9 stage, 10 marker (41 new tests, all green).
Back-compat anchor: 80/80 passing.
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(skill-surface-budget): add lint-skill-deps.cjs CI gate and fix 19 missed requires: entries
Two lint checks (scripts/lint-skill-deps.cjs):
a) Frontmatter-body consistency: skill body references must appear in requires:
b) Profile closure: every requires: dep of any profile skill must be in closure
Running the lint revealed 19 body references missed by the audit JSON (the
audit used static analysis; some bodies have conditional references). Fixed:
complete-milestone: +audit-milestone, discuss-phase, plan-phase, execute-phase, new-milestone
fast: +quick
health: +thread
map-codebase: +new-project, plan-phase
new-milestone, new-project, review, ultraplan-phase: +plan-phase
ship: +verify-work
sketch, spike: +new-project
verify-work: +execute-phase
workstreams: +new-milestone, resume-work
Wired into package.json as lint:skill-deps and added to pretest.
8 fixture-based tests: all green.
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(skill-surface-budget): wire --profile= arg, profile marker write/read in bin/install.js
- Add --profile=<name> / --profile=<n1>,<n2> arg parsing (composable).
Mutually exclusive with --minimal / --core-only (aliases for --profile=core).
Default (no flag): full.
- Import readActiveProfile / writeActiveProfile from install-profiles.cjs.
- After writeManifest: persist active profile to .gsd-profile marker.
- gsd update path: if no --profile flag given, read existing .gsd-profile
marker so non-full profiles are not silently re-expanded to full (ADR-0010).
- Update --help block to document --profile= with per-tier token costs.
New test: install-minimal-backcompat.test.cjs (6 tests):
- PROFILES.core === MINIMAL_SKILL_ALLOWLIST (contract)
- --minimal writes .gsd-profile marker "core"
- --profile=core, --profile=standard write correct markers
- default install writes marker "full"
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add feat-3408-skill-profiles changelog fragment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(install-profiles): derive agents from skill body refs and wire into resolveProfile
Deviation 1 of ADR-0010 phase 1b: tiered profiles (core, standard) now produce
a non-empty agents Set instead of always returning empty. resolveProfile() scans
each skill body for gsd-* agent name references (via new parseCallsAgents()),
stores them in _calls_agents_<stem> manifest entries, and unions them across the
resolved skill closure. stageAgentsForProfile() already checked resolvedProfile.agents
— it now gets real data so tiered profiles install the correct subset of agents
instead of zero.
Closes#3408 (partial — Deviation 1 only)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(install): honor .gsd-profile marker on update, add resolveEffectiveProfile/mostRestrictiveProfile
Deviation 2 of ADR-0010 phase 1b: the marker written during installation is now
actually honored when re-running without explicit flags (e.g. gsd update). The
dead-end logging block is replaced by resolveEffectiveProfile(), which picks the
marker profile over 'full' when no explicit --profile= flag was given. The resolved
profile is piped through to all 13 stageSkillsForMode dispatch sites (now _stageSkills)
so updates install only the previously-chosen skill subset.
--minimal retains its back-compat behavior (strict 6-skill allowlist, no closure)
while writing 'core' to the marker. mostRestrictiveProfile() is exported for callers
that need to reconcile disagreeing markers across runtimes (smallest skill set wins).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(surface): add CLUSTERS data + state IO module
Add clusters.cjs with 10 named skill groups covering all 66 skills
(verified by surface-clusters.test.cjs). Add surface.cjs with readSurface/
writeSurface atomic IO, resolveSurface, applySurface, and listSurface.
Tests: 17 passing (11 state IO + 6 cluster integrity).
Closes#3408
* docs(adr): add ADR-0011 Skill Surface Budget Module (Phase 1 accepted, Phase 2 amendment)
Records the install-time profile staging decision (Phase 1, landed) and the
runtime /gsd:surface cluster-toggle decision (Phase 2, in flight) as an
amendment. Updates the ADR README index.
Closes#3408
* docs(install-profiles): update module docblock for Phase 2 and ADR-0011
Corrects the ADR reference from 0010 to 0011, documents the three-profile
model and back-compat aliases, adds resolveEffectiveProfile precedence rule,
and notes the companion surface.cjs Phase 2 engine.
* docs(context): add Skill Surface Budget Module canonical entry
Adds the Domain terms entry for the Skill Surface Budget Module covering
both Phase 1 (install-time profiles, .gsd-profile marker) and Phase 2
(runtime /gsd:surface cluster toggles, clusters.cjs, .gsd-surface.json),
per ADR-0011 Consequences requirement.
* feat(surface): add resolveSurface and applySurface engine + tests
Tests cover: profile → surface equivalence, cluster disable/enable,
explicitAdds transitive closure, applySurface file sync (add missing,
remove superseded, preserve non-gsd files), listSurface token cost.
16 new tests passing.
* docs(readme): document --profile= flag and /gsd:surface command
Brief user-facing mention of install profiles (core/standard/full) and the
/gsd:surface slash command in the Commands table. Points to ADR-0011 for details.
* feat(surface): add /gsd:surface slash command runbook
New skill: gsd:surface — runtime profile/cluster toggle without reinstall.
Sub-commands: list, status, profile <name>, disable/enable <cluster>, reset.
Persists state to .gsd-surface.json (independent of .gsd-profile).
Description 96 chars (≤100 limit). lint:descriptions + lint:skill-deps: 0 violations.
* feat(surface): add changeset fragment for /gsd:surface runtime toggle
* feat(surface): add surface skill stem to utility cluster
surface.md is a new skill; add it to the utility cluster so the
surface-clusters.test.cjs coverage invariant stays satisfied.
* docs(adr): fix ADR references to 0011 and record Phase 2 as shipped
ADR-0010 number was already claimed by the file-operation-engine ADR; this
ADR landed as 0011-skill-surface-budget-module.md. Update inline ADR
references in clusters.cjs, surface.cjs, install-profiles.cjs, and the
Phase 2 changeset to ADR-0011. Update the ADR Status section to record
Phase 2 artifacts as shipped on this branch rather than "in progress".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(research): port skill-surface-budget memo and audit data
ADR-0011 references docs/research/2026-05-12-skill-surface-budget.md and
docs/research/data/2026-05-12-skill-audit.json, which only existed in the
research worktree. Port both onto this branch so the ADR's References
section resolves and reviewers can read the cluster taxonomy (§3.2),
dependency topology (§3.1), and option grading (§4) that justify Phase 1
and Phase 2 decisions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(registration): register surface/clusters in INVENTORY, COMMANDS, and help.md
- surface.md: convert allowed-tools from inline YAML array to block style
(was parsed as a single tool name "[Read, Write, Bash]" by test harness)
- docs/INVENTORY.md: add CLI module rows for clusters.cjs and surface.cjs;
add Commands row for /gsd-surface; bump CLI Modules count 55→57, Commands 66→67
- docs/INVENTORY-MANIFEST.json: add entries for clusters.cjs, surface.cjs,
and /gsd-surface (filename-based command key)
- docs/COMMANDS.md: add ### `/gsd-surface` heading in Configuration Commands
- get-shit-done/workflows/help.md: add /gsd:surface entry in Configuration section
Fixes registration failures introduced by Phase 2 of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(surface,docs): scrub .claude leakage and escape hypothetical slash tokens
Two PR regressions introduced earlier on this branch:
1. surface.cjs JSDoc comments contained the canonical paths
(~/.claude/commands/gsd, ~/.claude/agents) as example values, which the
cline-install leak regex (~\/\.claude\/(?:get-shit-done|commands|agents
|hooks)) flagged as install-time path leaks. Reworded the docblocks to
describe runtime-resolved paths without literal ~/.claude tokens.
2. The ported research memo proposed hypothetical Option C dispatchers
using slash syntax (/gsd:milestone, /gsd:research). The
docs-parity-live-registry test enforces that every slash-command token
in docs/ resolves to a real command. Rewrote the Option C sketch
without the slash prefix and added a clarifying note that the
dispatchers are illustrative, not shipped.
Targeted tests now pass: tests/cline-install.test.cjs and
tests/docs-parity-live-registry.test.cjs both green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: remove raw output/source grep in lint tests
* fix: close coderabbit profile and requires issues
* test: align surface token-cost assertion wording
* fix(install): align core profile alias and defer profile marker write
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: cover Windows hook shell drift for Claude (#3413)
* fix: scope Windows hook syntax to Gemini runtime (#3413)
* docs: add changeset for #3413
* docs: set changeset pr for #3413
* fix: route Windows hook formatting through runtime-aware projection seam
* test: cover runtime projection edge cases for Windows hooks
* fix(docs): add shell-command-projection to inventory parity
* docs: align CLI module shipped count after rebase