Commit Graph

102 Commits

Author SHA1 Message Date
Tom Boucher
334a64168e chore(npm): rebrand packages to @opengsd scope (#127)
* chore(npm): rebrand packages to @opengsd scope

Rename:
- get-shit-done-redux → @opengsd/get-shit-done-redux
- @gsd-redux/sdk → @opengsd/gsd-sdk

Add publishConfig.access=public for first-time scoped publish.
CLI binary names (get-shit-done-redux, gsd-sdk, gsd-tools) unchanged.

Sweeps install commands, npx invocations, CI publish/version-check
workflows, tests, docs, READMEs (all translations), and the
PACKAGE_NAME constant in check-latest-version.

Bumps qs 6.15.1 → 6.15.2 to clear a moderate advisory surfaced by
the audit-clean test (GHSA-q8mj-m7cp-5q26).

Closes #126

* chore: pin 2.0.0 release + remove canary workflow

- Bump both packages 1.50.0-canary.0 → 2.0.0 for first @opengsd publish
- Remove .github/workflows/canary.yml and canary dist-tag handling in
  release.yml / release-sdk.yml
- Drop canary section from VERSIONING.md

Refs #126

* chore: address review findings + harden tarball-smoke timeout

- .changeset/opengsd-org-rename.md: match project's custom
  parse.cjs frontmatter (type: Changed / pr: 127); the scoped
  @changesets/cli keys were silently rejected.
- CONTEXT.md: drop two canary-stream policy lines and a dangling
  DEFECT.CANARY-VERSION-LEAK.cross-ref now that canary.yml is gone.
- tests/release-tarball-smoke.install.test.cjs: pass
  timeout: 600_000 for npm pack + global install; the 3-minute
  runNpm default was timing out on slower Docker hosts (cartographer).

Refs #126

* fix(sdk): add missing type/runtime devDependencies for build

prepublishOnly invokes tsc which couldn't resolve @types/node,
@types/ws, or synckit. They had been hoisted from root but were
not declared in sdk/'s own package.json — first publish from a
clean SDK tree failed.

Refs #126

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

* fix(ci): use npm pack stdout instead of glob to find tarball

`npm pack --silent` for a scoped package (@opengsd/get-shit-done-redux)
produces `opengsd-get-shit-done-redux-*.tgz`, not `get-shit-done-redux-*.tgz`.
Capture the filename from stdout instead of a hardcoded glob so the step
works regardless of package name format.

Fixes smoke (ubuntu-latest, 22, false) CI failure.

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

* ci: treat workflow-file changes as test-skip eligible

`.github/workflows/install-smoke.yml` (and other workflow files)
were in neither `test.yml` paths nor `test-skip.yml` paths-ignore,
so neither workflow ran on a workflow-only commit — leaving the
required test-skip check perpetually missing.

Refs #126

* chore: reset version to 1.0.0 for first @opengsd publish

Nothing has been published yet under the @opengsd scope, so the
inaugural release uses 1.0.0 rather than 2.0.0. The "major bump"
in the changeset reflects the breaking install-command change for
users migrating from the prior unscoped `get-shit-done-redux`, not
a numeric continuation from a 1.x line under the new identity.

Refs #126

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:22:41 -04:00
Tom Boucher
dff176bfd2 chore: rebrand to GSD-redux/get-shit-done-redux
Mirror of code, issues, and PRs from the upstream gsd-build/get-shit-done,
which appears compromised or abandoned (maintainer unreachable since
2026-04-01; $GSD token linked to rug-pull).

- Adds rebrand notice block at top of English README
- Removes $GSD token badge and @gsd_foundation X badge (keeps Discord)
- Renames npm packages: get-shit-done-cc -> get-shit-done-redux,
  @gsd-build/sdk -> @gsd-redux/sdk
- Updates all repo URLs across docs, workflows, package.json, bin/
- Updates ci@gsd-build -> ci@gsd-redux in workflow git identities
- Leaves CHANGELOG and .changeset/* alone (historical, time-stamped)
2026-05-22 08:27:07 -04:00
Tom Boucher
ca2644a71a fix(worktree): unlock-retry on locked cleanup + startup orphan sweep (#3707) (#3719)
* fix(worktree): unlock-retry on locked cleanup + startup orphan sweep (#3707)

Two root causes fixed:

1. **In-session cleanup blocked**: `executeWorktreeWaveCleanupPlan` now attempts
   `git worktree unlock <path>` then retries `git worktree remove --force` when the
   initial single-force remove fails on a locked worktree. Previously every cleanup
   after a successful merge was silently blocked.

2. **Cross-session orphan accumulation**: new `reapOrphanWorktrees` helper sweeps
   `.git/worktrees/*/locked` at startup. It reaps entries where the pid is dead,
   the branch tip is an ancestor of the default branch (ancestry guard prevents data
   loss on squash-merge repos), and the lock mtime is older than 5 minutes (race
   guard). Wired into `quick.md` and `execute-phase.md` startup blocks guarded by
   `USE_WORKTREES != false`.

SDK: adds `worktree.reap-orphans` query command (routes through gsd-tools.cjs).
Tests: 11 real-fs tests covering unlock-retry, dead-pid reap, live-pid skip,
unmerged skip, fresh-mtime skip, idempotent double-call, and structural wiring.

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

* chore(changeset): add Fixed fragment for PR #3707 (worktree orphan cleanup)

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

* fix(worktree): fix test portability on Windows + macOS for bug-3707 reap tests

- worktreeMeta helper: replace /\/\.git$/ with /[/\\]\.git$/ so the
  gitdir path suffix is stripped on both Windows (backslash) and Unix.
- worktreeMeta helper: normalize CRLF→LF before splitting porcelain
  blocks, fixing block parsing when git emits CRLF on Windows.
- reapOrphanWorktrees: replace single 'main' rev-parse with a
  [defaultBranch, 'main', 'master'] candidate loop so test fixtures
  without a remote origin (where branch may be 'master') don't bail
  early. Intentionally excludes 'HEAD' to prevent false reaping when
  HEAD is detached or on a feature branch (Codex adversarial finding).

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

* fix(worktree): CI green — macOS symlink path, Windows test helper, pid portability, EPERM liveness

Four fixes to get macOS + Windows CI from red to green:

1. **macOS symlink mismatch** (worktree-safety.cjs): `reapOrphanWorktrees` now
   builds a canonical→listed path map from `git worktree list --porcelain` using
   `fs.realpathSync.native`. Uses the listed path (as git knows it) for
   `git worktree unlock/remove`, not the gitdir-derived path.  Fixes the
   `/var/folders` vs `/private/var/folders` discrepancy on GitHub macOS runners
   where `git worktree unlock <realpath>` was silently failing because git's
   list stored the unresolved symlink path.

2. **Windows path separator in test helper** (test file): `worktreeMeta`
   `.replace(/\/\.git$/, '')` → `.replace(/[/\\]\.git$/, '')`. On Windows,
   git writes backslash separators in the gitdir file; the Unix-only regex was
   causing `Cannot find .git/worktrees/<name>` for all Suite 2 tests.

3. **Non-portable PID in tests** (test file): All `'999999'` dead-PID literals
   replaced with `deadPid()` helper that spawns a real short-lived child, captures
   its PID, and returns it after exit. Eliminates flakiness on Linux systems where
   `pid_max` can reach 4194304, making 999999 a live PID.

4. **EPERM fail-closed in isPidAlive** (worktree-safety.cjs): `catch { return false }`
   → checks `err.code === 'EPERM'` and returns `true` (alive). On Windows and
   cross-user scenarios, `process.kill(pid, 0)` throws EPERM for live but
   inaccessible processes; treating that as dead would reap a live worktree.

Adversarial review via codex confirmed:
- Squash-merge repos: fail-closed (CONCERN, not BUG — by design, not data-loss)
- canonicalToListed map: SAFE (fail-closed on realpathSync error)
- Concurrent reapers: SAFE (both prune; second gets skipped: remove_failed)
- Startup blocking: CONCERN (no global cap, 10s/call × N worktrees) — tracked,
  not fixed here (requires separate perf work)
- gsd-sdk missing: SAFE (quick.md checks and fails fast with guidance)

All 27 local tests + Docker (holodeck) green.

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

* fix(worktree): address codex adversarial findings — fail-closed default branch + CRLF map

Two fixes from codex adversarial review of PR 3718:

1. **Default branch resolution (data-loss risk)**: `reapOrphanWorktrees` now
   uses `refs/remotes/origin/<branch>` exclusively when a remote is configured.
   If `origin/HEAD` is absent but a remote exists, we bail out (fail-closed)
   rather than falling back to a local `main`/`master` that may not be the
   real integration branch.  The `main`/`master` fallback is only used when
   there is provably no remote (local-only test fixtures).

2. **CRLF normalization in canonical-path mapper**: The `worktree list
   --porcelain` output was split on '\n\n' without normalizing CRLF first.
   On Windows, git emits CRLF, which caused block-splitting to fail and
   left the canonicalToListed map only partially populated, weakening the
   symlink/path-mismatch fix introduced earlier.

3. **Windows 8.3 short-path fix (test helper)**: Both `beforeEach` blocks
   now call `resolvedTmpDir()` which pre-resolves `os.tmpdir()` via
   `fs.realpathSync.native` so temp paths avoid RUNNER~1-style short names
   that git stores in long form, causing worktreeMeta path comparisons to
   fail on Windows CI.

All 11 real-fs + 16 unit tests green locally.

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

* fix(worktree): adversarial findings + macOS CI path-mismatch fix

## Root cause (macOS CI fail)
`reapOrphanWorktrees` stored `worktreePath` (gitdir-derived, real path via
git's symlink resolution, e.g. `/private/var/folders/…`) in results, while
the test's `wtDir` used the unresolved symlink form (`/var/folders/…`).  After
reaping, `canonicalPath(wtDir)` can no longer call `realpathSync.native`
(directory gone), so it falls back to `path.resolve` — which returns the
symlink form — causing the `result.find()` comparison to miss.

## Fixes applied

### Source — worktree-safety.cjs
1. **Finding 1 (fail-closed PID check)**: Non-parseable lock content (e.g.
   `"Locked by claude-code agent-xxx"`) is now treated as ALIVE with reason
   `lock_owner_unknown`, not as dead.  Previously it fell through as dead.
2. **Finding 1b (EPERM safe)**: `isPidAlive` call wrapped in try/catch; any
   thrown error (EPERM = process exists but cross-user on Windows) → ALIVE.
3. **Finding 2 (startup warning)**: `cmdWorktreeReapOrphans` now writes a
   one-line stderr warning when ≥1 entry is skipped or when reaper throws,
   while keeping exit-zero so workflows don't break.
4. **Finding 3 (default-branch discovery)**: Local-only fallback now tries
   `init.defaultBranch` config and HEAD symref before `main`/`master`, so
   repos configured with `trunk`, `dev`, etc. get correct orphan detection.
5. **macOS path fix**: Result entry for reaped worktrees now uses `gitKnownPath`
   (from `git worktree list`) instead of `worktreePath` (from gitdir file),
   ensuring the caller always sees the path git uses for the worktree.

### Test — bug-3707-locked-worktree-cleanup.test.cjs
6. **macOS CI fix**: Pre-compute `wtDirCanonical = canonicalPath(wtDir)` before
   calling `reapOrphanWorktrees` so the comparison works after removal.
7. **Gap 1**: New test — Claude Code lock format (`"Locked by claude-code …"`)
   must not be reaped; asserts `status=skipped, reason=lock_owner_unknown`.
8. **Gap 2**: New test — `isPidAlive` throwing EPERM → must not reap.
9. **Gap 3**: New test — repo with `init.defaultBranch=trunk`; merged worktree
   must be reaped (verifies trunk is discovered as the integration branch).

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

* fix(test): raise waitForStoppedAt timeout 2 s → 5 s for Windows/Node22 CI load

Subprocess write latency exceeds 2 s on loaded windows-latest/Node22 runners
(test duration was 6181 ms); 5 s gives sufficient headroom without changing
any production behaviour.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 15:22:12 -04:00
Tom Boucher
6a5fa59129 feat(3081): auto-trim review prompts for small-context model reviewers (#3708)
* feat(3081): auto-trim review prompts for small-context model reviewers

Adds review.max_prompt_tokens and review.max_prompt_tokens_per_reviewer
config keys. When configured, the /gsd-review workflow deterministically
trims the assembled prompt before sending to each reviewer (drop CONTEXT
→ RESEARCH → REQUIREMENTS; head-shrink PROJECT.md; tail-truncate PLANs
proportionally; reserve disclosure-note tokens upfront). Trim metadata
is recorded in REVIEWS.md frontmatter. Reviewer is skipped with a
warning if even the minimum review set exceeds the budget.

Closes #3081

* fix(3081): register prompt-budget in SDK query registry and update inventory manifest

review.md references `gsd-sdk query prompt-budget` at three call sites, but the
command had no handler in the SDK registry — failing the registry-integration
drift-guard test on all 6 CI matrix legs. Added a native TypeScript SDK handler
(sdk/src/query/prompt-budget.ts) that ports the applyBudget logic from the CJS
module, registered it in DOMAIN_STATIC_CATALOG, and regenerated
docs/INVENTORY-MANIFEST.json to include the new cli_modules/prompt-budget.cjs entry.

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

* fix(3081): bump ws to 8.20.1 and allowlist prompt-budget sibling pair

Two additional CI failures after the registry fix:

1. ws moderate CVE (GHSA-58qx-3vcg-4xpx, uninitialized memory disclosure):
   The advisory covers ws >=8.0.0 <8.20.1. Both root and sdk/package.json
   pinned ^8.20.0 which resolved to 8.20.0. Bumped both to 8.20.1 to clear
   the npm audit drift-guard test (bug-3588-npm-audit-clean.test.cjs).

2. lint-shared-module-handsync detected the new prompt-budget.ts / prompt-budget.cjs
   sibling pair without an allowlist entry. Added a cooperatingSiblings entry
   to scripts/shared-module-handsync-allowlist.json with classification and
   justification matching the established pattern.

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

* fix(3081): align prompt-budget skip semantics across CJS and SDK dispatch paths

Replace brittle `[ $EXIT -eq 2 ]` guards with `[ $EXIT -ne 0 ]` in all three
local-reviewer blocks (Ollama, LM Studio, llama.cpp) in workflows/review.md.
Any non-zero exit from prompt-budget now triggers a skip with a descriptive
warning — exit 2/11 prints "budget too small", any other non-zero prints
"unexpected exit code". This ensures the SDK bridge dispatch path (exit 11
via GSDError(Blocked)) triggers the same skip as the CJS path (exit 2).

The SDK handler (sdk/src/query/prompt-budget.ts) already writes both metadata
and prompt files before throwing, so no change needed there.

The Ollama block also gains the missing OLLAMA_SKIP guard so the reviewer
invocation is actually skipped (previously the block only suppressed the
OLLAMA_PROMPT_FILE update but still ran the curl invocation).

SDK integration path (hardFailed via GSDError(Blocked) → exit 11) is covered
by handler unit tests in tests/prompt-budget.test.cjs; no gsd-sdk-*.test.cjs
exercising the full bridge dispatch for this command exists yet — that gap
remains and is documented here.

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

* fix prompt-budget trim ordering and review guard follow-ups

* perf: optimize prompt-budget and dedup reviewer trim workflow

* fix(3708): drop source-grep theater tests to satisfy lint-no-source-grep

All four test files added in commit 2df566ed were pure source-grep theater:
they read .cjs / .ts / .md source files and asserted that specific string
literals were present or absent. None exercised runtime behaviour.

Deleted:
- tests/gsd-tools-memory-optimizer.test.cjs   — 7 includes() on gsd-tools.cjs
- tests/prompt-budget-hotpath-optimizer.test.cjs — includes() on prompt-budget.cjs + .ts
- tests/prompt-budget-io-optimizer.test.cjs   — includes() on prompt-budget.ts + gsd-tools.cjs
- tests/review-workflow-budget-dedup.test.cjs — includes() on review.md

Behavioural coverage for the prompt-budget feature already exists in
tests/prompt-budget.test.cjs and tests/prompt-budget-cli.test.cjs (also
added by this PR). No replacement tests needed.

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

* fix(3708): correct budget-pressure threshold and minSet accounting

Two bugs in applyBudget caused premature trimming and false hard-fails:

1. UNNEEDED_TRIM: budgetUnderPressure compared baseTokens against
   effectiveBudget - NOTE_RESERVE_TOKENS, triggering trim pressure 80
   tokens before the budget was actually exceeded. Fix: compare against
   effectiveBudget directly; NOTE_RESERVE_TOKENS are still reserved in
   contentBudget once real pressure is confirmed.

2. FALSE_HARDFAIL: minSet included NOTE_RESERVE_TOKENS unconditionally,
   treating the note as mandatory even when no trim would occur and no
   note would be injected. Fix: exclude NOTE_RESERVE_TOKENS from minSet;
   a prompt that fits untrimmed needs no note and must not hard-fail.

Both fixes applied in CJS and TypeScript implementations. Two regression
tests added (cycles 11 and 12) that reproduce each case behaviorally.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:13:09 -04:00
Tom Boucher
ae63cbe557 feat(3575): Phase 6 — CJS↔SDK seam migration end-to-end complete (#3524) (#3577)
* feat(3575): Phase 6 enforcement hardening + retrospective (#3524 feature-complete)

Phase 6 of the CJS↔SDK hard-seam migration (parent #3524). Final
phase per the PRD. After this lands the migration is feature-complete:
shared Modules from Phases 1-4 are in place, the runtime-bridge
primitive from Phase 5.0 is wired with the state.* family proof in
Phase 5.1 (PR #3574), and Phase 6 hardens the seam against future
drift via lint, CODEOWNERS, and retrospective documentation.

## What landed

- scripts/lint-shared-module-handsync.cjs (274 lines) — the
  drift-prevention gate. Scans bin/lib/*.cjs and looks for same-named
  sdk/src/<name>.ts or sdk/src/query/<name>.ts (excluding generated
  artifacts). Pairs not on the allowlist fail the lint with a clear
  message: either add to allowlist with justification, or migrate to
  a shared Module. Supports --root, --allowlist, --cjs-dir, --sdk-src,
  --warn-all flags for testability.
- scripts/shared-module-handsync-allowlist.json (148 lines) — two
  categories:
  - cooperatingSiblings (14 pairs) — legitimate Readers/Adapters
    that consume shared Modules or run structurally-different
    runtime paths.
  - migrateMeBacklog (8 pairs) — known drift anti-patterns that ARE
    on main today (config, decisions, intel, model-catalog, plan-scan,
    schema-detect, secrets, workstream-name-policy). Lint warns but
    does not fail on these; documented in the retrospective as
    candidate Shared Module migrations.
- tests/lint-shared-module-handsync.test.cjs (285 lines, 11 cases)
  — proves the lint catches new drift, honors the allowlist, and
  exits 0 on the current tree.
- .github/workflows/test.yml — new "Shared Module hand-sync drift
  check" step after the freshness checks.
- .github/CODEOWNERS — appended 11 architecture-owned path rules for
  source-of-truth files (Shared Module dirs, manifest JSONs, runtime
  bridge, lint script, allowlist). Existing blanket rule preserved.
- docs/agents/cjs-sdk-seam.md (280 lines) — full retrospective +
  guide:
  - Migration overview table linking Phases 1-6 with PR numbers.
  - 15 historical drift bugs (#1535 ... #3523) each mapped to the
    Phase 6 enforcement layer that would have blocked them.
  - "Guide: Adding a new Shared Module" — step-by-step using Phase 1
    (state-document) as the worked example.
  - "Guide: Adding a new canonical command" — step-by-step using
    Phase 5.1 (state.update) as the worked example.
  - "Open follow-ups" listing the 8 MIGRATE_ME pairs, per-family
    Phase 5.2+ candidates pending maintainer authorization, sync
    bridge workstream support, and Phase 5.1's parity divergences.
- CONTRIBUTING.md — short cross-reference paragraph in the
  Architecture & Domain Standards section.

## Audit findings

All 5 freshness checks from Phases 0-4 are already wired in CI:
command-aliases, state-document, configuration,
workstream-inventory-builder, project-root. Phase 6 adds the 6th
(hand-sync drift check) for total enforcement coverage.

## Numbers

- Full CJS suite: 9335/9335 pass (baseline 9323 + 11 new lint
  tests + 1 cooperating).
- Lint passes on current tree: 14 cooperating siblings + 8 backlog
  pairs accounted for, 0 unauthorized drift pairs.
- Lint exits 1 (fails CI) on an intentional new hand-synced pair
  added to a fixture — verified by the test suite.

Closes #3575. Closes the structural drift surface of #3524.

* chore(3577): add changeset fragment for Phase 6

* feat(3575): Phase 6 end-to-end completion — CJS↔SDK seam migration done

Per maintainer correction: Phase 6 is THE final phase and must
complete the migration end-to-end. This commit absorbs Phase 5.1's
work (state.* router + worker fix), finishes the remaining per-family
router migrations, completes all five resolvable Shared Module
extractions, resolves the parity divergences, lands native workstream
support in the sync bridge, and ships the lint + CODEOWNERS +
retrospective from the original Phase 6 scope.

After this commit the CJS↔SDK seam migration started in #3524 is
feature-complete. No follow-up "Phase 5.x" or "Phase 7" should be
needed — the only documented carve-outs are three pairs that
intentionally cannot be migrated (config CLI handlers, intel async
wrapper, model-catalog already on the shared-JSON pattern).

Cherry-picked state.* from Phase 5.1 (PR #3574 absorbed). Migrated
verify.*, init.*, phase.*, phases.*, validate.*, roadmap.* via the
same executeForCjs delegation pattern. Migrated the inline
gsd-tools.cjs cases for frontmatter.*, config-* CLI, and non-family
commands (generate-slug, current-timestamp, find-phase, docs-init)
with shared _dispatchNonFamily helper + _tryLoadSdkBridge loader.

CJS-native carve-outs documented: config-path, migrate-config,
detect-custom-files (no SDK counterpart yet); state.complete-phase
(no SDK counterpart yet); validate.context (CJS-only inline logic
with no clean SDK port); phases.archive (SDK-only).

- plan-scan (Module-via-generator from sdk/src/query/plan-scan.ts)
- secrets (Module-via-generator)
- schema-detect (Module-via-generator)
- decisions (Module-via-generator; SDK regex aligned to CJS
  alphanumeric IDs to preserve project compatibility)
- workstream-name-policy (Module-via-generator; SDK extended with
  hasInvalidPathSegment and isValidActiveWorkstreamName that CJS
  callers depend on)

Each ships with: SDK source-of-truth, generator at
sdk/scripts/gen-<name>.mjs, freshness check at
sdk/scripts/check-<name>-fresh.mjs, parity test at
tests/<name>-generator.test.cjs, CJS shim at
get-shit-done/bin/lib/<name>.cjs, scripts in sdk and root
package.json, pre-commit drift block, CI workflow step, CODEOWNERS
rule, INVENTORY.md row.

- config (config.cjs vs sdk/src/config.ts) — CJS file is CLI-handler
  surface (cmdConfigGet/Set/etc.); SDK file is loadConfig wrapper
  (already migrated in Phase 2). Zero logical overlap. Classified
  as CJS-CLI-ONLY in the allowlist.
- intel (intel.cjs vs sdk/src/query/intel.ts) — SDK is the async
  QueryHandler wrapper of the CJS module; intentional split per the
  SDK file's own docstring. Classified as cooperating-sibling.
- model-catalog (model-catalog.cjs vs sdk/src/model-catalog.ts) —
  both already consume sdk/shared/model-catalog.json (ADR-0003).
  No constants duplicated. Classified as ADAPTER-OVER-MODULE.

- state.record-metric: SDK aligned to CJS auto-create of
  ## Performance Metrics section when absent. Parity assertion now
  exact equality.
- state.prune: SDK aligned to CJS disk-based phase counting via
  stateExtractField. Parity assertion now exact equality. SDK unit
  tests updated to match.

GSDTransport.shouldUseNative no longer forces subprocess when
request.workstream is set — the Phase 5.0 worker fix already threaded
workstream through dispatchNative + registry.dispatch, making the
subprocess force unnecessary. state-command-router.cjs's workstream
fallback guard removed. cjs-sdk-seam.md and the regression test
updated to document the resolution.

Unchanged from the previous commit on this branch. The lint now
reports 22 cooperating siblings, 0 backlog pairs. The retrospective
section "Open follow-ups" is reduced to the three intentional
carve-outs above; the four stale subsections (8 MIGRATE_ME pairs,
per-family Phase 5.x candidates, workstream support, parity
divergences) are gone because they're all resolved in this commit.

- Full CJS suite: 9441/9441 pass (baseline pre-Phase-6 was 9323;
  +118 from the Phase 6 work — 11 lint tests + 12 state-router
  parity + 6 verify parity + 3 phase parity + 1 roadmap parity +
  24 plan-scan parity + 20 secrets parity + 18 schema-detect parity
  + 15 decisions parity + 19 workstream-name-policy parity).
- SDK vitest unit: 1863/1863 pass.
- Hand-sync lint: 22 cooperating siblings, 0 backlog pairs.
- All freshness checks: fresh.

Closes #3575. Closes the migration the CJS↔SDK seam was designed
to eliminate (#3524).

* fix(3575): lint-shared-module-handsync emits typed JSON; tests assert on IR

The lint-no-source-grep CI step rejected the original Phase 6 test
file (tests/lint-shared-module-handsync.test.cjs) because it
substring-matched on .stdout/.stderr from the lint script output —
prohibited per CONTRIBUTING.md "Raw Text Matching on Test Outputs".
Fix: add --json mode to the production lint script and assert on
typed IR fields.

## Changes

scripts/lint-shared-module-handsync.cjs:
- New --json flag. When set:
  - Success: emits { ok: true, cooperatingCount, backlogCount, warnings }
  - Unauthorized pairs: emits { ok: false, reason: 'unauthorized_pairs',
    errors: [{ relCjs, tsPaths }], warnings, cooperatingCount }
  - Missing CJS/SDK dir: emits { ok: false, reason: 'cjs_dir_missing'
    | 'sdk_src_missing', path }
- Default (human-readable) output unchanged.
- Warnings section is suppressed in --json mode (still surfaced in the
  IR's `warnings` field for tests to inspect).

tests/lint-shared-module-handsync.test.cjs:
- runLintJson() helper replaces runLint(), invoking the script with
  --json and parsing the IR.
- Every assertion now reads typed fields (payload.ok, payload.reason,
  payload.errors, payload.warnings, payload.cooperatingCount) instead
  of substring-matching stdout/stderr.
- Test count unchanged at 9 cases across 3 describe blocks.
- All pass.

## Verification

- node scripts/lint-no-source-grep.cjs → exit 0, 529 test files
  checked, 0 violations (was: 1 violation in this test file).
- node --test tests/lint-shared-module-handsync.test.cjs → 9/9 pass.
- node scripts/lint-shared-module-handsync.cjs → unchanged
  human-readable output, 22 cooperating siblings, 0 backlog pairs.
- node scripts/run-tests.cjs → 9449/9449 pass.

Addresses CI failure on PR #3577 (Phase 6 of #3524).

* fix(3575): address CodeRabbit review on PR #3577

Six findings resolved:

1. scripts/lint-shared-module-handsync.cjs — allowlist matching now
   pair-aware. Keys composite ${cjs}::${ts} instead of cjs-only, so
   an entry covering one (cjs, ts) pair no longer silently passes a
   sibling at a different ts path with the same module name.
   Header doc-comment also corrected: removed the stale claim about
   GSD_LINT_CHANGED_FILES filtering (no such code existed).

2. sdk/src/gsd-transport.ts — removed dead 'workstream_forced' member
   from the TransportDecision.reason union (no longer assigned after
   Phase 5.0 workstream-native refactor).

3. sdk/src/gsd-transport.ts — removed stale workstream interpolation
   from the subprocess-reason Error message; the field is no longer
   load-bearing for that decision path.

4. All eight generator scripts (sdk/scripts/gen-*.mjs and
   gen-state-document.ts) — replaced the manual entry-point check
   that used `new URL(process.argv[1], 'file://')`. On Windows that
   misparses `C:\…\gen-*.mjs` as scheme "c:" and breaks the check.
   Replaced with the cross-platform-safe direct comparison
   `fileURLToPath(import.meta.url) === process.argv[1]`. (Not using
   `import.meta.main` — that's only stable in Node 24+ and the
   project supports Node 22+.)

5. docs/agents/cjs-sdk-seam.md — added explicit `text` language
   specifier to the four file-path fenced blocks (lines 157, 165,
   173, 181). Closing fences correctly remain bare.

Verification

- node scripts/lint-no-source-grep.cjs → 0 violations
- node scripts/lint-shared-module-handsync.cjs → 22 cooperating
  siblings, 0 backlog (counts unchanged after pair-aware refactor)
- node scripts/lint-shared-module-handsync.cjs --json → typed IR
  unchanged
- All 9 generator freshness checks → fresh
- node scripts/run-tests.cjs → 9449/9449 pass
- sdk vitest src/gsd-transport.test.ts → 10/10 pass

Tests for pair-aware matching: the existing 9 cases in
tests/lint-shared-module-handsync.test.cjs already build fixture
allowlist entries with both `cjs` and `ts` fields, so they
implicitly exercise the new pair-aware lookup; all 9 pass.

* fix(3575): address second CodeRabbit review on PR #3577

Five new findings resolved.

1. Shared SDK bridge loader (`get-shit-done/bin/lib/cjs-sdk-bridge.cjs`)
   Eliminates seven-fold duplication of `tryLoadSdk` / `_executeForCjs`
   that lived verbatim in every `*-command-router.cjs` plus a near-identical
   variant in `gsd-tools.cjs`. The new module exposes `tryLoadSdk()`,
   `getExecuteForCjs()`, and `getSdkModule()` (the last for routers that
   pull additional named exports, e.g. state's `formatStateLoadRawStdout`).
   All eight call sites refactored to consume it. As a side benefit
   `gsd-tools.cjs` no longer imports from the private
   `@gsd-build/sdk/dist/runtime-bridge-sync/index.js` subpath; everyone now
   uses the public package entry consistently.

2. `phase remove` accepts zero positional args (#3577 review)
   `phase remove --force` previously passed validation with no phase number
   and invoked `cmdPhaseRemove(cwd, undefined, ...)`. Tightened to
   `positional.length !== 1` and added the early `return` so the handler
   never receives an undefined phase id.

3. Decisions parser regex hardened (#3577 review)
   `D-[A-Za-z0-9_-]+` allowed malformed IDs like `D--foo` and `D-_bar`.
   Tightened to `D-[A-Za-z0-9][A-Za-z0-9_-]*` so the first character after
   `D-` must be alphanumeric; internal `_`/`-` still permitted.
   Decisions generated CJS mirror regenerated.

4. plan-scan-generator test no longer uses hardcoded `/tmp` paths
   `/tmp/__gsd_test_nonexistent_dir_xyz__` and
   `/tmp/__nonexistent_gsd_test__` could collide with prior runs on shared
   CI runners. Replaced with `uniqueMissingPath()` helper that synthesizes
   `os.tmpdir()/<prefix>-<pid>-<ms>-<random>` and force-removes the path
   before returning.

5. lint-shared-module-handsync test now validates pair-aware TS matching
   Added `rejects pair when TS path differs from allowlist entry` — a
   regression guard that creates an on-disk pair at `sdk/src/query/<name>.ts`
   but allowlists the (cjs, sdk/src/<name>.ts) shape. The lint must reject
   because the (cjs, ts) tuple does not match. Demonstrates the pair-aware
   matching added in the previous commit and locks it in.

## Wiring

`cjs-sdk-bridge.cjs` added to `docs/INVENTORY.md` (count 68→69) and
`docs/INVENTORY-MANIFEST.json` regenerated.

## Verification

- node scripts/lint-no-source-grep.cjs → 0 violations (529 files)
- node scripts/lint-shared-module-handsync.cjs → 22 cooperating, 0 backlog
- node scripts/run-tests.cjs → 9452/9452 pass (was 9449 + 1 lint-test + 1
  changed plan-scan path test)
- node sdk/scripts/check-decisions-fresh.mjs → fresh
- sdk vitest src/query/decisions.test.ts → 15/15 pass

* docs(3575): correct PR/issue refs in cjs-sdk-seam.md

CodeRabbit caught two stale references that conflated the issue
number (#3575) with the PR number (#3577). Phase 6 ships as PR
#3577 closing issue #3575. Migration overview table row and the
Final Completion Summary updated accordingly.

* fix(3575): cjs-sdk-bridge actually loads the SDK (was dead-code since Phase 5.0)

## The bug

`cjs-sdk-bridge.cjs:tryLoadSdk()` resolved `require('@gsd-build/sdk')`,
but that package name is not installed in the root `node_modules`
(the SDK lives as `./sdk/` — a sibling workspace, not a dependency)
and the SDK's public entry doesn't re-export `executeForCjs` or
`formatStateLoadRawStdout` anyway. `tryLoadSdk()` always returned
false, the `_loadFailed = true` cache made every subsequent call
return false for the lifetime of the process, and every CJS router
silently fell through to the CJS handler.

The pattern shipped in Phase 5.0 (PR #3558, merged) via
`require('@gsd-build/sdk/dist/runtime-bridge-sync/index.js')` and
was inherited into the routers via `require('@gsd-build/sdk')` in
Phase 5.1 (PR #3574, merged). Both subpaths/imports failed in the
same way. CI passed for the whole CJS↔SDK migration because the
CJS fallback handlers kept running — meaning the entire claimed
"state.* delegation" never actually executed via the SDK in any
shipped run.

This is exactly the silent-drift class the Phase 6 lint and
retrospective are supposed to prevent. Catching it here closes the
loop.

## The fix

Resolve the bundled SDK by **package-relative filesystem path**:

  <root>/sdk/dist/runtime-bridge-sync/index.js
  <root>/sdk/dist/query/state-project-load.js

The `files` array in `package.json` keeps `sdk/dist` at the same
relative location inside the published tarball, so the path works
in both dev and post-install. The two-file split is necessary
because `formatStateLoadRawStdout` lives in the state handler,
not the runtime-bridge entry.

## Integration test

`tests/cjs-sdk-bridge-integration.test.cjs` proves four things and
locks the load-success invariant so this regression cannot recur:

  1. tryLoadSdk() returns true on the current checkout
  2. getExecuteForCjs() returns a function (not null)
  3. getFormatStateLoadRawStdout() returns a function (not null)
  4. executeForCjs() actually dispatches a canonical registry
     command (generate-slug) and returns an ok:true result — proving
     real SDK execution, not a silent CJS-fallback

## State-router formatter wiring

The state command router was reaching into `getSdkModule()` to pluck
`formatStateLoadRawStdout`. Replaced with the explicit
`getFormatStateLoadRawStdout()` getter so the bridge module owns
all SDK-export resolution.

## state.load --raw output mode

While the bridge was broken, the state.load --raw test happened to
pass via CJS fallback. The first SDK execution exposed a contract
mismatch: passing `mode: 'raw'` to the bridge tells the SDK to
pre-render result.data to a JSON string, but the router was also
calling `formatStateLoadRawStdout(result.data)` to project to
key=value lines — the formatter saw a string and no-op'd.

Fix: when a CJS-side rawFormatter is supplied, the router requests
`mode: 'json'` from the bridge (always get typed data) and runs the
formatter itself. When no rawFormatter, the user's --raw flag flows
through to the bridge as usual.

## Surfaced pre-existing parity gaps (NOT yet fixed)

With the bridge now actually executing the SDK, 8 `tests/state.test.cjs`
cases reveal pre-existing CJS↔SDK behavioral drift that Phase 5.1's
"104/104 pass" report could not see because the SDK was never running:

  - `state load returns error when STATE.md missing`
  - `state get returns error when STATE.md missing`
  - `state update returns error when STATE.md missing`
  - `state update reports field not found`
  - `state patch / record-metric / update-progress /
     resolve-blocker / record-session — error when STATE.md missing`
  - `add-decision --summary-file` / `add-blocker --text-file`
    (file-input path rejected by SDK security check)

Each is a real CJS↔SDK divergence that needs explicit alignment in
the SDK handler. Listed here so the next commit can address them
honestly rather than letting the broken bridge mask them again.

* fix(3575): align SDK with CJS contract — bridge-exposed divergences

The Phase 5.1 bridge fix (0fc60b0c) made executeForCjs() actually load and
dispatch. With routers now hitting the SDK in normal layouts, six CJS↔SDK
behavioral divergences became visible. This commit aligns the SDK to match
the canonical CJS contract test-by-test.

ROUTER CHANGES (mode: raw → mode: json)
All 7 CJS routers were passing `mode: raw ? 'raw' : 'json'`. With the bridge
active, `mode: 'raw'` makes the bridge pre-render result.data to a JSON string,
which CJS output() then re-stringifies — producing a JSON string of a JSON
string. Routers now always request typed JSON; CJS output() handles user-
facing rendering. Affected: gsd-tools, init, phase, phases, roadmap, state,
validate, verify routers.

SDK STATE MUTATION HANDLERS (sdk/src/query/state-mutation.ts)
state.update / record-metric / update-progress / resolve-blocker / record-
session no longer auto-create STATE.md via readModifyWriteStateMd. CJS errors
out when STATE.md is missing; SDK now does the same via an upfront existsSync
check returning {updated: false, reason: 'STATE.md not found'}. Also fixes:

  • resolve-blocker semantic: SDK returned resolved:false when no blocker
    line matched. CJS returns resolved:true whenever the Blockers section
    exists. Aligned.
  • readTextArgOrFile path validation: rejected /var/folders paths on macOS
    because /var → /private/var is a symlink. Now resolves both base and
    target via realpathSync before the prefix check.

STATE.MD STOPPED_AT SCOPING (sdk/src/query/state.ts)
buildStateFrontmatter extracted `Stopped At` from the entire body; CJS scopes
it to the ## Session section. Bug-2444 parity restored — the field no longer
bleeds in from unrelated sections of STATE.md.

PHASE_DIR_COUNT MILESTONE FILTER (sdk/src/query/init.ts)
initNewMilestone counted every directory under phases/ regardless of which
milestone it belonged to. CJS uses getMilestonePhaseFilter to count only
current-milestone phase dirs. Bug-2445 parity restored.

ARCHIVED PHASE GUARD (sdk/src/query/init.ts)
shouldDropArchivedPhaseMatch had an extra `archivedTag === milestone.version`
escape hatch that doesn't exist in CJS. CJS unconditionally drops the
archived match when the phase appears in the current ROADMAP. Removed the
escape hatch — fixes the bug #2391 regression where `init plan-phase 03`
returned the archived v1.0 phase instead of the current ROADMAP phase.

PADDING-TOLERANT ROADMAP PHASE LOOKUP (sdk/src/query/roadmap.ts)
searchPhaseInContent used `escapeRegex(phaseNum)` as the phase-number
fragment — `03` failed to match `Phase 3:` headings. CJS uses
phaseMarkdownRegexSource which emits `0*<integer>` for padding tolerance.
Restored same helper inline in roadmap.ts. Fixes bug #2391 / #3537 parity
in zero-padded phase lookups.

STATE COMMAND ROUTER STATE.MD-MISSING ERROR SURFACE
(get-shit-done/bin/lib/state-command-router.cjs)
state.get must surface "STATE.md not found" as an error (matching CJS exit
behavior); other state mutations must surface {updated: false, reason: ...}
as data. Added EXIT_ON_STATE_MD_MISSING discriminator with STATE_MD_MISSING_
MESSAGE constant.

VERIFICATION
  • init.test.cjs        — 93/93 pass (was 91/2 fail)
  • state.test.cjs       — 104/104 pass (was 95/9 fail)
  • core.test.cjs        — pass
  • roadmap.test.cjs     — pass
  • cjs-sdk-bridge-integration.test.cjs — 4/4 pass (bridge load locked in)

The 13 phase.test.cjs failures (next-decimal 999.x backlog skip, add-batch
JSON validation, insert dry-run rejection, find-phase non-canonical
warnings) are pre-existing SDK gaps from the broken-bridge era and will be
addressed in a follow-up commit on this same PR.

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

* fix(3575): align SDK phase handlers with CJS (wave 2 — phase.test.cjs)

The bridge-fix (0fc60b0c) exposed 13 more CJS↔SDK behavioral divergences
inside the phase command family. All are now aligned to the canonical CJS
contract, with per-test verification.

phase.ts:
  • Centralised isCanonicalPlanFile / looksLikePlanFile / describeNonCanonical
    Plans helpers mirroring phase.cjs:17–52. Exported for reuse from
    phase-lifecycle.ts (phasesList) so the warning shape never drifts between
    read sites.
  • searchPhaseInDir now emits result.warning (singular) with the canonical
    message when a plan-shaped file would be skipped by the canonical filter.
    Bug #2893 parity for find-phase.
  • phasePlanIndex moved its non-canonical warning to the singular result.warning
    field (was a generic entry in result.warnings) so consumers see the same
    field name and message format as find-phase / phases-list. Other
    diagnostics (unresolved deps, wave-declaration mismatches) still flow
    through the warnings array unchanged.
  • Added PhaseInfo.warning to the type. getPhaseFileStats now also returns
    allFiles so the caller can compute the diagnostic without re-reading the
    directory.

phase-lifecycle.ts:
  • phasesList (phases list --type plans) emits per-dir prefixed warnings
    matching phase.cjs:120 (`${dir}: ${describeNonCanonicalPlans(...)}`).
  • phaseAdd now matches the CJS router contract for arg parsing:
    accepts --raw (ignored), --dry-run, --id <value>; rejects every other
    --flag with "phase add does not support <flag>"; rejects dangling
    --id with "--id requires a value"; joins all positional tokens with
    space so `phase add User Dashboard` produces description "User
    Dashboard". customId comes from --id, never from positional[1].
  • phaseInsert now mirrors phaseAdd's arg parsing: rejects --dry-run
    with "does not support --dry-run", strips --raw, joins
    positional.slice(1) for the description. Also reports the bug-3098
    placeholder error ("Phase N exists in roadmap summary but is missing
    a detail section") when the ROADMAP has only a checklist entry but no
    detail section.
  • phaseAddBatch dangling --descriptions or --descriptions followed by
    another flag now surface "--descriptions must be a JSON array"
    instead of silently falling through to positional parsing or throwing
    "--descriptions must be a valid JSON array".
  • renameIntegerPhases now skips backlog phases (dirInt >= 999) — bug-2434
    parity. Without this, removing phase 3 in a project with 999.1-backlog-*
    on disk would rename the backlog dir to 998.1-backlog-*.
  • updateRoadmapAfterPhaseRemoval rewritten to mirror phase.cjs:880-922
    exactly: 5 targeted regex passes (not a loop), driven by three
    decrement helpers (decrementRoadmapPhaseNumber, decrementRoadmapPhase
    Token, decrementRoadmapPaddedPhaseNumber) that guard against
    `num >= 999`. The padded-prefix replace uses negative lookbehind/
    lookahead to skip YYYY-MM-DD substrings. Fixes:
      - bug-2435: integer phase remove no longer corrupts dates in ROADMAP
        (e.g. `(Shipped: 2025-04-15)` is left alone when removing phase 4).
      - bug-3355: integer phase remove no longer renumbers the same phase
        more than once (loop overlap removed).
      - Backlog phases stay frozen during renumbering.
  • phaseComplete next-phase scan skips backlog dirs (999.x). Without
    this, `phase complete 2` in a project with 999.1-backlog/ on disk
    would emit next_phase: '999.1' even though Phase 3 exists in
    ROADMAP.md. Bug #2129 parity.

VERIFICATION (per-test, targeted runs — full suite not exercised due to
prior 89GB OOM with concurrent runs):
  • phase.test.cjs        — 108/108 pass (was 13 fail)
  • init.test.cjs         — 93/93 pass (no regression)
  • state.test.cjs        — 104/104 pass (no regression)
  • validate.test.cjs     — pass (no regression)
  • verify.test.cjs       — pass (no regression)
  • core.test.cjs         — pass (no regression)
  • roadmap.test.cjs      — pass (no regression)
  • cjs-sdk-bridge-integration.test.cjs — 4/4 pass (bridge intact)

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

* fix(3575): align SDK roadmap-mutation helpers with CJS — bug-2005

Three CJS↔SDK divergences in the phase.complete write path were hiding
behind the broken bridge:

1. replaceInCurrentMilestone (sdk/src/query/phase-roadmap-mutation.ts)
   The SDK port carried an extra fallback that doesn't exist in the CJS
   (core.cjs:1013-1022): if the "after last </details>" slice didn't match
   the pattern, the SDK silently retried inside the last <details> block.
   That fallback corrupts the current milestone when it is itself wrapped
   in <details open>...</details> and there's no content after the close
   tag — the supposed-to-be-skipped scope is the only place the match
   exists. Aligned to CJS: split at the last </details>, replace only in
   the after-slice, return. No fallback. Documented with a "do not
   re-add" warning since this fallback has been added back twice in
   prior porting passes.

2. phase complete checkbox update (sdk/src/query/phase-lifecycle.ts)
   The SDK was scoping the `- [ ] Phase N:` → `- [x] Phase N:`
   replacement through replaceInCurrentMilestone. The CJS
   (phase.cjs:1057) uses a direct roadmapContent.replace(...) call. When
   the current milestone is wrapped in <details>, the scoped variant
   never reaches the checkbox; direct replace finds it. Aligned with
   CJS.

3. phase complete plan-count update (sdk/src/query/phase-lifecycle.ts)
   Same pattern — the SDK was scoping the `**Plans:** X/Y` update
   through replaceInCurrentMilestone. CJS (phase.cjs:1080) uses direct
   replace. Aligned.

VERIFICATION
  • bug-2005-phase-complete-details.test.cjs — 2/2 pass (was 1 fail)
  • phase.test.cjs                            — 108/108 pass (no regression)

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

* fix(3575): align SDK with CJS — add-decision DWIM + frontmatter paths

Two more CJS↔SDK divergences exposed by the bridge fix:

state.add-decision / state.add-blocker DWIM (sdk/src/query/state-mutation.ts)
  CJS state.cjs:481-498 + 532-548 auto-create the canonical Decisions /
  Blockers section when it's absent from STATE.md. The SDK was returning
  `{added: false, reason: '<Section> section not found in STATE.md'}`
  even when STATE.md was writable. Bug #3286 (parity for both verbs):

    • If section header pattern matches → append entry (existing path).
    • If section is absent → scaffold `## Decisions` (or `### Blockers`)
      and append the entry, then set `created: true` on the result.

  Matches the begin-phase / advance-plan DWIM behavior. Callers can now
  treat `state add-decision` as idempotent — first call creates the
  scaffold, subsequent calls append to it.

frontmatter get/set/merge/validate (helpers.ts + frontmatter.ts +
                                     frontmatter-mutation.ts)
  CJS frontmatter.cjs:323/340/354/369 resolves user paths with the
  simple `path.isAbsolute(p) ? p : path.join(cwd, p)`. The SDK port had
  promoted this to `resolvePathUnderProject` which adds a real-path
  prefix check against the project root.

  That check rejects absolute paths outside the project — including
  macOS tmpdir paths whose names contain spaces, the exact regression
  cited in bug #3509. Frontmatter verbs are deliberately path-flexible
  in CJS because they're called against external files (plan paths
  from other repos, scratch markdown, tmpdir fixtures).

  Introduced `resolveFrontmatterPath()` mirroring the CJS one-liner. The
  project-scoped `resolvePathUnderProject()` is unchanged — still used
  for template output, decision artifacts, etc.

VERIFICATION
  • bug-3286-state-write-routing.test.cjs — 13/13 pass (was 6 fail)
  • bug-3509-path-spaces.test.cjs         — 6/6 pass (was 3 fail)
  • phase.test.cjs / init.test.cjs / state.test.cjs / validate.test.cjs /
    verify.test.cjs / core.test.cjs / roadmap.test.cjs — all pass (no
    regression — 566 total tests).

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

* fix(3575): route SDK state handlers through scanPhasePlans — bug-3257

The SDK port of buildStateFrontmatter / stateValidate / stateSync was
using a naive top-level filter (`files.filter(/-PLAN\.md$/i)`) instead
of the canonical scanPhasePlans helper. The naive filter undercounts
every phase that uses the nested layout `phases/NN-name/plans/<NN>-PLAN-MM-slug.md`,
which is the default the planner agent produces.

CJS routes all three sites through scanPhasePlans (state.cjs:408, 824,
1427). scanPhasePlans is already a Shared Module — generated CJS at
plan-scan.generated.cjs from sdk/src/query/plan-scan.ts. The fix is
just to consume it.

CHANGES
  • buildStateFrontmatter (sdk/src/query/state.ts): replaced the
    inline `-PLAN.md` / `-SUMMARY.md` regex filters with scanPhasePlans;
    use the helper's `completed` flag for diskCompletedPhases.
  • stateValidate (sdk/src/query/state-mutation.ts): same swap on the
    current-phase plan-count drift check.
  • stateSync (sdk/src/query/state-mutation.ts): same swap on the
    rollup loop. Also routes the Progress percent through
    computeProgressPercent(completedPlans, totalPlans, diskCompletedPhases,
    syncTotalPhases) so the min(plan_fraction, phase_fraction) cap from
    bug #3242 Bug B is applied — without this, sync emitted 60% when the
    real progress was capped at 50% by phase-fraction.

VERIFICATION
  • bug-3257-nested-plans-undercount.test.cjs — 14/14 pass (was 12 fail)
  • phase.test.cjs / init.test.cjs / state.test.cjs / validate.test.cjs /
    verify.test.cjs / core.test.cjs / roadmap.test.cjs / bug-3286 /
    bug-2005 / bug-3509 — all pass (no regression — 580 total).

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

* test(3575): phase 6 CJS↔SDK seam behavioral contracts — TDD-found worker bug

Adds tests/phase-6-cjs-sdk-seam-contracts.test.cjs — a behavioral contract
suite for everything Phase 6 of #3524 introduced.  Written under the
issue #3592 test rewrite discipline:

  • No source-grep on .cjs files
  • No assert.match / .includes on free-form child-process stdout/stderr
  • Every assertion is on a parsed JSON object, a filesystem fact, an
    exit code, or a frozen enum value (SYNC_ERROR_KIND, BRIDGE_EXPORTS,
    TRANSPORT_MODE)
  • Helpers come from tests/helpers.cjs (runGsdTools, createTempProject,
    cleanup) — no inline fs.mkdtempSync
  • Fixture content built with array.join('\n'), never template literals
  • beforeEach/afterEach for shared setup; no try/finally inside tests

COVERAGE
  1. Bridge module surface — exports lock against BRIDGE_EXPORTS
  2. Bridge load lifecycle — tryLoadSdk, getters return cached refs,
     pre-load returns null
  3. executeForCjs RuntimeBridgeSyncResult shape — ok:true vs ok:false
     discriminated union; mode:"json" never double-stringifies
  4. CLI family-router dispatch — one structured-JSON assertion per
     family (roadmap, phase, phases, state, init, validate, find-phase)
  5. mode:"json" regression guard — stdout parses to object, not to
     JSON-encoded string (the Wave-1 double-stringify bug shape)
  6. GSD_WORKSTREAM gate — SDK path and CJS fallback produce identical
     structured fields for the same fixture
  7. Validation error taxonomy — empty arg → ok:false +
     errorKind: SYNC_ERROR_KIND.VALIDATION_ERROR
  8. phase.add filesystem facts — directory exists, ROADMAP file grew
     (asserted via fs.statSync, never by reading content back)

TDD-FOUND BUG (RED → GREEN)
  Suite §7 (validation_error taxonomy) failed in the RED phase:

    expected: 'validation_error'
    actual:   'native_failure'

  Root cause in sdk/src/runtime-bridge-sync/worker.ts: when an SDK
  handler throws a GSDError(Validation), the native direct adapter
  wraps it in a GSDToolsError via createNativeFailureError, preserving
  the original on `.cause`.  classifyError only checked for TypeError
  causes — every GSDError cause fell through to `native_failure`,
  breaking the documented SyncErrorKind contract.

  Fix: classifyError now unwraps the cause once.  When the cause is a
  GSDError with ErrorClassification.Validation or .Blocked, the result
  is errorKind: 'validation_error' (exit 10) — matching the direct
  branch a few lines below for unwrapped GSDError.

VERIFICATION (per-test, before and after the worker fix)
  • Phase 6 contract suite          — 21/21 pass (was 20/1 fail at RED)
  • phase.test.cjs                  — 108/108 pass
  • init.test.cjs                   — 93/93 pass
  • state.test.cjs                  — 104/104 pass
  • validate.test.cjs / verify.test.cjs / core.test.cjs / roadmap.test.cjs
                                    — all pass
  • cjs-sdk-bridge-integration.test.cjs — 4/4 pass (bridge intact)
  • npm run lint:tests              — 0 violations (no source-grep)

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

* fix(3575): SDK config-get/set parity + reason-code propagation — bugs #2943 #3086 #3212

Three CJS↔SDK divergences in config dispatch exposed when Phase 6 routes
`config-get` / `config-set` through `executeForCjs`:

1. SDK config-get was missing the SCHEMA_DEFAULTS map.
   CJS config.cjs:505-510 hard-codes documented defaults for
   `context_window` (200000), `executor.stall_detect_interval_minutes` (5),
   `executor.stall_threshold_minutes` (10), `git.create_tag` (true).  When a
   config.json omits the key, CJS returns the documented default with
   exit 0.  SDK threw `Key not found` for all four — every skill that
   reads `context_window`, executor stall thresholds, or the tag toggle
   broke under SDK dispatch.  Ported the table verbatim into
   sdk/src/query/config-query.ts and consult it at every "not found"
   exit point (matching the three CJS branches: missing file, traversal
   collapse, terminal undefined).

2. SDK config-set was missing the `git.create_tag` boolean-only guard.
   CJS rejects `config-set git.create_tag maybe` because the schema is
   boolean.  SDK silently accepted it and wrote "maybe" to disk under
   Phase 6 dispatch.  Added the matching guard + the missing
   `workflow.post_planning_gaps` boolean guard.

3. SDK errors lost their structured reason code at the bridge boundary.
   `--json-errors` callers expect `reason: 'config_key_not_found'` etc.
   from a frozen `ERROR_REASON` taxonomy; the bridge dispatcher in
   gsd-tools.cjs was calling `error(message)` without the second
   argument, so every SDK-routed error surfaced as `reason: 'unknown'`.
   Fix is end-to-end:
     • config handlers tag the GSDError with `.reason = 'config_*'`.
     • worker.ts:classifyError reads `.reason` off the cause (or off
       the direct error) and forwards it via `errorDetails.reason`.
     • `_dispatchNonFamily` in gsd-tools.cjs passes that reason as the
       second arg to `error()` when present.
     • Also added the `--raw` scalar pass-through here, so
       `output(data, raw, String(data))` is called for primitive
       results — without it, `config-get context_window --raw` emitted
       the JSON shape '200000\n' which happens to match but breaks any
       primitive whose JSON encoding differs from its String() form
       (booleans for example, where the CJS produces `true` while the
       SDK-routed path was producing `true` — same here, but the
       structural guarantee was wrong before).

VERIFICATION (per-test)
  • bug-2943-config-get-context-window-default.test.cjs — 5/5 pass
  • bug-3086-git-create-tag-config-gate.test.cjs        — 4/4 pass
  • bug-3212-execute-phase-stall-safe-resume.test.cjs   — 7/7 pass
  • Phase 6 contract suite                              — 21/21 pass
  • phase/init/state/core/roadmap/validate/verify       — all pass
                                                          (570 total)
  • Full bug-* suite: 24 fail → 17 fail (7 fixed in this commit).

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

* fix(3575): SDK milestone-archive layout discovery — bug #3164

Two CJS↔SDK divergences in phase discovery and validation surfaced
when projects moved to the milestone-archive layout
(`.planning/milestones/v<version>-phases/<phase>/`) instead of the
flat `.planning/phases/<phase>/`.

1. SDK findPhase had no `searched_directories` field on the not-found
   payload.  CJS surfaces this for diagnostics.  Added: track every
   directory probed (the active `.planning/phases/` plus each
   archive root) and include the relative paths in the not-found
   payload. Bug #3164 — #find-phase tests.

2. SDK validateConsistency only scanned `.planning/phases/`.  CJS
   `cmdValidateConsistency` (verify.cjs:467) walks every active
   phase root via `collectPhaseRoots(planBase)` — the flat dir plus
   the active milestone archive resolved from STATE.md.  Without
   parity, every roadmap phase on a milestone-archive-layout project
   emitted W006 ("no directory on disk") even though the phases were
   present in the archive.

   Ported the helper trio (listMilestoneArchiveDirs,
   getActiveMilestoneArchiveDir, collectPhaseRoots) verbatim from
   verify.cjs:400-444 and rewrote validateConsistency's disk-phase
   scan + per-phase plan scan to iterate `phaseRoots`.  Warning
   labels now include the archive prefix so users can tell which
   root surfaced the issue.

   Also accepts prefixed archive dir names (`CK-64-...`) as phase 64
   via the `(?:[A-Z]{1,6}-)?` group at the head of
   PHASE_TOKEN_FROM_DIR_RE — same regex CJS uses.

VERIFICATION (per-test)
  • bug-3164-milestone-archive-layout.test.cjs — 8/8 pass
  • Phase 6 contract suite                      — 21/21 pass
  • phase/init/state/validate/verify/core/roadmap — 570 pass
  • Full bug-* suite: 17 fail → 12 fail (5 fixed in this commit;
    cumulative 12 fixed since Wave 6 start).

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

* fix(3575): padded phase IDs match unpadded ROADMAP prose — bug #3537

Three failures in bug-3537-padded-id-against-unpadded-roadmap:

1. roadmap.get-phase returned `phase_number` verbatim from the user
   input — `02.7` produced `"phase_number": "02.7"` while `2.7`
   produced `"phase_number": "2.7"` on the same fixture, so a parity
   compare of the two stdouts fails.  Fixed by promoting the matched
   phase token in `searchPhaseInContent` to a capture group and
   returning that as the canonical `phase_number`.  Same fix in the
   checklist-fallback branch so the malformed-roadmap diagnostic
   carries the as-written form too.

2. phase.complete built every ROADMAP-prose regex from
   `escapeRegex(phaseNum)` instead of the padding-tolerant
   `phaseMarkdownRegexSource(phaseNum)`.  Calling
   `phase complete 02.7` against the un-padded heading
   `### Phase 2.7:` matched nothing — checkbox didn't flip, plan
   count stayed at `0/1`, table row stayed `Planned`.  Promoted
   `phaseMarkdownRegexSource` to an exported helper in roadmap.ts
   and wired it into phaseComplete's roadmap mutation block.

3. roadmap.annotate-dependencies infinite-looped through the bridge.
   The SDK handler delegates to `spawnSync(gsd-tools.cjs roadmap
   annotate-dependencies …)`; the child re-entered the roadmap
   router; the router re-dispatched through executeForCjs; synckit
   spawned the same SDK worker; that worker spawned gsd-tools.cjs
   again; …  Recursion hit the 15s timeout and the test reported
   `code=null`.  Fixed with a `GSD_SDK_NESTED=1` env-var guard:
   the SDK handler sets it when spawning the child, and the CJS
   roadmap router refuses SDK dispatch when it sees the flag.

VERIFICATION (per-test)
  • bug-3537-padded-id-against-unpadded-roadmap.test.cjs — 6/6 pass
  • Phase 6 contract suite                                — 21/21 pass
  • phase/init/state/validate/verify/core/roadmap         — 570 pass
  • Full bug-* suite: 12 fail → 7 fail (5 fixed in this commit;
    cumulative 17 fixed across the wave-6/7/8 sequence).

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

* fix(3575): final-7 SDK parity — bugs #2787 #2268 #2526

Closes out the bug-suite tail.  Three independent fixes against three
independent regressions surfaced when Phase 6 routed read-only and
mutation paths through the SDK.

1. extractCurrentMilestone truncated at heading-like lines inside
   fenced code blocks — bug #2787.  The `^#{1,N}\\s+...vX.Y` scan
   ran with the `/m` flag, which matches `^` at every newline,
   including newlines inside ``` and ~~~ fences.  A snippet like
     ```bash
     # Ops runbook — v1.0 compat
     ```
   placed between Phase 2 and Phase 3 of a v1.1 milestone shortened
   the milestone slice and made phases 3, 4 invisible to
   roadmap.analyze / roadmap.get-phase.

   Added `isInsideFencedCodeBlock(content, offset)` — a GFM-aware
   walker that toggles a `fenceChar` cursor on each fence boundary
   (backticks and tildes; closing fences require the matching
   character and no info string — so ```js inside ```text does NOT
   close).  The nextMilestoneRegex loop now skips any match that
   falls inside an open fence.

2. init.manager only marked the FIRST undiscussed phase as
   `is_next_to_discuss` — bug #2268.  Two and five-phase fixtures
   both proved the regression: parallel-discuss capacity was lost,
   recommended_actions emitted at most one discuss action even
   when callers were free to take several.  Replaced the sliding-
   window loop with an unconditional `phase.is_next_to_discuss =
   (status === 'empty' || status === 'no_directory')`.

3. phase.complete didn't surface "REQ-IDs found in body but
   missing from Traceability table" warnings — bug #2526.  CJS
   phase.cjs:1140-1167 scans REQUIREMENTS.md for `**REQ-ID**`
   references in the body, intersects against the IDs that actually
   appear in the Traceability section table, and warns about the
   diff.  The SDK port only ran the per-roadmap-REQ checkbox
   update and never emitted the body-scan warning.  Added the
   missing scan + warning push; also routed the writeFile through a
   `reqContentChanged` flag so we only write when at least one
   substitution actually fired (parity with the implicit
   "every checkbox already complete" no-write CJS branch).

VERIFICATION
  • bug-2787-milestone-fenced-block-truncation.test.cjs — 4/4 pass
  • bug-2268-parallel-discuss.test.cjs                   — 4/4 pass
  • bug-2526-phase-complete-req-discovery.test.cjs       — 3/3 pass
  • Phase 6 contract suite                               — 21/21 pass
  • Major suites (phase/init/state/validate/verify/core/roadmap) — 570 pass
  • **Full bug-* suite: 2397/2397 pass — ZERO failures.**
  • Combined run (major + bug-*): 2967/2967 pass — zero failures.

Cumulative since the bridge-fix landing (PR #3577): 12 sub-test
regressions surfaced + every one resolved.  Phase 6 is now byte-for-
byte CJS-parity across every command family verified by the test
suite.

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

* fix(3575): preserve codex runtime command shape after router migration

* test(3575): pin agent-install-validation init tests to GSD_AGENTS_DIR

PR #3577 routed init.execute-phase and init.plan-phase through executeForCjs
to the SDK handlers. The SDK side's resolveAgentsDir (sdk/src/query/helpers.ts)
honors GSD_AGENTS_DIR or falls back to <runtimeConfigDir>/agents; it does not
walk up from cwd to find <repo>/agents/ like the CJS-era code did. The two
init-suite tests that asserted agents_installed=true relied on that implicit
walk and only passed on dev machines where ~/.claude/agents/ already had the
33 agents installed — Linux CI runners have neither.

Match the pattern every passing sibling in this file already uses: pass
{ GSD_AGENTS_DIR: REPO_AGENTS_DIR } through runGsdTools so the SDK resolver
points at the repo's agents/ dir explicitly. No production code change.

Refs sdk/src/query/QUERY-HANDLERS.md ("subprocess vs in-process path
resolution") and CONTEXT.md DEFECT.PORT-DRIFT.cjs-sdk.

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

* fix(3577): Phase 6 config-* SDK port parity carve-outs

Restored the legacy contract for four CLI tests broken by the Phase 6
router migration:

1. `config-ensure-section` was bound to the new SDK `configEnsureSection`
   handler which requires `args[0]=sectionName`. Every real CLI caller
   uses the no-arg form expecting full default config.json creation.
   Reverted the dispatch case to call `config.cmdConfigEnsureSection`
   directly (matches the precedent in 7d5dfa9d for `codex` runtime).

2. SDK `configNewProject` `commit_docs` and `parallelization` defaults
   set to `true`/`true` (was `false`/`1`) — aligned with
   `sdk/shared/config-defaults.manifest.json` and the CJS
   `buildNewProjectConfig` `hardcoded` block.

3. SDK `configNewProject` returns the project-rooted relative path
   `.planning/config.json` instead of the absolute `paths.config`,
   matching the CJS `ensureConfigFile` output shape.

4. SDK error vocabulary aligned with CJS: `Unknown config key: <key>`
   (no surrounding quotes), and config-get's malformed-JSON message
   leads with `Failed to read config.json:` so legacy substring
   assertions in `tests/config.test.cjs` keep matching.

Local: 132/132 across `tests/{config,agent-skills,ai-evals}.test.cjs`.

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

* fix(3631): family routers forward --raw to SDK bridge as mode:'raw'

#3577 routed every family subcommand through the SDK bridge with a
hardcoded mode:'json'. With --raw set, the bridge returned the typed JSON
IR and routers called `output(result.data)` — bypassing output()'s
rawValue branch. Shell consumers expecting scalar tokens
(`gsd-tools phase next-decimal --raw 1` → `1.1`) received the JSON-
stringified IR instead.

Each `*-command-router.cjs` SDK dispatch path now requests
`mode: raw ? 'raw' : 'json'` from the bridge. The sync-bridge worker is
wired to `formatNativeRaw = formatQueryRawOutput` so the bridge returns
the per-command scalar projection. Routers route the formatted string
through `output(null, true, str)` (rawValue branch) so it lands on
stdout verbatim.

formatQueryRawOutput extended for the two commands covered by the issue
acceptance criteria — phase.next-decimal (→ data.next) and
roadmap.get-phase (→ data.section). Other registered raw projections
(state.load, commit, config-set, state.begin-phase) are unaffected; the
default `safeStringify` branch still applies to unprojected commands.

state-command-router already had a dispatchViaSdk helper that selected
mode based on a rawFormatter. The trailing fallthrough `output(result.data)`
when no rawFormatter was present is the same regression and was patched
to use the rawValue branch under --raw.

Regression test `tests/bug-3631-router-raw-flag.test.cjs` exercises
end-to-end:
  - `phase next-decimal --raw 1` emits a scalar phase token (not JSON).
  - `roadmap get-phase --raw 2` emits the section text (not JSON).

The fix targets `feat/3575-enforcement-hardening` (PR #3577, open) —
not origin/main as the issue body asserted. The #3577 regression lives
on that branch and the fix needs to land there before merge.

Fixes #3631

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

* test(3631): force CJS dispatch path in router unit tests via GSD_WORKSTREAM

phases-command-router.test.cjs and roadmap-command-router.test.cjs
mock the CJS-side `phase`/`milestone`/`roadmap` handlers and assert
they are called with the parsed args. Since #3577 the router prefers
SDK dispatch when sdk/dist is present — the mocks are then bypassed
and the SDK side fails because the test cwd `/tmp/proj` has no
`.planning/` fixture.

The router already gates SDK dispatch on `process.env.GSD_WORKSTREAM`
being unset (workstream-scoped requests fall through to CJS). Setting
GSD_WORKSTREAM in before()/after() deterministically routes through
the CJS handlers the tests were written against, without weakening
the assertions.

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

* fix(3632): report each ts sibling independently in lint-shared-module-handsync

The cooperatingPairs lookup ran inside `.some()` over all ts candidates for
a given cjs. When two ts siblings shared the same basename (e.g.
`sdk/src/foo.ts` and `sdk/src/query/foo.ts`) and only one pair was
allowlisted, `.some()` short-circuited and the unallowlisted sibling
silently passed through CI.

Classify each ts sibling independently against the allowlist so partially-
allowlisted multi-sibling drift surfaces. Added regression test
`reports unallowlisted ts sibling when another ts sibling for the same cjs
IS allowlisted (#3632)`.

Real-tree lint output unchanged on `feat/3575-enforcement-hardening`:
22 cooperating siblings, 0 unauthorized, 0 backlog pairs.

Fixes #3632

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

* fix(3577): ADR/PRD compliance + SDK port completeness for Phase 6

Multiple ADR/PRD violations in the Phase 6 cutover surfaced during
gsd-test-summary docker runs. Root causes traced to docs/adr/
3524-cjs-sdk-hard-seam.md §3 (out-of-seam module list) and
docs/prd/3524-cjs-sdk-hard-seam.md L160 (CJS-only verbs must not
route through the SDK runtime bridge), plus port-drift bugs the ADR
was specifically written to prevent (DEFECT.PORT-DRIFT.cjs-sdk).

Out-of-seam Module bindings removed from SDK catalog/manifests:
- verify.codebase-drift (drift is CJS-only; the SDK stub used
  execFileSync back to gsd-tools, recursing infinitely with the
  Phase 6 router rewrite — forked hundreds of node procs on the
  64 GiB plex2 docker host before manual kill)
- intel.* (8 verbs: diff, snapshot, validate, status, query,
  extract-exports, patch-meta, update — intel is CJS-only per ADR)
Both already have direct-CJS dispatch in gsd-tools.cjs (case
'intel') and verify-command-router.cjs (`'codebase-drift':` now
calls verify.cmdVerifyCodebaseDrift without going via sdkHandler).

config-ensure-section cutover restored via catalog rebind:
- 'config-ensure-section' in command-static-catalog-foundation.ts
  rebound from configEnsureSection (single-section semantics,
  requires args[0]=sectionName the CLI never passes) to
  configNewProject (whose no-args branch produces the full default
  config.json — matches the legacy ensureConfigFile contract).
- gsd-tools.cjs `case 'config-ensure-section'` restored to its
  Phase 6 _dispatchNonFamily form (no CJS fallback — the SDK
  handler now does the right thing).

configNewProject defaults from canonical manifest:
- Replaced the hardcoded duplicate `defaults` block with a
  derivation from CONFIG_DEFAULTS (sdk/src/configuration/index.ts,
  sourced from sdk/shared/config-defaults.manifest.json). The
  duplicate had drifted — omitted workflow.{ai_integration_phase,
  tdd_mode, human_verify_mode, pattern_mapper, plan_bounce*,
  auto_prune_state, subagent_timeout, security_*, post_planning_gaps},
  git.create_tag, claude_md_path, planning.*, graphify.*, mode,
  resolve_model_ids, context_window — every one of which had a
  test asserting the post-init value.

SDK configSet value-validation port (CJS cmdConfigSet parity):
- workflow.drift_action enum (warn|auto-remap)
- workflow.drift_threshold positive-integer
- workflow.human_verify_mode enum (mid-flight|end-of-phase)
- statusline.context_position enum (front|end)
- code_quality.fallow.scope enum (phase|repo)
- code_quality.fallow.profile enum (minimal|standard|strict)
- review.default_reviewers array shape + slug regex +
  lowercase-unique normalisation (matches
  bin/lib/review-reviewer-selection.cjs
  normalizeConfiguredDefaultReviewers, with the normalised value
  persisted to disk)

Init/roadmap/phase/workspace/frontmatter handler fixes:
- initExecutePhase + initPlanPhase parse --tdd boolean override
- initMapCodebase reads workflow.subagent_timeout with 300000
  default per manifest
- roadmapAnalyze surfaces `mode` per phase (parity with
  roadmapGetPhase)
- phaseComplete auto-prunes STATE.md when workflow.auto_prune_state
  is true (port of bin/lib/phase.cjs:1378-1390; #2087)
- initRemoveWorkspace throws GSDError on no-name and
  workspace-not-found instead of returning {data:{error}} which
  the CLI output path treated as success
- frontmatterGet parses --field <name> in addition to positional
  args[1]

Local: 150/150 across the failing-cluster test files
(review-default-reviewers-config, subagent-timeout, pattern-mapper,
tdd-mode, drift-detection, roadmap-mode-field, workspace,
phase-complete-auto-prune, frontmatter-cli). Docker gsd-test-summary
re-run in progress for full validation.

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

* fix(3577): clear 12 ubuntu-only regressions surfaced by gsd-test-summary

Docker test pass 3 (holodeck) surfaced 12 real bugs after the earlier
ADR/PRD-compliance commit (cf4dd0cb). Every one is a SDK-side bug —
fix-forward, not "pre-existing":

bug-3599 (2 subtests) — roadmap.get-phase project-code-prefix lookup:
  Ported phaseMarkdownRegexSourceExact from CJS (core.cjs:704-708) so
  `PROJ-42` queries try the exact escaped form FIRST before falling
  back to the padding-tolerant numeric. searchPhaseInContent now does
  two-pass lookup. Without this, `roadmap get-phase PROJ-42` returned
  not-found even when ROADMAP contains `### Phase PROJ-42:`, and
  bare `42` queries cross-matched the PROJ-42 heading.

roadmap-mode-field (1) — roadmapAnalyze surfaces `mode` per phase:
  Extracts the same `**Mode:**` field that roadmapGetPhase already
  parses (CONTEXT.md "MVP Mode" glossary). Without this, downstream
  consumers reading roadmap.analyze output couldn't tell which phases
  were MVP-mode.

bug-3601 (2 subtests) — phase.remove preserves peer-depth decimals:
  Ported the depth-aware end-of-section regex from CJS phase.cjs
  (named capture `(?<h>#{2,4})` + `\k<h>(?!#)` backreference). Now
  removing `### Phase 2:` stops at `### Phase 2.1:` (same depth, peer
  decimal) while continuing past `#### Phase 27.1:` (child depth).

bug-3602 (1 subtest) — phase.remove renumbers slugged plan refs:
  Extended the padded-plan-reference pattern with optional kebab-case
  slug segments `(?:-[A-Za-z][A-Za-z0-9-]*)*` between NN-NN and the
  PLAN/SUMMARY suffix, matching CJS phase.cjs:#3602 fix. Without this,
  `07-01-cherry-pick-foundation-PLAN.md` references stayed at `07-01-`
  after Phase 7 was removed, while the file on disk was already
  `06-01-...`.

config.test (1) — config-get git.base_branch returns "Key not found":
  configNewProject now filters out manifest keys legacy CJS init does
  NOT materialize: top-level `resolve_model_ids`, `context_window`,
  `mode`, `planning`, `graphify`; nested `git.base_branch`. These have
  their own resolution paths (origin/HEAD auto-detect for base_branch,
  feature opt-in for planning/graphify) and materializing the manifest
  defaults would suppress them. Manifest stays the schema source of
  truth per ADR §6; init shape stays minimal per legacy CJS contract.

gsd-sdk-query-registry-integration (1) — agents/gsd-intel-updater.md
references retargeted from `gsd-sdk query intel.*` to `gsd-tools intel
<subcommand>`. intel is out-of-seam per ADR §3 / PRD L160 ("CJS-only
Module handlers ... keep their in-process CJS implementations").
Removing the SDK catalog entries (cf4dd0cb) made the SDK route invalid;
the agent now correctly invokes the CJS handler via gsd-tools, which
routes through Shell Command Projection for cross-platform formatting.

Local: 79/79 across the failing test files. Docker re-run in progress.

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

* fix(3577): regenerate command-aliases + retarget workflow drift-gate

CI ubuntu-24 surfaced two remaining ADR-compliance gaps after the
previous push:

1. `sdk/src/query/command-aliases.generated.{ts,cjs}` still listed
   verify.codebase-drift + intel.{snapshot,patch-meta} from before the
   manifest-side removal. Ran `npx tsx sdk/scripts/gen-command-aliases.ts`
   to regenerate; both files now match the manifest source of truth.
   Closes the `command-seam-coverage.test.ts` "missing registry
   canonical verify.codebase-drift" failure (its assertion is correct —
   the SDK does NOT register codebase-drift, so the alias entry must
   not be present either).

2. `get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md`
   invoked `gsd-sdk query verify.codebase-drift` — drift is out-of-seam
   (CJS-only) per ADR §3 / PRD L160, so there is no SDK handler to
   route through. Retargeted to `gsd-tools verify codebase-drift` which
   dispatches direct to bin/lib/drift.cjs (the canonical implementation)
   via the CJS router. Closes the
   `gsd-sdk-query-registry-integration.test.cjs` failure.

Local: docker gsd-test-summary 11383/0 on plex2.

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

* fix(3575): raise Node heap for coverage in CI matrix

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: ci <ci@gsd-build>
2026-05-16 13:14:24 -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
570dddd1cd fix: make worktree cleanup fail closed (#3385)
* fix: make worktree cleanup fail closed

* chore: add changeset for worktree cleanup safety

* fix: address worktree cleanup review findings

* docs: label remove-workspace failure block

* fix: initialize worktree manifest before dispatch
2026-05-10 19:48:28 -04:00
Tom Boucher
2e87c60afc feat(3310): wire remaining ERROR_REASON typed codes into gsd-tools (#3311)
Closes #3310

Wires `ERROR_REASON.SDK_UNKNOWN_COMMAND` and `ERROR_REASON.USAGE` into the remaining untyped error paths in `gsd-tools.cjs` (template, frontmatter, requirements, milestone, uat, todo, workstream, graphify, learnings subcommand routers and `--cwd`/missing-required-arg paths). Tests assert via `JSON.parse(stderr).reason` rather than substring matching on prose. Closure regression guard locks the canonical `{ok, reason, message}` shape for every newly-typed path.

Also moves `--json-errors` activation up to the top of `main()` so `--cwd` validation paths emit JSON rather than plain text when `GSD_JSON_ERRORS=1` is set.
2026-05-09 13:54:06 -04:00
Tom Boucher
31e2c22309 feat(3255): add --json-errors structured error mode to gsd-tools (#3304)
* test(3255): add red/green tests for --json-errors structured error mode

Ten tests covering the --json-errors mode contract:
- Unknown command → sdk_unknown_command
- Dotted unknown command → sdk_unknown_command
- Missing --pick value → usage
- Config key not found → config_key_not_found
- Unknown subcommand → sdk_unknown_command
- GSD_JSON_ERRORS=1 env var activation
- Successful command unaffected
- Stable error shape ({ok, reason, message})
- Single error line per invocation
- Unknown flag → usage

All assertions use JSON.parse on stderr captures, never .includes() on
text (#2974 / CONTRIBUTING.md "Prohibited: Raw Text Matching" rule).

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

* feat(3255): add typed ERROR_REASON codes and GSD_JSON_ERRORS env var support

- Destructure ERROR_REASON from core in gsd-tools.cjs
- Add GSD_JSON_ERRORS=1 env var as alternative to --json-errors CLI flag
- Pass ERROR_REASON.SDK_UNKNOWN_COMMAND to unknown top-level command default path
- Pass ERROR_REASON.SDK_UNKNOWN_COMMAND to unknown intel subcommand path
- Pass ERROR_REASON.USAGE to --pick missing value error path
- Pass ERROR_REASON.USAGE to --version flag rejection path

All ten tests in feat-3255-json-errors-mode.test.cjs pass.

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

* docs(3255): add json-errors taxonomy doc, changeset, and CHANGELOG entry

- docs/json-errors.md: full error code taxonomy, wire format spec, and
  test-authoring guidelines for the --json-errors mode
- .changeset/gentle-tigers-roar.md: changeset fragment (pr will be updated
  after PR is opened)
- CHANGELOG.md: Unreleased → Added entry for the new structured error mode

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

* chore: update changeset PR number to 3304

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

* fix(gsd-tools): document --json-errors in usage/help text (#3255)

Add [--json-errors] to the TOP_LEVEL_USAGE synopsis line and introduce a
"Global flags:" section describing all four global flags (--raw, --pick,
--cwd, --ws) plus --json-errors with its GSD_JSON_ERRORS=1 env-var
alternative, so operators can discover the flag via `gsd-tools --help`.

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* adr: define planning workspace and worktree seam

* refactor(worktree): extract worktree safety policy module

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:15:04 -04:00
Tom Boucher
97bde8615f fix(cjs): accept dotted canonical command form (#3243) (#3248)
* test: reproduce CJS dispatcher rejecting dotted form (#3243)

runGsdTools assertions confirm that generate-slug.hello-world,
current-timestamp.date, validate.plan, roadmap.analyze, phases.list, and
check.decision-coverage-plan all fail with "Unknown command: <dotted>" —
the dispatcher switch only accepts the spaced form.

Edge cases (no dots unchanged, leading-dot rejected, unknown dotted form
suggests spaced equivalent) are also specified; those three pass already
because the shim is not yet implemented.

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

* fix(cjs): accept dotted canonical command form (#3243)

Add a shim at the top of main() in gsd-tools.cjs that splits args[0] on
the first dot when present, normalizing "state.update" → command='state'
args=['state','update',...] before the switch statement is reached.

Any caller that bypasses the SDK (stale npm-installed binary, workflow
shell-out, third-party script) can now use the canonical dotted form
natively without hitting "Unknown command: <domain>.<subcommand>".

The shim guards against empty head/rest so ".hidden" and bare "." args
are unchanged and fall through to the existing "Unknown command" path.

Also improves the default "Unknown command" error message to suggest
the spaced equivalent when a dotted form was passed — e.g. for "foo.bar"
the error now reads: Unknown command: foo — did you mean: "foo bar"?

Parallel to dottedCommandToCjsArgv in sdk/src/query/query-fallback-bridge-adapter.ts;
intentionally kept separate to avoid SDK coupling.

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

* changeset: pr=3248 for #3243

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

* test: tighten dotted-form suggestion assertion (CR nitpick)

* fix(cjs): suggestion uses first-dot split (CR finding 1, multi-dot consistency)

The "did you mean" hint in the Unknown-command default case was replacing ALL
dots with spaces (state.update.foo → "state update foo"), but the dispatcher
shim only splits on the FIRST dot (state.update.foo → head=state, rest=update.foo).
Apply CR's exact patch to use indexOf+slice so suggestion matches dispatch
behavior. Add a multi-dot regression test (a.b.c must suggest "a b.c", not
"a b c").

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 08:40:36 -04:00
Tom Boucher
e3b64b39f8 fix(#3019): query --help reaches handler instead of short-circuiting (#3026)
* fix(#3019): query --help reaches handler instead of short-circuiting to top-level usage

The query argv parser in sdk/src/cli.ts harvested -h/--help as a global
flag and main() short-circuited dispatch when args.help was true. Net
effect: every `gsd-sdk query <anything> --help` printed top-level USAGE
instead of contextual subcommand help. There was no path for users to
discover what arguments a query subcommand accepts — they had to trigger
"required" errors by trial and error.

Two-layer fix:

1. sdk/src/cli.ts (parseCliArgsQueryPermissive)
   - Push -h / --help onto queryArgv instead of consuming them silently,
     so the registered handler / gsd-tools.cjs fallback gets to interpret
     the flag and render contextual help.
   - Only honor the global help flag when there is NO real subcommand to
     dispatch to (i.e. queryArgv contains only help flags). Preserves
     `gsd-sdk query --help` → top-level USAGE while letting
     `gsd-sdk query phase add --help` reach the handler.

2. get-shit-done/bin/gsd-tools.cjs
   - Render top-level usage on --help / -h / -? / --usage instead of
     erroring with "Unknown flag". The discovery hint in the usage text
     points users at the working method (run without args → error names
     required arguments) and references #3019 for tracking subcommand-
     level help printers.
   - --version remains rejected (no discovery use-case).

#1818 anti-hallucination invariant preserved: the destructive command
NEVER executes when --help is present. The new shape returns success:true
+ usage on stdout instead of the old success:false + error on stderr —
both satisfy "destructive command did not run", and the new shape also
restores discoverability.

Tests:
- sdk/src/cli.test.ts: 4 new vitest cases covering #3019 — query argv
  parser keeps --help with subcommand, parses -h short flag, preserves
  bare `query --help` top-level behavior, preserves --help position when
  intermixed with other query flags.
- tests/bug-3019-help-passthrough.test.cjs: 5 node:test cases on the
  fallback — bare gsd-tools (no args) errors with usage; --help renders
  usage on stdout exit 0; -h same; subcommand --help renders usage; usage
  hint mentions discovery method (without prose substring matching —
  parses into typed sections).
- tests/bug-1818-unknown-flags.test.cjs: rewritten to assert the new
  invariant ("destructive command did not run" + "usage was rendered")
  instead of the old shape ("--help is rejected with non-zero exit").
  Each destructive test seeds a sentinel artifact (phase dir, slug
  output) and asserts it survives.

Verification:
- 47/47 vitest pass on sdk/src/cli.test.ts
- 5/5 pass on tests/bug-3019-help-passthrough.test.cjs
- 8/8 pass on tests/bug-1818-unknown-flags.test.cjs (rewritten)
- 6763/6763 pass on full node:test suite
- lint-no-source-grep clean (0 violations)

Closes #3019

* fix(#3019): SDK fallback forwards plain-text help, broader usage list (CR)

CodeRabbit on PR #3026 (4 findings — 1 Major outside-diff, 2 inline,
1 nitpick):

1. **Major outside-diff** — sdk/src/cli.ts:442-454. The fallback path
   that delegates to gsd-tools.cjs called parseCliQueryJsonOutput
   (JSON.parse) on stdout. Now that gsd-tools renders plain-text usage
   on --help, JSON.parse threw "Unexpected token 'U'". Wrapped the
   parse in try/catch — on parse failure, forward the plain stdout
   verbatim so subcommand help reaches the user. Regression test:
   tests/bug-3019-help-passthrough.test.cjs spawns the built SDK and
   asserts `gsd-sdk query phase --help` exits 0, stdout contains the
   gsd-tools usage, and stderr does NOT contain a JSON-parse error.

2. .changeset/help-passthrough.md:3 — `pr: TBD` → `pr: 3026`.

3. gsd-tools.cjs:346 (TOP_LEVEL_USAGE):
   - Removed self-referencing `#3019` link (immediately stale after
     this PR merges).
   - Expanded Commands list from 17 → all 47 dispatcher cases:
     agent-skills, audit-open, audit-uat, check-commit, commit, …
     phase, phases, roadmap, milestone, validate, progress, intel,
     graphify, learnings, etc. — the bulk of the surface that was
     previously unreachable via --help discovery.

4. Nitpick: `isUsageOutput` was duplicated in bug-1818 and
   bug-3019-help-passthrough tests. Moved to tests/helpers.cjs with
   structural-comment, removed both duplicates.

Verification: 47/47 vitest pass, 14/14 regression tests pass,
6764/6764 full suite, lint clean.

* test(#3019): use t.skip() instead of bare return when SDK not built (CR)

CodeRabbit follow-up on PR #3026:

The integration test guarded against missing sdk/dist/cli.js with a
bare `return;` — node:test counts that as a passing test (0 assertions
exercised, 0 failures). On a CI checkout that hasn't run the SDK build,
the #3026 regression test silently green-lit and no signal ever
surfaced that the integration check was skipped.

Switched to `t.skip(...)` via the test context parameter so the
omission shows up in the test report. The unit-level fix
(sdk/src/cli.ts) is still covered by vitest, so the skip only affects
the end-to-end spawn-built-SDK check.

Verification: 6/6 pass when SDK is built; 5 pass + 1 skip when not.
2026-05-02 11:45:33 -04:00
Tom Boucher
f55069ecbf test(#2974): migrate 8 test files to typed-IR assertions (#3016)
* test(#2974): migrate 8 test files to typed-IR assertions

Replaces raw stdout/stderr substring matching with structured-field
assertions per CONTRIBUTING.md "Prohibited: Raw Text Matching on Test
Outputs". Adds shared infrastructure for typed error emission so this
pattern is the easy path going forward.

Shared infrastructure:
- core.cjs: ERROR_REASON frozen enum + setJsonErrorMode/getJsonErrorMode
- gsd-tools.cjs: --json-errors CLI flag, parsed before subcommand dispatch
- config.cjs: typed reasons at all 7 error sites
- graphify.cjs: GRAPHIFY_REASON enum + reason/timeout_ms in execGraphify result
- bin/install.js: pure buildSdkFailFastReport() IR builder + renderer
- hooks/gsd-session-state.sh, gsd-phase-boundary.sh: emit Claude Code
  hookSpecificOutput JSON envelope with typed state_present/config_mode/
  planning_modified/file_path fields (no-op when hooks.community is off)

Test migrations (all pass, 171 tests across the 8 files):
- bug-2649-sdk-fail-fast: assert on ir.reason / ir.context / ir.fix_command
- bug-2687-config-read-warning-parity: assert.equal stderr === ''
- bug-2796-arg-parsing-regression: assert on result.json.updated/.phase
- bug-2838-summary-rescue: parse rescue footer, assert mtime invariant
- bug-2943-config-get-context-window: parse JSON, assert ERROR_REASON.CONFIG_KEY_NOT_FOUND
- graphify: assert reason === GRAPHIFY_REASON.ENOENT/TIMEOUT
- hooks-opt-in: parse hookSpecificOutput, assert typed fields
- security-scan: reclassified as source-text-is-the-product (scan label
  output and CI workflow YAML ARE the deployed contract)

Verification: lint-no-source-grep clean (0 violations), full suite
6741/6741 pass.

Closes #2974

* test(#2974): address CR feedback — typed code field, robust idempotency

Two CodeRabbit findings on #3016 addressed:

1. tests/hooks-opt-in.test.cjs:355 (Minor, inline) —
   parsed.reason.includes('Conventional Commits') was still substring
   matching after the typed-IR migration. Fixed at the source: the
   gsd-validate-commit hook now emits a typed `code` field
   ('CONVENTIONAL_COMMITS_VIOLATION', 'COMMIT_SUBJECT_TOO_LONG')
   alongside the human-readable `reason`. Test asserts strictEqual
   on the code; the prose copy is no longer part of the test contract.

2. tests/bug-2838-summary-rescue-gitignored-planning.test.cjs:224-250
   (Outside-diff) — mtimeMs alone can stay unchanged on coarse-grained
   filesystems (HFS+, FAT) when two rewrites land within the same
   timestamp tick, falsely passing the idempotency assertion.
   Replaced with a full snapshot (mtimeMs, ctimeMs, size, ino, sha256
   of contents) compared via assert.deepStrictEqual — the hash
   catches any rewrite the timestamp would miss.

Verification: 30/30 pass on the two affected files; lint-no-source-grep
clean (0 violations across 368 test files).
2026-05-02 09:27:23 -04:00
Tom Boucher
faee0287a0 fix(detect-custom-files): add skills/ to GSD_MANAGED_DIRS (#2942) (#2945)
After v1.39.0 skill consolidation (#2790), skills/ became a GSD-managed
root that the installer wipes on update. GSD_MANAGED_DIRS in gsd-tools.cjs
was missing 'skills', so user-added skill directories (e.g.
skills/custom-skill/SKILL.md) were never walked and silently destroyed
during /gsd-update.

- Add 'skills' to GSD_MANAGED_DIRS so the directory is walked
- Add tests/bug-2942-detect-custom-skills.test.cjs with 5 targeted tests
- Update tests/update-custom-backup.test.cjs: replace the now-incorrect
  "skills/ must NOT be scanned" assertion (written pre-#2790) with a test
  that verifies custom skills ARE detected and GSD-owned skills are not
  falsely flagged

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:22:13 -04:00
Tom Boucher
9a13d2fc0b fix(#2911): audit-open emits raw human report and parseable JSON
Two bugs in the audit-open dispatch case in bin/gsd-tools.cjs:

  1. Bare output(...) calls (only core.output is in scope) threw
     ReferenceError: output is not defined on every invocation,
     blocking the first step of /gsd-complete-milestone.
  2. Even after switching to core.output(formattedReport, raw), the
     human-readable branch JSON-stringified the formatted text because
     core.output only bypasses JSON encoding when called as
     core.output(null, true, rawValue).

Fix:
  - --json path:  core.output(result, raw)   — pass the object,
    let core.output JSON-stringify (don't pre-stringify).
  - text path:    core.output(null, true, formatAuditReport(result))
    — use the rawValue form to emit verbatim section dividers and
    item lists.

Adds tests/bug-2911-audit-open-output-shape.test.cjs which parses
both modes structurally — line-by-line for text mode (asserting the
report headers exist as standalone lines, not as escaped \n inside a
JSON quoted string), and JSON.parse + key-by-key shape assertions for
--json mode (matching the contract returned by auditOpenArtifacts).
2026-04-30 17:30:19 -04:00
Tom Boucher
444db1714b refactor(query): manifest-backed routing seam + family adapters (#2908)
Merging validated command-seam foundation.
2026-04-30 14:04:50 -04:00
Tom Boucher
abb2cb63f6 refactor: extract planning-workspace seam from core.cjs (#2901)
* refactor: extract planning workspace seam from core

* docs: document planning-workspace module and inventory updates

* fix: harden planning lock timeout and preserve workstream set contract

---------

Co-authored-by: Tom Boucher <thomas.boucher@sas.com>
2026-04-30 11:38:13 -04:00
Tom Boucher
5fdc950eb7 feat(#2792): namespace meta-skills + keyword-tag descriptions + context utilization guard (#2825)
* feat(#2792): namespace meta-skills retargeted at the post-#2790 surface

This branch is now based on #2790's HEAD (the consolidation PR) instead
of main, and every routing table targets the consolidated surface so a
user routed by a namespace meta-skill never lands at a deleted /
folded sub-skill.

Cross-PR inconsistencies the original PR #2825 carried (vs #2790):

  - ns-ideate routed to gsd-note / gsd-add-todo / gsd-add-backlog /
    gsd-plant-seed → all folded into gsd-capture by #2790. Now routes
    to gsd-capture (the parent picks the mode from the user's intent).
  - ns-context routed to gsd-scan and gsd-intel → folded into
    gsd-map-codebase --fast / --query by #2790. Now routes to those
    flag forms.
  - ns-manage routed all workspace intent to gsd-list-workspaces (a
    list-only entry) → CR also flagged the over-narrow target. #2790
    folds into gsd-workspace; routing now points there.
  - ns-workflow routed to gsd-research-phase → deleted outright by
    #2790. Removed.
  - ns-project routed to gsd-plan-milestone-gaps → deleted outright by
    #2790. Removed.
  - None of the namespaces previously surfaced #2790's new consolidated
    skills (gsd-capture, gsd-phase, gsd-config, gsd-workspace,
    gsd-progress). All five are now reachable through the routers.
  - extract_learnings → extract-learnings (canonicalized by #2858).

Defect fixes within the namespace skills:

  - Hyphen-form `name:` (gsd-workflow, …) per the canonical naming
    contract — the colon-form addressed CR's drift complaint.
  - `Skill` added to allowed-tools on every router. The body instructs
    "Invoke the matched skill directly using the Skill tool" — without
    Skill in the permission list the meta-skill cannot route at all.

New regression guard in tests/enh-2792-namespace-skills.test.cjs: every
gsd-* token in any namespace router's table column resolves to a
surviving commands/gsd/*.md file (or to a known consolidated parent for
flag-form targets like gsd-map-codebase --fast). This single test would
have caught every dead-end route the original PR shipped with.

Skill-count cap in tests/enh-2790-skill-consolidation.test.cjs now
filters out ns-*.md from its <= 63 cap. Namespace routers are
descriptor-only entries, not part of the consolidation surface that cap
is policing — they have their own contract in
tests/enh-2792-namespace-skills.test.cjs.

INVENTORY.md gains a "Namespace Meta-Skills" section with the 6 router
rows; INVENTORY-MANIFEST.json gains 6 entries; the headline count moves
59 → 65 to match.

Out of scope for this rebase: the gsd-health --context flag (PR #2825
advertised the contract but didn't implement it). That's a separate
feature concern and is left untouched here.

5908/5908 on `npm test`.

* feat(#2792): implement gsd-health --context utilization guard

The original PR #2825 advertised a `--context` flag on gsd-health with a
60%/70% utilization threshold table but never implemented the workflow
logic — CR caught it as a contract leak, the rebase deferred it. This
commit closes the gap with TDD red/green/refactor.

Math layer (pure):
  - get-shit-done/bin/lib/context-utilization.cjs
    classifyContextUtilization(tokensUsed, contextWindow) →
      { percent, state }
    State boundaries use the exact ratio:
      < 60% healthy / 60–70% warning / ≥ 70% critical (fracture point)
    Display percent rounded for humans. Throws TypeError on non-integer
    or out-of-range inputs.
  - STATES = Object.freeze({ HEALTHY, WARNING, CRITICAL }) exported
    so callers reference the names by symbol, not by literal string.

SDK CLI integration:
  - get-shit-done/bin/gsd-tools.cjs
    `validate context --tokens-used N --context-window M [--json]`
    routes to the classifier, owns the recommendation copy (the
    classifier intentionally does not — keeps the renderer free to
    evolve without touching the math layer or its tests), and uses
    core.output's rawValue path for the sync-flush guarantee.
  - sdk/src/query/validate.ts + sdk/src/query/index.ts
    TypeScript validateContext handler registered at 'validate.context'
    and 'validate context'. Mirrors the CJS classifier inline (15 lines
    of arithmetic; not worth a shared cross-language module).

User-facing wiring:
  - commands/gsd/health.md frontmatter advertises --context, body
    documents the three-state threshold table.
  - get-shit-done/workflows/health.md adds a `context_check` step
    that's reached only when --context is set. Step calls
    `gsd-sdk query validate.context` with self-reported tokensUsed and
    contextWindow, prints the SDK output verbatim, and ends. Includes
    a TEXT_MODE plain-text fallback for non-Claude runtimes per #2012.

Tests:
  - tests/context-utilization.test.cjs (17 tests) — pure-function
    contract: state thresholds at every boundary, percent rounding,
    input validation, return-shape (no recommendation field — that's
    the renderer's job).
  - tests/validate-context.test.cjs (9 tests) — SDK CLI plumbing:
    arg parsing errors, JSON vs human rendering, recommendation copy
    pinned per state.
  - tests/enh-2792-namespace-skills.test.cjs (4 new tests) — markdown
    contract: --context advertised in argument-hint, threshold table
    in command body, context_check step exists in workflow, step
    invokes gsd-sdk query validate.context with both flags.

Inventory bookkeeping:
  - docs/INVENTORY.md "CLI Modules" 31 → 32; new row for
    context-utilization.cjs.
  - docs/INVENTORY-MANIFEST.json mirror.

5939/5939 on `npm test`.
2026-04-30 01:04:41 -04:00
Tom Boucher
eddb2a205b fix(#2801): add ingest-docs handler to gsd-tools init dispatch (#2820)
* fix(#2801): add ingest-docs handler to gsd-tools init dispatch

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

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

Closes #2801

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

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:22:40 -04:00
Tom Boucher
dc9b712967 refactor(state): drop unused args + lift currentPhase in cmdStateCompletePhase (#2761)
* refactor(state): drop unused args param and lift currentPhase in cmdStateCompletePhase

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

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

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

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

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

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

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

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

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

---------

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

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

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

Closes #2734
Closes #2735

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

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

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:43 -04:00
Tom Boucher
e973ff4cb6 fix(#2630): reset STATE.md frontmatter atomically on milestone switch (#2666)
The /gsd:new-milestone workflow Step 5 rewrote STATE.md's Current Position
body but never touched the YAML frontmatter, so every downstream reader
(state.json, getMilestoneInfo, progress bars) kept reporting the stale
milestone until the first phase advance forced a resync. Asymmetric with
milestone.complete, which uses readModifyWriteStateMdFull.

Add a new `state milestone-switch` handler (both SDK and CJS) that atomically:
- Stomps frontmatter milestone/milestone_name with caller-supplied values
- Resets status to 'planning' and progress counters to zero
- Rewrites the ## Current Position section to the new-milestone template
- Preserves Accumulated Context (decisions, blockers, todos)

Wire the workflow Step 5 to invoke `state.milestone-switch` instead of the
manual body rewrite. Note the flag is `--milestone` not `--version`:
gsd-tools reserves `--version` as a globally-invalid help flag.

Red vitest in sdk/src/query/state-mutation.test.ts asserts the frontmatter
reset. Regression guard via node:test in tests/bug-2630-*.test.cjs runs
through gsd-tools end-to-end.

Fixes #2630

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:05:10 -04:00
Tom Boucher
1a3d953767 feat: add unified post-planning gap checker (closes #2493) (#2610)
* feat: add unified post-planning gap checker (closes #2493)

Adds a unified post-planning gap checker as Step 13e of plan-phase.md.
After all plans are generated and committed, scans REQUIREMENTS.md and
CONTEXT.md <decisions> against every PLAN.md in the phase directory and
emits a single Source | Item | Status table.

Why
- The existing Requirements Coverage Gate (§13) blocks/re-plans on REQ
  gaps but emits two separate per-source signals. Issue #2493 asks for
  one unified report after planning so that requirements AND
  discuss-phase decisions slipping through are surfaced in one place
  before execution starts.

What
- New workflow.post_planning_gaps boolean config key, default true,
  added to VALID_CONFIG_KEYS, CONFIG_DEFAULTS, hardcoded.workflow, and
  cmdConfigSet (boolean validation).
- New get-shit-done/bin/lib/decisions.cjs — shared parser for
  CONTEXT.md <decisions> blocks (D-NN entries). Designed for reuse by
  the related #2492 plan/verify decision gates.
- New get-shit-done/bin/lib/gap-checker.cjs — parses REQUIREMENTS.md
  (checkbox + traceability table forms), reads CONTEXT.md decisions,
  walks PHASE_DIR/*-PLAN.md, runs word-boundary coverage detection
  (REQ-1 must not match REQ-10), formats a sorted report.
- New gsd-tools gap-analysis CLI command wired through gsd-tools.cjs.
- workflows/plan-phase.md gains §13e between §13d (commit plans) and
  §14 (Present Final Status). Existing §13 gate preserved — §13e is
  additive and non-blocking.
- sdk/prompts/workflows/plan-phase.md gets an equivalent
  post_planning_gaps step for headless mode.
- Docs: CONFIGURATION.md, references/planning-config.md, INVENTORY.md,
  INVENTORY-MANIFEST.json all updated.

Tests
- tests/post-planning-gaps-2493.test.cjs: 30 test cases covering step
  insertion position, decisions parser, gap detector behavior
  (covered/not-covered, false-positive guard, missing-file
  resilience, malformed-input resilience, gate on/off, deterministic
  natural sort), and full config integration.
- Full suite: 5234 / 5234 pass.

Design decisions
- Numbered §13e (sub-step), not §14 — §14 already exists (Present
  Final Status); inserting before it preserves downstream auto-advance
  step numbers.
- Existing §13 gate kept, not replaced — §13 blocks/re-plans on
  REQ gaps; §13e is the unified post-hoc report. Per spec: "default
  behavior MUST be backward compatible."
- Word-boundary ID matching avoids REQ-1 matching REQ-10 and avoids
  brittle semantic/substring matching.
- Shared decisions.cjs parser so #2492 can reuse the same regex.
- Natural-sort keys (REQ-02 before REQ-10) for deterministic output.
- Boolean validation in cmdConfigSet rejects non-boolean values
  matches the precedent set by drift_threshold/drift_action.

Closes #2493

* fix(#2493): expose post_planning_gaps in loadConfig() + sync schema example

Address CodeRabbit review on PR #2610:

- core.cjs loadConfig(): return post_planning_gaps from both the
  config.json branch and the global ~/.gsd/defaults.json fallback so
  callers can rely on config.post_planning_gaps regardless of whether
  the key is present (comment 3127977404, Major).
- docs/CONFIGURATION.md: add workflow.post_planning_gaps to the Full
  Schema JSON example so copy/paste users see the new toggle alongside
  security_block_on (comment 3127977392, Minor).
- tests/post-planning-gaps-2493.test.cjs: regression coverage for
  loadConfig() — default true when key absent, honors explicit
  true/false from workflow.post_planning_gaps.
2026-04-22 23:03:59 -04:00
Tom Boucher
1a694fcac3 feat: auto-remap codebase after significant phase execution (closes #2003) (#2605)
* feat: auto-remap codebase after significant phase execution (#2003)

Adds a post-phase structural drift detector that compares the committed tree
against `.planning/codebase/STRUCTURE.md` and either warns or auto-remaps
the affected subtrees when drift exceeds a configurable threshold.

## Summary
- New `bin/lib/drift.cjs` — pure detector covering four drift categories:
  new directories outside mapped paths, new barrel exports at
  `(packages|apps)/*/src/index.*`, new migration files, and new route
  modules. Prioritizes the most-specific category per file.
- New `verify codebase-drift` CLI subcommand + SDK handler, registered as
  `gsd-sdk query verify.codebase-drift`.
- New `codebase_drift_gate` step in `execute-phase` between
  `schema_drift_gate` and `verify_phase_goal`. Non-blocking by contract —
  any error logs and the phase continues.
- Two new config keys: `workflow.drift_threshold` (int, default 3) and
  `workflow.drift_action` (`warn` | `auto-remap`, default `warn`), with
  enum/integer validation in `config-set`.
- `gsd-codebase-mapper` learns an optional `--paths <p1,p2,...>` scope hint
  for incremental remapping; agent/workflow docs updated.
- `last_mapped_commit` lives in YAML frontmatter on each
  `.planning/codebase/*.md` file; `readMappedCommit`/`writeMappedCommit`
  round-trip helpers ship in `drift.cjs`.

## Tests
- 55 new tests in `tests/drift-detection.test.cjs` covering:
  classification, threshold gating at 2/3/4 elements, warn vs. auto-remap
  routing, affected-path scoping, `--paths` sanitization (traversal,
  absolute, shell metacharacter rejection), frontmatter round-trip,
  defensive paths (missing STRUCTURE.md, malformed input, non-git repos),
  CLI JSON output, and documentation parity.
- Full suite: 5044 pass / 0 fail.

## Documentation
- `docs/CONFIGURATION.md` — rows for both new keys.
- `docs/ARCHITECTURE.md` — section on the post-execute drift gate.
- `docs/AGENTS.md` — `--paths` flag on `gsd-codebase-mapper`.
- `docs/USER-GUIDE.md` — user-facing behavior note + toggle commands.
- `docs/FEATURES.md` — new 27a section with REQ-DRIFT-01..06.
- `docs/INVENTORY.md` + `docs/INVENTORY-MANIFEST.json` — drift.cjs listed.
- `get-shit-done/workflows/execute-phase.md` — `codebase_drift_gate` step.
- `get-shit-done/workflows/map-codebase.md` — `parse_paths_flag` step.
- `agents/gsd-codebase-mapper.md` — `--paths` directive under parse_focus.

## Design decisions
- **Frontmatter over sidecar JSON** for `last_mapped_commit`: keeps the
  baseline attached to the file, survives git moves, survives per-doc
  regeneration, no extra file lifecycle.
- **Substring match against STRUCTURE.md** for `isPathMapped`: the map is
  free-form markdown, not a structured manifest; any mention of a path
  prefix counts as "mapped territory". Cheap, no parser, zero false
  negatives on reasonable maps.
- **Category priority migration > route > barrel > new_dir** so a file
  matching multiple rules counts exactly once at the most specific level.
- **Empty-tree SHA fallback** (`4b825dc6…`) when `last_mapped_commit` is
  absent — semantically correct (no baseline means everything is drift)
  and deterministic across repos.
- **Four layers of non-blocking** — detector try/catch, CLI try/catch, SDK
  handler try/catch, and workflow `|| echo` shell fallback. Any single
  layer failing still returns a valid skipped result.
- **SDK handler delegates to `gsd-tools.cjs`** rather than re-porting the
  detector to TypeScript, keeping drift logic in one canonical place.

Closes #2003

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

* docs(mapper): tag --paths fenced block as text (CodeRabbit MD040)

Comment 3127255172.

* docs(config): use /gsd- dash command syntax in drift_action row (CodeRabbit)

Comment 3127255180. Matches the convention used by every other command
reference in docs/CONFIGURATION.md.

* fix(execute-phase): initialize AGENT_SKILLS_MAPPER + tag fenced blocks

Two CodeRabbit findings on the auto-remap branch of the drift gate:

- 3127255186 (must-fix): the mapper Task prompt referenced
  ${AGENT_SKILLS_MAPPER} but only AGENT_SKILLS (for gsd-executor) is
  loaded at init_context (line 72). Without this fix the literal
  placeholder string would leak into the spawned mapper's prompt.
  Add an explicit gsd-sdk query agent-skills gsd-codebase-mapper step
  right before the Task spawn.
- 3127255183: tag the warn-message and Task() fenced code blocks as
  text to satisfy markdownlint MD040.

* docs(map-codebase): wire PATH_SCOPE_HINT through every mapper prompt

CodeRabbit (review id 4158286952, comment 3127255190) flagged that the
parse_paths_flag step defined incremental-remap semantics but did not
inject a normalized variable into the spawn_agents and sequential_mapping
mapper prompts, so incremental remap could silently regress to a
whole-repo scan.

- Define SCOPED_PATHS / PATH_SCOPE_HINT in parse_paths_flag.
- Inject ${PATH_SCOPE_HINT} into all four spawn_agents Task prompts.
- Document the same scope contract for sequential_mapping mode.

* fix(drift): writeMappedCommit tolerates missing target file

CodeRabbit (review id 4158286952, drift.cjs:349-355 nitpick) noted that
readMappedCommit returns null on ENOENT but writeMappedCommit threw — an
asymmetry that breaks first-time stamping of a freshly produced doc that
the caller has not yet written.

- Catch ENOENT on the read; treat absent file as empty content.
- Add a regression test that calls writeMappedCommit on a non-existent
  path and asserts the file is created with correct frontmatter.
  Test was authored to fail before the fix (ENOENT) and passes after.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 21:21:44 -04:00
Tom Boucher
57bbfe652b fix: exclude non-wiped dirs from custom-file scan; warn on non-Claude model profiles (#2511)
* fix(detect-custom-files): exclude skills and command dirs not wiped by installer (closes #2505)

GSD_MANAGED_DIRS included 'skills' and 'command' directories, but the
installer never wipes those paths. Users with third-party skills installed
(40+ files, none in GSD's manifest) had every skill flagged as a "custom
file" requiring backup, producing noisy false-positive reports on every
/gsd-update run.

Removes 'skills' and 'command' from both gsd-tools.cjs and the SDK's
detect-custom-files.ts. Adds two regression tests confirming neither
directory is scanned.

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

* fix(settings): warn that model profiles are no-ops on non-Claude runtimes (closes #2506)

settings.md presented Quality/Balanced/Budget model profiles without any
indication that these tiers map to Claude models (Opus/Sonnet/Haiku) and
have no effect on non-Claude runtimes (Codex, Gemini CLI, OpenRouter).
Users on Codex saw the profile chooser as if it would meaningfully select
models, but all agents silently used the runtime default regardless.

Adds a non-Claude runtime note before the profile question (shown in
TEXT_MODE, the path all non-Claude runtimes take) explaining the profiles
are no-ops and directing users to either choose Inherit or configure
model_overrides manually. Also updates the Inherit option description to
explicitly name the runtimes where it is the correct choice.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 10:10:10 -04:00
Tom Boucher
bd27d4fabe feat(roadmap): surface wave dependencies and cross-cutting constraints (#2487)
* feat(roadmap): surface wave dependencies and cross-cutting constraints (#2447)

Adds roadmap.annotate-dependencies command that post-processes a phase's
ROADMAP plan list to insert wave dependency notes and surface must_haves.truths
entries shared across 2+ plans as cross-cutting constraints. Operation is
idempotent and purely derived from existing PLAN frontmatter.

Closes #2447

* fix(roadmap): address CodeRabbit review findings on PR #2487

- roadmap.cjs: expand idempotency guard to also check for existing
  cross-cutting constraints header, preventing duplicate injection on
  re-runs; add content equality check before writing to preserve
  true idempotency for single-wave phases
- plan-phase.md: move ROADMAP annotation (13d) before docs commit (13c)
  so annotated ROADMAP.md is included in the commit rather than left dirty;
  include .planning/ROADMAP.md in committed files list
- sdk/src/query/index.ts: add annotate-dependencies aliases to
  QUERY_MUTATION_COMMANDS so the mutation is properly event-wired
- sdk/src/query/roadmap.ts: add timeout (15s) and maxBuffer to spawnSync;
  check result.error before result.status to handle spawn/timeout failures

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:19:21 -04:00
Tom Boucher
e8ec42082d feat(health): detect MILESTONES.md drift from archived snapshots (#2446) (#2486)
Adds W018 warning when .planning/milestones/vX.Y-ROADMAP.md snapshots
exist without a corresponding entry in MILESTONES.md. Introduces
--backfill flag to synthesize missing entries from snapshot titles.

Closes #2446
2026-04-20 18:19:14 -04:00
Rezolv
39623fd5b8 docs(cli): deprecate gsd-tools.cjs header in favor of gsd-sdk (#2343) (#2343)
Single-file change: JSDoc @deprecated notice pointing to SDK query registry.
No .planning or unrelated merges.
2026-04-19 23:10:32 -04:00
Tom Boucher
5f521e0867 fix(settings): route /gsd-settings reads/writes through workstream-aware config path (#2285)
settings.md was reading and writing .planning/config.json directly while
gsd-tools config-get/config-set route to .planning/workstreams/<slug>/config.json
when GSD_WORKSTREAM is active, causing silent write-read drift (Closes #2282).

- config.cjs: add cmdConfigPath() — emits the planningDir-resolved config path as
  plain text (always raw, no JSON wrapping) so shell substitution works correctly
- gsd-tools.cjs: wire config-path subcommand
- settings.md: resolve GSD_CONFIG_PATH via config-path in ensure_and_load_config;
  replace hardcoded cat .planning/config.json and Write to .planning/config.json
  with $GSD_CONFIG_PATH throughout
- phase.cjs: fix renameDecimalPhases to preserve zero-padded prefix (06.3 → 06.2
  not 6.2) — pre-existing test failure on main
- tests/config.test.cjs: add config-path command tests (#2282)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 16:46:10 -04:00
Tom Boucher
7b85d9e689 fix(cli): audit-open crashes with ReferenceError: output is not defined (#2236) (#2238)
The audit-open case in gsd-tools.cjs called bare output() on both the --json
and text paths. output is never in scope at the call site — the entire core
module is imported as `const core`, so every other command uses core.output().

Two-part fix:
- Replace output(...) with core.output(...) on both branches
- Pass result (the raw object) on the --json path, not JSON.stringify(result)
  — core.output always calls JSON.stringify internally, so pre-serialising
  caused double-encoding and agents received a string instead of an object

Adds three CLI-level regression tests to milestone-audit.test.cjs that invoke
audit-open through runGsdTools (the same path the agent uses), so a recurrence
at the dispatch layer is caught even if lib-level tests continue to pass.

Closes #2236

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:59:12 -04:00
Tom Boucher
62b5278040 fix(installer): restore detect-custom-files and backup_custom_files lost in release drift (#1997) (#2233)
PR #2038 added detect-custom-files to gsd-tools.cjs and the backup_custom_files
step to update.md, but commit 7bfb11b6 is not an ancestor of v1.36.0: main was
rebuilt after the merge, orphaning the change. Users on 1.36.0 running /gsd-update
silently lose any locally-authored files inside GSD-managed directories.

Root cause: git merge-base 7bfb11b6 HEAD returns aa3e9cf (Cline runtime, PR #2032),
117 commits before the release tag. The "merged" GitHub state reflects the PR merge
event, not reachability from the default branch.

Fix: re-apply the three changes from 7bfb11b6 onto current main:
- Add detect-custom-files subcommand to gsd-tools.cjs (walk managed dirs, compare
  against gsd-file-manifest.json keys via path.relative(), return JSON list)
- Add 'detect-custom-files' to SKIP_ROOT_RESOLUTION set
- Restore backup_custom_files step in update.md before run_update
- Restore tests/update-custom-backup.test.cjs (7 tests, all passing)

Closes #2229
Closes #1997

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 18:50:53 -04:00
pingchesu
c11ec05554 feat: /gsd-graphify integration — knowledge graph for planning agents (#2164)
* feat(01-01): create graphify.cjs library module with config gate, subprocess helper, presence detection, and version check

- isGraphifyEnabled() gates on config.graphify.enabled in .planning/config.json
- disabledResponse() returns structured disabled message with enable instructions
- execGraphify() wraps spawnSync with PYTHONUNBUFFERED=1, 30s timeout, ENOENT/SIGTERM handling
- checkGraphifyInstalled() detects missing binary via --help probe
- checkGraphifyVersion() uses python3 importlib.metadata, validates >=0.4.0,<1.0 range

* feat(01-01): register graphify.enabled in VALID_CONFIG_KEYS

- Added graphify.enabled after intel.enabled in config.cjs VALID_CONFIG_KEYS Set
- Enables gsd-tools config-set graphify.enabled true without key rejection

* test(01-02): add comprehensive unit tests for graphify.cjs module

- 23 tests covering all 5 exported functions across 5 describe blocks
- Config gate tests: enabled/disabled/missing/malformed scenarios (TEST-03, FOUND-01)
- Subprocess tests: success, ENOENT, timeout, env vars, timeout override (FOUND-04)
- Presence tests: --help detection, install instructions (FOUND-02, TEST-04)
- Version tests: compatible/incompatible/unparseable/missing (FOUND-03, TEST-04)
- Fix graphify.cjs to use childProcess.spawnSync (not destructured) for testability

* feat(02-01): add graphifyQuery, graphifyStatus, graphifyDiff to graphify.cjs

- safeReadJson wraps JSON.parse in try/catch, returns null on failure
- buildAdjacencyMap creates bidirectional adjacency map from graph nodes/edges
- seedAndExpand matches on label+description (case-insensitive), BFS-expands up to maxHops
- applyBudget uses chars/4 token estimation, drops AMBIGUOUS then INFERRED edges
- graphifyQuery gates on config, reads graph.json, supports --budget option
- graphifyStatus returns exists/last_build/counts/staleness or no-graph message
- graphifyDiff compares current graph.json against .last-build-snapshot.json

* feat(02-01): add case 'graphify' routing block to gsd-tools.cjs

- Routes query/status/diff/build subcommands to graphify.cjs handlers
- Query supports --budget flag via args.indexOf parsing
- Build returns Phase 3 placeholder error message
- Unknown subcommand lists all 4 available options

* feat(02-01): create commands/gsd/graphify.md command definition

- YAML frontmatter with name, description, argument-hint, allowed-tools
- Config gate reads .planning/config.json directly (not gsd-tools config get-value)
- Inline CLI calls for query/status/diff subcommands
- Agent spawn placeholder for build subcommand
- Anti-read warning and anti-patterns section

* test(02-02): add Phase 2 test scaffolding with fixture helpers and describe blocks

- Import 7 Phase 2 exports (graphifyQuery, graphifyStatus, graphifyDiff, safeReadJson, buildAdjacencyMap, seedAndExpand, applyBudget)
- Add writeGraphJson and writeSnapshotJson fixture helpers
- Add SAMPLE_GRAPH constant with 5 nodes, 5 edges across all confidence tiers
- Scaffold 7 new describe blocks for Phase 2 functions

* test(02-02): add comprehensive unit tests for all Phase 2 graphify.cjs functions

- safeReadJson: valid JSON, malformed JSON, missing file (3 tests)
- buildAdjacencyMap: bidirectional entries, orphan nodes, edge objects (3 tests)
- seedAndExpand: label match, description match, BFS depth, empty results, maxHops (5 tests)
- applyBudget: no budget passthrough, AMBIGUOUS drop, INFERRED drop, trimmed footer (4 tests)
- graphifyQuery: disabled gate, no graph, valid query, confidence tiers, budget, counts (6 tests)
- graphifyStatus: disabled gate, no graph, counts with graph, hyperedge count (4 tests)
- graphifyDiff: disabled gate, no baseline, no graph, added/removed, changed (5 tests)
- Requirements: TEST-01, QUERY-01..03, STAT-01..02, DIFF-01..02
- Full suite: 53 graphify tests pass, 3666 total tests pass (0 regressions)

* feat(03-01): add graphifyBuild() pre-flight, writeSnapshot(), and build_timeout config key

- Add graphifyBuild(cwd) returning spawn_agent JSON with graphs_dir, timeout, version
- Add writeSnapshot(cwd) reading graph.json and writing atomic .last-build-snapshot.json
- Register graphify.build_timeout in VALID_CONFIG_KEYS
- Import atomicWriteFileSync from core.cjs for crash-safe snapshot writes

* feat(03-01): wire build routing in gsd-tools and flesh out builder agent prompt

- Replace Phase 3 placeholder with graphifyBuild() and writeSnapshot() dispatch
- Route 'graphify build snapshot' to writeSnapshot(), 'graphify build' to graphifyBuild()
- Expand Step 3 builder agent prompt with 5-step workflow: invoke, validate, copy, snapshot, summary
- Include error handling guidance: non-zero exit preserves prior .planning/graphs/

* test(03-02): add graphifyBuild test suite with 6 tests

- Disabled config returns disabled response
- Missing CLI returns error with install instructions
- Successful pre-flight returns spawn_agent action with correct shape
- Creates .planning/graphs/ directory if missing
- Reads graphify.build_timeout from config (custom 600s)
- Version warning included when outside tested range

* test(03-02): add writeSnapshot test suite with 6 tests

- Writes snapshot from existing graph.json with correct structure
- Returns error when graph.json does not exist
- Returns error when graph.json is invalid JSON
- Handles empty nodes and edges arrays
- Handles missing nodes/edges keys gracefully
- Overwrites existing snapshot on incremental rebuild

* feat(04-01): add load_graph_context step to gsd-planner agent

- Detects .planning/graphs/graph.json via ls check
- Checks graph staleness via graphify status CLI call
- Queries phase-relevant context with single --budget 2000 query
- Silent no-op when graph.json absent (AGENT-01)

* feat(04-01): add Step 1.3 Load Graph Context to gsd-phase-researcher agent

- Detects .planning/graphs/graph.json via ls check
- Checks graph staleness via graphify status CLI call
- Queries 2-3 capability keywords with --budget 1500 each
- Silent no-op when graph.json absent (AGENT-02)

* test(04-01): add AGENT-03 graceful degradation tests

- 3 AGENT-03 tests: absent-graph query, status, multi-term handling
- 2 D-12 integration tests: known-graph query and status structure
- All 5 tests pass with existing helpers and imports
2026-04-12 18:17:18 -04:00
Andreas Brauchli
72af8cd0f7 fix: display relative time in intel status output (#2132)
* fix: display relative time instead of UTC in intel status output

The `updated_at` timestamps in `gsd-tools intel status` were displayed
as raw ISO/UTC strings, making them appear to show the wrong time in
non-UTC timezones. Replace with fuzzy relative times ("5 minutes ago",
"1 day ago") which are timezone-agnostic and more useful for freshness.

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

* test: add regression tests for timeAgo utility

Covers boundary values (seconds/minutes/hours/days/months/years),
singular vs plural formatting, and future-date edge case.

Addresses review feedback on #2132.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:54:17 -04:00
Tom Boucher
4bf3b02bec fix: add phase add-batch command to prevent duplicate phase numbers on parallel invocations (#2165) (#2170)
Parallel `phase add` invocations each read disk state before any write
completes, causing all processes to calculate the same next phase number
and produce duplicate directories and ROADMAP entries.

The new `add-batch` subcommand accepts a JSON array of phase descriptions
and performs all directory creation and ROADMAP appends within a single
`withPlanningLock()` call, incrementing `maxPhase` within the lock for
each entry. This guarantees sequential numbering regardless of call
concurrency patterns.

Closes #2165

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 15:52:33 -04:00
Tom Boucher
f0a20e4dd7 feat: open artifact audit gate for milestone close and phase verify (#2157, #2158) (#2160)
* feat(2158): add audit.cjs open artifact scanner with security-hardened path handling

- Scans 8 .planning/ artifact categories for unresolved state
- Debug sessions, quick tasks, threads, todos, seeds, UAT gaps, verification gaps, CONTEXT open questions
- requireSafePath with allowAbsolute:true on all file reads
- sanitizeForDisplay on all output strings
- Graceful per-category error handling, never throws
- formatAuditReport returns human-readable report with emoji indicators

* feat(2158): add audit-open CLI command to gsd-tools.cjs + Deferred Items to state template

- Add audit-open [--json] case to switch router
- Add audit-open entry to header comment block
- Add Deferred Items section to state.md template for milestone carry-forward

* feat(2157): add phase artifact scan step to verify-work workflow

- scan_phase_artifacts step runs audit-open --json after UAT completion
- Surfaces UAT gaps, VERIFICATION gaps, and CONTEXT open questions for current phase
- Prompts user to confirm or decline before marking phase verified
- Records acknowledged gaps in VERIFICATION.md Acknowledged Gaps section
- SECURITY note: file paths validated, content truncated and sanitized before display

* feat(2158): add pre-close artifact audit gate to complete-milestone workflow

- pre_close_artifact_audit step runs before verify_readiness
- Displays full audit report when open items exist
- Three-way choice: Resolve, Acknowledge all, or Cancel
- Acknowledge path writes deferred items table to STATE.md
- Records deferred count in MILESTONES.md entry
- Adds three new success criteria checklist items
- SECURITY note on sanitizing all STATE.md writes

* test(2157,2158): add milestone audit gate tests

- 6 tests for audit.cjs: structured result, graceful missing dirs, open debug detection,
  resolved session exclusion, formatAuditReport header, all-clear message
- 3 tests for complete-milestone.md: pre_close_artifact_audit step, Deferred Items,
  security note presence
- 2 tests for verify-work.md: scan_phase_artifacts step, user prompt for gaps
- 1 test for state.md template: Deferred Items section
2026-04-12 10:06:42 -04:00
Tom Boucher
8009b67e3e feat: expose tdd_mode in init JSON and add --tdd flag override (#2124)
* test(2123): add failing tests for TDD init JSON exposure and --tdd flag

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

* feat(2123): expose tdd_mode in init JSON and add --tdd flag override

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 15:39:50 -04:00
Tom Boucher
190804fc73 Merge pull request #2063 from Tibsfox/feat/state-prune-1970
feat(state): add state prune command for unbounded section growth
2026-04-11 13:56:19 -04:00
Tom Boucher
9435c4dd38 feat(init): add skill-manifest command to pre-compute skill discovery (#2101)
Adds `skill-manifest` command that scans a skills directory, extracts
frontmatter and trigger conditions from each SKILL.md, and outputs a
compact JSON manifest. This reduces per-agent skill discovery from 36
Read operations (~6,000 tokens) to a single manifest read (~1,000 tokens).

Closes #1976

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 09:15:18 -04:00
Tibsfox
01f0b4b540 feat(state): add --dry-run mode and resolved blocker pruning (#1970)
Review feedback from @trek-e — address scope gaps:

1. **--dry-run mode** — New flag that computes what would be pruned
   without modifying STATE.md. Returns structured output showing
   per-section counts so users can verify before committing.

2. **Resolved blocker pruning** — In addition to decisions and
   recently-completed entries, now prunes entries in the Blockers
   section that are marked resolved (~~strikethrough~~ or [RESOLVED]
   prefix) AND reference a phase older than the cutoff. Unresolved
   blockers are preserved regardless of age.

3. **Tests** — Added tests/state-prune.test.cjs (4 cases):
   - Prunes decisions older than cutoff, keeps recent
   - --dry-run reports changes without modifying STATE.md
   - Prunes resolved blockers, keeps unresolved regardless of age
   - Returns pruned:false when nothing exceeds cutoff

Scope items still deferred (to be filed as follow-up):
- Performance Metrics "By Phase" table row pruning — needs different
  regex handling than prose lines
- Auto-prune via workflow.auto_prune_state at phase completion — needs
  integration into cmdPhaseComplete

Also: the pre-existing test failure (2918/2919) is
tests/stale-colon-refs.test.cjs:83:3 "No stale /gsd: colon references
(#1748)". Verified failing on main, not introduced by this PR.
2026-04-11 03:43:46 -07:00
Tibsfox
f1b3702be8 feat(state): add state prune command for unbounded section growth (#1970)
Add `gsd-tools state prune --keep-recent N` that moves old decisions
and recently-completed entries to STATE-ARCHIVE.md. Entries from phases
older than (current - N) are archived; the N most recent are kept.

STATE.md sections grow unboundedly in long-lived projects. A 20+ phase
project accumulates hundreds of historical decisions that every agent
loads into context. Pruning removes stale entries from the hot path
while preserving them in a recoverable archive.

Usage: gsd-tools state prune --keep-recent 3
Default: keeps 3 most recent phases

Closes #1970

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 03:39:57 -07:00
Tom Boucher
96eef85c40 feat(import): add /gsd-from-gsd2 reverse migration from GSD-2 to v1 (#2072)
Adds a new command and CLI subcommand that converts a GSD-2 `.gsd/`
project back to GSD v1 `.planning/` format — the reverse of the forward
migration GSD-2 ships.

Closes #2069

Maps GSD-2's Milestone → Slice → Task hierarchy to v1's flat
Milestone sections → Phase → Plan structure. Slices are numbered
sequentially across all milestones; tasks become numbered plans within
their phase. Completion state, research files, and summaries are
preserved.

New files:
- `get-shit-done/bin/lib/gsd2-import.cjs` — parser, transformer, writer
- `commands/gsd/from-gsd2.md` — slash command definition
- `tests/gsd2-import.test.cjs` — 41 tests, 99.21% statement coverage

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 21:30:13 -04:00
Tom Boucher
14fd090e47 docs(config): document missing config keys in planning-config.md (#1947)
* fix(core): resolve @file: references in gsd-tools stdout (#1891)

Workflows used bash-specific `if [[ "$INIT" == @file:* ]]` to detect
when large JSON was written to a temp file. This syntax breaks on
PowerShell and other non-bash shells.

Intercept stdout in gsd-tools.cjs to transparently resolve @file:
references before they reach the caller, matching the existing --pick
path behavior. The bash checks in workflow files become harmless
no-ops and can be removed over time.

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

* docs(config): add missing config fields to planning-config.md (#1880)

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

---------

Co-authored-by: Tibsfox <tibsfox@tibsfox.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:36:47 -04:00
Tibsfox
602b34afb7 feat(config): add --default flag to config-get for graceful absent-key handling (#1893) (#1920)
When --default <value> is passed, config-get returns the default value
(exit 0) instead of erroring (exit 1) when the key is absent or
config.json doesn't exist. When the key IS present, --default is
ignored and the real value returned.

This lets workflows express optional config reads without defensive
`2>/dev/null || true` boilerplate that obscures intent and is fragile
under `set -e`.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:25:11 -04:00
Rezolv
f25ae33dff feat(tools): add global learnings store with CRUD library and CLI support (#1831)
* feat(tools): add global learnings store with CRUD library and CLI support

* fix(tools): address review feedback for global learnings store

- Validate learning IDs against path traversal in learningsRead, learningsDelete, and cmdLearningsDelete
- Fix total invariant in learningsCopyFromProject (total = created + skipped)
- Wrap cmdLearningsPrune in try/catch to handle invalid duration format
- Rename raw -> content in readLearningFile to avoid variable shadowing
- Add CLI integration tests for list, query, prune error, and unknown subcommand
2026-04-05 19:09:14 -04:00
Rezolv
7b369d2df3 feat(intel): add queryable codebase intelligence system (#1728)
* feat(intel): add queryable codebase intelligence system

Add persistent codebase intelligence that reduces context overhead:

- lib/intel.cjs: 654-line CLI module with 13 exports (query, status,
  diff, snapshot, patch-meta, validate, extract-exports, and more).
  Reads config.json directly (not via config-get which hard-exits on
  missing keys). Default is DISABLED (user must set intel.enabled: true).
- gsd-tools.cjs: intel case routing with 7 subcommand dispatches
- /gsd-intel command: 4 modes (query, status, diff, refresh). Config
  gate uses Read tool. Refresh spawns gsd-intel-updater agent via Task().
- gsd-intel-updater agent: writes 5 artifacts to .planning/intel/
  (files.json, apis.json, deps.json, stack.json, arch.md). Uses
  gsd-tools intel CLI calls. Completion markers registered in
  agent-contracts.md.
- agent-contracts.md: updated with gsd-intel-updater registration

* docs(changelog): add intel system entry for #1688

* test(intel): add comprehensive tests for intel.cjs

Cover disabled gating, query (keys, values, case-insensitive, multi-file,
arch.md text), status (fresh, stale, missing), diff (no baseline, added,
changed), snapshot, validate (missing files, invalid JSON, complete store),
patch-meta, extract-exports (CJS, ESM named, ESM block, missing file),
and gsd-tools CLI routing for intel subcommands.

38 test cases across 10 describe blocks.

* fix(intel): address review feedback — merge markers, redundant requires, gate docs, update route

- Remove merge conflict markers from CHANGELOG.md
- Replace redundant require('path')/require('fs') in isIntelEnabled with top-level bindings
- Add JSDoc notes explaining why intelPatchMeta and intelExtractExports skip isIntelEnabled gate
- Add 'intel update' CLI route in gsd-tools.cjs and update help text
- Fix stale /gsd: colon reference in intelUpdate return message
2026-04-05 18:33:15 -04:00
Tom Boucher
2ded61bf45 fix(cli): require --confirm flag before phases clear deletes directories (#1832)
phases clear now checks for phase dirs before deleting. If any exist and
--confirm is absent, the command exits non-zero with a message showing the
count and how to proceed. Empty phases dir (nothing to delete) succeeds
without --confirm unchanged.

Updates new-milestone.md workflow to pass --confirm (intentional programmatic
caller). Updates existing new-milestone-clear-phases tests to match new API.

Closes #1826

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 18:05:32 -04:00
Tom Boucher
e881c91ef1 fix(cli): reject help/version flags instead of silently ignoring them (#1822)
* fix(cli): reject help/version flags instead of silently ignoring them (#1818)

AI agents can hallucinate --help or --version on gsd-tools invocations.
Without a guard, unknown flags were silently ignored and the command
proceeded — including destructive ones like `phases clear`. Add a
pre-dispatch check in main() that errors immediately if any never-valid
flag (-h, --help, -?, --version, -v, --usage) is present in args after
global flags are stripped. Regression test covers phases clear, generate-
slug, state load, and current-timestamp with both --help and -h variants.

Closes #1818

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

* fix(agents): convert gsd-verifier required_reading to inline wiring

The thinking-model-guidance test requires inline @-reference wiring at
decision points rather than a <required_reading> block. Convert
verification-overrides.md reference from the <required_reading> block
to an inline reference inside <verification_process> alongside the
existing thinking-models-verification.md reference.

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

* fix(tests): resolve conflict between thinking-model and verification-overrides tests

thinking-model-guidance.test prohibited <required_reading> entirely, but
verification-overrides.test requires gsd-verifier.md to have a
<required_reading> block for verification-overrides.md between </role>
and <project_context>. The tests were mutually exclusive.

Fix: narrow the thinking-model assertion to check that the thinking-models
reference is not *inside* a <required_reading> block (using regex extraction),
rather than asserting no <required_reading> block exists at all. Restore the
<required_reading> block in gsd-verifier.md. Both suites now pass (2345/2345).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 17:32:18 -04:00
Tom Boucher
f26e1e1141 feat(state): add programmatic gates for STATE.md consistency (#1647)
* feat(state): add programmatic gates for STATE.md consistency

Adds four enforcement gates to prevent STATE.md drift:
- `state validate`: detects drift between STATE.md and filesystem
- `state sync`: reconstructs STATE.md from actual project state
- `state planned-phase`: records state after plan-phase completes
- Performance Metrics update in `phase complete`

Also fixes ghost `state update-position` command reference in
execute-phase.md (command didn't exist in CLI dispatcher).

Closes #1627

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

* fix(state): By Phase table regex ate next section when table body was empty

The lazy [\s\S]*? with a $ lookahead in byPhaseTablePattern would
match past blank lines and capture the next ## section header as table
body when no data rows existed. Replaced with a precise row-matching
pattern ((?:[ \t]*\|[^\n]*\n)*) that only captures pipe-delimited
lines. Added regression assertion to verify row placement.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 08:01:39 -04:00
Tom Boucher
d92cd7922a fix: clear phases directory when creating new milestone (#1588) (#1594)
* fix: clear phases directory when creating new milestone (#1588)

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

* ci: re-run CI with Windows pointer lifecycle fix in main

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 11:56:37 -04:00
LakshmanTurlapati
5ce8183928 fix: isolate active workstream state per session 2026-04-02 20:01:08 -05:00