Commit Graph

279 Commits

Author SHA1 Message Date
Jeremy McSpadden
9bee1cab7f fix(#526): include README assets in package 2026-05-31 07:28:12 -05:00
Jeremy McSpadden
c81d5fcb2c docs(#523): rebrand public docs as GSD Core 2026-05-31 07:16:19 -05:00
github-actions[bot]
63ad985b6a chore: finalize v1.2.0 2026-05-31 01:24:28 +00:00
github-actions[bot]
9e91489325 chore: bump to 1.2.0-rc.1 2026-05-31 00:02:19 +00:00
github-actions[bot]
ade4ee440b chore: bump version to 1.2.0 for release 2026-05-30 23:30:28 +00:00
Tom Boucher
79002a00cb chore(#518): rename npm package + bin to @opengsd/gsd-core (#519)
* chore: rename npm package + bin to @opengsd/gsd-core (functional)

- package.json: name @opengsd/get-shit-done-redux → @opengsd/gsd-core,
  bin key get-shit-done-redux → gsd-core, repository/homepage/bugs URLs
- package-lock.json: regenerated (npm install --package-lock-only)
- tests/**, scripts/**, bin/**, .github/**, agents/**, commands/**,
  get-shit-done/bin/**, get-shit-done/workflows/**:
  applied the 4-rule replacement (scoped npm ref, GitHub repo path,
  bin/clone invocations) per #505 single-source refactor

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

* docs: sweep live references to @opengsd/gsd-core

Update all live documentation (README.md + translations, docs/**,
CONTRIBUTING.md, VERSIONING.md, SECURITY.md, CONTEXT.md,
docs/CANARY.md) to reflect the renamed package and repository.

Rules applied:
- @opengsd/get-shit-done-redux → @opengsd/gsd-core (scoped npm name)
- open-gsd/get-shit-done-redux → open-gsd/gsd-core (GitHub repo)
- GSD-redux/get-shit-done-redux → open-gsd/gsd-core (stale badge org)
- bare bin/clone refs → gsd-core

CHANGELOG.md, docs/adr/**, docs/RELEASE-*.md, docs/research/**,
and .changeset/** are preserved byte-identical.

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

* fix: add negative lookbehind to slash-command regex in bug-2954 test

The extractSlashReferences regex matched /gsd-core inside npm package
URLs (@opengsd/gsd-core), producing a false /gsd:core command reference.
Adding a negative lookbehind (?<![a-z]) excludes matches preceded by a
letter, so only standalone /gsd-<cmd> and /gsd:<cmd> tokens are found.

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

* chore(#518): add changeset for package rename

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

* test(#518): update package-identity expectations to the renamed coordinates

The rebase regenerated the seam to @opengsd/gsd-core (bin gsd-core, repo
open-gsd/gsd-core). The #498 seam tests assert deriveIdentity against the REAL
package.json, so their expected literals must follow the rename. The drift-lint
unit test is left as-is — its SEAM is a self-consistent fixture and its
stale-literal detection cases would shift if altered; the live-repo scan in it
already passes.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 17:25:02 -04:00
Tom Boucher
6f2520786d feat(#498): single Package Identity seam for /gsd:update + fix runtime undefined-name bug (#499)
* feat(#498): generated package-identity seam derived from package.json

Introduce a single source for GSD's published-package coordinates:
scripts/generate-package-identity.cjs (pure deriveIdentity + formatManualInstall
+ render) emits the generated get-shit-done/bin/lib/package-identity.cjs with
values baked from package.json at build time. Baking is required because the
installed tree carries only a synthetic {"type":"commonjs"} package.json, so a
runtime require('package.json').name resolves to undefined (#378). Reconciles

Wired into npm run build; a parity test fails CI if the committed file drifts
from package.json.

Refs #498

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

* fix(#498): repoint update worker + check-latest-version at the seam

- check-latest-version.cjs sources PACKAGE_NAME from the package-identity seam
  instead of a re-typed literal (single source; #2992's constant guarantee is
  preserved since the seam bakes from package.json).
- gsd-check-update-worker.js no longer does require('../package.json').name
  (resolved to undefined in the installed tree → background update check
  silently broken, #378). It now delegates the latest-version lookup to
  checkLatestVersion(), collapsing the duplicated npm-view call onto the single
  deterministic adapter and inheriting its typed {ok,version,reason} surface.
- Move the PR #3102 Windows shell-gate contract test onto execNpm (where the
  spawn now lives) and assert the worker no longer spawns npm directly.
- Rewrite the #378 contract: worker must NOT use require(package.json).name and
  must delegate; check-latest-version PACKAGE_NAME is single-sourced from the seam.

Fixes #378-class runtime breakage. Refs #498

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

* chore(#498): changeset for package-identity seam + update-check fix

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

* feat(#498): drift-guard lint — value-check GSD coordinate literals against the seam

scripts/lint-package-identity-drift.cjs scans the runtime/code surface
(bin/, hooks/, scripts/, get-shit-done/) and asserts every GSD package name
and GitHub repo slug literal equals the Package Identity seam's current value.
Passes today; fails the moment a repoint isn't propagated (rename package.json,
regenerate the seam, and stale literals are reported until updated). This is
the second adapter that makes the seam real and a repoint mechanically safe.

Enforced via tests/issue-498-identity-drift-lint.test.cjs (scanRepo === [])
under npm test; also exposed as `npm run check:identity-drift`.

Refs #498

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

* feat(#498): update-context projection — port update.md resolution to a tested seam

Add get-shit-done/bin/lib/update-context.cjs: a pure, injected-fs port of
update.md's ~280-line get_installed_version bash. resolveUpdateContext()
reproduces the full precedence cascade (preferred fast-path -> local probe ->
global probe via env overrides then $HOME -> LOCAL-if-distinct -> scope
cascade -> UNKNOWN) and returns the 4-field contract { installedVersion,
scope, runtime, gsdDir }. The fs is injected so every branch is finally
testable without a live multi-runtime install.

Expose it as `gsd-tools update-context [--config-dir <d>] [--runtime <r>] --json`.
Purely additive — update.md is unchanged in this commit; the workflow swap
follows separately.

Refs #498

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

* feat(#498): swap update.md resolution to the update-context projection

Replace ~280 lines of inline runtime/scope/config-dir bash in update.md's
get_installed_version step with a call to `gsd-tools update-context --json`
(60 lines: derive PREFERRED_* from execution_context, resolve gsd-tools.cjs,
parse the 4-field JSON). Behavior is unchanged — the projection reproduces the
same cascade — but the logic is now tested in update-context.cjs instead of
untestable bash-in-markdown.

Relocate the #3608 antigravity-first-class contract onto the projection
(RUNTIME_DIRS order, inferPreferredRuntime, envRuntimeDirs) plus a behavioral
test; keep the execution_context path-classification assertion on update.md.
Re-point install.test's custom-config-dir assertion (kilo.jsonc/KILO_CONFIG)
to update-context.cjs where that detection now lives.

Full root suite: 2022 pass / 0 fail.

Refs #498

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

* docs(#498): record Update Context Module in CONTEXT.md

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

* fix(#498): CI — avoid bare gsd-tools in update.md; register new CLI modules

- update.md update-context invocation: resolve the PATH gsd-tools shim into a
  variable and call "$GSD_TOOLS" (never a bare `gsd-tools` command) — satisfies
  the #2851 workflow-bare-gsd-tools guard.
- Register package-identity.cjs and update-context.cjs in docs/INVENTORY.md
  (CLI Modules 76 -> 78 + rows) and regenerate docs/INVENTORY-MANIFEST.json,
  fixing inventory-counts and inventory-manifest-sync.

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

* test(#498): make update-context + parity tests OS-agnostic (Windows CI)

Two Windows-only test failures, both test-portability (production code is fine —
the real-fs CLI integration test passed on Windows):

- update-context resolver tests + bug-3608 behavioral test used POSIX path-string
  keys in their fake fs, but the resolver builds lookups via path.join/resolve
  (backslash + drive letter on Windows) → keys never matched → everything
  resolved to UNKNOWN/claude. Normalize fake-fs keys and gsdDir comparisons
  through path.resolve so they match on both platforms.
- package-identity parity test compared render() (LF) to the committed file,
  which Windows git checks out as CRLF (no .gitattributes eol rule). Normalize
  line endings before comparing, matching the repo convention
  (autonomous-decomposition, bug-3707).

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

* fix(#498): update.md backup must use GSD_DIR (adversarial-review finding)

The get_installed_version rewrite emits GSD_DIR but dropped the probe-loop
variables LOCAL_DIR/GLOBAL_DIR. The backup_custom_files step still read those,
so RUNTIME_DIR went empty for every LOCAL/GLOBAL install and detect-custom-files
was skipped — and since the update then runs a clean install that wipes managed
dirs (commands/gsd, get-shit-done), user-added files could be deleted without
the intended backup.

Set RUNTIME_DIR="$GSD_DIR" directly (the resolved config dir; empty for
UNKNOWN scope, which still skips the backup). Add a structural regression
(tests/issue-498-update-backup-runtime-dir.test.cjs).

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

* test(#503): re-point Antigravity .agent detection at the #498 projection

#499 moves the runtime/scope detection cascade out of update.md inline bash
into get-shit-done/bin/lib/update-context.cjs. The #503 regression test asserted
on the inline RUNTIME_DIRS array, which no longer exists, so it would fail
against the projected update.md even though the .agent guarantee is preserved.

Rewrite it to verify the surviving surfaces:
 - behavioral: resolveUpdateContext resolves a LOCAL ./.agent install to the
   antigravity runtime (the original root cause, now covered by adding
   ['antigravity', '.agent'] to the projection RUNTIME_DIRS table)
 - update.md prose classifier still maps /.agent/ -> antigravity
 - the post-update cache-clear for-dir loop still includes .agent

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

* fix(#498): finish de-hardcoding consumers + close adversarial-review parity gaps

Restore the consumer de-hardcoding that is the point of the seam, and close the
parity gaps an adversarial review (codex) found in the update-context projection.

De-hardcode the repo slug + install command in the changeset tooling — #516
only single-sourced the package NAME, leaving 'open-gsd/get-shit-done-redux'
hardcoded in scripts/changeset/cli.cjs and github-release-notes.cjs. Route both
through the seam's repoSlug/packageName so a rename is a regenerate, not a hand
edit. The drift-lint real scan now reports zero divergent coordinate literals.

Projection parity vs the old inline bash, as ONE consistent rule
(trustedVersionAt) applied on every path:
 - expand a leading ~/ in preferredConfigDir before the fast path (the bash ran
   expand_home first; a custom --config-dir ~/foo otherwise fell to UNKNOWN)
 - trust a version only when BOTH VERSION and the update.md marker exist — fast
   path AND LOCAL/GLOBAL cascade; a partial dir falls to 0.0.0 keeping scope
 - apply the same same-path dedup to the 0.0.0 fallback so a partial install
   probed from cwd===home is not misdetected as LOCAL

Adds regression tests for tilde expansion, VERSION-only (cascade + fast path),
and the cwd===home partial-install dedup.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:53:44 -04:00
dependabot[bot]
11c7fc590e chore(deps): bump tmp in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [tmp](https://github.com/raszi/node-tmp).


Removes `tmp`

---
updated-dependencies:
- dependency-name: tmp
  dependency-version:
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 00:17:25 +00:00
Tom Boucher
4b908e6bcd test(tests): antagonistic tier — fast-check property tests + Stryker mutation testing (PR-gated) (#461)
* feat(#454): add antagonistic tier — fast-check property tests + Stryker mutation config

Adds property-based testing (fast-check v4) and mutation testing scaffolding
(Stryker v8) as the antagonistic validation tier for lib/*.cjs pure logic.

## Files added

### Shared setup
- tests/helpers/fast-check-setup.cjs — configureGlobal({ numRuns:200, seed:42 })
  for deterministic CI; override locally with GSD_FC_SEED

### Property test suites (node:test + fast-check)
- tests/context-utilization.property.test.cjs — boundary at 60%/70% thresholds
  (exact Math.ceil boundary, not Math.floor), TypeError on all invalid inputs,
  overflow clamping to 100%/critical, shape invariants (7 tests, all pass)
- tests/prompt-budget.property.test.cjs — estimateTokens monotonicity + ceil(len/4)
  exactness; applyBudget shape invariant, instructions/roadmap verbatim, budget
  envelope, omit tracking (11 tests, all pass)
- tests/frontmatter.property.test.cjs — extractFrontmatter/reconstructFrontmatter/
  spliceFrontmatter never-throw + type shape + splice→extract round-trip (9 tests)
- tests/adr-parser.property.test.cjs — shouldRejectAdrStatus boundary (3 statuses
  only), parseAdrMarkdown shape + title trim invariant (discovered: parser trims
  trailing whitespace) (9 tests, all pass)
- tests/config-schema.property.test.cjs — isValidConfigKey never throws, returns
  boolean, accepts all VALID/RUNTIME_STATE_KEYS, rejects empty/null/unknown (8 tests)

### Stryker mutation config
- stryker.config.mjs — testRunner:'command', mutate bin/lib/**/*.cjs minus 13
  generated files, coverageAnalysis:'off', thresholds {high:80,low:60,break:50},
  incremental:true, reporters html+clear-text+progress

### CI workflow
- .github/workflows/mutation.yml — PR-gating job (pull_request + workflow_dispatch),
  runs stryker --incremental --since origin/next (changed files only), uploads
  HTML artifact; SINCE_REF via env not interpolation (injection-safe)

### Package config
- package.json: +test:mutation, +test:mutation:since scripts
- .gitignore: +.stryker-tmp/, +.stryker-incremental.json, +reports/mutation/
- package-lock.json: fast-check@4.8.0, @stryker-mutator/core@9.6.1

## Verified
node --test on all 5 property test files: 44 tests, 0 failures.
Stryker NOT run (slow; reserved for CI).

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

* fix(#454): pin upload-artifact to v7.0.1 SHA used across repo (bad SHA ea165f8d)

The SHA ea165f8d65b6e75b540449d3ec4f5dde0c5a4e1 (labeled v4.6.2) does not
resolve on GitHub Actions. All other workflows in this repo pin
043fb46d1a93c77aae656e7c1c64a875d1fc6a0a (v7.0.1) — align mutation.yml.

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

* fix(#454): mutation workflow — replace invalid --since flag with changed-core-files --mutate scoping

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

---------

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:07:07 -04:00
Tom Boucher
33afb4f6eb chore(#452): add ESLint 9 flat-config harness with three custom AST rules (#460)
Install eslint@9 + typescript-eslint@8 + globals@16 + eslint-plugin-n@17 +
eslint-plugin-no-only-tests@3 + typescript as devDependencies.

eslint.config.mjs (flat config):
- Global ignores: node_modules, dist, .worktrees, .claude, coverage, the
  12 generated get-shit-done/bin/lib/*.cjs files
- Block for get-shit-done/bin/**/*.cjs + scripts/**/*.cjs: js.recommended +
  eslint-plugin-n + local plugin; generic quality rules (no-var, prefer-const,
  no-unused-vars, no-empty, n/no-process-exit)
- Block for tests/**/*.test.cjs: no-only-tests (error), local timing rules,
  no-restricted-syntax timing bans

eslint-rules/ local plugin (three AST rules, all at warn pending cleanup):
- no-source-grep: flag readFileSync on source .cjs/.js/.ts + text methods
- no-magic-sleep-in-tests: flag Atomics.wait and await-new-Promise(setTimeout)
- no-elapsed-assertion: flag assert*() on timing props (elapsed/duration/took/ms)

tsconfig.lint.json: allowJs + checkJs + noEmit for future type-aware passes.

tests/eslint-rules.test.cjs: 15 RuleTester unit tests (all pass, 0 fail).

package.json: add lint/lint:fix scripts; remove lint:tests (subsumed by ESLint
local/no-source-grep). Rules that produced pre-existing errors downgraded to
warn: no-useless-escape, no-unsafe-finally, no-regex-spaces, no-control-regex,
no-irregular-whitespace. ESLint exits 0 (warnings ok).

.github/workflows/test.yml lint-tests job: add npm ci + ESLint step; remove
"Lint — no source-grep tests" step (now covered by ESLint); bump timeout 3→5
min. .gitignore: add node_modules/.cache/eslint/ entry.

eslint --fix auto-cleaned: no-regex-spaces in tests, prefer-const in state.cjs,
redundant eslint-disable-next-line comments.

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:54:49 -04:00
Tom Boucher
48b1e35187 fix(#431): enforce H1 shell policy (linux=bash, macOS=zsh, windows=pwsh) across PR + release gates (#434)
* test(#431): policy-shell-pinning linter — RED baseline (37 violations on origin/next)

Adds scripts/workflow-policy.cjs: H1 shell-policy linter with POLICY map,
VIOLATION enum, matrix expansion, effective-shell resolution order, and
runPolicyLint({ workflowsDir }) entry point.

Adds tests/policy-shell-pinning.test.cjs: 8 tests (baseline + 6 synthetic
counter-tests). Synthetic tests 2–7 pass; baseline test is intentionally RED
(37 violations: 28 in test.yml, 9 in install-smoke.yml — all macos/windows
lanes using shell: bash instead of native zsh/pwsh).

Adds js-yaml@4.1.1 as devDependency for YAML parsing.

* fix(#431): switch ubuntu/windows lanes to native shells; extract bash-isms to Node

Remove all explicit shell: bash pins from ubuntu-only jobs (changes, lint-tests,
coverage, required-tests, smoke-unpacked) — ubuntu runner default is bash, which
is both H1-compliant and the runner default, making the pin redundant.

For the test and test-full mixed-OS jobs (ubuntu+windows, windows+macos):
- Move bash-ism steps to shell-agnostic Node scripts:
    scripts/ci-guard-runner.cjs       — RUNNER_ENVIRONMENT check
    scripts/ci-rebase-check.cjs       — git fetch+merge PR base branch
    scripts/check-npm-integrity.cjs   — Node port of check-npm-integrity.sh
    scripts/ci-prepare-test-scope.cjs — write .ci-selected-tests.txt
    scripts/ci-smoke-skip.cjs         — set skip= output for full-only matrix entries
- Remove shell: bash from simple npm/node command steps (runner default applies)

This brings Windows violations from 19 to 0. Remaining 17 violations are all
MACOS_MISSING_EXPLICIT_ZSH in mixed-OS matrix jobs (test-full: windows+macos,
install-smoke smoke: ubuntu+macos) — these require job splitting to fix; see
BLOCKER in PR description.

* fix(#431): update workflow-shell-pinning test for H1 policy

The old test required all Windows-targeting npm steps to pin shell: bash
(to prevent pwsh stderr-swallow). Under H1, Windows runners must use
pwsh (native, no pin needed) — shell: bash on Windows is now the
violation, not the fix.

Update findViolations() to flag npm steps with effectiveShell === 'bash'
(rather than effectiveShell === null). Update synthetic tests to verify
the H1-inverted semantics: defaults.run.shell: bash on Windows is now 2
violations, not 0. Update test name and assertion messages to describe
the H1 constraint rather than the old missing-pin constraint.

* fix(#431): extend policy linter to resolve matrix.shell expressions

- expandRunsOn now captures all matrix.include row keys as realization
  context (os, node-version, shell, full_only, etc.) instead of only os
- effectiveShell now accepts a realizationContext and resolves
  ${{ matrix.<key> }} expressions against it before checking policy
- Unresolvable matrix key in shell expression emits UNRESOLVABLE_MATRIX
- Add 3 new tests: positive (zsh+pwsh per row → 0 violations),
  counter (bash in macOS row → WRONG_SHELL_FOR_OS), counter (missing
  shell key → UNRESOLVABLE_MATRIX)

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

* fix(#431): apply matrix.shell pattern to test-full and smoke jobs (clears BLOCKER)

test-full job (test.yml):
- Add shell: pwsh/zsh per matrix.include row (windows-latest→pwsh,
  macos-latest→zsh)
- Add job-level defaults.run.shell: ${{ matrix.shell }}
- No step-level shell pins existed to remove

smoke job (install-smoke.yml):
- Add shell: bash/zsh per matrix.include row (ubuntu→bash, macos→zsh)
- Add job-level defaults.run.shell: ${{ matrix.shell }}
- No step-level shell pins existed to remove

Policy linter now reports 0 violations across all workflow files.

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

* refactor(#431): migrate .sh check scripts to .cjs; remove .sh originals

- Add scripts/check-env.cjs: Node.js port of check-env.sh with
  identical exit codes (0/1/2), human-readable and --json output,
  --help flag, and all 5 checks (node-version, npm-version,
  lockfile-present, lockfile-sync, version-manager-pin)
- Migrate all callers:
  - package.json check:env → node scripts/check-env.cjs
  - package.json check:integrity → node scripts/check-npm-integrity.cjs
  - scripts/ci-test-scope.cjs path strings → .cjs equivalents
  - .github/workflows/release.yml rc+finalize jobs → node .cjs (drop chmod+x)
  - .github/workflows/security-scan.yml → node .cjs (drop chmod+x)
  - tests/check-env.test.cjs → spawn node process.execPath [.cjs]
  - tests/npm-integrity-gate.test.cjs → spawn node process.execPath [.cjs]
- Delete scripts/check-env.sh and scripts/check-npm-integrity.sh

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

* refactor(#431): update doc references from .sh to .cjs

Update SECURITY.md and docs/contributing/bootstrap.md to reference the
canonical Node invocation instead of the removed bash scripts.

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

* fix(#431): use per-step shell:matrix.shell instead of defaults.run.shell (GHA compat)

GHA does not reliably resolve matrix expressions inside defaults.run.shell.
Per-step shell: always resolves correctly. Removed the defaults.run.shell block
from the test-full job (test.yml) and the smoke job (install-smoke.yml), and
added shell: \${{ matrix.shell }} directly on every run: step in both jobs.

Codex finding: defaults.run.shell with matrix expressions is not a
GHA-supported pattern; per-step shell: is the safe form.

* fix(#431): policy linter validates every matrix.include row independently

Removed runner-label-only dedup from expandRunsOn() in workflow-policy.cjs.
The prior guard (if !realizations.find(r => r.runner === runner)) collapsed
two macos-latest rows with different node-version/shell contexts into one,
hiding the second row's policy violation.

Each matrix.include row is a distinct CI realization with its own context;
validating it twice is harmless but skipping it causes false negatives.

Added counter-test (Test 8) in tests/policy-shell-pinning.test.cjs:
two macos-latest rows (shell:zsh compliant + shell:bash violation) must
produce exactly one WRONG_SHELL_FOR_OS violation on the second row.

* fix(#431): remove dedup-by-runner in Cartesian matrix.<key> expansion (Codex round 3)

The base-list path in expandRunsOn (matrix.<key> arrays, e.g. matrix.os)
previously guarded each push with `if (!realizations.find(r => r.runner === runner))`,
collapsing duplicate runner values into a single realization and hiding policy
violations on later rows of a Cartesian matrix.

Remove the guard unconditionally; each entry in the base-list array now produces
its own realization, matching the same fix already applied to the matrix.include path.

Add counter-test "Cartesian matrix os × shell — dedup must not collapse rows by
runner alone": matrix.os: [macos-latest, macos-latest] + shell: ${{ matrix.shell }}
now yields 2 realizations (not 1). Documents that Cartesian cross-product expansion
(carrying all keys into realization context) is a separate follow-up; current violations
are UNRESOLVABLE_MATRIX pending that work.

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

* fix(#431): remove 60s timeout regression on npm ci --dry-run (parity with check-env.sh)

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

* fix(#431): ci-rebase-check.cjs — return truthy sentinel on success (Codex round 4)

run() used execFileSync with stdio:'inherit', which returns null on success.
Caller checked `result !== null`, always false → every successful fetch fell
through to "failed after 3 attempts" exit-1 path.

Fix: run() now returns true on success, false on failure.
Update caller from `result !== null` to `if (result)`.

Adds tests/ci-rebase-check.test.cjs (5 tests) covering the sentinel contract
and a local-bare-remote integration smoke that verifies the full fetch+merge
path exits 0 when fetch succeeds.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: CI Rebase Check <ci@gsd-redux>
2026-05-28 09:23:59 -04:00
Tom Boucher
2b9fa009d3 fix(#373): replace unquoted $GSD_SDK with space-safe gsd_run launcher (#379)
* fix(#373): replace unquoted $GSD_SDK with space-safe gsd_run launcher

Workflow bash blocks resolved the runtime as GSD_SDK="node $GSD_TOOLS" and
invoked it unquoted ($GSD_SDK query ...). On install paths containing spaces
(e.g. /Volumes/Mini Me/...) the unquoted expansion word-split into
`node /Volumes/Mini gsd-tools.cjs ...`, failing with "Cannot find module
'/Volumes/Mini'" and getting masked by `2>/dev/null || echo "{}"` into a
silent empty state.

Replace the string variable with a single-line shell launcher that defines a
gsd_run function, invokes the runtime with a fully-quoted path and "$@", and
preserves the local-cjs / installed-gsd-tools-on-PATH fallback (#3668) plus
the loud not-found error and install hint. The launcher uses _GSD_SHIM_NAME
indirection so no workflow emits the /gsd-tools substring that the do.md
dispatcher-parity scanner would misread, and is single-line to stay within
the per-file progressive-disclosure line budgets (#2551).

The canonical launcher lives in
get-shit-done/workflows/_runtime-launcher.snippet.sh, is propagated once per
file by scripts/sync-runtime-launcher.cjs, and is locked by
tests/runtime-launcher-parity.test.cjs (fails CI on drift, on a reappearing
$GSD_SDK token, or on a /gsd-tools substring). Dependent workflow-assertion
tests are updated from $GSD_SDK to gsd_run, and the runtime launcher is
registered in CONTEXT.md.

Fixes #373

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

* ci(#373): add changeset fragment and allow-test-rule for parity guard

The runtime-launcher parity test is a structural drift guard that reads
workflow markdown to assert the canonical launcher is present and the
retired $GSD_SDK / /gsd-tools tokens are absent; annotate it with
allow-test-rule per the no-source-grep lint escape hatch. Add the required
.changeset fragment for this user-facing fix.

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

* test(#373): make parity PATH-fallback assertion cross-platform (Windows)

Subtest (E) compared GSD_TOOLS against the Node-side absolute temp path,
but the value originates from git-bash which reports the POSIX form, so the
prefix comparison failed on windows-latest while the launcher itself worked
(the installed stub was invoked). Assert the resolved binary by normalized
suffix (/bin/gsd-tools, not .cjs) instead of the absolute prefix; the
behavioral stub-invocation assertion is unchanged.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:21:05 -04:00
Tom Boucher
9dd2c87c6c ci: supersede #367 with combined tiered PR pipeline redesign (#369)
* ci: streamline PR pipeline gates

* test: annotate ci-test-scope stderr assertions for lint rule

---------

Co-authored-by: Colin <colin@solvely.net>
2026-05-26 19:11:58 -04:00
Tom Boucher
1b16c60df9 ci(#365): run affected tests for pull request CI lanes (#366) 2026-05-26 18:54:27 -04:00
Tom Boucher
43eafea3d5 fix(#303): deepen command routing seams and alias parity guard (#304) 2026-05-26 11:23:53 -04:00
Tom Boucher
11918dcc37 chore(#191): retire sdk package seam 2026-05-25 10:58:36 -04:00
Tom Boucher
a80241139b build: run sdk build in root build script 2026-05-25 09:55:43 -04:00
Tom Boucher
30c572e537 refactor(#189): retire generated cjs artifacts from bin/lib 2026-05-25 09:54:50 -04:00
Tom Boucher
4c92aacca5 build(#188): retire sdk generator and freshness scripts 2026-05-25 09:41:37 -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
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
89886d90b4 feat(114): npm dependency integrity gate (npm ls invalid/extraneous) (#135)
* test(114): add failing regression tests for npm dependency integrity gate

Adds tests/npm-integrity-gate.test.cjs and four fixture directories under
tests/fixtures/npm-integrity/ covering:
  - clean: matching lockfile and node_modules (expects exit 0)
  - drift: declared vs installed version mismatch (expects exit 1)
    Reproduces the ws 8.20.1 declared / 8.20.0 installed incident shape
    using stable-dep@8.20.1 (package.json) vs stable-dep@8.20.0 (node_modules).
  - extraneous: unlisted package in node_modules (exits 1; exits 0 with --ignore-extraneous)
  - missing: declared package absent from node_modules (exits 1 regardless of flags)

Each test spawns scripts/check-npm-integrity.sh as a subprocess and asserts
on exit code first, then stderr content. Tests are RED at this commit because
the script does not yet exist.

Sources:
  npm ls docs: https://docs.npmjs.com/cli/v10/commands/npm-ls
  NIST SSDF PW.4.1: https://csrc.nist.gov/publications/detail/sp/800-218/final

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

* feat(114): add check-npm-integrity.sh + workspace-aware drift detection

Adds scripts/check-npm-integrity.sh, a Bash script that:
  1. Runs `npm ls --all --json` at the invocation directory
  2. Parses JSON output for invalid, missing, and extraneous package flags
  3. Exits 1 on any finding; emits a structured report to stderr listing offenders
     with both declared and installed versions for invalid packages
  4. Exits 2 on tool error (npm/node not found, JSON parse failure)
  5. Accepts --ignore-extraneous to suppress extraneous-only failures
  6. Documents behaviour in --help output including remediation path

Workspace behaviour: the root package.json in this repo has no "workspaces"
field. npm ls runs at the invocation root and covers that tree only. The sdk/
sub-package is a separate, non-workspace package and is out of scope for a
single invocation. If workspaces are added in future, npm ls will traverse
them automatically (npm >=7).

The drift scenario (ws 8.20.1 declared vs 8.20.0 installed) is reproduced by
using an exact version pin in package.json combined with a mismatched
node_modules/package.json -- npm ls marks this as "invalid" and exits 1.

npm exits 0 for extraneous packages even though they appear in the JSON
"problems" array; this script detects them via JSON parsing regardless of
the npm exit code.

Sources:
  npm ls docs: https://docs.npmjs.com/cli/v10/commands/npm-ls
  NIST SSDF PW.4.1: https://csrc.nist.gov/publications/detail/sp/800-218/final
  OpenSSF Scorecard Pinned-Dependencies:
    https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies

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

* ci(114): wire dependency integrity gate into CI/release/security workflows

Adds a "Dependency integrity gate" step invoking
scripts/check-npm-integrity.sh to three workflows, always after `npm ci`
and before any test or build step:

  .github/workflows/test.yml
    - matrix job: after "Install dependencies" / before "Build SDK dist"
    - coverage job: after "Install dependencies" / before "Build SDK dist"

  .github/workflows/release.yml
    - rc job "Install and test": after npm ci, before npm run test:coverage
    - finalize job "Install and test": after npm ci, before npm run test:coverage

  .github/workflows/security-scan.yml
    - Added setup-node + npm ci + gate before existing source-scan steps
    - Bumped timeout-minutes from 5 to 10 to accommodate the install step

Also adds "check:integrity": "./scripts/check-npm-integrity.sh" to root
package.json scripts for local contributor invocation.

No new workflow files created. All edits extend existing workflows.

Sources:
  npm ls docs: https://docs.npmjs.com/cli/v10/commands/npm-ls
  NIST SSDF PW.4.1: https://csrc.nist.gov/publications/detail/sp/800-218/final

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

* docs(114): document dependency integrity gate in audit runbook

Appends a "Dependency Integrity Verification" section to SECURITY.md
(no docs/runbooks/ directory exists in this repo). Covers:
  - The three detection classes: invalid, missing, extraneous
  - Local invocation: ./scripts/check-npm-integrity.sh + npm run check:integrity
  - Remediation: rm -rf node_modules && npm ci
  - Bypass policy: no flag; commit-message documentation required if skipped
  - Scope: root package only (sdk/ is a non-workspace package, out of scope)
  - CI coverage listing

Sources cited:
  NIST SSDF PW.4.1: https://csrc.nist.gov/publications/detail/sp/800-218/final
  OpenSSF Scorecard Pinned-Dependencies:
    https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies

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

* fix(#114): npm integrity gate satisfies its own clean/drift/extraneous fixtures

Replace npm-ls-based analysis with pure package-lock.json parsing so the
script runs correctly in CI and test environments where node_modules is not
installed. Key changes:

- Rewrite check-npm-integrity.sh parser to read package-lock.json directly
  instead of spawning `npm ls --all --json`, which required node_modules on
  disk and incorrectly flagged clean/drift fixtures as MISSING.
- Implement a self-contained semver satisfies() covering exact, caret, tilde,
  comparison-operator, and compound ranges — no external semver package needed.
- Update extraneous fixture package-lock.json to include ghost-pkg with
  "extraneous: true" so the lockfile-based detector can identify it.
- Update missing fixture package-lock.json to omit the node_modules/absent-dep
  entry, making the absent-dep MISSING condition derivable from lockfile alone.

All 13 tests (clean ×2, drift ×3, extraneous ×3, missing ×3, help ×2) pass.

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

* fix(#114): treat transitive deps as non-extraneous in integrity gate

The extraneous check was comparing all lockfile packages against root
package.json declarations only. This caused every transitive dependency
(e.g. hono, ajv, @anthropic-ai/claude-agent-sdk-darwin-arm64) to be
flagged as EXTRANEOUS, producing false-positive failures in CI.

Only packages that npm itself marks with "extraneous: true" in the
lockfile represent genuinely unwanted packages. Transitive dependencies
installed by parent packages are valid and should be skipped.

All 13 existing tests continue to pass; the extraneous fixture still
works because it uses "extraneous: true" explicitly (npm's own marker).

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

* 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.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:50:17 -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
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
6313baad63 chore(tests): lint rule — cap test files per production module at 2 (#3738)
* chore(tests): lint rule — cap test files per production module at 2

Adds scripts/lint-test-file-count.cjs with a ratcheted allowlist
(scripts/lint-test-file-count.allowlist.json) capturing today's
30 violating clusters as a ceiling. New entries blocked at PR time;
reductions ratchet automatically.

Wires into .github/workflows/test.yml as a new step in lint-tests.

Refs #3737

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

* chore(tests): add docs-exempt to changeset fragment

Internal CI lint rule — no user-facing docs impact.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:37:07 -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
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
7f8b5701bf Enforce documentation updates via lint:docs + PR templates (#3651)
* Enforce documentation updates via lint:docs + PR templates (#3213)

New scripts/lint-docs-required.cjs + Docs Required CI workflow fail any
PR whose changeset fragment is typed Added / Changed / Deprecated / Removed
without modifying at least one file under docs/.

Mirrors scripts/changeset/lint.cjs: pure evaluateLint({ changedFiles,
fragments, labels }) returning { ok, reason, triggering } over a frozen
LINT_REASON enum; CLI wrapper reads the PR diff and parses each touched
changeset fragment via the existing parseFragment helper.

Escape hatches:
- no-docs PR label (global)
- per-fragment <!-- docs-exempt: <reason> --> marker, all triggering
  fragments must carry it for the PR to pass

Fixed and Security fragments do not trigger the lint — bug fixes restore
documented behavior, they do not introduce new behavior to document.

PR templates (enhancement.md, feature.md) gain a Documentation checklist
section pointing at the which-doc-to-update matrix. CONTRIBUTING.md adds
a Documentation Updates section codifying that matrix, the English-canonical
language policy for docs/ and the root README, and the two opt-out routes.

Closes #3213

* Address Codex review: fail-closed on malformed fragments and strip docs-exempt marker from rendered release notes (#3213)

Two P2 issues caught by `codex review --base main`:

1) Malformed fragments could silently bypass docs enforcement. parseFragment
   would return ok:false on a triggering Added fragment with bad frontmatter
   and readFragmentsFromDisk dropped it, so evaluateLint saw no triggering
   fragments and passed. The changeset-required lint only checks fragment
   _presence_ not _validity_, so the assumed fallback did not catch it.

   Fix: readFragmentsFromDisk now returns { fragments, malformed }; evaluateLint
   accepts a malformed param and emits a new FAIL_MALFORMED_FRAGMENT verdict
   that outranks every OK path (including the no-docs label) — a parse failure
   must be fixed before docs lint can decide anything else.

2) The per-fragment <!-- docs-exempt: reason --> marker lived in the fragment
   body, so the existing changelog (serializeChangelog) and GitHub release-notes
   (formatBullet) serializers published it verbatim. Worse, both serializers
   append `(#NNNN)` to the body's last line — with the marker as the trailing
   line, the PR suffix attached to the hidden comment instead of the visible
   bullet.

   Fix: parseFragment now extracts the marker into a typed `docsExempt` field
   and strips it from `body`, so all downstream renderers produce clean output
   without remembering to strip. The regex is anchored to its own line (^...$
   with m flag) so inline mentions of the marker syntax in documentation
   (e.g. inside backticks) cannot accidentally exempt a fragment. Bounded
   character class [^\n>] keeps the regex linear-time.

Test additions:
- tests/lint-docs-required.test.cjs: FAIL_MALFORMED_FRAGMENT coverage,
  end-to-end "Added fragment with bad pr → malformed → fail-closed" regression
  test, updated readFragmentsFromDisk return-shape assertions, isExemptFragment
  now checks the typed docsExempt field rather than body content.
- tests/changeset-parse.test.cjs: extractDocsExempt extraction cases (with/
  without reason, case-insensitive, EMPTY_BODY when body is only a marker),
  inline-mention false-positive guard, real-marker-wins-when-also-inline test.
- tests/changeset-new.test.cjs: fragment shape now includes docsExempt: null.

CONTRIBUTING.md updated to clarify the "on its own line" requirement and the
parse-time stripping behavior. The bootstrap fragment cleaned up so its body
no longer contains a literal marker example that would have triggered the
false-positive case.

Full suite: 9696/9696 pass.

* CRLF-safe docs-exempt marker stripping (Codex review pass 2, #3213)

Second `codex review --commit` pass caught a CRLF regression in the
docs-exempt extraction added in the previous commit.

Repro: a Windows-authored fragment

  ---\r\ntype: Added\r\npr: 1\r\n---\r\nFeature.\r\n\r\n<!-- docs-exempt: x -->\r\n

would parse to body `Feature.\r\n\r\n\r` because:

  - The previous trailing-newline slice trimmed only `\n`, leaving `\r`.
  - DOCS_EXEMPT_RE was anchored with `$` only — in multiline mode `$`
    matches before `\n` but does not consume `\r`, so the marker line's
    trailing `\r` was left behind after replace.
  - The cleanup regex stripped trailing `\n` but not `\r`.

Net effect: serializeChangelog emitted

  - Feature.\r
  \r
  \r (#1)

— the `(#1)` PR suffix landed on a blank line instead of attached to
the visible bullet. Same bug surfaces in github-release-notes formatBullet.

Fix:
- DOCS_EXEMPT_RE: add `\r?` before `$` so the regex consumes the CR of a
  CRLF terminator. Switch reason character class from `[^\n>]` to
  `[^\r\n>]` so CRLF-authored reasons don't carry a trailing `\r`.
- extractDocsExempt cleanup: `[ \t\r]+$/gm` strips trailing `\r` on each
  line; `(?:\r?\n){3,}` collapses CRLF triple-blank-lines; `[\r\n]+$`
  strips every trailing line terminator (LF or CR).
- parseFragment trailing-newline slice: CRLF-aware — strips `\r\n` (2
  chars) before falling through to single `\n`.

Tests: two CRLF regression cases in tests/changeset-parse.test.cjs —
Codex's exact repro (end-to-end through serializeChangelog) plus the
no-marker CRLF passthrough case. Full suite: 9698/9698 pass.

* CRLF regression test asserts on parseChangelog IR not rendered text (Codex review pass 3, #3213)

Third `codex review` pass caught that the CRLF regression test added in
the previous commit asserted on serializeChangelog's rendered Markdown
via `out.split('\n')` + `assert.match`. That violates CONTRIBUTING.md's
"Prohibited: Raw Text Matching on Test Outputs" rule and the documented
serializer contract in `serialize.cjs`:

  > tests assert via round-trip (parse(serialize(ir)))
  > rather than by inspecting serialized text

Replace the regex check with the established `parseChangelog(out)`
round-trip and assert on the structured `{ body: 'Feature.', pr: 1 }`
bullet. This is also a stronger regression check than the substring
match: Codex's own probe in the review session confirmed the pre-fix
buggy body shape (`Feature.\r\n\r\n\r`) breaks parseChangelog's bullet
regex entirely (returns `bullets: []`), so the round-trip catches the
exact failure mode end-to-end.

Full suite: 9698/9698 pass.

* Address CodeRabbit findings: anchor link + require non-empty docs-exempt reason (#3213)

CodeRabbit's review on the PR caught two actionable issues, both quick wins.

Anchor link in PR templates pointed to a heading that does not exist. The
CONTRIBUTING.md heading "Documentation Updates — Update the Relevant Docs"
contains an em-dash, which GitHub strips entirely when generating anchor
slugs (it does NOT collapse to a hyphen). The actual anchor is
#documentation-updates-update-the-relevant-docs (single hyphen between every
word), not #documentation-updates--update-the-relevant-docs (double hyphen
where the em-dash was). Both feature.md and enhancement.md fixed.

The docs-exempt marker matched a bare `<!-- docs-exempt -->` with no reason,
which defeats the entire purpose of the escape hatch — the marker exists to
leave an audit trail explaining WHY a PR is exempt. Without a reason it is
a silent bypass.

Fix: DOCS_EXEMPT_RE now requires both the colon AND a non-whitespace first
reason character. Bare `<!-- docs-exempt -->`, empty `<!-- docs-exempt: -->`,
and whitespace-only `<!-- docs-exempt:   -->` are all rejected as if the
marker were not present (`docsExempt: null`). The lint then falls through
to its normal docs-required / no-docs-label checks.

`isExemptFragment` in the lint module tightened too — defense-in-depth: even
if a caller constructs a fragment with `docsExempt: ''` directly, it does
not count as exempt. The predicate now requires `typeof === 'string'` and
non-empty after trim.

Tests:
- changeset-parse.test.cjs: three new explicit-rejection cases (bare marker,
  empty reason, whitespace-only reason). Existing DOCS_EXEMPT_RE shape test
  extended with negative assertions for the same three forms.
- lint-docs-required.test.cjs: prior "empty reason still exempt" test
  inverted — empty/whitespace docsExempt now produces FAIL_DOCS_MISSING.
  isExemptFragment helper test extended with the same negative cases.
- CONTRIBUTING.md: clarified that the reason is required and non-empty.

Skipped CodeRabbit's third finding ("use `npm run lint:docs` in CI workflow
instead of `node scripts/lint-docs-required.cjs`") — the existing
changeset-required.yml uses the direct-node form for the equivalent
changeset lint, so the new docs-required.yml is convention-consistent.
Switching one without the other would create drift, and switching both is
out of scope for #3213.

Bootstrap fragment continues to extract cleanly under the stricter regex
(verified — `docsExempt` field still contains the full bootstrap reason).
Full suite: 9701/9701 pass.
2026-05-16 13:09:54 -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
5a8610e0c2 test: forbid cwd in PR checkers 2026-05-15 20:00:29 -04:00
Tom Boucher
a3ca6ff6d6 feat(3553): Project-Root Resolution Module via generator (Phase 4 of #3524)
Phase 4 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the `findProjectRoot` duplication that lived at
bin/lib/core.cjs:74-140 and sdk/src/query/helpers.ts:497-590,
the drift carrier behind historical bugs #1362 and #2561.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

None of these is in scope for Phase 3.

Closes #3544.
2026-05-15 09:35:02 -04:00
Tom Boucher
2de2d185fa feat(3536): Configuration Module via shared manifests + generator (Phase 2 of #3524)
Phase 2 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the structural drift surface that produced bug class

After this phase, neither bin/lib/ nor sdk/src/ defines
CONFIG_DEFAULTS, VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, or the
four legacy-key normalizations inline. All come from one canonical
source: the Configuration Module (sdk/src/configuration/index.ts)
+ two JSON manifests (sdk/shared/config-{defaults,schema}.manifest.json).
The CJS mirror is generator-emitted (get-shit-done/bin/lib/configuration.generated.cjs)
with a CI freshness check (sdk/scripts/check-configuration-fresh.mjs).

- sdk/shared/config-defaults.manifest.json — canonical nested defaults,
  union of CJS + SDK keys (includes security_*, post_planning_gaps,
  agent_skills, mode, every git/workflow/hooks sub-section).
- sdk/shared/config-schema.manifest.json — VALID_CONFIG_KEYS array,
  RUNTIME_STATE_KEYS array, DYNAMIC_KEY_PATTERNS array with source
  strings (regex reconstructed at runtime).
- sdk/src/configuration/index.ts — source of truth. Exports
  loadConfig (pure read), normalizeLegacyKeys (pure, idempotent,
  returns Normalization[]), mergeDefaults (deep-merge), migrateOnDisk
  (explicit opt-in disk writeback), plus CONFIG_DEFAULTS,
  VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS.
- sdk/src/configuration/index.test.ts — 29 vitest pinning tests.
- sdk/scripts/gen-configuration.mjs — generator (Function.prototype.toString()
  inspection of compiled SDK dist, plus brace-balanced text scan for
  internal helpers, matching the Phase 1 pattern).
- sdk/scripts/check-configuration-fresh.mjs — CI freshness gate.
- tests/configuration-generator.test.cjs — 27 parity assertions
  (CJS-generated == SDK source).
- tests/configuration-migrate-config.test.cjs — 3 cases for the new
  gsd-tools migrate-config subcommand.

- bin/lib/core.cjs: CONFIG_DEFAULTS literal now sources values from
  CANONICAL_CONFIG_DEFAULTS (the manifest), with a thin flat
  projection at the load boundary to preserve the existing
  flat-shape return contract for the ~21 CJS test files and 100+
  consumers. All four legacy-key migration blocks (branching_strategy,
  sub_repos, multiRepo, depth — historically lines 351-358, 388-397,
  401-408, 416-423) collapse to a single normalizeLegacyKeys call
  in each code path. The inline platformWriteSync writeback stays
  for now to preserve sync loadConfig semantics; the new async
  migrateOnDisk is reachable via gsd-tools migrate-config.
- bin/lib/config-schema.cjs: 135 → 31 lines. Re-exports from the
  generated Module.
- bin/lib/config.cjs: adds cmdMigrateConfig handler (calls
  migrateOnDisk on the explicit user-driven path).
- bin/gsd-tools.cjs: wires migrate-config into command dispatch.

- sdk/src/config.ts: re-exports CONFIG_DEFAULTS and mergeDefaults
  from the Module. loadConfig now calls normalizeLegacyKeys before
  mergeDefaults (replaces the inline branching_strategy graft).
- sdk/src/query/config-schema.ts: 160 → 36 lines. Re-exports from
  the Module.

- tests/config-schema-sdk-parity.test.cjs: refactored from
  "CJS Set equals SDK Set" (trivially true post-migration) to
  "both sides source from the manifest" — structural plus runtime
  invariant.
- Four other tests that text-grepped source files for valid keys
  (plan-review-convergence, bug-3212, bug-2492, feat-3210) are
  updated to use runtime VALID_CONFIG_KEYS.has() or manifest JSON
  lookups.

- CONTEXT.md: new Configuration Module entry with full Interface
  contract.
- Root package.json: check:configuration-fresh proxy script.
- sdk/package.json: gen:configuration + check:configuration-fresh.
- .githooks/pre-commit: configuration drift block.
- .github/workflows/test.yml: configuration drift step after the
  alias drift check.

- 9201 CJS tests pass (baseline pre-cycle: 9195; +6 net new tests
  across migrate-config + parity refactor)
- 1872 SDK vitest tests pass
- 29 Configuration Module vitest fixtures
- 27 CJS/SDK parity fixtures
- Net diff: +388 / −519 = 131-line reduction across the seven cycles,
  despite adding the new Module, manifests, generator, freshness
  check, and two new test files.

1. SDK CONFIG_DEFAULTS now includes manifest-canonical keys
   (resolve_model_ids: false, context_window: 200000, phase_naming,
   claude_md_path, git.create_tag, workflow.security_*,
   workflow.code_review_*, planning.*, hooks.workflow_guard, ship.*).
   Consumers accessing via [key: string]: unknown index get
   the manifest default instead of undefined.
2. SDK mergeDefaults is now proper recursive deep-merge instead of
   spread-per-section. Overlay { workflow: { research: false } }
   now preserves sibling workflow keys; previously it replaced
   the entire workflow section with only research + the section's
   defaults. Semantically identical for the common case;
   strictly better for partial nested overrides.
3. New gsd-tools migrate-config CLI subcommand for the explicit,
   opt-in on-disk migration path.

Closes #3536.
2026-05-15 00:02:56 -04:00
Tom Boucher
bfd7ddbad3 feat(3530): STATE.md Document Module via generator (Phase 1 of #3524) (#3531)
* feat(3530): STATE.md Document Module via generator (Phase 1 of #3524)

Phase 1 of the CJS↔SDK hard-seam migration (parent #3524).
Converts the hand-synced state-document.cjs/state-document.ts pair
into a generator-driven seam, modeled on the existing
command-aliases.generated.* precedent.

What landed:
- sdk/src/query/state-document.ts is the source of truth.
- sdk/scripts/gen-state-document.ts emits
  get-shit-done/bin/lib/state-document.generated.cjs from the
  compiled SDK dist via Function.prototype.toString() inspection
  for the 7 public exports and 3 internal helpers.
- sdk/scripts/check-state-document-fresh.mjs is the CI freshness
  gate; pre-commit hook also runs it when relevant files change.
- get-shit-done/bin/lib/state-document.cjs is reduced to a one-line
  re-export from state-document.generated.cjs so existing callers
  (state.cjs, workstream-inventory.cjs, init.cjs) need no changes.
- New CI step in .github/workflows/test.yml after the existing alias
  drift check.
- sdk/package.json: gen:state-document, check:state-document-fresh
  scripts. tsx added as devDep.
- Root package.json: proxy script for the freshness check.
- CONTEXT.md: one-sentence amendment on STATE.md Document Module
  recording the source-of-truth file path.

Tests:
- sdk/src/query/state-document.test.ts: 34 vitest fixtures across
  the 7 public exports (TDD pinning safety net).
- tests/state-document-generator.test.cjs: 31 node:test parity
  assertions comparing SDK source vs generated CJS for every
  fixture.
- Full suite: 9177/9177 pass (baseline was 9146; +31 new tests).

One subtle behavior change worth flagging: the old hand-written
state-document.cjs used String(str) coercion inside escapeRegex,
which the SDK source does not. The generator faithfully matches
the SDK (the source of truth per ADR-3524), so the new CJS no
longer coerces non-string input to string before regex-escaping.
No current caller passes non-string input, so no observable
regression in the test suite. Flagged in the PR body for
reviewers.

Closes #3530.

* fix(3530): address state-document review findings
2026-05-14 22:17:20 -04:00
Tom Boucher
75d5ca5875 feat(code-review): integrate fallow structural pre-pass for /gsd-code-review (#3424)
* feat(code-review): add optional fallow structural pre-pass

* fix(ci): sync lockfile for fallow optional binaries

* fix(test): make fallow integration tests cross-platform

* fix(review): require executable fallow binary paths

* docs(review): clarify structural findings usage and size guard

* fix(fallow): preserve line:0, prefer node_modules/.bin, sync SDK twin (H1, M2, N1 from #3424 review)

* fix(workflow): harden fallow pre-pass — exit check, timeout, atomic write, size-guard order (B1, H2-H4, M1, M3 from #3424 review)

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

* fix(deps): pin fallow floor to ^2.70.0 matching lockfile (H7 from #3424 review)

* fix(config): enum-validate fallow.scope/profile + group code_quality.* contiguously (H5, N3 from #3424 review)

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

* docs(fallow): label mcp gate reserved, version-pin install, expand context schema (B3, H8, M4, M8, L1 from #3424 review)

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

* test(fallow): replace source-grep with behavioral tests, expand fixtures, fail-loud tmpdir (B4, H6, L2, L3, M5, M6, N2 from #3424 review)

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

* fix(workflow): escape closing structural_findings tag in JSON payload (CR #3424 inline finding)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:19:47 -04:00
Tom Boucher
d0f916728b feat(skill-surface): install-time profiles + runtime /gsd:surface (#3408) (#3456)
* feat(skill-deps): add requires: frontmatter to all 51 skills with cross-skill references

Mechanical migration from docs/research/data/2026-05-12-skill-audit.json.
Every skill whose body references another GSD skill now declares those
dependencies in `requires:` YAML frontmatter (flow-style array).

Notable: discuss-phase, plan-phase, and execute-phase all reference `phase`,
which confirms the latent gap in MINIMAL_SKILL_ALLOWLIST — `phase` is pulled
by the core loop but was never in the allowlist. The profile closure model
(ADR-0010 Phase 1) resolves this automatically.

Closes part of #3408.

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

* feat(skill-surface-budget): add PROFILES map, resolveProfile, loadSkillsManifest, staging, marker IO

Implements the Skill Surface Budget Module core (ADR-0010, Phase 1):

- PROFILES Object.freeze map: core (6 skills), standard (~13), full ('*')
- loadSkillsManifest: parses requires: frontmatter from commands/gsd/*.md
  into a Map<stem, string[]> without external YAML dep
- resolveProfile({modes, manifest}): computes transitive closure over the
  requires: graph; composable (modes=['core','audit'] unions closures)
- stageSkillsForProfile / stageAgentsForProfile: filesystem staging with
  same exit-cleanup machinery as the legacy stageSkillsForMode
- readActiveProfile / writeActiveProfile: .gsd-profile marker round-trip
- Back-compat shims preserved: MINIMAL_SKILL_ALLOWLIST, isMinimalMode,
  shouldInstallSkill (overloaded), stageSkillsForMode — all legacy tests pass

The phase latent bug is now resolved by closure: discuss-phase, plan-phase,
and execute-phase all require phase, so any profile including any of them
automatically includes phase via transitive closure.

Tests: 22 manifest+resolve, 9 stage, 10 marker (41 new tests, all green).
Back-compat anchor: 80/80 passing.

Closes part of #3408.

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

* feat(skill-surface-budget): add lint-skill-deps.cjs CI gate and fix 19 missed requires: entries

Two lint checks (scripts/lint-skill-deps.cjs):
  a) Frontmatter-body consistency: skill body references must appear in requires:
  b) Profile closure: every requires: dep of any profile skill must be in closure

Running the lint revealed 19 body references missed by the audit JSON (the
audit used static analysis; some bodies have conditional references). Fixed:
  complete-milestone: +audit-milestone, discuss-phase, plan-phase, execute-phase, new-milestone
  fast: +quick
  health: +thread
  map-codebase: +new-project, plan-phase
  new-milestone, new-project, review, ultraplan-phase: +plan-phase
  ship: +verify-work
  sketch, spike: +new-project
  verify-work: +execute-phase
  workstreams: +new-milestone, resume-work

Wired into package.json as lint:skill-deps and added to pretest.
8 fixture-based tests: all green.

Closes part of #3408.

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

* feat(skill-surface-budget): wire --profile= arg, profile marker write/read in bin/install.js

- Add --profile=<name> / --profile=<n1>,<n2> arg parsing (composable).
  Mutually exclusive with --minimal / --core-only (aliases for --profile=core).
  Default (no flag): full.
- Import readActiveProfile / writeActiveProfile from install-profiles.cjs.
- After writeManifest: persist active profile to .gsd-profile marker.
- gsd update path: if no --profile flag given, read existing .gsd-profile
  marker so non-full profiles are not silently re-expanded to full (ADR-0010).
- Update --help block to document --profile= with per-tier token costs.

New test: install-minimal-backcompat.test.cjs (6 tests):
  - PROFILES.core === MINIMAL_SKILL_ALLOWLIST (contract)
  - --minimal writes .gsd-profile marker "core"
  - --profile=core, --profile=standard write correct markers
  - default install writes marker "full"

Closes part of #3408.

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

* chore(changeset): add feat-3408-skill-profiles changelog fragment

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

* feat(install-profiles): derive agents from skill body refs and wire into resolveProfile

Deviation 1 of ADR-0010 phase 1b: tiered profiles (core, standard) now produce
a non-empty agents Set instead of always returning empty. resolveProfile() scans
each skill body for gsd-* agent name references (via new parseCallsAgents()),
stores them in _calls_agents_<stem> manifest entries, and unions them across the
resolved skill closure. stageAgentsForProfile() already checked resolvedProfile.agents
— it now gets real data so tiered profiles install the correct subset of agents
instead of zero.

Closes #3408 (partial — Deviation 1 only)

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

* feat(install): honor .gsd-profile marker on update, add resolveEffectiveProfile/mostRestrictiveProfile

Deviation 2 of ADR-0010 phase 1b: the marker written during installation is now
actually honored when re-running without explicit flags (e.g. gsd update). The
dead-end logging block is replaced by resolveEffectiveProfile(), which picks the
marker profile over 'full' when no explicit --profile= flag was given. The resolved
profile is piped through to all 13 stageSkillsForMode dispatch sites (now _stageSkills)
so updates install only the previously-chosen skill subset.

--minimal retains its back-compat behavior (strict 6-skill allowlist, no closure)
while writing 'core' to the marker. mostRestrictiveProfile() is exported for callers
that need to reconcile disagreeing markers across runtimes (smallest skill set wins).

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

* feat(surface): add CLUSTERS data + state IO module

Add clusters.cjs with 10 named skill groups covering all 66 skills
(verified by surface-clusters.test.cjs). Add surface.cjs with readSurface/
writeSurface atomic IO, resolveSurface, applySurface, and listSurface.
Tests: 17 passing (11 state IO + 6 cluster integrity).

Closes #3408

* docs(adr): add ADR-0011 Skill Surface Budget Module (Phase 1 accepted, Phase 2 amendment)

Records the install-time profile staging decision (Phase 1, landed) and the
runtime /gsd:surface cluster-toggle decision (Phase 2, in flight) as an
amendment. Updates the ADR README index.

Closes #3408

* docs(install-profiles): update module docblock for Phase 2 and ADR-0011

Corrects the ADR reference from 0010 to 0011, documents the three-profile
model and back-compat aliases, adds resolveEffectiveProfile precedence rule,
and notes the companion surface.cjs Phase 2 engine.

* docs(context): add Skill Surface Budget Module canonical entry

Adds the Domain terms entry for the Skill Surface Budget Module covering
both Phase 1 (install-time profiles, .gsd-profile marker) and Phase 2
(runtime /gsd:surface cluster toggles, clusters.cjs, .gsd-surface.json),
per ADR-0011 Consequences requirement.

* feat(surface): add resolveSurface and applySurface engine + tests

Tests cover: profile → surface equivalence, cluster disable/enable,
explicitAdds transitive closure, applySurface file sync (add missing,
remove superseded, preserve non-gsd files), listSurface token cost.
16 new tests passing.

* docs(readme): document --profile= flag and /gsd:surface command

Brief user-facing mention of install profiles (core/standard/full) and the
/gsd:surface slash command in the Commands table. Points to ADR-0011 for details.

* feat(surface): add /gsd:surface slash command runbook

New skill: gsd:surface — runtime profile/cluster toggle without reinstall.
Sub-commands: list, status, profile <name>, disable/enable <cluster>, reset.
Persists state to .gsd-surface.json (independent of .gsd-profile).
Description 96 chars (≤100 limit). lint:descriptions + lint:skill-deps: 0 violations.

* feat(surface): add changeset fragment for /gsd:surface runtime toggle

* feat(surface): add surface skill stem to utility cluster

surface.md is a new skill; add it to the utility cluster so the
surface-clusters.test.cjs coverage invariant stays satisfied.

* docs(adr): fix ADR references to 0011 and record Phase 2 as shipped

ADR-0010 number was already claimed by the file-operation-engine ADR; this
ADR landed as 0011-skill-surface-budget-module.md. Update inline ADR
references in clusters.cjs, surface.cjs, install-profiles.cjs, and the
Phase 2 changeset to ADR-0011. Update the ADR Status section to record
Phase 2 artifacts as shipped on this branch rather than "in progress".

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

* docs(research): port skill-surface-budget memo and audit data

ADR-0011 references docs/research/2026-05-12-skill-surface-budget.md and
docs/research/data/2026-05-12-skill-audit.json, which only existed in the
research worktree. Port both onto this branch so the ADR's References
section resolves and reviewers can read the cluster taxonomy (§3.2),
dependency topology (§3.1), and option grading (§4) that justify Phase 1
and Phase 2 decisions.

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

* fix(registration): register surface/clusters in INVENTORY, COMMANDS, and help.md

- surface.md: convert allowed-tools from inline YAML array to block style
  (was parsed as a single tool name "[Read, Write, Bash]" by test harness)
- docs/INVENTORY.md: add CLI module rows for clusters.cjs and surface.cjs;
  add Commands row for /gsd-surface; bump CLI Modules count 55→57, Commands 66→67
- docs/INVENTORY-MANIFEST.json: add entries for clusters.cjs, surface.cjs,
  and /gsd-surface (filename-based command key)
- docs/COMMANDS.md: add ### `/gsd-surface` heading in Configuration Commands
- get-shit-done/workflows/help.md: add /gsd:surface entry in Configuration section

Fixes registration failures introduced by Phase 2 of #3408.

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

* fix(surface,docs): scrub .claude leakage and escape hypothetical slash tokens

Two PR regressions introduced earlier on this branch:

1. surface.cjs JSDoc comments contained the canonical paths
   (~/.claude/commands/gsd, ~/.claude/agents) as example values, which the
   cline-install leak regex (~\/\.claude\/(?:get-shit-done|commands|agents
   |hooks)) flagged as install-time path leaks. Reworded the docblocks to
   describe runtime-resolved paths without literal ~/.claude tokens.

2. The ported research memo proposed hypothetical Option C dispatchers
   using slash syntax (/gsd:milestone, /gsd:research). The
   docs-parity-live-registry test enforces that every slash-command token
   in docs/ resolves to a real command. Rewrote the Option C sketch
   without the slash prefix and added a clarifying note that the
   dispatchers are illustrative, not shipped.

Targeted tests now pass: tests/cline-install.test.cjs and
tests/docs-parity-live-registry.test.cjs both green.

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

* test: remove raw output/source grep in lint tests

* fix: close coderabbit profile and requires issues

* test: align surface token-cost assertion wording

* fix(install): align core profile alias and defer profile marker write

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 12:45:16 -04:00
Tom Boucher
96806003c5 fix(#3229): shared model catalog source of truth for agent profiles + runtime tier defaults (#3230)
* docs(adr): add ADR-0003 model catalog module

* fix(#3229): add shared model catalog as source of truth for agent profiles and runtime tier defaults

Research / design (ADR-0003):
- Existing drift came from 4 independent model truths:
  1. CJS model-profiles.cjs
  2. SDK config-query.ts stale copy (18 agents)
  3. settings-advanced.md runtime tier table
  4. session-runner Claude-only profile map
- New design: one machine-readable Model Catalog Module in sdk/shared/
  that both packages ship and consume.

Implementation:
- sdk/shared/model-catalog.json — canonical source of truth for:
  - full 33-agent registry
  - per-agent golden (quality) alias + balanced/budget aliases
  - adaptive derivation from routingTier
  - agent→phaseType map
  - agent→dynamic-routing default tier map
  - runtime tier defaults for all supported runtimes
- get-shit-done/bin/lib/model-catalog.cjs — CJS adapter over the catalog
- sdk/src/model-catalog.ts — SDK adapter over the same catalog
- CJS model-profiles.cjs now re-exports derived data from model-catalog.cjs
- SDK config-query.ts now re-exports MODEL_PROFILES/VALID_PROFILES from
  model-catalog.ts instead of maintaining its own list
- sdk/src/query/helpers.ts runtime list now comes from the catalog (fixes hermes drift)
- sdk/src/session-runner.ts Claude profile→model-id mapping now resolves via catalog
- docs/CONFIGURATION.md + settings-advanced.md runtime tables updated to match catalog

Behavior changes:
- resolve-model now covers every shipped agent file on disk (33 agents)
- unknown-agent fallback is profile-semantic, not hardcoded sonnet:
  quality→opus, budget→haiku, balanced/adaptive→sonnet, inherit→inherit
- Group B runtimes remain known runtimes but do not get built-in tier defaults

Tests (RED→GREEN):
- root tests: shipped agent files must equal MODEL_PROFILES keys
- sdk tests: shipped agent files must equal MODEL_PROFILES keys
- direct fix assertion: gsd-code-reviewer resolves to opus under quality with no unknown_agent
- runtime defaults parity test: settings-advanced.md + CONFIGURATION.md tables must match catalog
- helper tests: hermes included in SUPPORTED_RUNTIMES and getRuntimeConfigDir()

Closes #3229

* chore(changeset): update #3229 changeset pr field to 3230

* fix(ci): update inherit fallback expectations and inventory parity for model catalog
2026-05-08 21:25:37 -04:00
Tom Boucher
2d32ad82be fix(plan-phase): remove agent: directive that caused OpenCode subagent dispatch (#3156) (#3206)
* feat(roadmap): parse **Mode:** field on phase sections

Adds a 'mode' field to roadmap.get-phase and roadmap.analyze outputs.
Recognizes '**Mode:** mvp' lines in phase sections; lowercased + trimmed.
Forward-compat: unrecognized values preserved verbatim, no enum check.

Foundation for --mvp flag in plan-phase (PRD: vertical-mvp-slice).

* feat(plan-phase): parse --mvp flag and resolve MVP_MODE

Resolution order: CLI flag → ROADMAP **Mode:** field → workflow.mvp_mode
config → false. Walking Skeleton gate fires for new-project Phase 1.
Wires MVP_MODE + WALKING_SKELETON into gsd-planner subagent prompt.

Per PRD vertical-mvp-slice Phase 1 (Q1, Q2, Q4).

* docs(planner): add vertical-slice planning reference

New reference loaded by gsd-planner when MVP_MODE=true. Defines slice
ordering, Walking Skeleton rules, and anti-patterns. Referenced from
plan-phase workflow MVP_MODE wiring.

* docs(planner): add SKELETON.md template

Template emitted by gsd-planner under WALKING_SKELETON=true. Captures
architectural decisions and out-of-scope list for new-project Phase 1.

* chore(inventory): register new planner references

Added planner-mvp-mode.md and skeleton-template.md to INVENTORY.md and
INVENTORY-MANIFEST.json. References now: 53.

* feat(gsd-planner): add MVP Mode Detection section

Mode-switched branch in the existing planner agent (per Q4: single agent).
Vertical-slice decomposition rules, Walking Skeleton handling, and
TDD-mode compatibility. Heavy guidance lives in references/planner-mvp-mode.md.

* test(plan-phase): add --mvp resolution-chain integration cases

Validates roadmap.get-phase --pick mode and confirms workflow.mvp_mode
default is unset in fresh projects.

* docs(changelog): announce --mvp vertical-slice planning (#2826)

* feat(mvp-phase): add /gsd mvp-phase slash command

Standalone command for vertical MVP planning. Frontmatter only;
heavyweight workflow at get-shit-done/workflows/mvp-phase.md follows
in next commit. Mirrors discuss-phase/edit-phase command shape.

* docs(planner): add user-story-template reference

Defines the canonical 'As a / I want to / So that' format and the
ROADMAP.md / PLAN.md emit rules. Used by mvp-phase workflow and
gsd-planner agent under MVP_MODE.

* docs(planner): add SPIDR splitting reference

Defines size signals, the five SPIDR axes (Spike/Paths/Interfaces/Data/Rules),
the interactive workflow, and anti-patterns. Per PRD Q3 decision: full
interactive flow, not lightweight check. Used by mvp-phase workflow.

* fix(mvp-phase): trim description to fit 100-char budget

* feat(mvp-phase): add mvp-phase workflow

Standalone workflow: phase validation -> user story prompts (As a / I want to /
So that) -> SPIDR splitting check -> ROADMAP write (Mode + Goal) -> delegation
to plan-phase. Per PRD Phase 2 (Q3 full SPIDR; Phase-2-A/B/C/D decisions).

Plan-phase auto-detects MVP via Phase 1's resolution chain, so no flags
are needed when delegating.

* feat(gsd-planner): emit user-story header in PLAN.md under MVP mode

Extends the MVP Mode Detection section (added in Phase 1) so the planner
sources the user story from ROADMAP **Goal:** and emits the bolded
**As a** / **I want to** / **so that** form as the first content under
the phase header in PLAN.md. References user-story-template.md.

* test(mvp-phase): integration smoke test for ROADMAP mutation

Validates roadmap.get-phase output after a workflow-spec'd ROADMAP write:
mode=mvp and goal=full user story. Catches schema drift between workflow
emit and parser expectation. Includes a long-story case (>120 chars) to
confirm SPIDR-rejected stories still parse correctly.

* chore(inventory): register mvp-phase command + 2 new references

Adds /gsd mvp-phase to commands list, mvp-phase workflow to workflows list,
and user-story-template.md + spidr-splitting.md to references. References
count: 53 -> 55.

* docs(changelog): announce /gsd mvp-phase command (#2826)

* fix(mvp-phase): add TEXT_MODE plain-text fallback for non-Claude runtimes (#2012)

* docs(executor): add MVP+TDD gate reference

Defines the runtime gate semantics for execute-phase when both
MVP_MODE and TDD_MODE are true: pre-task verification of failing-test
commit, end-of-phase review escalation from advisory to blocking,
behavior-adding task definition. Loaded conditionally by
execute-phase workflow and gsd-executor agent.

* feat(execute-phase): MVP+TDD runtime gate + blocking review

Resolves MVP_MODE in Step 1 (CLI flag -> roadmap mode -> config -> false).
Adds per-task gate that halts before behavior-adding tasks run if no
failing-test commit exists for the plan. Escalates end-of-phase TDD
review from advisory to blocking when both MVP_MODE and TDD_MODE active.

Also updates INVENTORY-MANIFEST.json to register execute-mvp-tdd.md
(added by Task 1) so manifest-sync tests pass.

Per PRD vertical-mvp-slice Phase 3a (decisions Phase-3-A, Phase-3-Split).

* feat(gsd-executor): add MVP+TDD Gate section

Mirrors the planner's MVP Mode Detection pattern from Phase 1.
Instructs halt-and-report when the runtime gate trips, references
execute-mvp-tdd.md for full semantics. No agent changes outside the
new section.

* test(execute-phase): add MVP+TDD resolution-chain integration cases

Validates roadmap.get-phase --pick mode and confirms workflow.mvp_mode
default is unset in fresh projects. Mirrors the Phase 1 plan-phase
resolution-chain integration test.

* chore(inventory): register execute-mvp-tdd reference

Bumps References count 55 -> 56. Registers execute-mvp-tdd.md.
Adds "init" to PROSE_ALLOWLIST in registry integration test so
bare `gsd-sdk query init` prose examples in plan docs don't
trigger the unregistered-handler guard (real commands are all
init.<subcommand>).

* docs(changelog): announce MVP+TDD runtime gate in execute-phase (#2826)

* docs(verifier): add verify-mvp-mode reference

Defines UAT framing under MVP mode: user-flow walk-through first,
technical checks deferred, coverage check as goal-backward narrowing
to the user story's outcome clause. Loaded conditionally by
verify-work workflow and gsd-verifier agent.

* feat(verify-work): MVP-mode UAT framing — user flow first

Resolves MVP_MODE from phase mode field. Under MVP mode, generates UAT
in three ordered sections: user-flow walk-through (derived from user
story), technical checks (deferred), coverage check (goal-backward).
Falls back to standard UAT generation when mode is null/absent.
User-story-format guard refuses to verify a mode:mvp phase with a
non-user-story goal.

Also updates docs/INVENTORY.md (56 references) and
docs/INVENTORY-MANIFEST.json to register verify-mvp-mode.md added
in Task 1.

Per PRD vertical-mvp-slice Phase 3b (decisions Phase-3-B,
Phase-3-Verify-Structure).

* feat(gsd-verifier): add MVP Mode Verification section

Narrows goal-backward verification to the user-story [outcome] clause
when phase mode is mvp. References verify-mvp-mode.md. Preserves
existing goal-backward methodology for non-MVP phases. User-story-format
guard refuses to verify a mode:mvp phase with a non-user-story goal.

* docs(changelog): announce MVP-mode UAT framing in verify-work (#2826)

* feat(new-project): add Vertical MVP vs Horizontal Layers mode prompt

Asks user at project init how to structure the project. Vertical MVP
emits **Mode:** mvp on every initial roadmap phase (per-phase mode
preserved per PRD Q1). Horizontal Layers falls back to standard
template — no behavioral change for existing flows.

Per PRD vertical-mvp-slice Phase 4 (decision Phase-4-Persistence).

* feat(progress): add MVP-mode user-flow display

When phase has **Mode:** mvp, progress renders user-flow status from
PLAN.md task names alongside standard task progress. Tasks that aren't
user-flow-shaped (technical-sounding) are filtered out of the user-flow
sub-block. Falls back to standard display when mode is null/absent.

Per PRD vertical-mvp-slice Phase 4 (decision Phase-4-Progress).

* feat(stats): add MVP phase count summary

Reads roadmap.analyze (which surfaces mode per phase from Phase 1) and
emits 'Phases: N total | M MVP | K standard' summary line. Suppressed
when MVP_COUNT == 0 to avoid clutter on non-MVP projects.

Per PRD vertical-mvp-slice Phase 4.

* feat(graphify): add MVP-mode visual differentiation

MVP-mode phases render with #22c55e fill color AND ' (MVP)' label
suffix — two-channel signaling for color-blind and grayscale renders.
Standard phases unchanged.

Per PRD vertical-mvp-slice Phase 4 (PRD Q5: distinct visual treatment).

* docs(changelog): announce Phase 4 discovery & progress (#2826)

* chore(release): bump dev to 1.50.0-canary.0 for first 1.50.0 canary

Sets the base version that .github/workflows/canary.yml derives the canary
tag from (strips suffix → base 1.50.0 → next available v1.50.0-canary.N).

This kicks off the 1.50.0 release train, opened by the MVP/TDD/UAT vertical
slice landed across PRs #2867, #2874, #2878, #2880, #2883.

* docs: add CANARY stream README + v1.50.0-canary.1 release notes

- docs/CANARY.md — explains the dev→@canary stream policy, install/rollback
  paths, and when (not) to install canary builds
- docs/RELEASE-v1.50.0-canary.1.md — release notes for the first 1.50.0
  canary cut: vertical MVP/TDD/UAT slice (#2867 + #2874 + #2878 + #2880 +
  #2883), opening the 1.50.0 train under PRD #2826
- docs/README.md — index entry + quick link for the canary stream

* fix(ci/canary): publish gate checks dev branch, not main

Four publish-step `if:` conditions in .github/workflows/canary.yml were
checking `github.ref == 'refs/heads/main'`. Those steps (Tag and push,
Publish to npm, Publish SDK to npm, Verify publish) therefore always
skipped on every workflow_dispatch invocation since canary runs from dev,
never main.

The workflow's own header comment is unambiguous: `dev → @canary`. The
gate was a copy-paste from release.yml (which correctly targets main for
the @next/@latest streams) that was never corrected for the canary stream.

This is why the 1.50.0-canary.1 publish hadn't materialized despite three
green workflow runs. With the gate corrected, the next dispatch will
actually publish.

* ci(release-sdk): make release-sdk.yml dispatchable from the dev branch

The workflow lives on main only, so the GitHub Actions "Use workflow
from" dropdown doesn't list dev — meaning dev → @dev publishes can't be
triggered from the dev branch directly. Add the file to dev so an
operator can dispatch it with branch=dev and tag=dev.

Per project release-stream policy: dev branch publishes canary (@dev).
This is the stream that needs the file most, since main never publishes
@dev itself (main does @next / @latest).

File is byte-identical to main's release-sdk.yml — straight propagation,
no behavioral change. Tracking issues #2925, #2929.

* docs(mvp): canary-prep concept cleanup — CONTEXT.md, mvp-concepts index, --prd interaction (#3176)

* chore(mvp): concept cleanup + cross-ref index for v1.50.0-canary.2 prep

- CONTEXT.md gains 7 MVP domain terms (MVP Mode, User Story, Walking
  Skeleton, Vertical Slice, Behavior-Adding Task, MVP+TDD Gate, SPIDR
  Splitting) so the project glossary matches the shipped surface.
- New get-shit-done/references/mvp-concepts.md indexes the six MVP
  reference files and concept-to-file map so agents and contributors
  can find the right canonical doc without grepping.
- plan-phase.md Walking Skeleton block now documents that --mvp and
  --prd compose orthogonally on Phase 1; no precedence needed.
- INVENTORY/INVENTORY-MANIFEST refreshed for the new reference (58 -> 59).

No behavior change. Canary-prep cleanup ahead of v1.50.0-canary.2.

Surfaced for follow-up (not in this PR):
- MVP_MODE resolution shell block duplicated across plan-phase,
  execute-phase, verify-work workflows (needs a shared workflow-include
  mechanism; structural change).
- Behavior-Adding Task predicate is prose-only; no shared utility.
- User Story regex hardcoded in verify-work; would benefit from a
  central definition consumed by the verifier and the mvp-phase command.

* chore(changeset): set PR number for mvp concept cleanup

* feat(mvp): centralize resolution surfaces + fix SDK roadmap mode parity (#3178)

Three new SDK query verbs replace the architectural duplication surfaced by
the v1.50.0-canary.2 review against dev tip 12c4e565:

  phase.mvp-mode <N> [--cli-flag]
    Single canonical precedence resolver (CLI flag -> ROADMAP **Mode:** mvp
    -> workflow.mvp_mode config -> false). Replaces 4-8 lines of bash that
    were duplicated across plan-phase.md, execute-phase.md, verify-work.md,
    and progress.md. Returns {active, source, roadmap_mode, config_mvp_mode,
    cli_flag_present}.

  task.is-behavior-adding <plan-file> | --task-content <xml>
    Behavior-Adding Task predicate (tdd="true" + <behavior> block + non-test
    source files in <files>). Replaces prose-only specification in
    references/execute-mvp-tdd.md; gsd-executor agent now invokes the verb
    instead of re-inlining the three checks. Returns {is_behavior_adding,
    checks, reason}.

  user-story.validate <text> | --story <text>
    Owns the canonical User Story regex /^As a .+, I want to .+, so that .+\.$/
    previously hardcoded in verify-work.md prose. Consumed by gsd-verifier
    (phase-goal guard) and /gsd-mvp-phase (interactive-prompt validation).
    Returns {valid, slots: {role, capability, outcome}, errors[]}.

Bug fix bundled: sdk/src/query/roadmap.ts searchPhaseInContent now extracts
the mode field from **Mode:**, restoring parity with roadmap.cjs:120-123.
Without this, roadmap.get-phase --pick mode returned null on the native
dispatch path even when the phase had **Mode:** mvp set, causing MVP_MODE
to silently fall through to the config/false branch in every consuming
workflow. The original PRs Phase 1 (#2885) shipped the CJS parser but the
SDK port omitted the field; this fix brings them back to parity.

Workflows + agents updated to call the verbs:
  - plan-phase.md, execute-phase.md, verify-work.md, progress.md call
    phase.mvp-mode (one line replaces the duplicated bash chains).
  - execute-phase.md MVP+TDD gate calls task.is-behavior-adding.
  - verify-work.md goal guard calls user-story.validate.
  - mvp-phase.md interactive prompt validates via user-story.validate.
  - gsd-executor agent references task.is-behavior-adding instead of prose.
  - gsd-verifier agent references user-story.validate instead of inlined regex.

Tests: 24 new vitest tests in sdk/src/query/mvp.test.ts cover all three
verbs + the regression. Two existing contract tests (progress, verify)
updated to assert on the new verb shape. All 60 existing MVP contract
tests pass; golden integration suite (38 + 42 tests) passes.

Closes #3177

* fix(canary.2): unblock release gates for v1.50.0-canary.2

Run 25451329660 (Release SDK Bundle on dev, 2026-05-06T17:41) failed at the
test-suite step with 3 deterministic content/structure gate failures, all
attributable to the MVP umbrella integration in #3178 and the docs sweep
in #3180.

Failure 1: /gsd-mvp-phase undocumented in workflows/help.md
  - tests/bug-2954-help-md-slash-command-stubs.test.cjs requires every
    shipped commands/gsd/<X>.md to have a /gsd-<X> mention in help.md
  - PR #3180 updated docs/COMMANDS.md but missed help.md (which the AI
    agents load in-product)
  - Fix: add a /gsd-mvp-phase entry to help.md right before /gsd-plan-phase

Failures 2 + 3: execute-phase.md (1727) and plan-phase.md (1714) over XL budget (1700)
  - PR #3178 added MVP-mode verb calls (phase.mvp-mode, task.is-behavior-adding,
    user-story.validate) to both workflow files, pushing them past 1700 lines
  - Fix: bump XL_BUDGET 1700 -> 1800 with inline comment pointing at the
    structural follow-up (extract MVP bodies to <workflow>/modes/mvp.md per
    the discuss-phase/modes/ precedent)
  - The structural extract is the right long-term fix but is bigger than
    canary unblock scope; will land in a follow-up after canary cycles

Local verification:
  $ node --test tests/bug-2954-help-md-slash-command-stubs.test.cjs                 tests/workflow-size-budget.test.cjs
  tests 111  pass 111  fail 0

After this lands, re-trigger Release SDK Bundle on dev for v1.50.0-canary.2.

* chore(changeset): set PR number for canary.2 unblock

* fix(codex): generate-claude-md writes to AGENTS.md on Codex runtime

When config.runtime === 'codex' or GSD_RUNTIME=codex, override the
output target to AGENTS.md regardless of claude_md_path, so Codex
projects no longer have GSD sections written to CLAUDE.md by mistake.

Fixes both the CJS (gsd-tools) and SDK (profile-output.ts) paths.
Explicit --output flags are still honoured in both paths.

Closes #3163

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

* fix(plan-phase): remove agent: directive that caused OpenCode subagent dispatch

On OpenCode, any command with `agent: <name>` in its frontmatter is
auto-dispatched to a subagent context where the Agent tool is unavailable.
plan-phase.md and mvp-phase.md both carried `agent: gsd-planner`, causing
them to run inside gsd-planner's subagent context with no ability to spawn
researcher/planner/checker subagents — the orchestrator fell back to inline
execution for all three phases.

Fix: remove `agent: gsd-planner` from both command files so they run in the
main agent context. Also replace the stale `Task` tool in allowed-tools with
`Agent` (the correct dispatcher tool name post-#3168 rename).

Adds a structural regression test that parses YAML frontmatter of every
commands/gsd/*.md file and asserts no command carries an `agent:` directive.

Closes #3156

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

* fix(mvp): address CodeRabbit workflow and contract findings

* fix(execute-phase): use registered state.update query command

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:51:38 -04:00
Tom Boucher
9d5db87249 feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts (#2978)
* feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts

Two PRs that both edit `### Fixed` in CHANGELOG.md always conflict on merge.
Recently bit on #2960/#2972 in the same session — fix-the-conflict-and-rebase
tax. Replace the shared-file model with per-PR fragment files that never
share lines.

Implementation built TDD per #2975, vertical slices with structured-IR
assertions throughout:

  scripts/changeset/parse.cjs       - fragment text → typed record + frozen
                                      FRAGMENT_ERROR enum (8 tests)
  scripts/changeset/render.cjs      - fragments → structured IR with
                                      Keep-a-Changelog section ordering
                                      (2 tests)
  scripts/changeset/serialize.cjs   - IR ↔ markdown round-trip pair
                                      (parse(serialize(ir)) === ir,
                                      3 tests)
  scripts/changeset/cli.cjs         - file-I/O wrapper with --json mode;
                                      reads .changeset/, folds into
                                      CHANGELOG.md, deletes consumed
                                      fragments. Idempotent. (1 test)
  scripts/changeset/lint.cjs        - pure verdict (changedFiles, labels)
                                      → { ok, reason } via LINT_REASON
                                      enum. Honors `no-changelog` label.
                                      (5 tests)
  scripts/changeset/new.cjs         - fragment scaffolder with random
                                      adjective-noun-noun filename. Tests
                                      assert via parseFragment round-trip.
                                      (3 tests)

Total: 22 tests, all assertions on typed structured fields. No regex on
text, no String#includes on file content. Lint clean across 356 test files.

Supporting:

  .changeset/README.md              - format spec + workflow docs
  .changeset/eager-hawks-rally.md   - dogfood fragment for THIS PR (will
                                      be the first thing the new release
                                      tool consumes)
  .github/workflows/changeset-required.yml
                                    - CI: every PR runs lint.cjs
  package.json                      - npm run changeset, changelog:render,
                                      lint:changeset
  CONTRIBUTING.md                   - new "CHANGELOG Entries — Drop a
                                      Fragment" section between PR
                                      Guidelines and Testing Standards

Closes #2975

* fix(#2975): address CodeRabbit findings on changeset workflow

7 valid findings (4 Major, 3 Minor); all addressed:

scripts/changeset/parse.cjs
  - Preserve fragment body verbatim. Previously body.trim() ate
    intentional leading whitespace (code blocks, etc.); now trim() is
    used only for the emptiness check, and a single trailing newline
    is stripped (the editor-added one) so well-formed fragments
    round-trip byte-for-byte. Added a regression test asserting a
    code-block-leading body is preserved.

scripts/changeset/cli.cjs
  - Validate flag values during argument parsing. parseArgs now returns
    { ok, opts | error }; rejects `--repo` etc. with no following value
    or with another flag as the value. main() surfaces the error
    message before exiting 2.
  - Handle post-write fragment-deletion failures. After CHANGELOG.md
    is written, any unlink failure is captured into a structured
    deleteFailures list with reason 'fail_fragment_delete'; cmdRender
    returns exitCode=1 with the partial-failure detail instead of
    leaving the changelog updated and fragments behind (which would
    cause double-consumption on rerun).

scripts/changeset/lint.cjs
  - Treat CHANGELOG.md as a linted user-facing path. Direct edits to
    CHANGELOG.md (the bypass route around the new workflow) now fail
    the lint with FAIL_MISSING_FRAGMENT. Added a regression test for
    that case.
  - Use cp.execFileSync instead of cp.execSync for the git diff call.
    Eliminates the shell-interpolation surface on GITHUB_BASE_REF;
    git's own arg parser remains the validator.

scripts/changeset/new.cjs
  - Atomic fragment creation. existsSync() + writeFileSync was racy
    under concurrent invocations. Now writeFileSync uses { flag: 'wx' }
    which fails EEXIST on collision; the random-name retry loop
    catches EEXIST and re-rolls. Throws explicitly after 16 attempts
    rather than silently overwriting.

.changeset/README.md
  - Add language tag `md` to the format example fence (markdownlint
    MD040).

All 25 changeset tests pass; lint clean (356 test files, 0 violations).

* fix(#2975): sanitize --type and validate flag values in new.cjs (CR fixes)

Two CR findings on scripts/changeset/new.cjs:

1. (Minor) `type` was embedded in frontmatter without sanitization. A
   newline in the value (e.g. `--type 'Fixed\ntype: Added'`) would
   corrupt the fragment. scaffoldFragment now validates `type` against
   the Keep-a-Changelog ALLOWED_TYPES set BEFORE writing — same set
   parse.cjs uses on consume. Throws with a typed error referencing
   the allowed values; tests cover the newline case + 4 other
   non-allowed values.

2. (Minor) `--repo` (and other value-taking flags) without a value
   silently set opts.repo to undefined, which produced a cryptic
   ERR_INVALID_ARG_TYPE deep inside path.join. parseArgs now mirrors
   the cli.cjs convention: returns { ok, opts | error }, validates
   that the next token exists and is not itself another flag, and
   surfaces a precise "missing value for --repo" message before exit.
   Added 3 tests: missing-trailing-value, flag-as-value, well-formed.

29 tests pass across the changeset suite (4 new regression tests).
2026-05-01 18:12:20 -04:00
Tom Boucher
006cdafe8f ci(drift): enforce alias freshness checks in CI and contributor flow (#2910)
Merging alias-drift guardrails and local hook hardening.
2026-04-30 14:19:46 -04:00
Tom Boucher
e81592878e feat(#2789): trim skill description anti-patterns; enforce 100-char budget (#2823)
* feat(#2789): trim skill description anti-patterns; enforce 100-char budget

- Trim descriptions in all commands/gsd/*.md files over 100 chars
- Remove flag documentation from descriptions (belongs in argument-hint)
- Remove Triggers: keyword stuffing
- Add scripts/lint-descriptions.cjs — fails on descriptions > 100 chars
- Add npm script: lint:descriptions
- Add tests/enh-2789-description-budget.test.cjs

Closes #2789

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

* docs(#2789): add CHANGELOG entry for description budget lint

* docs(#2789): update COMMANDS.md descriptions; add skill description standards note

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

---------

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

Two fixes for gsd-sdk binary issues:

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

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

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

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

Closes #2791

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

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:23:32 -04:00
Tom Boucher
f983c95ffc Release/1.39.0-rc.4 (#2797)
* chore: bump version to 1.39.0 for release

* chore: bump to 1.39.0-rc.1

* chore: bump to 1.39.0-rc.2

* chore: bump to 1.39.0-rc.3

* chore: bump to 1.39.0-rc.4

* docs: add v1.39.0-rc.4 release notes

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 22:32:14 -04:00
Lex Christopherson
f3685d9173 1.38.5 2026-04-25 17:56:06 -06:00
Lex Christopherson
a5a2d44121 1.38.4 2026-04-25 14:13:49 -06:00
Lex Christopherson
73abae60f0 1.38.3 2026-04-25 14:11:46 -06:00
Tom Boucher
aeef87de7f docs(test-standards): enforce no-source-grep rule with CI linter + CONTRIBUTING.md (#2700)
* docs(test-standards): enforce no-source-grep rule with CI linter + update CONTRIBUTING.md

Adds scripts/lint-no-source-grep.cjs — a static linter that detects readFileSync
on .cjs source files in tests without an allow-test-rule annotation. Wires it
into CI as a new lint-tests job in test.yml and as npm run lint:tests.

Resolves all 9 existing violations across the test suite:
- Rewrites workspace routing tests (3) as behavioral runGsdTools calls that
  verify each command is router-recognized (exit != "Unknown init workflow")
- Adds allow-test-rule annotations with explanatory comments to 7 legitimate
  structural tests: architectural invariants (locking, orphan-worktree),
  structural regression guards (milestone-regex-global), docs-parity
  (config-field-docs), integration-test-input (copilot-install), and
  structural-implementation-guards (bug-1891, discuss-mode)

Updates CONTRIBUTING.md Testing Standards section with:
- "Prohibited: Source-Grep Tests" section with the before/after pattern,
  root cause analysis of why it breaks (commit 990c3e64), and CI reference
- allow-test-rule exemption table (6 recognized categories with when-to-use)
- "CI Test Quality Checks" table showing lint-tests job and local run command

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

* fix: resolve CodeRabbit findings on PR #2700

- CONTRIBUTING.md: "four recognized categories" → "six" (table has 6 rows)
- workspace.test.cjs: use positional args in routing tests (no --name flag)
- lint-no-source-grep.cjs: add source-dir guard to READ_WITH_INLINE_CJS_RE
  (mirrors CJS_PATH_CONST_RE's protection against false positives on temp files)

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

* fix(lint): tighten allow-test-rule and add recursive test discovery

- ALLOW_ANNOTATION now requires at least one non-whitespace char after the
  colon so bare '// allow-test-rule:' cannot bypass the lint gate
- findTestFiles() recurses into subdirectories so nested *.test.cjs files
  are covered if the tests/ tree ever grows subdirs

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:34:55 -04:00
Jeremy McSpadden
0a049149e1 fix(sdk): decouple from build-from-source install, close #2441 #2453 (#2457)
* fix(sdk): decouple SDK from build-from-source install path, close #2441 and #2453

Ship sdk/dist prebuilt in the tarball and replace the npm-install-g
sub-install with a parent-package bin shim (bin/gsd-sdk.js). npm chmods
bin entries from a packed tarball correctly, eliminating the mode-644
failure (#2453) and the full class of NPM_CONFIG_PREFIX/ignore-scripts/
corepack/air-gapped failure modes that caused #2439 and #2441.

Changes:
- sdk/package.json: prepublishOnly runs `rm -rf dist && tsc && chmod +x
  dist/cli.js` (stale-build guard + execute-bit fix at publish time)
- package.json: add "gsd-sdk": "bin/gsd-sdk.js" bin entry; add sdk/dist
  to files so the prebuilt CLI ships in the tarball
- bin/gsd-sdk.js: new back-compat shim — resolves sdk/dist/cli.js relative
  to the package root and delegates via `node`, so all existing PATH call
  sites (slash commands, agents, hooks) continue to work unchanged (S1 shim)
- bin/install.js: replace installSdkIfNeeded() build-from-source + global-
  install dance with a dist-verify + chmod-in-place guard; delete
  resolveGsdSdk(), detectShellRc(), emitSdkFatal() helpers now unused
- .github/workflows/install-smoke.yml: add smoke-unpacked job that strips
  execute bit from sdk/dist/cli.js before install to reproduce the exact
  #2453 failure mode
- tests/bug-2441-sdk-decouple.test.cjs: new regression tests asserting all
  invariants (no npm install -g from sdk/, shim exists, sdk/dist in files,
  prepublishOnly has rm -rf + chmod)
- tests/bugs-1656-1657.test.cjs: update stale assertions that required
  build-from-source behavior (now asserts new prebuilt-dist invariants)

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

* chore(release): bump to 1.38.2, wire release.yml to build SDK dist

- Bump version 1.38.1 -> 1.38.2 for the #2441/#2453 fix shipped in 0f6903d.
- Add `build:sdk` script (`cd sdk && npm ci && npm run build`).
- `prepublishOnly` now runs hooks + SDK builds as a safety net.
- release.yml (rc + finalize): build SDK dist before `npm publish` so the
  published tarball always ships fresh `sdk/dist/` (kept gitignored).
- CHANGELOG: document 1.38.2 entry and `--sdk` flag semantics change.

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

* ci: build SDK dist before tests and smoke jobs

sdk/dist/ is gitignored (built fresh at publish time via release.yml),
but both the test suite and install-smoke jobs run `bin/install.js`
or `npm pack` against the checked-out tree where dist doesn't exist yet.

- test.yml: `npm run build:sdk` before `npm run test:coverage`, so tests
  that spawn `bin/install.js` don't hit `installSdkIfNeeded()`'s fatal
  missing-dist check.
- install-smoke.yml (both smoke and smoke-unpacked): build SDK before
  pack/chmod so the published tarball contains dist and the unpacked
  install has a file to strip exec-bit from.

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

* fix(sdk): lift SDK runtime deps to parent so tarball install can resolve them

The SDK's runtime deps (ws, @anthropic-ai/claude-agent-sdk) live in
sdk/package.json, but sdk/node_modules is NOT shipped in the parent
tarball — only sdk/dist, sdk/src, sdk/prompts, and sdk/package.json are.
When a user runs `npm install -g get-shit-done-cc`, npm installs the
parent's node_modules but never runs `npm install` inside the nested
sdk/ directory.

Result: `node sdk/dist/cli.js` fails with ERR_MODULE_NOT_FOUND for 'ws'.
The smoke tarball job caught this; the unpacked variant masked it
because `npm install -g <dir>` copies the entire workspace including
sdk/node_modules (left over from `npm run build:sdk`).

Fix: declare the same deps in the parent package.json so they land in
<pkg>/node_modules, which Node's resolution walks up to from
<pkg>/sdk/dist/cli.js. Keep them declared in sdk/package.json too so
the SDK remains a self-contained package for standalone dev.

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

* fix(lockfile): regenerate package-lock.json cleanly

The previous `npm install` run left the lockfile internally inconsistent
(resolved esbuild@0.27.7 referenced but not fully written), causing
`npm ci` to fail in CI with "Missing from lock file" errors.

Clean regen via rm + npm install fixes all three failed jobs
(test, smoke, smoke-unpacked), which were all hitting the same
`npm ci` sync check.

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

* fix(deps): remove unused esbuild + vitest from root devDependencies

Both were declared but never imported anywhere in the root package
(confirmed via grep of bin/, scripts/, tests/). They lived in sdk/
already, which is the only place they're actually used.

The transitive tree they pulled in (vitest → vite → esbuild 0.28 →
@esbuild/openharmony-arm64) was the root of the CI npm ci failures:
the openharmony platform package's `optional: true` flag was not being
applied correctly by npm 10 on Linux runners, causing EBADPLATFORM.

After removal: 800+ transitive packages → 155. Lockfile regenerated
cleanly. All 4170 tests pass.

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

* fix(sdk): pretest:coverage builds sdk; tighten shim test assertions

Add "pretest:coverage": "npm run build:sdk" so npm run test:coverage
works in clean checkouts where sdk/dist/ hasn't been built yet.

Tighten the two loose shim assertions in bug-2441-sdk-decouple.test.cjs:
- forwards-to test now asserts path.resolve() is called with the
  'sdk','dist','cli.js' path segments, not just substring presence
- node-invocation test now asserts spawnSync(process.execPath, [...])
  pattern, ruling out matches in comments or the shebang line

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

* fix: address PR review — pretest:coverage + tighten shim tests

Review feedback from trek-e on PR 2457:

1. pretest:coverage + pretest hooks now run `npm run build:sdk` so
   `npm run test[:coverage]` in a clean checkout produces the required
   sdk/dist/ artifacts before running the installer-dependent tests.
   CI already does this explicitly; local contributors benefit.

2. Shim tests in bug-2441-sdk-decouple.test.cjs tightened from loose
   substring matches (which would pass on comments/shebangs alone) to
   regex assertions on the actual path.resolve call, spawnSync with
   process.execPath, process.argv.slice(2), and process.exit pattern.
   These now provide real regression protection for #2453-class bugs.

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

* fix: correct CHANGELOG entry and add [1.38.2] reference link

Two issues in the 1.38.2 CHANGELOG entry:
- installSdkIfNeeded() was described as deleted but it still exists in
  bin/install.js (repurposed to verify sdk/dist/cli.js and fix execute bit).
  Corrected the description to say 'repurposes' rather than 'deletes'.
- The reference-link block at the bottom of the file was missing a [1.38.2]
  compare URL and [Unreleased] still pointed to v1.37.1...HEAD. Added the
  [1.38.2] link and updated [Unreleased] to compare/v1.38.2...HEAD.

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

* fix(sdk): double-cast WorkflowConfig to Record for strict tsc build

TypeScript error on main (introduced in #2611) blocks `npm run build`
in sdk/, which now runs as part of this PR's tarball build path. Apply
the double-cast via `unknown` as the compiler suggests.

Same fix as #2622; can be dropped if that lands first.

* test: remove bug-2598 test obsoleted by SDK decoupling

The bug-2598 test guards the Windows CVE-2024-27980 fix in the old
build-from-source path (npm spawnSync with shell:true + formatSpawnFailure
diagnostics). This PR removes that entire code path — installSdkIfNeeded
no longer spawns npm, it just verifies the prebuilt sdk/dist/cli.js
shipped in the tarball.

The test asserts `installSdkIfNeeded.toString()` contains a
formatSpawnFailure helper. After decoupling, no such helper exists
(nothing to format — there's no spawn). Keeping the test would assert
invariants of the rejected architecture.

The original #2598 defect (silent failure of npm spawn on Windows) is
structurally impossible in the shim path: bin/gsd-sdk.js invokes
`node sdk/dist/cli.js` directly via child_process.spawn with an
explicit argv array. No .cmd wrapper, no shell delegation.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-04-23 08:36:03 -04:00