Commit Graph

1018 Commits

Author SHA1 Message Date
Tom Boucher
38f5201935 Merge pull request #3613 from gsd-build/fix/3605-stale-slash-command-references-in-5-agen
fix(3605): scrub retired slash commands from agents/*.md
2026-05-15 23:04:39 -04:00
Tom Boucher
d4dbca5b49 Merge pull request #3614 from gsd-build/fix/3608-bug-antigravity-gsd-update-does-not-dete
fix(3608): model Antigravity as a first-class runtime in update.md
2026-05-15 23:04:35 -04:00
Tom Boucher
55e1360b2c Merge pull request #3616 from gsd-build/fix/3599-bug-roadmap-get-phase-no-longer-matches-
fix(3599): preserve project-code prefix when looking up roadmap phases
2026-05-15 23:04:32 -04:00
Tom Boucher
6bd4091aa0 Merge pull request #3618 from gsd-build/fix/3610-error-installing-v1-42-2-in-codex
fix(3610): unblock fresh Codex install when leftover bundled hooks present
2026-05-15 23:04:29 -04:00
Tom Boucher
9351d0a0e7 Merge pull request #3619 from gsd-build/fix/3601-bug-phase-remove-can-delete-following-de
fix(3601): preserve peer-depth decimal phases on integer phase removal
2026-05-15 23:04:26 -04:00
Tom Boucher
9fab09cdce Merge pull request #3620 from gsd-build/fix/3602-bug-phase-remove-leaves-stale-slugged-pl
fix(3602): renumber slugged plan references on phase removal
2026-05-15 23:04:22 -04:00
Tom Boucher
8c46c3f1af Merge pull request #3622 from gsd-build/fix/3600-bug-init-new-milestone-counts-prefixed-p
fix(3600): count project-code-prefixed phase dirs in milestone filter
2026-05-15 23:04:20 -04:00
Tom Boucher
0862df15df fix(3610): replace resolved prompt actions and keep explicit block path 2026-05-15 22:56:32 -04:00
Tom Boucher
0efe4f0612 fix(3621): release-sdk hotfix cherry-picks test-fixture commits (#3623)
* fix(3621): cherry-pick test-fixture commits in hotfix runs

The release-sdk hotfix loop excluded test-fixture updates that align CI
with a cherry-picked production fix, leaving the hotfix branch with new
production behavior and stale test assertions. Broke v1.42.3 CI (run
25949422676) when fix(3562) was cherry-picked but its bundled test
correction in docs(3562) commit 08848df8 was POLICY_SKIPPED by the
prefix filter.

Two-part fix:

1. release-sdk.yml prefix regex now accepts test: alongside fix:/chore:.
   feat:, docs:, refactor: still POLICY_SKIPPED as before.

2. scripts/diff-touches-shipped-paths.cjs treats tests/-rooted paths and
   sdk/src vitest specs as CI-gating-equivalent. A test: commit touching
   only those paths now passes the shipped-paths gate.

The #2980 push-blocking guard is preserved as a separate first-priority
check: any commit touching .github/workflows/<file> still skips
regardless of test paths in the same bundle, because the default
GITHUB_TOKEN lacks the workflow scope and the push step would fail.

New regression coverage in tests/bug-3621-cherry-pick-test-fixtures.test.cjs:
- workflow prefix regex includes test:
- isCiGating accepts tests/ and sdk/src vitest specs, rejects
  non-spec sdk/src paths and incidental "test" name occurrences
- classifier exits 0 for test-only, mixed test+docs, and the original
  shipped paths
- classifier exits 1 for pure docs-only and workflow-only diffs
- new explicit assertion that #2980 push-blocking wins over #3621:
  workflow + test + changelog bundle still skips

Adjusted one pre-existing bug-2980 test fixture to use a non-
push-blocking non-shipped path (planning/notes.md) instead of
.github/workflows/release-sdk.yml. The original assertion was
documenting "mixed diff includes shipped path → include" but its
fixture happened to also trigger the push-blocking guard now made
explicit by this PR.

Fixes #3621

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

* fix(release-sdk): align hotfix summary labels with test matcher

* fix(3621): align operator-facing strings with the fix/chore/test matcher

The candidate-loop regex was updated to accept test: but several
human-facing strings in the same job still read fix/chore. Update every
description/comment/summary line for consistency so operators reading
the run summary or workflow_dispatch inputs see the same set of accepted
prefixes the matcher actually applies.

Also corrected the NON_SHIPPED_SKIPPED summary text — it claimed test
changes belong on main, not in a hotfix. That assumption is what #3621
fixes; tests under tests/ and sdk/src vitest specs are now CI-gating
candidates and may be picked. The summary now scopes the never-pick
guidance to CI / docs / planning paths only.

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-05-15 22:43:21 -04:00
Tom Boucher
55e50cf392 fix(3600): count project-code-prefixed phase dirs in milestone filter
`init.new-milestone` reported `phase_dir_count: 0` for projects whose
phase directories carry a project_code prefix (`.planning/phases/CK-01-name`)
when the ROADMAP used numeric `### Phase N:` headings. Verified via a
temp-project repro that mirrors the reporter's setup.

Root cause: `getMilestonePhaseFilter` builds an `isDirInMilestone(dirName)`
predicate that tries two paths:

  1) Numeric — requires the dir name to START with a digit. `CK-01-name`
     starts with `C`, so this skips.
  2) Custom-ID — captures the leading kebab token (`CK-01-name` as a
     whole) and compares it to the normalised milestone phase IDs
     (`{"1"}`). No match.

There was no path that stripped the project_code prefix before retrying
the numeric match. Added a third path that strips the same shape
`normalizePhaseName` already recognises (`^[A-Z]{1,6}-(?=\d)`) and retries
the numeric match. This runs AFTER the custom-ID path so a ROADMAP that
uses `### Phase PROJ-42:` continues to win via the custom-ID match for
a `PROJ-42` directory; the new branch only fires when the milestone is
keyed on the bare numeric form.

The fix lands in both:

  - get-shit-done/bin/lib/core.cjs:isDirInMilestone (active CJS runtime)
  - sdk/src/query/state.ts:isDirInMilestone (SDK twin)

`getMilestonePhaseFilter` is shared by multiple callers — init.new-milestone,
phase complete, verify-work, validate-health — so the fix benefits every
caller that walks `.planning/phases/` against a numeric ROADMAP.

Regression test
(tests/bug-3600-milestone-phase-filter-project-code-prefix.test.cjs):

  1. Reporter's case: CK-01-name + CK-02-build dirs against Phase 1 / 2
     headings → phase_dir_count === 2.
  2. Existing contract: 01-first dir against Phase 1 heading still counts.
  3. Custom-ID contract: PROJ-42 dir against `### Phase PROJ-42:` still
     counts via the existing custom-ID match (no regression).
  4. Counter-test: CK-99-backlog and CK-100-future dirs MUST NOT count
     against a milestone with only Phase 1 — the strip-and-retry must
     still respect the milestone's actual phase set.

All assertions go through `init new-milestone --json` (typed payload —
`phase_dir_count`). No raw text matching.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:28:49 -04:00
Tom Boucher
99567d8eb5 fix(3602): renumber slugged plan references on phase removal
phase.cjs:updateRoadmapAfterPhaseRemoval renumbers plan references in
ROADMAP.md via a regex that captured `NN-NN` followed by an optional
suffix:

  /(?<![0-9-])(\d{2})-(\d{2})(?=(?:-(?:PLAN|SUMMARY)\.md)?(?![0-9-]))/g

The suffix branch was strict: it only accepted `-(PLAN|SUMMARY).md`
directly after the plan number. A slug like
`07-01-cherry-pick-foundation-PLAN.md` placed `-cherry-…` between the
number and the canonical suffix, so both the suffix branch AND the
"bare token" branch (`(?![0-9-])` — fails because the next char is `-`)
failed. Result: the on-disk file got renamed to
`06-01-cherry-pick-foundation-PLAN.md` by the directory-rename pass,
but the ROADMAP entry kept pointing at the stale `07-01-…` prefix —
disk/ROADMAP inconsistency.

Fix: extend the suffix branch to allow an optional kebab-case slug
between the plan number and the PLAN/SUMMARY suffix:

  (?:(?:-[A-Za-z][A-Za-z0-9-]*)*-(?:PLAN|SUMMARY)\.md)|(?![0-9-])

Each slug token must start with a letter so `07-01-02-PLAN.md` is not
silently consumed as one slugged token (the `-02` is correctly
unreachable from the slug branch because it starts with a digit).

Regression test exercises three cases via the typed `roadmap get-phase
--json` query (no raw text matching on ROADMAP.md content):

  1. Slugged PLAN + SUMMARY filenames get renumbered (#3602 fix).
  2. Compact `NN-NN-PLAN.md` filenames still renumber correctly
     (#3601 / earlier contracts preserved).
  3. Counter-test: ISO dates (`2026-01-01`) and version tags (`v1-2-3`)
     in ROADMAP prose are NOT modified — the existing `(?<![0-9-])` /
     `(?![0-9-])` boundaries hold against false positives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:23:46 -04:00
Tom Boucher
7036bb4019 fix(3601): preserve peer-depth decimal phases on integer phase removal
`phase remove N` for an integer phase silently deleted the adjacent
`### Phase N.1:` decimal section when the decimal was a peer-depth
heading. The bug was in the section-removal regex inside
get-shit-done/bin/lib/phase.cjs:updateRoadmapAfterPhaseRemoval:

  (?=\n#{2,4}\s+Phase\s+\d+\s*:|$)

The lookahead required the next header's digits to be followed by
`\s*:` — true for `### Phase 3:` but false for `### Phase 2.1:` because
the `.1` breaks the match. The non-greedy `[\s\S]*?` body then consumed
`Phase 2.1` along with `Phase 2` until it found the next integer
header. The on-disk phase directory `.planning/phases/02.1-*` survived
but its ROADMAP entry was gone — disk/ROADMAP inconsistency.

The fix uses a depth-aware lookahead: capture the hash count of the
header being removed with a named group `(?<h>#{2,4})` and require the
end-of-section lookahead to match the SAME depth via `\k<h>(?!#)`. The
`(?!#)` guards against `###` accidentally matching a deeper `####`
header by anchoring on the captured hash count.

This preserves two contracts simultaneously:

  - #3601: removing `### Phase 2:` (depth 3) stops at the next depth-3
    header, including `### Phase 2.1:` — the peer-level decimal is
    preserved.
  - #3355: removing `### Phase 27:` (depth 3) continues past
    `#### Phase 27.1:` (depth 4, a child of the integer phase) until it
    reaches the next depth-3 header. The child decimal is part of the
    integer phase being removed.

The regression test exercises the public CLI via runGsdTools and
asserts on typed JSON output from `roadmap get-phase --json` — no raw
text matching on ROADMAP.md content (per CONTRIBUTING.md
"Prohibited: Raw Text Matching on Test Outputs").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:20:34 -04:00
Tom Boucher
f422a05450 fix(3610): unblock fresh Codex install when leftover bundled hooks present
`npx get-shit-done-cc@latest --codex` aborted with
"installer migration blocked pending user choice" listing 12 hooks/gsd-*
files. Those files are part of the GSD npm distribution
(hooks/gsd-prompt-guard.js, hooks/gsd-context-monitor.js, etc.), not
user-owned content, so asking the user to choose between keep/remove for
them was a UX bug, not a real choice. The installer is about to write
the fresh bundled versions in their place.

Root cause: `classifyPromptUserAction` in
get-shit-done/bin/lib/installer-migration-report.cjs knew two
unambiguous categories (`stale-sdk-build-artifact`, `user-facing-skill`)
but had no rule for the bundled GSD hooks. The first-time-baseline scan
classified them as `stale-gsd-looking` prompt-user blockers, and
`assertInstallerMigrationsUnblocked` threw.

A second gate compounded the bug: the safe-default resolver in
bin/install.js was wrapped in `if (!_migrationIsTty)`, so even with a
correct classification rule, TTY runs (every `npx get-shit-done-cc`
invocation) skipped the resolver and went straight to the hard throw.

Fix:
1) Add `hooks/gsd-<name>.(js|sh|cjs|mjs)` to `classifyPromptUserAction`
   as `bundled-gsd-hook` → `remove`. The regex is anchored at the
   top-level `hooks/` directory so nested paths like
   `hooks/gsd-helpers/index.js` (or any user-owned helper directory) do
   NOT auto-classify.
2) Remove the `!_migrationIsTty` gate from the resolver call in
   bin/install.js. The classifier-based path is unambiguous and must
   apply regardless of TTY; the env-override branch
   (GSD_INSTALLER_MIGRATION_RESOLVE) still applies only when isTty=false
   inside the resolver, preserving the #3541 semantic.

Regression test added
(tests/bug-3610-installer-migration-bundled-hooks-classification.test.cjs):

- Positive: hooks/gsd-*.{js,sh} → category=bundled-gsd-hook, choice=remove.
- Counter-test: hooks/my-custom-hook.js → classifier returns null
  (user files are preserved).
- Boundary: hooks/gsd-helpers/index.js → classifier returns null
  (nested directories don't auto-classify).
- End-to-end: 12 reporter-exact bundled hooks + empty manifest →
  resolver clears every blocker, assertInstallerMigrationsUnblocked
  does not throw.

Test exercises the real installer-migration code path
(`runInstallerMigrations` + `resolveInstallerMigrationPromptsForNonTty`
+ `assertInstallerMigrationsUnblocked`) — no source-grep, no raw text
matching on outputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:13:34 -04:00
Tom Boucher
2336c27141 fix(3599): preserve project-code prefix when looking up roadmap phases
`roadmap get-phase PROJ-42` returned `{found: false}` because
phaseMarkdownRegexSource() unconditionally strips the project-code prefix
(`^[A-Z]{1,6}-(?=\d)`) before matching, building the regex `0*42` which
matches `### Phase 42:` but never `### Phase PROJ-42:`. The function's
own docstring promised a fallback to escapeRegex(phaseNum) for custom
IDs, but the line-680 regex match consumes the stripped-numeric form
before that branch is reachable.

Fix: add phaseMarkdownRegexSourceExact() that returns the exact-escaped
source for project-code-prefixed inputs (or null for un-prefixed). Update
cmdRoadmapGetPhase to do a two-pass search — try the exact-prefixed form
first, only fall back to the existing padding-tolerant numeric form if
the exact heading is not present.

Two-pass at the call site (rather than alternation inside the regex
source) is required: a roadmap containing both `### Phase 42:` and
`### Phase PROJ-42:` cannot be disambiguated by a single alternation
because regex match-position is leftmost-wins, so the bare numeric
heading at line N would always intercept the match intended for the
prefixed sibling at line M.

The #3537 contract is preserved: `roadmap get-phase CK-01` against a
roadmap that uses `### Phase 1:` prose still resolves correctly via the
numeric fallback, because the exact-prefixed pass returns null and the
existing padded-numeric pass runs unchanged.

Tests added (tests/bug-3599-roadmap-get-phase-project-code-prefix.test.cjs):
1. PROJ-42 query against `### Phase PROJ-42:` heading — found
2. Counter-test: bare `42` query against `### Phase PROJ-42:` — NOT found
3. #3537 contract preserved: CK-01 query → `### Phase 1:` heading
4. Disambiguation: both `### Phase 42:` and `### Phase PROJ-42:` in
   one roadmap; each query resolves to its specific match

All assertions go through runGsdTools + JSON parse — typed payload,
no raw text matching on stdout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:08:28 -04:00
Maxim Brashenko
79ea076daa fix(3571): load configuration manifests after install (#3572)
* fix(3571): load configuration manifests after install

* test(3571): simplify missing manifest check
2026-05-15 22:03:03 -04:00
Andreas Brauchli
6a2bf05de7 feat(#3039): tier /gsd-help output (--brief, default, --full, <topic>) (#3040)
Replace the single 747-line /gsd-help reference with a progressive-disclosure
dispatcher (#2551 pattern). Newcomers get a one-page tour; returning users get
a 10-line refresher with --brief; the complete reference stays available behind
--full; /gsd-help <topic> emits one section; and /gsd-help --brief <topic>
is a compact scoped lookup (signature + one-line summary).

- workflows/help.md becomes a small dispatcher routing on $ARGUMENTS
- workflows/help/modes/{brief,default,full,topic}.md hold the tier bodies
- commands/gsd/help.md passes $ARGUMENTS through, advertises composable form
- docs/COMMANDS.md documents the new flags and topic form
- existing tests that read help.md repointed at help/modes/full.md
  (bug-2836, bug-2950, bug-2954, cursor-reviewer, execute-phase-wave)
- new feat-3039-help-tiered test enforces structure, size budgets,
  dispatcher routing, shim arg passthrough, topic→section coverage,
  orphan-heading detection, conflict-resolution rules, routing preamble,
  and compact-scope rule

Trek-e review fixes (PR #3040):
- topic.md output rules split into 5a/5b/5c — explicit handling for single
  sections, multi-section "plus" joins, and bold-line sub-block anchors;
  each rule also takes scope (full vs compact) into account
- explicit resolved-routing preamble line emitted by topic.md before content
  ("**Topic:** `<alias>` → `<heading>` *(scope: full | compact)*") so the
  user sees which alias matched at which scope (review finding #3)
- composable `--brief <topic>` invokes topic.md in compact scope: heading
  + first `**/gsd:*`** signature line + one-line summary. Dispatcher and
  topic.md cooperate via $ARGUMENTS pass-through (review finding #4)
- full.md capped at LARGE-tier budget (FULL_BUDGET = 1500); the non-recursive
  workflow-size-budget test does not reach modes/ subdirs
- structural <progressive_disclosure> table parse (5-row assertion) replaces
  substring-soup regex matching — 5 rows = 4 base tiers + composable scope
- forward /gsd:* sub-block token coverage + reverse orphan-heading allowlist
  catch alias-table drift in both directions
- four conflict-resolution tests guard dispatcher promises (--brief+--full
  without topic → --full; --brief <topic> → compact; --full <topic> or bare
  → full; dispatcher retains --brief when delegating to topic.md)
- hardcoded topic lists removed from docs/COMMANDS.md and full.md (drift
  surfaces reduced from 5 to 2)
- topic.md alias bloat trimmed (~75 → ~25 rows); cleanup/update split into
  distinct sub-block rows under ### Utility Commands
- comment-rot ("~750 lines") removed from default.md and full.md
- dispatcher size guard tightened from < 100 to <= 40 lines
- commands/gsd/help.md <process> block trimmed to one line
- MD040 fence languages added to all plain code blocks across mode files

Main-merge conflict resolution:
- workflows/help.md kept as dispatcher (body lives in help/modes/full.md)
- /gsd-<cmd> → /gsd:<cmd> rename from #3452 reapplied to the mode files
  (full.md, default.md, brief.md, topic.md) — the six namespace routers
  (/gsd-context, /gsd-ideate, /gsd-manage, /gsd-project, /gsd-quality,
  /gsd-workflow) and wildcards (/gsd-*) preserved in hyphen form per
  main's convention
- bug-2950 test combines branch's path repointing with main's namespaced
  replacement strings
2026-05-15 22:02:55 -04:00
Tom Boucher
f27a20a388 fix(3608): model Antigravity as a first-class runtime in update.md
bin/install.js and the SDK already treat Antigravity as a distinct runtime
with config dir ~/.gemini/antigravity, env var ANTIGRAVITY_CONFIG_DIR, and
CLI flag --antigravity. get-shit-done/workflows/update.md did not — so
/gsd-update invoked from an Antigravity install classified the runtime as
base Gemini, because:

- RUNTIME_DIRS listed "gemini:.gemini" with no antigravity entry, so the
  scan matched ~/.gemini before ever looking for ~/.gemini/antigravity.
- The PREFERRED_RUNTIME env-var ladder checked GEMINI_CONFIG_DIR but not
  ANTIGRAVITY_CONFIG_DIR.
- The local-scope scan loops at lines 101 and 590 listed .gemini with no
  .gemini/antigravity sibling.
- The ENV_RUNTIME_DIRS append block ignored ANTIGRAVITY_CONFIG_DIR.
- The path-to-runtime classification bullets only mapped /.gemini/ ->
  gemini, with no /.gemini/antigravity/ -> antigravity branch.

Every list is now updated so the more-specific antigravity entry precedes
the base gemini entry, matching the installer at bin/install.js (lines
396-404, 1745-1749, 6175, 6475).

tests/bug-3608-antigravity-update-runtime-classification.test.cjs is the
structural regression guard. It parses the RUNTIME_DIRS bash array out of
update.md and asserts the antigravity entry is present and ordered before
gemini, asserts the env-var ladder checks ANTIGRAVITY_CONFIG_DIR before
GEMINI_CONFIG_DIR, asserts every `for dir in ...` scan loop that mentions
.gemini also lists .gemini/antigravity ordered before it, and asserts the
path-classification prose bullet lists antigravity before gemini.

Per CONTRIBUTING.md: the test uses readFileSync on a .md file annotated
`// allow-test-rule: source-text-is-the-product` because the bash blocks
inside update.md ARE the deployed program — the agent loads update.md and
runs them as-written.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:58:51 -04:00
Tom Boucher
18ffaa86cc fix(3605): scrub retired slash commands from agents/*.md
Six surviving references to /gsd-research-phase (deleted in #3042) and
/gsd-insert-phase (consolidated into /gsd:phase insert in v1.40.0)
remained in five agent contracts because every prior scrub pass (#3029,
#3044, #3131) limited its SEARCH_DIRS to workflows/, references/,
templates/, contexts/, commands/, and hooks/ — agents/ was outside scope.
agents/gsd-executor.md:195 is user-facing: the executor surfaces it
during a package-install failure recovery checkpoint, so a real user
hits "Unknown command" while trying to recover from a stalled phase.

Replacements:
- /gsd-research-phase -> /gsd:plan-phase --research-phase <N>
  (agents/gsd-executor.md:195, agents/gsd-phase-researcher.md:17,
   agents/gsd-planner.md:186, agents/gsd-planner.md:991,
   agents/gsd-research-synthesizer.md:115)
- /gsd-insert-phase -> /gsd:phase insert
  (agents/gsd-roadmapper.md:205)

Adds tests/bug-3605-stale-research-insert-phase-agent-refs.test.cjs as
the regression guard. It scans agents/*.md for any retired command name
(/gsd-research-phase, /gsd-insert-phase, /gsd-add-phase,
/gsd-remove-phase, /gsd-analyze-dependencies) with proper word-boundary
matching so a future consolidation that misses agents/ fails CI.

The guard mirrors tests/bug-2950-stale-command-refs.test.cjs which
covers the same anti-pattern for workflows/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:54:03 -04:00
Tom Boucher
3edbf7f357 Merge pull request #3563 from gsd-build/fix/3561-gsd-ultraplan-phase-runtime-gate-checks-
fix: use Claude Code runtime markers in ultraplan gate
2026-05-15 15:03:14 -04:00
Tom Boucher
eb92d107c8 Merge pull request #3568 from gsd-build/fix/3562-codex-0-130-0-gsd-1-42-2-installs-workfl
fix(3562): generate Codex skill surface so $gsd-* commands resolve on Codex CLI 0.130.0+
2026-05-15 15:03:04 -04:00
Tom Boucher
ec4025d538 test(3566): clarify ownership semantics for legacy codex_hooks docs 2026-05-15 14:54:58 -04:00
Tom Boucher
9a04332953 docs+test(3562): align codex roots wording and parse SKILL frontmatter 2026-05-15 14:44:40 -04:00
Tom Boucher
acf92de6e2 test(3566): remove raw-text greps from codex hooks regression 2026-05-15 14:42:14 -04:00
Tom Boucher
1746f89ac7 fix(3566): narrow scope; update codex-config tests for canonical hooks key
Two follow-up adjustments after running the full suite:

1. Reverted the rewriteTomlKeyLines() change. The original
   `match.keyRaw || key` fallback respects user ownership of pre-existing
   legacy lines (#2760 defensive principle). My fix now applies the
   canonical-vs-legacy split at the INSERTION points only: fresh installs
   write `hooks = true`, but a pre-existing user-authored
   `codex_hooks = true` is preserved verbatim. Codex's own runtime
   legacy_key alias handles backward-compat at the Codex layer.

2. Updated 12 test cases in tests/codex-config.test.cjs that pinned the
   old fresh-write key. These assertions now expect canonical `hooks` for
   fresh-write scenarios; tests covering legacy-line preservation already
   pass against the narrowed fix without further edits.

Also updated bug-3566 regression-test cases for the legacy-preservation
path — they now verify that user-owned `codex_hooks` survives an install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:22:32 -04:00
Tom Boucher
55b3f45ad0 fix(3566): emit canonical [features].hooks; recognize legacy codex_hooks as alias
Closes #3566

Codex itself marks codex_hooks as a legacy_key in
codex-rs/features/src/legacy.rs. The canonical current Codex feature flag
under [features] is hooks. The GSD installer was still writing codex_hooks
on every fresh install / reinstall, leaving deprecated config behind on
Codex CLI >= 0.130.0.

Introduces a canonical/legacy split in bin/install.js:

  CODEX_HOOKS_FEATURE_KEY = 'hooks'
  CODEX_HOOKS_FEATURE_LEGACY_KEYS = ['codex_hooks']
  isCodexHooksFeatureKey(key)  // recognizes canonical OR any legacy alias

Threaded through:

- ensureCodexHooksFeature(): emits canonical hooks; recognizes legacy
  codex_hooks; migrates legacy -> canonical in section, root-dotted, and
  block-fallback insertion paths.
- hasEnabledCodexHooksFeature(): accepts either canonical or legacy.
- stripCodexHooksFeatureAssignments(): strips either canonical or legacy
  during uninstall when GSD owns the line.
- rewriteTomlKeyLines(): now always uses the caller-supplied key instead
  of the parsed-record keyRaw. The old `match.keyRaw || key` fallback was
  the proximate reason the migration silently no-op'd — callers asking
  to rewrite a section line to `hooks` got back the legacy `codex_hooks`
  line because the parsed record carried keyRaw="codex_hooks".

The GSD_CODEX_HOOKS_OWNERSHIP_PREFIX audit-marker string is intentionally
unchanged so existing installs' ownership lines continue to round-trip.

Tests:
- New tests/bug-3566-codex-hooks-feature-canonical-key.test.cjs (6 cases):
  fresh install writes hooks; section-form legacy migrated; root-dotted
  legacy migrated; user-owned hooks preserved; uninstall removes
  GSD-owned canonical; uninstall preserves user-owned hooks.
- Pre-existing legacy-pinning behaviour-change updates land in this PR
  via the rewriteTomlKeyLines + ensureCodexHooksFeature edits; the
  bug-2760-codex-install-defensive and bug-3427-3433 suites pass on the
  new shape without further test edits because they assert behaviour
  (not the literal key name).

Docs:
- docs/ARCHITECTURE.md row for Codex notes [features].hooks (canonical,
  legacy codex_hooks recognized and migrated forward).
- docs/installer-migrations.md row updated to reflect canonical key
  and the new Codex 0.130.0 features.hooks compatibility sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:03:32 -04:00
Tom Boucher
08848df839 docs(3562): pin minimum Codex CLI version (0.130.0) and explain the seam
Rationale for the version pin (the timeline that produced the oscillation):

  2026-05-08  Codex CLI 0.130.0 ships, dropping extra-skills-roots
              discovery via openai/codex#21485 (scans only ~/.codex/skills,
              cwd .codex/skills, and registered plugin roots).
  2026-05-14  GSD PR #3512 lands, removing ~/.codex/skills/gsd-* under the
              assumption Codex would auto-discover from extra roots.
              That assumption was already obsolete in shipped Codex.
  2026-05-15  #3562 filed — Codex CLI 0.130.0 users have zero $gsd-*
              commands after install.

The previous fix (#3427) was for Codex Desktop's official-skills surface,
which is a different product; that surface still exists on Desktop and
remains harmless duplication when both root scans see the gsd-* dirs.

Documents the supported version inline at the Codex sections of both
USER-GUIDE.md and CONFIGURATION.md, plus a one-line note in README's
Troubleshooting block. No runtime version-detection added — out of scope
and brittle against future Codex changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:46:12 -04:00
Tom Boucher
365340596d fix(3562): generate Codex skill surface; $gsd-* commands discoverable after install
Closes #3562

Codex CLI 0.130.0 only registers commands from skills/<name>/SKILL.md; it
does NOT auto-discover from get-shit-done/workflows/*.md or agents/*.md.
Prior installer logic (#3427/#3433) removed the gsd-* skill copies under the
assumption that Codex would discover the official skills directly. That
assumption does not hold — users ended up with workflows on disk and zero
$gsd-* entrypoints after restart.

Fix: re-wire copyCommandsAsCodexSkills() (line 5519, already present) into
the Codex install dispatch path (line 8090). Generates one
~/.codex/skills/gsd-<name>/SKILL.md per commands/gsd/*.md — same shape the
Copilot/Antigravity/Cursor/Windsurf/Augment/Trae installs use.

Behaviour change: the pre-existing test in bug-3427-3433-codex-install-shape
asserted "does not regenerate gsd-* skill copies". Updated it to assert
the new behaviour (regenerate gsd-* with refreshed body, preserve non-gsd
user skills).

Tests:
- New tests/bug-3562-codex-install-skill-surface.test.cjs (4 cases):
  - skills/gsd-help/SKILL.md exists
  - SKILL.md has YAML frontmatter with name: gsd-help
  - >= 10 gsd-* skill directories produced (lower-bound, currently 67)
  - Pre-existing custom-user-skill directory preserved
- tests/bug-3427-3433-codex-install-shape.test.cjs: updated to assert
  regeneration + body refresh + unrelated-skill preservation.

Verified by re-running the issue's repro: `node bin/install.js --codex
--global --config-dir <tmp>` now produces 67 gsd-* skills and the target
~/.codex/skills/gsd-help/SKILL.md exists with valid frontmatter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:31:31 -04:00
Tom Boucher
f097598689 fix(3561): fail closed when Claude version is unavailable 2026-05-15 12:59:18 -04:00
Tom Boucher
18397fb7f5 fix(3561): gate ultraplan runtime on Claude Code markers 2026-05-15 12:47:54 -04:00
Tom Boucher
1a4e6df6b3 Merge pull request #3557 from gsd-build/feat/3347-auto-update-knowledge-graph-after-main-h
feat(3347): opt-in auto-update of knowledge graph after main HEAD advances
2026-05-15 12:06:41 -04:00
Tom Boucher
dd4f75376a fix(3347): surface auto-build state via graphifyStatus; trim planner edits to free size budget
agents/gsd-planner.md was 49,316 chars after the initial PR; the
planner-decomposition <48K test was passing on main at 49,150 chars (just under
the 49152 limit). My addition pushed it over.

Restructure: instead of teaching the planner agent to read .last-build-status.json
directly, fold the auto-build state into graphifyStatus()'s existing `stale: true`
signal. The planner's existing rule ("if stale: true, treat as approximate") fires
correctly for failed and in-flight auto-builds — no new planner-side prompt content
needed. The full state is exposed under `last_build_auto_update` for callers that
want exit_code / duration_ms / commit-sha context.

- get-shit-done/bin/lib/graphify.cjs: graphifyStatus() reads
  .planning/graphs/.last-build-status.json; OR-folds status in {failed, running}
  into the existing stale signal; exposes last_build_auto_update field
- agents/gsd-planner.md: revert the auto-update awareness paragraph (49,524 → 49,150)
- agents/gsd-phase-researcher.md: revert the parallel paragraph for consistency
- get-shit-done/references/planner-graphify-auto-update.md: rewrite to document
  the graphifyStatus seam instead of planner-side prompt instructions
- tests/feat-3347-graphify-auto-update-config.test.cjs: 4 new graphifyStatus
  tests pinning the failed/running/ok/missing matrix
- tests/feat-3347-graphify-auto-update-hook.test.cjs: bump per-spawn timeout
  5s → 30s and wait-deadline 5s → 15s to absorb cold-start latency under
  parallel-test-file load (full suite runs many *.test.cjs concurrently)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:59:43 -04:00
Tom Boucher
b0fd732579 fix(3347): allowlist graphify-update hook path in docs-parity-live-registry test
docs/CONFIGURATION.md references the bundled hook by its file path
(hooks/gsd-graphify-update.sh). The docs-parity regex captures
/gsd-graphify-update from the path component and looks it up in the
live command registry, where it does not (and should not) exist —
it's a hook script, not a slash command. Add the slug alongside the
other hook-path slugs (statusline, context-monitor, update-banner).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:59:43 -04:00
Tom Boucher
dacc23137a feat(3347): opt-in auto-update of knowledge graph after main HEAD advances
Closes #3347

Config:
- Add graphify.auto_update (default false) to manifests:
  sdk/shared/config-defaults.manifest.json, config-schema.manifest.json

Hook:
- hooks/gsd-graphify-update.sh — PostToolUse Bash matcher
  - Gates: tool_name=Bash, HEAD-advancing git op, CI=unset, in git repo,
    current branch == default branch (git.base_branch override or main/
    master/trunk fallback), graphify.enabled && graphify.auto_update both
    true, graphify on PATH, no live PID lock
  - Writes .planning/graphs/.last-build-status.json with status=running
    synchronously, then detaches hooks/lib/gsd-graphify-rebuild.sh
- hooks/lib/gsd-graphify-rebuild.sh — detached rebuild runner
  - PID-lock acquire + trap-on-exit cleanup
  - graphify update . then cp graphify-out/* → .planning/graphs/
  - Status file rewritten to status=ok|failed with exit_code, duration_ms,
    head_at_build
- Portable detach (subshell + disown, no setsid dependency)

Installer:
- bin/install.js: register hook as PostToolUse Bash matcher (5s timeout)
- Add to gsdHooks uninstall list and expectedShHooks warning list

Planner / researcher status surface (issue #3347 reviewer must-have AC):
- agents/gsd-planner.md and agents/gsd-phase-researcher.md
  load_graph_context steps now read .last-build-status.json and surface:
  running → "rebuild in flight"; failed → "auto-rebuild FAILED at {ts},
  context is from prior build"; ok with stale head_at_build → "HEAD has
  advanced since last build"

Settings:
- get-shit-done/workflows/settings.md adds "Graph auto-update" question
  with No-Recommended default; bullets and update_config block updated

Inventory:
- docs/INVENTORY.md hook count 12 → 13 with new row
- docs/INVENTORY-MANIFEST.json regenerated

Tests:
- tests/feat-3347-graphify-auto-update-config.test.cjs (8 tests):
  isValidConfigKey accepts graphify.auto_update, CANONICAL_CONFIG_DEFAULTS
  default false, config-set round-trip, sibling key preservation
- tests/feat-3347-graphify-auto-update-hook.test.cjs (18 tests):
  all bail paths (non-Bash, non-HEAD-advancing, enabled=false,
  auto_update=false, CI=true, non-default-branch, missing graphify bin,
  live-PID lock), dispatch path with mock graphify bin (sync running
  status + detached transition to ok/failed), stale-PID lock, all five
  HEAD-advancing command matchers, git.base_branch override

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:59:43 -04:00
Tom Boucher
8090456b66 feat(3555): QueryRuntimeBridge.executeForCjs synchronous primitive (Phase 5.0 of #3524)
Phase 5.0 of the CJS↔SDK hard-seam migration (parent #3524).
Foundational PR. Ships ONLY the synchronous primitive on the SDK
runtime bridge plus pinning tests. Per-family CJS router migrations
(state.*, verify.*, init.*, phase.*, phases.*, validate.*,
roadmap.*, frontmatter.*, config.*) become follow-up enhancements
that each reuse this primitive.

## What landed

- sdk/src/runtime-bridge-sync/index.ts (155 lines) — public API.
  Exports executeForCjs(input: RuntimeBridgeExecuteInput):
  RuntimeBridgeSyncResult. Synchronous; lazily creates the
  synckit sync function on first call.
- sdk/src/runtime-bridge-sync/worker.ts (167 lines) — synckit
  worker. Constructs a native-only QueryRuntimeBridge (with
  allowFallbackToSubprocess: false), awaits its async execute,
  catches GSDToolsError / GSDError, maps classification to the
  six ADR-0001 canonical error kinds plus exit code.
- sdk/src/runtime-bridge-sync/index.test.ts (197 lines, 8 vitest
  pinning fixtures) — success path; unknown_command;
  native_failure (shape); validation_error (shape); shape
  invariants; idempotency.
- tests/runtime-bridge-sync-smoke.test.cjs (99 lines, 4
  node:test cases) — proves the primitive works from CJS
  callers via require().

## Decisions

1. Synchronous-bridging mechanism: synckit. Disqualified:
   - deasync: stagnant (68 open issues, single maintainer,
     last release Nov 2025), private Node API (process.binding('uv')),
     untested on Node 22, documented deadlocks with modern
     Promise chains.
   - Sync-native SDK refactor: technically infeasible —
     acquireStateLock in state-mutation.ts uses await setTimeout
     for retry backoff; making that fully sync requires either
     Atomics.wait (which IS synckit), busy-loop (degrades
     responsiveness), or breaking 100+ SDK consumers.
   Synckit (v0.11.12) is pure JS, actively maintained (last
   push today), stable public APIs (Atomics.wait +
   SharedArrayBuffer), Node 22 compatible, no native compile.

2. Native-only transport inside the worker. The sync bridge
   uses allowFallbackToSubprocess: false. Unknown commands
   surface as unknown_command instead of spawning gsd-sdk.
   Keeps the worker self-contained and predictable.

3. Worker path resolution. resolveWorkerPath() navigates ../..
   from the loaded module URL to land at
   dist/runtime-bridge-sync/worker.js — works under both
   vitest (loads src) and CJS consumers (load dist).

4. GSDError.Blocked → validation_error. ADR-0001's 6-kind
   taxonomy has no `blocked` kind; Blocked classification is
   mapped onto validation_error since the operational shape
   matches (prerequisite missing).

## Numbers

- 8 SDK vitest pinning tests pass.
- 4 CJS smoke tests pass.
- Full suite: 9286/9286 pass (baseline 9282; +4 from the
  new smoke test cases).
- SDK vitest unit: 1860/1860 pass.
- Performance: 80ms first-call cold latency (Worker startup +
  bridge construction); 0.1ms steady-state per-call latency
  (10-call average after warmup). Well within budget for CJS
  dispatcher overhead.

## Canonical error kind coverage

- unknown_command: covered with pinning fixture
- native_failure: shape coverage (handler that throws)
- validation_error: shape coverage (GSDError.Validation +
  GSDError.Blocked)
- internal_error: shape coverage only (eliciting TypeError
  reliably from a registered handler requires elaborate fixture)
- native_timeout: NOT pinned (no registered handler genuinely
  times out; classification logic present in worker)
- fallback_failure: NOT pinned (subprocess fallback disabled
  by design in sync bridge)

The classification logic is in the worker regardless; per-family
migration PRs will exercise the unpinned kinds incidentally.

## Wiring

- sdk/package.json: synckit ^0.11.12 added as runtime
  dependency (not devDependency — it's required at runtime
  whenever a CJS caller invokes executeForCjs).
- sdk/package-lock.json: regenerated.
- CONTEXT.md: new "Sync Runtime Bridge Module" entry added
  after Dispatch Policy Module. Existing "CJS Command Router
  Adapter Module" entry amended with one sentence pointing at
  the primitive and the per-family migration roadmap.

No generator/freshness check needed for this phase — the
primitive IS the SDK (not a generated CJS mirror).

Closes #3555.
2026-05-15 11:10:03 -04:00
Tom Boucher
a3ca6ff6d6 feat(3553): Project-Root Resolution Module via generator (Phase 4 of #3524)
Phase 4 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the `findProjectRoot` duplication that lived at
bin/lib/core.cjs:74-140 and sdk/src/query/helpers.ts:497-590,
the drift carrier behind historical bugs #1362 and #2561.

- sdk/src/project-root/index.ts — source of truth (120 lines,
  pure-with-sync-fs). Exports findProjectRoot(startDir: string)
  and FIND_PROJECT_ROOT_MAX_DEPTH constant.
- sdk/src/project-root/index.test.ts — 13 vitest pinning fixtures
  covering all four heuristics, the #1362 guard, malformed
  config fallback, empty sub_repos, deep nesting, and depth-limit
  enforcement.
- sdk/scripts/gen-project-root.mjs — generator. Captures
  function body via Function.prototype.toString() from compiled
  sdk/dist/. Emits CJS preamble for destructured node:fs /
  node:path / node:os imports.
- sdk/scripts/check-project-root-fresh.mjs — freshness check.
  Imports the generator function directly (Phase 3's cleaner
  pattern).
- get-shit-done/bin/lib/project-root.generated.cjs — generator-
  emitted CJS mirror.
- tests/project-root-generator.test.cjs — 11 parity assertions
  comparing SDK source and generated CJS for every fixture.

- sdk/src/query/helpers.ts: -127 lines. The 94-line inline
  findProjectRoot plus the FIND_PROJECT_ROOT_MAX_DEPTH constant
  (originally at line 471) replaced by a single re-export:
  `export { findProjectRoot } from '../project-root/index.js';`
  Removed unused `parse as parsePath` import.
- get-shit-done/bin/lib/core.cjs: -83 lines net. The 67-line
  inline findProjectRoot replaced by a single
  `require('./project-root.generated.cjs')`. The detectSubRepos
  helper at lines 40-56 stays (used by loadConfig migration).

- sdk/package.json: gen:project-root + check:project-root-fresh
  scripts.
- package.json: proxy for the freshness check.
- .githooks/pre-commit: drift block.
- .github/workflows/test.yml: drift check step after the
  state-document drift step.
- CONTEXT.md: Project-Root Resolution Module entry.
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json:
  +1 module count, +1 row.

- Full suite: 9226/9226 pass (baseline 9215 + 11 new parity
  fixtures).
- SDK vitest: 1804/1804 pass.
- Reader shrink: -127 SDK + -83 CJS = 210 lines of duplication
  deleted across the two Readers. New shared Module is 120 lines.

1. Depth limit canonicalization. CJS findProjectRoot previously
   had no explicit walk-up bound (walked until dir === root or
   homedir). The new Module uses FIND_PROJECT_ROOT_MAX_DEPTH = 10,
   matching the SDK's pre-existing value. Only affects paths
   nested more than 10 levels deep from a .planning/ root — a
   pathological case in practice. None of the existing 22 CJS
   findProjectRoot tests covered this; the new parity test does.
2. platformReadSync → readFileSync. The old CJS findProjectRoot
   used the platformReadSync wrapper from
   shell-command-projection.cjs for reading .planning/config.json,
   which returns null on read failure. The Module uses raw
   readFileSync, which throws — caught by the surrounding
   try/catch that already swallowed errors. Functionally
   equivalent for the existing code path; no test exercises the
   null-return semantic.

Closes #3553.
2026-05-15 10:07:10 -04:00
Tom Boucher
ed8f4c9a31 feat(3544): Workstream Inventory Builder/Reader split (Phase 3 of #3524)
Phase 3 of the CJS↔SDK hard-seam migration (parent #3524).
Introduces the Builder/Reader pattern for paired Modules with
mixed pure-and-I/O concerns — the template for Phase 4 and
follow-up enhancements that migrate other paired Modules.

Phase 1 and Phase 2 migrated Modules where both sides used
character-equivalent logic. Phase 3 introduces the case where
the pure logic is shareable but the I/O is legitimately per-side.
The Builder/Reader split resolves this:

- The Builder is pure — accepts pre-collected data
  (BuilderInputs struct), returns the typed projection. One
  source of truth; one generator-emitted CJS mirror. Drift
  is structurally impossible.
- The Readers are per-side hand-authored Adapters that do the
  fs reads in their native idiom (currently both sync; either
  side can go async later without touching the Builder), then
  delegate to the Builder.

- sdk/src/workstream-inventory/builder.ts — Builder source.
  170 lines. Pure. Exports buildWorkstreamInventory(inputs),
  isCompletedInventory(status), plus the three typed inventory
  interfaces (WorkstreamPhaseInventory, WorkstreamInventory,
  WorkstreamInventoryList).
- sdk/src/workstream-inventory/builder.test.ts — 18 vitest
  pinning fixtures across all status branches, progress-percent
  clamping, active-marker projection, and isCompletedInventory
  classifier.
- sdk/scripts/gen-workstream-inventory-builder.mjs — generator.
  Captures function bodies via Function.prototype.toString();
  emits with the standard GENERATED FILE banner. Includes a
  small `const relative = path.relative;` preamble in the
  output to handle ESM destructured imports in the compiled
  source.
- sdk/scripts/check-workstream-inventory-builder-fresh.mjs —
  freshness check. Imports the generator function directly
  (rather than duplicating logic) — a cleaner pattern than
  Phase 1/2's approach.
- get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs —
  generator-emitted CJS mirror.
- tests/workstream-inventory-builder-generator.test.cjs — 16
  parity assertions confirming CJS-generated output ==
  SDK source output for every fixture.

- bin/lib/workstream-inventory.cjs: 159 → 132 lines.
  Projection logic gone. `inspectWorkstream` and
  `listWorkstreamInventories` collect BuilderInputs via the
  existing sync fs functions and delegate to the Builder.
  `isCompletedInventory` re-exported from the Builder (its
  signature changed from object→string, but no external
  callers exist so the change is safe).
- sdk/src/query/workstream-inventory.ts: 196 → 143 lines.
  Same shape, sync fs (the SDK was already sync — surprise from
  recon). Types re-exported from the Builder.

- sdk/package.json: gen:workstream-inventory-builder and
  check:workstream-inventory-builder-fresh scripts.
- package.json: proxy for the freshness check.
- .githooks/pre-commit: drift block.
- .github/workflows/test.yml: drift check step.
- CONTEXT.md: amended "Workstream Inventory Module" entry
  to document the Builder/Reader split.
- docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json:
  +1 module count, +1 row for the generated builder.

- Full suite: 9229/9229 pass (baseline 9215 + 14 net new from
  the parity assertions).
- Vitest: 18 Builder fixtures pass.
- Reader shrink: -27 lines on CJS, -53 lines on SDK.
- Net diff (modified files only): +68 / -133 = 65-line
  reduction. New files (Builder, generator, freshness check,
  parity test) add ~600 lines of new structured code.

1. `isCompletedInventory` signature changed from
   isCompletedInventory(inventory: object) to
   isCompletedInventory(status: string). Original CJS exported
   the object form but no external caller passed an object —
   they all passed inventory.status. Verified by grep before
   committing.
2. Generator preamble. The compiled ESM uses
   `import { relative } from 'node:path'`, making `relative`
   a free variable in `buildWorkstreamInventory`. The generator
   emits `const relative = path.relative;` so the captured
   function body works in CJS.
3. Freshness check imports the generator. The freshness check
   imports the generator's buildWorkstreamInventoryBuilderCjs()
   function directly rather than duplicating generation logic.
   Cleaner than Phase 1/2; future generators should follow this.

Shareable via the Builder/Reader pattern in future enhancements:
- frontmatter (pure YAML/markdown parsing)
- plan-scan (pure PLAN.md structure parsing)
- decisions (pure decision-record parsing)
- secrets (regex-based detection in text)
- uat (UAT-criteria parsing)

Structural divergence — different approach needed:
- state — sync vs async file ops; mutation paths differ.
- workstream — lifecycle ops; per-side API surface differs.
- phase, roadmap, init, profile-output, template — large
  surfaces; each its own potential enhancement.

None of these is in scope for Phase 3.

Closes #3544.
2026-05-15 09:35:02 -04:00
Tom Boucher
fa862c77ee Merge pull request #3540 from gsd-build/feat/3536-configuration-module
feat(3536): Configuration Module via shared manifests + generator (Phase 2 of #3524)
2026-05-15 09:33:22 -04:00
Tom Boucher
a31d45eedd Merge pull request #3547 from gsd-build/fix/3541-first-time-baseline-migration-prompt-use
fix(3541): resolve prompt-user migration actions in non-TTY runs; improve error grouping
2026-05-15 09:24:40 -04:00
Tom Boucher
544037e132 fix: honor installer migration resolution env override 2026-05-15 09:16:41 -04:00
Tom Boucher
c39a7e1f6f fix(3541): resolve prompt-user migration actions in non-TTY runs; improve error grouping
Closes #3541

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:16:28 -04:00
Tom Boucher
85d7a8b4b1 fix(3542): prohibit git stash in executor agents — shared stash storage violates worktree isolation
Closes #3542

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:16:26 -04:00
Tom Boucher
4b2de40f90 fix(3536): silence source-grep lint on config-schema parity test
The SDK side of the parity test asserts the source shape of
sdk/src/query/config-schema.ts (must re-export from
../configuration/index.js; must NOT contain inline `new Set([...])`
literals). Runtime/IR comparison cannot distinguish a re-export from
a redeclared Set with identical contents — only source inspection
catches drift back to inline literals.

Adds the documented `// allow-test-rule:` annotation explaining why
the three `src.includes()` calls are structurally necessary. Test
behavior unchanged; all 6 tests still pass; lint-no-source-grep now
reports 0 violations across 514 test files.
2026-05-15 08:14:28 -04:00
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
a7f0af2ce9 fix(3537): route every phase-number ROADMAP regex through phaseMarkdownRegexSource (#3538)
* fix(3537): route every phase-number ROADMAP regex through phaseMarkdownRegexSource

v1.42.1 added the padding-tolerant `phaseMarkdownRegexSource()` helper but
wired it into only 1 of 8 call sites that build phase-number regexes against
ROADMAP/STATE prose. The other 7 used raw `escapeRegex(phaseNum)` or partial
`0*${escapeRegex(...)}` (tolerated extra padding, not missing), so when
skills passed the resolved padded form (`02.7`) against un-padded ROADMAP
prose (`### Phase 2.7:`, `- [ ] **Phase 2.7:**`), the verbs silently no-op'd
while reporting success.

This consolidates every phase-number ROADMAP/STATE regex through the
canonical helper:

- Promote `phaseMarkdownRegexSource` from `roadmap.cjs` to `core.cjs` so
  `phase.cjs` and `core.cjs` itself can consume it (no circular dep —
  both already import `core.cjs`).
- Wire the helper into the 7 remaining sites:
  - `core.cjs:getRoadmapPhaseInternal` (replaces hand-rolled `isNumeric`
    branch that only padded integers, not decimals).
  - `roadmap.cjs:cmdRoadmapGetPhase` (searchPhaseInContent escapedPhase).
  - `roadmap.cjs:cmdRoadmapAnalyze` checkbox lookup.
  - `roadmap.cjs:cmdRoadmapAnnotateDependencies` phase header lookup.
  - `phase.cjs:cmdPhaseNextDecimal` ROADMAP prose scan.
  - `phase.cjs:cmdPhaseInsert` target anchor + decimal scan + header.
  - `phase.cjs:cmdPhaseComplete` (3 regexes: checkbox, plan-count,
    REQUIREMENTS extraction).

Adds `tests/bug-3537-padded-id-against-unpadded-roadmap.test.cjs` — a
parity-style regression matching CONTEXT.md DEFECT.GENERATIVE-FIX: for
each user-facing verb, asserts that the padded form (`02.7`) and the
un-padded form (`2.7`) produce identical ROADMAP.md against an identical
fixture. Includes one control case (`update-plan-progress`, already wired
in 1.42.1) to prove the parity assertion is non-vacuous.

Closes #3537

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

* chore(3537): add changeset fragment (pr: placeholder, amended post-create)

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

* chore(3537): pin changeset pr: field to #3538

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-05-14 23:17:38 -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
e6790238aa Merge pull request #3527 from gsd-build/fix/3523-bug-cjs-loadconfig-warns-top-level-branc
fix(3523): stop false 'branching_strategy will be ignored' warning in CJS loadConfig
2026-05-14 19:58:08 -04:00
Tom Boucher
cda6cd3e06 Merge pull request #3526 from gsd-build/fix/3521-workflows-quick-md-two-bugs-in-post-merg
fix(quick): pin cleanup-loop CWD to project root before bare git commands (#3521)
2026-05-14 19:57:45 -04:00
Tom Boucher
7c5adeaad1 test: allow runtime output assertions 2026-05-14 19:47:48 -04:00
Tom Boucher
75dffc8069 fix: address branching strategy review findings 2026-05-14 19:44:01 -04:00