Commit Graph

231 Commits

Author SHA1 Message Date
Tom Boucher
2de2d185fa feat(3536): Configuration Module via shared manifests + generator (Phase 2 of #3524)
Phase 2 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the structural drift surface that produced bug class

After this phase, neither bin/lib/ nor sdk/src/ defines
CONFIG_DEFAULTS, VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, or the
four legacy-key normalizations inline. All come from one canonical
source: the Configuration Module (sdk/src/configuration/index.ts)
+ two JSON manifests (sdk/shared/config-{defaults,schema}.manifest.json).
The CJS mirror is generator-emitted (get-shit-done/bin/lib/configuration.generated.cjs)
with a CI freshness check (sdk/scripts/check-configuration-fresh.mjs).

- sdk/shared/config-defaults.manifest.json — canonical nested defaults,
  union of CJS + SDK keys (includes security_*, post_planning_gaps,
  agent_skills, mode, every git/workflow/hooks sub-section).
- sdk/shared/config-schema.manifest.json — VALID_CONFIG_KEYS array,
  RUNTIME_STATE_KEYS array, DYNAMIC_KEY_PATTERNS array with source
  strings (regex reconstructed at runtime).
- sdk/src/configuration/index.ts — source of truth. Exports
  loadConfig (pure read), normalizeLegacyKeys (pure, idempotent,
  returns Normalization[]), mergeDefaults (deep-merge), migrateOnDisk
  (explicit opt-in disk writeback), plus CONFIG_DEFAULTS,
  VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS.
- sdk/src/configuration/index.test.ts — 29 vitest pinning tests.
- sdk/scripts/gen-configuration.mjs — generator (Function.prototype.toString()
  inspection of compiled SDK dist, plus brace-balanced text scan for
  internal helpers, matching the Phase 1 pattern).
- sdk/scripts/check-configuration-fresh.mjs — CI freshness gate.
- tests/configuration-generator.test.cjs — 27 parity assertions
  (CJS-generated == SDK source).
- tests/configuration-migrate-config.test.cjs — 3 cases for the new
  gsd-tools migrate-config subcommand.

- bin/lib/core.cjs: CONFIG_DEFAULTS literal now sources values from
  CANONICAL_CONFIG_DEFAULTS (the manifest), with a thin flat
  projection at the load boundary to preserve the existing
  flat-shape return contract for the ~21 CJS test files and 100+
  consumers. All four legacy-key migration blocks (branching_strategy,
  sub_repos, multiRepo, depth — historically lines 351-358, 388-397,
  401-408, 416-423) collapse to a single normalizeLegacyKeys call
  in each code path. The inline platformWriteSync writeback stays
  for now to preserve sync loadConfig semantics; the new async
  migrateOnDisk is reachable via gsd-tools migrate-config.
- bin/lib/config-schema.cjs: 135 → 31 lines. Re-exports from the
  generated Module.
- bin/lib/config.cjs: adds cmdMigrateConfig handler (calls
  migrateOnDisk on the explicit user-driven path).
- bin/gsd-tools.cjs: wires migrate-config into command dispatch.

- sdk/src/config.ts: re-exports CONFIG_DEFAULTS and mergeDefaults
  from the Module. loadConfig now calls normalizeLegacyKeys before
  mergeDefaults (replaces the inline branching_strategy graft).
- sdk/src/query/config-schema.ts: 160 → 36 lines. Re-exports from
  the Module.

- tests/config-schema-sdk-parity.test.cjs: refactored from
  "CJS Set equals SDK Set" (trivially true post-migration) to
  "both sides source from the manifest" — structural plus runtime
  invariant.
- Four other tests that text-grepped source files for valid keys
  (plan-review-convergence, bug-3212, bug-2492, feat-3210) are
  updated to use runtime VALID_CONFIG_KEYS.has() or manifest JSON
  lookups.

- CONTEXT.md: new Configuration Module entry with full Interface
  contract.
- Root package.json: check:configuration-fresh proxy script.
- sdk/package.json: gen:configuration + check:configuration-fresh.
- .githooks/pre-commit: configuration drift block.
- .github/workflows/test.yml: configuration drift step after the
  alias drift check.

- 9201 CJS tests pass (baseline pre-cycle: 9195; +6 net new tests
  across migrate-config + parity refactor)
- 1872 SDK vitest tests pass
- 29 Configuration Module vitest fixtures
- 27 CJS/SDK parity fixtures
- Net diff: +388 / −519 = 131-line reduction across the seven cycles,
  despite adding the new Module, manifests, generator, freshness
  check, and two new test files.

1. SDK CONFIG_DEFAULTS now includes manifest-canonical keys
   (resolve_model_ids: false, context_window: 200000, phase_naming,
   claude_md_path, git.create_tag, workflow.security_*,
   workflow.code_review_*, planning.*, hooks.workflow_guard, ship.*).
   Consumers accessing via [key: string]: unknown index get
   the manifest default instead of undefined.
2. SDK mergeDefaults is now proper recursive deep-merge instead of
   spread-per-section. Overlay { workflow: { research: false } }
   now preserves sibling workflow keys; previously it replaced
   the entire workflow section with only research + the section's
   defaults. Semantically identical for the common case;
   strictly better for partial nested overrides.
3. New gsd-tools migrate-config CLI subcommand for the explicit,
   opt-in on-disk migration path.

Closes #3536.
2026-05-15 00:02:56 -04:00
Tom Boucher
bfd7ddbad3 feat(3530): STATE.md Document Module via generator (Phase 1 of #3524) (#3531)
* feat(3530): STATE.md Document Module via generator (Phase 1 of #3524)

Phase 1 of the CJS↔SDK hard-seam migration (parent #3524).
Converts the hand-synced state-document.cjs/state-document.ts pair
into a generator-driven seam, modeled on the existing
command-aliases.generated.* precedent.

What landed:
- sdk/src/query/state-document.ts is the source of truth.
- sdk/scripts/gen-state-document.ts emits
  get-shit-done/bin/lib/state-document.generated.cjs from the
  compiled SDK dist via Function.prototype.toString() inspection
  for the 7 public exports and 3 internal helpers.
- sdk/scripts/check-state-document-fresh.mjs is the CI freshness
  gate; pre-commit hook also runs it when relevant files change.
- get-shit-done/bin/lib/state-document.cjs is reduced to a one-line
  re-export from state-document.generated.cjs so existing callers
  (state.cjs, workstream-inventory.cjs, init.cjs) need no changes.
- New CI step in .github/workflows/test.yml after the existing alias
  drift check.
- sdk/package.json: gen:state-document, check:state-document-fresh
  scripts. tsx added as devDep.
- Root package.json: proxy script for the freshness check.
- CONTEXT.md: one-sentence amendment on STATE.md Document Module
  recording the source-of-truth file path.

Tests:
- sdk/src/query/state-document.test.ts: 34 vitest fixtures across
  the 7 public exports (TDD pinning safety net).
- tests/state-document-generator.test.cjs: 31 node:test parity
  assertions comparing SDK source vs generated CJS for every
  fixture.
- Full suite: 9177/9177 pass (baseline was 9146; +31 new tests).

One subtle behavior change worth flagging: the old hand-written
state-document.cjs used String(str) coercion inside escapeRegex,
which the SDK source does not. The generator faithfully matches
the SDK (the source of truth per ADR-3524), so the new CJS no
longer coerces non-string input to string before regex-escaping.
No current caller passes non-string input, so no observable
regression in the test suite. Flagged in the PR body for
reviewers.

Closes #3530.

* fix(3530): address state-document review findings
2026-05-14 22:17:20 -04:00
Tom Boucher
d4d4178603 Merge pull request #3483 from radioflyer28/feat/agent-launch-reasoning-transport-3474
feat: transport resolved reasoning effort to agent launches
2026-05-14 20:01:32 -04:00
Tom Boucher
585e417f9e fix: harden respect-staged pathspec handling 2026-05-14 19:44:04 -04:00
Tom Boucher
017abd9236 feat(sdk): add --respect-staged opt-in to gsd-sdk query commit --files (#3522)
Closes #3522. When --respect-staged is passed the git add loop is skipped
entirely so per-hunk staging from git add -p is preserved. Default behavior
(full re-stage) is unchanged. The #3061 pathspec invariant holds under both
modes. Nothing-staged within scope returns { committed: false, reason:
'nothing staged' } without error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:08:08 -04:00
Tom Boucher
200c012f84 fix(sdk): derive completed_phases from ROADMAP and refresh all STATE.md fields after phase.complete
Fixes two root causes behind bug #3517:

1. Idempotency: completed_phases was blindly incremented (parseInt + 1),
   causing phase.complete N run twice to double-count (4 → 5 → 6).
   Now derives from ROADMAP progress table Complete-row count, making
   the operation idempotent.

2. Field coverage: eight STATE.md fields were left stale after phase
   completion. Now updates in the same atomic lock section:
   - frontmatter: stopped_at, last_updated, total_plans, completed_plans
   - body: Current focus, Status line, By Phase table row

   completed_plans = count of *-SUMMARY.md files across all phase dirs
   total_plans = sum of M/N plan counts from ROADMAP progress table
   percent = recomputed from fresh derived counts

Closes #3517

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:56:18 -04:00
Tom Boucher
e0adba7e08 feat(statusline): add opt-in context_position config for narrow terminals (#2937)
Extract composeStatusline() helper from duplicated inline template logic in
runStatusline() and renderStatusline(). Both call sites now route through the
helper, which accepts a position param ('end' | 'front', default 'end').

- 'end' (default) preserves byte-identical output to v1.38.x and earlier
- 'front' renders ctx immediately after model name, before the first │
- Invalid values silently coerce to 'end' at runtime (belt-and-suspenders;
  config-set rejects invalid values upfront via enum validator)

Adds statusline.context_position to VALID_CONFIG_KEYS in both CJS and TS
schemas, enum validator in config.cjs, docs row in CONFIGURATION.md,
and a changeset. Closes #2937.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:45:41 -04:00
Tom Boucher
da21edfb59 feat(workflow): add git.create_tag config to disable milestone tagging
Adds boolean config key `git.create_tag` (default: true, fully backcompat)
so projects with their own release flow can disable GSD's automatic
`git tag -a v[X.Y]` on milestone completion. Also adds tag-collision
pre-check to prevent silent failure on re-run. Closes #3086

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 12:06:36 -04:00
Tom Boucher
62c64a757c Merge pull request #3500 from gsd-build/fix/3493-extract-milestone-generic-phase-details
fix(sdk): extractCurrentMilestone preserves generic Phase Details heading (#3493)
2026-05-14 10:08:44 -04:00
Tom Boucher
bb02b16698 Merge pull request #3501 from gsd-build/fix/3488-decimal-phase-short-form-depends-on
fix(sdk): expand short-form depends_on for decimal-phase plans (#3488)
2026-05-14 10:08:40 -04:00
Tom Boucher
5a8495adb7 fix(init): preserve inside=true on root lookup failures 2026-05-14 09:56:36 -04:00
Tom Boucher
fb6633ceda fix(workflow): detect nested git worktree in new-project bootstrap (#3491)
The `has_git` boolean returned by `init new-project` and `init ingest-docs`
was derived from a shallow `pathExists(cwd, '.git')` check, so a subdirectory
of an existing repo reported `has_git: false`. The workflow then ran
`git init`, creating a nested `.git` inside the outer worktree and silently
diverting subsequent `gsd-sdk commit` calls into the nested repo.

Replace the shallow check with `git rev-parse --is-inside-work-tree`
semantics in both CJS (`get-shit-done/bin/lib/init.cjs`) and TS
(`sdk/src/query/init.ts`, `sdk/src/query/init-complex.ts`) handlers via a new
shared `gitWorktreeInfoInternal` helper, and expose `git_worktree_root` +
`in_nested_subdir` so the workflows can refuse `git init` inside an existing
worktree and warn that planning files will track to the outer repo.

Regression test: `tests/bug-3491-nested-git-worktree.test.cjs`.

Fixes #3491

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:11:59 -04:00
Tom Boucher
809c2bee39 fix(sdk): expand short-form depends_on for decimal-phase plans (#3488)
The DAG resolver in phase-plan-index only matched full-stem
('03-01-auth-hardening') and canonical-prefix ('03-01') forms when
resolving `depends_on` references, so plans in decimal phases
(e.g. `99.9-test`, `02.2-cross-repo`) declaring short-form
`depends_on: [01]` had their edges silently dropped. Dependents
collapsed into wave 1 and the SDK emitted a misleading
"declared wave: N but depends_on DAG places it in wave 1" warning
that pointed at the wave declaration rather than the broken reference.

Add a tertiary short-form index keyed on the trailing `-NN` of each
plan's canonical ID — derived per plan via `lastIndexOf('-')` so it
handles integer, letter-suffixed, and decimal phase IDs uniformly.
Emit a dedicated `Plan X: unresolved depends_on reference 'NN' — no
matching plan in phase` warning whenever a dep fails all three lookup
forms, so a dropped edge can no longer hide behind the wave-mismatch
warning.

Regression coverage added in `sdk/src/query/phase.test.ts` for the
decimal-phase short-form case, the integer-phase short-form case, and
the unresolved-reference warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:28:11 -04:00
Tom Boucher
15e9c86ead fix(sdk): extractCurrentMilestone preserves generic Phase Details heading (#3493)
When a roadmap places shared phase-detail bodies under a non-version-prefixed
`## Phase Details` heading AFTER a `### 📋 vX.Y+ (Planned)` sibling in document
order, the entire Phase Details section fell outside the slice returned by
`extractCurrentMilestone`. `gsd-sdk query phase.insert N` then reported
"Phase N not found in ROADMAP.md" even though `### Phase N:` was unambiguously
present.

PR #2455 (closing #2422) added a same-version `continue` branch that already
handles the version-prefixed variant (e.g. `## v2.0 Phase Details`). This fix
extends the same intent to the generic-label variant: after the initial
boundary scan, look for a literal `^#{1,3}\s+Phase\s+Details\b` heading past
`sectionEnd` and, if found, append the Phase Details block (up to the next
real milestone boundary — version-bearing or milestone-emoji-bearing heading —
or EOF) to the returned slice. The intervening planned-milestone content is
skipped so it does not leak into the active-milestone view.

Bounded to a single append so a malformed roadmap can't loop. Only matches the
literal `Phase Details` label (canonical per GSD ROADMAP template); anything
else continues to terminate the slice. Does not regress the v2.0/v2.1+
version-prefixed handling shipped by #2455 (existing `bug-2422` test still
passes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:52:51 -04:00
Tom Boucher
49e7c48007 feat(execute-phase): classify quota/rate-limit failures across runtimes (#3095) (#3490)
* feat(execute-phase): classify quota/rate-limit failures across runtimes (#3095)

Dispatched executor subagents that die from provider quota or rate-limit
errors currently look identical to a crashed agent to the orchestrator —
so step 7's recovery prompt offers "retry now" when the right action is
"wait for reset and resume". This adds a runtime-agnostic classifier and
wires execute-phase step 7 to it.

- `agent.classify-failure` SDK query returns
  `{class: 'quota-exceeded' | 'classify-handoff-bug' | 'unknown-failure',
    sentinel?, retryAfterSeconds?}`. Sentinels cover Claude Code
  (`usage limit`, `429`), Copilot CLI (`rate_limit`,
  `user_weekly_rate_limited`), Codex (`usage_limit_reached`,
  `too many requests`), and Gemini (`RESOURCE_EXHAUSTED`,
  `exceeded your`).
- `execute-phase.md` step 7 now branches on the class. Quota-exceeded
  presents a wait-for-reset prompt and points at the safe-resume gate
  landing in #3212 instead of re-dispatching a fresh executor.
- `docs/research/provider-rate-limit-signals.md` records the proactive
  (header / SDK event) signals each provider exposes and the upstream
  Claude Code / Copilot / Codex issues blocking hook-side detection —
  the forward path once host runtimes surface them.

Resume-from-partial-worktree and context-load metrics from the original
report are deliberately out of scope; they overlap #3212's
`state.verify-against-disk` work already in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(execute): render quota retry hint and refresh alias artifacts

* fix(workflow): restore slash namespace and execute-phase size budget

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:46:56 -04:00
radioflyer28
810778e137 fix(sdk): gate reasoning effort by runtime allowlist 2026-05-13 22:27:15 -04:00
Tom Boucher
75d5ca5875 feat(code-review): integrate fallow structural pre-pass for /gsd-code-review (#3424)
* feat(code-review): add optional fallow structural pre-pass

* fix(ci): sync lockfile for fallow optional binaries

* fix(test): make fallow integration tests cross-platform

* fix(review): require executable fallow binary paths

* docs(review): clarify structural findings usage and size guard

* fix(fallow): preserve line:0, prefer node_modules/.bin, sync SDK twin (H1, M2, N1 from #3424 review)

* fix(workflow): harden fallow pre-pass — exit check, timeout, atomic write, size-guard order (B1, H2-H4, M1, M3 from #3424 review)

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

* fix(deps): pin fallow floor to ^2.70.0 matching lockfile (H7 from #3424 review)

* fix(config): enum-validate fallow.scope/profile + group code_quality.* contiguously (H5, N3 from #3424 review)

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

* docs(fallow): label mcp gate reserved, version-pin install, expand context schema (B3, H8, M4, M8, L1 from #3424 review)

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

* test(fallow): replace source-grep with behavioral tests, expand fixtures, fail-loud tmpdir (B4, H6, L2, L3, M5, M6, N2 from #3424 review)

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

* fix(workflow): escape closing structural_findings tag in JSON payload (CR #3424 inline finding)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:19:47 -04:00
radioflyer28
c20f714d68 docs: document reasoning effort transport 2026-05-13 19:43:42 -04:00
radioflyer28
f70829d067 feat: transport resolved reasoning effort 2026-05-13 19:43:28 -04:00
Tom Boucher
6ea25ec8c7 fix(sdk): skip terminal-labeled phases in init.progress next_phase (#3478)
* fix(sdk): treat terminal roadmap labels as complete in init.progress (#3472)

* chore(changeset): add #3478 fragment

* test(sdk): assert promoted/registered/inserted labels stay non-terminal
2026-05-13 19:34:53 -04:00
Tom Boucher
409295b450 fix(sdk): preserve same-milestone archived phase handling (#3480)
* fix(sdk): preserve same-milestone archived phase resolution (#3469)

* chore(changeset): add #3480 fragment

* fix(sdk): guard roadmapPhase null narrowing in initPhaseOp

* fix(sdk): use explicit roadmapPhase narrowing in archived guard
2026-05-13 19:25:33 -04:00
Tom Boucher
10101f07be fix(sdk): reduce validate.health false positives (#3479)
* fix(sdk): reduce validate.health false positives (#3473)

* chore(changeset): add #3479 fragment
2026-05-13 19:25:14 -04:00
Tom Boucher
f4c4240fea fix(sdk): state.prune current phase fallback handling (#3477)
* fix(sdk): derive state.prune phase from frontmatter fallbacks (#3471)

* chore(changeset): add #3477 fragment

* chore(sdk): clarify state.prune fallback guidance
2026-05-13 19:24:07 -04:00
Tom Boucher
245d5f66a1 feat: add review.default_reviewers config for /gsd-review defaults (#3464)
* feat(review): add review.default_reviewers selection policy

* docs(review): explain default reviewer config and precedence

* chore(changeset): add feature entry for review.default_reviewers

* chore(changeset): set pr field for #3464

* test(review): cover unavailable default-reviewer failure path

* fix(review): sync sdk and inventory parity for default reviewers

* fix(review): use canonical /gsd:review namespace in source
2026-05-13 14:10:15 -04:00
Tom Boucher
c5d4cf35e6 fix: state mutations recurse when Current Position has dollar amounts (#3463)
* fix(state): prevent  backreference expansion in state mutations

* chore(changeset): set pr field for #3463

* test(state): use structured STATE parser in bug-3454 regression
2026-05-13 13:54:30 -04:00
Tom Boucher
a4f94c87aa fix: align planner plan contract with phase index (#3436)
* test: add planner/query contract regressions (#3430)

* fix: align planner plan contract with phase index (#3430)

* docs: add changeset for #3430

* fix: keep planner contract docs within size budget (#3430)

* docs: set changeset pr for #3430
2026-05-12 18:50:35 -04:00
Tom Boucher
b5676b59c0 fix: accept flag-first syntax for phase remove (#3416)
* fix(phase): parse remove --force regardless of position

* chore(changeset): add fragment for phase-remove flag fix

* fix(phase): fail when phase.remove target is missing
2026-05-11 21:13:07 -04:00
Tom Boucher
5c35eceb41 feat(sdk): deepen package seam for legacy query compatibility (#3419)
* feat(sdk): deepen compatibility seam for legacy profile path

* chore(changeset): set PR number for #3419

* test(sdk): make query seam wiring assertion behavioral
2026-05-11 21:12:29 -04:00
Tom Boucher
e79c472d7b Feat(ship): add configurable PR body sections (#3391)
* feat: add configurable ship PR body sections

* chore: add changeset for ship PR sections

* docs: avoid prompt scanner trigger in PR body guide

* docs: escape pr body source separator
2026-05-11 15:27:40 -04:00
Tom Boucher
fd20373cf4 Fix(workflow): expose new-project agent diagnostics (#3390)
* fix: expose new-project agent diagnostics

* chore: add changeset for new-project agent diagnostics

* docs: tag new-project warning fence
2026-05-11 15:27:35 -04:00
Tom Boucher
543a8569e4 Fix(workflow): normalize SDK init phase flags (#3389)
* fix: normalize sdk init phase flags

* chore: add changeset for sdk init phase flags

* test: cover sdk phase equals flag variants
2026-05-11 15:27:32 -04:00
Tom Boucher
574d1f448c fix: honor workstream in verify-work init (#3386)
* fix: honor workstream in verify-work init

* fix: define verify-work phase arg
2026-05-10 20:25:42 -04:00
Tom Boucher
570dddd1cd fix: make worktree cleanup fail closed (#3385)
* fix: make worktree cleanup fail closed

* chore: add changeset for worktree cleanup safety

* fix: address worktree cleanup review findings

* docs: label remove-workspace failure block

* fix: initialize worktree manifest before dispatch
2026-05-10 19:48:28 -04:00
Tom Boucher
52337a8861 fix(sdk): honor Codex model overrides in init progress 2026-05-10 17:40:11 -04:00
Jeremy McSpadden
6ddbb97951 fix roadmap progress padded phase matching 2026-05-10 14:43:53 -05:00
Tom Boucher
0afcea0723 fix: block verifier pass on unresolved debt markers (#3343)
* fix: block verifier pass on unresolved debt markers

* chore: add changeset for verifier debt gate

* test: align verifier debt cleanup with standards

* fix: address coderabbit verifier debt findings

* fix: address follow-up coderabbit guard findings

* fix: tighten debt marker matching

* fix: ignore deleted files in debt scan

* docs: document debt scan path contract

* fix: harden debt scan path handling

* fix: tighten debt marker reference parsing

* fix: clarify debt scan failure logging

* fix: preserve verifier debt error contract
2026-05-10 11:19:50 -04:00
Tom Boucher
25fb81d01e feat(3309): workflow.human_verify_mode = end-of-phase (new default; mid-flight opt-back-in) (#3325)
* test(3309): red — workflow.human_verify_mode contract

New behavioral test file covers:
- workflow.human_verify_mode is a recognized config key (VALID_CONFIG_KEYS)
- defaults to 'mid-flight' (preserves current behavior)
- config-set / config-get round-trips for both values
- persists in config.json as string
- planner agent file references the flag with canonical wording, couples
  end-of-phase mode with the rule that checkpoint:human-verify is not
  emitted, and documents the <verify><human-check> deferred-item shape
- verifier agent file references harvesting <verify><human-check> blocks
- references/checkpoints.md documents the cost-control alternative

Source-text assertions on agent .md files are exempted via
allow-test-rule: source-text-is-the-product — those files ARE the
runtime contract loaded by AI runtimes, so asserting their wording is
the only way to verify the agents will respect the flag.

Fails 10/11 against current source. Will pass after the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(3309): add workflow.human_verify_mode = end-of-phase opt-out

Each mid-flight checkpoint:human-verify halt costs a full executor
cold-start (CLAUDE.md, MEMORY.md, STATE.md, plan re-read on every
respawn) because subagent context is discarded across the pause. A plan
with N human-verify checkpoints pays the cold-start cost N+1 times. The
reporter (rentanything-nb) measured this at "tens of thousands of tokens"
per round-trip and "hundreds of thousands per week."

This adds workflow.human_verify_mode (default 'mid-flight') with an
'end-of-phase' value that:
- instructs gsd-planner to NOT emit <task type="checkpoint:human-verify">
  tasks; verification details go into a <verify><human-check> sub-block
  on the relevant auto task instead
- instructs gsd-verifier (Step 8) to harvest those <verify><human-check>
  blocks at end-of-phase and merge them into its own human-verification
  list
- the existing human_needed → HUMAN-UAT.md flow in execute-phase.md is
  the single sink — no new file/writer is created

checkpoint:decision and checkpoint:human-action are unaffected — those
gate the work itself, not post-hoc verification.

Surfaces touched:
- bin/lib/config-schema.cjs, bin/lib/config.cjs — register key + default
- sdk/src/config.ts, sdk/src/query/config-schema.ts — SDK parity
- agents/gsd-planner.md — slim Detection section + reference link
- agents/gsd-verifier.md — Step 8 harvest instruction
- get-shit-done/references/planner-human-verify-mode.md — full rules,
  loaded conditionally to keep planner.md under its size budget
- get-shit-done/references/checkpoints.md — surface the alternative
- docs/CONFIGURATION.md — config table row
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json — track new reference

Tag name <human-check> chosen instead of <human> to avoid the
prompt-injection scan pattern that flags <system|assistant|human> tags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(3309): align changeset pr: to actual PR number

The pr: field was authored as 3319 (a guess at the next number) before
the PR was opened. Actual PR is #3325.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(3309): flip workflow.human_verify_mode default to end-of-phase

Per maintainer direction on PR #3325, end-of-phase is the new project
default. Mid-flight checkpoint:human-verify halts cost a full executor
cold-start (CLAUDE.md, MEMORY.md, STATE.md, plan re-read on respawn) per
round-trip — reported at "tens of thousands of tokens" per round-trip,
"hundreds of thousands per week" on real projects. The cost-control
mode is what new projects should get out of the box.

mid-flight remains a one-line opt-back-in via:

    gsd config-set workflow.human_verify_mode mid-flight

Behavior change for existing projects: the new default takes effect
when .planning/config.json is rewritten (config-set, fresh project).
Existing in-flight PLAN.md files with checkpoint:human-verify tasks
continue to work in either mode — the flag only changes what the
planner emits next time it runs.

Surfaces updated:
- bin/lib/config.cjs, sdk/src/config.ts — default flipped
- sdk/src/config.ts docstring — describes new default + opt-back-in
- agents/gsd-planner.md — Detection section explains new default
- references/planner-human-verify-mode.md — reordered modes; added
  guidance on when to opt back into mid-flight
- references/checkpoints.md — surface the default flip and the why
- docs/CONFIGURATION.md — table row reflects new default + reason
- tests/feat-3309-human-verify-mode.test.cjs — default test asserts
  end-of-phase
- .changeset/fierce-geese-march.md — describes the default flip and
  the migration semantics

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address human verify mode review

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:29:11 -04:00
Tom Boucher
e7942c21b3 fix: add executor stall recovery contract (#3329)
* fix: add executor stall recovery contract

* chore: add changeset for executor recovery

* chore: keep execute phase within size budget
2026-05-09 23:28:52 -04:00
Tom Boucher
dc003610e2 fix(3323): keep human-needed verification pending (#3339)
* fix: keep human-needed verification pending

* chore: add changeset for human verification gating

* docs: align ship verification status wording
2026-05-09 23:28:25 -04:00
Tom Boucher
d49e8872b5 fix(3317): SDK detect-custom-files now scans skills/ (parity with CJS port) (#3318)
* test(3317): red — SDK detect-custom-files must scan skills/

Mirrors tests/bug-2942-detect-custom-skills.test.cjs on the SDK side.
The SDK's GSD_MANAGED_DIRS array omits 'skills', so user-added skills
under <config-dir>/skills/<name>/ are never returned and get destroyed
on /gsd-update. New vitest covers:

- detects custom skill at skills/<name>/SKILL.md
- does not flag manifest-tracked skill as custom
- still detects custom files under get-shit-done/workflows/ (regression)
- custom_count matches custom_files.length across multiple skills

Fails 2/4 against current SDK source. Will pass after fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(3317): SDK detect-custom-files now scans skills/ (parity with CJS)

The SDK port of detect-custom-files declared GSD_MANAGED_DIRS without
'skills', while the canonical bin/gsd-tools.cjs port (which the SDK
docstring explicitly cites as its source) had the entry. Because
update.md prefers gsd-sdk over the CJS shim, real-world users with the
SDK installed never had their custom skills detected — the installer's
'Installed N skills to skills/' step then wiped any non-manifest skill
without backing it up to gsd-user-files-backup/. Real-world incident:
skills/gsd-roadmap/SKILL.md (fully user-owned) destroyed during the
1.40.0 → 1.41.0 update, recoverable only via Time Machine.

One-line fix adds 'skills' to the SDK's GSD_MANAGED_DIRS, matching the
CJS source the port was supposed to mirror. The 4 vitest cases added in
the prior commit now all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: correct changeset pr number

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:28:10 -04:00
Tom Boucher
26dcdb1ad0 Refactor SDK-first architecture seams (#3316)
* refactor: tighten sdk-first architecture seams

Refs #3312

* refactor: finish state document seam cleanup

Refs #3312

* test: harden minimal install cleanup assertion

* ci: support sdk-scoped package lock

* fix(3316): restore root package-lock.json and align changeset pr ref

Reverts dec57a83 ("ci: support sdk-scoped package lock") and restores
the root package-lock.json that c249d34d deleted. The deletion was the
wrong direction:

- The root package.json declares its own runtime and dev deps
  (@anthropic-ai/claude-agent-sdk, ws, c8). Without a root lockfile,
  `npm install --no-package-lock` resolves whatever satisfies semver at
  install time — CI today and CI in six months can install different
  transitive trees, defeating reproducibility.
- The lockfile has been part of every release on this repo (long
  history on main); removing it loses the npm audit / Dependabot
  target without compensating benefit.
- The CI workaround pattern (cache-dependency-path: sdk/package-lock.json
  + `npm install --no-package-lock`) papered over the symptom rather
  than fix the cause.

Also fix the changeset pr: from 3312 (issue) to 3316 (PR). CONTEXT.md
flags this exact failure mode as a recurring CodeRabbit finding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address coderabbit review findings

* fix: close remaining coderabbit threads

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:21:42 -04:00
Tom Boucher
04031244b8 feat(sdk): add NON_FAMILY_COMMAND_ALIASES to manifest — 14 missing commands (#3305)
* test(3251): red — assert 14 missing commands in command-aliases.generated.cjs

Parametrized test covering all 14 commands from issue #3251. Requires the
CJS manifest and asserts structurally (never greps source). Currently fails
because NON_FAMILY_COMMAND_ALIASES is not exported and all 14 are missing.

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

* feat(3251): add 14 missing commands to command-aliases.generated.cjs

Adds NON_FAMILY_COMMAND_ALIASES export to command-aliases.generated.cjs and
extends sdk/src/query/command-manifest.non-family.ts with the 10 commands that
were registered in static catalogs but absent from the manifest source-of-truth:
- check.decision-coverage-plan / check.decision-coverage-verify
- frontmatter.get
- phase.mvp-mode
- progress.bar
- stats.json
- task.is-behavior-adding
- todo.match-phase
- uat.render-checkpoint
- workstream.list

The other 4 (frontmatter.set, learnings.copy, milestone.complete,
requirements.mark-complete) were already in non-family.ts but unexported.

Generator (sdk/scripts/gen-command-aliases.ts) now produces both the TS and CJS
artifacts including the non-family section, sorted by canonical for determinism.
Freshness check (sdk/scripts/check-command-aliases-fresh.mjs) verifies TS and CJS
non-family parity against the manifest source-of-truth.

Fixes #3251

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

* chore(3251): add changeset and CHANGELOG entry for PR #3305

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

* fix(gen-command-aliases): preserve TS type interfaces and annotations on regen (#3251)

- Add FamilyCommandAlias and NonFamilyCommandAlias interface declarations to tsBody so regen never strips them
- Replace JSON.stringify with single-line compact serialisers for TS output (matches committed file format)
- Apply typed annotations (readonly FamilyCommandAlias[] / readonly NonFamilyCommandAlias[]) to all exported TS constants
- CJS path unchanged: remains untyped pure-JS with JSON.stringify multi-line format
- Regenerate sdk/src/query/command-aliases.generated.ts to sync with updated generator

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

* chore: drop redundant CHANGELOG.md edit (use .changeset/ fragment per CONTRIBUTING.md)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:46:40 -04:00
Tom Boucher
90b33d050c fix(phase): unify phase-dir naming via shared helper across creation paths (#3287) (#3292)
* 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>
2026-05-09 07:32:31 -04:00
Tom Boucher
8d5f509edf fix(3266): preserve wave 0 and bucket plans by depends_on DAG in phase-plan-index (#3276)
* fix(3266): preserve wave 0 and bucket plans by depends_on DAG in phase-plan-index

Fixes two cooperating bugs in the phase-plan-index builder:

1. Wave 0 collapse: `parseInt(...) || 1` coerced parsed value `0` to `1` due to
   JS falsy default. Fixed with `Number.isNaN` guard.
2. depends_on ignored: wave-bucketing used only the `wave:` frontmatter field.
   Now replaced with Kahn's topological-level algorithm over `depends_on`:
   source nodes (no in-phase deps) → lowest level; each plan's level = max(deps'
   levels) + 1.  Declared `wave:` that disagrees with computed level emits a
   non-fatal warning on the result.  Cycle detection throws GSDError.

`PlanInfo` gains `depends_on: string[]`. `PhasePlanIndex` gains `warnings?: string[]`.
Both TS (`sdk/src/query/phase.ts`) and CJS twin (`get-shit-done/bin/lib/phase.cjs`)
fixed identically.

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

* chore: add changeset for #3276

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

* fix(phase): resolve depends_on against canonical plan id (#3276 CR)

Build a secondary `canonicalToId` index alongside `planMap` so that a
dependency declared as '03-01' resolves to a descriptive plan stored
under '03-01-auth-hardening', preventing silent wave-ordering failures.
Applied at both DAG construction sites in phase.cjs and the SDK's
phase.ts (k014 parity). Regression test added.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:25:05 -04:00
Tom Boucher
8bc255c266 fix(workstream): normalize migration workstream names (#3269)
* fix(workstream): normalize migrate-name to valid slug

* docs(context): record workstream migrate-name slug invariant

* fix(catalog-cjs): balanced fallback for unknown profile (CR finding A)

profiles[profile] could return undefined for any profile key absent from
the catalog entry, causing downstream callers like formatAgentToModelMapAsTable
to crash on .length. Add ?? profiles.balanced fallback to match the SDK adapter.

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

* test(sdk): anchor path resolution on import.meta.url not cwd (CR finding B)

resolve(process.cwd(), '..') breaks when Vitest is invoked from the repo root
because cwd is already the repo root and '..' goes one level above. Replace
with a file-relative path using fileURLToPath(new URL('../../../', import.meta.url))
anchored at the test file's location (sdk/src/query/).

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

* test: derive Group B runtime list from catalog (CR finding C)

Hardcoded ['kilo', 'cline', ...] throws TypeError if a runtime name is
removed from the catalog. Derive group B dynamically via
Object.keys(catalog.runtimeTierDefaults).filter(r => !r.opus) so the
test never goes stale and auto-covers future Group B additions.

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

* docs(workflow): add hermes to Step B runtime options (CR finding D)

hermes appears in the Group A built-in defaults table but was missing from
the AskUserQuestion options in Step B, forcing users to manually type it via
'Other (Group B or custom)'. Add explicit hermes entry for UI consistency.

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

* docs(config): refresh dynamic_routing tier table; fix stale L671 (findings E+F)

Finding E: tier table was missing 6 heavy-tier agents and 15 standard/light
agents added by this PR. Updated all three rows to match catalog routingTier
assignments (33 agents total).

Finding F: removed stale '18 of 31' claim and agent enumeration; replaced
with accurate note that all 33 agents have explicit catalog entries. Updated
authoritative source pointers to model-catalog.cjs / model-catalog.ts.

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

* test(core): add profile-fallback unit tests for quality and budget (CR nitpick G)

The PR introduced quality→opus and budget→haiku unknown-agent fallbacks but
only balanced→sonnet and inherit→inherit were tested. Add two tests covering
the remaining two branches to complete coverage.

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

* adr: define planning workspace and worktree seam

* refactor(worktree): extract worktree safety policy module

* refactor(workstream): extract active workstream pointer store seam

* test(worktree): cover policy branch paths and persist seam guardrails

* refactor(worktree): centralize health inventory seam for W017

* fix(workspace): align SDK project path policy with CJS planningDir

* refactor(query): unify SDK planning path projection seam

* refactor(init): route workspace projection through planningPaths seam

* docs(adr): add SDK architecture and planning path ADRs

* refactor(worktree): deepen name, pointer, inventory, and config seams

* docs(config): harmonize claude-opus-4-6 to 4-7 in resolve_model_ids example (CR finding 2)

* fix(sdk): return undefined for model_profile='inherit' sentinel (CR finding 3)

* docs(adr): renumber conflicting 0003-sdk-package-seam-module to 0007, update seam-map reference (CR finding 4)

* fix(workstream): align CJS and SDK name validation to accept dots, guard path traversal via includes('..') (CR finding 5)

* fix(sdk): guard writeActiveWorkstream against non-existent workstream directory, k014/k031 parity (CR finding 6)

* chore(changeset): add #3269 changeset (CR finding 1 — proper changeset for this PR)

* docs(inventory): register 3 new CLI modules in INVENTORY.md/MANIFEST (active-workstream-store, workstream-name-policy, worktree-safety)

* fix(sdk): use relPlanningPath(workstream) in planningPaths, fix setActiveWorkstream/getActiveWorkstream name errors in workstream.ts

* fix(sdk): validate GSD_WORKSTREAM in planningPaths before use (#3269 regression)

planningPaths() called resolveWorkspaceContext() which returned GSD_WORKSTREAM
raw (no validation). An invalid value like '../evil' was used as effectiveWorkstream,
constructing a bad path; roadmapAnalyze() caught the ENOENT and returned a
no-phase_count error object instead of the root ROADMAP result.

Fix: validate envCtx.workstream with validateWorkstreamName() in planningPaths()
before accepting it as effectiveWorkstream. Invalid env → null → root .planning/
fallback, preserving the bug-2791 contract: invalid GSD_WORKSTREAM is silently
ignored and falls back to the root context (phase_count: 0 for empty root ROADMAP).

The bug-2791 regression test now passes. No other call sites read GSD_WORKSTREAM
without validation: query-runtime-context.ts already validates; cli.ts already
validates; context-engine.ts takes a caller-validated workstream parameter.

Closes #3268 (regression introduced by #3269 workstream-name-policy work).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:15:04 -04:00
Tom Boucher
65abc4fc90 refactor(query): deepen phase lifecycle seams (#3267)
* refactor(query): extract phase lifecycle policy module

* refactor(query): extract phase fs and roadmap mutation adapters

* fix(sdk): propagate non-ENOENT readdir errors in phase-filesystem-adapter (CR finding 1)

Swallow only ENOENT in listDirectories; rethrow EACCES, EIO, and other
unexpected errors so callers surface real failures rather than silently
treating a permission-denied phases dir as empty.

Also adds regression test: EACCES from readdir now propagates as thrown
error instead of returning [].

* fix(sdk): propagate non-ENOENT readFile errors in phase-roadmap-mutation (CR finding 4)

readModifyWriteRoadmapMd now falls back to empty content only on ENOENT;
EACCES, EIO, and other errors are rethrown so a subsequent write cannot
clobber real roadmap content that is temporarily unreadable.

Regression tests: EACCES propagates; absent ROADMAP.md still starts empty.

* fix(sdk): omit Depends on: Phase 0 for first sequential phase; align prefix grammar (CR findings 2+3)

Finding 2: buildPhaseRoadmapEntry now omits the "Depends on" line when
phaseId == 1 (prevPhase would be 0, which is not a valid predecessor).
The guard is `prevPhase < 1` so future phase-0 configs are also safe.

Finding 3: collectDecimalSuffixesFromDirNames regex prefix pattern
updated from `[A-Z]{1,6}` to `[A-Z][A-Z0-9]*` (case-insensitive flag
added), matching the grammar used by scanSequentialMaxPhaseFromDirs.
Prevents k014 parity drift for alphanumeric project-code prefixes longer
than six characters or containing digits.

Regression tests for both fixes included.
2026-05-09 00:14:59 -04:00
Tom Boucher
288b3b4170 fix(3259): non-mutating --help guard for native query handlers (#3272)
* fix(3259): non-mutating --help guard for native query handlers; reject --help as milestone version

Adds a dispatcher-level guard in query-dispatch.ts that short-circuits
to a non-mutating help stub whenever --help/-h appears in args destined
for a native mutating handler (fail-closed by default). Adds defense-
in-depth in milestoneComplete to reject --help/-h as a version value
before any disk write. Regression tests cover: per-handler --help guard,
registry-driven invariant across all mutating commands, handler-level
GSDError for both flags, and preservation of the #3019 CJS fallback
contract.

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

* chore: add changeset fragment for #3272

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:53:27 -04:00
Tom Boucher
ecd57e622c fix(3265): prefer YAML frontmatter for state-snapshot canonical fields (#3275)
* fix(3265): prefer YAML frontmatter for state-snapshot canonical fields

stateSnapshot in both sdk/src/query/state.ts and the CJS twin
(get-shit-done/bin/lib/state.cjs cmdStateSnapshot) passed the whole
STATE.md blob to stateExtractField, whose bold pattern (**Field:**)
has no line anchor.  A body table cell such as
"**Status:** to ✅ COMPLETE" therefore silenced the correct YAML
frontmatter value.

Fix: extractFrontmatter(content) first; stripFrontmatter(content) for
the body passed to stateExtractField; for each canonical scalar field
prefer the non-empty frontmatter value, falling back to body extraction
when the key is absent or the file has no frontmatter block at all.

Regression tests added in sdk/src/query/state.test.ts (vitest) and
tests/state.test.cjs (node:test) covering:
- frontmatter status beats **Status:** inside a table cell
- frontmatter current_plan beats bold body value
- no-frontmatter files continue to extract from body
- field absent from frontmatter falls through to body extractor

Fixes #3265

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

* chore: add changeset for #3275

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

* test: reproduce fmStr drops non-string YAML scalars (#3275 CR finding)

Add tests/bug-3275-fmstr-non-string-scalars.test.cjs with 5 cases covering
CJS state-snapshot with numeric frontmatter scalars (current_phase: 19,
total_phases: 7, total_plans_in_phase: 5), string regression, and
no-frontmatter body fallback regression.

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

* fix(state): fmStr accepts numeric/boolean YAML scalars (CR finding)

Rename `fmStr` to `fmScalar` in both state.cjs and sdk/src/query/state.ts
and broaden the type guard so that non-null number/boolean frontmatter values
are coerced to String(v) instead of being discarded.

The previous `typeof v === 'string'` check was a latent bug: if the YAML
parser ever returns typed scalars (e.g. `current_phase: 19` as the number 19),
the frontmatter value would be silently dropped and the stale body value used
instead.  Both files are updated identically (k014 parity).

Also adds three SDK vitest regression cases (numeric current_phase,
total_phases, total_plans_in_phase) in sdk/src/query/state.test.ts.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:53:21 -04:00
Tom Boucher
96806003c5 fix(#3229): shared model catalog source of truth for agent profiles + runtime tier defaults (#3230)
* docs(adr): add ADR-0003 model catalog module

* fix(#3229): add shared model catalog as source of truth for agent profiles and runtime tier defaults

Research / design (ADR-0003):
- Existing drift came from 4 independent model truths:
  1. CJS model-profiles.cjs
  2. SDK config-query.ts stale copy (18 agents)
  3. settings-advanced.md runtime tier table
  4. session-runner Claude-only profile map
- New design: one machine-readable Model Catalog Module in sdk/shared/
  that both packages ship and consume.

Implementation:
- sdk/shared/model-catalog.json — canonical source of truth for:
  - full 33-agent registry
  - per-agent golden (quality) alias + balanced/budget aliases
  - adaptive derivation from routingTier
  - agent→phaseType map
  - agent→dynamic-routing default tier map
  - runtime tier defaults for all supported runtimes
- get-shit-done/bin/lib/model-catalog.cjs — CJS adapter over the catalog
- sdk/src/model-catalog.ts — SDK adapter over the same catalog
- CJS model-profiles.cjs now re-exports derived data from model-catalog.cjs
- SDK config-query.ts now re-exports MODEL_PROFILES/VALID_PROFILES from
  model-catalog.ts instead of maintaining its own list
- sdk/src/query/helpers.ts runtime list now comes from the catalog (fixes hermes drift)
- sdk/src/session-runner.ts Claude profile→model-id mapping now resolves via catalog
- docs/CONFIGURATION.md + settings-advanced.md runtime tables updated to match catalog

Behavior changes:
- resolve-model now covers every shipped agent file on disk (33 agents)
- unknown-agent fallback is profile-semantic, not hardcoded sonnet:
  quality→opus, budget→haiku, balanced/adaptive→sonnet, inherit→inherit
- Group B runtimes remain known runtimes but do not get built-in tier defaults

Tests (RED→GREEN):
- root tests: shipped agent files must equal MODEL_PROFILES keys
- sdk tests: shipped agent files must equal MODEL_PROFILES keys
- direct fix assertion: gsd-code-reviewer resolves to opus under quality with no unknown_agent
- runtime defaults parity test: settings-advanced.md + CONFIGURATION.md tables must match catalog
- helper tests: hermes included in SUPPORTED_RUNTIMES and getRuntimeConfigDir()

Closes #3229

* chore(changeset): update #3229 changeset pr field to 3230

* fix(ci): update inherit fallback expectations and inventory parity for model catalog
2026-05-08 21:25:37 -04:00
Tom Boucher
deeb6deb67 fix(install): accept Codex TOML floats; idempotent rollback (#3245) (#3254)
* test: reproduce extractFrontmatter LAST-block bug (#3240)

* test: reproduce state.update progress trampling and percent formula (#3242)

Two failing regression tests:
- Bug A: state.update "Last Activity" tramples curated progress.* frontmatter via readModifyWriteStateMd → syncStateFrontmatter
- Bug B: 12 declared ROADMAP phases / 6 realized / 6/6 plans done → percent: 100 instead of 50 (phase-fraction ignored)

* test: reproduce TOML float rejection and partial rollback (#3245)

Two failing regression tests:
1. parseTomlToObject rejects valid Codex TOML floats (tool_timeout_sec = 20.0)
2. Post-install validation failure leaves skills/, agents/, VERSION on disk
   despite restoring config.toml — hybrid state after abort

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

* fix(install): accept TOML floats; idempotent codex rollback (#3245)

Two fixes for the Codex install failure introduced by #2760 CR4 finding 3:

1. parseTomlValue now accepts TOML 1.0 float literals (decimals,
   exponents, underscore separators, signed). Codex CLI's serde schema
   requires f64 for tool_timeout_sec / startup_timeout_sec — the prior
   strict-integer-only check was the inverse of what Codex requires,
   causing every config with a float to trigger a fatal schema validation
   failure. Date/time separators (-/:T/Z) are still rejected.

2. restoreCodexSnapshot is extended into a unified idempotent rollback
   that reverts ALL Codex-specific mutations on failure:
   - config.toml (existing behavior)
   - skills/gsd-* directories (new)
   - agents/gsd-*.{md,toml} files (new)
   - get-shit-done/VERSION (new)
   - orphaned atomic-write temp files (new)
   Pre-install state is captured before the first Codex write so the
   rollback reflects the true pre-GSD state. Non-gsd-* user content is
   untouched. The rollback is safe to call multiple times and before any
   snapshots are captured.

Fixes #3245

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

* changeset: pr=3254 for #3245

* test: fix source-grep lint violation in bug-3242 test (#3242)

Replace content.includes() check with line-by-line parse of STATE.md body.
The lint enforces structural assertions over raw text matching.

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

* test: mark #3242 RED tests as todo pending fix (#3242)

The three failing tests are intentional regression tests for bugs in
state.cjs that will be fixed in a separate PR. Mark them { todo: true }
so they don't block CI on this branch.

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

* fix(install): tighten TOML underscore placement validation (CR finding 1)

The float regex used [\d_]* which accepts invalid forms like 1__0, 1_.0,
and 1._0. TOML 1.0 §2 requires underscores only between digits. Switch
both the integer pre-check and the full float pattern to (?:_?\d)* so
consecutive underscores, leading underscores on a segment, and trailing
underscores on a segment are all rejected before replace(/_/g,'') can
silently normalize them into valid JS numbers.

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

* fix(install): restore pre-existing gsd-* content on rollback (CR finding 2)

The snapshot only recorded names of pre-existing skills/gsd-* dirs and
agents/gsd-* files. On a failed reinstall the rollback could delete
newly-created dirs but could not restore the bytes of dirs/files that
were overwritten, leaving the user in a hybrid state (old config.toml,
new skill files).

Now snapshot the full file tree of every pre-existing gsd-* skill dir
into codexPreInstallSkillContents (Map<name, Map<relPath, Buffer>>) and
every pre-existing agent file into codexPreInstallAgentContents
(Map<filename, Buffer>). restoreCodexSnapshot() uses these maps to
wipe-and-restore overwritten entries and only removes entries that had
no pre-install state, giving a true atomic rollback guarantee.
Reads are best-effort so a partial snapshot is still better than none.

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

* fix(install): scope temp-file cleanup to installer-owned writes (CR finding 3)

_cleanTmpFiles() was deleting any *.tmp-<pid>-<n> file found under
targetDir. This is too broad: other tools in the user's Codex/home
directory may create temp files matching the same suffix pattern, and a
GSD install rollback would silently delete them.

Add __atomicWrittenTmps (a module-level Set<string>) populated by
atomicWriteFileSync for every temp path it creates. _cleanTmpFiles()
now checks __atomicWrittenTmps.has(full) before unlinking, so only temp
files this installer process actually wrote are eligible for cleanup.

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

* fix(test): remove no-op doesNotThrow wrapping try/catch (CR finding 4)

assert.doesNotThrow(() => { try { f(); } catch(_){} }) always passes
because the catch block swallows every exception before the outer
assertion can see it. This meant the rollback-idempotency guarantee was
never actually verified.

Replace with an explicit threw flag around runCodexInstall, assert that
the install did throw (validation failure is expected), and add a
post-rollback state assertion that skills/ was not created. This gives
a loud failure surface if runCodexInstall starts crashing from inside
the rollback path, matching the intent described in the test comment.

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

* fix(test): correct describe title for float-acceptance tests (CR nitpick 1)

The describe block title said 'rejects malformed input that previously
slipped through', but the test inside now asserts that TOML floats are
accepted (the #3245 inversion). This misled readers expecting every
sub-test to assert rejection. Update the title to reflect the mixed
behaviour: floats are accepted; dates and trailing-garbage are rejected.

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

* fix(test): rename test to match what the assertion actually checks (CR nitpick 2)

The test name 'post-install config retains float literal form (20.0 not
truncated to 20)' promised a string-form invariant, but the assertion
uses numeric equality (assert.strictEqual(parsed.tool_timeout_sec, 20))
which cannot distinguish 20 from 20.0 in JS. Rename to 'post-install
config round-trips tool_timeout_sec as numeric 20' so the description
matches what the test actually verifies.

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

* fix(test): replace raw text scan with state json assertion (CR nitpick 3)

The 'Last Activity updates the body field' test was reading STATE.md as
raw text, splitting on newlines, and using lines.find/startsWith to
locate the 'Last Activity:' line — the exact pattern-match-on-source
approach prohibited by the no-source-grep testing standard.

Replace with runGsdTools('state json', tmpDir) which surfaces the body-
extracted Last Activity value as fm.last_activity in its JSON output,
and assert against that structured field instead.

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

* fix(test): correct post-rollback state assertion for early-failure case

The previous assertion checked that skills/ didn't exist, but the
installer writes skills/ before the schema validator fires. Rollback
removes gsd-* dirs inside skills/, not skills/ itself. Update the
assertion to verify that no gsd-* skill dirs survive rollback, which
is the actual invariant the test name describes.

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

* changeset: document full rollback scope (CR finding 1)

Adds config.toml restoration and orphaned atomic-write temp-file
cleanup to the changeset description — the previous text only listed
skills/, agents/, and VERSION.

* fix(install): wrap post-snapshot scope in rollback handler (CR finding 2)

Any throw between the pre-install snapshot capture and the Codex config
block (skills copy, agents copy, VERSION write, manifest write, leaked-
path scan, etc.) now triggers _codexPreConfigRollback() so the caller
is never left in a partially-installed state.  Previously only the later
config.toml mutation paths had rollback wired in.

Introduces _codexPreConfigRollback (defined right after snapshot capture)
and wraps the intervening operations in a try/catch that invokes it on
error for Codex installs; non-Codex paths are unaffected.

* test: assert threw=true to prevent vacuous pass (CR finding 4)

Two tests used bare try/catch without asserting threw === true, so they
would silently pass even if runCodexInstall never threw (k060 pattern).
Each bare catch block is replaced with a threw flag and a
strictEqual(threw, true, ...) assertion.

CR findings 2+3 are both addressed in the preceding install commit:
finding 3 (restore from snapshot manifest, not current FS state) lands
alongside the rollback-wrapper change as part of the restoreCodexSnapshot
refactor.

* fix(install): reject leading zeros in TOML float integer part per TOML 1.0 (CR finding round 4)

TOML 1.0 §2 disallows leading zeros in the integer part of numeric
literals — `01`, `00`, `01.5`, `00e2`, `+01.0`, `-01.0` are all invalid.
The pre-check and float regexes in parseTomlValue used `\d(?:_?\d)*` which
accepted any digit as the leading digit.

Both regexes are tightened to `(0|[1-9](?:_?\d)*)` for the integer part:
- `0` alone is valid
- a non-zero leading digit followed by optional underscored digits is valid
- `01`, `00`, and any variant with a leading zero and further digits is rejected

The "still rejects bare time (07:32:00)" test assertion is broadened from
`/unsupported TOML value/` to `/unsupported TOML value|trailing bytes/`
because the parser now stops at `0` and the remainder `7:32:00` is rejected
as trailing bytes — the invariant (time literals are not accepted) is unchanged.

25 new regression tests cover all rejection cases and valid TOML forms.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:25:59 -04:00