Commit Graph

4780 Commits

Author SHA1 Message Date
Tom Boucher
172e6920eb fix(3670): break migration lock self-deadlock on Windows (#3765)
* test(installer): add regression tests for #3670 migration lock self-deadlock

- T1: same-process PID re-entry reclamation (primary regression)
- T2: dead-PID stale lock reclamation
- T3: unlinkSync EPERM surfaces (not silently swallowed via force:true)
- T4: counter-test — normal round-trip still works
- T5: counter-test — genuinely-held live lock still errors clearly
- Update existing 'reports lock release failures' test to mock
  fs.unlinkSync (not fs.rmSync) matching the fixed release path

Windows wall-clock deadlock repro depends on Docker matrix Windows runners.

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

* fix(installer): break migration lock self-deadlock on Windows (#3670)

Root cause: `acquireInstallMigrationLock` release closure called
`fs.rmSync(lockPath, { force: true })`. On Windows NTFS, a file
recently closed via `closeSync(fd)` may still return EPERM from
`unlink` until the OS fully releases the handle. The `force: true`
flag silently swallows EPERM, leaving the lock file on disk. The
subsequent `runInstallerMigrations` call in the same install()
invocation hits EEXIST, spins for 30 s, then throws
"installer migration lock is held".

Fix:
1. Release closure uses `fs.unlinkSync` (not rmSync+force) so
   EPERM propagates via releaseError instead of being swallowed.
2. `acquireInstallMigrationLock` closes the fd before writing the
   payload (path-based write), eliminating the open handle that
   caused the deferred EPERM on Windows.
3. Stale-lock reclamation: on EEXIST, parse the on-disk PID and
   reclaim immediately if it matches process.pid (same-process
   re-entry, the primary #3670 failure mode) or if the PID is
   dead (ESRCH). Live alien PIDs still trigger the 30 s timeout.
4. Error message on a genuinely-held lock now includes the holder
   PID and acquiredAt timestamp for operator diagnostics.

No public API change. All callers of runInstallerMigrations are
inside installer-migrations.cjs and bin/install.js.

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

* chore(3670): update changeset to reference PR #3765

* fix(3670): timeout on reclaim-unlink failure to prevent spin-loop regression

When unlinkSync throws (e.g. Windows EPERM on an open handle) in the
same-PID / dead-PID reclamation path, the original code continued
unconditionally — bypassing the timeout check and reintroducing the
exact deadlock the PR is supposed to fix.

Guard the continue behind a `reclaimed` flag: only loop back to
openSync if unlink SUCCEEDED. On failure, fall through to the existing
bounded sleep + timeout, which surfaces "installer migration lock is
held" within lockTimeoutMs instead of spinning indefinitely.

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

* test(3670): tighten T5 — assert bounded failure when reclaim unlink fails on live lock

The old T5 accepted BOTH success and throw, which allowed over-reclamation
of a genuinely un-reclaimable lock to pass undetected.

Rewrite T5 to force deterministically unreclaimable conditions:
- Pre-seed lock with process.pid (triggers isSameProcess path)
- Mock fs.unlinkSync via mock.method() to throw EPERM for the lock file

With the production fix: reclaimed=false → falls through to timeout →
throws "installer migration lock is held" within ~200ms.

Without the production fix: unlink throws but continue runs anyway →
process spins and eventually OOMs (confirmed RED: 136s runtime, V8 heap
exhaustion from infinite readLockFile + new Error() allocations).

assert.throws() now makes success a hard failure, closing the
over-reclamation gap.

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

* fix(3670): clean up orphan lock file when writeFileSync fails after closeSync

If closeSync(fd) succeeds (fd=null) but the subsequent writeFileSync
throws, the empty lock file was left on disk. readLockFile returns null
for an empty/invalid-JSON file, so the stale-lock reclamation path
skips it, causing the next acquire attempt to spin to timeout.

Track ownership with lockCreatedByUs flag; add a second cleanup branch
in the catch block for the fd-already-closed case.

Also fix changeset body to use the bold-prefix format required by all
other fragments in .changeset/.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:12:08 -04:00
Tom Boucher
49dcabff26 fix(3749): port strategy-branch switching to SDK commit handler (#3763)
* test(3749): add RED test — SDK commit handler missing strategy-branch port

Structural assertions on sdk/src/query/commit.ts verify the branching-strategy
block (phase/milestone) is present. 9 tests fail today; pass after the fix.

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

* fix(3749): port PR #1279 strategy-branch logic from CJS to SDK commit handler

sdk/src/query/commit.ts lacked the branching-strategy block that PR #1279
added to commands.cjs:285-320. Pre-execution workflow commits (discuss-phase,
plan-phase) with branching_strategy:"phase" or "milestone" were landing on
whatever branch was active rather than the configured strategy branch.

Changes:
- sdk/src/query/phase.ts: export findPhaseByNumber() so commit.ts can look up
  a phase without going through the QueryHandler dispatch stack
- sdk/src/query/commit.ts: import loadConfig, findPhaseByNumber, getMilestoneInfo;
  add ensureStrategyBranch() helper (extracts the logic, preserving SRP);
  call it between the commit_docs check and the staging step

Semantics match the CJS path exactly: best-effort (errors are swallowed so a
misconfigured branching_strategy never aborts a commit), same phase-number
extraction from file paths, same checkout -b / checkout fallback sequence,
same current-branch guard to avoid repeated checkout calls.

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

* chore(3749): update changeset to reference PR #3763

* fix(3749): validate phase_branch_template before substitution

Before this commit, a missing or empty `phase_branch_template` (or
`milestone_branch_template`) would cause `.replace()` to throw inside
the try-block, which was swallowed by the surrounding catch → silent
strategy-skip with no observable signal.

Exports `validateBranchTemplate(template)` as a pure helper that
checks the template is a non-empty string BEFORE any `.replace()` call
is attempted.  Also exports `resolveStrategyBranchName(template,
phaseNumber, phaseSlug)` which validates that no `{placeholder}` tokens
survive substitution.

Both an invalid template and an unresolved-placeholder result now return
`{ ok: false, reason: '...' }` from `ensureStrategyBranch`, surfaced
distinctly — satisfying the no-silent-skip contract (codex finding 3).

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

* test(3749): replace source-includes with typed-IR assertions on extracted helpers

The original test file used `source.includes(...)` throughout — grep
theater that verifies text presence, not behavior (codex finding 4 /
test-rigor Contract 1 violation).

This commit upgrades the test suite:

1. Typed-IR unit tests (ts-node, skipped gracefully when unavailable):
   - `parsePhasesFromFiles`: empty, single-phase, mixed-phase, dotted
     phase numbers, root numeric tokens
   - `validateBranchTemplate`: undefined, empty, whitespace, valid
   - `resolveStrategyBranchName`: well-formed, unresolved placeholder,
     slug fallback

2. Structural assertions (retained and tightened) now verify:
   - Named exports of the three helpers exist (enabling typed-IR tests)
   - Caller halts on `strategyResult.ok === false` (Finding 2)
   - Mixed-phase rejection message contains "single phase" (Finding 1)
   - Template validation precedes resolveStrategyBranchName in source
     (Finding 3, using lastIndexOf to skip helper definitions)
   - `alreadyExists` guard and `branch_switch_failed` reason present
     (Finding 2)

Old structural tests that verified implementation tokens
(loadConfig, phase_branch_template, --abbrev-ref, etc.) are
retained as they verify observable contracts, not code style.

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

* fix(3749): bug-2767 tests skip cleanly when sdk/dist is absent (was hard-fail)

The 4 behavioral tests in bug-2767-gsd-sdk-commit-files-flag.test.cjs
invoke the built SDK CLI (sdk/dist/cli.js) end-to-end. When that dist is
absent, they threw MODULE_NOT_FOUND and were reported as 4 hard failures
rather than observable skips.

Apply the same `if (!existsSync(SDK_CLI)) { t.skip(...); return; }` guard
already used in bug-3019-help-passthrough.test.cjs. When dist is present,
all 4 tests run and pass. When dist is absent, all 4 emit a ﹣ skip line
with an actionable reason ('run `cd sdk && npm run build`'), leaving 0
hard failures and maintaining full observability.

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

* fix(3749): address pr-review-toolkit + codex review findings

- Add allow-test-rule annotation to satisfy lint-no-source-grep
- Remove dead identifiers (registerScript, tsConfigPath, os import)
- Standardize issue #1278 / PR #1279 references in JSDoc
- Document root-level phase-number false-positive in parsePhasesFromFiles
- Generalize resolveStrategyBranchName parameter naming (phase + milestone reuse)
- Add behavioral integration test for commit handler with branching_strategy: phase

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

* fix(3749): normalize extracted phase token in phaseTokenMatches

parsePhasesFromFiles extracts "1" from a path like ".planning/phases/1-setup/PLAN.md".
normalizePhaseName pads this to "01" before passing it to searchPhaseInDir, which calls
phaseTokenMatches("1-setup", "01"). extractPhaseToken("1-setup") returned "1" (raw,
unpadded), so "1" !== "01" and the phase directory was not found — ensureStrategyBranch
fell through with "phase not found" and never switched the branch.

Fix: normalize the extracted token via normalizePhaseName before comparing so that "1"
and "01" both resolve to "01" and match correctly. Applied to both the primary comparison
and the project-code-prefix-stripping retry path.

Verified by bug-3749-sdk-commit-strategy-branch-integration.test.cjs passing:
  ✔ commit with --files in a phase dir switches to the strategy branch
  ✔ commit with --files outside a phase dir skips branch switch

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:11:33 -04:00
Tom Boucher
f27b10736e fix(3751): resolveAgentsDir falls back to repo-local .claude/agents (#3762)
* test(#3751): add RED test for resolveAgentsDir missing repo-local .claude/agents

Drive resolveAgentsDir() with a repo-local .claude/agents present and
~/.claude/agents absent. Structural contracts assert the function body
must reference projectDir when constructing the fallback path, and that
init-complex.ts must pass projectDir to the call site. Both fail
deterministically before the fix.

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

* fix(#3751): resolveAgentsDir() falls back to repo-local .claude/agents for --local installs

resolveAgentsDir() was probing only GSD_AGENTS_DIR or the runtime-global
config dir (~/.claude/agents for Claude). For Claude Code --local installs
where agents land in ./.claude/agents, the SDK reported agents_installed:
false even when the agent files were present.

Precedence (post-fix):
  1. GSD_AGENTS_DIR (explicit override, unchanged)
  2. <getRuntimeConfigDir(runtime)>/agents (when the dir exists)
  3. <projectDir>/.claude/agents (repo-local fallback for claude runtime)

Both init.ts:checkAgentsInstalled and init-complex.ts:initNewProject now
receive projectDir and thread it to resolveAgentsDir().

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

* chore(3751): update changeset to reference PR #3762

* fix(3751): add allow-test-rule annotation to satisfy lint-no-source-grep

The structural-contract test reads TypeScript SDK source files to verify
resolveAgentsDir() signature shape, fallback ordering, and call-site
wiring. These are legitimate SDK-seam contracts (the TS source IS the
product artifact). The allow-test-rule annotation bypasses the
lint-no-source-grep check that flags readFileSync-bound variable usage.

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

* fix(3751): repair Windows config-set concurrent-write race

withPlanningLock threw on EPERM instead of retrying, causing the losing
process to crash silently and its config write to never land.

On Windows, when two processes race to create the same lock file via
O_EXCL (writeFileSync with flag:'wx'), the OS can return EPERM instead of
EEXIST while NTFS holds an internal exclusive handle on the newly-created
file during the write. The previous catch block only recognized EEXIST as
a contention signal — any other code (including EPERM) fell through to
`throw err`. The calling test used .catch(()=>{}) to suppress process
errors, so the losing process silently exited without writing its value;
the winning process then wrote from the stale pre-race config, producing
the observed 'balanced' instead of 'quality'. Fix: port the
PLANNING_LOCK_RETRY_ERRNOS Set from main (landed in #3777) into this
branch. The set treats EPERM, EBUSY, and six other transient filesystem
codes as retry signals rather than fatal errors.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:11:05 -04:00
Tom Boucher
75e013191c fix(3726): replace racy 600ms subprocess poll with deterministic test (#3745)
* test(3726): replace racy spawn-poll with deterministic sentinel assertions

The original bug-1974 test asserted STATE.md content against a
2000ms wall-clock deadline while the hook's fire-and-forget
spawn().unref() subprocess raced to write it — causing intermittent
CI failures under Docker contention (#3726).

Fix: assert the synchronously-written warnData.criticalRecorded
sentinel (readable the moment spawnSync runHook() returns), and
invoke `gsd-tools state record-session` synchronously via spawnSync
to verify the persistence seam. Adds a counter-test for WARNING-only
fires (5 tests total). No production code changed.

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

* fix(3726): repair Windows state add-blocker concurrent-write race

acquireStateLock silently returned a false-success lockPath on any non-EEXIST
openSync error (the old guard was `if (err.code !== 'EEXIST') return lockPath`).
On Windows-24 CI, the releaseStateLock unlink briefly leaves the file in a
transitional state so the next O_CREAT|O_EXCL call from a racing process gets
EPERM or EBUSY instead of EEXIST. The old guard treated this as "lock not yet
created, you own it" and let the second process proceed with its own
read-modify-write while the first still held the actual lock — causing one
blocker to be silently overwritten.

Fix mirrors commits ab24d80b and 47983914 already on main: replace the
silent-bypass guard with an explicit ACQUIRE_LOCK_RETRY_ERRNOS allowlist
(EPERM, EBUSY, EAGAIN, EINTR, EINVAL, EIO, ENOENT, ESTALE) that retries on
transient filesystem conditions, and throw on all other non-EEXIST codes rather
than returning a false-success lockPath. The same retry set is applied to
withPlanningLock in planning-workspace.cjs (shared root cause, same fix).
Local test run: 10/10 pass (locking-bugs-1909-1916-1925-1927.test.cjs).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:10:32 -04:00
Tom Boucher
26773bd669 fix(3735): add surface to PROFILES.core (restore ADR-0011 expand contract) (#3744)
* test(3735): add failing test that PROFILES.core includes surface

Regression test asserting that resolveProfile({ modes: ['core'] }) includes
'surface' in its transitive closure — the ADR-0011 contract that --profile=core
users can expand via /gsd:surface enable <cluster>. Also updates stale
hardcoded skill-count assertions (7→8) across install-minimal*.test.cjs and
install-profiles-resolve.test.cjs to reflect the correct post-fix baseline.

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

* fix(3735): add 'surface' to PROFILES.core to restore ADR-0011 expand contract

PROFILES.core omitted 'surface', silently breaking the documented contract
in ADR-0011 that --profile=core users can expand their skill surface via
/gsd:surface enable <cluster>. The sub-command is only available if surface.md
is staged — which requires it to appear in the resolved set for the core profile.

Added 'surface' to both PROFILES.core and PROFILES.standard (standard is a
documented superset of core; omitting it from standard would break the
"standard must include all core skills" invariant and the resolveProfile tests).

The MINIMAL_SKILL_ALLOWLIST back-compat shim is derived from PROFILES.core so
it picks up surface automatically, ensuring --minimal and --core-only installs
also stage surface.md.

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

* chore(3735): update changeset to reference PR #3744

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

* docs(install-profiles): fix stale 'six skills' count in module header comment

After #3735 added surface to PROFILES.core the skill count became eight,
but the module-level comment still said "six skills covering the main project
loop". Update the description to reflect the correct count and reference the
ADR-0011 expand contract that surface fulfils.

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

---------

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

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

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

Fixes #3727

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

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

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

Fixes #3727

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

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

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

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

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:09:42 -04:00
Tom Boucher
e690f1bc90 fix(3718): shell-free Node.js UI safety gate — fixes PowerShell silent-fail (#3718)
* fix(workflows): add word-boundary anchoring to UI safety gate grep

Replace unanchored grep -iE "UI |..." alternation with POSIX ERE
word-boundary-anchored form:

  LC_ALL=C grep -iE "(^|[^[:alnum:]])(UI|...)([^[:alnum:]]|$)"

Unanchored form matched 'ui' inside 'requirements', 'view' inside
'overview' and 'review', 'form' inside 'performance'/'platform'/
'transform' — producing HAS_UI=0 on 100% of standard roadmap phases
(every phase contains a **Requirements**: field).

Fix applied to both plan-phase.md:625 and autonomous.md:284.
LC_ALL=C added for POSIX locale portability on both BSD and GNU grep.

Closes #3706

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

* test(workflows): add regression tests for UI safety gate false-positives (#3706)

- bug-3706-ui-safety-gate-false-positives.test.cjs: 36-test suite covering
  both plan-phase.md and autonomous.md gate behavior; verifies that
  Requirements/overview/performance/platform/transform/review/build/screening
  do NOT trigger the gate, while standalone UI/view/form/screen/dashboard/
  component/lowercase-ui/hyphenated-non-UI DO trigger it.

- autonomous-ui-steps.test.cjs: update stale assertion that checked for the
  old broken grep pattern; now asserts the word-boundary-anchored form.

Test strategy: extract the POSIX ERE pattern from the workflow file and
simulate grep match semantics in JS (no shell exec, no source-grep).

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

* chore(changeset): add Fixed fragment for PR #3718 (UI safety gate false-positives)

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

* fix(workflows): document compound-token boundary contract; add comment to gate

Addresses adversarial review finding: word-boundary anchoring intentionally
does not match tokens embedded in compound alphanumeric words (e.g.
"microfrontend", "dashboardWidget", "uiSpec"). This is correct behavior —
gsd-roadmapper generates natural English prose, not camelCase compounds.
Hyphenated forms ("micro-frontend") and spaced forms are caught by the
anchored pattern (hyphen is [^[:alnum:]]).

Add inline comment in both workflow files explaining the pattern intent,
the false-positive prevention, and the compound-word contract.

Add 4 tests (2 per workflow) documenting the compound-word contract:
- "microfrontend" (compound) must NOT trigger gate (documented behavior)
- "micro-frontend" (hyphenated) MUST trigger gate (correct true-positive)

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

* fix(3718): replace shell grep gate with shell-free Node.js helper

Moves UI safety gate logic from `LC_ALL=C grep -iE` (silently broken on
Windows PowerShell — locale env-var prefix not recognised by pwsh) to
`bin/lib/ui-safety-gate.cjs` (Node.js, reads via stdin to avoid ARG_MAX).

Path is anchored via `git rev-parse --show-toplevel` (GSD_REPO_ROOT) to
avoid CWD-sensitive failure when Claude Code executes from a subdirectory.

Word-boundary regex is identical to the original POSIX ERE pattern:
  (^|[^a-zA-Z0-9])(TOKEN)([^a-zA-Z0-9]|$)
Exit codes mirror grep: 0 = UI found, 1 = not found.

Tests: 51/51 pass — includes spawnSync shell:false + stdin cross-shell
portability tests and ARG_MAX large-input test.

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

* fix(3706): address pr-review-toolkit + codex review findings

- Multi-token-per-line: use matchAll to capture all UI tokens
- Add test for multiple distinct tokens on same line
- Clarify ASCII vs POSIX [:alnum:] in word-boundary comment
- Correct misleading "path anchored" comment in plan-phase/autonomous workflows
- Remove UI_GATE_PATTERN from module.exports (internal implementation detail)

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

* fix(state): restore ACQUIRE_LOCK_RETRY_ERRNOS in acquireStateLock (#3718)

Commit 473c279c removed ACQUIRE_LOCK_RETRY_ERRNOS and replaced the
correct `throw err` path with `return lockPath`, which silently
"succeeds" on any non-EEXIST error — allowing two concurrent processes
to both hold the lock simultaneously and causing lost updates.

This restores the set of recoverable transient errno codes (Docker
overlay-fs EINVAL/EIO/ENOENT, NFS ESTALE, POSIX EAGAIN/EINTR, Windows
EPERM/EBUSY) that should retry, and restores `throw err` for genuinely
fatal codes.  Equivalent to commit 47983914 on main.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:08:56 -04:00
Tom Boucher
58643ff70b fix(sdk): treat empty lock file as live in isLockProcessDead
Between `open(lockPath, O_CREAT | O_EXCL)` and `fd.writeFile(pid)` there
is an async await gap.  If a second process reads the lock file during that
window it sees empty content, which parseInt returns as NaN.  The previous
code returned `true` (dead) for non-finite PID values, causing the second
process to unlink the live lock and steal it — both processes then entered
readModifyWriteStateMd simultaneously, producing a lost-update TOCTOU.

Fix: return `null` (unknown) instead of `true` when the lock file
contains no parseable PID.  The caller already treats `null` as "not
confirmed dead" and falls through to the normal retry + timeout path,
giving the first process time to finish writing its PID.

The CJS acquireStateLock in state.cjs never had this race because it uses
synchronous fs.openSync / fs.writeSync / fs.closeSync with no async gap.

Fixes intermittent failure of:
  #1925 TOCTOU: state commands use readModifyWriteStateMd
  → state add-blocker: both concurrent calls append different blockers
(observed: macos-latest / Node 22 and windows-latest / Node 22 on main)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 22:40:02 -04:00
Tom Boucher
c1c8b0d109 fix(3739): gap-checker now detects padded-prefix CONTEXT.md (#3764)
* test(3739): add RED tests for padded-prefix CONTEXT.md gap-checker miss

Covers bare and padded (01-CONTEXT.md, 02.1-CONTEXT.md) forms, an
uncovered-decision counter-test, and unit tests for the upcoming
findContextMdIn() helper. All 6 new tests fail before the fix.

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

* fix(3739): extract findContextMdIn() helper; fix gap-checker bare lookup

gap-checker.cjs:136 used a bare path.join(absPhaseDir, 'CONTEXT.md')
that silently returned '' for any phase using the padded-prefix
convention (01-CONTEXT.md, 02.1-CONTEXT.md, etc.).

Extract findContextMdIn(absDir) to planning-workspace.cjs — the module
already imported by gap-checker, init, roadmap, and core — and wire
gap-checker.cjs to call it instead of the bare lookup.

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

* refactor(3739): replace inline dual-form predicate with findContextMdIn() at all 4 remaining sites

The dual-form predicate `f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'`
existed verbatim at 5 sites across init.cjs (×3), roadmap.cjs, and
core.cjs — Rule of Three mandates extraction at ≥3 sites. All 4
remaining call sites now delegate to findContextMdIn() from
planning-workspace.cjs. No behaviour change; all existing tests pass.

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

* chore(3739): update changeset to reference PR #3764

* fix(3739): findContextMdIn prefers bare CONTEXT.md over padded form (deterministic precedence)

`.find()` with `f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'` returned
the first match in `readdirSync` order — undefined on most filesystems.
When both `CONTEXT.md` and `01-CONTEXT.md` exist the winner was arbitrary.

The old gap-checker.cjs:136 code always used bare `CONTEXT.md` first
(existsSync on the bare path). Restore that invariant: check
`files.includes('CONTEXT.md')` before falling through to the padded scan.

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

* test(3739): dual-file precedence test — bare CONTEXT.md wins over padded form

Adds two test cases for the scenario where both CONTEXT.md and
01-CONTEXT.md exist in the same phase directory:

1. Helper level: findContextMdIn() must return 'CONTEXT.md' (not the
   padded filename) when both files are present on disk.
2. Integration level: gap-analysis must resolve decisions from the bare
   form only; D-PADDED (from 01-CONTEXT.md) must not appear when the
   bare form shadows it.

Without these tests a future change to findContextMdIn could silently
regress the precedence guarantee.

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

* fix(3739): bug-2798 tests skip cleanly when sdk/dist is absent (was hard-fail)

The 3 tests in bug-2798-context-window-config-key.test.cjs invoke the built
SDK CLI (sdk/dist/cli.js) and require sdk/dist/query/config-schema.js. When
dist is absent, they threw hard errors rather than observable skips.

Apply the same `if (!existsSync(...)) { t.skip(...); return; }` guard used in
bug-2767-gsd-sdk-commit-files-flag.test.cjs (c2812313). Tests 1 & 2 guard on
sdk/dist/cli.js; test 3 guards on sdk/dist/query/config-schema.js. When dist
is present all 3 run; when absent all 3 emit actionable skip lines.

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

* fix(3739): eliminate double readdirSync in findContextMdIn callers

findContextMdIn now accepts either a directory path or an already-read
files array, allowing callers that already hold a directory listing
(core.cjs:getPhaseFileStats, roadmap.cjs:countPhasePlansAndSummaries,
gap-checker.cjs:runGapAnalysis) to skip redundant readdirSync calls.

Test coverage added for the array-argument overload.

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

* test: redesign flaky concurrent add-blocker test with deterministic barrier

Previous design relied on OS scheduler to interleave two subprocess writes,
producing a flake under CI load. Redesigned using a file-barrier (Option A)
that forces both subprocesses to reach a ready-gate before either proceeds,
guaranteeing true concurrent lock contention and eliminating timing dependency.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 22:39:34 -04:00
Tom Boucher
556c1c0492 fix(3184): address pr-review-toolkit + codex review findings
- Export isPhaseUatPassed and supporting types/enums from SDK public surface
- Remove /m flag from frontmatter regex to prevent mid-body strip
- Implement reasonsHuman population with per-ReasonCode humanizer
- Add test for multiple UAT files in same phase
- Add test for INVALID_PHASE_NUM error path
- Add test for workstream routing
- Eliminate redundant stripMarkdownInjection call
- Update stale cycle-number comments

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:47:56 -04:00
Tom Boucher
aab01f43e9 refactor(tests): consolidate Phase Lifecycle Module — 20 files → 4 (#3741)
* 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>

* refactor(tests): consolidate Phase Lifecycle Module — 20 files → 4

- Merge 10 CJS bug-fix test files into tests/phase.test.cjs
- Merge sdk/src/phase-runner-types.test.ts + sdk/src/phase-prompt.test.ts
  into sdk/src/phase-runner.test.ts (97 → 152 tests, 0 failures)
- Rename 4 mis-attributed test files out of phase cluster:
    phase-researcher-app-aware    → gsd-researcher-app-aware
    phase-researcher-flow-diagram → gsd-researcher-flow-diagram
    feat-3023-phase-type-models   → feat-3023-model-phase-types
    phase-6-cjs-sdk-seam-contracts → cjs-sdk-bridge-seam-contracts
- Fix phasePlanIndex (#3430): non-canonical plan filename warning now
  surfaces in data.warnings[] as "Ignored noncanonical plan files: ..."
  instead of a separate singular data.warning field
- Update allowlist: phase ceiling 20 → 4 (closes #3740)
- Add Phase Lifecycle Module glossary entry to CONTEXT.md

Closes #3740

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

* fix(phase-roadmap-mutation): replaceInCurrentMilestone handles active milestone inside <details> block

When the active milestone is wrapped in a <details> block (e.g. user
collapsed it, or milestone transition), the after-</details> slice is
empty or contains only footer text — the pattern never matches and the
replacement is silently dropped.

Fix: when after.replace() produces no change, fall back to replacing
inside the last <details>...</details> block. Shipped-milestone blocks
are untouched because only the last <details> block is targeted.

Closes #2641

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

* fix(changeset): add required type/pr frontmatter to 3740 fragment

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:57:06 -04:00
Tom Boucher
4f56c3b10b refactor(tests): consolidate Worktree Module — 13 files → 3 (#3752)
* refactor(tests): consolidate Worktree Module — 13 files → 2

Closes #3742

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

* fix(changeset): correct frontmatter format for 3742 fragment

type:/pr: fields required by docs-lint; replaces @changesets/cli
package-bump format with the repo's custom fragment schema.

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

* refactor(tests): split consolidated worktree.test.cjs along cleanup seam (≤ 800 LOC/file)

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

* chore(changeset): update 3742 fragment — 13→3 files, ≤800 LOC/file

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

* chore(changeset): fix pr reference 3738→3752 in 3742 fragment

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:56:26 -04:00
Tom Boucher
c4e39bc23a refactor(tests): consolidate graphify Module — 7 files → 1 (#3769)
* refactor(tests): consolidate graphify Module — 7 files → 1

Closes #3761

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

* refactor(tests): split graphify.test.cjs along describe-block seams — keep files ≤ 800 LOC

- tests/graphify.test.cjs (653 LOC): status + build
- tests/graphify-query.test.cjs (447 LOC): query
- tests/graphify-visualization.test.cjs (577 LOC): staleness + mvp-viz + regressions
- tests/graphify-auto-update.test.cjs (625 LOC): auto-update hook
- tests/helpers/graphify.cjs (112 LOC): shared helpers extracted

Total: 132 tests, 0 failures. Refs #3761.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:39:17 -04:00
Tom Boucher
1926353b5e refactor(tests): consolidate Runtime Artifact Layout Module — 12 files → 3 (#3759)
* refactor(tests): consolidate Runtime Artifact Layout Module — 12 files → 3

Closes #3757

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

* fix(changeset): add proper frontmatter to 3757 fragment

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:39:09 -04:00
Tom Boucher
3d52f5ee46 refactor(tests): consolidate Init Command Module — 7 files → 5 (#3756)
* fix(3687): update insert-phase docs and roadmapper to use --insert flag

Updates stale references in insert-phase.md workflow and
gsd-roadmapper.md agent to use the consolidated /gsd:phase --insert
command syntax instead of the retired /gsd-insert-phase and
/gsd:phase insert forms.

Closes #3687

* refactor(tests): consolidate Init Command Module — 7 files → 5

Closes #3755

Merges `tests/init-manager-deps.test.cjs` (#2267 regression) into
`tests/init-manager.test.cjs` (718 LOC), and
`sdk/src/query/init-progress-precedence.test.ts` (#2674 regression)
into `sdk/src/query/init-complex.test.ts` (788 LOC).

The 800 LOC ceiling prevents further consolidation:
- `tests/init.test.cjs` is pre-existing at 1630 LOC
- `sdk/src/query/init.test.ts` is at 791 LOC
- `sdk/src/query/init-workstream-milestone-op.test.ts` is a distinct
  seam testing initMilestoneOp, roadmapAnalyze, and
  resolveQueryRuntimeContext workstream resolution.

Also adds Init Command Module Glossary entry to CONTEXT.md.

Allowlist update deferred to rebase after #3738 merges (allowlist
file does not exist on origin/main).

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

* fix(init): initExecutePhase preserves same-milestone archived phase dir (#3469)

When `phases clear` archives current-milestone phases into
`.planning/milestones/<version>-phases/` but the workflow is still on
that same milestone, `shouldDropArchivedPhaseMatch` was unconditionally
dropping the archived dir match. This caused `phase_dir: null` when the
phase was still executing in the current milestone.

Fix: detect when `phaseInfo.archived === currentMilestone` (read from
STATE.md) and skip the drop. The #2391 regression guard is safe because
that scenario involves archived.version != current milestone.

Also corrects two tests in `initRemoveWorkspace` to expect thrown
GSDError instead of `{ data: { error } }` — the production code was
intentionally changed to throw for CLI non-zero exit propagation.

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

---------

Co-authored-by: arya rizky <aryarizkyardhipratama@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:39:01 -04:00
Tom Boucher
9f57f7488e refactor(tests): consolidate Milestone Module — 10 files → 4 (#3754)
* refactor(tests): consolidate Milestone Module — 10 files → 4

Closes #3753

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

* fix(changeset): correct fragment format for 3753-consolidate-milestone-tests

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

* fix(changeset): add required frontmatter to 3753 changeset fragment

Adds `type: Fixed` and `pr: 3753` frontmatter so docs-lint passes.

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

* fix(tests): bug-2943 execFileSync timeout 5s→15s for Windows starvation

PR #3754's milestone consolidation reshuffles run-tests.cjs chunk
composition. On Windows/Node 22 under --test-concurrency=4, bug-2943
subprocesses now share concurrency slots with bug-2760-codex-install
subtests (8–15s each), starving past the 5000ms timeout. 15s covers
the observed 13.5s worst case with headroom.

Refs #3753

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:38:53 -04:00
Tom Boucher
f955bf872c refactor(tests): consolidate Installer Module — 11 files → 2 (#3760)
* 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>

* refactor(tests): consolidate Installer Module — 11 files → 2

Closes #3758

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

* fix(changeset): replace invalid type 'Chore' with 'Changed' — ALLOWED_TYPES only accepts Added|Changed|Deprecated|Removed|Fixed|Security

Fragment already had a docs-exempt marker; only the type value was wrong.

Refs #3758

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

* refactor(tests): split install.test.cjs along section seams — keep files ≤ 800 LOC

1828-LOC monolith split into 3 files by semantic boundary:
  install.test.cjs (602 LOC) — S1-5: dir resolution, install/uninstall spot-checks, Kilo
  install-runtime-artifacts.test.cjs (347 LOC) — S6-8+12: layout loop, Contract 6, legacy migrations
  install-minimal-hooks.test.cjs (782 LOC) — S9-11+13: profiles, minimal E2E, hooks copy

Shared constants and helpers extracted to tests/helpers/install-shared.cjs (216 LOC).
Total test count unchanged: 210 tests (68+37+105).

Refs #3758

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

* fix(tests): ratchet state test-file ceiling to 9 — actual count at baseline

The allowlist was initialised with state=8 but the repo already had 9 files
matching the 'state' prefix at the time the lint rule was created, causing
lint-test-file-count.test.cjs to fail on the real repo immediately.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:38:30 -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
479839147e fix(state): acquireStateLock retries on transient Docker/NFS errno codes (#3777)
* fix(state): acquireStateLock retries on transient Docker/NFS errno codes

Expands retry allowlist to ENOENT/EINVAL/EIO/ESTALE/EAGAIN/EINTR in addition
to existing EPERM/EBUSY. Truly fatal codes (EMFILE/ENOSPC/EROFS/EACCES) still
throw. Resolves the locking-bugs regression seen across all 8 open
consolidation PRs (#3738, #3741, #3752, #3754, #3756, #3759, #3760, #3769).

Closes #3776

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

* chore(changeset): update pr number to 3777 in retry-allowlist fragment

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 15:56:22 -04:00
Tom Boucher
ca2644a71a fix(worktree): unlock-retry on locked cleanup + startup orphan sweep (#3707) (#3719)
* fix(worktree): unlock-retry on locked cleanup + startup orphan sweep (#3707)

Two root causes fixed:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

All 27 local tests + Docker (holodeck) green.

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

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

Two fixes from codex adversarial review of PR 3718:

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

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

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

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

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

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

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

## Fixes applied

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

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

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

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

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

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 15:22:12 -04:00
Tom Boucher
ab24d80b68 fix(state): acquireStateLock throws on non-EEXIST openSync errors (#3773)
* fix(state): acquireStateLock throws on non-EEXIST openSync errors

Closes #3772

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

* chore(changeset): update pr reference to #3773

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

* fix(state): retry on EPERM/EBUSY in acquireStateLock and withPlanningLock

Resolves state update TOCTOU failure on macos-22 and config-set
concurrency failure on windows-24.
Root cause: openSync(O_CREAT|O_EXCL) can return EPERM or EBUSY
transiently on some CI OS+AV combinations when the lock file is
briefly held open by the deleting process; the new throw-on-non-EEXIST
guard from #3772 propagated these transient errors, killing child
processes and causing lost updates in the concurrency tests.
Fix: guard EPERM/EBUSY with an explicit continue before the
throw-on-non-EEXIST line in both acquireStateLock and withPlanningLock;
the C1 source-audit test still passes because the throw pattern is
preserved for all other non-EEXIST codes.

Refs #3772

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 13:59:20 -04:00
Tom Boucher
e5461ad4e2 refactor(tests): consolidate Dispatch Pipeline Module — 7 files → 1 (#3733)
Consolidates 7 sibling test files for sdk/src/query/query-dispatch.ts and
its stage handlers into a single query-dispatch.test.ts with 8 describe
blocks (699 LOC, 53 tests). Deletes 3 clean shim sources with zero non-test
importers. Leaves query-dispatch-formatting.ts and query-dispatch-error-mapper.ts
in place (non-test importer: query-fallback-executor.ts — deferred to #3732).

- query-dispatch-input-validation.ts deleted (clean shim)
- query-dispatch-plan.ts deleted (clean shim)
- query-dispatch-result-builder.ts deleted (clean shim)
- 6 per-stage test files deleted (consolidated into query-dispatch.test.ts)
- counter-tests added per Contract 6 for each stage field
- CONTEXT.md Glossary updated with Dispatch Pipeline Module entry

Closes #3731

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:49:04 -04:00
Tom Boucher
473c279c23 fix(state): acquireStateLock must not unlink live locks on retry exhaustion (#3714) (#3717)
PR #3711 fixed a Windows phantom-lock bug by making the last-retry
path unconditionally unlink the existing lock and re-acquire. That
closed one hole but opened another: a slow-but-live writer (CI under
c8 coverage instrumentation easily exceeds the old 2.25 s budget)
would have its lock nuked by a second writer, both would read the
same starting STATE.md, and the second write would clobber the
first append.

Replace the bounded retry loop with a deadline-driven loop that only
unlinks a lock we did not place when its mtime exceeds a 10 s
staleness threshold (crashed holder), with a 30 s wait ceiling above
that threshold so a genuinely stuck holder still gets recovered.

Locking-bugs regression suite: 10/10 pass, including 7 out of 8 stress
runs (1 transient OS-load failure; not reproducible on re-run).

Fixes #3714

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:00:44 -04:00
Tom Boucher
9c4895f409 chore(ci): bump actions/upload-artifact v4.6.2 → v7.0.1 (#3715) (#3716)
v4.6.2 ships a node20 runtime; GitHub Actions is deprecating Node 20
(removed from runners 2026-09-16). v7.0.1 ships node24 and is the last
Node-20 reference in the repo.

Pinned by full commit SHA per repo convention. Verified v5/v6/v7
breaking changes do not apply: artifact names are unique per workflow
run and neither step uses include-hidden-files.

Closes #3715

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:00:14 -04:00
Tom Boucher
dfc1bb1a43 chore(changeset): update pr field + docs-exempt for #3713 (#3184)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:37:32 -04:00
Tom Boucher
942ed8974b chore(changeset): phase.uat-passed v1 (#3184)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:40 -04:00
Tom Boucher
dde529f0b1 fix(sdk): regenerate command-aliases CJS surface for phase.uat-passed (#3184)
Cycle 16 hand-edited command-aliases.generated.ts to register the
new query — but command-aliases is auto-generated and has a CJS
counterpart that must stay in sync. Running `npx tsx scripts/gen-command-aliases.ts`
from sdk/ syncs both surfaces; feat-3598 parity tests now pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:39 -04:00
Tom Boucher
fb473f353d fix(sdk): add index signature to UatItem so callers accepting Record<string, unknown>[] compile (#3184)
The 16 TDD cycles passed vitest but tsc --noEmit flagged TS2322:
UatItem lacked a string index signature, so the consumer at
phase-uat-passed.ts:249 (returning Record<string, unknown>[])
failed to typecheck. Index signature added; behaviour unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:39 -04:00
Tom Boucher
fd74c9d68a feat(sdk): register phase.uat-passed as canonical read-only query (#3184)
Cycle 16 of 16: wires isPhaseUatPassed into the SDK query
registry as 'phase.uat-passed' (alias 'phase uat-passed').
Read-only, JSON output. Handler validates the phase argument
and adapts isPhaseUatPassed to the QueryHandler signature.

Adds INVALID_PHASE_NUM to ERROR_CODE for argument validation.
Updates command-aliases.generated.ts to keep seam coverage parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:39 -04:00
Tom Boucher
99e81b2605 feat(sdk): throw PhaseUatPassedError on missing projectDir (#3184)
Cycle 15 of ~15: introduces PhaseUatPassedError (extends
GSDError, classification Validation) with typed ERROR_CODE
enum. Missing-projectDir now fails fast with a typed error
rather than letting downstream code crash on a vague fs error.
2026-05-18 23:30:39 -04:00
Tom Boucher
7956d324c5 feat(sdk): flag UAT files that produce no extractable items (#3184)
Cycle 14 of ~15: a UAT file present but empty of headings,
orphans, and placeholders now emits NO_ITEMS_EXTRACTED rather
than reporting passed=false with reasons=[] (which was
indistinguishable from missing-files).
2026-05-18 23:30:39 -04:00
Tom Boucher
b116bb07b9 feat(sdk): flag bracketed-placeholder result values (#3184)
Cycle 13 of ~15: 'result: [pending]' (template placeholder
copy-pasted without being filled) was previously dropped by
the \w+ regex. Now flagged as BRACKETED_PLACEHOLDER.
De-conflicted against ORPHAN_ITEM_MISSING_RESULT so an item
with a bracketed result doesn't double-report.
2026-05-18 23:30:38 -04:00
Tom Boucher
f78f26688b feat(sdk): flag headings missing the result field (#3184)
Cycle 12 of ~15: heading-without-result-line was previously
silently dropped by the regex — meaning a phase with an
unfilled UAT item could falsely pass the predicate. We now
scan for orphan headings and emit ORPHAN_ITEM_MISSING_RESULT
so the operator's typo / unfilled-template is surfaced.
2026-05-18 23:30:38 -04:00
Tom Boucher
d7aa4ce6e3 feat(sdk): merge frontmatter human_verification items into roster (#3184)
Cycle 11 of ~15: reuses parseVerificationFrontmatterItems from
uat.ts. Frontmatter-declared manual-verification items roll
into the same items[] roster and emit HUMAN_VERIFICATION_NEEDED
reasons so passed=false is justified, not silent.
2026-05-18 23:30:38 -04:00
Tom Boucher
29a48d8a78 feat(sdk): emit CASE_MISMATCH reason for non-canonical pass casing (#3184)
Cycle 10 of ~15: a captured 'result:' value that lowercases to
'pass' but isn't literally 'pass' now produces a CASE_MISMATCH
reason rather than a generic NON_PASS_RESULT — so operators can
distinguish a real non-pass from a likely typo.
2026-05-18 23:30:38 -04:00
Tom Boucher
42cc42de8b feat(sdk): accept bold-prefixed **result:** key (#3184)
Cycle 9 of ~15: regex now matches both bare 'result:' and
bold-prefixed '**result:**' forms. Requester used the bold
form in the issue text; canonical template uses bare. Both
are equally valid; predicate is form-agnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:38 -04:00
Tom Boucher
7c1ffb4131 feat(sdk): strip blockquote-prefixed lines before UAT item scan (#3184)
Cycle 8 of ~15: adds blockquote-line stripping pass. Quoted
documentation snippets no longer pollute the item roster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:38 -04:00
Tom Boucher
47a7412164 feat(sdk): strip HTML comment regions before UAT item scan (#3184)
Cycle 7 of ~15: adds HTML-comment stripping pass. Operator
notes inside <!-- ... --> no longer pollute the item roster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:38 -04:00
Tom Boucher
3c0490c021 feat(sdk): strip fenced code blocks before UAT item scan (#3184)
Cycle 6 of ~15: adds fenced-block stripping pass to
stripMarkdownInjection. Docs-and-examples inside code fences
no longer pollute the item roster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:38 -04:00
Tom Boucher
a38fa6c7a6 feat(sdk): strip frontmatter region before UAT item scan (#3184)
Cycle 5 of ~15: introduces stripMarkdownInjection helper.
First pass strips the YAML frontmatter region so injected
'### N. item' patterns inside frontmatter literal blocks
cannot be mistaken for real UAT items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:38 -04:00
Tom Boucher
1a27780443 feat(sdk): empty phase dir surfaces NO_UAT_FILES reason (#3184)
Cycle 4 of ~15: phase dir present but no *-HUMAN-UAT.md files
now returns a typed NO_UAT_FILES reason instead of an empty
reasons array (which prior cycles used as a 'shouldn't happen'
fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:38 -04:00
Tom Boucher
c1664e52ae feat(sdk): missing phase dir surfaces NO_PHASE_DIR reason (#3184)
Cycle 3 of ~15: when resolvePhaseDir returns null, predicate
returns a typed NO_PHASE_DIR reason. Previously the empty
reasons array was indistinguishable from other failure modes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:37 -04:00
Tom Boucher
0ff6b4cc88 feat(sdk): non-pass UAT items emit NON_PASS_RESULT reason (#3184)
Cycle 2 of ~15: introduces REASON_CODE frozen enum and UatReason
typed shape. Non-pass items (result not literally 'pass') now
contribute a typed reason instead of being invisible to callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:37 -04:00
Tom Boucher
3efb39e321 feat(sdk): isPhaseUatPassed walking skeleton (#3184)
Cycle 1 of ~15: minimal end-to-end predicate. One phase dir, one
UAT file, one pass result. No injection stripping, no orphan
detection, no CLI registration yet — those follow in subsequent
commits per TDD discipline.

Also exports resolvePhaseDir from phase-list-queries.ts (was
private) so the new predicate can reuse it without duplication.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:30:37 -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
e15348ea43 docs(context): add RULESET.TESTS.boundary-coverage + LEARNING.prompt-budget.boundary-gap
Adds four predicates derived from PR #3708 post-mortem:
- RULESET.TESTS.boundary-coverage — must test {limit-1, limit, limit+1} and near-reserve-distance inputs
- RULESET.TESTS.boundary-coverage.fixtures — required fixtures for budget/limit/quota/threshold code
- RULESET.TESTS.boundary-coverage.anti-pattern — names the trivially-large + trivially-small pairing as the failure mode
- LEARNING.prompt-budget.boundary-gap — full post-mortem of the UNNEEDED_TRIM + FALSE_HARDFAIL regressions
2026-05-18 21:56:17 -04:00
Tom Boucher
77b6bb62db fix(state): acquireStateLock last-retry now re-acquires before proceeding (#3711) 2026-05-18 17:55:06 -04:00
Tom Boucher
3c2c56b9e8 fix(ci): skip install + slow lanes on Windows in main test matrix (#3710)
* fix(ci): skip install + slow lanes on Windows in main test matrix

Gates the `Run install tests` and `Run slow tests` steps in
`.github/workflows/test.yml` to `matrix.os != 'windows-latest'`.

The install lane performs `npm install -g <tarball>` 7× per invocation
of release-tarball-smoke.install.test.cjs (1× in the shared before()
hook + 1× per of the 6 test cases). On windows-latest each install
costs 60–90 s (NTFS + Defender) so the lane alone consumes ~8–9 min
on top of the ~7 min already spent on npm ci + build:sdk + unit +
integration + security — overflowing the 15-min `timeout-minutes` cap
and cancelling the job mid-install.

The dedicated install-smoke.yml workflow already excludes Windows from
its matrix (ubuntu + macOS only); the weekly windows-compat workflow
provides Windows-specific regression coverage. Linux + macOS install
and slow lanes remain on main push for parity.

Refs #3709

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

* fix(deps): bump ws 8.20.0 → 8.20.1 to clear GHSA-58qx-3vcg-4xpx

The bug-3588 `npm audit --omit=dev reports zero advisories` test
(tests/bug-3588-npm-audit-clean.test.cjs) is failing on main after a
new advisory dropped against ws@8.20.0:

  GHSA-58qx-3vcg-4xpx — Uninitialized memory disclosure
  ws: range >=8.0.0 <8.20.1 (CVSS 4.4, moderate, CWE-908)

Fix: `npm audit fix --omit=dev` at both root and sdk/. Lockfile-only
bump to ws@8.20.1; package.json untouched (ws is transitive).

`npm audit --omit=dev` reports `found 0 vulnerabilities` in both
workspaces after the bump.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:08:57 -04:00
Tom Boucher
f1a61620bc fix(ci): bump coverage heap budget + Windows npm timeout in runNpm helper (#3704)
Set NODE_OPTIONS=--max-old-space-size=6144 on the Unit coverage step so the
c8 report phase has 6 GB instead of Node's default ~4 GB heap; the Linux
Node 24 runner has 7 GB available so this leaves 1 GB headroom. Raise the
runNpm default timeout from 55 000 ms to 180 000 ms so cold-cache Windows
npm install -g runs (which take 60-90 s on NTFS + Defender) complete before
the child process is killed.

Refs #3703

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:14:52 -04:00
Cristian Uibar
463eb26544 Match gsd-sdk query commit in graphify auto-update hook (#3653) (#3658)
* Match `gsd-sdk query commit` in graphify auto-update hook (#3653)

The PostToolUse Bash hook only substring-matched direct shell git ops in
tool_input.command. `gsd-sdk query commit` invokes git via spawnSync,
so the literal "git commit" never appears in the Bash tool's command
string and the hook silently skipped every SDK-issued commit. Result:
.planning/graphs/ drifted stale after every phase that closed via
gsd-sdk query commit, with no error and no log.

Gate 2 now also matches `gsd-sdk query commit`. Other SDK verbs
(phase.complete, roadmap.update-plan-progress, state.begin-phase) do
not invoke git themselves and remain non-matching to avoid spurious
rebuilds per state mutation. Adds positive + negative matcher tests.

* Fix changeset frontmatter for #3658

`type: Bug Fix` rejected by scripts/changeset/parse.cjs ALLOWED_TYPES
(Keep a Changelog values: Added/Changed/Deprecated/Removed/Fixed/Security).
Switch to `type: Fixed` and add `pr: 3658` required by MISSING_PR check.

docs-lint now reports `ok_no_triggering_fragments` locally.

* fix(#3658): bound graphify SDK commit matcher

* fix(#3658): exempt release note docs lint
2026-05-18 13:15:15 -04:00