Commit Graph

162 Commits

Author SHA1 Message Date
Tom Boucher
b2f0fdf250 fix(sdk): anchor extractFrontmatter at file start (#3240) (#3247)
* test: reproduce extractFrontmatter LAST-block bug (#3240)

* fix(sdk): anchor extractFrontmatter at file start (#3240)

* changeset: pr=3247 for #3240
2026-05-08 08:40:30 -04:00
Tom Boucher
447763411a fix(sdk): phase.add honors --dry-run; rejects unknown flags (#3226) (#3246)
* test: reproduce phase.add dry-run + flag validation gaps (#3226)

Add failing tests for:
- --dry-run silently absorbed into description (symptom A)
- Unknown --flag should return validation error (symptom C)
- ### Phase N: ROADMAP heading scan verification (symptom B)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sdk): phase.add honors --dry-run; rejects unknown flags (#3226)

- Add flag parser to phaseAdd: strip recognized flags (--dry-run) from
  args before positional parsing so they never silently become description
  or customId values
- --dry-run computes the next phase number and roadmap_entry string but
  skips mkdir, writeFile, and readModifyWriteRoadmapMd; returns
  { dry_run: true, roadmap_entry } alongside normal fields
- Any unrecognized --flag throws a Validation GSDError naming the flag
- ROADMAP ### Phase N: heading scan for numbering (symptom B) was already
  correct; verified with new regression test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* changeset: pr=3246 for #3226

* fix(sdk): phase.add scans disk AND roadmap (union, not fallback)

Address CodeRabbit finding: the conditional `if (maxPhase === 0)` guard
around the filesystem scan meant that if ROADMAP had any phases but disk
was ahead (e.g. ROADMAP max=10, dirs include 12-*), phase.add would
pick 11 and collide with the existing directory.

Remove the guard: always scan on-disk phase directories and take the
max across both ROADMAP and filesystem (union semantics).

All 57 phase-lifecycle tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: reproduce phase.add concurrent ID collision (CR finding)

Two concurrent phase.add calls against the same project observe
maxPhase before the lock is held, producing duplicate phase IDs.
Adds a Promise.all regression test that asserts both calls succeed
with distinct phase numbers {11, 12}.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sdk): compute phase number under roadmap lock (CR finding)

Move maxPhase/newPhaseId/dirName computation inside the
readModifyWriteRoadmapMd callback so the entire read → compute → write
cycle is serialised under the lock. Previously, two concurrent
phase.add calls could both observe maxPhase=N before either acquired
the lock, then both write with phase ID N+1 — producing duplicate IDs.

In dry-run mode (no write, no race) the computation still happens
outside the lock to avoid unnecessary contention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 08:40:24 -04:00
Tom Boucher
8054959417 fix(query): workstream resolution in init.milestone-op and roadmap.analyze (#3196)
- 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>
2026-05-06 22:01:45 -04:00
Tom Boucher
2d32ad82be fix(plan-phase): remove agent: directive that caused OpenCode subagent dispatch (#3156) (#3206)
* 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>
2026-05-06 21:51:38 -04:00
Tom Boucher
4237b0d78e Merge pull request #3106 from nicholasferrer/fix/3061-commit-pathspec-leak
fix(commit): scope every commit call to its staged pathspec
2026-05-06 20:55:30 -04:00
Tom Boucher
d44fcee013 Merge pull request #3110 from patrickclery/fix/3100-search-dirs-colon-leaks
fix: replace stale /gsd: references in agents/, sdk/src/, and .clinerules
2026-05-06 20:52:43 -04:00
Tom Boucher
995e24431b Merge pull request #3193 from gsd-build/fix/2641-details-summary-milestone-anchor
fix(sdk): extractCurrentMilestone supports <details><summary> milestone headers (#2641)
2026-05-06 20:40:13 -04:00
Tom Boucher
ca148036d2 fix(roadmap): prevent milestone version substring matches 2026-05-06 20:37:18 -04:00
Ben Lamm
13bf56477a fix(#2641): symmetric attribute tolerance in stripShippedMilestones + lockdown tests
Address CodeRabbit follow-up review on PR #3046. One real bug + two lockdown
gaps + one defensive assertion.

REAL BUG — sibling-asymmetry in <details> attribute tolerance:
  extractCurrentMilestone's <details>-aware fallback uses <details\b[^>]*>
  to tolerate attributes (#2641 hardening commit). stripShippedMilestones
  still used literal <details>, so shipped content wrapped in
  `<details open>` (or any attributed tag) leaked through the strip.
  This is the failure mode trek-e's review almost caught with the
  "<details open>" / extended-attribute test gap I deferred — CodeRabbit
  caught the deeper issue: it's not just a test gap, it's an actual
  asymmetry between the two functions that handle <details> blocks.

  Fix: align stripShippedMilestones's regex with extractCurrentMilestone's
  <details\b[^>]*> form. Comment explicitly notes the symmetry contract so
  a future change to either function flags the other.

  Tests added in stripShippedMilestones describe block:
  - removes <details open> blocks
  - removes <details class="..." data-..."> blocks

LOCKDOWN — leading-# strip in synthesized heading:
  My existing inline-HTML test exercised tag-stripping but didn't directly
  exercise the leading-# strip path (`.replace(/^#+\s*/, '')`). Added a
  dedicated test with `<summary># v0.9 Hash-Prefixed</summary>` so a
  future refactor that drops the strip would fail loudly instead of
  producing `## # v0.9 …` (which downstream `#{2,4}` regex parses as a
  4-hash header).

DEFENSIVE — toBeDefined guard in roadmapAnalyze regression test:
  Added `expect(data.milestones).toBeDefined()` before casting and
  calling `.some()`. Failure now reports "expected undefined to be
  defined" instead of TypeError.

META: my prior adversarial pass missed the sibling-asymmetry because
the checklist's "sibling consistency" item only audited PARSERS for the
same INPUT field (STATE.md's `milestone:`), not ADJACENT FUNCTIONS that
process the same DATA SHAPE (<details> blocks). The latter is a wider
audit — every adjacent function that touches the data shape my new code
relies on. Will refine the learned rule.

Verification: 51/51 roadmap.test.ts pass (was 48; +3 tests).
FAMP smoke unchanged: roadmap.get-phase 3 returns active milestone phase.
2026-05-06 15:41:27 -04:00
Ben Lamm
19041b8824 test(#2641): lockdown tests from self-adversarial pass
Self-run adversarial pass on PR #3046 before next reviewer round-trip.
Three lockdown tests added — none uncovered new bugs, all lock current
behavior so a future change doesn't silently flip a convention.

1. Single-quote YAML version (`milestone: 'v0.9'`)
   Parity with the existing double-quote test. The strip pattern
   `/^["']|["']$/g` handles both — locked here so a future change to
   either character class doesn't silently regress one form.

2. Heading-anchor wins over <details> fallback (precedence lock)
   When a ROADMAP has BOTH `### v0.9` heading AND
   `<details><summary>v0.9</summary>` block, the heading-level lookup
   matches first and the fallback never fires. Test asserts the heading
   slice is returned starting at offset 0 AND the synthesized
   `## v0.9 ... details-anchored` heading is NOT prepended (proves
   fallback didn't run). Also documented in-test that the heading-anchor
   slice naturally includes downstream <details> blocks verbatim — a
   property of the heading path, not of this PR's fallback.

3. Multiple <details> blocks for same version → first match wins
   `content.match(detailsPattern)` (non-`g`) returns first match in
   document order. Locked so a future change to the matcher (e.g.
   switching to `matchAll` and picking last) doesn't silently change
   which block is treated as active.

Adversarial-checklist coverage on commit 781cc6f8:
- Boundary cases: empty / whitespace / single-char / single-quote /
  double-quote / digit-suffix (`v0.91`) / dot-suffix (`v0.9.0`) /
  hyphen-suffix (`v0.9-rc.1`, intentional same-milestone match per
  existing currentVersionStr convention) — all covered.
- Sibling consistency: parseMilestoneFromState, getMilestoneInfo,
  extractCurrentMilestone all strip quotes identically.
- Comment-vs-behavior: walked nested-guard, empty-guard, lookahead,
  tag-strip by hand against the regex; all comments accurate.
- Downstream consumers: roadmapAnalyze + roadmapGetPhase both verified
  end-to-end via tests + FAMP smoke.
- Failure-mode locality: all fall-through paths produce loud failures
  (empty arrays, `{found:false}`); no silent confident-wrong outputs.

48/48 roadmap.test.ts tests pass.
2026-05-06 15:41:27 -04:00
Ben Lamm
4b66ca5800 fix(#2641): harden <details> fallback per trek-e review
Address trek-e's adversarial review on PR #3046. Two critical merge-blockers
plus four hardening items, all now covered with tests.

CRITICAL #1 — substring-version trap:
  `[^<]*${escapedVersion}[^<]*` did substring containment, so
  `milestone: v0.1` matched <summary>v0.10 …</summary> and returned the
  v0.10 block's body as the active milestone — confidently-wrong content
  worse than the pre-PR fall-through. Add `(?![\d.])` non-version-character
  lookahead, mirroring the same boundary protection used by the existing
  `currentVersionStr` logic on the heading path. Test asserts v0.1 active
  with v0.10 sibling block returns v0.1's phases, not v0.10's.

CRITICAL #2 — nested <details> silent truncation:
  The lazy `[\s\S]*?</details>` terminates on the FIRST </details>, which
  is the inner closer when nesting is present. Prior comment claimed
  "would mis-anchor (acceptable; falls through)" — factually wrong: the
  match succeeds with truncated body and is returned with a confident
  `## ${summary}` heading. Future maintainer investigating a "missing
  phase" report would be misled. Add `!detailsMatch[2].includes('<details')`
  guard so nesting falls through to stripShippedMilestones (loud failure)
  instead of returning truncated content (silent failure). Test locks
  the contract: no synthesized v0.9 heading anchored to truncated body.

HARDENING:
  - Empty-body guard: `<details><summary>v0.9</summary></details>` would
    synthesize `## v0.9\n` (phantom milestone, zero phases, no error
    signal). Treat as no-match.
  - Inline-HTML in <summary>: rejected by `[^<]*` capture. Widen to
    `(?:(?!</summary>).)*?` (non-greedy until close tag) and strip tags
    + leading `#` from the captured summary before promoting to a `##`
    heading. Covers GitHub-rendered <em>(active)</em>, <code>v0.9</code>,
    <strong>...</strong> patterns.
  - JSDoc: rewrote to describe both anchoring strategies and the
    synthesized-heading contract; demoted stale "Port of core.cjs lines
    1102-1170" to historical context with the divergence list.
  - Comment block: rewrote in contract style ("any consumer scanning
    /##\s*.*vX.Y/ sees the active milestone") instead of coupling to
    specific call sites (roadmapAnalyze, "later in this file"). Adds
    explicit regex anatomy + hardening-guards section so future readers
    can audit each guard.

OUT OF SCOPE (per trek-e's "Recommended action" tier):
  - Debug logging on fall-through paths (Suggestion #10) — adds tracing
    surface to a function that doesn't currently use logger; appropriate
    for a follow-up if/when other extraction bugs surface.
  - Uppercase <DETAILS>/<SUMMARY> + extended attribute coverage
    (Test gap #7 last two rows) — already covered by the documented `i`
    flag and the existing <details open> test; adding redundant cases
    inflates the test set without locking new contracts.

Verification: 45/45 roadmap.test.ts tests pass (was 41/41; added 4
hardening tests). FAMP end-to-end smoke unchanged: roadmap.get-phase 3
returns "Claude Code integration polish", roadmap.analyze surfaces
v0.9 Local-First Bus in data.milestones with phase_count: 4.
2026-05-06 15:41:27 -04:00
Ben Lamm
c8239f67f8 fix(#2641): inject normalized ## heading from <details><summary> capture
Address CodeRabbit review on PR #3046: the prior commit returned only the
body inside <details>...</details>, which fixed the `roadmapGetPhase`
miss but left `roadmapAnalyze`'s downstream `data.milestones` scan
(`/##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi` at the bottom of roadmap.ts)
without an active-milestone anchor in the returned slice.

Now capture the <summary> text and prepend it as a synthesized `##`
heading on the returned slice. This makes both `data.phases` (the
original bug) AND `data.milestones` (the downstream consumer) surface
the active milestone correctly for <details>-wrapped ROADMAPs.

Also widened the inner tag to `<summary\b[^>]*>` for symmetry with the
outer `<details\b[^>]*>` — both now tolerate attributes.

Verified end-to-end against FAMP's v0.9 ROADMAP:
- Before this commit (after PR #3046 base):
    milestones: [{heading: '# Phase 1: ... (v0.5.2 atomic bump)', version: 'v0.5.2'}]
- After this commit:
    milestones: [{heading: 'v0.9 Local-First Bus', version: 'v0.9'},
                 {heading: '# Phase 1: ... (v0.5.2 ...)', version: 'v0.5.2'}]

(The v0.5.2 entry is pre-existing noise from the loose `##\s*` regex
matching the `### Phase 1: famp-bus (v0.5.2 atomic bump)` body heading;
unrelated to this fix and out of scope for this PR.)

Tests:
- Updated the two `<details><summary>` tests to assert the synthesized
  `## v0.9 Local-First Bus` heading is present on the returned slice.
- Added a 4th regression test (`roadmapAnalyze`) confirming
  `data.milestones` now contains the active milestone for
  <details>-wrapped ROADMAPs.
- All 40 roadmap.test.ts tests pass.
2026-05-06 15:41:27 -04:00
Ben Lamm
ba6a3efc3e fix(#2641): strip YAML quotes from STATE.md milestone version
Address CodeRabbit review on PR #3046: extractCurrentMilestone read the
`milestone:` value from STATE.md frontmatter via `.trim()` only, while
parseMilestoneFromState() and getMilestoneInfo() both also strip
surrounding YAML quotes via `.replace(/^["']|["']$/g, '')`.

For projects whose STATE.md uses quoted YAML (`milestone: "v0.9"`),
`version` carried literal quotes, `escapedVersion` became `\"v0\.9\"`,
and neither the markdown-heading regex nor the new <details><summary>
fallback could match anything — falling through to
stripShippedMilestones() and reintroducing the same archived-milestone
misrouting this PR addresses.

Strip quotes for parity. Three-line addition + one new test.
All 41 roadmap.test.ts tests pass.
2026-05-06 15:41:27 -04:00
Ben Lamm
592b676414 fix(#2641): recognize <details><summary> as active-milestone anchor
`extractCurrentMilestone` only matched markdown headings (## v0.9, ### v0.9)
to find the active milestone slice. Projects that wrap their active
milestone's phase details inside `<details><summary>vX.Y …</summary>`
(a common GitHub-friendly collapse pattern, e.g. FAMP) fell through to
`stripShippedMilestones`, which strips ALL `<details>` blocks indiscriminately.

Net effect: `roadmapGetPhase` returned `{found:false}` for phases that ARE in
the active ROADMAP. The `init.phase-op` safety guard at `init.ts:133`
('drop archived disk match when phase is in current ROADMAP') depends on
`roadmapPhase.found`, so it didn't fire. `init.phase-op` then returned a
`phase_dir` pointing at an ARCHIVED milestone's same-numbered phase —
silently routing downstream workflows (e.g. /gsd-discuss-phase) into
completed phases.

Fix: when no markdown heading matches the active version, try matching
`<details\b[^>]*><summary>...vX.Y...</summary>`. Returns the inner content
of the matching block. Purely additive — `stripShippedMilestones` behavior
and its tests are unchanged.

The `\b[^>]*>` form tolerates attributes like `<details open>` or
`<details class="...">` (GitHub commonly emits `<details open>` for
default-expanded sections). Lazy `[\s\S]*?` matches up to the first
`</details>`; nested `<details>` inside the active milestone are not
expected and would mis-anchor (acceptable; falls through to the existing
`stripShippedMilestones` path with no regression vs. today's behavior).

Closes #2641. Distinct from the closed #2642 which bundled three orthogonal
changes (parser fix + checkbox-scan fix + STATE.md counting auth) into one
PR; this PR addresses only the parser anchoring bug, leaving
`stripShippedMilestones`, `roadmapAnalyze`, and `initMilestoneOp` untouched.

Tests added (3, all in `roadmap.test.ts`):
- `bug-2641: finds active milestone wrapped in <details><summary>vX.Y …</summary>`
- `bug-2641: finds active milestone in <details open><summary>vX.Y …</summary>`
- `bug-2641: returns found:true for phase inside <details>-wrapped active milestone` (end-to-end via `roadmapGetPhase`)

All existing `roadmap.test.ts` tests pass (39/39). Real-world repro
verified against an FAMP-style ROADMAP: before the fix,
`gsd-sdk query roadmap.get-phase 3` returned `{found:false}` despite the
phase being at line 113 of the active ROADMAP; after the fix, it returns
the correct phase metadata, and `init.phase-op 3` no longer returns the
v0.8 archived `phase_dir`.
2026-05-06 15:41:27 -04:00
Tom Boucher
a1a81eec90 fix(config): align SDK runtime-state key validation with CJS 2026-05-06 15:19:34 -04:00
Tom Boucher
96ce608ee6 fix(config): add resolve_model_ids to VALID_CONFIG_KEYS; accept workflow._auto_chain_active via RUNTIME_STATE_KEYS
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>
2026-05-06 14:56:34 -04:00
Tom Boucher
54b06e653e docs(sdk): document runtime bridge seam, strict mode, and fallback policy 2026-05-05 19:29:59 -04:00
Tom Boucher
9811782e6d fix(#3121): implement commands verb in SDK native registry (#3146)
- 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
2026-05-05 15:02:34 -04:00
Nicholas Ferrer
85ef9553d2 fix(commit): scope every commit call to its staged pathspec
The commit handler ran `git add <paths>` followed by `git commit`
without a pathspec, so anything pre-staged externally before the
handler ran was swept into the commit. #2767 fixed every call site
to use --files but left the handler emitting a pathspec-less commit,
so the bug survived the well-formed form too.

Compute pathsToCommit once and pass `'--', ...pathsToCommit` to every
git commit invocation: regular, --amend, and commit-to-subrepo. The
staged-files check uses the same pathspec so "nothing staged" reflects
what would actually be committed, not unrelated index entries.

Two follow-up safeguards on the same surface:

* When `--files` is passed but every following token gets filtered out
  (e.g. `--files --no-verify`), reject with `--files requires at least
  one path` instead of silently falling back to .planning/.
* Both `git add` invocations now use the `--` separator so a path
  starting with `-` (e.g. a file literally named `-A.md`) is treated
  as a pathspec rather than a git option.

Adds five regression tests in `commit.test.ts`: three covering the
pathspec scope (`--files`, `.planning/` fallback, and `--amend` with
pre-staged unrelated changes), one covering the empty `--files`
rejection, and one covering the `-A.md` round-trip.

Closes #3061
2026-05-05 15:30:05 -03:00
Patrick Clery
f9c1f01971 fix: extend fix-slash-commands SEARCH_DIRS to agents/, sdk/src/, .clinerules
scripts/fix-slash-commands.cjs SEARCH_DIRS did not cover agents/, sdk/src/,
or top-level files, so 9 colon-form references survived in 6 files. The hit
at agents/gsd-codebase-mapper.md:105 propagated into ~/.claude/agents/ at
install time (the fixer is not wired into install) and produced unrunnable
/gsd:<cmd> suggestions in agent output on non-Gemini runtimes.

This commit includes Pass 1 (the 9 line edits) AND Pass 2 (extending the
fixer's SEARCH_DIRS so future regressions are auto-rewritten and caught by
the bug-2543 guard, which mirrors that list). The standalone bug-3100 test
added in the prior revision is removed in favor of the bug-2543 guard's
extended scan, per CONTRIBUTING.md test standards (no source-grep tests on
non-.md files).

Refs #3100
2026-05-05 13:19:10 -04:00
Tom Boucher
aa64638176 Merge pull request #3112 from gsd-build/fix/3101-plan-summary-matcher-in-core-cjs-reports
fix: canonicalize plan-summary matching for suffixless summaries
2026-05-04 23:35:34 -04:00
Tom Boucher
e7ecd46bbe Merge pull request #3115 from gsd-build/fix/3053-sdk-ignores-multi-plan-phase-layout-plan
fix: count nested plans/ layout in phase status indexing
2026-05-04 23:35:26 -04:00
Tom Boucher
083e813aea Merge pull request #3116 from gsd-build/fix/3055-bug-top-level-branching-strategy-in-plan
fix: normalize legacy top-level branching_strategy into git config
2026-05-04 23:33:28 -04:00
Tom Boucher
f01f6b76dd Merge pull request #3122 from gsd-build/fix/3088-gsd-complete-milestone-leaves-state-md-n
fix: normalize stale STATE narrative tails on milestone completion
2026-05-04 23:31:06 -04:00
Tom Boucher
67684626d8 fix(3088): append missing STATE narrative sections on milestone close 2026-05-04 23:29:45 -04:00
coderabbitai[bot]
2d25c97706 fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-05-05 03:17:22 +00:00
Tom Boucher
2dcf374da0 fix(milestone): normalize STATE narrative after milestone completion 2026-05-04 23:17:00 -04:00
Tom Boucher
58062a64a0 fix(sdk-config): honor legacy top-level branching_strategy in init 2026-05-04 23:06:54 -04:00
Tom Boucher
65024683fd fix(init): count plans/ summaries from nested plans/ layout 2026-05-04 23:03:10 -04:00
Tom Boucher
c7886415c3 fix(phase): canonicalize plan-summary matching for suffixless summaries 2026-05-04 22:51:15 -04:00
Tom Boucher
0500bdf619 refactor: deepen query architecture seams with compatibility shims 2026-05-04 21:49:40 -04:00
Tom Boucher
c66ff96de8 test: use typed GSDToolsError constructors in cli output tests 2026-05-04 21:49:39 -04:00
Tom Boucher
9bee4dce4a test: adopt typed GSDToolsError constructors across failure tests 2026-05-04 21:49:39 -04:00
Tom Boucher
abf7779088 test: cover typed timeout mapping in query dispatch 2026-05-04 21:49:39 -04:00
Tom Boucher
ccda572ade refactor: default typed failure classification across query errors 2026-05-04 21:49:38 -04:00
Tom Boucher
7298a76b20 refactor: centralize dispatch error projection from failure signals 2026-05-04 21:49:38 -04:00
Tom Boucher
5cfd874058 refactor: add typed query failure signals 2026-05-04 21:49:38 -04:00
Tom Boucher
ba6100c548 refactor: deepen query failure classification module 2026-05-04 21:49:38 -04:00
Tom Boucher
42ed7cee8d refactor: deepen GSDTools query execution seams (#3085)
* refactor: deepen gsdtools query execution seams

* docs: add changeset for query seam deepening

* docs: fix changeset summary text

* fix: address coderabbit query seam findings

* test: address remaining coderabbit findings and notes

* refactor: use internal gsdtools error type import
2026-05-03 18:56:41 -04:00
Tom Boucher
5e21bf7567 Deepen query dispatch seam with Command Topology Module (#3078)
* Deepen query dispatch seam with command topology module

* Stabilize SDK parity defaults and integration test gating

* docs(architecture): record pre-project config policy and e2e gate

* refactor(query): stop injecting native adapter in CLI dispatch path

* fix(config): align workflow auto-chain typing and docs
2026-05-03 18:11:38 -04:00
Tom Boucher
9c92c32f6e refactor(query): deepen runtime context/native adapter/output seams (#3076)
* refactor(query): deepen runtime context, native adapter, and cli output seams

* chore(changeset): add fragment for query seam deepening continuation

* refactor(query): converge internal command-resolution imports on canonical seam

* refactor(query): remove dead seam wrappers and converge on canonical modules

* docs(architecture): update context and adr for query seam completion

* fix(query): preserve gsd-tools stderr in cli output and clarify static ws test scope

* test(query): cover whitespace stderr and null exitCode fallback
2026-05-03 16:31:48 -04:00
Tom Boucher
5c9f34bd31 refactor(cli): extract Query CLI Adapter Module seam (#3074)
* refactor(cli): extract query adapter seam from cli entrypoint

* test: update ws forwarding guard for query-cli-adapter seam

* fix(query): close remaining CodeRabbit findings on cli adapter

* test: address remaining CodeRabbit nitpicks on ws forwarding coverage
2026-05-03 15:57:01 -04:00
Tom Boucher
b6c401dc90 refactor(query): deepen command/dispatch seams and resolve coderabbit findings
* refactor(query): deepen command definition seam and fold fallback mapping cleanup

* refactor(query): add shared dispatch formatting module seam

* fix(query): restore QueryResult type import in dispatch deps

* test/query: align raw-output policy and definition normalization contracts

* refactor(query): deepen diagnosis, invariant report, and error taxonomy seams

* refactor(query): deepen dispatch plan, fallback bridge, policy snapshot, and hints seams

* refactor(query): deepen validation, fallback policy, capability, and result builder seams

* refactor(query): deepen resolution strategy, output classifier, observability, and policy-capability seams

* refactor(query): finalize deep strategy/classifier/observability/capability seams

* test/query: address coderabbit inline and out-of-diff dispatch nits

* fix(query): address remaining coderabbit input-validation and bridge stderr threads

* fix(query): address remaining coderabbit dispatch and strategy/output nits
2026-05-03 15:29:34 -04:00
Tom Boucher
f104dab332 refactor(query): deepen dispatch policy seam with structured result contract (#3066)
* refactor(query): deepen dispatch policy seam with structured result contract

Closes #3065.

- unify query dispatch outcome as typed success/failure union
- include error kind/details + final exit_code in failure path
- align native and fallback paths under one dispatch policy seam
- make CLI query path consume seam result (thin adapter)
- add ADR + context term for Dispatch Policy Module

* refactor(query): strengthen dispatch seam with shared error mapper and typed details

- add query-dispatch-error-mapper module shared by native/fallback paths
- remove ad-hoc inline mapping in dispatch/fallback executors
- lock error-details schema in mapper + dispatch tests
- document structured dispatch contract in QUERY-HANDLERS.md

* fix(query): return structured fallback failure when path resolution throws

- guard resolveGsdToolsPath in cjs dispatch path
- map thrown resolution errors to fallback_failure result
- add regression test for structured failure contract
2026-05-03 14:30:27 -04:00
Tom Boucher
5975f06b6a refactor(query): extract command catalog seam for registry wiring (#3060)
* refactor(sdk): extract gsdtools transport seam with per-command policy

* refactor(query): centralize registry command catalog wiring

* refactor(query): unify command resolution seam across sdk callers

* fix(sdk): address CodeRabbit transport policy and timeout findings

* refactor(query): extract mutation event mapper seam

* refactor(query): converge mutation and transport policy data

* refactor(query): share fallback orchestration across cli and sdk

* refactor(query): split static registry catalog by domain clusters

* refactor(query): extract mutation event emission decorator seam

* refactor(query): extract alias-family handler catalog module

* refactor(query): extract cjs fallback execution adapter

* refactor(query): deepen command semantics seam

* refactor(query): extract deep dispatch seam

* refactor(query): deepen cjs fallback execution seam

* refactor(query): merge routing plan into dispatch seam

* fix(query): address CodeRabbit review findings on PR #3060

Critical: prevent double-execution race by checking timeout errors
before subprocess fallback (gsd-transport.ts).

Major: fix execRaw() to respect transport policy outputMode instead
of hardcoding 'raw' (gsd-tools.ts).

Major: add explicit 30s timeout to subprocess fallback execution
(query-fallback-executor.ts).

Major: remove raw args from stderr banner to prevent secret leakage
(query-fallback-executor.ts).

Minor: ensure native text output has trailing newline for CLI parity
(query-dispatch.ts).

Update gsd-tools.test.ts to match new execRaw() behavior.

* fix(tests): update CLI integration tests for catalog-based registration

The refactoring moved handler registration from inline registry.register()
calls to catalog-based registration (registerStaticCatalog/registerAliasCatalog).

- gsd-sdk-query-registry-integration.test.cjs: collectRegisteredNames() now
  also scans catalog files for handler names registered via the new system.
- bug-2492-context-coverage-gate.test.cjs: checks for catalog-based
  registration (DECISION_ROUTING_STATIC_CATALOG) instead of inline strings.
- bug-2524-sdk-query-ws-flag.test.cjs: checks for dispatchNative callback
  pattern instead of direct registry.dispatch() call.

* fix(query): address remaining CodeRabbit review findings

- query-command-semantics.ts: guard stats/progress rewrite so option
  tokens (e.g. --pick) are not turned into subcommands, preserving the
  top-level handler dispatch.

- query-dispatch.ts: formatOutput now skips --pick for text-format
  responses (matching CJS fallback behavior) and surfaces a proper error
  when extractField returns undefined instead of silently producing
  'undefined'.

- query-dispatch.ts: fix backwards error message — 'registered' is the
  restrictive policy that disables fallback, not enables it.

- tests/bug-2492-context-coverage-gate.test.cjs: check
  VERIFY_DECISION_STATIC_CATALOG (the correct catalog for plan-gate
  handlers) instead of DECISION_ROUTING_STATIC_CATALOG.

- tests/gsd-sdk-query-registry-integration.test.cjs: resolve catalog
  variable before loading entries so the drift guard checks each
  referenced catalog individually.

* refactor(query): deepen registry assembly module with strict invariants

- extract registry assembly into dedicated module
- split build vs mutation decoration internals
- add strict assembly invariants:
  1) no duplicate keys
  2) alias canonicals must have handlers
  3) mutation commands must be registered
  4) raw-output policy commands must be registered
- slim query index to thin re-export seam
- add focused registry assembly tests
- update drift-guard tests to target new seam

* test(query): add thin-seam coverage for query index re-exports

* fix(query): return structured native dispatch errors + tighten decisions.parse guard

- runQueryDispatch native path now catches adapter errors and returns
  QueryDispatchResult.error instead of throwing.
- preserve legacy CLI exit contract by using code=1 for native dispatch
  failures.
- strengthen bug-2492 guard: decisions.parse assertion now checks
  VERIFY_DECISION_STATIC_CATALOG OR explicit command token.
2026-05-03 13:57:32 -04:00
Tom Boucher
e1d661ece0 feat(#3024): dynamic routing with failure-tier escalation (#3031)
* feat(#3024): dynamic routing with failure-tier escalation

Adds a `dynamic_routing` block to .planning/config.json that lets
the resolver start agents on a cheap tier and escalate one tier up
when the orchestrator detects a soft failure (verification
inconclusive, plan-check FLAG, etc.). Solves the "pay Opus rates as
insurance" anti-pattern by making escalation observed-quality-driven.

Architecture:
- AGENT_DEFAULT_TIERS map (light/standard/heavy) — every agent in
  MODEL_PROFILES declares a default tier; tests assert coverage
  so adding a new agent without updating the map fails CI.
- nextTier(currentTier) helper — light → standard → heavy → heavy
  (heavy stays at heavy; can't go further).
- resolveModelForTier(cwd, agentType, attempt) — new resolver. The
  orchestrator tracks the attempt counter and passes 0 for the
  first spawn, 1+ on escalation. The resolver caps internally at
  max_escalations so the orchestrator can blindly bump the counter.
- Schema validation: dynamic_routing.enabled / escalate_on_failure /
  max_escalations / tier_models.<light|standard|heavy>. Unknown
  tiers and unknown sub-keys rejected at config-set time.
- SDK schema mirror updated to keep CJS/SDK in lockstep (#2653).

Resolution precedence (highest → lowest):
  1. model_overrides[<agent>]              (full IDs accepted)
  2. dynamic_routing.tier_models[<tier>]   (NEW; escalation-aware)
  3. models[<phase_type>]                  (#3023 phase-type map)
  4. model_profile                         (per-agent column)
  5. Runtime default

Backward compatibility: dynamic_routing is disabled by default
(enabled: false or block omitted). resolveModelForTier short-
circuits to resolveModelInternal in that case, so callers can
adopt unconditionally without breaking existing behavior.

This PR delivers the JS-layer infrastructure: schema + tier map +
resolver. Orchestrator adoption (workflow markdown updates that
detect soft failures and call resolveModelForTier with attempt+1)
is incremental follow-up — verifier / plan-checker / integration-
checker each adopt the protocol when ready.

Tests (23 cases, all structural-IR — no stdout grep):
- Schema invariants: AGENT_DEFAULT_TIERS coverage, VALID_AGENT_TIERS
  exact match, every assignment uses a valid tier
- nextTier helper: light→standard→heavy→heavy, null on invalid input
- Disabled mode: no block + enabled:false both no-op (back-compat)
- Enabled mode: attempt=0 returns default tier model, attempt=1
  escalates, beyond max_escalations caps, heavy agents stay heavy,
  default max_escalations=1 when omitted
- Precedence: per-agent override beats dynamic_routing,
  dynamic_routing beats phase-type models
- Validation: every settings key accepted, unknown tiers/sub-keys
  rejected, bare `dynamic_routing` rejected as config-set target

Documentation:
- get-shit-done/references/model-profiles.md — full reference section
- docs/CONFIGURATION.md — full settings table + escalation flow
- docs/USER-GUIDE.md — task-oriented "Cheap-by-default" section
- docs/FEATURES.md — config row cross-link

Verification:
- 23/23 pass on regression test
- 6843/6843 full suite (23 net new from 6820)
- lint-no-source-grep clean (376 test files)
- SDK schema mirror keeps CJS/SDK in sync per #2653 parity test

Closes #3024

* fix(#3024): honor escalate_on_failure:false + 3 CR follow-ups

CodeRabbit on PR #3031 (4 findings — 1 Major + 2 Minor + 1 Nitpick):

1. **Major (inline)** — get-shit-done/bin/lib/core.cjs:1668
   resolveModelForTier ignored dynamic_routing.escalate_on_failure.
   When the user set it to false, escalation should be disabled, but
   the resolver only checked attempt/max_escalations. An orchestrator
   that always passes attempt+1 on retry would silently escalate
   despite the user opting out.
   Fix: gate effectiveAttempt on `dr.escalate_on_failure !== false`
   so false short-circuits every attempt back to the default tier.

2. **Minor (inline)** — docs/CONFIGURATION.md:123-126
   The dynamic_routing rows in the Core Settings table had 4 cells
   instead of 5 (missing the Options column), breaking the table
   structure. Added explicit Options values for enabled / escalate_on_failure
   / max_escalations rows.

3. **Minor (outside-diff)** — references/model-profiles.md:179-195
   "Resolution Logic" sketch was pre-#3024 and didn't include
   dynamic_routing in the precedence ladder. Updated to a 6-step
   block with dynamic_routing at step 3 (between override and
   phase-type).

4. **Nitpick** — tests/feat-3024-dynamic-routing.test.cjs:189+
   Tests used `if (lightAgent) { ... }` guards that silent-pass
   when AGENT_DEFAULT_TIERS drifts. Replaced all 5 conditional
   skips with `assert.ok(lightAgent, '...')` preconditions so a
   tier-mapping change surfaces as a test failure.

Plus: 2 new regression tests for the Major fix:
- escalate_on_failure:false caps every attempt at default tier
- escalate_on_failure:true (explicit) still escalates normally

Verification:
- 25/25 pass on regression test (23 prior + 2 escalate_on_failure)
- 6845/6845 full suite (2 net new)
- lint-no-source-grep clean

* docs(#3024): align precedence + add fence language tags (CR follow-up)

CodeRabbit (3 minor):

1. docs/CONFIGURATION.md:691 — "Per-Phase-Type Models → Resolution
   precedence" was a 4-step block written pre-#3024; readers got
   contradictory rules between the per-phase-type section and the
   later dynamic_routing section. Updated to the same 5-step ladder
   with dynamic_routing at step 2, and noted that dynamic_routing
   is disabled by default so this section's behavior is unchanged
   when the kill-switch is off.

2. docs/CONFIGURATION.md:770 — escalation-flow code fence missing
   language tag (MD040). Added `text`.

3. references/model-profiles.md:184 — resolution-ladder code fence
   missing language tag (MD040). Added `text`.

No code changes; docs only. Verification: regression test still 25/25.

* docs(#3024): clarify precedence prose — five layers, not four (CR nitpick)

CodeRabbit nitpick: the "Per-Phase-Type Models → Resolution
precedence" prose said "The four layers compose..." but the ladder
above lists five (including Runtime default). Also "dynamic_routing
escalates per-attempt above all of them" misreads as suggesting
dynamic_routing wins over model_overrides — actually overrides still
win at step 1.

Reworded top-down so the precedence direction is unambiguous:
  - model_profile = base
  - models = phase-level override
  - dynamic_routing = per-attempt escalation
  - model_overrides = per-agent exception (top)
  - runtime default = fallback

No code changes; docs only.

* docs(#3024): note escalate_on_failure:false in escalation-flow diagram (CR)

CodeRabbit nitpick: the escalation-flow diagram in
docs/CONFIGURATION.md described the soft-failure → respawn →
tier_models[next_tier_up] path, but didn't surface the
`dynamic_routing.escalate_on_failure: false` kill-switch right next
to it. Users reading the flow diagram (which is the canonical place
to understand attempt behavior) wouldn't see that the kill-switch
overrides the soft-failure branch.

Added a one-paragraph note immediately after the flow listing,
before the tier-sequence example, so the kill-switch is visible
exactly where users decide whether escalation will happen.

No code changes; docs only.
2026-05-02 14:26:35 -04:00
Tom Boucher
d812c66020 feat(#3023): per-phase-type model map in .planning/config.json (#3030)
* feat(#3023): per-phase-type model map in .planning/config.json

Adds a new `models` block to .planning/config.json with six phase-type
slots (planning / discuss / research / execution / verification /
completion). Lets users express coarse tuning ("Opus for planning,
Sonnet for the rest") without learning the agent taxonomy.

Resolution precedence (highest → lowest):
  1. Per-agent `model_overrides[agent]`     (full IDs; targeted exception)
  2. Phase-type `models[phase_type]`         (NEW; tier alias)
  3. Profile table (`model_profile`)          (per-agent column)
  4. Runtime default

The three layers compose: `models` defaults a phase, `model_overrides`
carves an exception. Phase-type values are tier aliases (opus/sonnet/
haiku/inherit) so the runtime-resolution chain (#2517) stays correct
end-to-end without further branching.

Implementation:
- model-profiles.cjs: new AGENT_TO_PHASE_TYPE map + VALID_PHASE_TYPES
  set. Each agent in MODEL_PROFILES gets one phase-type assignment;
  tests assert coverage so adding a new agent without updating the
  table fails CI.
- core.cjs (resolveModelInternal): inserts phase-type tier lookup
  between per-agent override and profile-derived tier. Skips runtime
  resolution when the resolved tier is 'inherit' (was previously gated
  only on profile === 'inherit'; phase-type can now produce inherit
  independently).
- core.cjs (loadConfig): pass `parsed.models` through both code paths
  so resolveModelInternal can read it.
- config-schema.cjs + sdk/src/query/config-schema.ts: dynamic-pattern
  validator accepts only the six known phase-types; unknown slots
  rejected at config-set time.

Backward compat: configs without `models` behave exactly as today.

Tests (15 cases, all structural-IR — no stdout grep):
- Schema: AGENT_TO_PHASE_TYPE coverage, VALID_PHASE_TYPES exact match
- Resolver: phase-type alone; per-agent override beats phase-type;
  phase-type beats profile; issue's full example; "inherit"; empty
  block is no-op; no block is no-op
- Validation: each of the 6 slots accepted; unknown slot rejected;
  bare `models` (no slot) rejected

Verification:
- 15/15 pass on new regression test
- 6808/6808 full suite (5 net new), 0 fail
- lint-no-source-grep clean across 375 test files

Closes #3023

* docs(#3023): document `models` per-phase-type config in user-facing docs

Adds `models` block coverage to the three user-facing docs that ship
with each release:

1. docs/CONFIGURATION.md
   - New "Per-Phase-Type Models" section between "Per-Agent Overrides"
     and "Non-Claude Runtimes" with:
       * full example mixing models + model_overrides
       * phase-type → agent mapping table
       * resolution-precedence pseudocode
       * accepted values (tier alias only)
       * "When to use which" decision matrix
       * validation behavior + example error
   - Added `"models": {}` to the Full Schema snippet
   - Added a row for `models.<phase_type>` to the config keys table
     (next to model_profile_overrides for adjacency)

2. docs/FEATURES.md
   - Added a row for models.<phase_type> in the Configurable Settings
     table (right under model_profile)
   - Cross-link to CONFIGURATION.md for the full surface

3. docs/USER-GUIDE.md
   - New task-oriented "Tuning model cost by phase" section above
     "Using Non-Claude Runtimes" — leads with the concrete config
     and shows the override pattern (one-shot phase + targeted exception)
   - Cross-link to CONFIGURATION.md

Verification:
- 29/29 pass on config-schema-docs-parity + docs-update + new feature
  test (parity-check passes, so the config-schema entry I added in the
  feature commit is now matched by a docs row)
- 6808/6808 full suite pass
- lint-no-source-grep clean

Doc style follows the same pattern used by the existing model_profile,
model_overrides, and model_profile_overrides sections — example-led,
table-backed, cross-referenced. Each doc surfaces the feature at the
right depth (reference / settings table / task guide).

* fix(#3023): mirror phase-type tier in resolveReasoningEffortInternal (CR Major)

CodeRabbit caught a real Codex correctness bug + 3 minor docs/test issues:

1. **Major (outside-diff)** — resolveReasoningEffortInternal in core.cjs
   derived its tier exclusively from the profile table, ignoring the
   models.<phase_type> override added in #3023. Failure mode on Codex:

     Config: model_profile=balanced, models.execution=opus, agent=gsd-executor
     resolveModelInternal:           tier=opus    → gpt-5.4
     resolveReasoningEffortInternal: tier=sonnet  → reasoning_effort=medium
                                                     ↑
                                                  WRONG — should be xhigh
                                                  (opus tier on Codex)

   The runtime received a mismatched (model, effort) pair. Mirrored the
   phase-type lookup from resolveModelInternal so both functions derive
   from the same tier source. 'inherit' phase-type returns null effort
   (no runtime entry maps to 'inherit'; let runtime decide).

2. Minor — .changeset/per-phase-type-models.md `pr: TBD` → `pr: 3030`.

3. Minor (outside-diff) — model-profiles.md "Resolution Logic" section
   omitted the new phase-type tier. Updated the 4-step block to a 5-step
   block including `models[phase_type]` between override and profile,
   plus a paragraph noting that `model` and `reasoning_effort` derive
   from the same tier source.

4. Nitpick — added 2 typo-safety tests:
   - models.research = "haiku3" (typo) → falls through to profile
   - models.research = "openai/gpt-5" (full ID) → falls through to profile
   Plus 5 new reasoning_effort tests covering the Major fix:
   - exported correctly
   - phase-type override flips both model AND effort to same tier
   - inherit phase-type returns null effort
   - per-agent override still bypasses phase-type for effort
   - claude runtime ignores models.* (no effort propagation)

Verification:
- 24/24 pass on regression test (15 original + 2 typo-safety + 5 effort + 2 outside-diff related)
- 6815/6815 full suite (7 net new from 6808)
- lint-no-source-grep clean

The reasoning_effort tests are written semantically (phase-type override
must produce the SAME effort as a profile-only opus config) rather than
hard-coding tier-specific effort strings, so changes to the runtime tier
map don't break them.

* fix(#3023): phase-type override beats profile=inherit (CR Major round 2)

CodeRabbit caught another precedence inversion: when
  { model_profile: 'inherit', models: { execution: 'opus' } }
both resolvers short-circuited on `profile === 'inherit'` BEFORE the
phase-type override could be honored. Result: model returned 'inherit'
and reasoning_effort returned null — both contradicting the documented
precedence where models[phase_type] wins over model_profile.

Fix in resolveModelInternal:
- Compute tier from phase-type FIRST. If phase-type is a valid alias,
  it wins. Otherwise, fall back to profile-derived tier OR 'inherit'
  (when profile === 'inherit').
- Gate the runtime-resolution branch on `tier !== 'inherit'` (was
  `profile !== 'inherit'`) so phase-type=opus can flip runtime mapping
  on even when profile=inherit.
- Gate the inherit-return on `tier === 'inherit'` (was
  `profile === 'inherit'`).

Fix in resolveReasoningEffortInternal:
- Remove the `if (profile === 'inherit') return null;` early-return.
- Compute tier from phase-type first, fall back to profile. If
  phase-type is explicitly 'inherit' OR the resolved tier is 'inherit',
  return null (no runtime entry maps to inherit).

Tests added (5 new):
- model: phase-type wins over profile=inherit (with explicit opus, with
  haiku for one phase + planner-without-slot still inheriting)
- model: profile=inherit + no models block → all agents inherit (no
  regression on existing inherit semantics)
- model: profile=inherit + models block but agent has no slot → that
  agent inherits, agents with slots get phase-type tier
- effort: phase-type opus + profile=inherit → produces opus-tier
  effort, NOT null (the original bug)

Verification:
- 27/27 pass on regression test (24 prior + 3 model + 1 effort)
- 6820/6820 full suite (5 net new)
- lint-no-source-grep clean

The effort test reads the expected value by running a profile-only opus
config and comparing — semantic check, not hard-coded effort string. So
runtime tier map changes don't break the test.
2026-05-02 13:19:15 -04:00
Tom Boucher
ffeeb92c14 fix(#2997): mask SECRET_CONFIG_KEYS in SDK config-set/get and init responses (#2999)
* fix(#2997): mask SECRET_CONFIG_KEYS in SDK config-set/get and init responses

The CJS→TS port at sdk/src/query/config-mutation.ts:240,243 and
config-query.ts:122,128,132 dropped the masking layer that secrets.cjs
spec defines for brave_search/firecrawl/exa_search. Result: the SDK
echoed plaintext API keys into machine-readable JSON output (stdout,
transcripts, CI logs).

Adjacent leak in init.ts:673-675 / init.cjs:728-730: the init bundle
passed config.brave_search through raw, leaking the API key whenever
the user had stored one.

Fix:
- New sdk/src/query/secrets.ts ports SECRET_CONFIG_KEYS, isSecretKey,
  maskSecret, maskIfSecret. Exact CJS parity (verified by 17 tests
  in secrets.test.ts that import secrets.cjs and compare).
- config-set masks value + previousValue in response; on-disk plaintext
  intact (key stays usable).
- config-get masks read response. --default flows through unmasked
  (user's own input, not stored secret).
- init.ts/init.cjs mask string values only; booleans (availability
  flags) pass through unchanged so the typed contract is preserved.

Tests: 17 in secrets.test.ts (including CJS parity), 5 in
config-mutation.test.ts (#2997 block — covers on-disk-preserved,
previousValue masking, short-value, unset, non-secret pass-through),
4 in config-query.test.ts.

Closes #2997

* chore(#2997): add changeset fragment for PR #2999

* chore(#2997): add changeset fragment for PR #2999

* chore(#2999): drop direct CHANGELOG.md edit; release entry now lives in .changeset/

The changeset-fragment workflow (#2975) renders fragments into
CHANGELOG.md at release time. Direct edits to [Unreleased] on
each PR caused merge conflicts on every concurrent PR. This commit
restores CHANGELOG.md to match origin/main; the release entry for
this fix is preserved in the .changeset/*.md fragment(s) on this
branch, which the release workflow consolidates.
2026-05-02 00:17:45 -04:00
javeroff
9d0d085a17 fix(query/agent-skills): emit raw <agent_skills> block instead of JSON-wrapped string (#2917)
* fix(query/agent-skills): emit raw <agent_skills> block instead of JSON-wrapped string

The CLI dispatcher (`cli.ts`) JSON-stringifies all query handler results via
`console.log(JSON.stringify(result.data, null, 2))`.  For the `agent-skills`
handler this produced a JSON-quoted string literal — e.g.
`"<agent_skills>\n…</agent_skills>"` — which workflows embedded verbatim via
`$(gsd-sdk query agent-skills gsd-planner)`, breaking all `<agent_skills>`
injection into spawned subagent prompts.

Fix: add an optional `format: 'json' | 'text'` field to `QueryResult`.  When a
handler returns `format: 'text'` and `--pick` is not active, the CLI writes the
string directly via `process.stdout.write` instead of JSON-stringifying it.
`agentSkills` sets `format: 'text'` for non-empty blocks.

Regression guard: two new CLI integration tests in `skills.test.ts` spawn the
CLI as a child process and assert that (a) a mapped agent type receives the raw
XML block on stdout and (b) an unmapped agent type produces the existing JSON
empty-string output.

Fixes #2914.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(changelog): add #2917 entry under Unreleased Fixed

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 12:21:06 -04:00
Tom Boucher
8de8acee46 fix(workflows): assert HEAD on per-agent branch before worktree commits (#2924) (#2941)
* fix(workflows): assert HEAD on per-agent branch before worktree commits

Worktree-mode setup could leave HEAD attached to a protected branch (master),
causing agent commits to land there. The previous response was a destructive
self-recovery via 'git update-ref refs/heads/master <sha>', which silently
rewinds the protected branch and destroys concurrent commits in multi-active
scenarios (parallel agents, user committing while agent runs).

- Reorder <worktree_branch_check> in execute-phase.md and quick.md to assert
  HEAD via 'git symbolic-ref' BEFORE any 'git reset --hard'. HALT with a
  blocker if HEAD is on main/master/develop/trunk/release/* or detached.
- Add a per-commit HEAD assertion (step 0) to gsd-executor.md
  <task_commit_protocol>; HEAD attachment can drift after 'git checkout <sha>'.
- Forbid 'git update-ref refs/heads/<protected>' in
  <destructive_git_prohibition>; surface the blocker rather than self-heal.
- Remove '--no-verify' as the worktree-mode default in execute-phase.md,
  execute-plan.md, quick.md, and references/git-integration.md. Hooks now
  run on every executor commit; opt out only via workflow.worktree_skip_hooks.
- Add regression test that parses the worktree_branch_check blocks structurally
  and asserts the symbolic-ref check precedes the reset --hard, no workflow
  performs update-ref on a protected ref, and --no-verify is no longer the
  default in any parallel-execution prompt.

* fix(#2924): address CodeRabbit review findings on worktree HEAD PR

- Add positive worktree-agent-* allow-list to <task_commit_protocol> step 0
  in gsd-executor.md and to <worktree_branch_check> in execute-phase.md and
  quick.md. The deny-list (main|master|develop|trunk|release/*) silently
  allowed feature/* and other arbitrary branches outside the agent namespace.
- Register workflow.worktree_skip_hooks in both config schemas
  (sdk/src/query/config-schema.ts and get-shit-done/bin/lib/config-schema.cjs)
  and document it in docs/CONFIGURATION.md so config-set accepts it.
- Fix stash lifecycle in execute-phase.md post-wave hook validation: stash
  under a named ref and pop after the hook run; warn on pop failure.
- Pre-dispatch PLAN.md commit in quick.md: gate on git diff --cached --quiet
  for idempotency and exit 1 with a clear error on commit failure (both the
  --no-verify and the normal branches) — no more swallowing real errors.
- Test fixes (tests/bug-2924-worktree-head-attachment.test.cjs):
  - Parse the protected-branch alternation structurally and require
    main, master, develop, trunk, release/.* (release/* was previously
    skipped by the \\b...\\b regex).
  - Use fs.readdirSync(dir, { recursive: true }) so workflows in nested
    subdirectories are also asserted against the update-ref ban.
  - Add allow-list assertions for execute-phase.md, quick.md, and
    gsd-executor.md to lock in the new positive namespace check.

* test(#2924): assert sub-section end marker exists before slicing

* test(#2924): use section boundary instead of fixed window for parallel-agents slice
2026-05-01 09:23:02 -04:00