Commit Graph

2786 Commits

Author SHA1 Message Date
Tom Boucher
afbf0d80de Merge pull request #3609 from gsd-build/fix/3582-codex-0-130-0-gsd-1-42-2-installation-su
fix(3582): regression test for Codex install skill-materialization
2026-05-15 23:04:45 -04:00
Tom Boucher
2cf4c8e8ec Merge pull request #3611 from gsd-build/fix/3587-bug-security-check-ship-ready-shell-inje
fix(3587)(security): argv-based subprocess for check.ship-ready
2026-05-15 23:04:42 -04:00
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
afb0692522 Merge pull request #3624 from gsd-build/fix/3591-bug-gsdtools-native-runtime-drops-workst
fix(3591): forward workstream to native registry dispatch
2026-05-15 23:04:16 -04:00
Tom Boucher
8b31739a01 Merge pull request #3626 from gsd-build/fix/3589-bug-security-sdk-planningpaths-accepts-u
fix(3589)(security): validate workstream in relPlanningPath against traversal
2026-05-15 23:04:13 -04:00
Tom Boucher
66a429adf4 docs(3621): forbid bundling test-fixture updates into docs: commits (#3625)
Adds a Pull Request Guidelines bullet making explicit what v1.42.3
hotfix taught us the hard way: when a production change makes an
existing test assertion stale, the test correction must be its own
test: (or fix:) commit, not bundled into a docs: commit that also
explains the change.

The release-sdk hotfix cherry-pick filter routes by commit-subject
prefix (fix:, chore:, test: — see release-sdk.yml). A docs: commit
that hides a test fix is invisible to the picker. The result is a
half-shipped state on the hotfix branch: production code changed,
test assertion stale, CI red.

This is the upstream contributor-side mitigation. The picker-side
fix landed in PR #3623 (broadens the prefix filter and the
classifier's CI-gating-path detection); this PR documents the
upstream discipline that keeps the picker from getting fooled in
the first place.

Refs #3621

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 23:04:08 -04:00
Tom Boucher
11a6d3fdd6 test(3589): assert empty-string workstream fallback 2026-05-15 22:58:36 -04:00
Tom Boucher
8bc4148eea test(3582): annotate adapter text assertion for source-grep lint 2026-05-15 22:56:32 -04:00
Tom Boucher
6e9f8101da fix(3605): retire stale slash syntax and satisfy planner size gate 2026-05-15 22:56:32 -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
a2967bfa24 fix(3589)(security): validate workstream in relPlanningPath against traversal
`relPlanningPath(workstream)` previously called `posix.join('.planning',
'workstreams', workstream)` without validating the workstream argument.
Direct SDK callers — and `planningPaths` / `ContextEngine` which both
forward through `relPlanningPath` — could pass values like
`'../../../outside'`, `'foo/bar'`, or `'foo\\bar'` and route planning
operations outside the intended `.planning/workstreams/<name>` subtree.

The env-sourced workstream code path in `planningPaths` already validated
via `validateWorkstreamName` (line 444-445, pre-filtering to `null` on
failure per the #2791 silent-fallback contract). Explicit SDK arguments
had no equivalent gate.

Fix: validate inside `relPlanningPath` using the same shared
`validateWorkstreamName` policy. Every caller — direct SDK use,
`planningPaths`, `ContextEngine` — fails closed at the same seam.
Empty/undefined workstream still returns `.planning` for back-compat
(treated as "no workstream provided"); non-empty invalid names throw a
synchronous Error with the offending value in the message.

Env-sourced behaviour is unchanged: `planningPaths` continues to filter
invalid env values to `null` before they reach `relPlanningPath`, so the
silent-fallback path for malformed `GSD_WORKSTREAM` env still works.

Regression test
(sdk/src/bug-3589-planning-paths-validation.test.ts):

  - 9 traversal/invalid cases (.., /, \\, spaces, .hidden, /abs,
    -leading-hyphen) all throw with a `/workstream/i`-matching message.
  - Valid names (`frontend`, `api_v2`, `alpha.beta-1`) continue to
    produce the expected `.planning/workstreams/<name>` path.
  - `planningPaths('/tmp', '../../../outside')` rejects before path
    construction (proven via try/catch — resultPath stays null).
  - Valid workstream + `planningPaths` produces the expected subtree
    (`.planning/workstreams/frontend/STATE.md` etc.).
  - Omitted workstream still returns root `.planning` with no `workstreams`
    segment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:52:14 -04:00
Tom Boucher
ed4dfbea16 test(3591): assert native dispatch forwards projectDir/workstream 2026-05-15 22:44:25 -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
ab51253370 fix(3599): treat prefixed phase headings as section boundaries 2026-05-15 22:40:18 -04:00
Tom Boucher
06da35a95a fix(3601): stop phase removal at non-numeric peer headers 2026-05-15 22:40:18 -04:00
Tom Boucher
4824aefe42 fix(3591): forward workstream to native registry dispatch in GSDToolsRuntime
createGSDToolsRuntime accepted opts.workstream and forwarded it to the
QuerySubprocessAdapter (line 38) but the QueryNativeDirectAdapter's
dispatch closure dropped it:

  dispatch: (registryCommand, registryArgs) =>
    registry.dispatch(registryCommand, registryArgs, opts.projectDir)

`registry.dispatch(command, args, projectDir, workstream?)` accepts a
4th workstream argument and forwards it to handlers. When a GSDTools
instance was created with a workstream, the native fast-path silently
routed planning-path queries to the root `.planning/` tree instead of
`.planning/workstreams/<name>/`. Subprocess dispatch correctly carried
the workstream; native dispatch did not — runtime-bridge mode parity
broke for any workstream-aware GSDTools consumer using the native path.

One-line fix: pass opts.workstream as the 4th arg to registry.dispatch.

Regression test exercises three paths:

  1. Constructor-seam unit test: spy on QueryNativeDirectAdapter,
     capture the dispatch closure, verify it reaches a registry that
     reports the unknown-command error message.
  2. Back-compat: same with workstream omitted — closure still reaches
     the registry.
  3. End-to-end: spy on createRegistry to inject a probe registry with a
     registered handler that records its args. Invoke through
     runtime.bridge.dispatchHotpath(). Assert the handler observed
     workstream='frontend-ws' as its 3rd arg.

RED verified: end-to-end probe fails on pre-fix tree with
`expected undefined to be 'frontend-ws'`. GREEN after the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:35:13 -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
ac34ab2cec fix(3587): address codex-review findings on PR #3611
Codex review surfaced 2 MED + 2 LOW in-scope findings (3 LOW were
pre-existing or out-of-scope, see below); all in-scope items addressed:

1. (MED, test sensitivity) The gh probe test only checked the boolean
   return shape, so a future change that re-introduces shell-string
   execSync for the gh path would pass. Added an
   `architectural-invariant` structural test that reads the production
   source file and asserts:
     - no `execSync(` call appears anywhere in code,
     - no `spawnSync` with `shell: true`,
     - `execFileSync` is the only imported child_process primitive,
     - every options object explicitly pins `shell: false`.
   This is the canonical pattern from CONTRIBUTING.md for invariants
   that behavioral tests can't observe — the defect is the *presence*
   of the shell parsing primitive, not its output.

2. (MED, cross-platform) The execFileSync options didn't explicitly pin
   `shell: false`. Default is already false, but spelling it out (a)
   documents the architectural invariant at the call site, (b) prevents
   a future options-spread refactor from silently flipping it, and
   (c) hardens against a Windows `git.cmd` shim path that could
   otherwise route through cmd.exe.

3. (LOW, test visibility) The exploit-blocked test silently `return`ed
   when git rejected the payload branch name on a stricter platform,
   turning a coverage loss into a stealth pass. Replaced with vitest's
   `ctx.skip()` so a lane that loses coverage now shows up in the skip
   count.

Out of scope, intentionally not changed:
- The `try/finally` at sdk/src/query/check-ship-ready.test.ts:79 is
  pre-existing test code from before this PR. One-concern-per-PR rule
  says no drive-by cleanup.
- The "use createTempGitProject helper" suggestion: that helper lives
  in tests/helpers.cjs (root, node:test world). SDK tests use vitest
  with their own ad-hoc tmpdir pattern; matching the SDK convention.
- The afterEach cleanup uses `rm` directly, matching the surrounding
  SDK test convention; not changing without broader SDK-side refactor.

Validation:
- SDK unit suite via vitest: 1,870/1,870 pass (+1 invariant test).
- Full root suite via gsd-test-both: 10,676/10,676 Mac AND Linux Docker,
  zero cross-platform diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:51:05 -04:00
Tom Boucher
4e7e83bf58 fix(3587)(security): argv-based subprocess for check.ship-ready
`gsd-sdk query check.ship-ready <phase>` built a git command as a shell
string with the current branch name interpolated. Git branch names can
legally contain shell metacharacters, so a repo checked out on a
malicious branch like `foo;touch${IFS}INJ;bar` executed arbitrary shell
commands.

Vulnerability site (pre-fix):

  sdk/src/query/check-ship-ready.ts:50
    runSyncSafe(`git config --get branch.${current_branch}.merge`, cwd)
  → execSync('git config --get branch.foo;touch${IFS}INJ;bar.merge')
  → /bin/sh -c parses three commands; the middle one runs `touch INJ`
    in the project dir and creates the sentinel file.

Manually reproduced on git 2.53.0:
  - refname `foo;touch${IFS}INJ;bar` is accepted by `git check-ref-format`
    and by `git checkout -b`.
  - `current_branch` returned from `git rev-parse --abbrev-ref HEAD`
    contains the metacharacters verbatim.
  - Interpolation into the buggy execSync call creates the sentinel.

Fix:

- Replace `runSyncSafe(cmd: string, cwd)` (execSync, shell-string) with
  `runArgvSafe(file, args: readonly string[], cwd)` (execFileSync,
  argv-based, no shell).
- Same shape for the boolean wrapper: `boolArgvSafe`.
- Convert all 7 subprocess sites in the module to argv form:
  - `git status --porcelain`
  - `git rev-parse --abbrev-ref HEAD`
  - `git config --get branch.<name>.merge`   ← the interpolation site
  - `git rev-parse --verify main`
  - `git remote`
  - `gh --version`
  - `which gh`
- Shell is never invoked. Branch names — even ones with `;`, `$IFS`,
  backticks, `$()` — are passed as a single argv element and treated
  as opaque data.

Regression test (`sdk/src/query/check-ship-ready.test.ts`):

- `#3587: branch name with shell-injection payload does not execute
  injected command` — creates a real git repo, checks out the proven
  exploit branch `foo;touch${IFS}INJECTED_BY_3587;bar`, runs
  checkShipReady, and asserts the sentinel file does NOT exist. This
  test FAILS on the unfixed code (verified pre-implementation) and
  PASSES on the fixed code — true red→green TDD.
- `#3587: round-trips a metacharacter branch name verbatim in
  current_branch` — positive proof the branch name survives argv as
  data (would fail if a future change re-introduces shell quoting).
- `#3587: gh probe does not invoke a shell` — locks the gh path
  against a future regression that might add an interpolation site.

Validation:
- SDK unit suite via vitest: 1,869/1,869 pass.
- Full root suite via gsd-test-both (per CLAUDE.md): 10,676/10,676
  on Mac AND 10,676/10,676 on Linux Docker, zero cross-platform diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:42:52 -04:00
Tom Boucher
8e728fd2d6 fix(3582): address codex-review findings on PR #3609
Codex review surfaced 1 HIGH, 2 MED, 2 LOW; all addressed:

1. (HIGH, CONTRIBUTING.md violation) Adapter body assertion was raw
   text matching `content.includes('<codex_skill_adapter>')` on the
   rendered SKILL.md. Replaced with a structural assertion against
   the exported builder: `content.includes(getCodexSkillAdapterHeader(name))`.
   The expected value is now the full closed adapter block produced
   by the production IR — open tag, body, and `</codex_skill_adapter>`
   closing tag — so a truncated, empty, or missing-closing-tag adapter
   cannot satisfy the assertion. Pre-flight sanity-checks the builder
   itself emits the expected open/close shape.

2. (MED, sensitivity) Count-only check on installed skills was
   replaceable by a same-count partial install that swapped real
   commands for bogus `gsd-*` dirs. Replaced with `deepStrictEqual`
   on the sorted full set, computed from `commands/gsd/**/*.md` via
   a local `expectedSkillNames()` walker that mirrors the installer's
   naming rule (nested dirs collapse to `gsd-<dir>-<file>`).

3. (LOW, flakiness) `runCodexInstallCaptured()` could throw after
   creating the temp `codexHome` but before returning, so the
   describe-level `afterEach` couldn't see the path and the dir would
   leak. Added a try/catch around `install()` that cleans up the
   temp dir before rethrowing.

4. (LOW, flakiness) Harness mutates process-global `console.*`,
   `process.env`, `process.cwd()` — added `{ concurrency: false }` to
   the describe block, matching the existing convention in
   `tests/bug-3562-codex-install-skill-surface.test.cjs:45`.

5. (LOW, CONTRIBUTING.md helpers) Local `parseFrontmatter()` was
   duplicating the shared helper. Switched to the canonical
   `parseFrontmatter` exported by `tests/helpers.cjs`; also adopted
   `createTempDir` and `cleanup` from the same module for consistency.

Validation:
- New test still 5/5 pass.
- Full suite via gsd-test-both: 10,682/10,682 on Mac AND Linux Docker,
  zero cross-platform diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:21:12 -04:00
Tom Boucher
71f39eb778 fix(3582): regression test for Codex install skill-materialization
GSD 1.42.2 reported a successful Codex install but printed
`Skipped Codex skill-copy generation (Codex discovers official skills
directly)` and left users with no routable `$gsd-*` entrypoints in
Codex CLI 0.130.0+. Confirmed reproducible on Windows 11 and macOS by
three independent reporters.

Root cause: Codex CLI 0.130.0 does NOT auto-discover commands from
`~/.codex/get-shit-done/workflows/*.md` or `agents/*.md`. It only
registers slash commands derived from `~/.codex/skills/<name>/SKILL.md`.
The "Codex discovers official skills directly" assumption was wrong.

The fix already shipped in #3562 — `bin/install.js` now calls
`copyCommandsAsCodexSkills()` for the Codex install path, materializing
one `SKILL.md` per `commands/gsd/*.md` with Codex-flavored frontmatter
and the `<codex_skill_adapter>` body. Verified locally: a Codex global
install into a temp `CODEX_HOME` produces 67 `gsd-*/SKILL.md` files
including `gsd-map-codebase` (the literal command from the bug report).

This change adds the regression test the triage diagnose pass called
for. It pins five facets of the contract so the 1.42.2 failure mode
cannot silently come back:

1. `<CODEX_HOME>/skills/gsd-*/SKILL.md` exists for every shipped
   command (count matches `commands/gsd/**/*.md` exactly).
2. Each generated SKILL.md has YAML frontmatter with `name:` matching
   the directory and a non-empty `description:`.
3. Every SKILL.md body contains the `<codex_skill_adapter>` block —
   without it Codex can't route `$gsd-<cmd>` invocations.
4. The five representative commands named in the report and triage
   (`gsd-map-codebase`, `gsd-execute-phase`, `gsd-plan-phase`,
   `gsd-new-project`, `gsd-health`) are all present.
5. Installer output never prints "Skipped Codex skill-copy generation"
   or "Codex discovers official skills directly", and DOES print
   "Installed N skills to skills/" — locking the success-while-skipping
   anti-pattern out.

Validation:
- New test: 5/5 pass on Mac and Linux Docker.
- Full suite (`gsd-test-both` per CLAUDE.md): 10,682/10,682 on Mac,
  10,682/10,682 on Linux Docker, zero cross-platform diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:10:55 -04:00
Tom Boucher
38c35a608d docs: add QA test examples 2026-05-15 18:16:33 -04:00
Tom Boucher
823b4ece0a Merge pull request #3580 from gsd-build/codex/revert-pr-3578-rate-limit
fix: revert #3578 due failed CodeRabbit rate-limit gate
2026-05-15 15:21:46 -04:00
Tom Boucher
05d4ba8147 Revert "Merge pull request #3578 from gsd-build/fix/3569-init-plan-phase-status"
This reverts commit a244dd7fc3, reversing
changes made to 8f95b2fe23.
2026-05-15 15:16:55 -04:00
Tom Boucher
a244dd7fc3 Merge pull request #3578 from gsd-build/fix/3569-init-plan-phase-status
fix(3569): surface phase_status from init.plan-phase; gate /gsd:plan-phase on closed phases
2026-05-15 15:11:38 -04:00
Tom Boucher
8f95b2fe23 Merge pull request #3564 from gsd-build/fix/3560-validate-health-w007-false-positive-scan
fix: prevent archived phase dirs from triggering W007
2026-05-15 15:11:10 -04:00
Tom Boucher
0080c791ed chore(3569): add changeset fragment for PR #3578
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:07:49 -04:00
Tom Boucher
1cdfe3f452 chore(changeset): add fragment for #3560 2026-05-15 15:07:34 -04:00
Tom Boucher
21ae65f433 fix(3560): ignore archived phases for W007 warnings 2026-05-15 15:07:34 -04:00
Tom Boucher
0aa4bd92fb fix(3569): surface phase_status from init.plan-phase + gate /gsd:plan-phase on closed phases
Adds a new `phase_status` field to the `init.plan-phase` SDK + CJS query
output and a §1.5 "Closed-Phase Gate" in workflows/plan-phase.md that
short-circuits on closed phases instead of silently replanning over
shipped code.

## What was broken

`gsd-sdk query init.plan-phase <N>` returned the same "ready to plan"
payload for a closed phase (REQUIREMENTS Met, VERIFICATION.md status:
passed, ROADMAP flipped) as for an open one. No field signaled closure,
so `/gsd:plan-phase --reviews` happily replanned over closed phases —
risking documentation drift on already-shipped code.

## Fix

- Export `determinePhaseStatus` from `commands.cjs` (already present, was
  module-private).
- Both `cmdInitPlanPhase` (CJS) and `initPlanPhase` (TS SDK) now compute
  `phase_status` from plan/summary counts + VERIFICATION.md status using
  the existing `determinePhaseStatus` helper — the project-wide phase
  lifecycle vocabulary (Pending | Planned | In Progress | Executed |
  Complete | Needs Review). No directory yet → Pending.
- Workflow `plan-phase.md` adds §1.5 "Closed-Phase Gate":
  - `phase_status == "Complete"` with `--reviews` → hard-stop, no
    override (replanning a closed phase via review feedback is never
    legitimate; concerns belong in a follow-up phase or new issue).
  - `phase_status == "Complete"` without `--force` → exit with a clear
    notice pointing at VERIFICATION.md.
  - `phase_status == "Complete"` with `--force` → continue with a
    transcript banner so the deliberate replan is visible.

`Executed` and `Needs Review` are intentionally not gated — those mean
planning finished but verification did not pass, and replanning is the
correct next step.

## Tests

- SDK: 4 new `phase_status` cases in init.test.ts covering Pending /
  Planned / Executed / Complete transitions.
- Existing init.plan-phase golden parity test continues to pass (the
  `researcher_model: '' vs sonnet` drift in that test predates this
  change and is unrelated).
- Full Mac+Docker suite: 9323 / 9323 passed (Mac), 9318 / 9323 passed
  (Docker, 5 skipped).

Fixes #3569
2026-05-15 15:04:39 -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
54d27b6542 Merge pull request #3565 from gsd-build/fix/3559-validate-health-w006-false-positive-for-
fix: avoid W006 for not-started future phases
2026-05-15 15:03:08 -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
1783d10205 Merge pull request #3573 from gsd-build/fix/3566-codex-hooks-canonical-feature-key
fix(3566): emit canonical [features].hooks; recognize legacy codex_hooks alias
2026-05-15 15:03:00 -04:00
Tom Boucher
56a95adeb6 Merge pull request #3574 from gsd-build/feat/3567-state-router-bridge-delegation
feat(3567): state.* router delegates via executeForCjs + bundled Phase 5.0 worker fix (Phase 5.1 of #3524)
2026-05-15 15:01:47 -04:00
Tom Boucher
ec4025d538 test(3566): clarify ownership semantics for legacy codex_hooks docs 2026-05-15 14:54:58 -04:00