Commit Graph

1535 Commits

Author SHA1 Message Date
Tom Boucher
f4412349f0 fix(#2835): align CR-INTEGRATION tests with hyphen-form skill names (#2843)
* fix(#2835): align CR-INTEGRATION tests with hyphen namespace

PR #2819 changed autonomous.md skill invocations from `gsd:code-review`
(colon) to `gsd-code-review` (hyphen). Tests still asserted the legacy
colon form against the user-installed plugin dir (which lags the repo).

Switch tests to:
- Read autonomous.md from the canonical repo WORKFLOWS_DIR (not the
  plugin install location, which can be stale)
- Parse `Skill(skill="...")` invocations structurally instead of
  substring matching, and assert the canonical hyphen form is present
  while explicitly rejecting the legacy colon form.

Closes #2835

* test(#2835): parse Skill() invocations structurally in CR-INTEGRATION tests

Replace raw-text regex/.includes() assertions with a proper parser that
walks autonomous.md, skips escaped string contexts, and yields
[{ skill, args }] objects. The three CR-INTEGRATION tests now assert
against parsed fields and tokenized args (not substring matches),
addressing CodeRabbit feedback on PR #2843.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 07:57:30 -04:00
Tom Boucher
a7f83ee663 fix(#2831): expand HOME in OpenCode @file references on all platforms (#2842)
* fix(#2831): expand HOME in OpenCode skill/template paths

OpenCode does not shell-expand $HOME in @file references on any platform —
the literal `@$HOME/...` path is resolved relative to the config command/
dir, producing `command/$HOME/...` (file not found). The previous fix for
#2376 only guarded Windows; extend to all platforms.

Closes #2831

* test(#2831): assert behavior via exported computePathPrefix, not source grep

Addresses CodeRabbit review on PR #2842:
- Extracts pathPrefix logic into a named, test-exported computePathPrefix
  helper in bin/install.js (no behavior change at the call site).
- Rewrites bug-2376 and bug-2831 regression tests to call the exported
  function directly instead of regex-matching install.js source text,
  per the repo's no-source-grep testing standard.
- Wraps temp-dir test setup in try/finally so cleanup runs on assertion
  failures (no leaked tmp dirs).
2026-04-29 07:56:51 -04:00
Tom Boucher
7fae804296 fix(#2839): transactional cleanup tail for /gsd-code-review-fix (#2846)
* fix(#2839): make /gsd-code-review-fix cleanup transactional

Cleanup tail in agents/gsd-code-fixer.md previously did 'git worktree
remove' without any recovery marker. If the process was killed between
fix commits and worktree removal, the orphan worktree + branch survived
with no resume path — the next run had no way to discover or finish
the cleanup.

Introduce a recovery sentinel at ${phase_dir}/.review-fix-recovery-pending.json
with strict ordering:
- Sentinel written AFTER 'git worktree add' succeeds (never points at a
  worktree that does not exist).
- Sentinel removed ONLY AFTER 'git worktree remove' returns successfully
  (interruption between commits and removal leaves a sentinel behind).
- New runs detect a pre-existing sentinel, force-remove the recorded
  orphan worktree, then drop the stale sentinel before continuing —
  making the agent self-healing after a crash.

Closes #2839

* fix(#2839): harden sentinel JSON parse and scope ordering assertion

Address CodeRabbit review feedback on PR #2846:

- agents/gsd-code-fixer.md: Guard the recovery-sentinel JSON parse with
  try/catch so a corrupted/truncated sentinel (a realistic crash artifact)
  emits a warning and yields an empty prior_wt instead of aborting setup.
  This preserves the self-healing recovery path even when the sentinel
  itself is the casualty of the original crash.

- tests/bug-2839-review-fix-transactional-cleanup.test.cjs: Scope the
  cleanup-ordering assertion to the cleanup-tail section of the
  setup_worktree step rather than first global occurrences. Previously
  the assertion could pass on pre-recovery references even if cleanup-tail
  ordering regressed. The regex also now accepts the shell-variable form
  (\`rm -f \"\$sentinel\"\`) used in the cleanup tail.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 07:56:32 -04:00
Tom Boucher
c3a42d66f9 Revert "feat(install): add Hermes Agent runtime support" (#2849) 2026-04-29 07:44:49 -04:00
teknium1
5a636bc90a feat(install): add Hermes Agent runtime support (#2841)
Adds Hermes Agent as a supported installation target. Users can run
\`npx get-shit-done-cc --hermes\` to install all 86 GSD commands as
skills under \`~/.hermes/skills/gsd-*/SKILL.md\`, following the same
open skill standard as Claude Code 2.1.88+, Qwen Code, Antigravity,
Trae, Augment, and Codebuddy.

Hermes Agent is an open-source AI agent framework by Nous Research
(NousResearch/hermes-agent, MIT). Its skill loader accepts the Claude
skill format as-is: frontmatter parsed with PyYAML SafeLoader (unknown
keys like \`allowed-tools\` / \`argument-hint\` ignored), body XML tags
(\`<objective>\`, \`<execution_context>\`, \`<process>\`) passed directly
to the model. Compatibility proven end-to-end with all 86 GSD skills
loading cleanly, \`skill_view()\` returning full bodies, and
\`build_skills_system_prompt()\` emitting them into the agent system
prompt — zero Hermes code changes required.

Changes:
- \`bin/install.js\`: --hermes flag, getDirName/getGlobalDir/getConfigDirFromHome
  support, HERMES_HOME env var (native to Hermes — used for profile
  mode / Docker deploys), install/uninstall pipelines, interactive
  picker option 10 (alphabetical: between Gemini and Kilo), .hermes
  path replacements in copyCommandsAsClaudeSkills and
  copyWithPathReplacement, legacy commands/gsd cleanup, CLAUDE.md ->
  HERMES.md and "Claude Code" -> "Hermes Agent" content rewrites in
  skills/agents/hooks, runtime-appropriate finish message.
- \`get-shit-done/bin/lib/core.cjs\`: add hermes to KNOWN_RUNTIMES;
  add RUNTIME_PROFILE_MAP.hermes with OpenRouter-slug defaults
  (Hermes is provider-agnostic; these defaults resolve across
  OpenRouter, native Anthropic, and Copilot via Hermes' aggregator-
  aware resolver, and are overridable per-tier via
  model_profile_overrides.hermes.{opus,sonnet,haiku}).
- \`README.md\`: Hermes Agent in tagline, runtime list, verification
  command, install/uninstall examples, \`--hermes\` flag reference.
- \`tests/hermes-install.test.cjs\`: new, 14 tests covering directory
  mapping, HERMES_HOME env var precedence, install/uninstall
  lifecycle, user-skill preservation, engine cleanup.
- \`tests/hermes-skills-migration.test.cjs\`: new, 11 tests covering
  frontmatter conversion, path replacement (~/.claude/ ->
  \$HERMES_HOME/skills/), CLAUDE.md -> HERMES.md, "Claude Code" ->
  "Hermes Agent", stale skill cleanup, SKILL.md format validation.
- \`tests/multi-runtime-select.test.cjs\`: updated for new option
  numbering (hermes=10, kilo=11, opencode=12, qwen=13, trae=14,
  windsurf=15, all=16).
- \`tests/kilo-install.test.cjs\`: updated assertions for Kilo having
  moved from option 10 to option 11.

Closes #2841

Implementation notes:
- Zero custom code paths: Hermes reuses copyCommandsAsClaudeSkills()
  identical to Qwen Code / Antigravity pattern.
- Path replacement: ~/.claude/, \$HOME/.claude/, ./.claude/ ->
  .hermes equivalents in skill/agent/hook content.
- Config precedence: --config-dir > HERMES_HOME > ~/.hermes (matches
  how Hermes itself resolves its home directory).
- Legacy cleanup: removes commands/gsd/ if present from a prior
  install, preserving dev-preferences.md (same as Qwen).
- No external dependencies added.

Testing: 5841 / 5841 tests pass (0 failures, 0 regressions)
- 14 new tests in hermes-install.test.cjs
- 11 new tests in hermes-skills-migration.test.cjs
- multi-runtime-select.test.cjs renumbered + 1 new test (single choice for hermes)
2026-04-29 04:27:46 -07:00
Tom Boucher
eeaf9c556f fix(#2787): track fenced code blocks in extractCurrentMilestone (#2812)
* fix(#2787): track fenced code blocks in extractCurrentMilestone

The milestone-end search used a multiline regex against the raw
restContent string. Lines inside fenced code blocks (``` or ~~~)
that matched the milestone-heading pattern (e.g. `# note v1.0`)
prematurely set sectionEnd, hiding all phases after the block from
roadmap analyze, roadmap get-phase, and every downstream command.

Replace the regex match with a line-by-line scan that tracks fence
state. Lines inside an open fence are skipped regardless of content.
Adds three regression tests covering backtick fences, tilde fences,
and the roadmap get-phase code path.

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

* fix(#2787): track fence delimiter instead of toggling bare boolean

Replace the inFence boolean with fenceChar/fenceLen tracking so that
indented fences (up to 3 leading spaces) and mixed-delimiter content
(~~~ inside a backtick fence) are parsed correctly. A closing fence
is only recognised when it uses the same character as the opening
delimiter and has at least the same run length, matching the CommonMark
spec.

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

* fix(#2787): require fence-only closing line — reject info-string lines as closers

A closing fence delimiter must contain only optional trailing whitespace.
A line like \`\`\`js inside an open fence has an info string and must not
close it. The previous regex /^\s{0,3}([`~]{3,})/ matched the opening of any
such line, so the closing check could toggle fenceChar off on an info-string
line and expose subsequent heading-like content to the milestone-end detector.

Fix: capture the trailing portion of every fence-candidate line and only clear
fenceChar when trailing matches /^\s*$/ (per CommonMark §4.5).

Adds a regression test covering the ```text / ```js nesting scenario.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:37:47 -04:00
Tom Boucher
9e58c45ea1 fix(#2791): GSD_WORKSTREAM env var respected by gsd-sdk query + gsd-tools bin alias (#2821)
* fix(#2791): GSD_WORKSTREAM env var respected by gsd-sdk query + gsd-tools bin alias

Two fixes for gsd-sdk binary issues:

**Issue 1 — Binary name collision:**
Both `get-shit-done-cc` and `@gsd-build/sdk` declare `bin: { "gsd-sdk": ... }`.
Added `"gsd-tools": "bin/gsd-sdk.js"` to `package.json` bin so users with the
collision can invoke `gsd-tools query <cmd>` as a conflict-free alternative.

**Issue 2 — Query registry not workstream-aware:**
`gsd-sdk query` commands ignored `GSD_WORKSTREAM` env var, always reading from
the root `.planning/` even when a workstream was active. `gsd-tools.cjs` reads
`GSD_WORKSTREAM` via `planningDir()`, so all ~35 `gsd-sdk query` call sites in
workflow files were broken in workstream-scoped projects.

Fix: added env var fallback in `sdk/src/cli.ts` — when `--ws` is not provided,
`GSD_WORKSTREAM` is used (with name validation; invalid values are silently
ignored, matching CJS behaviour).

Regression test: `tests/bug-2791-sdk-workstream-env.test.cjs`

Closes #2791

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

* fix(#2791): address CodeRabbit — precedence test, invalid env fallback assertion, bash fence

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:23:32 -04:00
Tom Boucher
897cff6051 fix(#2805): find-phase returns null phase_dir for archived phases (not archive path) (#2818)
* fix(#2805): add regression test — archived phase fallback already fixed in source

getPhaseInfoWithFallback already discards archived disk matches when the
current ROADMAP lists the phase (line 133: phaseInfo?.archived &&
roadmapPhase?.found). The regression test confirms this behavior and
prevents the bug from being reintroduced by future refactors.

Regression test: tests/bug-2805-archived-phase-fallback.test.cjs
(3 tests: phase_dir null, phase_found true, phase_name from ROADMAP)

* fix(#2805): address CodeRabbit — exact phase_name assertion, bash fence
2026-04-28 20:23:29 -04:00
Tom Boucher
a4e15d5616 fix(#2788): audit-uat reads human_verification items from frontmatter (#2814)
* fix(#2788): audit-uat reads frontmatter human_verification array

parseVerificationItems only searched the body for a '## Human Verification'
section. gsd-verifier writes items to the frontmatter human_verification:
YAML array, so audit-uat returned total_items: 0 for all such files.

Two fixes:
1. Read frontmatter human_verification: array first (via extractFrontmatter);
   return those items if present (primary path for gsd-verifier output).
2. Relax the body-section heading regex to accept underscore separators and
   parenthetical suffixes (e.g. '## human_verification (action required)').

Regression test: tests/bug-2788-audit-uat-frontmatter.test.cjs

* fix(#2788): address CodeRabbit — trim whitespace entries, support hyphenated headings, bash fence
2026-04-28 20:22:59 -04:00
Tom Boucher
eddb2a205b fix(#2801): add ingest-docs handler to gsd-tools init dispatch (#2820)
* fix(#2801): add ingest-docs handler to gsd-tools init dispatch

The `/gsd-ingest-docs` workflow was broken because `workflows/ingest-docs.md`
called `gsd-sdk query init.ingest-docs` but the installed binary is `gsd-tools`,
and `gsd-tools init` had no `ingest-docs` case in its dispatch switch.

- Added `cmdInitIngestDocs` function to `init.cjs` and exported it; returns
  `project_exists`, `planning_exists`, `has_git`, `project_path`, `commit_docs`
- Added `case 'ingest-docs'` to the `init` switch in `gsd-tools.cjs`
- Updated `workflows/ingest-docs.md` to call `gsd-tools init ingest-docs`
  (line 55) and `gsd-tools commit` (line 292) instead of `gsd-sdk query ...`
- Regression test: `tests/bug-2801-ingest-docs-handler.test.cjs`

Closes #2801

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

* fix(#2801): address CodeRabbit — commit_docs assertion, broader gsd-sdk detection, bash fence

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:22:40 -04:00
Tom Boucher
5fe1f00a0d fix(#2808): SKILL.md files use hyphen name form (gsd-cmd not gsd:cmd) (#2819)
* fix(#2808): SKILL.md name uses hyphen form for Claude Code autocomplete

skillFrontmatterName() was converting gsd-<cmd> to gsd:<cmd> (colon) so
installed SKILL.md files had name: gsd:add-phase etc. Claude Code surfaces
this name in autocomplete, showing the deprecated colon form to users even
though the hyphen form is canonical everywhere else.

Root cause: the colon form was needed because workflows called
Skill(skill="gsd:<cmd>"). All 4 remaining colon-form Skill() calls in
autonomous.md and execute-phase.md are updated to hyphen form.

skillFrontmatterName() now returns the hyphen dir name unchanged.
Updated 4 existing tests that asserted colon form.

Regression test: tests/bug-2808-skill-hyphen-name.test.cjs

* fix(#2808): address CodeRabbit — bash/text fences, structured test assertions, fail-loud on errors
2026-04-28 20:22:37 -04:00
Tom Boucher
fa78692167 fix(#2796): roadmap-update-plan-progress accepts --phase flag form (#2815)
* fix(#2796): roadmap update-plan-progress accepts --phase flag form

roadmap-update-plan-progress used positional-only arg parsing: args[0].
When execute-phase.md:228 calls it with --phase <N>, args[0] was the
literal string "--phase", which findPhase received as the phase number.
findPhase returned found:false, causing updated:false with no write.
ROADMAP.md plan checkboxes silently never advanced.

Fix: check for --phase <value> first; fall back to the first non-flag
positional argument for backward-compatible direct calls.

Regression test: tests/bug-2796-arg-parsing-regression.test.cjs

* fix(#2796): address CodeRabbit — guard --phase against flag-like values, bash fence
2026-04-28 20:22:30 -04:00
Tom Boucher
b959b1844f fix(#2803): config-get supports --default <value> fallback for missing keys (#2817)
* fix(#2803): honor --default flag in SDK config-get handler

The gsd-sdk query config-get handler ignored the --default <value> flag.
Missing keys always threw 'Key not found' (exit 1), making 8 workflow
sites that rely on config-get --default fall through to error paths.

The CJS path (gsd-tools.cjs) honored --default since #1893; this ports
that behavior to the SDK configGet handler.

Regression test: tests/bug-2803-config-get-default-flag.test.cjs

* fix(#2803): address CodeRabbit — require --default value, keep missing config.json as error, bash fence
2026-04-28 20:21:48 -04:00
Tom Boucher
7616309a32 fix(#2798): add context_window to VALID_CONFIG_KEYS allowlist (#2816)
* fix(#2798): add regression test — context_window key already in VALID_CONFIG_KEYS

context_window was already added to both VALID_CONFIG_KEYS allowlists
(CJS and SDK) in a prior fix. The regression test confirms it stays there
and that config-set context_window succeeds end-to-end.

Regression test: tests/bug-2798-context-window-config-key.test.cjs

* fix(#2798): address CodeRabbit — add bash language to release notes fence
2026-04-28 20:21:44 -04:00
Tom Boucher
d46efb4790 fix(#2784): clear shared ~/.cache/gsd/ update-check cache in update workflow (#2813)
* fix(#2784): clear shared ~/.cache/gsd/ cache in update workflow

The SessionStart hook (hooks/gsd-check-update.js) writes update-check
results to $HOME/.cache/gsd/gsd-update-check.json (shared, tool-agnostic).
The update.md run_update step only cleared per-runtime paths like
~/.claude/cache/gsd-update-check.json, so the statusline kept showing the
stale upgrade indicator after a successful update.

Fix: add rm -f "$HOME/.cache/gsd/gsd-update-check.json" to the
cache-clear block in the run_update step.

Regression test: tests/bug-2784-update-cache-clear-path.test.cjs

* fix(#2784): address CodeRabbit review — four edge-cases count, bash fence, structured test assertions
2026-04-28 20:21:41 -04:00
Tom Boucher
055b43054f fix(#2794): embed model_profile_overrides.opencode.<tier> into generated OpenCode agents (#2822)
* docs: add CHANGELOG entry and rc.5 release notes for #2809 Codex hooks migrator fixes

Covers the five correctness findings addressed in the round-5 CR of PR #2809:
parseHooksBody key parser (hyphenated/quoted keys), buildNestedBlock empty-handler
guard, legacyMapSections segment-count filter, quoted-dot regression test, and
strengthened command path assertion.

Closes #2810

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

* fix(#2794): embed model_profile_overrides.opencode.<tier> into generated OpenCode agents

OpenCode agent files were missing `model:` frontmatter when the user configured
tier-based model resolution via `model_profile_overrides.opencode.*`. Only
explicit `model_overrides[agent]` was consulted; the runtime profile resolver
(used by the Codex path since #2517) was never called for OpenCode agents.

Added a tier-resolver fallback in the OpenCode agent conversion block in
`bin/install.js`. Precedence (matching Codex behavior):
  model_overrides[agent] > model_profile_overrides.opencode.<tier> > omit

Regression test: `tests/bug-2794-opencode-model-profile-overrides.test.cjs`

Closes #2794

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:16:27 -04:00
Tom Boucher
3c03a153a5 fix(#2773): emit correct Codex 0.124.0+ two-level nested hooks schema (#2809)
* fix(#2773): emit correct Codex 0.124.0+ two-level nested hooks schema

Codex 0.124.0's stable spec requires:

  [[hooks.SessionStart]]          ← event entry (optional matcher)

  [[hooks.SessionStart.hooks]]    ← handler sub-table
  type = "command"
  command = "node ..."

Previous GSD versions wrote the flat [[hooks]] + event = "SessionStart"
form (#2637) or a single-block [[hooks.SessionStart]] without the nested
.hooks sub-table (#2760). Both are rejected by Codex 0.124.0+ at launch.

Changes:

bin/install.js
  - Hook block emission now always writes the two-level nested AoT form.
  - migrateCodexHooksMapFormat extended to also migrate flat [[hooks]]
    array-of-tables entries (event = "..." key → [[hooks.<EVENT>]] form).
    Flat [[hooks]] and [[hooks.<EVENT>]] are mutually exclusive TOML types;
    any pre-existing flat entries must be promoted before GSD appends its
    own namespaced hooks.
  - Migrated flat AoT blocks are inserted BEFORE the GSD marker so they
    stay in the "user" portion of the file and survive stripGsdFromCodexConfig.
  - stripCodexGsd* regexes cover all four historical block shapes.
  - validateCodexConfigSchema no longer rejects flat [[hooks]] at the root
    level (removing the false-positive that blocked install when users had
    their own AfterCommand hooks). The validator still enforces the nested
    [[hooks.<EVENT>.hooks]] shape for entries that have a .hooks sub-table.

tests/
  - bug-2760-codex-install-defensive.test.cjs: 29/29 passing.
    Added 5 new regression cases for fresh install, upgrade from each
    legacy shape, idempotent reinstall, and user hook preservation.
  - codex-config.test.cjs: 106/106 passing.
    All migration tests updated to assert [[hooks.<TYPE>.hooks]] sub-table
    (command now in handler level, not event-entry level).
    New tests: flat [[hooks]] migration (SessionStart, AfterCommand),
    install+uninstall preserves non-GSD AfterCommand hook.

Closes #2773

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

* fix: address CodeRabbit review + CI regression in bug-2698-crlf-install

CI regression (#2698 tests):
  Strip GSD-managed hook blocks BEFORE running migrateCodexHooksMapFormat.
  The previous order let migration convert the stale [[hooks]] + event =
  "SessionStart" + gsd-update-check.js block to [[hooks.SessionStart]] form
  before Shape 1 strip regex could match it; Shape 1 only matches the flat
  [[hooks]] form, so the stale block survived reinstall. Swapping to
  strip-then-migrate ensures only user-authored hooks reach the migration step.
  Shape 3/4 regexes also extended to match both gsd-check-update.js and the
  legacy gsd-update-check.js filename so no variant slips through.

CodeRabbit actionable (major):
  migrateCodexHooksMapFormat now accepts single-quoted TOML event values
  (event = 'SessionStart') in the flat [[hooks]] filter and event-name
  extractor. TOML spec allows single-quoted literal strings; double-quote-only
  regexes silently skipped them, leaving the block unmigrated and triggering
  the hard-fail validator.

CodeRabbit nitpicks:
  tests/codex-config.test.cjs: replace indexOf('[[hooks.AfterCommand]]')
  ordering check with parseTomlToObject structural assertions (no-source-grep
  rule).
  tests/bug-2760-codex-install-defensive.test.cjs: replace three
  content.match(/…/g).length raw-text counts with parseTomlToObject structural
  assertions for single-handler and single-event-entry invariants.

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

* fix: address CodeRabbit review #2 — extractFlatHookEventName helper + type assertions

- bin/install.js: consolidate TOML_QUOTED_STRING + TOML_EVENT_CAPTURE into a
  single extractFlatHookEventName() helper that rejects empty-string event values
  (event = "" or event = ''); previously two independent regexes had to be kept
  in sync and neither guarded against a blank event name producing a [[hooks.]]
  header

- tests/bug-2760-codex-install-defensive.test.cjs: add comments explaining why
  the e.command fallback is retained in both allSessionStartCommands and
  afterToolCommands collectors — migration only upgrades [hooks.TYPE] map-format
  sections, not existing [[hooks.TYPE]] namespaced AoT entries authored with
  command at event-entry level; removing the fallback causes false failures for
  preserved user entries

- tests/codex-config.test.cjs: add type = "command" assertions to all migration
  tests that verify .command but were missing .type checks; buildNestedBlock
  injects type = "command" when the source body has no explicit type key, so
  every migrated handler must carry it per the Codex 0.124.0+ schema

138 tests pass, 0 fail.

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

* fix: CR round 3 + proactive audit — TOML quoting, stale AoT migration, strict validator

Three real issues from CodeRabbit round 3, plus the collateral improvements they
enable:

bin/install.js — tomlBareKey() helper (#2773 CR6a)
  buildNestedBlock interpolated the raw event name into [[hooks.${type}]] and
  [[hooks.${type}.hooks]] headers without TOML escaping. An event name containing
  spaces or punctuation (e.g. "Before Tool") would produce invalid TOML that
  parseTomlToObject would subsequently reject. Added tomlBareKey() — wraps the
  key in double-quoted TOML strings when it contains non-bare-key characters
  ([A-Za-z0-9_-]).

bin/install.js — staleNamespacedAotSections migration path (#2773 CR6b)
  migrateCodexHooksMapFormat handled [hooks.TYPE] (map-format) and flat [[hooks]]
  with event = "..." but ignored [[hooks.TYPE]] AoT entries that carried handler
  fields (command, type, timeout, statusMessage) at event-entry level without a
  nested [[hooks.TYPE.hooks]] sub-table. This is the pre-#2773 single-block shape
  that Codex 0.124.0+ rejects. Added staleNamespacedAotSections as the third
  migration category: detected by STALE_HANDLER_FIELD_PATTERN + absence of a
  [[hooks.TYPE.hooks]] sub-table in the same file; promoted to the two-level
  nested form by buildNestedBlock. Matcher-only entries (no handler fields) are
  intentionally skipped.

bin/install.js — validator now rejects event-level handler fields (#2773 CR6c)
  With migration covering the stale AoT shape, validateCodexConfigSchema can be
  strict: entries that have handler fields at event-entry level but no .hooks
  sub-array return ok: false instead of silently passing. Matcher-only entries
  (no handler fields and no .hooks) remain valid as event filters.

tests/codex-config.test.cjs — four new migration tests + missing type assertion
  Four tests cover the new stale AoT migration path: single-entry promotion,
  already-nested entry is left untouched (no double-wrap), multiple event types,
  and matcher-only entry is skipped. Added the missing type = "command" assertion
  to the CRLF migration test (the one miss from CR round 2).

tests/bug-2760-codex-install-defensive.test.cjs — strict .hooks-only collectors
  With stale AoT entries now migrated, the entry.command fallbacks in
  allSessionStartCommands and afterToolCommands are dead code. Replaced with
  strict entry.hooks-only collection guarded by an every(Array.isArray(e.hooks))
  pre-assertion, so any future regression that leaves handler fields at event
  level produces an explicit test failure rather than silently collecting them.

142 tests pass, 0 fail.

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

* fix: CR round 4 — segment-safe quoted-key detection + structural test assertions

bin/install.js — getTomlTableSections now exposes segments (#2773 CR7a)
  The staleNamespacedAotSections filter used section.path.split('.').length > 2
  to skip [[hooks.TYPE.hooks]] sub-table entries. That check misclassifies quoted
  event names containing dots: [[hooks."before.tool"]] has path hooks.before.tool
  (3 dot-parts) but only 2 true parsed segments, so it was incorrectly excluded
  from migration. Fixed by adding segments to the getTomlTableSections return
  shape (already available on record.tableHeader.segments) and replacing the
  split-based check with section.segments.length !== 2, which uses the true
  parsed key count regardless of dots inside quoted names.

tests/codex-config.test.cjs — replace raw-equality assertions (#2773 CR7b)
  The two new no-op migration tests (already-nested and matcher-only) used
  assert.strictEqual(result, content) — raw string equality that conflicts with
  the repo no-source-grep testing standard. Replaced with structural assertions
  using parseTomlToObject: the already-nested test verifies the handler stays
  under .hooks[0] and no double-wrap occurs; the matcher-only test verifies the
  matcher key is preserved and no .hooks sub-array is added.

142 tests pass, 0 fail.

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

* fix: CR round 5 — parseHooksBody key parser, empty-handler guard, segment-safe legacyMap filter, stronger test assertions

- parseHooksBody: replace /^([\w.]+)\s*=/ regex with parseTomlKey() so
  hyphenated keys (status-message) and quoted keys are not silently dropped
- buildNestedBlock: guard against handlerEntries.length === 0 — do not
  synthesise [[hooks.TYPE.hooks]] with type="command" but no command for
  matcher-only or otherwise handler-empty stale sections
- legacyMapSections filter: use section.segments.length === 2 (same fix
  applied to staleNamespacedAotSections in round 4) to prevent [hooks.X.Y]
  3-segment tables from being misclassified as event entries
- tests: add regression test for [[hooks."before.tool"]] quoted-dot event
  names; strengthen command path assertion to exact absolute path comparison

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:28:53 -04:00
Tom Boucher
b44482cf03 fix(#2760): defensive Codex install — strip legacy agents blocks, default hooks to AoT, validate post-write schema (#2785)
* fix(#2760): defensive Codex install — strip legacy agents blocks, default hooks to AoT, validate post-write schema

Three defects, three defensive fixes shipped together. Issue reporter
never returned with the requested diagnostic backup, but four additional
users have since confirmed the same Codex breakage and ZakAnun confirmed
manual cleanup is the only working workaround — defensive triple ships
without the original backup grep, justified by the corroborating reports.

Fix 1 (defect 3 — confirmed real). The Codex hooks emit path always
appended a top-level `[[hooks]]` AoT block, which collides with users
who already use the namespaced AoT form `[[hooks.SessionStart]]`. New
helper `hasUserNamespacedAotHooks()` detects the user's preferred shape
on parse and the install emits the GSD-managed hook in that same shape
when present. Default for fresh configs stays at top-level `[[hooks]]`
so status-quo behavior is preserved.

Fix 2 (defects 1+2 — defensive). `stripLeakedGsdCodexSections()` (the
install-time stripper) now always purges bare `[agents]` single-bracket
tables and `[[agents]]` sequence tables regardless of GSD marker
presence — both forms are invalid in current Codex schema and produce
"invalid type: ..., expected struct AgentsToml". Previously gated on
GSD-name lookup which missed marker-stripped configs and third-party
authored entries. The uninstall-time stripper (`stripCodexGsdAgentSections`)
keeps its old conservative behavior so user-authored entries survive
uninstall.

Fix 3 (defensive). Post-write schema validation parses the bytes about
to be committed and asserts no bare `[agents]`, no `[[agents]]`, and no
bare `[hooks.<Event>]` tables remain. On failure the install restores
the pre-install backup of config.toml and aborts loudly so the user is
never left with a Codex CLI that refuses to load. Pre-install snapshot
is captured before installCodexConfig runs (not after) so restore
returns the file to its true pre-GSD state.

Tests added (10 new, 1 updated):
  - bug-2760-codex-install-defensive.test.cjs (10 new tests across 4
    describes: hooks AoT preservation, strip robustness for both
    [agents] and [[agents]] without marker, schema validator behavior,
    abort+restore via test seam)
  - codex-config.test.cjs "case 2 ..." updated to reflect new defensive
    bare-[agents] purge

Full suite: 5747 pass / 0 fail.

Closes #2760

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

* fix(#2760): normalize Codex hooks emit field name across migration and managed paths

The migrateCodexHooksMapFormat path emitted `type = "<TYPE>"` for legacy
[hooks.TYPE] sections, while the GSD-managed Codex install emitted
`event = "SessionStart"` — same target [[hooks]] schema, two different
field names. Codex currently tolerates both via permissive parsing, but
the moment one path tightens this becomes a silent #2760-class regression.

Normalize both call sites on `event` (the existing GSD-managed
convention). Update migration emit, docstring, and existing migration
assertions to match. Add a parity regression test that drives both code
paths and asserts the [[hooks]] field key is identical.

* test(#2153): fix test isolation by building hooks/dist on demand

The "Codex install copies hook file (#2153)" regression depends on
hooks/dist/ being populated, but that directory is gitignored and only
built by `npm run build:hooks`. The npm pretest chain runs `build:sdk`
but not `build:hooks`, so when this file is run in isolation
(`node --test tests/codex-config.test.cjs`) the hook copy step skips
silently and the regression test fails on a stale-environment artifact
rather than a real bug.

Add a top-level before() hook that runs scripts/build-hooks.js when
hooks/dist/ is missing or empty. Matches the pattern already used by
bug-1834-sh-hooks-installed and other install integration tests, so the
suite passes regardless of runner ordering or which tests are targeted.

* fix(#2760): structural TOML validation, atomic writes, and behavioral test rewrites

Addresses CodeRabbit review on PR #2785 plus source-grep violations the
maintainer flagged in the regression test.

Fix 1 (CR 3149606220) — validateCodexConfigSchema now parses the TOML into
a structured object first via the new parseTomlToObject helper, then
runs schema-shape checks against both the parsed structure and the table
section headers. Malformed TOML with valid-looking headers no longer
slips past validation.

Fix 2 (CR 3149606224) — Replaced the four source-grep assertions in
tests/bug-2760-codex-install-defensive.test.cjs (lines 109, 125, 169,
201) with structural assertions against the parsed TOML object via the
exported parseTomlToObject helper. Tests now verify behavior (the file
parses and contains the expected structure) instead of literal byte
patterns. Robust to formatting changes — exactly what the regex-loosening
suggestion was reaching for, done correctly. Confirmed clean by
`npm run lint:tests` (0 violations).

Fix 3 (CR 3149606234) — The describe block that mutates
installModule.__codexSchemaValidator now runs with concurrency: false
so the test seam mutation cannot leak into sibling suites that also
call runCodexInstall.

Fix 4 (CR outside-diff) — Approach (b): atomic temp-file + renameSync.
Added atomicWriteFileSync helper used by mergeCodexConfig and the final
hooks-write. A mid-write failure leaves the .tmp-<pid>-<n> sibling
behind (cleaned up immediately) and never truncates the original
config.toml. Paired with try/catch wrapping around the entire
post-snapshot mutation sequence so any unexpected throw also triggers
restoreCodexSnapshot. Two layers of defense: atomic write prevents the
corruption window, snapshot restore handles non-atomic write paths.

Added behavioral test for fix 4: stubs fs.renameSync to throw on the
configPath rename, asserts the on-disk bytes match the pre-install
snapshot byte-for-byte, asserts the parsed structure is still the
user's [model] section (no half-written GSD agents block), and asserts
no stray .tmp-* files remain. Marked concurrency: false because it
monkey-patches a global.

Test results: 5749/5749 pass, 0 fail. lint:tests clean.

* test(#2760): TOML-parse based assertions for bare-agents purge and hook-field parity (CodeRabbit follow-up)

* fix(#2760): treat write failures as fatal, strip legacy hooks before guard, tighten TOML parser (CR4)

CR4 finding 1 (MAJOR) — Write failures silently succeeded. The inner catch
around atomicWriteFileSync restored the snapshot then re-threw, but the outer
catch only matched 'post-write Codex schema validation failed' and downgraded
everything else to a warn-and-continue. Install finished with "Done!" while
Codex had no GSD agents configured. Fix: wrap writeErr with a `post-write
Codex install failed:` prefix and broaden the outer guard to `.startsWith(
'post-write')` so both schema-validation and write failures abort install.

CR4 finding 2 (MAJOR) — Legacy flat [[hooks]] block prevented namespaced AoT
upgrade. The `!configContent.includes('gsd-check-update')` guard short-
circuited the new namespaced emit when an existing install had the legacy
flat [[hooks]] block, leaving users stuck in the mixed layout this fix is
designed to eliminate. Fix: strip ALL existing managed gsd-check-update
hook blocks (top-level [[hooks]] AND namespaced [[hooks.SessionStart]])
BEFORE evaluating the includes guard, so every install converges on the
right shape regardless of prior state.

CR4 finding 3 (MAJOR) — Homegrown TOML parser silently accepted malformed
input. parseTomlValue happily consumed the `0` prefix of `timeout = 0.5`
and parseTomlToObject did not verify the full RHS was consumed, so
`key = "x" junk` and date/time literals slipped through. Per CONTRIBUTING
("No external dependencies in core"), option (b) was chosen over adding
@iarna/toml: (a) parseTomlValue rejects any integer immediately followed
by `.`, `e`, `E`, `:`, `-`, `T`, or `Z` (floats / dates / times); (b)
parseTomlToObject scans from parsed.end to the next newline and throws
`trailing bytes after value` if anything other than whitespace + optional
`# comment` is present.

* test(#2760): add CR4 regression tests + scope GSD_TEST_MODE + rename rename-fault test

CR4 finding 5 (NIT) — GSD_TEST_MODE leak. Saved previous value, set '1' for
the require, then restored (delete if undefined). No more test-only env var
leaking to siblings in the same node process.

CR4 finding 4 (NIT) — Renamed the existing fix-4 test from 'fs.writeFileSync'
to 'fs.renameSync' (the only call actually faulted) and added a sibling test
that stubs fs.writeFileSync to throw on the .tmp- target — exercising the
pre-rename branch of atomicWriteFileSync that was previously untested. Both
serialize via concurrency: false on the existing describe block.

CR4 finding 1 (MAJOR test) — New behavioral test asserts install throws with
a `post-write Codex install failed` message AND never prints "Done!" when
the hook-block atomic rename fails. Captures stdout via console.log stub,
asserts byte equality of restored snapshot. Faults only the rename whose
temp source contains gsd-check-update so earlier mergeCodexConfig writes
are not collateral damage.

CR4 finding 2 (MAJOR test) — New TOML-parsed behavioral test for the
legacy-hook upgrade path: pre-install has [[hooks.SessionStart]] (user) +
legacy flat [[hooks]] managed gsd-check-update entry; post-install must
have hooks.SessionStart as Array-of-tables with both user hook and GSD
entry, and no top-level [[hooks]] AoT remaining. Also asserts exactly one
gsd-check-update entry (no duplicates).

CR4 finding 3 (MAJOR test) — parseTomlToObject regression suite: rejects
floats (timeout = 0.5), dates (created = 1979-05-27), trailing garbage
(key = "x" junk), and accepts trailing whitespace + # comment.

* fix(#2760): CR5 — pre-write fatal, TOML duplicate-key/header rejection, namespaced AoT migration

Address all five CodeRabbit round-5 findings on PR #2785:

Finding 1 (MAJOR) — Pre-write failures in the Codex hook configuration
catch (around bin/install.js:7002) used to fall through to console.warn
even though restoreCodexSnapshot() had already run. This produced "Done!"
output with no Codex hooks configured. Now wraps the original error with
a "(pre-write)" prefix and rethrows so install aborts loudly. Same defect
class as CR4 finding 1, different layer.

Finding 2 (MAJOR) — parseTomlToObject silently reused existing tables and
overwrote duplicate keys. Real TOML 1.0 rejects:
  - duplicate scalar key in same table ([a]\nx=1\nx=2)
  - re-declared [a] header (two [a] sections)
  - [[arr]] then [arr] for same path (shape mismatch)
Tracks pathShape, declaredHeaders, and per-table-instance key sets;
throws "duplicate or shape-mismatched table header at <path>" or
"duplicate key <name> in <path>".

Finding 3 (MAJOR) — migrateCodexHooksMapFormat used to emit flat
[[hooks]]\nevent="<TYPE>", which produced mixed flat+namespaced layouts
when the user already had [[hooks.<OTHER>]] entries. Now emits
[[hooks.<TYPE>]] directly (the namespace IS the event); managed-emit
detector hasUserNamespacedAotHooks fires correctly so the install
converges on a single namespaced layout regardless of pre-existing state.

Finding 4 (NIT) — tests/bug-2760-codex-install-defensive.test.cjs
rename-failure test tightened from "throw OR warn acceptable" to
assert.equal(threw, true), locking the contract Finding 1 establishes.

Finding 5 (NIT) — bug-2760 test suite snapshots and restores fs.renameSync
defensively in beforeEach/afterEach (symmetric with fs.writeFileSync),
removing the fragile per-test try/finally. Second test in the same
suite cleaned up to drop its try/finally.

Updates tests/codex-config.test.cjs to assert the new namespaced AoT
migration shape via parseTomlToObject (no source-grep). Existing field-
parity test reframed as shape-parity since both paths now emit
namespaced.

Tests: 5764 pass (+8 new). lint:tests: 0 violations.

* docs(#2760): add CHANGELOG entry for Codex install defensive triple

Adds the [Unreleased] Fixed entry for the Codex install fix landed in this
PR — defensive strip of legacy [agents]/[[agents]] blocks, namespaced AoT
hook detection across all events, atomic write + rollback, strict TOML
validation rejecting duplicate keys/repeated headers/trailing bytes.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:11:59 -04:00
Tom Boucher
936cf26706 fix(#2769): tolerate Requirements header with colon inside bold delimiters (#2782)
* fix(#2769): tolerate Requirements header with colon inside bold delimiters

extractReqIds in sdk/src/query/init.ts and the legacy init.cjs port only
matched `**Requirements**:` (colon outside bold), so phases declared with
the equally-valid markdown form `**Requirements:**` (colon inside bold,
which is what the project's own templates emit) returned phase_req_ids:
null for both `init plan-phase` and `init execute-phase`.

The mirror-image bug in `phase complete`'s REQUIREMENTS.md traceability
sweep at get-shit-done/bin/lib/phase.cjs:871 only matched the inside-bold
form, silently skipping the REQ-ID checkbox flips for any roadmap that
used the outside-bold form. Both parsers now share the same canonical
regex that accepts all three rendered-identical variants:

  **Requirements:**     (colon inside bold)
  **Requirements**:     (colon outside bold)
  **Requirements** :    (space before outside colon)

Tests:
- tests/init.test.cjs — parameterized over the three header variants for
  both init plan-phase and init execute-phase (6 new behavioral cases).
- sdk/src/query/init.test.ts — describe.each over the same variants
  exercising initPlanPhase through the SDK.
- tests/bug-2769-requirements-header-variants.test.cjs — phase complete
  flips REQ-001 in REQUIREMENTS.md across all three header variants.

Closes #2769

* refactor(#2769): centralize REQUIREMENTS_HEADER_RE constant per CodeRabbit
2026-04-27 12:31:49 -04:00
Tom Boucher
54e6da3126 fix(#2767): pass paths via --files to gsd-sdk query commit + lint guard (#2781)
* fix(#2767): pass paths via --files to gsd-sdk query commit + lint guard

Workflows, agents, commands, and references passed file paths positionally
to `gsd-sdk query commit`, which silently appended them to the commit
subject and triggered the `.planning/` wholesale-stage fallback in
sdk/src/query/commit.ts:136. Regression of #733/#798.

Inserted `--files` before the path list at every site (81 invocations
across 50 files). Added tests/bug-2767-gsd-sdk-commit-files-flag.test.cjs
as a permanent lint that scans every shipped .md file and asserts each
`gsd-sdk query commit[-to-subrepo]` invocation either uses `--files` or
carries no path arguments.

Closes #2767

* test(#2767): replace source-grep with behavioral SDK test

The original test walked every shipped .md file and regex-tokenized
`gsd-sdk query commit` invocations to assert `--files` was present.
CONTRIBUTING.md prohibits this source-grep pattern.

Rewrite as behavioral SDK tests against `sdk/dist/cli.js` over a real
tmp git project (createTempGitProject helper). Cover both the
well-formed (`--files <paths>`) form — clean subject, exactly-staged
files, .planning/ left untouched — and the buggy positional form,
asserting the documented misbehavior (paths leak into subject + the
`.planning/` wholesale-stage fallback at commit.ts:136). Also asserts
`commit-to-subrepo` rejects when `--files` is omitted (commit.ts:258).

The doc-lint is retained as a supplementary defense-in-depth guard
since agent-prompt markdown invocations cannot be exercised end-to-end
— but it is no longer the primary contract.

* docs(#2767): correct contradictory --files guidance in zh-CN/en docs + fix test docstring
2026-04-27 12:31:43 -04:00
Tom Boucher
3ac3a2ae70 fix(#2770): coerce non-string depends_on YAML values to preserve dependencies (#2780)
* fix(#2770): coerce non-string truths to preserve cross-cutting constraints

`cmdRoadmapAnnotateDependencies` skipped non-string truth entries via
`if (typeof t !== 'string') continue`. That avoided the TypeError reported
in #2770 but silently dropped legitimate constraints — numeric YAML scalars
(`- 3`) and kv-shaped truths from parseMustHavesBlock's continuation-kv
path (#2757) — from the cross-cutting analysis, leaving ROADMAP.md
under-annotated.

Replace the skip-guard with a `coerceTruthToString` helper that:
  * passes strings through
  * `String()`-coerces numbers, booleans, bigints
  * extracts a string field (title, text, name, rule, path, provides) from
    object-shaped items

Composes cleanly with #2757 (objects from kv continuation lines now
contribute their title rather than being dropped) and the existing
`splitInlineArray` quote-aware parser.

Tests: tests/bug-2770-annotate-deps-int-coerce.test.cjs
  - numeric scalar truth shared across plans surfaces as constraint
  - kv-shaped truth surfaces via title field
  - bare-int depends_on regression guards on extractFrontmatter

Full suite: 5678 pass, 0 fail.

Closes #2770

* test(#2770): use array join() for multi-line fixtures per CONTRIBUTING

* refactor(#2770): cache trim() and avoid no-op truthCounts.set in aggregation
2026-04-27 12:31:38 -04:00
Tom Boucher
8b6c44433f fix(#2772): only disable worktree isolation when planned paths touch submodules (#2779)
* fix(#2772): only disable worktree isolation when planned paths touch submodules

The previous guard in execute-phase.md and quick.md unconditionally set
USE_WORKTREES=false whenever .gitmodules existed, penalising every plan in
a submodule project even when no plan touched a submodule path.

Replace with submodule-path parsing + per-plan path intersection:

- Parse SUBMODULE_PATHS once from .gitmodules via
  `git config --file .gitmodules --get-regexp '^submodule\..*\.path$'`.
- In execute-phase.md, intersect SUBMODULE_PATHS with each plan's
  files_modified frontmatter; disable worktree isolation only for plans
  with non-empty intersection. Fall back to safe-disable for that plan
  when files_modified is missing/unparseable, with a log line explaining
  why.
- In quick.md (no pre-declared paths), keep submodule-path parsing and
  document a fail-loud commit-time guard so the executor aborts only when
  it actually stages a submodule path.

Add tests/bug-2772-gitmodules-path-intersection.test.cjs covering both
files: no unconditional disable, submodule paths are parsed, intersection
logic exists in execute-phase, fallback path is documented.

Full suite: 5680 / 5680 pass.

Closes #2772

* test(#2772): replace source-grep with behavioral test of submodule path intersection

* fix(#2772): wire USE_WORKTREES_FOR_PLAN into dispatch + fix glob matcher + add quick.md commit guard

Address CodeRabbit review on PR #2779 — the original fix computed
USE_WORKTREES_FOR_PLAN but never read it, so the per-plan submodule
intersection was dead code. Dispatch sites still branched on the
project-level USE_WORKTREES.

Changes:

1. execute-phase.md (CRITICAL — dispatch wiring): Move per-plan
   computation into execute_waves as sub-step 2.5, run it for each plan
   before its dispatch, and gate all four dispatch sites on
   USE_WORKTREES_FOR_PLAN: worktree-mode header, sequential-mode header,
   "worktrees disabled" sequential rule, and post-wave cleanup. Document
   PLAN_FILES extraction via jq from the phase-plan-index JSON. Track
   WAVE_WORKTREE_PLANS so post-wave cleanup only runs when at least one
   plan in the wave actually used worktrees.

2. Per-plan gate matcher (MAJOR — glob safety): Strip leading "./" and
   trailing "/" from both submodule and planned paths. Match
   bidirectionally (pf inside sm AND sm inside pf). Handle globby
   planned paths like "vendor/**/*.c" by extracting the literal prefix
   before the first glob metachar and re-checking. Wrap the iteration
   in set -f / set +f so glob expansion does not corrupt patterns.
   Extracted the gate (~92 lines) into
   workflows/execute-phase/steps/per-plan-worktree-gate.md to keep
   execute-phase.md under the 1700-line XL budget.

3. quick.md (CRITICAL — fail-loud guard): Inject SUBMODULE_PATHS into
   the executor Task prompt and add a <submodule_commit_guard> bash
   block the executor must run before every git commit. The guard
   inspects staged paths via `git diff --cached --name-only`, normalizes
   paths, and aborts with a clear ABORT message + recovery instruction
   ("re-run with workflow.use_worktrees=false") when any staged path
   falls inside a submodule.

4. tests/bug-2772-gitmodules-path-intersection.test.cjs: 25 tests total.
   Updated GATE_SNIPPET to match the new bash matcher. Added
   normalization tests (./ prefix, trailing /, glob "vendor/**/*.c",
   parent directory, ./ in .gitmodules). Added workflow-markdown
   wiring assertions for all 4 dispatch sites + per-plan gate file
   extraction. Added quick.md guard tests: prompt injection assertion +
   behavioral fixture-repo tests that stage a submodule path and assert
   the guard exits non-zero with the ABORT message.

Test count: 5701 pass / 0 fail (was 5698/1 before).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:31:32 -04:00
Tom Boucher
77d929429f fix(#2774): inclusion-based worktree cleanup to protect workspace .git (#2778)
* fix(#2774): inclusion-based worktree cleanup to protect workspace .git

The cleanup blocks in execute-phase.md and quick.md used an exclusion
filter (`grep -v "$(pwd)$"`) to skip the current worktree before calling
`git worktree remove --force` on everything else. The exclusion fails
whenever the current workspace is itself a worktree of an upstream repo:

- multi-workspace setups where `git worktree list` reports the registry
  path as a different absolute path than `$(pwd)`
- the cross-drive Windows case where the registry reports `E:/...` while
  `$(pwd)` resolves to `C:/...` — the equality test never holds, every
  other worktree (including the workspace itself) is removed, and the
  workspace's `.git` pointer file is destroyed.

Switches both cleanup blocks to an inclusion-based filter that targets
only agent-spawned worktrees under `.claude/worktrees/agent-`, the
namespace Claude Code's `isolation="worktree"` always uses for executor
worktrees. The workspace path can never collide with that prefix.

Adds tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs covering:
- both workflow files use the inclusion filter
- neither falls back to the broken `grep -v "$(pwd)$"` guard
- end-to-end simulation of porcelain output with workspace + agent
  worktrees yields only the agent worktree

Closes #2774

* test(#2774): replace source-grep with behavioral test of cleanup pipeline

* fix(#2774): whitespace-safe worktree iteration with while/read

CodeRabbit review on PR #2778 flagged that `for WT in $WORKTREES` splits
on whitespace. Any agent worktree path containing a space (e.g. a workspace
under '/Users/dev/My Workspace/') would be torn into broken half-paths,
`git -C` would fail on each fragment, and the executor branch would never
be deleted.

Switch both cleanup blocks (quick.md and execute-phase.md) to:

  while IFS= read -r WT; do
    [ -z "$WT" ] && continue
    ...
  done < <(git worktree list --porcelain | grep ... | sed ...)

Process substitution feeds the pipeline output line-by-line — IFS= and -r
preserve every byte of the path including embedded spaces.

Also rename the misleading `makeBareTempGitRepo` helper to
`makeTempUpstreamRepo` (it does not pass --bare; it inits a normal repo
with an initial commit so worktree-add works).

Add two new behavioral tests:
- discovery pipeline yields whitespace paths intact on a single line
- the actual while/read loop iterates each whitespace-bearing path
  exactly once (would fail with the previous `for WT in` form)

Tests: 5681 pass, 0 fail.
2026-04-27 12:31:26 -04:00
Tom Boucher
6a293cfc2a fix(#2775): verify gsd-sdk on PATH before reporting SDK ready (#2777)
* fix(#2775): verify gsd-sdk on PATH before reporting SDK ready

`npx get-shit-done-cc@latest` printed `✓ GSD SDK ready` even though
`gsd-sdk` was not callable. Root cause: npx only links the package's
primary bin (`get-shit-done-cc`); secondary bins like `gsd-sdk` are not
materialized into a PATH directory. The installer asserted the weaker
invariant "sdk/dist/cli.js exists on disk" and treated it as proof of
the stronger invariant "command -v gsd-sdk resolves" — they aren't the
same.

Fix tightens the gate in installSdkIfNeeded:

  1. After confirming the dist is present, walk PATH for an executable
     `gsd-sdk` shim (isGsdSdkOnPath, no spawn).
  2. If absent, attempt to materialize the shim via symlink at
     `~/.local/bin/gsd-sdk` (or the first HOME-rooted PATH dir we can
     write to), falling back to a copy on filesystems that reject
     symlinks (trySelfLinkGsdSdk).
  3. Re-probe PATH after linking. Only print `✓ GSD SDK ready` when the
     probe succeeds; otherwise emit a clear ⚠ + remediation.

Also strips the misleading "or `npx get-shit-done-cc`" clause from the
shim header (it never linked the secondary bin).

Closes #2775

* test(#2775): use centralized helpers from helpers.cjs per CONTRIBUTING

* fix(#2775): wrapper script in symlink fallback to preserve __dirname resolution

CodeRabbit follow-up on PR #2777. The previous symlink-fallback in
trySelfLinkGsdSdk used fs.copyFileSync(shimSrc, target), but
bin/gsd-sdk.js resolves the CLI via path.resolve(__dirname, '..',
'sdk', 'dist', 'cli.js'). After a copy, __dirname becomes the link
directory (e.g. ~/.local/bin), so the resolved CLI path was broken
(~/.local/sdk/dist/cli.js) — and isGsdSdkOnPath() only checked file
existence + execute bit, so the success line still printed over a
broken install.

Replace the copy with a tiny wrapper script that require()s the real
shim by absolute path. This preserves __dirname inside bin/gsd-sdk.js
because the require runs against shimSrc's own location.

Also fixes the PATH restoration nit in the regression test (was
coercing undefined to the string "undefined" if PATH was unset).

Adds a behavioral fallback test that mocks fs.symlinkSync to throw,
exercises the fallback path, and asserts the resulting target is a
require()-wrapper (not a verbatim copy) and is executable.

* fix(#2775): PATH-backed dir ordering + tighten captureConsole + drop tautological assertion (CodeRabbit follow-up)
2026-04-27 12:31:21 -04:00
Tom Boucher
290c8b2909 fix(#2771): unify user-owned-artifacts list to suppress false patches warning (#2776)
* fix(#2771): unify user-owned-artifacts list to suppress false patches warning

USER-PROFILE.md was both preserved across reinstalls (correctly) AND tracked in
gsd-file-manifest.json (incorrectly). On the next install, saveLocalPatches()
hashed the on-disk file, found it differed from the stale manifest hash (because
/gsd-profile-user --refresh regenerated it), and reported it as a "locally
modified GSD file" — a spurious warning every time the profile refreshed.

A file is either distribution (manifest-tracked, diff'd against manifest) or
user artifact (preserved across installs, never diff'd). Never both. This
extracts USER_OWNED_ARTIFACTS as a single source of truth, referenced by both
the preserveUserArtifacts call site and writeManifest, so the invariant cannot
drift again.

Adds a regression test that exercises the full reproduction path: install,
create USER-PROFILE.md, reinstall, refresh USER-PROFILE.md, reinstall, assert
no patch backup and no warning text.

Closes #2771

* test(#2771): use centralized helpers from helpers.cjs per CONTRIBUTING

* fix(#2771): normalize legacy USER_OWNED_ARTIFACTS entries from manifest + tighten test
2026-04-27 12:31:15 -04:00
Tom Boucher
dc9b712967 refactor(state): drop unused args + lift currentPhase in cmdStateCompletePhase (#2761)
* refactor(state): drop unused args param and lift currentPhase in cmdStateCompletePhase

Two cleanup items surfaced by CodeRabbit review of PR #2759:

1. cmdStateCompletePhase(cwd, args, raw) — args is never read inside the
   function. All sibling state subcommands use the leaner (cwd, raw) shape.
   Remove the unused parameter and update the dispatch call in gsd-tools.cjs.

2. output() at line 1754 called fs.readFileSync(statePath) after
   readModifyWriteStateMd had already released the lock, re-extracting
   Current Phase via an extra fs read. The closure already computed
   currentPhase at line 1704; lifting resolvedPhase into outer scope and
   capturing it in the callback eliminates the post-lock read and closes the
   small race window.

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

* test(#2761): apply CodeRabbit nitpicks with regression tests

Two CodeRabbit nitpicks from PR #2761 review, each landed with a
regression test so a future refactor can't unwind them.

1. tests/dispatcher.test.cjs — pin the enumerated subcommand list:
   the 'state unknown subcommand errors' test now also asserts that
   the dispatcher's error string includes 'complete-phase'. Without
   this, a future reformat of the available-subcommands enumeration
   could silently drop entries and the existing
   'Unknown state subcommand' substring check would still pass.

2. get-shit-done/bin/lib/state.cjs — tighten the Phase fallback in
   cmdStateCompletePhase: when STATE.md is missing the canonical
   '**Current Phase:**' field and the only phase signal is the
   decorated body line under '## Current Position' (e.g.
   'Phase: 01 (Foo) — EXECUTING'), the previous fallback returned
   the entire decorated string, producing messy downstream output:
     Status: Phase 01 (Foo) — EXECUTING complete
     Phase: 01 (Foo) — EXECUTING — COMPLETE
   The fallback now strips everything past the leading
   numeric/decimal token via /^\\s*([\\w.-]+)/ so degraded inputs
   produce clean output identical to the canonical path.

3. tests/state.test.cjs — two new tests in a dedicated describe block:
   - decorated Phase line writes clean Phase identifier
   - canonical Current Phase wins over Current Position decoration
   Both run real `gsd state complete-phase` against synthetic
   STATE.md fixtures and assert on the rendered Status field.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 09:03:36 -04:00
Tom Boucher
9472f343db feat(#2762): --minimal install profile (≥94% cold-start token reduction) (#2764)
* feat(#2762): add --minimal install profile to cut cold-start token cost

Eager system-prompt load from 86 gsd-* skill descriptions plus 33
subagent descriptions costs ~12k tokens per turn even in directories
with no .planning/. Frontier models (Sonnet 4.6 / Opus 4.7) with 200K-1M
context don't feel it; local LLMs with 32K-128K do.

--minimal (alias --core-only) installs only the main GSD loop:
new-project, discuss-phase, plan-phase, execute-phase, plus help/update.
Zero gsd-* subagents are written. Re-running gsd update without
--minimal expands to the full surface. Default install behavior is
unchanged.

DRY: a single stageSkillsForMode() helper filters the source dir; all
13 runtime-specific copy fns are unchanged because they recurse the
staged dir. Allowlist + helpers live in get-shit-done/bin/lib/install-
profiles.cjs as the single source of truth.

Manifest now records mode: 'minimal' | 'full' so future commands can
detect install profile.

Tested end-to-end: --minimal yields 6 skill folders + 0 agents; default
yields 86 + 33 (unchanged).

* docs(#2762): document --minimal install in README

Adds a collapsible 'Minimal Install' section under Getting Started
covering: who it's for (local LLMs, token-billed APIs), what you get
(6 skills, 0 subagents, ~700 token floor vs ~12k), and the critical
caveat that re-installing without --minimal restores the full surface
and erases the savings. Includes a comparison table, the manifest
inspection one-liner, and the use-case decision matrix.

* fix(#2762): address CodeRabbit review + CI failures

CodeRabbit findings:
1. Temp dir leak (Minor): stageSkillsForMode created tmp dirs that were
   never cleaned up. Added a module-level Set tracking every staged dir
   plus a process.on('exit') handler that rm -rf's them. Also wrap the
   copy loop in try/catch to remove a partially-populated tmp dir on
   mid-flight failure. Verified end-to-end: 0 leaked dirs in /tmp after
   a real install.

2. Codex full -> minimal stale state (Major): a previous full Codex
   install left agents/gsd-*.toml files plus [agents.gsd-*] sections in
   config.toml. The original cleanup only removed .md files, so a switch
   to --minimal would leave Codex still advertising the full agent
   surface. Cleanup now also handles .toml under isCodex, and minimal
   mode strips GSD sections from config.toml via the existing
   stripGsdFromCodexConfig helper (same path used by --uninstall).

3. Nitpick — Codex downgrade regression test: added a spawnSync-based
   end-to-end test that fakes a previous full install (stale gsd-*.md +
   gsd-*.toml + GSD-marked config.toml + a user-owned agent/setting),
   runs install.js --codex --minimal, and asserts stale GSD files +
   sections are gone while user content is preserved.

CI failures (inventory parity):
- docs/INVENTORY.md CLI Modules table now lists install-profiles.cjs
  with the correct headline count (30 -> 31).
- docs/INVENTORY-MANIFEST.json regenerated via gen-inventory-manifest.cjs.

Test count: 149 pass (was 116 in last commit; +14 new install-minimal +
all previously-failing inventory tests now green).

* test(#2762): expand install-minimal test coverage for future-proofing

Each new test pins a specific guarantee that closes off a future
regression class — turning every CodeRabbit finding (including the
nitpicky one) into a permanent guard.

cleanupStagedSkills suite (+3 tests):
- 'full mode does not register a staged dir' — catches a future
  regression where someone forgets the early-return in stageSkillsForMode
  and starts polluting STAGED_DIRS in default installs.
- 'exit handler registers exactly once across many calls' — catches
  removal of the exitHandlerRegistered guard. install.js has 13
  dispatch sites, so a missing guard would attach 13 listeners.
- 'mid-copy failure removes partial staged dir and re-throws' —
  intercepts fs.copyFileSync to throw mid-loop and asserts the staged
  dir count in /tmp is unchanged after the throw. Pins the exact
  CodeRabbit-flagged leak.

Claude full -> minimal downgrade (+1 test):
- Mirrors the Codex downgrade test for the .md-only path that the
  other 12 runtimes share. Asserts user-owned agents are preserved.

Manifest mode round-trip (+3 tests):
- Default install -> mode: 'full' with >6 skills and >0 agents
- --minimal -> mode: 'minimal' with exactly 6 skills and 0 agents
- --core-only alias produces identical manifest to --minimal

Allowlist scope guards (+3 tests):
- Every main-loop command IS in allowlist (positive)
- Off-loop commands (autonomous, ship, do, progress, next, fast,
  quick, debug, code-review, verify-work) are NOT (guards against
  silent scope creep — future contributor adds 'autonomous' to core
  and the floor erodes)
- Unknown mode strings fall through to full behavior — pre-emptive
  guard for future 'compact'/'tier2' modes that might forget to
  update the predicate.

Total: 25 tests in this file (was 15), 159/159 passing across the
install + inventory suites.

* fix(#2762): clean up staged tmp dirs on SIGINT/SIGTERM/SIGHUP

CodeRabbit follow-up review on c727bf5f flagged that process.on('exit')
does not fire on signal-driven termination. An installer is exactly
the kind of process users abort mid-run with Ctrl+C, so without
explicit signal handlers the staged tmp dirs in STAGED_DIRS would be
left behind until the OS reaps tmpdir.

Fix: ensureExitCleanup now also registers process.once handlers for
SIGINT, SIGTERM, SIGHUP. Each handler runs cleanupStagedSkills then
re-raises the same signal via process.kill(pid, sig) so the OS-default
handler takes over and the parent shell sees the correct exit code
(130 for SIGINT, etc.) — CI scripts and interactive users see the
abort the way they expect.

Test: spawns a child that stages a tmp dir then blocks; parent
captures the staged path from stdout, sends SIGINT, asserts (a) the
staged dir is gone after child exit, (b) child exits via the signal
not via code 0. Skipped on Windows (signal semantics differ; the
natural-exit cleanup test covers the Windows CI matrix).

Total: 26 tests in install-minimal.test.cjs (was 25).
2026-04-27 00:13:20 -04:00
Tom Boucher
ab5ad6c8bc fix(#2757): unquoted truths with colons crash annotate-dependencies (#2759)
parseMustHavesBlock dispatched on `includes(':')` to detect key-value pairs,
but unquoted YAML strings like `GET /foo/:id resolves...` and
`Class::Method is idempotent` also contain colons. When the KV regex failed
to match, `current` was left as `{}` (the empty object initialized before
the branch), which then caused `t.trim()` in roadmap.cjs to throw
`TypeError: t.trim is not a function`.

Two fixes:
- frontmatter.cjs: tighten the KV regex to require at least one space after
  the colon (`\s+` instead of `\s*`), matching YAML convention. When the
  regex still fails to match, fall back to treating the item as a plain
  string instead of leaving `current` as `{}`.
- roadmap.cjs: add `typeof t !== 'string'` guard before `.trim()` as a
  cheap safety net against any future parser anomaly.

Closes #2757

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:40:38 -04:00
Tom Boucher
1405728292 fix: parseMustHavesBlock quoted strings + gsd state complete-phase (#2744)
* fix: parseMustHavesBlock quoted strings + gsd state complete-phase

Bug #2734: parseMustHavesBlock dropped quoted truths containing ':' because
fully-quoted strings like `"App-side UUIDv4: generated locally"` fell into
the kv-parse branch, the regex failed (value starts with '"'), and current
stayed as empty {}. Fix: detect fully-quoted strings before the ':' check
and extract them directly. Two regression tests added to frontmatter.test.cjs.

Bug #2735: `gsd state complete-phase` subcommand was missing — unknown
subcommands fell through to cmdStateLoad. Added cmdStateCompletePhase to
state.cjs (updates Status, Last Activity, and Current Position to COMPLETE),
exported it, and wired it into the case 'state': dispatch in gsd-tools.cjs.

Closes #2734
Closes #2735

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

* fix(state): unknown subcommand returns explicit error instead of silent fallthrough

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:43 -04:00
Tom Boucher
3246810876 feat: extend RUNTIME_PROFILE_MAP to gemini/qwen/opencode/copilot + settings-advanced UI (#2754)
Closes #2612

- Add gemini, qwen, opencode, and copilot entries to RUNTIME_PROFILE_MAP in core.cjs
- Group B runtimes (kilo, cline, cursor, windsurf, augment, trae, codebuddy, antigravity) intentionally have no built-in map and fall through to the existing unknown-runtime fallback
- Add 40 new tests to tests/issue-2517-runtime-aware-profiles.test.cjs covering each new runtime's three tiers, Group B fall-through, and partial override merge semantics
- Add Section 7 "Runtime Model Tiers" to settings-advanced.md with interactive UI to view and override built-in tier defaults per runtime
- Update docs/CONFIGURATION.md built-in tier table to include all four new runtimes

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:33:54 -04:00
Tom Boucher
e0b4561fa9 feat: add /gsd-edit-phase command to modify roadmap phases in place (#2753)
Adds a new slash command that lets developers modify any field of an
existing phase in ROADMAP.md without affecting phase number or position.

- commands/gsd/edit-phase.md: command file with --force flag support
- get-shit-done/workflows/edit-phase.md: full workflow with status guard,
  depends_on validation, diff+confirmation, and STATE.md update
- tests/edit-phase.test.cjs: 32 tests covering all acceptance criteria
- docs/INVENTORY.md, INVENTORY-MANIFEST.json, COMMANDS.md: registered

Closes #2617

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:33:52 -04:00
Tom Boucher
bf73cbe1a4 test(#2688): add review.models.claude tests for per-runtime review model config (#2748)
Adds two tests to review-model-config.test.cjs:
- isValidConfigKey accepts review.models.claude (schema validation)
- round-trip: config-set then config-get for review.models.claude

The dynamic key pattern (^review\.models\.[a-zA-Z0-9_-]+$), the workflow
model-read logic in review.md, and the CONFIGURATION.md docs were already
in place. Only the claude-specific test coverage was missing.

Closes #2688

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:33:45 -04:00
Tom Boucher
d5cd64dde5 fix(#2637): migrate legacy Codex [hooks] map format to [[hooks]] array on install (#2747)
Codex 0.124.0 changed the required config.toml hooks format from the old
map-style ([hooks.shell]) to array-of-tables ([[hooks]]). Old GSD installs
that wrote the legacy format now cause a startup parse error on upgrade.

Add migrateCodexHooksMapFormat() which detects non-array [hooks] and
[hooks.TYPE] sections and rewrites them to [[hooks]] entries with an
injected type = "TYPE" key. The migration runs at the start of every Codex
install so affected configs self-heal on the next `gsd install --codex`.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:33:43 -04:00
Tom Boucher
2b95ccbddd Merge pull request #2743 from gsd-build/fix/2714-workstream-config-inheritance
feat(config): workstream config.json inherits from root .planning/config.json
2026-04-26 11:49:30 -04:00
Tom Boucher
80f4c9063f Merge pull request #2741 from gsd-build/fix/2727-codex-agents-toml-format
fix(installer): revert Codex agents section to [agents.<name>] struct format
2026-04-26 11:49:19 -04:00
Tom Boucher
0eef943f0a Merge pull request #2739 from gsd-build/fix/2732-graphify-cli-invocation
fix(graphify): update CLI invocation from legacy flag form to subcommand
2026-04-26 11:49:09 -04:00
Tom Boucher
cb149383c1 fix(config): bump MODEL_ALIAS_MAP to claude-opus-4-7 (#2733)
* fix(config): bump MODEL_ALIAS_MAP and RUNTIME_PROFILE_MAP to claude-opus-4-7

Opus 4.7 shipped Q1 2026 but MODEL_ALIAS_MAP and RUNTIME_PROFILE_MAP.claude.opus
were still pinned to claude-opus-4-6. Users with resolve_model_ids: true received
stale model IDs in logs and agent-tool calls.

Also adds a resolve_model_ids: true test suite — this path had zero coverage,
which is why the stale ID survived undetected.

Closes #2712

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

* refactor(config): derive RUNTIME_PROFILE_MAP.claude from MODEL_ALIAS_MAP (coderabbit)

RUNTIME_PROFILE_MAP.claude was duplicating model IDs that MODEL_ALIAS_MAP
already owns. Future model bumps now only require updating MODEL_ALIAS_MAP.
Also fixes stale test assertion (claude-opus-4-6 → claude-opus-4-7).

* fix(tests): update stale claude-opus-4-6 refs to claude-opus-4-7; DRY: derive RUNTIME_PROFILE_MAP.claude from MODEL_ALIAS_MAP

- Update 3 hardcoded `claude-opus-4-6` assertions in tests/issue-2517-runtime-aware-profiles.test.cjs to `claude-opus-4-7`
- Update comment on line 128 that referenced the old model ID
- Replace manual per-tier expansion of RUNTIME_PROFILE_MAP.claude with Object.fromEntries so future alias bumps only require updating MODEL_ALIAS_MAP

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 11:48:47 -04:00
Tom Boucher
022b577922 test(graphify): tighten version-check assertions per CodeRabbit nitpick
- Success path: add explicit python3Calls.length === 0 assertion so "no
  fallback" is stated directly rather than implied by calls.length === 1
- Fallback path: add explicit calls[0].cmd === 'graphify' assertion so
  "graphify precedes python3" is verified by name, not just argument
2026-04-26 11:36:14 -04:00
Tom Boucher
7c08a155ea test(graphify): tighten call-sequence assertions per coderabbit review
Adds explicit call-count and ordering assertions to version-check tests:
- Success path: exactly 1 spawnSync (graphify --version only, no python fallback)
- Failure path: graphify --version attempted first, python3 fallback second

Addresses coderabbit nitpick on PR #2739.
2026-04-26 11:21:54 -04:00
Tom Boucher
1cb4bebcf5 feat(config): workstream config.json inherits from root .planning/config.json
- Add _deepMergeConfig() with correct null-override semantics
- loadConfig() reads root config.json when GSD_WORKSTREAM is set, then
  deep-merges with workstream config (workstream wins on conflict)
- Workstream without config.json falls back to root config entirely
- Migrations and disk writes operate on fileData (on-disk content) only,
  never on the merged result, to prevent workstream pollution
- Fixes null-override bug from PR #2717: explicit null in workstream now
  correctly overrides root value instead of falling back to root
- Tests: inherit root model_overrides, workstream override, nested
  workflow.* deep merge, explicit null override, missing workstream config

Closes #2714
2026-04-26 11:15:16 -04:00
Tom Boucher
8f7f43abaa test(installer): add failing test for [agents.<name>] struct format (Codex 0.124.0+)
Adds test asserting generateCodexConfigBlock emits [agents.<name>] struct
format, not [[agents]] sequence format. Confirms RED phase for #2727 fix.
2026-04-26 10:35:23 -04:00
Tom Boucher
f89a56eb55 fix(graphify): update CLI invocation from legacy flag form to subcommand
graphify . --update was removed in favor of graphify update . in v0.4.x.
Also improves version detection to try `graphify --version` before
falling back to python3 importlib query.

Closes #2732
2026-04-26 10:20:41 -04:00
Tom Boucher
470c1a0bff fix(#2722): forensics gh commands pin --repo gsd-build/get-shit-done (#2723)
* fix(#2722): forensics gh commands pin --repo gsd-build/get-shit-done

gh issue create and gh label list both defaulted to the repo inferred
from $PWD, causing issues to be submitted to the user's current project
instead of this repo. Added --repo gsd-build/get-shit-done to both
commands. Added two regression tests covering both gh calls.

Closes #2722

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

* fix(#2723): scope forensics tests to specific gh commands, not whole file

CodeRabbit found that the gh issue create test searched the whole
workflow file, so it would pass even if gh issue create lacked --repo
(because gh label list already contains the repo string elsewhere).
Also replaced the brittle 200-char slice in the label-list test with
a regex. Both tests now use assert.match() with command-scoped regexes.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:47:55 -04:00
Tom Boucher
caf6974bbf fix(ci): remove stale SDK-variant tests for files deleted in 377a6d2 (#2725)
377a6d2 deleted sdk/prompts/agents/ and sdk/prompts/workflows/ (13 files)
but did not update 3 test files that reference them, causing ENOENT
failures on every CI run (main and all PRs) since that commit.

Removed:
- sdk/prompts/agents variants describe block (enh-2427-sycophancy-hardening)
- PLAN_PHASE_SDK_PATH constant and headless plan-phase test (post-planning-gaps-2493)
- sdk/prompts/workflows/verify-phase.md describe block (verifier-deferred-items)

The underlying behaviour is covered by the existing main agent/workflow
tests; the SDK variant tests are moot now that the SDK loads installed
files instead of bundled stripped-down copies.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:26:15 -04:00
Tom Boucher
1068223439 feat(#2500): enrich gsd-codebase-mapper arch-focus ARCHITECTURE.md with ASCII diagrams, data flow traces, and constraints (#2715)
* feat(#2500): enrich gsd-codebase-mapper arch-focus ARCHITECTURE.md template

The codebase mapper's arch-focus template was a sparse structural inventory.
After major refactors, the research/ARCHITECTURE.md (created at /gsd-new-project
and never refreshable) went stale while the refreshable codebase version lacked
the visual richness that makes architecture docs useful for planning.

Add to the ARCHITECTURE.md template:
- <!-- refreshed: {date} --> marker at the top (maintainer request)
- ASCII system overview diagram with component boxes and flow arrows
- Component responsibility table (Component / Responsibility / File)
- Primary request path traces with numbered steps and code references
- Architectural constraints section (threading, global state, circular imports)
- Anti-patterns section with codebase-specific patterns and correct alternatives

All existing sections (Pattern Overview, Layers, Key Abstractions, Entry Points,
Error Handling, Cross-Cutting Concerns) are preserved.

7 new tests in tests/enh-2500-codebase-mapper-arch-rich-format.test.cjs verify
each required section is present in the deployed template.

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

* fix(#2500): resolve CodeRabbit review findings

- Add 'text' language tag to bare ASCII diagram fenced block (markdownlint MD040)
- Tighten data flow test: require '### Primary Request Path' heading, 3+
  numbered steps, and file:line reference pattern — prevents loose-match
  false positives
- Tighten constraints test: require '## Architectural Constraints' heading
  AND Threading / Global state / Circular imports tokens — prevents broad
  keyword matches masking regressions

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:23:40 -04:00
Tom Boucher
b40110111d feat(#2306): plan-review-convergence v2 — CYCLE_SUMMARY contract, config gate, local model reviewers (#2718)
* feat(#2306): plan-review-convergence v2 — CYCLE_SUMMARY contract, config gate, local model reviewers

Fixes the false-stall detection bug in the plan→review→replan convergence
loop. REVIEWS.md accumulates history across cycles so raw grep inflated
HIGH counts; HIGH count now comes from a per-cycle CYCLE_SUMMARY contract
emitted in the review agent's return message.

Key changes:
- workflow.plan_review_convergence config gate (disabled by default, same
  pattern as workflow.code_review / workflow.nyquist_validation)
- Review agent prompt defines CYCLE_SUMMARY: current_high=<N> contract with
  PARTIALLY RESOLVED / FULLY RESOLVED counting rules
- Orchestrator aborts on absent/malformed CYCLE_SUMMARY (distinguishes both)
- Warns when HIGH_COUNT > 0 but ## Current HIGH Concerns section is missing
- Stall detection and --ws forwarding preserved and tested
- Local model reviewers: --ollama, --lm-studio, --llama-cpp flags added to
  convergence workflow and review workflow; all three use OpenAI-compatible
  /v1/chat/completions endpoint with jq --rawfile for safe JSON encoding
- review.ollama_host / review.lm_studio_host / review.llama_cpp_host config
  keys registered and documented (default to localhost:11434/1234/8080)
- review.models.ollama / .lm_studio / .llama_cpp model-name config support
- 58 tests (up from 29 in PR #2339), all passing

Closes #2306
Closes #2339
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>

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

* fix(ci): sync sdk/src/query/config-schema.ts with CJS schema (#2306)

Add workflow.plan_review_convergence, review.ollama_host,
review.lm_studio_host, and review.llama_cpp_host to the SDK-side
TypeScript mirror — required by the CJS↔SDK parity test (#2653).

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

* fix(#2306): resolve CodeRabbit review findings

- Anchor HIGH_COUNT extraction with head -1 to prevent multi-match when
  agent return message contains multiple CYCLE_SUMMARY lines (e.g. quoted
  back from prompt context)
- Replace hardcoded reviewers list in REVIEWS.md frontmatter template with
  runtime-derived placeholder — the static list did not reflect which
  reviewers were actually invoked
- Broaden workflow.plan_review_convergence docs to include local reviewers
  (Ollama, LM Studio, llama.cpp) alongside cloud reviewers

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

* fix(ci): restore reviewers frontmatter list with runtime note

The cursor-reviewer.test.cjs (and equivalent per-reviewer tests) assert
that each supported reviewer appears on the reviewers: line — these are
wiring tests that catch when a new reviewer is added to invocation but
not to the REVIEWS.md template. Replacing the list with a placeholder
broke those tests.

Restore the full static list and add an inline comment clarifying that
the actual committed frontmatter should be filtered to only the reviewers
invoked that run — satisfying both the per-reviewer tests and the
CodeRabbit correctness note.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:18:34 -04:00
Tom Boucher
3da9420a38 fix(#2698,#2678): CRLF agent-block strip regex + local install skips SDK check (#2710)
Fixes #2698 — The two separate LF/CRLF .replace() calls in the Codex hooks
migration could not handle mixed line endings (e.g. header in LF, body in
CRLF), leaving stale gsd-update-check blocks after reinstall. Consolidated to
a single \r?\n-aware regex with gm flags that handles LF, CRLF, and mixed
content in one pass.

Fixes #2678 — installSdkIfNeeded() called process.exit(1) unconditionally when
sdk/dist/cli.js was missing, even during --local installs where users cannot
write to global node_modules. Added isLocal option: when true, prints a warning
and returns instead of exiting.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:16:04 -04:00
Tom Boucher
3fe5759d7c fix(#2686): review-fix agent uses isolated git worktree, prevents main-tree race (#2705)
* fix(#2686): review-fix agent now uses git worktree for isolation

The gsd-code-fixer agent operated directly against the main working tree,
racing any concurrent foreground session for HEAD, the index, and on-disk
files. Added a setup_worktree step (git worktree add /tmp/sv-N-reviewfix
HEAD) as the first action before any file operations, with unconditional
git worktree remove cleanup on exit. Mirrors the pattern used by all other
GSD per-issue agents.

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

* fix(#2686): address CodeRabbit review — mktemp unique path, branch-aware worktree, tighten test assertions

- Use mktemp -d for unique worktree path (prevents concurrent-run collision)
- Resolve branch via git branch --show-current before worktree add (prevents detached HEAD)
- Error-and-exit on worktree add failure instead of force-removing shared path
- Test: use .exec().index for checkout position (not indexOf on match string)
- Test: match gsd-sdk query commit as well as git commit for ordering assertion
- Test: tighten /tmp path assertion to require actual /tmp/sv- assignment

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:15:39 -04:00
Tom Boucher
8e21c9b1b7 fix(#2684,#2676): milestone.complete arg forwarding + parallel milestone phase routing (#2708)
* test(#2692): add behavioral --wave N test, annotate source-text assertions

Adds two behavioral tests for wave filtering via phase-plan-index:
- Verifies plans with wave frontmatter are correctly grouped by wave number
- Verifies plans with no wave field default to wave 1

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

* fix(#2684,#2676): milestone.complete version validation + parallel milestone phase routing

#2684: Confirms milestone.complete correctly validates and uses its version
argument end-to-end. The inline archive path in milestoneComplete already
forwarded version correctly; regression tests lock in that contract.

#2676: phase.complete applied getMilestonePhaseFilter unconditionally, using
STATE.md's primary milestone to scope the candidate set. When the completed
phase belongs to a parallel (secondary) milestone, the filter excluded all
phases from that milestone, leaving an empty candidate set and incorrectly
returning is_last_phase: true / next_phase: null.

Fix: before applying the milestone filter in Step E, check whether the
completed phase itself appears in the filtered set. If not, skip the filter
for both the directory scan and the ROADMAP.md fallback so phases from the
secondary milestone remain visible for next-phase detection.

Closes #2684
Closes #2676

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:10:01 -04:00
Tom Boucher
8393f4b355 fix(#2601): config-set-model-profile now accepts 'inherit' value (#2707)
VALID_PROFILES was derived solely from Object.keys(MODEL_PROFILES['gsd-planner']),
which only contained the named tiers (quality/balanced/budget/adaptive). The
cmdConfigSetModelProfile validator rejected 'inherit' even though the runtime has
supported it since #1829. Fix: append 'inherit' to VALID_PROFILES and handle it
in getAgentToModelMapForProfile so the agent→model table shows 'inherit' instead
of undefined.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:09:27 -04:00