Commit Graph

289 Commits

Author SHA1 Message Date
Tom Boucher
dc4b90ae4b docs(#15): ADR for cross-AI convergence flags in existing orchestration (#242)
* docs(#15): propose convergence flag naming and doc-only test-gate exception

* docs(#15): define convergence config and allowed AI selection
2026-05-24 23:12:24 -04:00
Tom Boucher
22e9c1de62 refactor(#181): migrate workstream inventory builder to sdk/src/workstream (#250) 2026-05-24 21:32:53 -04:00
Tom Boucher
59bcdf03b6 refactor(#180): migrate STATE.md Document Module to sdk/src/state (#249)
* refactor(#180): migrate state document module to sdk/src/state

* test(#180): ratchet lint allowlists for state module relocation
2026-05-24 21:06:50 -04:00
Tom Boucher
9aae41f22d refactor(#179): migrate Configuration Module to sdk/src/config (#244)
* refactor(#179): migrate configuration module to sdk/src/config

* chore(#179): add changeset for config module path migration
2026-05-24 20:49:35 -04:00
Tom Boucher
5434c8c4cb docs: update inventory surfaces for semver compare module 2026-05-24 18:25:21 -04:00
Tom Boucher
1bc7d61294 chore: introduce next integration branch (Phase 1 — additive) (#231)
Adds:
  - docs/branching.md              — beginner contributor guide
  - docs/adr/XXXX-...md            — ADR (will be renamed with issue#)
  - .github/workflows/auto-backmerge.yml      — disabled in Phase 1
  - .github/workflows/pr-target-validator.yml — warn-only in Phase 1
  - scripts/setup-branch-protection.sh        — idempotent gh api script

Modifies:
  - .github/workflows/branch-naming.yml  — recognize 'next'
  - CONTRIBUTING.md                       — 'Where Do I Open My PR?' section

Phase 1 is additive: nothing operational changes until Phase 2 flips
auto-backmerge.yml's if:false→true, flips pr-target-validator.yml's
WARN_ONLY→false, creates the next branch, and switches the default
branch. See the ADR for the migration plan.
2026-05-24 17:11:31 -04:00
Tom Boucher
5f3eb42864 feat(observability): propagate parentTraceId on DispatchEvent — ADR-0174 SDK retirement Phase 1.4 (#178) (#225)
* test(#178): update DispatchEvent factory tests to propagate parentTraceId

P1.3 test 'parentTraceId is always undefined' replaced with four P1.4
contracts: absent → undefined, string → propagated, null → undefined,
non-string → undefined (defensive normalization policy).

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

* feat(#178): propagate parentTraceId through DispatchEvent factory

Stop ignoring the parentTraceId parameter added as a forward-compat hook
in P1.3. Defensive normalization: only non-null strings are propagated;
null, non-string values, and absent callers all yield undefined, keeping
P1.3 behavior intact for all existing dispatch call sites.

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

* test(#178): add Hub-level parentTraceId propagation tests

Four new assertions: req.parentTraceId propagates to event, absent →
undefined (P1.3 regression), shared parentTraceId across multiple
dispatches, and unique traceId invariant despite shared parentTraceId.

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

* feat(#178): plumb parentTraceId through Hub dispatch and _notifyLogger

dispatch() now reads req.parentTraceId and passes it to _notifyLogger,
which forwards it to makeDispatchEvent. Backward-compatible: callers
that omit parentTraceId emit events with parentTraceId: undefined,
identical to P1.3 behavior.

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

* test(#178): add trace correlation end-to-end test

Dispatches a root command then 3 children with parentTraceId=rootTraceId.
Reads the real .gsd-trace.jsonl audit file and verifies: 4 events total,
root has no parentTraceId, all children carry rootTraceId, all traceIds
unique, JS filter returns exactly the 3 children given the root's traceId.

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

* docs(#178): document traceId/parentTraceId in audit file

Update Observability section to note that audit events now carry both
traceId and parentTraceId, and explain the correlation filter pattern.
Note that leaf dispatches emit parentTraceId: undefined until the Phase 2
composer wires it automatically.

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

* chore(#178): add changeset for trace correlation seam

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

* test(#178): cover invalid parentTraceId values in DispatchEvent factory

Adds 9 new test cases for UUID v4 validation of parentTraceId:
empty string, whitespace, non-UUID, oversized, UUID v1, missing-hyphen,
extra-char (all dropped to undefined), plus UPPERCASE and lowercase v4
(both propagated). Tests are intentionally red until the implementation
commit that follows.

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

* feat(#178): validate parentTraceId against UUID v4 before propagation

Adds UUID_V4_REGEX constant and isValidParentTraceId() helper to
event.cjs. makeDispatchEvent now silently coerces any parentTraceId that
fails the UUID v4 check (wrong version nibble, wrong variant, missing
hyphens, oversized, empty, etc.) to undefined. No stderr warn is emitted
— the factory remains pure and side-effect-free. Closes the correlation-
poisoning vector identified in the Codex adversarial review of PR #225.

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

* test(#178): assert Hub silently drops invalid parentTraceId at the seam

Adds two tests to hub-logger-integration.test.cjs:
1. dispatch with 'junk' parentTraceId emits event with parentTraceId===undefined.
2. The logger-failure warn path is NOT triggered — the factory coerces the bad
   value before onEvent is called, confirmed by zero stderr output even when a
   logger that would throw on non-undefined parentTraceId is installed.

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

* test(#178): assert invalid parentTraceId does not poison correlation siblings

Adds one test to trace-correlation.test.cjs: dispatches a root, a valid
child (parentTraceId = rootTraceId), and an invalid child (parentTraceId =
'junk'). Asserts: valid child carries correct parentTraceId, invalid child
has parentTraceId dropped to undefined, filtering by rootTraceId yields
exactly 1 event (the valid child only), and all 3 events have unique
traceIds. Uses an isolated Hub + tmpdir to avoid shared fixture interference.

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

* docs(#178): document UUID v4 contract for parentTraceId

Appends one sentence to the Observability audit-trail paragraph in
CONFIGURATION.md: parentTraceId must be canonical UUID v4 (RFC 4122);
values that don't match are silently dropped from audit output. No section
restructuring — single sentence addition only.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:32:28 -04:00
Tom Boucher
7c539cb86a docs(227): ADR on input-validation checking semantic shape, not just type (#228)
* docs(227): create ADR for input-validation-shape-not-just-type

Captures the architectural standard that defensive normalization at trust
boundaries must validate both type and semantic shape, with silent
coercion on failure. Concrete cases: parentTraceId UUID v4 fix in
PR #225 and release-version validation in ADR 218.

Closes #227

* docs(227): cross-reference new ADR from ADR 218

Appends a "See also" section at the end of ADR 218 pointing forward to
ADR 227, which generalises the type+semantic-shape validation principle
documented in ADR 218's narrower release-workflow context.

* docs(227): add CONTRIBUTING pointer to new ADR

Adds a "Code Review Lessons → Input validation" section after the
Reviewer Standards block, linking to ADR 227 as the citable reference
for the type+semantic-shape validation standard.
2026-05-24 16:32:01 -04:00
Tom Boucher
2d14eb8873 feat(observability): add DispatchLogger seam — ADR-0174 SDK retirement Phase 1.3 (#177) (#223)
* test(#177): add DispatchEvent factory failing tests

Red tests for makeDispatchEvent shape, traceId UUID v4, uniqueness,
parentTraceId-always-undefined (P1.3), args redaction toggle, ISO 8601
timestamp, and all result variant passthrough.

* feat(#177): introduce DispatchEvent factory

makeDispatchEvent produces an immutable event record per dispatch:
- traceId: crypto.randomUUID() (UUID v4)
- parentTraceId: always undefined (P1.4 wires composer)
- command, result, timestamp (ISO 8601)
- args only included when includeArgs === true (default: omitted)

* test(#177): add arg redaction policy failing tests

Red tests for shouldIncludeArgs (GSD_AUDIT_ARGS env gating) and
redactEvent (strips args from frozen events, preserves all other
fields, returns a new object, never mutates the source).

* feat(#177): introduce arg redaction policy

shouldIncludeArgs(): only GSD_AUDIT_ARGS==='1' opts in; all other
values (unset, '', '0', 'true') default to omitting args.

redactEvent(event): returns a shallow copy of the event, dropping the
args field unless opted in. Never mutates the (frozen) source event.

* test(#177): add DispatchLogger interface failing tests

Red tests covering:
- no-op logger: silent on all events, never throws
- default logger: silent on ok, one flattened JSON line to stderr on error
- default logger: audit file creation + append-only + redaction + config gate
- GSD_AUDIT env var and config.audit.enabled config gate
- GSD_AUDIT_ARGS opt-in for args inclusion
All tests use real fs under os.tmpdir() — no mocked appendFileSync.

* feat(#177): introduce DispatchLogger with default and no-op implementations

createNoOpLogger(): silent on all events — Hub default when no logger injected.
createDefaultLogger({ cwd, config }):
  - Silent on ok result
  - Flattened JSON line to stderr on error: { kind, traceId, ...typedPayload }
  - Append-only audit at .planning/.gsd-trace.jsonl when GSD_AUDIT=1 or config.audit.enabled
  - Args redacted by default; GSD_AUDIT_ARGS=1 opts in
  - Logger errors caught internally; never break dispatch callers

* test(#177): add Hub+logger integration failing tests

Red tests verifying:
- onEvent called exactly once per dispatch (ok, error, handler-throw, unknown)
- DispatchEvent shape: traceId uniqueness, command, result.kind, parentTraceId
- Logger errors contained (dispatch still returns Result, warn line to stderr)
- Hub defaults to no-op when no logger injected
- End-to-end with createDefaultLogger: silent on success, stderr on error, audit file

* feat(#177): wire DispatchLogger into CommandRoutingHub

Add optional logger param to createHub({ ..., logger }).
Defaults to createNoOpLogger() — silent, no behaviour change for callers
that don't inject a logger.

After every dispatch (success and error):
- Normalises HubResult { ok } to DispatchEvent { kind: 'ok'|error-kind }
- Calls makeDispatchEvent({ command, args, result }) to mint the event
- Calls logger.onEvent(event) exactly once
- Wraps in try/catch: logger errors emit { level:'warn', source:'DispatchLogger' }
  to stderr but never propagate to dispatch callers

* chore(#177): gitignore .planning/.gsd-trace.jsonl audit file

The audit trail is local-only, append-only, and must never be committed.
Slotted under the existing "Local scratch + Claude-test artifacts" block.

* docs(#177): document GSD_AUDIT, GSD_AUDIT_ARGS, config.audit.enabled

New ## Observability section at end of CONFIGURATION.md covering:
- Default silent/stderr behaviour overview
- Stderr error JSON format
- Audit file opt-in (env var and config key)
- Args redaction policy and GSD_AUDIT_ARGS opt-in

Also slots GSD_AUDIT and GSD_AUDIT_ARGS into the existing
## Environment Variables table (alphabetical order).

* chore(#177): add changeset for observability seam

type: Added — new DispatchLogger seam with default silent/stderr/audit behaviour.
2026-05-24 15:22:36 -04:00
Tom Boucher
5b3646d6c8 fix(#213): support antigravity 2.x config directory split (#217)
* fix(#213): support antigravity 2.x config directory split

* fix(#213): harden antigravity tests for windows parity
2026-05-24 14:17:03 -04:00
Tom Boucher
6fc46db49a fix(release): reject leading-zero versions and pre-check npm before publish (#219)
* fix(release): reject leading-zero versions and pre-check npm before publish

The validate-version job used ^[0-9]+\.[0-9]+\.0$ which accepted leading
zeros (e.g. 1.01.0). npm version silently normalises such inputs to their
canonical semver form (1.1.0), creating divergent state across npm, git
tags, GitHub releases, and release branches — leaving orphaned artefacts
that require manual surgery to clean up.

Two changes to the validate-version job only:

1. Replace the format regex with ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.0$
   so any segment with a leading zero is rejected in under 5 seconds with
   an error message that includes the offending value.

2. Add a "Reject already-published versions" step that calls
   `npm view $pkg@$VERSION` for both packages before any branch, install,
   or build work begins. Duplicate-version requests now fail fast instead
   of burning ~10 minutes before dying at the dry-run publish step.

Adds ADR-0175 documenting the incident, the decisions, and the recovery
runbook for the orphaned v1.01.0 / v1.03.0 artefacts.

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

* docs(adr): rename ADR to issue-number format per CONTRIBUTING.md policy

Renames docs/adr/0175-release-version-validation.md to
docs/adr/218-release-version-validation.md to match the issue-number
prefix convention required by CONTRIBUTING.md (section: Proposing an
ADR or PRD). Issue #218 was opened to track this CI hardening work.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 11:21:43 -04:00
Tom Boucher
4a03ada279 docs(#209): refresh README continuity and audit messaging (#210) 2026-05-24 01:53:18 -04:00
Tom Boucher
6e31630f2a docs(adr): ADR-0174 — retire @opengsd/gsd-sdk package boundary (#198)
* docs(adr): retire @opengsd/gsd-sdk package boundary

Authors ADR-XXXX (Single-runtime collapse onto src/ TypeScript)
and marks ADR-0005, 0007, 0012, 3524 as Superseded.

The dual-runtime SDK design accumulated ~120 files of scaffolding
(worker pool, generators, parity tests, transition shims, two
release pipelines) for a feature set CJS provides in-process.
This ADR records the decision to collapse onto a single TS source
tree in src/ within get-shit-done-cc.

Implementation tracked separately in the umbrella tracking issue
and per-phase sub-issues (referenced in the PR body).

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

* docs(adr): assign ADR-0174 number and finalize supersedes references

Replaces XXXX placeholder with real ADR number (0174 = umbrella
tracking issue #174 per project convention, zero-padded to 4 digits).
Updates Status lines in ADR-0005, ADR-0007, ADR-0012, ADR-3524 to
reference ADR-0174. Adds README.md index entry for ADR-0174 and
marks the four superseded ADRs accordingly.

Refs #174.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 23:38:35 -04:00
Tom Boucher
6177e3a5f5 fix(4): retire cooperating-sibling for phase.*, introduce generator + I/O adapter, fix cmdPhaseComplete (#154)
* test(4): reproduce non-idempotent phase complete + unclamped percent in CJS CLI

RED regression tests for issue #4:
- T1: double invocation of cmdPhaseComplete double-increments **Completed Phases:**
  in STATE.md body (blind parseInt+1 instead of deriving from ROADMAP)
- T2: progress percent can exceed 100% when Completed Phases > Total Phases

The CJS path (bin/lib/phase.cjs:cmdPhaseComplete) has the bug; the SDK path
(phase-lifecycle.ts:phaseComplete, fixed in ~PR#3520) already derives
completed_phases from ROADMAP Complete-row count, making it idempotent.

References:
- Issue #4 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)

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

* chore(4): add sdk/scripts/gen-phase-lifecycle-policy.mjs generator + freshness check placeholder

Generates phase-lifecycle-policy.generated.cjs from sdk/src/query/phase-lifecycle-policy.ts.
All functions in phase-lifecycle-policy.ts are pure transforms (no I/O), directly
serializable via Function.prototype.toString(). The GSDError dependency is replaced
with a lightweight stub that throws plain Error objects — CJS callers that need
process.exit(1) behavior catch these and delegate to error().

This is the "I/O adapter pattern" from ADR-3524 Section 4 applied to pure helpers.

References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)

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

* chore(4): add sdk/scripts/gen-phase.mjs generator

Generates phase.generated.cjs from sdk/src/query/phase.ts.
Only pure helpers (isCanonicalPlanFile, describeNonCanonicalPlans) are generated;
async query handlers (findPhase, phasePlanIndex) are I/O-bound and remain per-side
per ADR-3524 Section 4.

References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)

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

* chore(4): add sdk/scripts/gen-phase-lifecycle.mjs + core idempotency fix logic

Generates phase-lifecycle.generated.cjs providing two pure functions that are the
root-cause fix for issue #4:

1. deriveProgressFromRoadmap(roadmapContent): counts Complete rows in ROADMAP
   progress table — makes completed_phases idempotent (derived from ground truth
   instead of blind +1). Direct transcription of the SDK's "Root cause 1 fix"
   block in phase-lifecycle.ts (~line 1644).

2. clampPercent(completed, total): percent capped at 100 — prevents >100% progress
   when Completed Phases exceeds Total Phases.

Design note: the full phase lifecycle mutations (add, insert, remove, complete) are
inherently async and I/O-bound. Per ADR-3524 Section 4 ("I/O stays per-side"), those
are NOT generated. Only the pure-computation kernel is extracted, following the
I/O adapter pattern: pure logic shared; each side (CJS sync, SDK async) supplies
its own I/O adapter.

The pure functions are defined in the generator as real JS functions and serialized
via Function.prototype.toString() — same technique as gen-project-root.mjs — rather
than embedded in template literals (which would require double-escaping all regex
backslashes).

References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)

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

* chore(4): emit phase.generated.cjs, phase-lifecycle.generated.cjs, phase-lifecycle-policy.generated.cjs

Three generated CJS artifacts from their respective generator scripts:

- phase.generated.cjs (1.7K): isCanonicalPlanFile + describeNonCanonicalPlans
  from sdk/src/query/phase.ts
- phase-lifecycle.generated.cjs (3.6K): deriveProgressFromRoadmap + clampPercent
  — the idempotency+clamp fix for issue #4
- phase-lifecycle-policy.generated.cjs (7.0K): 14 pure phase naming/directory
  helpers from sdk/src/query/phase-lifecycle-policy.ts

Run to regenerate:
  node sdk/scripts/gen-phase.mjs
  node sdk/scripts/gen-phase-lifecycle.mjs
  node sdk/scripts/gen-phase-lifecycle-policy.mjs

References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- Issue #4 (open-gsd/get-shit-done-redux)

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

* fix(4): migrate bin/lib/phase.cjs cmdPhaseComplete to use generated helpers

Replace the blind-increment + unclamped percent bug in cmdPhaseComplete with
the idempotent ROADMAP-derived approach: read freshly-updated ROADMAP, call
deriveProgressFromRoadmap() from phase-lifecycle.generated.cjs, fall back to
existing value when ROADMAP is unavailable. clampPercent() prevents >100%.

Root cause fix for issue #4: the original parseInt(completedRaw) + 1 on every
call made phase complete non-idempotent; the missing Math.min(100, ...) clamp
allowed Progress to exceed 100%.

I/O adapter pattern (ADR-3524 §4): pure computation in generated module;
CJS supplies sync readFileSync; SDK supplies async readFile. Same logic, two adapters.

Closes: Tests in 4-phase-complete-cjs-regression.test.cjs go GREEN.

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

* chore(4): add freshness checks + npm scripts for phase generated artifacts (D4/D6)

Add check-phase-fresh.mjs, check-phase-lifecycle-fresh.mjs, and
check-phase-lifecycle-policy-fresh.mjs (same pattern as check-project-root-fresh.mjs:
import buildXCjs() from the generator, regenerate in-memory, byte-compare to committed
file, exit 1 if stale).

Add gen:phase, check:phase-fresh, gen:phase-lifecycle, check:phase-lifecycle-fresh,
gen:phase-lifecycle-policy, check:phase-lifecycle-policy-fresh to sdk/package.json.
Note: gen:phase-lifecycle / check:phase-lifecycle-fresh do not require 'npm run build'
because the generator defines pure functions directly rather than importing dist.

Update shared-module-handsync-allowlist.json: reclassify phase.cjs justification to
reflect that it now consumes phase-lifecycle.generated.cjs for cmdPhaseComplete. The
*.generated.cjs files are excluded by the lint scanner (excludes *.generated.cjs) so
no new allowlist entries are required for the generated artifacts.

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

* ci(4): add freshness-check CI steps for phase, phase-lifecycle, phase-lifecycle-policy

Add three drift-check steps to .github/workflows/test.yml following the same
pattern as the existing freshness checks (ubuntu-latest + node 24 only):
  - SDK generated phase artifact drift check
  - SDK generated phase-lifecycle artifact drift check
  - SDK generated phase-lifecycle-policy artifact drift check

These guard against editors modifying the generated *.cjs files directly.
They run check-phase-fresh.mjs, check-phase-lifecycle-fresh.mjs, and
check-phase-lifecycle-policy-fresh.mjs respectively (added in D4).

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

* docs(4): amend ADR-3524 — phase * I/O adapter pattern for issue #4 (D8)

Append a 2026-05-23 amendment to docs/adr/3524-cjs-sdk-hard-seam.md documenting
the Phase * cooperating-sibling retirement: three new generator scripts extract
pure-computation helpers from phase.ts / phase-lifecycle.ts / phase-lifecycle-policy.ts,
cmdPhaseComplete migrates to deriveProgressFromRoadmap + clampPercent for idempotency,
freshness checks + CI steps added.

Clarifies what is NOT generated (async I/O-bound mutation handlers stay per-side per
Section 4) and notes open drift bugs #6 and #26 for traceability.

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

* chore(4): add changeset fragment for cmdPhaseComplete fix

Refs #4

* fix(154): use canonical /gsd:plan-phase form in phase-lifecycle-policy.ts

Replaces the retired /gsd-plan-phase slash command reference with the
canonical colon-namespaced /gsd:plan-phase in the TS source template
string that feeds the generated CJS roadmap entry helper.

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

* chore(154): regenerate phase-lifecycle-policy.generated.cjs after slash-namespace fix

Regenerated via node sdk/scripts/gen-phase-lifecycle-policy.mjs after
fixing /gsd-plan-phase → /gsd:plan-phase in the TS source. Generated
file now contains the canonical colon form.

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

* fix(154): cross-platform frontmatter regex anchor in 4-phase-complete-cjs-regression.test.cjs

Replaces /^---\n/ with /^---\r?\n/ so the frontmatter extraction helper
in the regression test tolerates Windows CRLF line endings (autocrlf=true
checkout leaves \r before \n, causing /^---\n/ to never match).

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

* docs(154): add new generated CJS modules to INVENTORY.md and regenerate manifest

Adds three missing rows to the CLI Modules table:
  - phase-lifecycle-policy.generated.cjs
  - phase-lifecycle.generated.cjs
  - phase.generated.cjs

Bumps the headline count from 74 to 77 to match the filesystem.
Also regenerates docs/INVENTORY-MANIFEST.json via
node scripts/gen-inventory-manifest.cjs --write.

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

* fix(154): regenerate phase-lifecycle-policy.generated.cjs with hyphen form

Root cause: commit 6cd701f4 regenerated the CJS artifact but at that point
sdk/dist/query/phase-lifecycle-policy.js already had the correct /gsd-plan-phase
(hyphen) form while sdk/src/query/phase-lifecycle-policy.ts still had /gsd:plan-phase
(colon). The generator uses Function.prototype.toString() on the compiled dist, so
the CJS picked up the wrong string from the stale TS source that was compiled into
dist at some earlier point.

Fix: correct the TS source to /gsd-plan-phase and re-run gen-phase-lifecycle-policy.mjs
so that buildPhaseRoadmapEntry in the CJS emits the hyphen form, satisfying the
bug-3584-runtime-slash-emitters.test.cjs assertion at line 179.

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

* fix(154): restore /gsd:plan-phase canonical form in phase-lifecycle-policy

Commit 0c3a9c75 incorrectly reverted the slash-namespace fix by misreading
sdk/dist/ (a build artifact in hyphen form for non-Claude runtimes) as the
authoritative source. The canonical form for Claude-facing source is
/gsd:plan-phase (colon-namespaced).

Fix: revert TS source back to /gsd:plan-phase, rebuild dist, regenerate
phase-lifecycle-policy.generated.cjs.

Fixes bug-2543-gsd-slash-namespace test failure.

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

* fix(154): update INVENTORY.md CLI Modules count to 79 after rebase onto main

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

* fix(154): use hyphen form /gsd-plan-phase in persisted phase section template

- sdk/src/query/phase-lifecycle-policy.ts: use /gsd-plan-phase (routable
  hyphen form) in the phase scaffold template that gets persisted to
  ROADMAP.md; bug-3584 requires persisted artifacts use the hyphen form
- docs/INVENTORY.md: add missing runtime-name-policy.cjs row in CLI
  Modules table
- tests/4-phase-complete-cjs-regression.test.cjs: add maxRetries/retryDelay
  to rmSync calls to satisfy Windows parity ratchet (baseline was 95)
- Regenerate phase-lifecycle-policy.generated.cjs

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:47:11 -04:00
Tom Boucher
63396dbb16 enh(#142): centralize runtime alias canonicalization seam (#143)
* fix(#142): canonicalize runtime aliases across cjs and sdk

* fix(#142): satisfy hand-sync and inventory parity gates

* test(#1974): remove record-session lock contention in context monitor spec

* test(config): retry transient config-ensure-section failures

* docs(context): capture PR #143 CI reliability findings

* fix(#142): bump CLI Modules inventory headline to 76 (runtime-name-policy + runtime-slash)

docs/INVENTORY.md had the two new .cjs rows listed but the headline
count stayed at 75; fs count is 76. inventory-counts.test.cjs caught
the drift.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 18:39:02 -04:00
Tom Boucher
33ffc647e2 feat(117): reproducible npm environment bootstrap + check-env validator (#136)
* test(117): add failing tests for env validator (check-env.sh)

RED phase. Six tests for scripts/check-env.sh — none pass because the
script does not exist yet. Fixtures:
  good/             — engines.node >=22, .nvmrc 26, synced lockfile
  bad-node-version/ — engines.node <14.0.0 (current Node v26 fails)
  missing-lockfile/ — no package-lock.json
  bad-nvmrc/        — .nvmrc says 22, current Node is v26

Tests cover:
  1. Happy path exits 0
  2. engines.node constraint failure exits 1
  3. Missing lockfile exits 1
  4. .nvmrc major mismatch exits 1
  5. --json flag emits {pass: boolean, checks: array}
  6. Integration smoke: exits 0 on live worktree root

Sources:
  npm engines: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
  npm ci docs: https://docs.npmjs.com/cli/v10/commands/npm-ci

Closes #117

* feat(117): add scripts/check-env.sh with Node/npm/lockfile/version-manager checks

GREEN phase. Implements the five-check environment validator:

  1. Node version vs engines.node (semver constraint — >=, >, <=, <, =)
  2. npm version vs engines.npm (skipped if field absent)
  3. package-lock.json presence
  4. Lockfile sync via `npm ci --dry-run` (exits non-zero when drift detected)
  5. Version-manager pin (.nvmrc / .node-version / .tool-versions) vs active Node major

Exit codes: 0 = all green; 1 = at least one failure; 2 = tool error.
Flags: --json (structured report), --help.

All 7 tests pass. shellcheck clean. bash -n syntax check clean.

Sources:
  npm engines:         https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
  Reproducible builds: https://reproducible-builds.org/docs/source-tree/
  npm ci docs:         https://docs.npmjs.com/cli/v10/commands/npm-ci

Closes #117

* chore(117): pin Node engines + .nvmrc; add check:env npm script

- Add engines.npm: ">=10.0.0" (npm 10 ships with Node 22, the CI floor).
  Source: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
- Add .nvmrc pinning Node 22 (lowest supported version per CI matrix in
  .github/workflows/test.yml; node-version: [22, 24]).
- Add "check:env": "./scripts/check-env.sh" script to package.json.

No generator is involved (not a .generated. file). The test update in this
commit adjusts the integration smoke: it now asserts on --json structured
output rather than raw text, and accepts exit 0 or 1 (version-manager pin
mismatch is expected when developer runs Node 26 against a .nvmrc of 22).

Closes #117

* ci(117): wire environment check into test workflow

Add "Environment check" step to .github/workflows/test.yml in the `test`
job. Positioned AFTER actions/setup-node and BEFORE npm ci so that env
mismatches (wrong Node version, missing npm version, absent lockfile) are
caught before the install step obscures the root cause.

Runs `npm run check:env` (./scripts/check-env.sh) on every matrix lane
(ubuntu, macos, windows) × (Node 22, 24).

Source: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines

Closes #117

* docs(117): publish docs/contributing/bootstrap.md + link from CONTRIBUTING.md

Adds docs/contributing/bootstrap.md with:
  1. Prerequisites (nvm, fnm, asdf, mise; gh CLI)
  2. One-time setup (clone, nvm use, check:env, npm ci)
  3. Daily commands table
  4. Validation guide (check table, exit codes, --json usage)
  5. Troubleshooting (node-version, npm-version, lockfile-present,
     lockfile-sync, version-manager-pin, missing modules, locale errors)
  6. Alternative: Docker via gsd-test-runner
     (https://github.com/open-gsd/gsd-test-runner)

Adds "Bootstrap your environment" section to CONTRIBUTING.md pointing to
the new doc. No content duplication — CONTRIBUTING.md links only.

Adds .changeset/117-npm-bootstrap.md (type: Added) for changelog.

Sources:
  npm engines:         https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
  Reproducible builds: https://reproducible-builds.org/docs/source-tree/
  npm ci docs:         https://docs.npmjs.com/cli/v10/commands/npm-ci
  gsd-test-runner:     https://github.com/open-gsd/gsd-test-runner

Closes #117

* fix(#117): make check:env script run on Windows runners

Invoke check-env.sh via `bash` instead of a bare POSIX path so
Windows CI runners (which have Git Bash on PATH) execute the script
without requiring a POSIX shell shebang dispatcher.

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

* test(117): make check-env.sh fixture .nvmrc adapt to active Node major (cross-platform fix)

Before() hook writes good/.nvmrc = activeNodeMajor and bad-nvmrc/.nvmrc = activeNodeMajor+99
at test-run time. Hardcoded .nvmrc=26 failed on every CI matrix row except Node 26.
After() restores originals so the checked-in files stay stable.

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

* docs(117): exempt gsd-test-runner URL path from slash-command registry check

docs/contributing/bootstrap.md links to https://github.com/open-gsd/gsd-test-runner.
The parity-test regex captures /gsd-test-runner from the URL path component and
flags it as an unregistered slash command. Add 'test-runner' to INTERNAL_COMPONENT_SLUGS
(mirrors the existing 'build' entry for GitHub org URLs) with an explanatory comment.

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

* fix(117): fix Node-24 and Windows-22 CI failures in check-env

Two root causes:

1. version-manager-pin on Node 24 (mac/ubuntu/win):
   The project root .nvmrc pins major 22 for local dev. When the CI
   matrix runs Node 24, check-env.sh fails the version-manager-pin
   check and exits 1, blocking the entire test job before any test
   runs. Fix: skip the pin check when CI=true (GitHub Actions always
   sets this). The pin is a local dev guard, not a gate for multi-
   version matrix CI.

2. engines.node appears missing on Windows-22 (pkg_field backslash):
   pkg_field() embedded PACKAGE_JSON directly into a node -e string
   literal using require(). On Windows, the path uses backslashes
   (D:\a\...) which are silently interpreted as JS escape sequences
   inside the string, causing require() to fail silently (2>/dev/null
   || true). engines.node returns empty, triggering a spurious FAIL.
   Fix: switch to fs.readFileSync + JSON.parse and normalise
   backslashes to forward-slashes before embedding in the JS literal.

Also pass { CI: '' } from the bad-nvmrc unit test so the
version-manager-pin fixture test still exercises the mismatch path
even when running inside CI runners.

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

* fix(117): use relative ./package.json path in pkg_field to fix Windows CI

On Windows, Git Bash exposes \$PWD as a POSIX path (/d/a/…) which
node.exe cannot resolve via fs.readFileSync. The previous fix embedded
the absolute PACKAGE_JSON path in the node -e string after converting
backslashes to forward-slashes, but the POSIX form produced by Git Bash
(/d/a/…) has no backslashes — so the conversion was a no-op and node
received an unresolvable path. The silent catch(e) { process.exit(0) }
swallowed the ENOENT, returning empty string for every engines.* field.

Fix: use './package.json' (relative to CWD). pkg_field() is always
called before any cd in the script so CWD === PROJECT_ROOT at call time.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 18:38:49 -04:00
Tom Boucher
41210f014e fix(5): decision-coverage gate parses <action> XML tag bodies for D-NN citations (#155)
* test(5): add failing test for decision IDs inside <objective>/<tasks>/<task>/<action> XML bodies

Regression test for issue #5 — the translation gate (check.decision-coverage-plan)
is blind to D-NN citations placed inside XML tag bodies by gsd-planner.

Maintainer acceptance criteria (verbatim, issue #5):
  "Gate parses <action> tag bodies for decision ID citations; regression test
   with XML-tag plan body covers all decision IDs."

Five new test cases added to the 'XML tag body citation parsing (issue #5)' suite:
  1. RED: five decisions cited only in <objective>/<action> bodies → gate fails (before fix)
  2. Non-canonical tag <comment> → must NOT count (negative control, passes)
  3. Plain prose under undesignated heading → must NOT count (negative control, passes)
  4. Self-closing <action/> → no crash, D-NN not covered (passes)
  5. D-NN in <objective> body → should count (also fails before fix, GREEN after)

Tests 1 and 5 are the load-bearing RED cases. All others are negative controls.

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

* fix(5): extend extractPlanSections to scan <objective>/<tasks>/<task>/<action> for D-NN citations

Closes #5.

Root cause: extractPlanSections() in check-decision-coverage.ts collected only
front-matter (must_haves/truths/objective) and body lines under designated
markdown headings. The gsd-planner spec (agents/gsd-planner.md line 66) says
'Task actions reference the decision ID they implement (e.g., "per D-03")' and
emits citations inside <action> tag bodies — a location the gate could not see.

Fix: add extractXmlTagBodies() helper that matches the four canonical planner
XML tags (<objective>, <tasks>, <task>, <action>) via a deliberately narrow
regex (no XML parser library — D2 design decision). The helper output is
appended to the designated string inside extractPlanSections(), making any
D-NN citation inside those tag bodies count toward coverage.

Maintainer acceptance criteria (verbatim, issue #5):
  "Gate parses <action> tag bodies for decision ID citations; regression test
   with XML-tag plan body covers all decision IDs."

Self-closing tags (<action/>) are safely ignored — the capturing group does
not match. Non-canonical tags (<comment>, <note>, etc.) are not in the
alternation and are ignored by design.

The CJS surface for check-decision-coverage.ts does NOT have a generator
(gen-decisions.mjs covers decisions.ts, not this gate). No CJS artifact
exists for this module. Per the generator framework established in PR #154
(ADR-3524), a CJS migration is a follow-up; this PR focuses on the TS fix.

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

* docs(5): clarify in gsd-planner.md that decision-coverage gate reads XML tag bodies

Adds a parenthetical note to the existing self-check bullet (line 66) explaining
which locations the gate scans so the planner's own guidance and the gate's
behavior are explicitly aligned.

Refs #5. No behavior change — documentation truthing only.

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

* chore(5): add changeset fragment for decision-coverage XML body fix

Refs #5.

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

* docs(5): extract Interface Context for Executors into reference file to pass planner-decomposition gate

gsd-planner.md was 49446 chars after the XML-tag clarification added in
this PR, exceeding the 48K threshold enforced by
tests/planner-decomposition.test.cjs. Extracted the "Interface Context
for Executors" section (~2137 chars) into
get-shit-done/references/planner-interface-context.md, leaving a one-line
pointer in gsd-planner.md. New normalized size: 47310 chars (1842 chars
under threshold).

Refs #5

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

* fix(5): register planner-interface-context.md in INVENTORY.md and manifest

- Bump References headline from 61 to 62 to match filesystem count
- Add planner-interface-context.md row in Modular Planner Decomposition table
- Update footnote from 61 to 62 top-level references
- Regenerate docs/INVENTORY-MANIFEST.json via gen-inventory-manifest.cjs --write

Fixes inventory-counts and inventory-manifest-sync CI failures caused by the
extraction commit (32e8950f) adding a new reference file without updating the
inventory artefacts.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:22:26 -04:00
Tom Boucher
7f02f9090b fix(26): retire validate.ts/verify.cjs cooperating-sibling for W005/W006-archived/I001 via generator (stacks on #156) (#158)
* test(26): reproduce W005/W006-archived/I001 false positives in CJS validate path

Issue #26 (open-gsd/get-shit-done-redux): three validation drift items from
PR #3479 were hand-ported to verify.cjs via PR #3806 but never routed through
the generator pattern. This means they can drift again whenever validate.ts
changes.

RED tests assert that validate.generated.cjs exports four new items:
  - phaseDirNameRe (W005 regex — /^\d{2,}(?:\.\d+)*-[\w-]+$/)
  - MILESTONE_ARCHIVE_DIR_RE (W006-archived — /^v\d+.*-phases$/i)
  - PHASE_TOKEN_FROM_DIR_RE (W006-archived — phase dir token extractor)
  - canonicalPlanStem (I001 — plan/summary stem canonicalization)

Three of four new export tests are RED (exports missing from generated artifact).
Behavioral tests (W005 no-false-positive, W006-archived no-false-positive,
I001 no-false-positive) are GREEN because #3806's hand-ported fixes are present.

References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port).

* chore(26): extend gen-validate.mjs to export W005/W006-archived/I001 helpers

Issue #26 (open-gsd/get-shit-done-redux): extend gen-validate.mjs (introduced
in PR #156 / issue #6) to also extract the three drift items that PR #3806
hand-ported to verify.cjs but were never routed through the generator.

New helpers added to gen-validate.mjs:

  phaseDirNameRe (PHASE_DIR_NAME_RE) — W005 phase directory naming regex.
    /^\d{2,}(?:\.\d+)*-[\w-]+$/ accepts multi-digit prefixes (999.1-foo valid).
    Requires adding PHASE_DIR_NAME_RE as a named constant to validate.ts so
    it appears as an extractable identifier in the compiled output.

  PHASE_TOKEN_FROM_DIR_RE — W006-archived regex; extracts phase token from
    directory names like "64-auth-service" → "64". Used by
    forEachArchivedPhaseToken() and collectDiskPhases() in verify.cjs.

  MILESTONE_ARCHIVE_DIR_RE — W006-archived regex; matches milestone archive
    directory names like "v1.0-phases". Used by listMilestoneArchiveDirs().

  canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization.
    '68-01-scaffolding' → '68-01'. Top-level named function in compiled output.

Extraction approach: PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE are
module-level const assignments, extracted via extractConstRegExp() (handles both
`const` and `export const` prefixes). PHASE_DIR_NAME_RE is the new named export
added to validate.ts in this commit. canonicalPlanStem is a top-level function,
extracted via extractTopLevelFunction() (brace-balanced).

validate.ts change: inline regex in Check 6 extracted to named constant
PHASE_DIR_NAME_RE (exported) and Check 6 updated to reference it.

References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).

* chore(26): regenerate validate.generated.cjs with W005/W006-archived/I001 helpers

Re-run of sdk/scripts/gen-validate.mjs after extending it in the preceding
commit. The artifact now exports seven items (was three):

  New (issue #26):
    phaseDirNameRe       — /^\d{2,}(?:\.\d+)*-[\w-]+$/ (W005 check)
    PHASE_TOKEN_FROM_DIR_RE — phase token extractor regex (W006-archived)
    MILESTONE_ARCHIVE_DIR_RE — archive dir name matcher (W006-archived)
    canonicalPlanStem()  — PLAN/SUMMARY stem canonicalization (I001)

  Existing (issue #6):
    phaseVariants()
    buildRoadmapPhaseVariants()
    buildNotStartedPhaseVariants()

Freshness check: node sdk/scripts/check-validate-fresh.mjs → "fresh".

References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).

* fix(26): migrate verify.cjs W005/W006-archived/I001 call sites to generated helpers

Issue #26 (open-gsd/get-shit-done-redux): three hand-maintained items in
verify.cjs now consumed from validate.generated.cjs (ADR-3524 §4 adapter pattern).

Changes:
  - Top-of-file require(): extend to also destructure phaseDirNameRe,
    PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem
    from validate.generated.cjs (issue #26 exports).

  - Remove inline PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE constants
    (lines ~403-404). Now sourced from generated artifact. listMilestoneArchiveDirs
    and forEachArchivedPhaseToken pick them up via the require() at top of file.

  - Check 6 (W005): replace inline regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ with
    phaseDirNameRe from validate.generated.cjs. No behavior change.

  - Remove inline canonicalPlanStem() function (~8 lines). Now sourced from
    validate.generated.cjs. Check 7 (I001) continues to call it as before.

Public API of verify.cjs unchanged. Same migration shape as PR #156's Check 8.

References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port that #26 supersedes).

* docs(26): extend ADR-3524 2026-05-23 amendment with #26 scope

Extends the existing 2026-05-23 amendment (not a new dated section) to document
the W005/W006-archived/I001 generator migration introduced by issue #26.

Key points documented:
  - Four new exports added to validate.generated.cjs (phaseDirNameRe,
    PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem)
  - W006-archived coverage note: both fixes were already in verify.cjs from
    #3806; the gap was generator coverage of the regex constants
  - Extraction methods: extractConstRegExp() and extractTopLevelFunction()
  - Parity tests: tests/26-w005-w006-i001-cjs-drift-regression.test.cjs (7 tests)
  - Cross-reference: issue #26 completes the validate.ts ↔ verify.cjs migration
    scope started by issue #6

References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port).

* chore(26): add changeset fragment for W005/W006-archived/I001 generator migration

Touches get-shit-done/bin/lib/validate.generated.cjs and verify.cjs which
match USER_FACING_PREFIXES. Required by the fix-template checklist + the
changeset-lint CI workflow.

References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
2026-05-23 15:57:33 -04:00
Tom Boucher
5414da2ce5 fix(6): retire validate.ts/verify.cjs cooperating-sibling, fix W007/phaseVariants/W006 drift via generator (#156)
* test(6): reproduce W007 + phaseVariants + W006 drift between CJS verify and SDK validate

Adds tests/6-validate-cjs-drift-regression.test.cjs with 5 RED tests covering the
three drift items from issue #6 between verify.cjs (Check 8) and validate.ts (Check 8):

  1. W007 activeDiskPhases — verify.cjs uses diskPhases (includes archived) for W007;
     archived phase "1" absent from current ROADMAP fires false W007.
     validate.ts: activeDiskPhases (active phasesDir only) correctly excludes archives.

  2. phaseVariants() normalization — ROADMAP says "01A", disk has "1A-foo".
     verify.cjs parseInt("01A")=1 → padded "01" (drops letter suffix) → miss.
     validate.ts phaseVariants("01A") = {"01A","1A","01A"} → "1A" matched.
     Both W006 and W007 fire as false positives in verify.cjs.

  3. W006 letter-suffix padding mismatch — ROADMAP says "3B", disk has "03B-foo".
     verify.cjs parseInt("3B")=3 → padded "03" (drops "B") → diskPhases.has("03B") missed.
     W006 and W007 fire as false positives.

All 5 tests RED on origin/main. Will turn GREEN after generator + verify.cjs migration.

References:
  - Issue #6 (open-gsd/get-shit-done-redux) — maintainer acceptance criteria:
    "Port all three items to verify.cjs; add parity tests confirming identical output
    for all three cases on both paths"
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
  - PR #154 (issue #4) — precedent for the generator pattern

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

* chore(6): add sdk/scripts/gen-validate.mjs generator

Extracts phaseVariants() from sdk/dist/query/validate.js via brace-balanced
source-text parsing (phaseVariants is a closure inside validateHealth, not a
module export, so Function.prototype.toString() is unavailable).

Emits get-shit-done/bin/lib/validate.generated.cjs with three pure helpers:
  - phaseVariants(phase): normalized Set of padded/unpadded/letter-suffix variants
  - buildRoadmapPhaseVariants(content): {roadmapPhases, roadmapPhaseVariants}
  - buildNotStartedPhaseVariants(content): Set of unchecked-phase variants

These three helpers directly address the three drift items in issue #6.
Follows the gen-phase-lifecycle-policy.mjs extraction pattern from PR #154.

References:
  - Issue #6 (open-gsd/get-shit-done-redux)
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
  - PR #154 (issue #4) — generator pattern precedent

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

* chore(6): add sdk/scripts/check-validate-fresh.mjs freshness check

Mirrors check-phase-lifecycle-policy-fresh.mjs from PR #154: imports
buildValidateCjs() directly, regenerates in-memory, and diffs against the
committed validate.generated.cjs. Exits 1 if stale (CI gate).

References:
  - Issue #6 (open-gsd/get-shit-done-redux)
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
  - PR #154 (issue #4) — precedent

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

* chore(6): emit validate.generated.cjs from validate.ts

Generated by: node sdk/scripts/gen-validate.mjs

Exports three pure helpers extracted from sdk/src/query/validate.ts Check 8:
  - phaseVariants(phase): Set of normalized variants {"01A","1A"} etc.
  - buildRoadmapPhaseVariants(content): {roadmapPhases, roadmapPhaseVariants}
  - buildNotStartedPhaseVariants(content): Set of unchecked-phase variants

Freshness check: node sdk/scripts/check-validate-fresh.mjs → FRESH

References:
  - Issue #6 (open-gsd/get-shit-done-redux)
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
  - PR #154 (issue #4)

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

* fix(6): migrate verify.cjs to consume validate.generated.cjs helpers (GREEN)

Check 8 in verify.cjs now uses three generated helpers from validate.generated.cjs:

  1. buildRoadmapPhaseVariants(roadmapContent) — replaces hand-rolled roadmapPhases
     Set. Produces both roadmapPhases (raw, for W006 message) and roadmapPhaseVariants
     (all variants, for W007 membership check). Fixes false W007 for letter-suffix
     phases with padding mismatch.

  2. activeDiskPhases — now uses collectDiskPhases() WITHOUT forEachArchivedPhaseToken.
     W007 iterates activeDiskPhases, not diskPhases, so archived phases absent from
     current ROADMAP no longer trigger false W007.

  3. buildNotStartedPhaseVariants(roadmapContent) — replaces raw+parseInt-padded
     notStartedPhases population. Uses phaseVariants() expansion so zero-padded
     letter-suffix unchecked entries (e.g. "03B") correctly suppress W006 for
     their un-padded counterpart ("3B") and vice versa.

  4. phaseVariants() in W006 loop — replaces parseInt-padded disk-existence check.
     "3B" now matches disk dir "03B-foo" via variant expansion.

Also updates test fixture for drift item 1 to use two milestone archives (v1.0 + v1.1),
accurately reproducing the scenario where forEachArchivedPhaseToken walks ALL archives
while getActiveMilestoneArchiveDir returns only the most recent one.

All 5 tests GREEN. Confirmed RED on pre-fix code (git stash test).

References:
  - Issue #6 (open-gsd/get-shit-done-redux) — maintainer acceptance criteria:
    "Port all three items to verify.cjs; add parity tests confirming identical output"
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
  - PR #154 (issue #4) — generator pattern precedent

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

* ci(6): wire validate freshness check into test workflow

Adds 'SDK generated validate artifact drift check' step to .github/workflows/test.yml,
mirroring the pattern used by all PR #154 generator freshness checks.
Runs on ubuntu-latest/node-24 only (same as other artifact drift checks).

Placement: after workstream-name-policy check, before Shared Module hand-sync drift check.

References:
  - Issue #6 (open-gsd/get-shit-done-redux)
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
  - PR #154 (issue #4) — precedent

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

* chore(6): wire gen:validate into sdk/package.json, root package.json, and allowlist

sdk/package.json: adds gen:validate and check:validate-fresh npm scripts.
package.json: adds check:validate-fresh script (mirrors other check:*-fresh entries).
scripts/shared-module-handsync-allowlist.json: updates verify.cjs justification to
  note that Check 8 W006/W007 helpers are now generated from validate.ts via
  gen-validate.mjs (issue #6), with freshness check at check-validate-fresh.mjs.

References:
  - Issue #6 (open-gsd/get-shit-done-redux)
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)

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

* docs(6): amend ADR-3524 — validate.ts now uses generator pattern

Adds 2026-05-23 amendment section to docs/adr/3524-cjs-sdk-hard-seam.md documenting:
  - Generator/artifact/freshness-check/CI paths
  - Three drift items resolved (W007 activeDiskPhases, phaseVariants normalization,
    W006 unchecked-phase variant skip)
  - phaseVariants extraction technique (brace-balanced source-text parsing)
  - Parity test coverage (5 tests, RED→GREEN)
  - Allowlist classification preserved (cooperating-sibling)

References:
  - Issue #6 (open-gsd/get-shit-done-redux)
  - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
  - PR #154 (issue #4)

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

* chore(6): add changeset fragment for validate.ts/verify.cjs generator migration

Touches get-shit-done/bin/lib/validate.generated.cjs and verify.cjs which
match USER_FACING_PREFIXES. Required by the fix-template checklist + the
changeset-lint CI workflow.

Refs #6 #156

* docs(6): register validate.generated.cjs in INVENTORY + manifest

INVENTORY parity test demanded a row for the new generated CJS surface
and a matching entry in INVENTORY-MANIFEST.json. Headline count bumped
from 74 → 75.

Refs #6

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:51:34 -04:00
Tom Boucher
c439890e26 docs(2): clarify installer is required for cross-runtime compatibility (#144)
* docs(readme): add cross-runtime compatibility note for installer requirement

Source files in agents/ and commands/ are Claude Code-format frontmatter.
The installer (bin/install.js:5208 convertClaudeToOpencodeFrontmatter) is
the mandatory conversion layer for non-Claude-Code runtimes. Manually
copying source files to ~/.config/opencode/agents (or equivalent) bypasses
conversion and produces schema validation errors.

README lines 37, 165, 273 all claim OpenCode as a first-class runtime
without flagging the installer as mandatory. This adds an explicit note
immediately after the Getting Started section (README.md line ~175).

Refs #2

* docs(user-guide): add manual install / no-Node.js setup section for non-Claude runtimes

Users without Node.js (Windows + OpenCode being the most common case per
issue #2) cannot run the installer and may attempt to copy agents/ source
files directly. This section documents what manual conversion is required
for OpenCode (remove tools:, convert color: to hex), references the
installer function at bin/install.js:5208, links to the OpenCode schema
docs, and covers the Docker/WSL alternative.

USER-GUIDE.md insertion after line 1258 (after the Codex/non-Claude
runtime section, before Installing for Cline).

Refs #2

* ci: retrigger checks after transient git-auth runner failure

The original run for this PR had a single CI job fail with:
"fatal: could not read Username for 'https://github.com': terminal prompts disabled"
That is a hosted-runner infrastructure flake — no code defect. The run
cannot be retried via gh CLI (too old). This empty commit kicks a fresh
full CI cycle.
2026-05-23 15:50:20 -04:00
Tom Boucher
418db1ef36 docs(118): org-level security baseline RFC (draft) (#137)
* docs(118): scaffold docs/security/baseline.md with section structure

Creates docs/security/ directory and baseline.md with the full nine-section
RFC skeleton for the open-gsd org-level security baseline.

Sections: Status & scope, Minimum security controls (2.1–2.6), Incident-audit
checklist (NIST SP 800-61 Rev. 2), Reporting format, Ownership model, Rollout
plan, KPIs, Follow-up tracking checklist, References.

Source: https://csrc.nist.gov/publications/detail/sp/800-218/final (SSDF v1.1)

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

* docs(118): link baseline from SECURITY.md

Adds pointer section "Org-level security baseline" to SECURITY.md pointing
to docs/security/baseline.md. Per D1: no content duplication — SECURITY.md
retains its vulnerability-reporting focus; the new section links out only.

Source: https://docs.github.com/en/code-security/security-advisories

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

* docs(118): fill PR #136 reference for reproducible env bootstrap (#117)

PR #136 was opened for #117 after this RFC was drafted; updating the
cross-reference. Replaces two "TBD" / "PR for #117" placeholders at
§ 2.5 and § 7 rollout table, and marks the §8 tracking checkbox as done.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 00:48:32 -04:00
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
2a915c1b82 chore: migrate references from gsd-build to open-gsd/get-shit-done-redux (#120) (#121)
Security-motivated migration of all stale repository and npm-scope references.

Three categories of changes (58 files, 174 substitutions):

1. gsd-build → open-gsd (security-critical):
   - .github/workflows/release-sdk.yml — npm token comment, tarball filename pattern
   - .github/workflows/hotfix.yml — same
   - .changeset/fix-3406-detect-stale-sdk-shadow.md — @gsd-build/sdk → @open-gsd/sdk
   - .changeset/sharp-quails-leap.md — same
   - get-shit-done/workflows/update.md — CHANGELOG raw GitHub URL

2. GSD-redux org slug → open-gsd (canonical rename):
   - package.json + sdk/package.json — repository/homepage/bugs metadata
   - All README.*.md — live badge and link sections
   - CONTRIBUTING.md, CONTEXT.md, QUICK-WINS-CONFIRMED-BUGS.md
   - .coderabbit.yaml, .release-monitor.sh, scripts/sync-rulesets.sh
   - docs/** — all live agent/ADR/user-facing documentation
   - tests/** — repo slug assertions and test fixtures
   - scripts/changeset/cli.cjs + github-release-notes.cjs
   - .github/ISSUE_TEMPLATE/*, .github/pull_request_template.md
   - bin/install.js, get-shit-done/bin/lib/model-catalog.cjs
   - sdk/HANDOVER-*.md, sdk/src/*.test.ts

3. CLAUDE.md (gitignored local file — not in this commit):
   Updated separately outside git: --repo gsd-build/get-shit-done →
   --repo open-gsd/get-shit-done-redux with security warning.

Intentionally unchanged: CHANGELOG.md, docs/RELEASE-*.md,
.changeset/README.md, .changeset/build-hooks-atomic-write.md,
README.md migration table (historical fork record),
tests/changeset-serialize.test.cjs line 78 (serialization fixture).

The gsd-build/get-shit-done repo is compromised (rug-pull documented in
README.md). Do not push to or interact with that repo.

Closes #120
2026-05-22 12:28:16 -04:00
Tom Boucher
74cb493373 fix(3784): expose adaptive in model_profile settings flow (#91)
* fix(3784): expose adaptive in model_profile settings flow

Split the single 4-option model-profile AskUserQuestion into a two-question
flow: Q1 (Adaptive / Standard tier / Inherit) routes top-level intent; Q2
(Quality / Balanced / Budget) appears only when Standard tier is chosen.
Updates the confirm table and success_criteria to include adaptive.

Adds regression test asserting all five valid profiles are reachable
interactively via the settings UI.

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

* changeset: add Fixed entry for #3784 / PR #3795

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

* fix(3784): correct Q2-skip comment and remove duplicate brace in settings.md

Codex review followup:
- Replaced vague "preserve existing config" comment with accurate description:
  Q1 still writes model_profile on Adaptive/Inherit branches; only Q2 is skipped.
- Removed stray duplicate `{` line before the Spawn Plan Researcher question block
  (pseudocode had two consecutive `{` openers, one spurious).

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

* fix(3784): address review — gate Q2 structurally, define cancel rule, harden tests

Addresses gsd-code-reviewer (M1/M2/m1/m2/m3/m4) and codex adversarial
(Q2 gating, save-mapping, Claude-only wording, step-of-2 wording).

- F1: Replace //comment-only Q2 gating with Conditional visibility block
  (mirrors code_review_depth / graphify.auto_update structural pattern)
- F2: Define model_profile cancel rule in update_config step (leave
  existing value unchanged when Q1="Standard tier…" but Q2 cancelled)
- F3: Fix Adaptive description — remove "Claude only" tail; describe
  heavy/light role tiers across all supported runtimes
- F4: Remove "step 1 of 2 for standard profiles" from Q1 question text
  (2-step nature now structurally documented by Conditional visibility)
- F5: Fix vacuously-true test disjunct (|| content.includes('Adaptive')
  always true — 6+ occurrences); assertion now requires role-based cost
  optimization + heavy roles wording
- F6: Add 4-option cap enforcement test (ASK_USER_QUESTION_OPTION_CAP=4
  named constant, counts per question object not per AskUserQuestion call)
  and brace-balance regression test (guards against bd53925f recurrence)

* docs(3784): list adaptive in model_profile reference docs

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:39:57 -04:00
Tom Boucher
3d80494e31 ci: skip full test matrix on doc-only PRs (PR-2 of 4)
Adds path filter to test.yml so the 6-lane matrix only runs on code
changes. New test-skip.yml fires on the inverse paths and emits
noop jobs with identical names so the required status checks
(lint-tests + 6x test (...)) are satisfied regardless of which
workflow ran. Doc-only PRs now complete CI in <30s.

Canonical code-paths list mirrors changeset-required.yml; keep
them in sync (documented in docs/branch-protection.md).

Closes #108
Refs #107

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:06:11 -04:00
Tom Boucher
cc208b350a Merge pull request #106 from gsd-redux/chore/branch-protection-specs
chore: add branch protection ruleset specs (PR-1 of 3)
2026-05-22 09:52:09 -04:00
Tom Boucher
9f9c1a7909 chore: add branch protection ruleset specs (PR-1 of 3)
Checks in JSON specs for three rulesets (main-protection,
release-branches, tag-immutability), a CODEOWNERS file (advisory),
and scripts/sync-rulesets.sh to apply them.

Enforcement is `disabled` in all three files — PR-2 will apply with
`evaluate` for a 1-week dry-run, PR-3 will flip to `active`.

See docs/branch-protection.md for the full rollout plan.
2026-05-22 09:19: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
b533f71857 chore: introduce CommandRoutingHub and migrate phase-command-router (PoC) (#3828)
* feat(routing): add CommandRoutingHub with behavioral test suite (#3788)

Introduces createHub({ mode, sdkLoader, cjsRegistry, manifest }) and
hub.dispatch({ family, subcommand, args, cwd, raw }) -> Result with a
closed 6-value ERROR_KINDS frozen enum. Hub never throws, never prints,
and enforces no transparent fallback between sdk/cjs modes. 34 behavioral
tests cover all errorKind values, mode fixation, and the no-throw contract.

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

* refactor(routing): migrate phase-command-router to CommandRoutingHub (#3788)

Rewrites phase-command-router.cjs to dispatch through CommandRoutingHub.
Public entry point routePhaseCommand({ phase, args, cwd, raw, error }) is
unchanged. The adapter determines mode (sdk/cjs) from env + tryLoadSdk(),
constructs a hub, dispatches, and translates the pure Result back to
output()/error() calls. New behavioral test suite (23 tests) replaces the
old mock-heavy approach and includes two integration tests through the real hub.

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

* docs(routing): ADR + glossary + changeset for CommandRoutingHub (#3788)

Adds ADR-3788 documenting the hub's design contract (pure result, fixed mode,
closed 6-value errorKind enum, no transparent fallback). Adds Command Routing
Hub glossary entry to CONTEXT.md and a one-paragraph reference to
ARCHITECTURE.md. Changeset fragment records the Changed entry.

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

* fix(docs): rename ADR to sequential convention 0012 (#3788)

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

* docs(inventory): register CommandRoutingHub in INVENTORY (#3788)

Add command-routing-hub.cjs row to docs/INVENTORY.md CLI Modules table,
bump headline count from 72 to 73, and regenerate INVENTORY-MANIFEST.json
via scripts/gen-inventory-manifest.cjs --write.

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

* docs(adr): add 0012 to ADR index (#3788)

Add entry for 0012-command-routing-hub.md to the index table in
docs/adr/README.md so the enh-3271-sdk-adr-structure lint passes.

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

* chore(lint): bump phase test-file ceiling to accommodate command-router suite (#3788)

phase-command-router.test.cjs added by the CommandRoutingHub migration
pushes the phase prefix cluster from 4 to 5 test files. Bump the allowlist
ceiling from 4 to 5 (issue 3788) so lint-test-file-count passes.

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

* fix(routing): preserve phase.mvp-mode JSON error and ROADMAP scan through hub (#3788)

mvp-mode was never registered in the SDK; the pre-#3788 CJS router
always dispatched it via the CJS handler even when sdkAvailable was
true. After the hub migration, SDK-mode hubs (Docker, where the SDK
build exists) sent mvp-mode to the SDK bridge, which returned
SdkDispatchFailed with reason 'unknown' instead of the expected
'usage' code, and failed ROADMAP lookups. Fix by short-circuiting
mvp-mode to the CJS handler before hub construction, matching the
pre-migration observable behaviour.

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

* docs(adr): note SDK-incomplete subcommand limitation in ADR-0012 (#3788)

* fix(inventory): bump CLI Modules headline to 74 after rebase onto main (#3788)

Upstream added code-review-flags.cjs (72→73) at the same time our branch
added command-routing-hub.cjs. After rebase both modules exist (74 total)
but the headline stayed at 73; bump to 74.

* fix(routing): remove dead mvp-mode handler from cjsRegistry (#3788)

The cjsRegistry['phase']['mvp-mode'] handler (previously lines 65–68)
was unreachable: the early-return bypass at line 56 intercepts mvp-mode
before hub construction in CJS mode, and in SDK mode cjsRegistry is
passed as undefined. Remove the dead handler; all 57 tests still pass.

* docs(adr): correct router count in ADR-0012 (#3788)

The context section cited "eight" routers including "frontmatter" but
there is no frontmatter-command-router.cjs. The actual count is seven:
phase, phases, roadmap, state, verify, validate, init.

* fix(routing): guard missing subcommand + use ERROR_KINDS constant (#3788)

Two fixes in phase-command-router.cjs:

1. Add early-return for missing subcommand before hub construction.
   Pre-#3788 the routeCjsCommandFamily fell through to error() for
   undefined args[1]; post-#3788 the hub's manifest check skips falsy
   subcommands, which would have sent bare 'phase' into SDK dispatch
   in SDK mode instead of the expected "Available: ..." error message.

2. Switch on ERROR_KINDS.UnknownCommand instead of bare 'UnknownCommand'
   string, per ADR-0012's closed-enum contract ("callers switch on
   ERROR_KINDS values, not bare string literals").

* docs(routing): fix factual errors in ARCHITECTURE, ADR-0012, changeset (#3788)

Three corrections:

1. ARCHITECTURE.md: softened "All CJS command family routers dispatch
   through CommandRoutingHub" — only phase-command-router.cjs is
   migrated in this PR; remaining routers still use routeCjsCommandFamily
   and migrate in follow-up issues.

2. ADR-0012: corrected the SDK mvp-mode claim. The ADR said "the SDK
   has no equivalent entry" but sdk/src/query/command-static-catalog-
   domain.ts:104-105 registers phase.mvp-mode. The actual reason for
   the early-return bypass is divergent ROADMAP scan behaviour and
   error reason codes, not SDK absence.

3. .changeset/mellow-tigers-gather.md: corrected pr: 1 → pr: 3828.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 23:32:10 -04:00
Tom Boucher
86e067924f fix(3727): wire --fix flag dispatch in code-review workflow (#3743)
* test(3727): add failing test for --fix flag dispatch in code-review workflow

Adds bug-3727-code-review-fix-flag-dispatch.test.cjs with:
- Pure-function tests on parseCodeReviewFlags() / resolveCodeReviewWorkflow()
  from new code-review-flags.cjs typed IR module
- Structural docs-parity tests asserting dispatch_fix step exists in
  code-review.md and that initialize step references code-review-flags.cjs

Structural tests FAIL today (RED): workflow has no dispatch_fix step and
no reference to code-review-flags.cjs in initialize. IR-level tests pass
because the lib module is introduced in this commit as the typed seam.

Fixes #3727

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

* fix(3727): wire --fix/--all/--auto flags through code-review workflow

The initialize step now parses --fix, --all, and --auto from argv (via the
code-review-flags parser seam added in the previous test commit) and the
workflow dispatches gsd-code-fixer when --fix is truthy and the review
returned findings. Fixes the regression introduced when PR #2947 closed
#2946 without actually shipping the workflow dispatch.

Fixes #3727

* chore(3727): update changeset to reference PR #3743

The fix commit landed with the linked-issue number as the pr: field
placeholder. Updating to the actual PR number now that the PR is open.

* fix(3727): register code-review-flags.cjs in INVENTORY.md and manifest

Add missing row for get-shit-done/bin/lib/code-review-flags.cjs to the CLI
Modules table in docs/INVENTORY.md (count 72→73) and regenerate
docs/INVENTORY-MANIFEST.json to fix three failing CI tests:
  - cli-modules-doc-parity: every CLI module must have a row in INVENTORY.md
  - inventory-counts: headline "CLI Modules (N shipped)" must match file count
  - inventory-manifest-sync: INVENTORY-MANIFEST.json must match filesystem

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:09:42 -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
40a442b21f Merge pull request #3649 from gsd-build/feat/3597-split-suites-node-matrix
feat(3597): split test suites and add Node 22/24/26 OS matrix
2026-05-17 01:51:25 -04:00
Tom Boucher
b8fa89b5b6 fix(3663): address CodeRabbit surface/layout follow-ups 2026-05-17 01:39:10 -04:00
Tom Boucher
aa73c2d914 docs(3663): regenerate INVENTORY-MANIFEST.json to include runtime-artifact-layout.cjs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 01:12:12 -04:00
Tom Boucher
37805f6d2c docs(3663): add runtime-artifact-layout.cjs row to INVENTORY.md and bump CLI Modules count to 71
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 01:12:00 -04:00
Tom Boucher
fb2f251010 docs(3663): accept ADR-3660; add Phase 1 implementation status
Flip status from Proposed → Accepted and record the branch,
implementation reference, and Phase 2 dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 01:12:00 -04:00
Tom Boucher
e7f7465778 Merge remote-tracking branch 'origin/main' into fix/pr3649-review 2026-05-17 00:45:09 -04:00
Tom Boucher
3eb1fec1c9 docs(3660): ADR + CONTEXT.md entry for Runtime Artifact Layout Module (#3661)
* docs(3660): add Runtime Artifact Layout Module ADR + CONTEXT.md entry

Records the architectural decision to add a Runtime Artifact Layout Module
that owns per-runtime artifact placement (commands / agents / skills) as a
typed seam. Closes the bug class behind #3659 where the Runtime Surface
Module forgets artifact kinds the install/uninstall pipelines track.

CONTEXT.md gains the new module entry directly after the Skill Surface
Budget Module since the two seams compose.

Pure docs — no code changes. Phase 1 implementation lands in a separate PR.

Closes #3660

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

* docs(3660): fix ADR parity and clarify phase boundaries

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 00:34:47 -04:00
Tom Boucher
1a1ad2ec87 Merge origin/main into feat/3597-split-suites-node-matrix
Resolves conflict in .github/workflows/test.yml: keep the 6 new drift-check
steps from main (plan-scan, secrets, schema-detect, decisions,
workstream-name-policy, Shared Module hand-sync) before the split-lane test
runs from this PR. PR's dedicated `coverage` job replaces main's per-matrix
`Run tests with coverage` step.

Other conflicting files (CONTRIBUTING.md, get-shit-done/bin/lib/init.cjs,
package.json) auto-merged cleanly. Changeset files (.changeset/*) brought in
from main as adds.
2026-05-16 13:22:07 -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
Cristian Uibar
05316369ae fix(3583): normalize retired colon-form commands in generated Claude/Qwen/Hermes SKILL.md bodies (#3629)
* Add first-class grok runtime support (maps to ~/.agents); wire installer, runtime-homes.cjs and sync-skills; update Grok Build engine in local ~/.agents to latest; record session progress in discussion doc

* Normalize gsd colon references to hyphen in generated Claude SKILL.md bodies using the shared transformer. Fixes #3583.

* Refine #3583 implementation after review: cache command names, improve tests, clean up comments

* Harden gsd colon-to-hyphen transformer with bidirectional word boundaries and body-only regression guard

* Track quick-wins batch status and local session notes for #3583/#3579 handoff

* Port installer robustness (hoist copyLibDir + selective Codex hooks) from 3579 to make Codex tests pass on this branch. Fixes ReferenceError and prevents extra hook pollution in Codex installs.

* Restore #3583 transformer wiring and Codex .sh GSD_VERSION branch lost in 50ff8f17 port

Commit 50ff8f17 ('Port installer robustness from #3579') accidentally reverted:
- the top-level require of transformContentToHyphen/readGsdCommandNames
- the body normalization inside convertClaudeCommandToClaudeSkill
- the Codex hook loop's .sh branch with {{GSD_VERSION}} substitution

These were the actual #3583 fix and the Codex half of the #2136 invariant.
Failing tests fixed: bug-2808-skill-hyphen-name, claude-skills-migration #3583
case, bug-2136 Codex .sh substitution.

* Exempt 'sync-skills' slug from docs-parity check (skill dir name in path references)

gsd-sync-skills is an installed Claude skill name and a workflow file but
not a registered slash command. The docs-parity regex catches /gsd-sync-skills
from filesystem path references like ~/.agents/skills/gsd-sync-skills/ in
docs/discussions/grok-build-support-2026-05.md.

Adding to INTERNAL_COMPONENT_SLUGS matches the existing exemption pattern
for 'statusline', 'workspaces', 'graphify-update', etc.

* Restrict hooks/lib/ install to hook-enabled runtimes and managed allowlist

Codex/Copilot/Cursor/Windsurf/Trae/Cline already skip the hooks block but were still copying hooks/lib/ helpers, contradicting the downstream Codex comment. Gate the call on the same runtime check and pass GSD_HOOK_LIB_FILES so install scope matches the uninstall/manifest scope.
2026-05-16 13:09:58 -04:00
Tom Boucher
7fa5eb7e63 fix(3597): resolve CR threads — drop substring-assertion guidance, use $testDir
Two CodeRabbit threads from PR #3649 review:

- docs/TESTING-SUITES.md:75 — removed the "stable message substring"
  fallback from the error-assertion guidance. Project rule (per the
  no-source-grep lint and lint-no-source-grep.cjs) is structured/typed
  checks only — err.code, JSON fields, enums. Substring matching
  re-introduces the exact prose-coupling we banned.

- scripts/run-tests.cjs:106 — the "no test files found" error now
  reports the resolved testDir variable instead of the hardcoded
  'tests/' string, so when GSD_TEST_DIR points elsewhere the message
  names the actual directory the harness searched.

Local: docker gsd-test-summary 11224/0 on holodeck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:44:51 -04:00
Tom Boucher
e82876fe45 feat(3597): split test suites and add Node 22/24/26 OS matrix
scripts/run-tests.cjs gains `--suite <name>` filtering using a filename
suffix convention (`*.security.test.cjs`, `*.integration.test.cjs`, …).
Files with no marker are `unit` (the default fast lane); files with a
marker land in the matching suite. No `--suite` flag preserves the prior
behavior of running every test (backcompat for `npm test` and
`npm run test:coverage`).

New package scripts wire the suites to stable entrypoints:
test:unit, test:integration, test:install, test:security, test:slow,
test:coverage:unit, test:coverage:all. Unknown suite → exit 2 with the
list of valid suites; empty suite → exit 0 with a stderr notice so empty
lanes (e.g. `security` before adversarial tests land) don't gate CI.

CI matrix grows from `ubuntu × {22,24}` + a single macOS lane to
`{ubuntu, macos, windows} × {22, 24, 26}`. `fail-fast: false` so one
lane failure doesn't cancel siblings. Node 26 is `continue-on-error`
until actions/setup-node stabilises that image. PR CI runs unit +
integration + security on every cell; `install` and `slow` only on
`main` push. A dedicated `coverage` job runs `test:coverage:unit` on
ubuntu/Node 24 and uploads the report.

Grouping policy lives in docs/TESTING-SUITES.md with a pointer from
CONTRIBUTING.md. New harness test covers arg parsing, filter selection,
empty-suite behavior, and failure propagation.

Closes #3597.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 08:34:09 -04:00
Tom Boucher
beb7e05254 docs(3635): add v1.42.3 release notes and link from docs index (#3636)
New docs/RELEASE-v1.42.3.md follows v1.42.1's section layout
(hotfix-shaped: Fixed only, no Added/Changed). Headline is Codex CLI
0.130.0 install routability — after install, $gsd-* skills now
resolve under Codex 0.130.0+ where the previous build left zero
routable entrypoints. Twelve user-visible fix categories covered:

  - Codex CLI 0.130.0 install materializes routable skills (#3562, #3582)
  - Runtime-aware slash formatter (#3584)
  - Argv-based subprocess for check.ship-ready (#3587)
  - phase_status field + /gsd:plan-phase closed-phase gate (#3569)
  - Canonical [features].hooks for Codex configs (#3566)
  - SDK bridge loads via public package export (#3567)
  - W006/W007 skip archived and future phases (#3559, #3560)
  - Ultraplan gates on Claude Code markers (#3561)
  - Padded phase IDs match unpadded ROADMAP prose (#3537)
  - Prompt-user migrations resolve in non-TTY runs (#3541)
  - Executor agents forbidden from git stash (#3542)
  - Configuration manifests load from installed payload (#3571)

docs/README.md updates:
  - Documentation Index gets a row for v1.42.3 above v1.42.1
  - Quick Links "What's new" lists v1.42.3 first, then v1.42.1

Codex CLI 0.130.0 minimum-version note added to the Installing
section so operators upgrading to 1.42.3 know the Codex dependency.

Closes #3635

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:42:41 -04:00
Tom Boucher
724133967a Merge pull request #3606 from gsd-build/fix/3584-runtime-bin-lib-cjs-emit-gsd-cmd-in-user
fix(3584): runtime-aware slash formatter for user-facing emissions
2026-05-15 23:05:37 -04:00
Andreas Brauchli
6a2bf05de7 feat(#3039): tier /gsd-help output (--brief, default, --full, <topic>) (#3040)
Replace the single 747-line /gsd-help reference with a progressive-disclosure
dispatcher (#2551 pattern). Newcomers get a one-page tour; returning users get
a 10-line refresher with --brief; the complete reference stays available behind
--full; /gsd-help <topic> emits one section; and /gsd-help --brief <topic>
is a compact scoped lookup (signature + one-line summary).

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

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

Main-merge conflict resolution:
- workflows/help.md kept as dispatcher (body lives in help/modes/full.md)
- /gsd-<cmd> → /gsd:<cmd> rename from #3452 reapplied to the mode files
  (full.md, default.md, brief.md, topic.md) — the six namespace routers
  (/gsd-context, /gsd-ideate, /gsd-manage, /gsd-project, /gsd-quality,
  /gsd-workflow) and wildcards (/gsd-*) preserved in hyphen form per
  main's convention
- bug-2950 test combines branch's path repointing with main's namespaced
  replacement strings
2026-05-15 22:02:55 -04:00
Tom Boucher
034b47c8d0 fix(3584): runtime-aware slash formatter for user-facing emissions
Introduce `runtime-slash.cjs` as the single source of truth for emitting
GSD slash-command references in user-facing runtime output and persisted
artifacts. `formatGsdSlash(commandName, runtime)` produces `/gsd-<cmd>`
for skills-based runtimes (Claude/Cursor/OpenCode/Kilo/etc.) and
`$gsd-<cmd>` for Codex. The deprecated `/gsd:<cmd>` colon form is never
emitted — pasting a recommended-action command into Claude Code now
routes correctly instead of failing with `Unknown command`.

Wired into the high-impact emitters identified in #3584:

- `init.cjs` `cmdInitManager` recommended_actions[].command (the
  original failure path in the bug report) plus the no-ROADMAP /
  no-STATE error hints.
- `phase.cjs` `cmdPhaseAdd`, `cmdPhaseAddBatch`, `cmdPhaseInsert` —
  ROADMAP.md `Plans:` references now persist the routable form
  instead of the legacy colon form.
- `verify.cjs` `cmdValidateHealth` — every fix-hint addIssue() call
  (E001/E002/E003/E004/E005, W002/W003/W008/W009/W011/W016/W018) and
  the persisted STATE.md regenerate / MILESTONES.md backfill notes.
- `milestone.cjs` `cmdMilestoneComplete` — Operator Next Steps tail
  rewrite.
- `validate-command-router.cjs` — `validate context` recommendation
  strings for WARNING/CRITICAL utilization bands.
- `workstream.cjs` — missing .planning hint.
- `profile-output.cjs` — `generate-claude-md` workflow enforcement
  block, project/skills fallbacks, profile placeholder, and the
  dev-preferences refresh hints.
- `drift.cjs`, `gsd2-import.cjs`, `commands.cjs scaffold context` —
  remaining one-off persisted references.

Runtime detection: `resolveRuntime(projectDir)` reads
`process.env.GSD_RUNTIME` first, then a side-effect-free direct read of
`.planning/config.json` (NOT `loadConfig`, which would normalize legacy
keys and re-write the file just to read the runtime name).

Tests:
- `tests/bug-3584-runtime-slash-formatter.test.cjs` — 22 unit tests
  covering the pure formatter and resolver (hyphen vs codex, prefix
  normalization, defensive returns, env/config/default chain).
- `tests/bug-3584-runtime-slash-emitters.test.cjs` — 6 integration
  tests exercising `init manager`, `phase add` (via the structured
  `roadmap get-phase` payload to avoid raw-text matching on the
  on-disk artifact), `validate health`, `validate context`, and the
  codex variant.
- Existing tests updated to assert the new contract: validate-context
  recommendations, claude-md workflow block, milestone complete
  Operator Next Steps. Copilot-install engine-conversion test now
  asserts against a synthetic input since bin/lib/*.cjs no longer
  contains literal `/gsd:` references for the install-time converter
  to rewrite.

INVENTORY.md and INVENTORY-MANIFEST.json updated for the new module
(64 CLI modules shipped, +1).

Fixes #3584

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:32:02 -04:00
Tom Boucher
eb92d107c8 Merge pull request #3568 from gsd-build/fix/3562-codex-0-130-0-gsd-1-42-2-installs-workfl
fix(3562): generate Codex skill surface so $gsd-* commands resolve on Codex CLI 0.130.0+
2026-05-15 15:03:04 -04:00
Tom Boucher
55b3f45ad0 fix(3566): emit canonical [features].hooks; recognize legacy codex_hooks as alias
Closes #3566

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

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

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

Threaded through:

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:46:12 -04:00