* test: red — bounded git subprocess + structured worktree warnings (#3281)
Regression tests for #3281: worktree-related git subprocess calls have no
timeout bound, and timeout/error outcomes are not surfaced as structured signals.
Failing assertions:
- planWorktreePrune / listLinkedWorktreePaths / snapshotWorktreeInventory must
return reason=git_timed_out (not generic git_list_failed) when execGit returns
timedOut:true — enables callers to distinguish timeout from auth failure
- executeWorktreePrunePlan must include timedOut:true in result when the git
prune call itself times out
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): bounded git subprocess + structured warning surfacing (#3281)
Root cause (PRED.k014): execGit / execGitDefault called spawnSync with no
timeout, so `git worktree list --porcelain` against a hung/locked repo
blocked the parent process indefinitely. Downstream callers in core.cjs
and verify.cjs then swallowed any resulting failure silently via
catch { /* intentionally empty */ } (PRED.k302).
Fix:
- worktree-safety.cjs: execGitDefault now passes timeout:10000 to spawnSync.
Detects SIGTERM+ETIMEDOUT and returns { timedOut:true } in the result shape.
readWorktreeList maps timedOut:true -> reason:'git_timed_out' (distinct from
generic git_list_failed) so callers can emit a structured warning.
executeWorktreePrunePlan propagates timedOut:true as a first-class result field.
- core.cjs: execGit receives the same timeout+timedOut treatment (PRED.k014
uniform-fix discipline). pruneOrphanedWorktrees now emits a [gsd-tools]
WARNING to stderr when the git prune call times out instead of silent-catch.
- verify.cjs: Check 11 branches on worktreeHealth.ok to surface W018 warning
when the worktree list times out, instead of silent-catch on ok:false.
Backward-compatible: exitCode/stdout/stderr continue to work for all existing
callers; timedOut and error are additive new fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3283 for #3281
* fix(verify): rename W020 for worktree-timeout warning to avoid W018 collision
W018 is already used for milestone archive drift (Check 12). The new
worktree-health-degraded timeout warning was assigned W018, causing
warning-code ambiguity in triage. Rename to W020 (next available code).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3266): preserve wave 0 and bucket plans by depends_on DAG in phase-plan-index
Fixes two cooperating bugs in the phase-plan-index builder:
1. Wave 0 collapse: `parseInt(...) || 1` coerced parsed value `0` to `1` due to
JS falsy default. Fixed with `Number.isNaN` guard.
2. depends_on ignored: wave-bucketing used only the `wave:` frontmatter field.
Now replaced with Kahn's topological-level algorithm over `depends_on`:
source nodes (no in-phase deps) → lowest level; each plan's level = max(deps'
levels) + 1. Declared `wave:` that disagrees with computed level emits a
non-fatal warning on the result. Cycle detection throws GSDError.
`PlanInfo` gains `depends_on: string[]`. `PhasePlanIndex` gains `warnings?: string[]`.
Both TS (`sdk/src/query/phase.ts`) and CJS twin (`get-shit-done/bin/lib/phase.cjs`)
fixed identically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for #3276
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(phase): resolve depends_on against canonical plan id (#3276 CR)
Build a secondary `canonicalToId` index alongside `planMap` so that a
dependency declared as '03-01' resolves to a descriptive plan stored
under '03-01-auth-hardening', preventing silent wave-ordering failures.
Applied at both DAG construction sites in phase.cjs and the SDK's
phase.ts (k014 parity). Regression test added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(workstream): normalize migrate-name to valid slug
* docs(context): record workstream migrate-name slug invariant
* fix(catalog-cjs): balanced fallback for unknown profile (CR finding A)
profiles[profile] could return undefined for any profile key absent from
the catalog entry, causing downstream callers like formatAgentToModelMapAsTable
to crash on .length. Add ?? profiles.balanced fallback to match the SDK adapter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(sdk): anchor path resolution on import.meta.url not cwd (CR finding B)
resolve(process.cwd(), '..') breaks when Vitest is invoked from the repo root
because cwd is already the repo root and '..' goes one level above. Replace
with a file-relative path using fileURLToPath(new URL('../../../', import.meta.url))
anchored at the test file's location (sdk/src/query/).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: derive Group B runtime list from catalog (CR finding C)
Hardcoded ['kilo', 'cline', ...] throws TypeError if a runtime name is
removed from the catalog. Derive group B dynamically via
Object.keys(catalog.runtimeTierDefaults).filter(r => !r.opus) so the
test never goes stale and auto-covers future Group B additions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(workflow): add hermes to Step B runtime options (CR finding D)
hermes appears in the Group A built-in defaults table but was missing from
the AskUserQuestion options in Step B, forcing users to manually type it via
'Other (Group B or custom)'. Add explicit hermes entry for UI consistency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(config): refresh dynamic_routing tier table; fix stale L671 (findings E+F)
Finding E: tier table was missing 6 heavy-tier agents and 15 standard/light
agents added by this PR. Updated all three rows to match catalog routingTier
assignments (33 agents total).
Finding F: removed stale '18 of 31' claim and agent enumeration; replaced
with accurate note that all 33 agents have explicit catalog entries. Updated
authoritative source pointers to model-catalog.cjs / model-catalog.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(core): add profile-fallback unit tests for quality and budget (CR nitpick G)
The PR introduced quality→opus and budget→haiku unknown-agent fallbacks but
only balanced→sonnet and inherit→inherit were tested. Add two tests covering
the remaining two branches to complete coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* adr: define planning workspace and worktree seam
* refactor(worktree): extract worktree safety policy module
* refactor(workstream): extract active workstream pointer store seam
* test(worktree): cover policy branch paths and persist seam guardrails
* refactor(worktree): centralize health inventory seam for W017
* fix(workspace): align SDK project path policy with CJS planningDir
* refactor(query): unify SDK planning path projection seam
* refactor(init): route workspace projection through planningPaths seam
* docs(adr): add SDK architecture and planning path ADRs
* refactor(worktree): deepen name, pointer, inventory, and config seams
* docs(config): harmonize claude-opus-4-6 to 4-7 in resolve_model_ids example (CR finding 2)
* fix(sdk): return undefined for model_profile='inherit' sentinel (CR finding 3)
* docs(adr): renumber conflicting 0003-sdk-package-seam-module to 0007, update seam-map reference (CR finding 4)
* fix(workstream): align CJS and SDK name validation to accept dots, guard path traversal via includes('..') (CR finding 5)
* fix(sdk): guard writeActiveWorkstream against non-existent workstream directory, k014/k031 parity (CR finding 6)
* chore(changeset): add #3269 changeset (CR finding 1 — proper changeset for this PR)
* docs(inventory): register 3 new CLI modules in INVENTORY.md/MANIFEST (active-workstream-store, workstream-name-policy, worktree-safety)
* fix(sdk): use relPlanningPath(workstream) in planningPaths, fix setActiveWorkstream/getActiveWorkstream name errors in workstream.ts
* fix(sdk): validate GSD_WORKSTREAM in planningPaths before use (#3269 regression)
planningPaths() called resolveWorkspaceContext() which returned GSD_WORKSTREAM
raw (no validation). An invalid value like '../evil' was used as effectiveWorkstream,
constructing a bad path; roadmapAnalyze() caught the ENOENT and returned a
no-phase_count error object instead of the root ROADMAP result.
Fix: validate envCtx.workstream with validateWorkstreamName() in planningPaths()
before accepting it as effectiveWorkstream. Invalid env → null → root .planning/
fallback, preserving the bug-2791 contract: invalid GSD_WORKSTREAM is silently
ignored and falls back to the root context (phase_count: 0 for empty root ROADMAP).
The bug-2791 regression test now passes. No other call sites read GSD_WORKSTREAM
without validation: query-runtime-context.ts already validates; cli.ts already
validates; context-engine.ts takes a caller-validated workstream parameter.
Closes#3268 (regression introduced by #3269 workstream-name-policy work).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3263): harden code-review SUMMARY parser; accept BL-/blocker as Critical-tier across pipeline
Bug 1: compute_file_scope Node script used ^\s*\w+: boundary regex, which excluded
hyphens and left inSection sticky after key-decisions:/patterns-established:/
requirements-completed: blocks. Prose bullets were captured as file paths. Fixed
to [\w-]+ boundary and added em-dash/parenthetical stripping with a path validity
guard so only path-shaped strings are emitted.
Bug 2: present_results grep matched only critical: in frontmatter. When reviewer
emitted blocker:, CRITICAL was silently empty. Fixed grep to accept both keys via
-E "^\s*(critical |blocker):". Top-issues preview also missed BL-* headings; fixed
to include ### BL-\ in the grep pattern.
Bug 3: gsd-code-fixer finding_parser documented CR-\d+ only. BL-* findings from
a drifted reviewer were silently dropped from critical_warning scope. Updated ID
alphabet, severity description, filter sets, and sort order to treat BL-* as
Critical-tier-equivalent to CR-*.
Reviewer contract: gsd-code-reviewer write_review step now declares blocker:/BL-
as accepted tier-equivalent alternatives to critical:/CR-, so the contract
acknowledges the reality the workflow defenses accept.
Regression tests: tests/code-review-pipeline-regression.test.cjs (18 tests)
covers all three bugs behaviourally (pure-function parsers) plus docs-parity
assertions on the workflow and agent .md files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: add fragment for PR 3274 (fix(3263) code-review parser)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(workflow): use POSIX [[:space:]] instead of \s in grep -E (CR finding 1)
BSD grep on macOS does not support \s in ERE; replace with the POSIX
[[:space:]] character class so the critical/blocker grep works on both
GNU and BSD grep. Also update the corresponding docs-parity test assertion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: tighten em-dash and grep docs-parity assertions (CR finding 2)
- Replace `includes('split(/\\s+')` with `includes('split(/\\s+—\\s')`
so the assertion actually enforces the em-dash narrative strip and
cannot be satisfied by a bare whitespace split.
- Update the present_results grep assertion to expect [[:space:]] after
the workflow portability fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3265): prefer YAML frontmatter for state-snapshot canonical fields
stateSnapshot in both sdk/src/query/state.ts and the CJS twin
(get-shit-done/bin/lib/state.cjs cmdStateSnapshot) passed the whole
STATE.md blob to stateExtractField, whose bold pattern (**Field:**)
has no line anchor. A body table cell such as
"**Status:** to ✅ COMPLETE" therefore silenced the correct YAML
frontmatter value.
Fix: extractFrontmatter(content) first; stripFrontmatter(content) for
the body passed to stateExtractField; for each canonical scalar field
prefer the non-empty frontmatter value, falling back to body extraction
when the key is absent or the file has no frontmatter block at all.
Regression tests added in sdk/src/query/state.test.ts (vitest) and
tests/state.test.cjs (node:test) covering:
- frontmatter status beats **Status:** inside a table cell
- frontmatter current_plan beats bold body value
- no-frontmatter files continue to extract from body
- field absent from frontmatter falls through to body extractor
Fixes#3265
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for #3275
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: reproduce fmStr drops non-string YAML scalars (#3275 CR finding)
Add tests/bug-3275-fmstr-non-string-scalars.test.cjs with 5 cases covering
CJS state-snapshot with numeric frontmatter scalars (current_phase: 19,
total_phases: 7, total_plans_in_phase: 5), string regression, and
no-frontmatter body fallback regression.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(state): fmStr accepts numeric/boolean YAML scalars (CR finding)
Rename `fmStr` to `fmScalar` in both state.cjs and sdk/src/query/state.ts
and broaden the type guard so that non-null number/boolean frontmatter values
are coerced to String(v) instead of being discarded.
The previous `typeof v === 'string'` check was a latent bug: if the YAML
parser ever returns typed scalars (e.g. `current_phase: 19` as the number 19),
the frontmatter value would be silently dropped and the stale body value used
instead. Both files are updated identically (k014 parity).
Also adds three SDK vitest regression cases (numeric current_phase,
total_phases, total_plans_in_phase) in sdk/src/query/state.test.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(adr): add ADR-0003 model catalog module
* fix(#3229): add shared model catalog as source of truth for agent profiles and runtime tier defaults
Research / design (ADR-0003):
- Existing drift came from 4 independent model truths:
1. CJS model-profiles.cjs
2. SDK config-query.ts stale copy (18 agents)
3. settings-advanced.md runtime tier table
4. session-runner Claude-only profile map
- New design: one machine-readable Model Catalog Module in sdk/shared/
that both packages ship and consume.
Implementation:
- sdk/shared/model-catalog.json — canonical source of truth for:
- full 33-agent registry
- per-agent golden (quality) alias + balanced/budget aliases
- adaptive derivation from routingTier
- agent→phaseType map
- agent→dynamic-routing default tier map
- runtime tier defaults for all supported runtimes
- get-shit-done/bin/lib/model-catalog.cjs — CJS adapter over the catalog
- sdk/src/model-catalog.ts — SDK adapter over the same catalog
- CJS model-profiles.cjs now re-exports derived data from model-catalog.cjs
- SDK config-query.ts now re-exports MODEL_PROFILES/VALID_PROFILES from
model-catalog.ts instead of maintaining its own list
- sdk/src/query/helpers.ts runtime list now comes from the catalog (fixes hermes drift)
- sdk/src/session-runner.ts Claude profile→model-id mapping now resolves via catalog
- docs/CONFIGURATION.md + settings-advanced.md runtime tables updated to match catalog
Behavior changes:
- resolve-model now covers every shipped agent file on disk (33 agents)
- unknown-agent fallback is profile-semantic, not hardcoded sonnet:
quality→opus, budget→haiku, balanced/adaptive→sonnet, inherit→inherit
- Group B runtimes remain known runtimes but do not get built-in tier defaults
Tests (RED→GREEN):
- root tests: shipped agent files must equal MODEL_PROFILES keys
- sdk tests: shipped agent files must equal MODEL_PROFILES keys
- direct fix assertion: gsd-code-reviewer resolves to opus under quality with no unknown_agent
- runtime defaults parity test: settings-advanced.md + CONFIGURATION.md tables must match catalog
- helper tests: hermes included in SUPPORTED_RUNTIMES and getRuntimeConfigDir()
Closes#3229
* chore(changeset): update #3229 changeset pr field to 3230
* fix(ci): update inherit fallback expectations and inventory parity for model catalog
* test: reproduce nested plans/ undercount in buildStateFrontmatter (#3257)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(state): count nested plans/<N>-PLAN-<NN>-<slug>.md in buildStateFrontmatter (#3257)
`buildStateFrontmatter` did a flat `readdirSync` on each phase directory and
missed plan files inside the nested `plans/` subdirectory written by
gsd-plan-phase (post-#3139 / #3115). Every state mutation flowing through
`syncStateFrontmatter` overwrote the curated `progress.*` frontmatter block
with the under-counted disk scan.
The fix adds a `plans/` descent using the same regex shapes as
`roadmap.cjs:countPhasePlansAndSummaries` and `phase.cjs:looksLikePlanFile`
(#2893/#3128). Both the `{N}-PLAN-{NN}-{slug}.md` (agent-emitted) and
`PLAN-{NN}-{slug}.md` (bare-prefix) forms are now matched. Outline files
(`-PLAN-OUTLINE.md`) and pre-bounce files are excluded. Flat-layout repos
are unaffected.
Note: the same algorithm now lives in 4 places (state.cjs, roadmap.cjs,
init.cjs, phase.cjs). Shared-helper extraction per CONTEXT.md k014 is
tracked in the follow-on issue filed with this PR.
Sibling fix to #3115 / #3139 / #3191 — state.cjs was missed in the
post-#3139 migration that updated the other three files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3261 for #3257
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(changelog): add entry for #3257 nested plans/ fix (#3261)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(state): broaden PLAN_PRE_BOUNCE_RE to match bare PLAN- prefix (CR)
PLAN_PRE_BOUNCE_RE was /-PLAN.*\.pre-bounce\.md$/i, which missed bare-prefix
files like PLAN-01-foo.pre-bounce.md in the nested plans/ scan — those would
incorrectly count as real plans. Broadened to /\.pre-bounce\.md$/i to exclude
any .pre-bounce.md file regardless of prefix shape.
Adds regression test for this exclusion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(state): extend nested plans scan to cmdStateValidate and cmdStateSync (CR finding)
`buildStateFrontmatter` already received the nested-aware scan in this PR, but
`cmdStateValidate` and `cmdStateSync` still did flat-only `readdirSync` on the
phase root, producing false plan-count drift warnings and under-counted totals
on `phases/<N>/plans/` repos. Extend the identical scan pattern to both sites
(regex byte-identical to the `buildStateFrontmatter` site, k014). Regression
tests added for all three commands.
Closes#3257
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(bug-3257): replace readFileSync+.includes() with structural dry-run idempotency check
The lint-no-source-grep rule flags readFileSync-bound variables used with
text-match methods (.includes, .match, etc.). Replace the afterContent.includes()
check with a structural idempotency assertion: run state sync --verify twice and
confirm the second run still reports a pending change, proving the first dry-run
did not mutate STATE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(bug-3257): fix progress assertion to use min(plan,phase) formula (#3242)
After rebasing onto main, computeProgressPercent now applies
min(plan_fraction, phase_fraction) per #3242 Bug B. Update the
multi-phase sync test to assert 50% (min(3/5, 1/2)) instead of 60%.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Deepen SDK package seam and converge runtime skills policy
* fix(sdk): unified install-root resolution for workflows and agents (CR finding 1)
Use the already-resolved gsdInstallDir constant instead of calling
resolveLegacyInstallDir() again when computing agentsDir, ensuring
workflowsDir and agentsDir share the same install root.
* fix(sdk): tilde shortening requires path-boundary match (CR finding 2)
Both renderGlobalSkillsBaseDisplayPath and renderGlobalSkillDisplayPath
used startsWith(home) which could incorrectly shorten unrelated paths
sharing the same prefix. Now checks for home === base or
base.startsWith(home + sep) to ensure a real directory boundary.
* fix(sdk): validate loadConfig export before invocation (CR finding 3)
After requiring core.cjs, check typeof mod.loadConfig === 'function'
before calling it. Throws a classified GSDError with the module path
if the export is missing, rather than a generic TypeError.
* fix(test): guard root lookup before .path dereference (CR finding 4)
Added assert.ok() guards for claudeRoot and codexRoot after the .find()
calls so that a missing root produces an explicit assertion failure
rather than a TypeError on .path dereference.
* fix(ci): fail-safe on transient API errors in approval dismissal (CR finding 6)
resolveRole() returns 'unknown' for non-404 errors (rate limits, 5xx,
network blips). shouldDismissReviewer() now treats 'unknown' as
unresolvable and skips dismissal, preventing legitimate approvals from
being dismissed due to a transient API failure. Only 'none' (true 404)
is treated as a confirmed non-collaborator.
* changeset: pr=3238 SDK package seam and runtime skills convergence
* fix(sdk): harden resolveGlobalSkillDir against path traversal (CR finding 1)
Use resolve+relative to validate that skillName cannot escape the global
skills base directory. Values like "../../foo" or absolute paths now
return null instead of joining directly. All imports (resolve, relative,
isAbsolute) were already present in helpers.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sdk): split skill-dir-resolution and skill-not-found warnings (CR finding 2)
After resolveGlobalSkillDir's hardening can return null for traversal
attempts, the old single-branch warning "Global skill not found at ..."
was misleading. Split into two distinct cases:
- skillDir === null → "Could not resolve global skill directory for ..."
- skillMd missing → "Global skill not found at ..."
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: lock skill path-traversal rejection in resolveGlobalSkillDir
Regression test verifying that traversal segments (../../foo, ../escape),
empty string, and absolute paths are all rejected (return null), while
a legitimate skill name resolves correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(sdk): align display-path contract + traversal coverage for resolveGlobalSkillMarkdownPath (CR nitpicks)
- renderGlobalSkillsBaseDisplayPath now returns a non-null string for
unsupported runtimes (e.g. cline → "(cline does not use a skills directory)")
matching the existing renderGlobalSkillDisplayPath contract; callers
of both helpers no longer need null-checks for unsupported runtimes.
- Remove now-redundant ! non-null assertion on renderGlobalSkillsBaseDisplayPath
calls in skill-manifest.ts (return type is string, not string | null).
- Extend the path-traversal test block to assert resolveGlobalSkillMarkdownPath
also propagates null for ../../foo, ../escape, empty, and /abs/path inputs,
locking the null-propagation contract against future refactors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: forbid stale /gsd-intel references in workflow/reference docs (#3258)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: replace retired /gsd-intel with /gsd-map-codebase --query (#3258)
Fixes 5 stale references across the two primary source files called out in
the issue. PR #2790 folded /gsd-intel into /gsd-map-codebase --query; these
prose surfaces were not updated at that time.
Fixes#3258
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: fix additional stale /gsd-intel references found in adversarial sweep (#3258)
Sweep found 7 more occurrences in docs/INVENTORY.md (x2), docs/USER-GUIDE.md (x4),
docs/FEATURES.md (x2), and agents/gsd-intel-updater.md (x2). All replaced with
/gsd-map-codebase --query. The gsd-intel-updater agent name itself (without leading
slash) is intentionally preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3260 for #3258
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: fail loudly on unreadable files in bug-3258 regression scan (CR finding)
Replace silent early-return on readFileSync failure with an explicit
throw so unreadable files surface as test failures rather than skipped
coverage gaps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: reproduce state.update progress trampling and percent formula (#3242)
Two failing regression tests:
- Bug A: state.update "Last Activity" tramples curated progress.* frontmatter via readModifyWriteStateMd → syncStateFrontmatter
- Bug B: 12 declared ROADMAP phases / 6 realized / 6/6 plans done → percent: 100 instead of 50 (phase-fraction ignored)
* fix(state): preserve curated progress on body-only updates; correct percent formula (#3242)
Bug A: readModifyWriteStateMd now accepts { resync: false } to preserve existing
frontmatter progress.* when only body text changes. cmdStateUpdate passes this flag
since it only replaces a body field and must not trample manually-curated
cross-milestone counters.
Bug B: extract computeProgressPercent() helper — shared by buildStateFrontmatter and
cmdStateSync — that applies min(plan_fraction, phase_fraction). When ROADMAP declares
more phases than are realized on disk, phase_fraction caps percent so 22/22 plans
done with only 6/12 phases gives 50%, not a false 100%.
* changeset: pr=3252 for #3242
* fix(test): replace content.includes with structured state json assertion (#3242)
* test: lock one-shot --seed capture contract (#3236)
* fix(capture): restore one-shot --seed contract (#3236)
* changeset: pr=3250 for #3236
* fix(capture): define $KEYWORD from $IDEA in collect-breadcrumbs step
* fix(workflow): add MD040 language identifiers to plant-seed code blocks (CR finding)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(workflow): wire --enrich path to skip parse-idea and target resolved seed (CR findings)
- parse-idea now detects --enrich SEED-NNN in $ARGUMENTS, sets $ENRICH_TARGET
and $SEED_FILE, and skips the interactive prompt + all capture steps entirely
- When $ARGUMENTS is non-empty but has no --enrich flag, uses it as $IDEA inline
- enrich-seed step derives $SEED_ID from $ENRICH_TARGET (already resolved by
parse-idea) and falls back to most-recent seed if $SEED_FILE is empty
- Enrichment commit now uses ${SEED_ID} in message and "$SEED_FILE" as --files,
targeting the resolved seed rather than the current capture-context path
Fixes CR findings on PR #3250 (Finding A lines 19-27, Finding B lines 132-133, 180-183)
* fix(workflow): add bash extraction for \$KEYWORD from \$IDEA (CR finding)
The collect-breadcrumbs step documented that \$KEYWORD should be derived
from \$IDEA, but provided no code to perform the extraction. Add a bash
block that lower-cases \$IDEA, strips punctuation, and picks the first
token longer than 2 characters, with a "seed" fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: reproduce CJS dispatcher rejecting dotted form (#3243)
runGsdTools assertions confirm that generate-slug.hello-world,
current-timestamp.date, validate.plan, roadmap.analyze, phases.list, and
check.decision-coverage-plan all fail with "Unknown command: <dotted>" —
the dispatcher switch only accepts the spaced form.
Edge cases (no dots unchanged, leading-dot rejected, unknown dotted form
suggests spaced equivalent) are also specified; those three pass already
because the shim is not yet implemented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cjs): accept dotted canonical command form (#3243)
Add a shim at the top of main() in gsd-tools.cjs that splits args[0] on
the first dot when present, normalizing "state.update" → command='state'
args=['state','update',...] before the switch statement is reached.
Any caller that bypasses the SDK (stale npm-installed binary, workflow
shell-out, third-party script) can now use the canonical dotted form
natively without hitting "Unknown command: <domain>.<subcommand>".
The shim guards against empty head/rest so ".hidden" and bare "." args
are unchanged and fall through to the existing "Unknown command" path.
Also improves the default "Unknown command" error message to suggest
the spaced equivalent when a dotted form was passed — e.g. for "foo.bar"
the error now reads: Unknown command: foo — did you mean: "foo bar"?
Parallel to dottedCommandToCjsArgv in sdk/src/query/query-fallback-bridge-adapter.ts;
intentionally kept separate to avoid SDK coupling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3248 for #3243
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: tighten dotted-form suggestion assertion (CR nitpick)
* fix(cjs): suggestion uses first-dot split (CR finding 1, multi-dot consistency)
The "did you mean" hint in the Unknown-command default case was replacing ALL
dots with spaces (state.update.foo → "state update foo"), but the dispatcher
shim only splits on the FIRST dot (state.update.foo → head=state, rest=update.foo).
Apply CR's exact patch to use indexOf+slice so suggestion matches dispatch
behavior. Add a multi-dot regression test (a.b.c must suggest "a b.c", not
"a b c").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(roadmap): parse **Mode:** field on phase sections
Adds a 'mode' field to roadmap.get-phase and roadmap.analyze outputs.
Recognizes '**Mode:** mvp' lines in phase sections; lowercased + trimmed.
Forward-compat: unrecognized values preserved verbatim, no enum check.
Foundation for --mvp flag in plan-phase (PRD: vertical-mvp-slice).
* feat(plan-phase): parse --mvp flag and resolve MVP_MODE
Resolution order: CLI flag → ROADMAP **Mode:** field → workflow.mvp_mode
config → false. Walking Skeleton gate fires for new-project Phase 1.
Wires MVP_MODE + WALKING_SKELETON into gsd-planner subagent prompt.
Per PRD vertical-mvp-slice Phase 1 (Q1, Q2, Q4).
* docs(planner): add vertical-slice planning reference
New reference loaded by gsd-planner when MVP_MODE=true. Defines slice
ordering, Walking Skeleton rules, and anti-patterns. Referenced from
plan-phase workflow MVP_MODE wiring.
* docs(planner): add SKELETON.md template
Template emitted by gsd-planner under WALKING_SKELETON=true. Captures
architectural decisions and out-of-scope list for new-project Phase 1.
* chore(inventory): register new planner references
Added planner-mvp-mode.md and skeleton-template.md to INVENTORY.md and
INVENTORY-MANIFEST.json. References now: 53.
* feat(gsd-planner): add MVP Mode Detection section
Mode-switched branch in the existing planner agent (per Q4: single agent).
Vertical-slice decomposition rules, Walking Skeleton handling, and
TDD-mode compatibility. Heavy guidance lives in references/planner-mvp-mode.md.
* test(plan-phase): add --mvp resolution-chain integration cases
Validates roadmap.get-phase --pick mode and confirms workflow.mvp_mode
default is unset in fresh projects.
* docs(changelog): announce --mvp vertical-slice planning (#2826)
* feat(mvp-phase): add /gsd mvp-phase slash command
Standalone command for vertical MVP planning. Frontmatter only;
heavyweight workflow at get-shit-done/workflows/mvp-phase.md follows
in next commit. Mirrors discuss-phase/edit-phase command shape.
* docs(planner): add user-story-template reference
Defines the canonical 'As a / I want to / So that' format and the
ROADMAP.md / PLAN.md emit rules. Used by mvp-phase workflow and
gsd-planner agent under MVP_MODE.
* docs(planner): add SPIDR splitting reference
Defines size signals, the five SPIDR axes (Spike/Paths/Interfaces/Data/Rules),
the interactive workflow, and anti-patterns. Per PRD Q3 decision: full
interactive flow, not lightweight check. Used by mvp-phase workflow.
* fix(mvp-phase): trim description to fit 100-char budget
* feat(mvp-phase): add mvp-phase workflow
Standalone workflow: phase validation -> user story prompts (As a / I want to /
So that) -> SPIDR splitting check -> ROADMAP write (Mode + Goal) -> delegation
to plan-phase. Per PRD Phase 2 (Q3 full SPIDR; Phase-2-A/B/C/D decisions).
Plan-phase auto-detects MVP via Phase 1's resolution chain, so no flags
are needed when delegating.
* feat(gsd-planner): emit user-story header in PLAN.md under MVP mode
Extends the MVP Mode Detection section (added in Phase 1) so the planner
sources the user story from ROADMAP **Goal:** and emits the bolded
**As a** / **I want to** / **so that** form as the first content under
the phase header in PLAN.md. References user-story-template.md.
* test(mvp-phase): integration smoke test for ROADMAP mutation
Validates roadmap.get-phase output after a workflow-spec'd ROADMAP write:
mode=mvp and goal=full user story. Catches schema drift between workflow
emit and parser expectation. Includes a long-story case (>120 chars) to
confirm SPIDR-rejected stories still parse correctly.
* chore(inventory): register mvp-phase command + 2 new references
Adds /gsd mvp-phase to commands list, mvp-phase workflow to workflows list,
and user-story-template.md + spidr-splitting.md to references. References
count: 53 -> 55.
* docs(changelog): announce /gsd mvp-phase command (#2826)
* fix(mvp-phase): add TEXT_MODE plain-text fallback for non-Claude runtimes (#2012)
* docs(executor): add MVP+TDD gate reference
Defines the runtime gate semantics for execute-phase when both
MVP_MODE and TDD_MODE are true: pre-task verification of failing-test
commit, end-of-phase review escalation from advisory to blocking,
behavior-adding task definition. Loaded conditionally by
execute-phase workflow and gsd-executor agent.
* feat(execute-phase): MVP+TDD runtime gate + blocking review
Resolves MVP_MODE in Step 1 (CLI flag -> roadmap mode -> config -> false).
Adds per-task gate that halts before behavior-adding tasks run if no
failing-test commit exists for the plan. Escalates end-of-phase TDD
review from advisory to blocking when both MVP_MODE and TDD_MODE active.
Also updates INVENTORY-MANIFEST.json to register execute-mvp-tdd.md
(added by Task 1) so manifest-sync tests pass.
Per PRD vertical-mvp-slice Phase 3a (decisions Phase-3-A, Phase-3-Split).
* feat(gsd-executor): add MVP+TDD Gate section
Mirrors the planner's MVP Mode Detection pattern from Phase 1.
Instructs halt-and-report when the runtime gate trips, references
execute-mvp-tdd.md for full semantics. No agent changes outside the
new section.
* test(execute-phase): add MVP+TDD resolution-chain integration cases
Validates roadmap.get-phase --pick mode and confirms workflow.mvp_mode
default is unset in fresh projects. Mirrors the Phase 1 plan-phase
resolution-chain integration test.
* chore(inventory): register execute-mvp-tdd reference
Bumps References count 55 -> 56. Registers execute-mvp-tdd.md.
Adds "init" to PROSE_ALLOWLIST in registry integration test so
bare `gsd-sdk query init` prose examples in plan docs don't
trigger the unregistered-handler guard (real commands are all
init.<subcommand>).
* docs(changelog): announce MVP+TDD runtime gate in execute-phase (#2826)
* docs(verifier): add verify-mvp-mode reference
Defines UAT framing under MVP mode: user-flow walk-through first,
technical checks deferred, coverage check as goal-backward narrowing
to the user story's outcome clause. Loaded conditionally by
verify-work workflow and gsd-verifier agent.
* feat(verify-work): MVP-mode UAT framing — user flow first
Resolves MVP_MODE from phase mode field. Under MVP mode, generates UAT
in three ordered sections: user-flow walk-through (derived from user
story), technical checks (deferred), coverage check (goal-backward).
Falls back to standard UAT generation when mode is null/absent.
User-story-format guard refuses to verify a mode:mvp phase with a
non-user-story goal.
Also updates docs/INVENTORY.md (56 references) and
docs/INVENTORY-MANIFEST.json to register verify-mvp-mode.md added
in Task 1.
Per PRD vertical-mvp-slice Phase 3b (decisions Phase-3-B,
Phase-3-Verify-Structure).
* feat(gsd-verifier): add MVP Mode Verification section
Narrows goal-backward verification to the user-story [outcome] clause
when phase mode is mvp. References verify-mvp-mode.md. Preserves
existing goal-backward methodology for non-MVP phases. User-story-format
guard refuses to verify a mode:mvp phase with a non-user-story goal.
* docs(changelog): announce MVP-mode UAT framing in verify-work (#2826)
* feat(new-project): add Vertical MVP vs Horizontal Layers mode prompt
Asks user at project init how to structure the project. Vertical MVP
emits **Mode:** mvp on every initial roadmap phase (per-phase mode
preserved per PRD Q1). Horizontal Layers falls back to standard
template — no behavioral change for existing flows.
Per PRD vertical-mvp-slice Phase 4 (decision Phase-4-Persistence).
* feat(progress): add MVP-mode user-flow display
When phase has **Mode:** mvp, progress renders user-flow status from
PLAN.md task names alongside standard task progress. Tasks that aren't
user-flow-shaped (technical-sounding) are filtered out of the user-flow
sub-block. Falls back to standard display when mode is null/absent.
Per PRD vertical-mvp-slice Phase 4 (decision Phase-4-Progress).
* feat(stats): add MVP phase count summary
Reads roadmap.analyze (which surfaces mode per phase from Phase 1) and
emits 'Phases: N total | M MVP | K standard' summary line. Suppressed
when MVP_COUNT == 0 to avoid clutter on non-MVP projects.
Per PRD vertical-mvp-slice Phase 4.
* feat(graphify): add MVP-mode visual differentiation
MVP-mode phases render with #22c55e fill color AND ' (MVP)' label
suffix — two-channel signaling for color-blind and grayscale renders.
Standard phases unchanged.
Per PRD vertical-mvp-slice Phase 4 (PRD Q5: distinct visual treatment).
* docs(changelog): announce Phase 4 discovery & progress (#2826)
* chore(release): bump dev to 1.50.0-canary.0 for first 1.50.0 canary
Sets the base version that .github/workflows/canary.yml derives the canary
tag from (strips suffix → base 1.50.0 → next available v1.50.0-canary.N).
This kicks off the 1.50.0 release train, opened by the MVP/TDD/UAT vertical
slice landed across PRs #2867, #2874, #2878, #2880, #2883.
* docs: add CANARY stream README + v1.50.0-canary.1 release notes
- docs/CANARY.md — explains the dev→@canary stream policy, install/rollback
paths, and when (not) to install canary builds
- docs/RELEASE-v1.50.0-canary.1.md — release notes for the first 1.50.0
canary cut: vertical MVP/TDD/UAT slice (#2867 + #2874 + #2878 + #2880 +
#2883), opening the 1.50.0 train under PRD #2826
- docs/README.md — index entry + quick link for the canary stream
* fix(ci/canary): publish gate checks dev branch, not main
Four publish-step `if:` conditions in .github/workflows/canary.yml were
checking `github.ref == 'refs/heads/main'`. Those steps (Tag and push,
Publish to npm, Publish SDK to npm, Verify publish) therefore always
skipped on every workflow_dispatch invocation since canary runs from dev,
never main.
The workflow's own header comment is unambiguous: `dev → @canary`. The
gate was a copy-paste from release.yml (which correctly targets main for
the @next/@latest streams) that was never corrected for the canary stream.
This is why the 1.50.0-canary.1 publish hadn't materialized despite three
green workflow runs. With the gate corrected, the next dispatch will
actually publish.
* ci(release-sdk): make release-sdk.yml dispatchable from the dev branch
The workflow lives on main only, so the GitHub Actions "Use workflow
from" dropdown doesn't list dev — meaning dev → @dev publishes can't be
triggered from the dev branch directly. Add the file to dev so an
operator can dispatch it with branch=dev and tag=dev.
Per project release-stream policy: dev branch publishes canary (@dev).
This is the stream that needs the file most, since main never publishes
@dev itself (main does @next / @latest).
File is byte-identical to main's release-sdk.yml — straight propagation,
no behavioral change. Tracking issues #2925, #2929.
* docs(mvp): canary-prep concept cleanup — CONTEXT.md, mvp-concepts index, --prd interaction (#3176)
* chore(mvp): concept cleanup + cross-ref index for v1.50.0-canary.2 prep
- CONTEXT.md gains 7 MVP domain terms (MVP Mode, User Story, Walking
Skeleton, Vertical Slice, Behavior-Adding Task, MVP+TDD Gate, SPIDR
Splitting) so the project glossary matches the shipped surface.
- New get-shit-done/references/mvp-concepts.md indexes the six MVP
reference files and concept-to-file map so agents and contributors
can find the right canonical doc without grepping.
- plan-phase.md Walking Skeleton block now documents that --mvp and
--prd compose orthogonally on Phase 1; no precedence needed.
- INVENTORY/INVENTORY-MANIFEST refreshed for the new reference (58 -> 59).
No behavior change. Canary-prep cleanup ahead of v1.50.0-canary.2.
Surfaced for follow-up (not in this PR):
- MVP_MODE resolution shell block duplicated across plan-phase,
execute-phase, verify-work workflows (needs a shared workflow-include
mechanism; structural change).
- Behavior-Adding Task predicate is prose-only; no shared utility.
- User Story regex hardcoded in verify-work; would benefit from a
central definition consumed by the verifier and the mvp-phase command.
* chore(changeset): set PR number for mvp concept cleanup
* feat(mvp): centralize resolution surfaces + fix SDK roadmap mode parity (#3178)
Three new SDK query verbs replace the architectural duplication surfaced by
the v1.50.0-canary.2 review against dev tip 12c4e565:
phase.mvp-mode <N> [--cli-flag]
Single canonical precedence resolver (CLI flag -> ROADMAP **Mode:** mvp
-> workflow.mvp_mode config -> false). Replaces 4-8 lines of bash that
were duplicated across plan-phase.md, execute-phase.md, verify-work.md,
and progress.md. Returns {active, source, roadmap_mode, config_mvp_mode,
cli_flag_present}.
task.is-behavior-adding <plan-file> | --task-content <xml>
Behavior-Adding Task predicate (tdd="true" + <behavior> block + non-test
source files in <files>). Replaces prose-only specification in
references/execute-mvp-tdd.md; gsd-executor agent now invokes the verb
instead of re-inlining the three checks. Returns {is_behavior_adding,
checks, reason}.
user-story.validate <text> | --story <text>
Owns the canonical User Story regex /^As a .+, I want to .+, so that .+\.$/
previously hardcoded in verify-work.md prose. Consumed by gsd-verifier
(phase-goal guard) and /gsd-mvp-phase (interactive-prompt validation).
Returns {valid, slots: {role, capability, outcome}, errors[]}.
Bug fix bundled: sdk/src/query/roadmap.ts searchPhaseInContent now extracts
the mode field from **Mode:**, restoring parity with roadmap.cjs:120-123.
Without this, roadmap.get-phase --pick mode returned null on the native
dispatch path even when the phase had **Mode:** mvp set, causing MVP_MODE
to silently fall through to the config/false branch in every consuming
workflow. The original PRs Phase 1 (#2885) shipped the CJS parser but the
SDK port omitted the field; this fix brings them back to parity.
Workflows + agents updated to call the verbs:
- plan-phase.md, execute-phase.md, verify-work.md, progress.md call
phase.mvp-mode (one line replaces the duplicated bash chains).
- execute-phase.md MVP+TDD gate calls task.is-behavior-adding.
- verify-work.md goal guard calls user-story.validate.
- mvp-phase.md interactive prompt validates via user-story.validate.
- gsd-executor agent references task.is-behavior-adding instead of prose.
- gsd-verifier agent references user-story.validate instead of inlined regex.
Tests: 24 new vitest tests in sdk/src/query/mvp.test.ts cover all three
verbs + the regression. Two existing contract tests (progress, verify)
updated to assert on the new verb shape. All 60 existing MVP contract
tests pass; golden integration suite (38 + 42 tests) passes.
Closes#3177
* fix(canary.2): unblock release gates for v1.50.0-canary.2
Run 25451329660 (Release SDK Bundle on dev, 2026-05-06T17:41) failed at the
test-suite step with 3 deterministic content/structure gate failures, all
attributable to the MVP umbrella integration in #3178 and the docs sweep
in #3180.
Failure 1: /gsd-mvp-phase undocumented in workflows/help.md
- tests/bug-2954-help-md-slash-command-stubs.test.cjs requires every
shipped commands/gsd/<X>.md to have a /gsd-<X> mention in help.md
- PR #3180 updated docs/COMMANDS.md but missed help.md (which the AI
agents load in-product)
- Fix: add a /gsd-mvp-phase entry to help.md right before /gsd-plan-phase
Failures 2 + 3: execute-phase.md (1727) and plan-phase.md (1714) over XL budget (1700)
- PR #3178 added MVP-mode verb calls (phase.mvp-mode, task.is-behavior-adding,
user-story.validate) to both workflow files, pushing them past 1700 lines
- Fix: bump XL_BUDGET 1700 -> 1800 with inline comment pointing at the
structural follow-up (extract MVP bodies to <workflow>/modes/mvp.md per
the discuss-phase/modes/ precedent)
- The structural extract is the right long-term fix but is bigger than
canary unblock scope; will land in a follow-up after canary cycles
Local verification:
$ node --test tests/bug-2954-help-md-slash-command-stubs.test.cjs tests/workflow-size-budget.test.cjs
tests 111 pass 111 fail 0
After this lands, re-trigger Release SDK Bundle on dev for v1.50.0-canary.2.
* chore(changeset): set PR number for canary.2 unblock
* fix(codex): generate-claude-md writes to AGENTS.md on Codex runtime
When config.runtime === 'codex' or GSD_RUNTIME=codex, override the
output target to AGENTS.md regardless of claude_md_path, so Codex
projects no longer have GSD sections written to CLAUDE.md by mistake.
Fixes both the CJS (gsd-tools) and SDK (profile-output.ts) paths.
Explicit --output flags are still honoured in both paths.
Closes#3163
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(plan-phase): remove agent: directive that caused OpenCode subagent dispatch
On OpenCode, any command with `agent: <name>` in its frontmatter is
auto-dispatched to a subagent context where the Agent tool is unavailable.
plan-phase.md and mvp-phase.md both carried `agent: gsd-planner`, causing
them to run inside gsd-planner's subagent context with no ability to spawn
researcher/planner/checker subagents — the orchestrator fell back to inline
execution for all three phases.
Fix: remove `agent: gsd-planner` from both command files so they run in the
main agent context. Also replace the stale `Task` tool in allowed-tools with
`Agent` (the correct dispatcher tool name post-#3168 rename).
Adds a structural regression test that parses YAML frontmatter of every
commands/gsd/*.md file and asserts no command carries an `agent:` directive.
Closes#3156
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mvp): address CodeRabbit workflow and contract findings
* fix(execute-phase): use registered state.update query command
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the inverted PRE_MERGE_FILES grep in the worktree-merge cleanup
block with the git-log --diff-filter=D history check introduced for
execute-phase.md by PR #2510. The old form deleted any .planning/ file
absent from the pre-merge snapshot — including brand-new files such as
SUMMARY.md — rather than only files with a confirmed deletion event on
main. Remove the now-unused PRE_MERGE_FILES snapshot line. Adds a
drift-guard test (node:test) asserting both workflows use WAS_DELETED and
neither uses the bare PRE_MERGE_FILES grep form.
Closes#3195
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(artifacts): register RETROSPECTIVE.md as canonical planning artifact
Adds RETROSPECTIVE.md to CANONICAL_EXACT in artifacts.cjs so gsd-health
no longer raises W019 after any /gsd-complete-milestone run. The file was
established as a living artifact in PR #644 but omitted from the W019
registry created in PR #2488.
Closes#3198
* chore(changeset): point pr metadata to #3200
Fixes#3164
Validators and find-phase hardcoded phasesDir = .planning/phases/, so projects
using the milestone-archive layout (.planning/milestones/v*-phases/) had an empty
diskPhases set, triggering W006 for every active phase and find-phase returning
found:false.
Add collectDiskPhases(planBase) helper that scans both flat layout and all
.planning/milestones/v*-phases/ subdirs. Wire it into cmdValidateConsistency,
cmdValidateHealth (both the Check 4 validPhases set and Check 8 diskPhases),
and refactor cmdFindPhase to iterate candidate search dirs so it also searches
milestone-archive dirs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#3168
The Claude Code subagent dispatcher tool is named `Agent` (with `subagent_type`
parameter). The `Task*` namespace (TaskCreate, TaskList, TaskGet, TaskUpdate,
TaskOutput, TaskStop) is the separate task-tracker. GSD's commands, workflows,
and agents were partially migrated and still referenced `- Task` / `Task(` in
55 files, causing orchestrators to silently fall back to inline execution when
no `Task` tool appeared on their tool surface.
Changes:
- `commands/gsd/*.md` allowed-tools: replaced `- Task` with `- Agent` in 24
files; removed duplicate `- Task` from autonomous.md (already had `- Agent`)
- `get-shit-done/workflows/*.md`: replaced dispatcher `Task(` → `Agent(` in
29 workflow files (~133 call sites); TaskCreate/List/Get/Update/Output/Stop
left untouched
- `agents/gsd-debug-session-manager.md`: replaced `Task` → `Agent` in tools
frontmatter (the only remaining agent with the wrong name)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#3162
`resolve_model_ids` is a documented top-level config key (CONFIGURATION.md)
read by core.cjs and session-runner.ts, but was missing from the CJS and SDK
VALID_CONFIG_KEYS allowlists — causing config-set to reject it with
"Unknown config key".
`workflow._auto_chain_active` is internal runtime state intentionally excluded
from VALID_CONFIG_KEYS by #2530, but plan-phase, execute-phase, discuss-phase,
transition, and new-project workflows all write it via `config-set`. Without
a valid write path these calls emit spurious errors (silenced with `|| true`
but noisy in logs). A new RUNTIME_STATE_KEYS set in config-schema.cjs holds
keys that isValidConfigKey() accepts without exposing them as user-settable
options — preserving the #2530 intent while fixing the runtime error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(graphify): TDD-red design contract for #3170 commit-staleness signal
Captures the proposed extension to graphifyStatus() as 8 failing
assertions across 3 groups (git-aware, non-git, back-compat). Suite is
describe.skip()'d so npm test stays green on the branch — removing
.skip is the green-light moment when the enhancement is approved and
implementation lands.
Verified against safishamsi/graphify v0.7.0 release notes: the field
on graph.json is built_at_commit (full git HEAD), not commit_hash as
originally guessed in #3170. Tests assert against the verified name.
Design highlights captured in the file's docstring:
- Tri-state commit_stale (true/false/null) — null means "we don't
know" (pre-v0.7 graph or no git), distinct from false ("known fresh")
- Argument-injection fence /^[0-9a-f]{4,40}$/i validates built_at_commit
before it reaches `git` as an argv element
- Existing graphifyStatus() fields (node_count, edge_count, stale,
age_hours, etc.) are unchanged — back-compat fenced
Per the issue's enhancement template: no PR will be opened until the
issue is labeled `approved-enhancement`.
Refs #3170
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(graphify): surface commit-based staleness from graphify v0.7+ built_at_commit
Closes#3170
graphify v0.7+ embeds built_at_commit (full git HEAD) into graph.json at
write time. GSD's existing graphifyStatus() ignored it; staleness was
mtime-only, which is a poor proxy for "does this graph reflect the
current code." A CI-built graph rebuilt minutes ago against an old
checkout reads as FRESH on mtime but is materially stale.
graphifyStatus() now returns four additional fields on the success path:
built_at_commit short hash from graph.built_at_commit, or null
current_commit short hash of git HEAD, or null when no git
commits_behind git rev-list --count <built>..HEAD, or null
commit_stale true | false | null
Tri-state on commit_stale is load-bearing. null means "we don't know"
(pre-v0.7 graph, non-git cwd, unreachable commit) — semantically
distinct from false ("known fresh"). Agents reading null should fall
back to mtime; reading false can confidently skip a rebuild.
Security: built_at_commit is on-disk and user-influenceable. Without
validation, a hostile value (e.g. "--upload-pack=evil") would reach git
as an argv element and be interpreted as an option. The
/^[0-9a-f]{4,40}$/i fence rejects anything else as absent. spawnSync's
array args (no shell) is defense in depth, not the boundary.
Skill (commands/gsd/graphify.md) Step 2b renders one conditional line:
Source commit: abc1234 (5 commits behind HEAD)
Source commit: abc1234 (current)
Source commit: abc1234 (freshness unknown)
Pre-v0.7 graphs omit the line entirely — no confusing "Source commit:
unknown" rendered.
Also documents `graphify hook install` in docs/CONFIGURATION.md for
multi-dev teams who would otherwise hit graph.json merge conflicts on
parallel rebuilds (sub-enhancement 2 from #3170).
TDD red→green: tests/enh-3170-graphify-commit-staleness.test.cjs
(8 assertions across git-aware, non-git, back-compat) was committed
describe.skip()'d in c567f23d when the issue was filed; this commit
removes .skip and lands the implementation that makes them green.
Full suite 7503/7503 passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MAJOR (security/correctness):
- commands/gsd/debug.md: add Write to allowed-tools (session file creation
requires it — workflow explicitly says 'use Write tool, never heredoc')
- workflows/debug.md: add SLUG sanitization guard to steps 1b+1c (status/
continue subcommands used raw user input in file paths — path traversal)
- workflows/thread.md: sanitize $ARGUMENTS in RESUME mode before file path
construction (was bypassing the sanitization guard in CLOSE/STATUS modes)
MINOR (consistency/correctness):
- docs/INVENTORY-MANIFEST.json: remove stale top-level 'workflows' array
(duplicate of families.workflows introduced in earlier update)
- commands/gsd/resume-work.md: normalize process to 'Execute end-to-end.'
- commands/gsd/settings.md: normalize process to 'Execute end-to-end.'
- commands/gsd/update.md: normalize otherwise branch to 'execute end-to-end.'
- docs/adr/0002: add Status: Accepted + Date header (ADR convention)
- workflows/extract-learnings.md: rename step extract_learnings → extract-learnings
- tests/extract-learnings.test.cjs: tighten step-name assertion to exact name
ARCHITECTURE:
- scripts/command-contract-helpers.cjs: extract CANONICAL_TOOLS, parseFrontmatter,
executionContextRefs as shared module — single source of truth consumed by
both lint script and test suite (prevents silent lint/test disagreement)
- scripts/lint-command-contract.cjs: require() helpers instead of duplicating
- tests/command-contract.test.cjs: require() helpers; move readFileSync calls
inside test() callbacks (registration-time throws surface as named failures)
After extracting debug.md and thread.md implementations to workflow files
and renaming extract_learnings.md, existing tests still referenced the
old locations:
- debug-session-management.test.cjs: commands/gsd/debug.md → workflows/debug.md
- thread-session-management.test.cjs: commands/gsd/thread.md → workflows/thread.md
- extract-learnings.test.cjs: extract_learnings.md → extract-learnings.md
- enh-2430-learnings-consumption.test.cjs: extract_learnings.md → extract-learnings.md
Also adds <available_agent_types> block and TEXT_MODE fallback note to
get-shit-done/workflows/debug.md to satisfy the spawn-type-consistency
(#1357) and AskUserQuestion text-mode fallback (#2012) contract tests
that scan all workflow files.
* fix(#3121): implement commands verb in SDK native registry
- Add commandsList handler — returns sorted JSON array of all registered
verb strings; satisfies workstream-flag.md + agent tooling discoverability
- Register ['commands', commandsList] in DECISION_ROUTING_STATIC_CATALOG
- Add golden-policy exemption (SDK-only, no CJS mirror needed)
- check.decision-coverage-plan/verify were already registered; commands was the remaining gap
Closes#3121
* fix(#3135): restore workflows/add-backlog.md — capture --backlog had no workflow to load
Root cause: PR #2824 consolidated add-backlog into gsd-capture --backlog and
wired capture.md to delegate to workflows/add-backlog.md via execution_context.
The workflow file was never created (same gap class as reapply-patches.md which
was caught and fixed in the same PR). With no file to load, the agent had no
implementation steps to follow when --backlog was invoked.
Fix:
- Restore get-shit-done/workflows/add-backlog.md with full process from deleted
commands/gsd/add-backlog.md (phase.next-decimal, ROADMAP write, mkdir, commit)
- Preserve #2280 ordering invariant: ROADMAP entry written before directory
- Fix docs/INVENTORY.md: remove incorrect attribution of --backlog to add-todo.md,
add add-backlog.md row, bump workflow count 84→85
- Update docs/INVENTORY-MANIFEST.json
- Add regression test: every execution_context @-reference in commands/gsd/*.md
must resolve to an existing workflow file on disk
Closes#3135
* fix(#3127): make state.begin-phase idempotent on mid-flight phases
Root cause: cmdStateBeginPhase() unconditionally overwrote execution-
progress fields regardless of current phase status. When execute-phase
called it on a phase already mid-flight (--wave N resume), it regressed:
- Current Plan to 1 (from e.g. 3)
- Last Activity Description to 'context gathered; ready for plan-phase'
- Plan: N of M body line to 'Plan: 1 of M'
- last_updated timestamp to an older value
- progress.percent could decrease
Fix: read Status field before writing. If phase is already executing
(Status: Executing Phase N), skip execution-progress fields and only
update fields safe on resume:
- Last Activity date (always safe)
- Resume-specific 'execution resumed (wave continue)' activity line
First-time execution (Status != Executing Phase N) writes all fields
as before -- no regression on the normal path.
Regression test: 4 real unit tests using synthetic STATE.md files:
- mid-flight phase does not reset Current Plan (was the bug)
- mid-flight phase does not overwrite stopped_at narrative
- fresh phase sets Current Plan to 1 (normal path, no regression)
- both paths update Last Activity date (safe field)
Suite: 6990/6990. Closes#3127.
* fix(lint+state): allow-test-rule, escapeRegex phaseNumber in idempotency guard
* fix(#3097, #3099): add cwd-drift + absolute-path guards to executor worktree protocol
#3097 — cwd-drift sentinel (gsd-executor.md task_commit_protocol step 0a):
A Bash cd out of the worktree makes [ -f .git ] false, silently skipping
all HEAD/branch safety guards. Commits land on main's branch.
Fix: on first commit, capture spawn-time toplevel into sentinel file at
.git/worktrees/<name>/gsd-spawn-toplevel. Before every subsequent commit,
verify ACTUAL_TL matches EXPECTED_TL. Exits 1 with recovery instructions
if drift detected.
#3099 — absolute-path guard (gsd-executor.md task_commit_protocol step 0b):
Absolute paths constructed from the orchestrator's pwd (main repo root)
resolve to the main repo inside worktrees. Edit/Write lands in wrong dir;
git commit sees a clean worktree tree; work silently lost or leaks to main.
Fix: before any absolute-path Edit/Write, verify path starts with
WT_ROOT=/Users/thbouc/projects/get-shit-done. Prefer relative paths.
Both guards are documented in references/worktree-path-safety.md, which
is now loaded into every executor spawn prompt via <execution_context>.
The <worktree_branch_check> footnote references all three steps (0/0a/0b).
execute-phase.md: extracted worktree bash commands to reference file
(safe embed — @ files are inlined before the executor processes the prompt).
The blank line in <required_reading> was removed to stay at the XL=1700 line
budget after adding the @ reference.
Suite: 6986/6986. Closes#3097. Closes#3099.
* fix(lint+executor+docs): allow-test-rule, fix [ -f .git ] guard, fail-closed abs-path check, fix INVENTORY count
* fix(#3096): enforce sequential Steps 7+8 + Edit-only discipline in ai-integration-phase
Root cause: Steps 7 (gsd-ai-researcher) and 8 (gsd-domain-researcher)
were listed without an explicit sequential constraint. An orchestrator
optimizing for speed could parallelize them since sections appeared
disjoint. gsd-domain-researcher's Write at finalization replaced the
full AI-SPEC.md with its in-memory copy (pre-researcher state), losing
Sections 3/4. Confirmed at 40% incidence (2/5 agents on a real run).
Recovery cost: one extra ai-researcher dispatch, ~18 min wall.
Fix:
- Explicit 'MUST run sequentially' note on Step 7 (ordering note)
- 'Wait for Step 7 to complete before spawning Step 8' on Step 8
- Edit-only tool discipline injected into both agent prompts:
'Use Edit exclusively - NEVER use Write on this file'
prevents the last-writer-wins overwrite regardless of dispatch order
Suite: 7043/7043. Closes#3096.
* fix(lint): allow-test-rule for ai-integration-phase structural contract test
* fix(#3120): add register_authored_at_plan_time guard to secure-phase
Root cause: Step 3 short-circuit used threats_open: 0 as the sole
condition to skip directly to Step 6 (write clean SECURITY.md). It
did not distinguish empty-by-all-mitigated from empty-by-no-planning.
Legacy phases authored before <threat_model> blocks were canonical
received a rubber-stamped clean SECURITY.md with no audit performed.
Fix:
Step 2c: track register_authored_at_plan_time (true iff >=1 PLAN
file contained a parseable <threat_model> block)
Step 3: two-condition short-circuit:
- threats_open:0 AND register_authored_at_plan_time:true
-> skip to Step 6 (legitimate, all mitigated)
- threats_open:0 AND register_authored_at_plan_time:false
-> retroactive-STRIDE mode in Step 5 (build register
from implementation, then verify)
Step 5: auditor constraint varies by mode:
planned -> Verify mitigations exist, do not scan
retroactive -> Build STRIDE register first, then verify
Suite: 7039/7039. Closes#3120.
* fix(lint+changeset): allow-test-rule, drop dead regex branches, fix pr field to 3142
* fix(#3128): extend roadmap.cjs plan-count to match {N}-PLAN-{NN}-{slug}.md
Root cause: same regex flaw as #2893 (fixed in phase.cjs by #2896).
The manager-dashboard countPhasePlansAndSummaries() in roadmap.cjs was
not updated alongside the phase.cjs fix. Files like 5-PLAN-01-setup.md
end in -setup.md, not -PLAN.md, so plan_count returned 0.
Symptom: init manager returned plan_count=0 / disk_status=discussed for
fully-planned phases, triggering redundant background planner agents that
correctly detected existing plans and declined -- wasted runs.
Fix: apply the same looksLikePlanFile pattern from phase.cjs with
PLAN-OUTLINE and pre-bounce exclusions to countPhasePlansAndSummaries.
Regression test: tests/bug-3128-roadmap-plan-count-slug-layout.test.cjs
Suite: 6985/6985. Closes#3128.
* fix(lint): allow-test-rule for roadmap isPlanFile structural contract test
* fix(#3130): harden update.md npx invocations against cache-stale and token-routing
Two failure modes with the old form:
1. Cache-stale: npx serves a cached older version (no --package= flag)
2. Token-routing: Bash-tool wrapper misroutes @ token in package@tag spec
All three sibling invocations (local/global/unknown) now use:
npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc $ARGS
--package= forces a fresh registry fetch; -- prevents token misrouting.
Also fixes the manual-update hint in the error-exit block.
Regression test: tests/bug-3130-update-npx-robust-invocation.test.cjs
Suite: 6973/6973 pass. Closes#3130.
* fix(lint): allow-test-rule for update.md structural contract test
- discuss-phase --assumptions → list-phase-assumptions.md
- pause-work --report → session-report.md
- manager --analyze-deps → analyze-dependencies.md
- import --from-gsd2 → gsd-tools.cjs from-gsd2 CLI
TDD: 8 new assertions in enh-2790-skill-consolidation.test.cjs
(argument-hint presence + body dispatch reference per flag).
Confirmed RED before wiring, GREEN after. Full suite 6971/6971.
help.md updated with all four new flag forms to satisfy
bug-2954-help-md-slash-command-stubs parity test.
Closes#3131