Reconciles main to the v1.8.0 tree while preserving the [1.7.0] CHANGELOG
section that release/1.8.0 omitted (root-caused in #2502: the main->next
auto-backmerge failed after 1.7.0, so next never received 1.7.0's promoted
CHANGELOG). Both parents kept so the v1.7.0 and v1.6.1 tags remain in
main's ancestry. This also delivers the fixed auto-backmerge.yml (build:lib
step, #2281) to main so the main->next backmerge stops failing.
The sonnet-5 catalog change alters model-catalog.json and settings-advanced.md,
so the per-runtime install-hash fixtures are recaptured (UPDATE_GOLDEN=1). Only
those two file hashes change per runtime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 1.5.0 release section rendered two product-name parentheticals that
the product-name-purity gate (#1777) forbids:
Claude Code (background dispatch is kept ...)
Claude (`~/.claude/skills/gsd-ns-<router>/skills/<stem>/SKILL.md`)
Rewritten to the already-accepted forms (semicolon clause; "Claude at
`path`") so the back-merge into next passes the gate next enforces.
No code or behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: bump version to 1.4.5 for hotfix
* fix(#1133): honor model_policy on the claude runtime via alias mapping
model_policy (including the anthropic-fable Claude Fable 5 preset) was
silently ignored on the default `claude` runtime: resolveModelInternal
gated the policy branch on `runtime !== 'claude'`, so agents kept
resolving from model_profile and no warning was emitted.
Wire model_policy into the claude path: map the policy-resolved model ID
back to a Claude Code agent alias (claude-fable-5 -> fable, claude-opus-4-8
-> opus, etc.). A value that is already a bare alias is returned as-is; an
ID with no Claude alias warns once to stderr and falls back to the
configured tier alias rather than emitting an unspawnable ID. Non-claude
runtimes return full IDs verbatim (unchanged) and resolveModelForTier
precedence is unchanged.
Authored against the v1.4.4 base (src/core.cts) for the 1.4.5 hotfix
line — NOT next, where this logic lives in src/model-resolver.cts; the
forward-port to next is a separate follow-up.
Closes#1133
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: promote CHANGELOG for v1.4.5
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#690): promote CHANGELOG 1.3.x + gate release-notes promotion
/gsd:update showed an empty "What's New" preview after updating to 1.3.1
because CHANGELOG.md's 1.3.x content was never promoted out of [Unreleased]
into dated sections, so `scripts/changeset/cli.cjs extract` returned exit 2
("no releases in range").
- CHANGELOG.md: split [Unreleased] into dated [1.3.0] and [1.3.1] sections
(1.3.1 = hono advisory bump + installer-migration checksum self-heal, #670;
1.3.0 = the feature release), restoring an empty [Unreleased].
- scripts/changeset/cli.cjs: new `verify` subcommand that exits non-zero when
CHANGELOG has no dated `## [x.y.z]` heading for a version; hoist shared
stripV/resolveChangelogPath helpers used by extract + verify.
- .github/workflows/release.yml: gate the finalize job on `verify` (after the
build, before tag/publish) so an unpromoted CHANGELOG can never ship again.
- gsd-core/workflows/update.md: move `rm -f $CHANGELOG_TMP` after the
human-readable extract re-run so the preview no longer degrades to
"(changelog unavailable)".
- tests: regression guard for the 1.3.x headings + extract range + verify
command coverage (present/absent/undated/v-prefixed/--json/prerelease).
Closes#690
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#690): add changeset fragment for #694
Fixed-type fragment for the user-facing /gsd:update preview fix and the
release-notes promotion gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Consolidate all pre-rename release notes (the retired get-shit-done-cc /
get-shit-done-redux lineage, 1.0.0 -> 1.50.0-canary.1) into one condensed,
read-only archive so the legacy 1.x version numbers no longer collide with
the current @opengsd/gsd-core line.
- Add docs/RELEASE-NOTES-LEGACY.md: rename banner, master version-index
table, condensed per-version sections (1.42.3 -> 1.0.0), and a separate
pre-release & canary builds section. Stale install commands stripped.
- Trim CHANGELOG.md to the current @opengsd/gsd-core line only; replace the
Legacy Release History block with a pointer to the archive and drop the
orphaned numbered legacy reference-link definitions.
- Remove the 10 standalone docs/RELEASE-v*.md files.
- Repoint docs/CANARY.md and docs/FEATURES.md links to the new archive.
Closes#658
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore: revert redundant CHANGELOG.md row from #3308
PR #3308 added a Shared scanPhasePlans helper row to CHANGELOG.md while
also dropping the canonical .changeset/3262-extract-scan-phase-plans.md
fragment. CONTRIBUTING.md (line 110) prohibits hand-editing CHANGELOG.md
since the release workflow folds .changeset/*.md fragments into the
file at release time — the manual row would duplicate at next release.
Removes only the 4-line Enhancement block added by #3308. The fragment
remains unchanged and is the single source of truth for this entry.
Refs #3262
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(changeset): add Removed fragment for redundant CHANGELOG row cleanup
Documents the deletion in PR #3313 of the 4-line ### Enhancement block
that #3308 hand-wrote into CHANGELOG.md alongside its canonical
.changeset/3262-extract-scan-phase-plans.md fragment.
Refs #3262
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(phase-plans): red — shared scanPhasePlans contract + parity across call sites (#3262)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(phase-plans): extract shared scanPhasePlans helper (k014) (#3262)
Eliminates four divergent copies of the plan-scan algorithm:
- roadmap.cjs:countPhasePlansAndSummaries (root call site)
- state.cjs:buildStateFrontmatter (1 of 3)
- state.cjs:cmdStateValidate (2 of 3)
- state.cjs:cmdStateSync (3 of 3)
- init.cjs:listPhasePlanFiles / listPhaseSummaryFiles
New bin/lib/plan-scan.cjs exports scanPhasePlans(phaseDir) → {
planCount, summaryCount, completed, hasNestedPlans,
planFiles, summaryFiles
}
Divergences resolved:
- roadmap.cjs used a broad isPlanFile (any .md containing PLAN in name,
matching the extended layout 5-PLAN-01-setup.md); canonical helper
adopts this wider pattern as the reference implementation.
- state.cjs used a strict endsWith(-PLAN.md) filter, missing extended-
layout root files; now unified with roadmap.cjs semantics.
- init.cjs listPhasePlanFiles used ^PLAN-\d+ for nested, missing
the -PLAN-\d+ variant state.cjs also matched; helper includes both.
- pre-bounce exclusion broadened to /.pre-bounce.md$/i (any pre-bounce
file), not just -PLAN.*\.pre-bounce\.md (roadmap form) or flat
.pre-bounce.md (state form).
- OUTLINE exclusion broadened to /-OUTLINE\.md$/i to catch both
flat (-PLAN-OUTLINE.md) and nested (PLAN-01-OUTLINE.md) forms.
Sibling audit: no 5th call site found. phase.cjs:looksLikePlanFile is
a diagnostic probe for non-canonical naming (not a counter) — left
in place per its distinct purpose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changelog): add entry for #3262 scanPhasePlans extraction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add changeset fragment for #3262
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(inventory): add plan-scan.cjs row to INVENTORY.md CLI Modules table (#3262)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3262): update bug-3128 test + INVENTORY counts for plan-scan.cjs
- Update tests/bug-3128-roadmap-plan-count-slug-layout.test.cjs to verify
that roadmap.cjs delegates to plan-scan.cjs (require check) and that the
extended filter lives in plan-scan.cjs as isRootPlanFile with /PLAN/i
- Bump docs/INVENTORY.md CLI Modules headline from 46 to 47 (plan-scan.cjs)
- Regenerate docs/INVENTORY-MANIFEST.json to include cli_modules/plan-scan.cjs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3262): migrate two missed call sites to scanPhasePlans (k014)
- init.cjs cmdInitExecutePhase: replace inline /-PLAN\.md$/i filter
with listPhasePlanFiles(path) to honour nested, extended-layout, OUTLINE
and pre-bounce exclusions (CR finding)
- state.cjs cmdStateUpdateProgress: replace dual /-PLAN\.md$/i and
/-SUMMARY\.md$/i filters with scanPhasePlans() so the progress-bar
body field uses the same counts as buildStateFrontmatter frontmatter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3262): correct INVENTORY-MANIFEST.json to tracked files only
Remove 3 untracked local entries from cli_modules so the manifest matches
what CI sees (47 tracked .cjs files, not 50 local). Previous regeneration
ran against the local filesystem which included cjs-command-router-adapter.cjs,
state-document.cjs, and workstream-inventory.cjs (all untracked on this branch).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: phase-dir prefix parity across creation paths (#3287 RED)
Add failing tests asserting that init.phase-op and init.plan-phase
expose expected_phase_dir with the project_code prefix when the phase
directory does not yet exist — matching the prefix applied by phase.add.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(phase): unify phase-dir naming via shared getPhaseDirName helper (#3287)
Both init.phase-op (discuss-phase workflow) and init.plan-phase
(plan-phase workflow) now compute expected_phase_dir — the canonical
directory name including the project_code prefix when set — and expose
it in their JSON bundle.
Workflow fallback mkdir calls are updated to use ${expected_phase_dir}
instead of constructing the path from padded_phase + phase_slug, which
was missing the project_code prefix.
This eliminates the two-headed naming convention where phase.add/insert
produced XR-01-foundation/ while the first-touch paths produced
01-foundation/ for the same project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(phase): apply project_code prefix in scaffold phase-dir (#3287)
Audit finding: phase.scaffold (CJS commands.cjs + SDK phase-lifecycle.ts)
also constructed phase dir names without project_code prefix.
Both implementations now read config.project_code and apply the same
prefix logic as phase.add/phase.insert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3292 for #3287
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(changelog): add entry for #3287 phase-dir prefix parity fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
scripts/build-hooks.js used fs.copyFileSync (truncate-then-write, non-atomic).
Under --test-concurrency=4, multiple builder invocations raced; a parallel
install.js subprocess could readFileSync between truncate and write and
observe an empty file, then write that emptiness into the install target.
Surfaced as the release-blocking bug-2136-sh-hook-version part 4 failure on
main even though the same SHA passed every install-smoke matrix entry.
Fix: stage outputs to hooks/.dist-staging/ then fs.renameSync into hooks/dist/.
POSIX rename(2) is atomic, so concurrent readers always observe a complete
file. The existing bug-2136 part 4 test locks the post-fix invariant.
Failing run: https://github.com/gsd-build/get-shit-done/actions/runs/25472202941/job/74738276687Closes#3214
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
process.execPath on Homebrew resolves symlinks and returns the versioned
Cellar path (e.g. /usr/local/Cellar/node/25.8.1/bin/node). After
brew upgrade node, the old Cellar binary fails with dyld: Library not
loaded because shared libraries have changed SOVERSION.
- Add normalizeNodePath() helper that maps Cellar paths to stable
Homebrew symlinks (/usr/local/bin/node or /opt/homebrew/bin/node)
- resolveNodeRunner() now calls normalizeNodePath() before quoting
- rewriteLegacyManagedNodeHookCommands() also normalizes baked Cellar
runner paths in existing hook commands so reinstall doesn't re-bake them
- Export normalizeNodePath for testability
- Add 22 tests covering all cases (Cellar paths, stable symlinks,
NVM, system node, Windows, null/empty, both function surfaces)
Closes#3181
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- initMilestoneOp now accepts and propagates the workstream parameter:
relPlanningPath(workstream) replaces the hardcoded '.planning' dir,
getMilestoneInfo gets workstream passed, extractCurrentMilestone gets
workstream passed, archiveDir is derived from planningDir not root.
- resolveQueryRuntimeContext now reads .planning/active-workstream as a
third-priority fallback after --ws flag and GSD_WORKSTREAM env var,
completing the documented resolution chain for all query handlers.
Closes#3196
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hasSdk was parsed in bin/install.js but never passed to
installSdkIfNeeded, so `npx get-shit-done-cc@latest --sdk` silently
skipped SDK deployment via the isLocal early-return and emitted a
misleading "✓ GSD SDK ready" message.
installSdkIfNeeded now accepts opts.forceSdk. When true (set from
hasSdk at the call site in installAllRuntimes), the local-install
soft-skip is bypassed so the full shim-link path runs regardless of
install mode. When dist is also missing with forceSdk=true, the
fail-fast diagnostic fires instead of silently returning.
The #2678 soft-skip (isLocal + missing dist + no --sdk) is preserved.
Closes#3033
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>
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>
* 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>
* fix(graphify): run /gsd-graphify build inline instead of spawning a sub-agent
Closes#3166
graphify v0.7+ split the build into a fast AST-extraction phase (cached)
followed by a separate clustering + report-write phase. The cached
extraction phase survived sub-agent isolation, but the post-extraction
phase was SIGTERM'd when the agent exited, leaving the cache populated
and no graph.json / graph.html / GRAPH_REPORT.md artifacts written to
.planning/graphs/.
The skill now runs `graphify update .`, the three artifact copies, the
snapshot, and the status report as a single foreground Bash call so the
entire pipeline survives to completion. The CLI's `graphify build`
pre-flight still returns `action: "spawn_agent"` so external callers
and existing tests in tests/graphify.test.cjs keep working.
Regression test (tests/bug-3166-graphify-inline-build.test.cjs) parses
the skill's YAML frontmatter and body structurally to fence against
re-introducing Task to allowed-tools or `Task(` invocation syntax — a
future edit cannot regress the fix without tripping the fence.
Verified against safishamsi/graphify v0.7.0–v0.7.8 release notes:
`graphify update .` invocation and output filenames are unchanged in
v0.7+; no GSD-side interface migration is required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): drop yaml dep from bug-3166 fence — replace with inline parser
CI failed with MODULE_NOT_FOUND on `require('yaml')` — the package
resolved locally as a transitive dep but isn't declared in package.json.
The project pattern (see tests/helpers.cjs `parseFrontmatter`) deliberately
avoids pulling in yaml/js-yaml.
Replace with a narrow inline parser that handles the scalar + block-list
subset used in this skill's frontmatter. Verified the fence still trips
when Task is reintroduced to allowed-tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): parse fenced blocks structurally for #3166 fence
Address CodeRabbit nitpicks on PR #3169: the body assertions used raw
markdown text regex (\bTask\s*\(/, /graphify\s+update\s+\./) which
violates the project's "parse, never grep" testing convention and risks
false-positives on prose.
Replace with extractFencedBlocks(body) which returns
[{lang, content}, ...] tuples per markdown code fence. Body assertions
now run against parsed blocks:
- "no fenced code block contains Task("
→ deepEqual offending blocks to [] (vs. regex on raw body)
- "a bash block invokes graphify update . / build snapshot"
→ filter to lang === 'bash', then substring-check inside parsed content
Substring checks within already-parsed fenced content are structural —
prose mentioning the word "Task" can no longer false-positive, and a
future prose reference to graphify cannot satisfy the positive assertions
either. The frontmatter side already used a parser; both sides now match.
Verified: re-introducing Task( inside a code fence still trips the
assertion. Full suite 7499/7499 passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): rename readFileSync-bound var to satisfy lint-no-source-grep
The structural-parse refactor introduced `b.content.includes(...)` calls
on parsed fenced-block records, but `loadSkill()` had also bound
`const content = fs.readFileSync(...)` for the markdown text. The
lint-no-source-grep regex scanner cannot distinguish scopes — it sees
"variable `content` is bound from readFileSync" and "`content.includes`
is called" and flags it as a source-grep test, even though the two
`content`s are different lexical entities.
Rename the readFileSync-bound local to `markdown`. Now `b.content` is
unambiguously a property access on a parsed-block record. Lint passes
(0 violations across 401 test files); behavior unchanged (4/4 tests
still pass, including the negative regression case).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): tighten snapshot assertion to gsd-tools.cjs prefix
CodeRabbit nitpick on bug-3166 fence: the snapshot bash assertion accepted
any 'graphify build snapshot' substring. Tighten to require it follows
'gsd-tools.cjs', matching the actual fenced invocation in
commands/gsd/graphify.md (which uses node "$HOME/.../gsd-tools.cjs" graphify
build snapshot — note the closing quote, so a literal 'gsd-tools graphify build
snapshot' substring would not match).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(en): update FEATURES/USER-GUIDE/COMMANDS for v1.40.0 surface
- FEATURES.md: append v1.40.0 section (#122 skill consolidation, #123
namespace meta-skills, #124 context-window guard, #125 phase-lifecycle
status-line read-side); add to TOC.
- USER-GUIDE.md: add slash-command form (hyphen vs colon) primer and
namespace routing primer; replace deleted slash forms in walkthroughs
(`/gsd-add-backlog`, `/gsd-plant-seed`, `/gsd-add-phase`,
`/gsd-set-profile`, `/gsd-list-workspaces`, etc.) with consolidated
forms (`/gsd-capture --backlog`, `/gsd-phase --insert`,
`/gsd-config --profile`, `/gsd-workspace --list`, etc.); fix
`/gsd-spike-wrap-up` and `/gsd-sketch-wrap-up` to flag form.
- COMMANDS.md: clarify Command Syntax (Gemini = colon form, others =
hyphen form); add Namespace Meta-Skills section with all six routers;
add `--context` to /gsd-health flag table.
Refs #3047
* docs(en): refresh INVENTORY/CLI-TOOLS/STATE-MD-LIFECYCLE for v1.40.0
- INVENTORY.md: workflow-row "Invoked by" column updated to point at
consolidated commands (`/gsd-phase` family, `/gsd-workspace --list`,
`/gsd-config --advanced/--integrations/--profile`,
`/gsd-sketch --wrap-up`, `/gsd-spike --wrap-up`); CLI-modules row for
`secrets.cjs` updated to `/gsd-config --integrations`. Command count
and namespace meta-skills section already reflect 65 shipped (= 59
consolidated sub-skills + 6 ns-* routers).
- CLI-TOOLS.md: add `validate context` row under Validation Commands
with the 60 %/70 % threshold envelope used by `/gsd-health --context`.
- STATE-MD-LIFECYCLE.md: flip status header from "proposed" to
"shipped in v1.40.0" since `parseStateMd()` and `formatGsdState()`
now read and render `active_phase`, `next_action`, `next_phases`,
and `progress`.
`docs/AGENTS.md` audited and verified clean — `gsd-code-fixer` row
already lists the correct `/gsd-code-review --fix` spawner; no
deleted-skill references found. `docs/INVENTORY-MANIFEST.json`
audited and verified clean — already enumerates the 65 commands
(including six ns-* routers) and contains no deleted slash forms.
Refs #3047
* docs(en): cleanup ARCHITECTURE/CONFIGURATION for v1.40.0
- ARCHITECTURE.md: split Commands install-target list to call out the
Gemini colon form (`/gsd:command-name`) vs hyphen form for every
other runtime. Add a new subsection covering two-stage hierarchical
routing via the six namespace meta-skills (#2792) and a paired note
on the MCP token-budget interaction so readers see the two big
per-turn cost levers in one place.
- CONFIGURATION.md: rewrite three references to the deleted
`/gsd-settings-advanced` and `/gsd-settings-integrations` slash
forms to use the consolidated `/gsd-config --advanced` /
`/gsd-config --integrations` invocations. Add a new "STATE.md
Frontmatter (Phase Lifecycle)" section documenting the four
optional fields (`active_phase`, `next_action`, `next_phases`,
`progress`) read by the v1.40 status-line, with a pointer to
STATE-MD-LIFECYCLE.md for the full reference.
`docs/manual-update.md` audited and verified clean — already documents
`/gsd-update --reapply` (the consolidated form), no reference to the
deleted `/gsd-reapply-patches`.
Refs #3047
* docs(i18n): mirror v1.40.0 slash-command rename into ja-JP/ko-KR/zh-CN/pt-BR
Mechanical token-level renames only — every reference to a deleted
micro-skill slash form is rewritten to the consolidated form on the
matching parent skill. No prose was machine-translated; new prose
sections (slash-form primer, namespace routing primer, v1.40 feature
entries, STATE.md frontmatter) were left for human translator
follow-up.
Renames applied uniformly across all four trees:
/gsd-add-todo, /gsd-add-note, /gsd-add-backlog,
/gsd-plant-seed, /gsd-check-todos → /gsd-capture[ --note|
--backlog|--seed|--list]
/gsd-add-phase, /gsd-insert-phase,
/gsd-remove-phase, /gsd-edit-phase → /gsd-phase[ --insert|
--remove|--edit]
/gsd-new-workspace, /gsd-list-workspaces,
/gsd-remove-workspace → /gsd-workspace[ --new|
--list|--remove]
/gsd-settings-advanced,
/gsd-settings-integrations,
/gsd-set-profile → /gsd-config[ --advanced|
--integrations|--profile]
/gsd-sketch-wrap-up → /gsd-sketch --wrap-up
/gsd-spike-wrap-up → /gsd-spike --wrap-up
/gsd-reapply-patches → /gsd-update --reapply
/gsd-code-review-fix → /gsd-code-review --fix
/gsd-plan-milestone-gaps → /gsd-audit-milestone
Refs #3047
* docs(changelog): regroup [Unreleased] under Feature/Enhancement/Fix
Replace the existing Keep-a-Changelog \`Added\` / \`Changed\` /
\`Performance\` / \`Removed\` / \`Fixed\` sub-headers in the [Unreleased]
block with the issue/PR template taxonomy:
Added → Feature
Changed / Performance → Enhancement
Removed → Enhancement
Fixed → Fix
Order within the release: Feature → Enhancement → Fix. Every bullet
preserved verbatim — only headers and grouping changed; the awkward
inline-versioned headers (\`### Added — 1.40.0-rc.1\`,
\`### Changed — 1.40.0-rc.1\`, \`### Fixed — 1.40.0-rc.1\`) folded into
the same buckets with the \`— 1.40.0-rc.1\` suffix dropped, since the
[Unreleased] block IS 1.40.0-rc.1.
The [1.39.2] hotfix block called out in #3047's spec does not yet
exist in CHANGELOG.md (the previously released hotfix is [1.39.1]),
so this commit only regroups [Unreleased]. Older release blocks
([1.39.1] and earlier) are frozen and untouched.
Refs #3047
* docs(changeset): add fragment for v1.40.0 doc audit
Refs #3047
* docs(en): strip leading / from deleted slash-command tokens in FEATURES
REQ-CONSOLIDATE-03 and REQ-CONSOLIDATE-04 listed deleted commands by
their `/gsd-foo` form for the historical record. The docs-parity tests
in bug-3010, bug-3029-3034, and bug-3042-3044 use the regex
`/\/gsd-[a-z0-9][a-z0-9-]*/g` to scan user-facing surfaces for any
remaining mention of removed slash forms — they cannot tell prose
about a deleted command from a live recommendation.
Strip the leading slash from the bare-name references (preserve the
historical text otherwise). Tests now require a `/` prefix to match,
so `gsd-add-todo` reads identically to a human but no longer trips
the parser.
Verified locally: 65/65 tests pass across the three docs-parity
suites that were red on CI run 25270072600.
Refs #3047
* docs(en): fix CR feedback + drop literal /gsd:plan-phase from USER-GUIDE
CI: tests/bug-2543-gsd-slash-namespace.test.cjs flagged
docs/USER-GUIDE.md:35 for embedding the literal `/gsd:plan-phase`
token in the parenthetical Gemini-form example. The test scans every
.md under docs/ for `/gsd:<live-cmd>` because non-Gemini surfaces must
not advertise the colon form. Replaced the literal example with a
prose substitution rule.
CR: docs/ARCHITECTURE.md:125 — the namespace meta-skills were listed
by file-prefix (`gsd-ns-workflow`) but the invocable frontmatter `name:`
is the bare form (`gsd-workflow`). Verified against the six
`commands/gsd/ns-*.md` files. Replaced with the canonical names and
noted the file/name disagreement in-line.
CR: docs/COMMANDS.md:723 — `v1.40` aligned to canonical `v1.40.0`.
CR: docs/FEATURES.md:2679 — REQ-CTX-GUARD-02 advertised the wrong
invocation (`gsd-tools validate context`). The shipped handler is
exposed via `gsd-sdk query validate.context` and requires explicit
`--tokens-used <int>` + `--context-window <int>` flags (verified
against sdk/src/query/validate.ts:849-882 and
get-shit-done/bin/lib/validate-command-router.cjs:19-36).
CR: docs/zh-CN/README.md:533 — added `inherit` to the profile-options
parenthetical to match the canonical set (verified against
model-profiles.cjs:29 `VALID_PROFILES = […MODEL_PROFILES['gsd-planner'], 'inherit']`).
Verified locally: 74/74 tests pass across the four docs-parity suites
that were red on CI runs 25270072600 and 25270182903.
Refs #3047
* fix(#2998): populate gsd-pristine/ from install transform pipeline so verifier has a real baseline
saveLocalPatches declared a pristineDir variable and JSDoc'd 'saves
pristine copies to gsd-pristine/' but no code ever wrote there. Effect:
/gsd-reapply-patches Step 5 verifier (#2972) silently fell back to its
over-broad heuristic ('every significant backup line') -- exactly the
silent-success-on-lost-content failure mode #2969 was designed to
prevent.
Fix: new populatePristineDir({...}) helper runs copyWithPathReplacement
(the install transform pipeline) into a tmp staging dir, then copies out
only the modified-file paths into gsd-pristine/. saveLocalPatches now
accepts a pristineCtx and calls the helper when local patches are
detected. Soft-fails on transform errors (logs warning, continues with
empty pristine -- no worse than pre-fix).
Pristine reflects the about-to-install version's content, which is the
right baseline for 'what would survive without the user's modifications'.
Tests: bug-2998-pristine-dir-populated.test.cjs asserts the helper is
exported, no-ops on empty input, writes one pristine file per source-
existing path, skips ghost paths, and produces deterministic output
(byte-identical across runs -- the property pristine_hashes depends on).
Closes#2998
* chore(#2998): add changeset fragment for PR #3004
* fix(#2998): expand pristine to all manifest install roots; clear stale pristine on populate (CR)
CodeRabbit on PR #3004 caught two issues:
1. populatePristineDir only staged packageSrc/get-shit-done/ but
manifest.files records edits under several install roots (commands/,
agents/, hooks/, skills/, root files like .clinerules). Modified
paths outside get-shit-done/ were silently skipped, leaving the
verifier with no baseline for those edits. Fixed by computing the
set of top-level dirs from the modified set and staging each one
that exists in source. Root-level files (no slash) bypass the
transform pipeline and are copied directly.
2. populatePristineDir did not wipe pre-existing gsd-pristine/ before
populating. A previous run's stale pristine could survive into the
current run's diff baseline. Now wipe before populate AND in the
catch path so soft-failures don't leave half-populated data on disk.
Tests: bug-2998-pristine-dir-populated.test.cjs grows by 2 cases:
- agents/ paths are staged and copied (was silently skipped pre-fix)
- mixed get-shit-done/ + agents/ in same modified list both stage
* test(#2986): mutation-killer suite for config-schema.cjs (95 typed assertions)
Stryker measured 4.62% mutation score on config-schema.cjs (6 killed,
124 survived). Surviving mutants documented that existing tests were
exercising paths without verifying outputs.
Adds tests/bug-2986-config-schema-mutation-killers.test.cjs (95 tests,
4 suites) targeting each surviving mutant class:
- M1/M4: parameterized isValidConfigKey(key) === true for every member
of VALID_CONFIG_KEYS. Kills static-key-fast-path mutations
(if (VALID_CONFIG_KEYS.has(...)) return true; -> if (false) return true;)
because no static key matches any DYNAMIC_KEY_PATTERN by design.
- M2: representative dynamic-pattern keys (one per pattern). Each matches
exactly one pattern. Kills .some -> .every mutation: with .every, no
single key matches all patterns -> all dynamic keys would be rejected.
- M3: strictEqual against the literal boolean true/false (not assert.ok
truthy checks). Kills polarity-flip mutations.
- Anchor-tightening: keys that differ from valid by one char beyond the
documented shape (trailing dot-segment, empty agent name, non-enum tier,
etc.). Kills regex-loosening mutations on ^, $, charset boundaries.
Tests assert on typed boolean return values from the lib's public surface.
Zero source-grep, zero raw-text matching.
* chore(#2986): add changeset fragment for PR #3005
* test(#2986): use dynamic-only rep key for features pattern (CR feedback)
CodeRabbit on PR #3005: features.thinking_partner is in the static
VALID_CONFIG_KEYS set, so the static fast-path returns true before
DYNAMIC_KEY_PATTERNS.some() is ever called. A Stryker mutant that
removed only the features entry from DYNAMIC_KEY_PATTERNS would
survive because the test only ever exercised the static path for
that key.
Replaced features.thinking_partner with features.some_dynamic_feature
which is NOT in static keys, so isValidConfigKey must reach the
dynamic path to return true. Added a per-rep invariant that asserts
each representative key is NOT a member of VALID_CONFIG_KEYS,
catching this class of mistake at test time on any future
representative-key change.
Both v1.39.0 (stable, tagged 2026-05-01T03:05:33Z) and v1.39.1
(hotfix, tagged 2026-05-01T21:03:54Z) shipped to npm but the
CHANGELOG `[Unreleased]` link still pointed at `v1.38.5...HEAD` and
the entries that landed in v1.39.1 were still un-promoted.
Move the five v1.39.1 hotfix entries (#2917, #2949, #2954, #2962,
#2969) into a new `## [1.39.1] - 2026-05-01` section above
`## [1.38.5]`, with a one-line intro and install snippet matching
the conventions used in earlier dated sections.
Update the `[Unreleased]` link to point at `v1.39.1...HEAD`.
Out of scope (separate cleanup):
- Backfilling a `## [1.39.0]` section. The CHANGELOG never had one;
this PR doesn't make that worse but also doesn't try to invent
release-note text from commit messages.
- The eight v1.39.1 commits without `[Unreleased]` entries
(#2942, #2944, #2924/#2941, #2940, #2947, #2950, #2948, #2957).
These weren't in `[Unreleased]` to begin with; faithful
promotion only moves what was already documented.
- Adding a `docs/RELEASE-v1.39.1.md` file. The `docs/RELEASE-*.md`
pattern in this repo is RC-only; stable patches historically
don't have a counterpart.
The post-v1.39.1 hardening entries (#2980, #2983, #2987 from this
session, plus #2976 which was pre-skipped from the v1.39.1
cherry-pick set after #2980 landed) remain in the new
`[Unreleased]` section — they ship in the next release.
Closes#2989
The `Dry-run publish validation` step ran `npm publish --dry-run` with
no `if:` guard. `npm publish --dry-run` contacts the registry and
exits 1 with "You cannot publish over the previously published
versions" when the target version exists.
The earlier `Detect prior publish (reconciliation mode)` step already
discovers this case and sets steps.prior_publish.outputs.skip_publish=true.
The actual publish step (further down) is gated on that. The
rehearsal step was missing the gate, so any re-run of an
already-published hotfix blew up at the rehearsal before reaching
the reconciliation logic — exactly when an operator is trying to
recover from a later-step failure (merge-back, summary, etc.).
Add `if: ${{ steps.prior_publish.outputs.skip_publish != 'true' }}`
matching the publish step's gate. The rehearsal still runs on first
publishes where it has value.
Trigger: run 25233855236.
Closes#2987
* fix(#2983): classifier exit-code discipline, base-tag staging, drop vestigial merge-back
Three issues surfaced by CodeRabbit's post-merge review of #2981 plus
a production failure on the v1.39.1 release run.
(1) Overloaded classifier exit code
scripts/diff-touches-shipped-paths.cjs reused exit 1 for both the
legitimate "no shipped paths" result and Node's default exit on
uncaught throw, so any classifier failure (corrupt package.json,
EPERM, etc.) was indistinguishable from a normal skip — the workflow's
`if ! ... ; then skip` idiom would silently drop the commit.
Distinct exit codes now:
0 shipped — at least one path is in the npm `files` whitelist
1 not shipped — CI / test / docs / planning only
2 classifier error — workflow MUST fail-fast
uncaughtException + unhandledRejection + try/catch around fs/JSON
parsing all route to exit 2 with stderr context.
(2) Classifier missing at the base tag (CRITICAL)
`Prepare hotfix branch` runs `git checkout -b "$BRANCH" "$BASE_TAG"`
BEFORE the cherry-pick loop, replacing the working tree with the base
tag's contents. Base tags predating #2980 (notably v1.39.0, the most
likely next hotfix base) don't have scripts/diff-touches-shipped-paths.cjs
at all — `node <missing>` exits non-zero — `if !` skips every commit —
empty hotfix branch published. Strictly worse than the original #2980
push-rejection, which at least failed loudly.
Stage the classifier from the dispatched ref's working tree into
$RUNNER_TEMP at the top of the run script (before any working-tree-
mutating git command). The cherry-pick loop now references $CLASSIFIER
(staged) instead of the in-tree path. Sanity guards: refuse to start
if scripts/diff-touches-shipped-paths.cjs is missing in the dispatched
ref, refuse to proceed if cp didn't materialize $CLASSIFIER.
The cherry-pick loop captures node's exit via ${PIPESTATUS[1]} and
dispatches via explicit case:
0 proceed with cherry-pick
1 skip into NON_SHIPPED_SKIPPED
* emit ::error:: + exit "$CLASSIFIER_RC"
(3) Drop the merge-back PR step
Auto-cherry-pick only picks commits already on main (`git cherry HEAD
origin/main` outputs the unmerged ones; we filter fix:/chore: from
main). By construction every code commit on the hotfix branch is
already on main. The only hotfix-branch-only commit is `chore: bump
version to X.Y.Z for hotfix`, which either no-ops against main or
rewinds main's in-progress version. The merge-back PR was vestigial.
It also failed in production on run 25232968975 with `GitHub Actions
is not permitted to create or approve pull requests (createPullRequest)`
— org policy blocks PR creation from the workflow's GH_TOKEN. Even
without that block, the PR would have nothing useful to merge.
Step removed. The `pull-requests: write` permission granted solely
for the merge-back step has been dropped from the release job
(least-privilege).
Regression coverage
tests/bug-2983-classifier-exit-codes-and-base-tag-staging.test.cjs
adds 12 assertions across two describe blocks:
- 5 classifier behavioral: exit 0/1 preserved, exit 2 on missing
package.json, exit 2 on malformed JSON, exit-code constants
exported.
- 7 workflow contract: classifier staged before checkout, target
is $RUNNER_TEMP, missing-source guard, missing-staged guard,
PIPESTATUS-based dispatch, error branch fails workflow, loop uses
staged path (not in-tree).
tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs updated
where it asserted the pre-#2983 `if ! ... ; then` shape: now accepts
the post-#2983 case-dispatch form. The test still proves the
classifier participates; bug-2983 enforces the specific shape.
Run summary references for the curious reviewer:
- Run 25232010071 — original #2980 trigger (workflow-file push
rejection)
- Run 25232968975 — failed merge-back step that prompted the
"is this even useful?" question that drove the removal
Closes#2983
* fix(#2983): address CodeRabbit findings on PR #2984
Two findings, both real, both fixed.
(1) [Critical] PIPESTATUS capture clobbered by `|| true`
Pre-fix shape:
git diff-tree ... | node "$CLASSIFIER" || true
CLASSIFIER_RC="${PIPESTATUS[1]}"
When the classifier exits 1 ("not shipped" — common case) or 2
(error), `|| true` triggers the right-hand side. `true` is a
one-command "pipeline" that overwrites PIPESTATUS to (0).
${PIPESTATUS[1]} on the next line is therefore unset (or stale
under set -u). The case dispatch then matched the empty string —
falling into `*)` and failing the workflow on every non-shipped
commit, OR matching `0)` after some shells default-init unset
to 0 and silently picking commits that don't ship.
Local repro confirms the issue:
$ bash -c 'set -euo pipefail; false | sh -c "exit 7" || true; \
echo "PIPESTATUS: ${PIPESTATUS[*]}"; \
echo "[1]: ${PIPESTATUS[1]:-<unset>}"'
PIPESTATUS: 0
[1]: <unset>
Fix: bracket the pipeline in `set +e`/`set -e`, snapshot
PIPESTATUS into a local array on the very next line, then
dispatch on the snapshot:
set +e
git diff-tree ... | node "$CLASSIFIER"
PIPE_RC=("${PIPESTATUS[@]}")
set -e
DIFFTREE_RC="${PIPE_RC[0]}"
CLASSIFIER_RC="${PIPE_RC[1]}"
The snapshot must happen on the first line after the pipeline;
any intervening simple command resets PIPESTATUS. The array form
is invariant against that.
Bonus from the new shape: $DIFFTREE_RC is now also captured.
git diff-tree is unlikely to fail on a known-good $SHA, but if
it does, we no longer feed partial/empty input to the classifier
and call it "not shipped." A non-zero DIFFTREE_RC emits
::error::git diff-tree failed and exits.
(2) [Minor] Stale "Merge-back PR opened against main" summary line
The hotfix run summary still printed:
echo "- Merge-back PR opened against main"
But the merge-back step itself was removed in the previous commit
on this branch. Operators reading the summary would expect a PR
that doesn't exist. Replaced with explicit non-action text:
echo "- No merge-back PR (auto-picked commits are already on main)"
Test coverage
bug-2983 test file gains 3 assertions:
- PIPE_RC array-snapshot pattern is required (regex matches the
exact `PIPE_RC=("${PIPESTATUS[@]}")` form).
- The `pipeline || true; ${PIPESTATUS[1]}` antipattern is
explicitly forbidden via assert.doesNotMatch.
- DIFFTREE_RC is captured from PIPE_RC[0] and a non-zero value
triggers ::error::git diff-tree failed.
- Run summary forbids `Merge-back PR opened against main` and
requires the new non-action sentence.
bug-2964 test's loop-anchor window bumped 6 KB → 8 KB to
accommodate the additional pre-pick scaffolding (the test's own
comment had already anticipated this kind of growth, citing prior
precedents from #2970 and #2980).
Mark CodeRabbit comments resolved post-commit.
Refs CR finding ids 3175253571, 3175253578 on PR #2984.
* fix(#2980): pre-skip workflow-file cherry-picks in release-sdk hotfix loop
The default GITHUB_TOKEN issued to the release-sdk run lacks the
`workflow` scope, so the prepare job's `git push origin "$BRANCH"` is
rejected by GitHub when any cherry-picked commit modifies a file under
`.github/workflows/`:
! [remote rejected] hotfix/X.YY.Z -> hotfix/X.YY.Z
(refusing to allow a GitHub App to create or update workflow ...
without `workflows` permission)
Pre-#2980 behavior: the auto_cherry_pick loop happily picked
workflow-file commits, then the trailing push exploded with no clear
signal which commit was the culprit. v1.39.1 hit this on PR #2977
(run 25232010071) — earlier release-sdk fixes (#2965, #2967, #2970)
had been skipped on conflict so their workflow-file changes never
reached the push step, masking the bug; #2977 was the first
workflow-file commit to apply cleanly and the push immediately
exploded.
Fix: pre-pick guard in the cherry-pick loop. Inspect each candidate
commit's file list via `git diff-tree --no-commit-id --name-only -r`
BEFORE attempting the pick. If any path matches `^\.github/workflows/`,
skip the commit, emit a `::warning::` annotation naming the dropped
commit, and append to a new `WORKFLOW_SKIPPED` bucket. The run summary
surfaces this bucket in its own section, distinct from `CONFLICT_SKIPPED`
(real merge conflicts) and `POLICY_SKIPPED` (feat/refactor exclusions),
so operators reviewing the run never confuse the remediation paths.
The loud-warning piece is non-negotiable: silent drops were explicitly
rejected as a failure mode during the option-1/2/3 tradeoff discussion.
If a workflow-file fix genuinely needs to ship in a hotfix, the
operator applies it manually on the hotfix branch using a token with
`workflow` scope, or lands it on main and re-cuts the release.
Regression covered by tests/bug-2980-skip-workflow-file-cherrypicks.test.cjs
(5 assertions: pre-pick guard exists, uses `git diff-tree`, emits
`::warning::`, lands in dedicated bucket, surfaces in summary).
The bug-2964 test's 4 KB window after the cherry-pick-loop anchor was
nudged to 6 KB to accommodate the new pre-pick scaffolding — the test's
own comment had already anticipated this kind of growth (citing #2970's
merge-commit pre-skip as prior precedent).
Closes#2980
* refactor(#2980): replace workflow-file pre-skip with shipped-paths filter
The previous commit on this branch caught only the .github/workflows/*
subset of the bug, treating the symptom (push rejection on workflow-file
changes) rather than the root cause (the fix:/chore: filter is too broad
— it picks any commit with that conventional-commit type even when the
diff cannot affect the published npm package).
CI-only fixes (release-sdk.yml itself, hotfix tooling, test-only
commits) shouldn't flow through hotfix runs at all — they cannot change
what `npm install get-shit-done-cc@X.YY.Z` produces. The
.github/workflows/* push rejection is just the loudest of these
"shouldn't have been picked" cases; tests/, docs/, .planning/ commits
get picked silently with the same lack of effect on consumers.
Replace the workflow-file pre-skip with a shipped-paths filter:
- New scripts/diff-touches-shipped-paths.cjs reads package.json `files`,
plus package.json itself (always-shipped per `npm pack` semantics),
and exits 0 iff any input path is in the shipped set. Lockfile is
not shipped (npm pack excludes it unless explicitly in `files`).
- Workflow loop now pipes `git diff-tree --no-commit-id --name-only -r`
through the classifier; on exit 1 the commit is skipped and
appended to a new NON_SHIPPED_SKIPPED bucket (replaces
WORKFLOW_SKIPPED).
- Run summary surfaces NON_SHIPPED_SKIPPED as informational — no
::warning:: annotation. A non-shipping commit cannot affect the
package, so a yellow alert would imply remediation is possible
and would mislead operators.
The classifier in a separate .cjs file (rather than inline bash
heredoc) is so its rules — directory-prefix vs exact-match,
package.json-always-shipped, lockfile-not-shipped — are unit-testable
in tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs (11 new
assertions: 4 static workflow + 6 classifier behavioral + 1 mixed-
diff edge case).
Why this dissolves the original push-rejection bug: workflow files
aren't in `files`, so workflow-only commits are skipped pre-pick.
The push step never sees them.
If a workflow-file fix genuinely needs to ship in a hotfix release
(extremely rare — the hotfix workflow is read from main's ref, not
the hotfix branch's), the operator applies it manually using a token
with `workflow` scope. The pre-skip puts that requirement in the run
summary explicitly.
Closes#2980
The release job's "Bump in-tree version (not committed)" step ran
`npm version "$VERSION" --no-git-tag-version` without --allow-same-version,
so on real hotfix runs it failed with `npm error Version not changed` —
because the prepare job had already committed the bump on the hotfix
branch (the release job checks out BRANCH on real runs vs BASE_TAG on
dry-runs, which is why dry-run never caught it).
Pass --allow-same-version to both bumps, matching release.yml:326.
Closes#2976