* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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
* chore(3686): add release-tarball lifecycle smoke to install-smoke workflow
Closes#3686.
Adds a non-interactive lifecycle smoke that runs against the installed
tarball (not the working tree). Catches the two recent release-time bug
classes that the working-tree test suite cannot see:
* #3684 — symbol mismatches between init.cjs imports and secrets.cjs
exports that landed in v1.42.3 (closed/fixed-pending-release).
* #3668 — bare `gsd-sdk` invocations in 75 of 78 workflow files with no
`command -v gsd-sdk … elif node "$GSD_TOOLS"` fallback (open).
Shape:
* `scripts/release-tarball-smoke.cjs` — pure CJS module exporting a
frozen `SMOKE` enum and a `runSmoke({ tarballPath, installPrefix,
expectedVersion, fixtureDir, lifecycleCommands })` function. CLI
`--json` mode prints `JSON.stringify(result)` and exits 0 iff
`result.code === SMOKE.OK`. Install is `--prefix <tmpdir>` so it
does not pollute global node_modules.
* `tests/release-tarball-smoke.test.cjs` — 6 tests covering happy
path, version mismatch, lifecycle command file resolution,
missing-command detection, sdk binary callability, and structural
workflow-body checks. Tests assert on the SMOKE enum directly; no
`assert.match` on rendered prose, no try/finally in test bodies,
no source-grep theater. Uses `before`/`after` to pack+install
once across the test file.
* `tests/release-tarball-smoke-workflow.test.cjs` — 7 structural
assertions on the parsed install-smoke.yml IR (workflow_call
trigger preserved, lifecycle step calls release-tarball-smoke.cjs
with --json, jq check enforces result.code === "ok", path filter
includes the new files, artifact-on-failure step present).
* `.github/workflows/install-smoke.yml` — extended (not duplicated).
New "Lifecycle smoke" step after the existing version check, on the
same matrix. Artifact upload on failure for debugging. Path filter
now triggers on changes to the new script + test.
Per CONTRIBUTING.md §"Prohibited: Raw Text Matching on Test Outputs"
this PR avoids the same anti-pattern that caused PR #3666 to be
reverted (PR #3688) — the script returns frozen enum codes, tests
assert on the enum, never on stdout strings.
Workflow-body checks in Cycle 3 are INFORMATIONAL (count returned,
not enforced) on this PR. After #3668's fix lands and the 75 missing
fallbacks are added, the lane can be tightened to enforce zero.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3686): harden release smoke workflow and query scanner
* fix(3686): move tarball-smoke test to install suite to fix Windows ETIMEDOUT + coverage OOM
Windows (Node 22/24/26, jobs 76565215694/76565215710/76565215812):
release-tarball-smoke.test.cjs had no suite marker so run-tests.cjs
classified it as 'unit', running it on Windows PR CI. The before() hook
calls execFileSync(npm.cmd install -g ...) with a 55 s timeout; on
Windows GHA runners this npm global install consistently hits ETIMEDOUT
(~62 s observed), causing all 3 Windows lanes to fail.
Coverage (job 76565215085):
c8 ran test:coverage:unit (unit suite only) with V8 coverage tracking
active across child processes. The tarball-smoke test's before() hook
spawned npm install subprocesses while c8 held V8 coverage descriptors
open, driving the Node heap to 4 GB+ and triggering an OOM abort during
report generation (exit code 134, all 5682 tests had already passed).
Fix: rename to tests/release-tarball-smoke.install.test.cjs so
run-tests.cjs routes it to the 'install' suite. The install suite is
already skipped on PR CI by design (test.yml lines 179-181: only runs on
main push). The dedicated install-smoke.yml workflow continues to exercise
this test on its own matrix. Also update the install-smoke.yml PR path
filter and the structural wiring test assertion to match the new filename.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(test): route tarball-smoke install test through tests/helpers.cjs
Replaces direct fs.mkdtempSync and execFileSync calls in tests/release-tarball-smoke.install.test.cjs with createTempDir() and a new runNpm() helper in tests/helpers.cjs. Cleanup is now automatic via the helper. Addresses CodeRabbit Major refactor at https://github.com/gsd-build/get-shit-done/pull/3692#discussion_r3260433892.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#3689): use find instead of chained ls for continue-here scan
The check_incomplete_work step in resume-project.md chained six bare-glob
ls arguments to discover .planning/.continue-here*.md handoff files.
Under zsh's default NOMATCH option (macOS default shell), the first
non-matching glob aborts the entire command during word-expansion,
silently dropping every pattern after it — including
.planning/.continue-here*.md, which holds the canonical pause checkpoint
for default-context handoffs. The 2>/dev/null || true guard suppresses
ls's own stderr / exit code but has no effect on the shell's pre-exec
glob-abort.
Replace the chain with two find invocations:
find .planning -maxdepth 3 -name '.continue-here*.md' -print 2>/dev/null
find . -maxdepth 1 -name '.continue-here*.md' -print 2>/dev/null
find does not use shell glob expansion and tolerates absent directories
on both bash and zsh.
Adds tests/bug-3689-resume-glob-nomatch.test.cjs covering:
- zsh -o nomatch with .planning/.continue-here-AT-1234.md present and
no spike/sketch/deliberation subdirs (the regression scenario)
- bash default, same layout
- zsh -o nomatch with no checkpoints anywhere (clean exit, no output)
- text invariant: resume-project.md no longer carries the chained-ls
pattern and does carry the find-based scan
Closes#3689
* fix(#3689): guard find commands with || true for Windows safety
Restore the trailing '|| true' guard that the original chained ls had,
per the windows-robustness.test.cjs invariant (informational bash
commands in critical workflows must not let an exit-1 from find on
exotic platforms tank the resume-project.md flow).
* fix(#3689): address CodeRabbit review feedback
- Set changeset frontmatter pr: 3693 (was 3690 placeholder); the release-
note link now points at the right PR.
- Use createTempDir / cleanup from tests/helpers.cjs instead of local
tmpdir/cleanup wrappers, per repo test standards.
* fix(#3689): update bug-3446 discovery contract to new find-based scan
bug-3446 enforced three text invariants on the chained-ls implementation
that this PR replaces with find. Update the assertions to verify the
same three discovery paths are still covered:
- .planning/.continue-here*.md at depth 1 -> find .planning -maxdepth 3
- .planning/sketches/SKETCH-NNN/.continue-here*.md at depth 3 -> same
find with -maxdepth >= 3 (assertion now reads the actual depth and
enforces a lower bound so future changes can deepen but not shallow it)
- repo-root .continue-here*.md legacy fallback -> find . -maxdepth 1
The discovery contract is preserved; only the implementation under
inspection changes.
* test(#3689): convert bug-3446 from source-grep to behavioral assertions
CodeRabbit flagged the text-regex assertions on the workflow source as a
violation of the no-source-grep testing standard. Replace them with a
behavioral integration test that:
1. Extracts the actual check_incomplete_work bash block from
resume-project.md (so the test stays in sync with whatever the
workflow does, no string match required).
2. Plants three handoff files in a temp dir covering the three
discovery surfaces bug #3446 originally filed:
- .planning/.continue-here.md (depth 1 under .planning)
- .planning/sketches/SKETCH-001/.continue-here.md (depth 3)
- ./.continue-here.md (legacy repo root)
3. Runs the snippet under bash and asserts each planted file appears
in stdout.
Same contract; now validated through runtime behavior instead of
regex-on-file.
* fix(#3689): use \\r?\\n in bash-fence regex for Windows CRLF parity
The Windows test-parity guard at tests/windows-test-parity-guard.test.cjs:106
flags any new fence-extraction regex that uses a literal \\n after the
language tag — on Windows CRLF the byte after `bash` is \\r, the regex
silently fails to match, and the extracted snippet is empty. Switch to
the canonical /```(?:bash|sh)\\r?\\n([\\s\\S]*?)```/ pattern.
* fix(#3689): harden extractCheckBlock against missing </step> and CRLF closing fence
Per CodeRabbit review: assert that the closing </step> tag is present before
slicing (otherwise slice(stepStart, -1) silently grabs the wrong block and
the test fails misleadingly), and add \r?\n to the closing fence as well so
Windows CRLF doesn't sneak a stray carriage return into the captured snippet.
* fix(3696): pr-template enforcer recognises CI/tooling carve-out
The enforcer in scripts/pr-template-policy.cjs only validated against
three typed templates and three hard-coded DEFAULT_TEMPLATE_MARKERS.
It had zero awareness of the documented CI/tooling/dep/doc-only exception
in .github/pull_request_template.md, meaning external contributors who
followed the documented escape hatch still had their PRs auto-closed.
Fix:
1. Path-scope auto-skip: if every changed file matches a tooling glob
allowlist (.github/**, scripts/**, docs/**, *.md, .changeset/**,
dependency manifests), skip enforcement and exit success with no
comment posted.
2. Explicit exemption marker: if the PR body contains
<!-- pr-template-exempt: <non-empty reason> -->, skip enforcement.
3. Workflow updated to fetch changed file paths via gh and pass them
as CHANGED_FILES env to the policy script.
4. Pull request template updated to document both mechanisms and retire
the old "delete this file" prose carve-out.
5. 14 new tests (TDD red→green); all 25 tests pass.
Closes#3696
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3696): allow hyphenated pr-template exemption reasons
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts c5657fcbfd (squash of PR #3666).
The production change was correct in intent. The new test file
tests/surface-state.test.cjs violates two CONTRIBUTING.md rules
that the project enforces specifically because they produce
passing-but-useless tests:
1. ~15 try { ... } finally { cleanup(dir); } blocks in test bodies
— CONTRIBUTING.md "Never use try/finally inside test bodies."
Approved forms are beforeEach/afterEach hooks or t.after(...).
2. 17 assert.match calls on rendered console.warn prose
— CONTRIBUTING.md "Prohibited: Raw Text Matching on Test
Outputs." Tests must assert on a typed structured surface
(frozen reason enum) instead of the rendered prose, otherwise
they pass-but-rot the moment a warning string is reworded.
The .changeset/witty-birds-gather.md fragment is also reverted so
the v1.43 changelog does not advertise a fix that is not on main.
A reworked PR addressing items 1-4 from the re-review (linked on
the closed PR thread) is welcome — the intent of the fix is right.
Refs #3666#3662
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* W002 health check: cross-reference milestones archive for STATE.md phase refs
After /gsd-complete-milestone, phase dirs move into milestones/vX.Y-phases/
and their `#### Phase N:` headings in ROADMAP.md are collapsed inside
<details> blocks. The ROADMAP heading scan misses them, so W002 fired for
every archived phase number mentioned in STATE.md's historical narrative body
("Recent", "Decisions", "Deferred Items") — leaving every project that ever
ran /gsd-complete-milestone permanently degraded with proportional W002 noise.
Union archived milestone phase directories into the validPhases set used by
the W002 check, mirroring the same milestone-archive lookup that W006
already uses (sdk/src/query/validate.ts:723-736).
Closes#3652
* Address review: use shared regex constants and mirror W002 archive fix into CJS path
CodeRabbit (PR #3655 review 1): the ad-hoc /^(\d+[A-Z]?(?:\.\d+)*)/i used to
extract phase tokens from archived phase dirs would skip project-code-prefixed
names like `CK-64-...`. Switch the new SDK block to the shared
PHASE_TOKEN_FROM_DIR_RE / MILESTONE_ARCHIVE_DIR_RE constants (defined at
sdk/src/query/validate.ts:32-33) so prefixed archives are recognised. Added a
companion regression test using `CK-`-prefixed dirs.
Codex review: the shipped CJS health command path (get-shit-done/bin/lib/verify.cjs
cmdValidateHealth, routed by validate-command-router.cjs) only unions
collectDiskPhases (active archive only) plus ROADMAP heading scan — same bug as
the SDK had. Port the all-archive scan into the CJS path via listMilestoneArchiveDirs
+ PHASE_TOKEN_FROM_DIR_RE (already declared at verify.cjs:401-402). Added a CJS
regression test covering the multi-sub-milestone (v1.3a + v1.3b) scenario from
the issue report.
Adds .changeset/lucky-lynx-wave.md.
* Address Gemini findings: shared regex + helper reuse + cross-platform path
P1 #2 — refactor the new W002 archive-scan block to reuse the existing
listMilestoneArchiveDirs helper (sdk/src/query/validate.ts:40) instead of
re-implementing readdir + filter inline. Eliminates duplication and prevents
the two call sites from drifting apart.
P2 #4 — listMilestoneArchiveDirs sorted by `a.slice(a.lastIndexOf('/') + 1)`,
which returns the full path on Windows where path.join produces backslashes.
Switch to path.basename(a) so the numeric version sort works cross-platform.
Brings the SDK helper in line with the CJS sibling at
get-shit-done/bin/lib/verify.cjs:411 which already uses path.basename.
P1 #1 / P2 #5 — the pre-existing W006/W007 archive + active phase scans
(Check 8) used an ad-hoc `^(\d+[A-Z]?(?:\.\d+)*)` regex that silently skipped
project-code-prefixed phase dirs like `CK-64-foo`, so W006 fired for a
correctly-archived phase and W007 fired for a correctly-on-disk phase. Switch
both scans to the shared PHASE_TOKEN_FROM_DIR_RE constant declared at
sdk/src/query/validate.ts:32. The W006 archive loop also now reuses
listMilestoneArchiveDirs for consistency.
P2 #6 — strengthen the CK-prefix regression test to also assert no W006
fires for `#### Phase 64: Prior shipped` (placed inside <details> so the
heading scan picks it up while the on-disk scan does not), pinning the
shared-regex behaviour in the W006 path.
* CI: switch retired /gsd-<cmd> comment syntax to canonical /gsd:<cmd>
The bug-2543 slash-namespace invariant lint scans get-shit-done/bin/lib/**
for /gsd-<cmd> patterns and fails CI when one slips into a comment. Use the
canonical /gsd:complete-milestone form in the new verify.cjs comment (and
mirror the change in the SDK + tests + changeset entry so all docstrings
referencing the milestone-completion command share one spelling).
Also: extract a small forEachArchivedPhaseToken helper in validate.ts (Gemini
P3 finding from review pass 2) so Check 4 (W002) and Check 8 (W006) share the
archive-walking loop instead of inlining it twice.
* Address rev3 review: shared regex parity, numeric sort, drop dead try/catch
Gemini P1 — Check 4's flat phases/ scan still used the ad-hoc
/^(\d+[A-Z]?(?:\.\d+)*)/ regex while the archive scan used the shared
PHASE_TOKEN_FROM_DIR_RE. Project-code-prefixed dirs (e.g. CK-65-current) on
the flat layout would have slipped past validity, so the W002 check could
still mis-classify them. Use PHASE_TOKEN_FROM_DIR_RE here too.
Gemini P3 — `[...validPhases].sort()` ordered tokens alphabetically, producing
error messages like "phases 1, 10, 19, 2, 20" instead of "1, 2, 10, 19, 20".
Switch both SDK and CJS to numeric localeCompare so the displayed list is
human-readable. Mirrored in both paths.
Grok P3 — the CJS Check 4 archive block wrapped listMilestoneArchiveDirs in
an outer try/catch even though the helper already swallows ENOENT/EACCES into
[]. The outer catch was unreachable. Removed; only the per-archive readdir
needs a catch.
Grok P2 / Gemini P1 (CJS Check 8 archive scan) — the assertion that CJS
Check 8 needs the same archive union as Check 4 was repeatedly raised across
review passes. It is incorrect: Check 8 filters ROADMAP.md through
extractCurrentMilestone() before scanning headings, which strips shipped
milestones (collapsed in <details> or not) so archived phase numbers never
reach `roadmapPhases`. Added an inline note documenting this and a positive
regression assertion in the CJS test that W006 does NOT fire for the
archived phases in the multi-sub-milestone fixture. (Skipped a parallel
W007 assertion because the active-archive fallback in
getActiveMilestoneArchiveDir is pre-existing behavior unrelated to #3652.)
* Port forEachArchivedPhaseToken helper to verify.cjs for SDK parity
Gemini rev5 P2 — the CJS Check 4 inlined the archive-walking loop while
the SDK already factored it into forEachArchivedPhaseToken(). Add a
mirror helper in verify.cjs so both seams use the same primitive,
matching the cooperating-sibling pattern documented in
scripts/shared-module-handsync-allowlist.json.
* ci: retrigger to clear unrelated TOCTOU flake
The previous CI run failed at the pre-existing #1925 concurrency test
(state add-blocker concurrent calls) on macos-24 and ubuntu-22 but
passed on ubuntu-24 — and the same test passed on the prior CI run of
this branch (commit 8a246916). The state add-blocker code path is
completely independent of the W002 archive-union changes in this PR.
* surface: default missing optional fields in readSurface, normalize writeSurface input
readSurface used to reject any .gsd-surface.json missing one of its four fields
and return null with no diagnostic, so the active surface silently degraded to
the 'full' profile. Optional array fields (disabledClusters, explicitAdds,
explicitRemoves) now default to [] when missing or wrong-typed; hard failures
(malformed JSON, non-object root, missing/non-string baseProfile) still return
null but emit a console.warn naming the file + reason.
writeSurface now normalizes its input to the full SurfaceState shape and
throws on missing baseProfile, so partial writes can no longer land on disk
and trip readSurface later. Tests extended to cover the new lenient and
warn-on-hard-fail behavior plus the writer guard.
Fixes#3662
* surface: reject whitespace-only baseProfile + migrate new tests to helpers.cjs
Applies CodeRabbit findings on PR #3666:
1. readSurface and writeSurface now reject baseProfile values that are blank
after trim() (e.g. " "), not only the empty string. Whitespace-only
strings would split-by-comma to [''] downstream and silently produce an
unresolvable profile mode. Both guards updated symmetrically; warn/error
messages reworded to "missing, non-string, or blank".
2. tests/surface-state.test.cjs now uses createTempDir + cleanup from
tests/helpers.cjs instead of local mkdtempSync + fs.rmSync, aligning with
the repo coding guideline for root-level tests. The local tmpDir() helper
delegates to createTempDir for backward-compat with the existing test
bodies. Per-test cleanup calls swapped to cleanup(dir).
3. Added regression tests:
- readSurface rejects whitespace-only baseProfile and warns
- writeSurface rejects whitespace-only baseProfile, non-string baseProfile,
and null surfaceState
Refs #3662
* surface: warn on unknown baseProfile mode names in read and write
Applies a Codex review finding on PR #3666:
readSurface and writeSurface used to accept any non-blank string as
baseProfile. A typo like {"baseProfile":"standrad"} would pass validation,
then resolveProfile() in install-profiles.cjs would silently fall back to
'full' with no diagnostic — the same silent-degradation symptom that #3662
was filed to fix, just through a different code path.
Both functions now split baseProfile by comma, validate each mode against
the registered PROFILES set ('core', 'standard', 'full'), and emit a single
[gsd] console.warn line that names the unknown modes and lists the valid
ones. The state is still parsed/written — resolveProfile() decides the
actual resolution fallback. Composed profiles where some modes are valid
and some are not warn only about the unknown subset.
Side note: the pre-existing 'round-trips composed base profile' test used
'core,audit' as a stand-in composed string. 'audit' is not a registered
profile (the three known profiles are 'core', 'standard', 'full'), so the
test was relying on the old lack of validation. Switched to 'core,standard'
to preserve the round-trip intent without producing diagnostic noise.
Refs #3662
* changeset: include blank/typo baseProfile in documented read failure cases
CodeRabbit minor finding on PR #3666 — the changeset wording only mentioned
"missing/non-string baseProfile" but the implementation also rejects blank
(including whitespace-only) baseProfile values, and warns on typo'd /
unknown profile mode names. Updated to match actual behavior.
* changeset: pr field should be PR number, not issue number
Codex review finding on PR #3666. The changeset's `pr: 3662` was the linked
issue (#3662), but the convention across other .changeset/*.md files is that
`pr:` carries the PR number. Verified by spot-checking other changesets
(2937 → pr: 3515, 3298 → pr: 3306, 3541 → pr: 3547 — all PR numbers).
Updated to `pr: 3666`. The body text still references the issue.
* surface: reject comma-only baseProfile + warn on wrong-typed optional fields
Three Gemini review findings on PR #3666 — all spirit-of-#3662 edge cases:
1. Comma-only baseProfile bypasses validation. readSurface used to accept
baseProfile: ", ," because trim() returned "," (non-empty). Downstream
resolveProfile() would split-and-filter to [] and silently fall back to
'full'. Added effectiveProfileModes() helper that splits, trims, filters
empty — both readSurface and writeSurface now reject when the result is
empty. Same silent-degradation symptom as the original bug.
2. Wrong-typed optional fields silently coerced. readSurface used to coerce
{disabledClusters: 42} to {disabledClusters: []} with no diagnostic.
Now warns via mistypedOptionalFields() before normalizeSurfaceState()
does the coercion, in both reader and writer.
3. Missing test coverage for the EACCES branch in readSurface. Added a
chmod-000 unreadable-file test, skipped on Windows and root accounts
(mode bits are ignored on those platforms).
48/48 surface tests pass. Final codex pass returned LGTM on the prior
state; these three additions strengthen the same lenient/loud contract.
Refs #3662
* fix(3678): executor must respect commit_docs:false; teach SDK skip envelope
Closes#3678
When `commit_docs: false` in `.planning/config.json`, the SDK's
`cmdCommit` correctly short-circuits and returns
`{committed: false, hash: null, reason: 'skipped_commit_docs_false'}`
without staging or committing anything. The agent prompt at
`agents/gsd-executor.md:710-720` (final_commit block) tells the executor
to call `gsd-sdk query commit "docs(...)" --files .planning/...` but says
NOTHING about how to interpret a skipped return. With no explicit
instruction, the LLM improvises raw `git add` / `git add -f` / `git commit`
to "fulfill" the per-plan commit step it was told to make, which leaks
gitignored `.planning/` artifacts into the user's git history (exactly
what the reporter observed).
Three coordinated fixes:
1. **agents/gsd-executor.md final_commit block** — adds explicit handling
text for all three SDK return envelopes (`committed:true`, `skipped:true
commit_docs`, `skipped:true gitignored`, `committed:false other reasons`).
States plainly: "Do not fall back to raw `git add` / `git commit` /
`git add -f` when the SDK returns `skipped: true`."
2. **get-shit-done/bin/lib/commands.cjs cmdCommit** — adds `skipped: true`
to both skip-path envelopes so agents see "skipped" as a first-class
success signal rather than inferring "no commit happened, I must
improvise" from absent `hash` / `committed:false`. Backward-compatible:
existing callers reading `committed` / `hash` / `reason` are unaffected.
3. **tests/bug-3678-executor-commit-docs-respect.test.cjs** — 7-test
regression covering:
- A1/A2: agent prompt mentions the skip envelope AND explicitly forbids
raw-git fallback (`source-text-is-the-product` exception)
- B1: SDK envelope carries `committed:false`, `skipped:true`, canonical
`reason: 'skipped_commit_docs_false'` (frozen enum)
- B2: git index empty after commit_docs:false skip (no `.planning/` staged)
- B3: HEAD unchanged after commit_docs:false skip
- C1/C2: structural ban on `git add -f` / `git add --force` in any agent
or workflow body (prohibition-sentence exception preserves audit prose)
Verification:
- node --test tests/bug-3678-*: 7/7 pass
- Targeted regression (10 commit/executor-adjacent files): 135/135 pass
- Full docker suite (gsd-test-summary): 11751/11740 pass / 0 fail
(the 11 added are this test plus a few collateral pickups)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(changeset): add fragment for #3678 fix (Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>)
* chore(changeset): set PR number 3679 (Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>)
* fix(3678): preserve skip-aware carve-out in executor completion checklist
The new `final_commit` prose at lines 717-741 teaches the executor to
treat `skipped:true` as success and forbids raw-git fallback, but the
downstream completion checklist still contained an unconditional
"Final metadata commit made" checkbox. An LLM executor reading an
unchecked mandatory box may attempt to satisfy it via raw `git add`,
re-introducing the exact regression this PR is meant to prevent.
Update the checklist line to carve out the intentional-skip case and
add a regression test asserting the carve-out remains present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3683): normalize /gsd:<cmd> → /gsd-<cmd> in command, workflow, and reference bodies
Extends #3677's agent-body normalizer to all body text staged through
copyWithPathReplacement (commands, workflows, references). The initial
isCommand guard was structurally redundant — normalizeAgentBodyForRuntime
already self-gates on shouldNormalizeHyphenNamespaceInAgentBody(runtime),
so dropping it covers all hyphen-name runtimes (Claude / Qwen / Hermes)
without affecting colon-canonical runtimes (Gemini).
Addresses the user-visible symptom in #3683: workflows like
get-shit-done/workflows/discuss-phase.md (7 colon refs) leaked /gsd:<cmd>
markers to the model context, which the model echoed at the end of
/gsd-discuss-phase runs.
Source-prose drift caught by the new cross-reference invariant test:
- commands/gsd/plan-phase.md: removed a slash-form mention of the
deleted /gsd-research-phase command (#3042)
- commands/gsd/profile-user.md: replaced a slash-form artifact
reference with a backticked bare name (the referenced item is a
skill config, not a user-callable slash command)
Tests:
- tests/bug-3683-command-colon-namespace-leak.test.cjs — runtime-form
regression for commands/gsd/*.md staging
- tests/bug-3683-command-cross-reference-invariant.test.cjs — locks
cross-reference coherence: every /gsd-X / /gsd:X reference in a
command body must resolve to commands/gsd/X.md (so a future rename
forces every cross-reference to update)
- tests/bug-3683-workflow-colon-namespace-leak.test.cjs — runtime-form
regression for workflows + references; negative test for gemini
asserting the colon form is preserved
Fixes#3683
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changeset): remove undefined cycle reference
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3677): normalize /gsd:<cmd> → /gsd-<cmd> in agent bodies for hyphen-name runtimes
Closes#3677
The executor agent bodies installed to `~/.claude/agents/gsd-*.md` (and
the Qwen / Hermes equivalents) still contained retired `/gsd:<cmd>`
colon-form references in their prose. Every GSD skill / agent has
registered under the canonical hyphen `name:` form since #2808, so the
colon form is unroutable — Claude Code rejects it with `Unknown command:
/gsd:execute-phase. Did you mean /gsd-execute-phase?`. Reporter measured
~28 agent files / ~96 leaked refs on a full Claude global install.
This is the agent-body surface of the same class of bug as the two
already-fixed sibling surfaces:
- #3583 (SKILL.md skill bodies) — fixed via #3629
- #3584 (user-facing runtime "Next step: /gsd:…" emissions) — fixed via #3606
The agent-body surface in `bin/install.js`'s agent install loop was
never covered: the Claude-default / Qwen / Hermes branches register
hyphen `name:` but copy bodies verbatim (Qwen/Hermes do branding-only
swaps; Claude-default falls through with no body conversion at all), so
the colon refs leak.
Fix:
1. Add a pure predicate `shouldNormalizeHyphenNamespaceInAgentBody(runtime)`
backed by an explicit allow-list `HYPHEN_NAME_AGENT_RUNTIMES =
{claude, qwen, hermes}`. Unknown / future runtimes default to false
(better to leak than to mangle).
2. Add `normalizeAgentBodyForRuntime(content, runtime, cmdNames)` that
conditionally applies the shared `transformContentToHyphen` from
`scripts/fix-slash-commands.cjs` (same transform #3629 used for
SKILL.md bodies).
3. Call `normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames())`
in the agent install loop right before `fs.writeFileSync`, so it
composes with all the existing runtime branches. For Gemini and
self-converting runtimes the predicate short-circuits, so their
convertClaudeAgentToXAgent output is not re-rewritten.
4. Export both functions from `bin/install.js` for the regression test.
Regression test (`tests/bug-3677-agent-colon-namespace-leak.test.cjs`):
24 tests across 4 groups — A (exports exist), B (predicate matrix
covering all 15 runtimes in the layout table + an unknown-runtime case),
C (normalize helper applies/skips correctly for claude/qwen/hermes/
gemini/copilot), D (sanity check of the underlying transform).
Verification:
- node --test tests/bug-3677-*: 24/24 pass
- Sibling-regression (6 slash-namespace test files): 76/76 pass
- All install-minimal-all-runtimes suites: 54/54 pass after `npm run
build:sdk` (the prior 27 fails were pre-existing — missing local
sdk/dist build, not introduced by this change)
- Full docker suite (gsd-test-summary): 11769/0 fail
(11751 baseline + 18 new = my 24 tests with some collateral pickups)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(changeset): set PR number 3680 (Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>)
* test(3677): port real-source efficacy + idempotence tests from #3681
Adds describe group E with 5 behavioral tests credited to John Turner
(johnzilla, PR #3681 — closed in favor of this PR by its author):
E0: command roster is populated and includes symptom commands
E1: every agents/gsd-*.md transforms clean — real-source efficacy
E2: idempotent — repeat transform on hyphenated input is a no-op
E3: word boundary — /gsd:plan-phase-extra is not a roster match
E4: rewrites bare gsd:<cmd> shorthand (no leading slash)
E1 is the test that would have caught the original bug — pure-function
tests can pass while the install.js wiring silently bypasses the
transform. E2 guards against double-rewrite mangling during reinstall.
29/29 tests pass (24 original + 5 ported).
Co-Authored-By: John Turner <johnzilla@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: John Turner <johnzilla@users.noreply.github.com>
Phase 2 of #3660 / ADR-3660. Routes both lifecycle verbs through the
Runtime Artifact Layout Module landed in Phase 1 (#3663):
- Add installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)
and uninstallRuntimeArtifacts(runtime, configDir, scope) as the public
orchestrators. Both pre-prune stale gsd-* entries before staged copy;
installRuntimeArtifacts brackets the prune+copy with preserveUserArtifacts /
restoreUserArtifacts so user-owned content (e.g. gsd-dev-preferences) survives
wipe-and-replace for claude/qwen/hermes runtimes.
- Add applyRuntimeContentRewritesInPlace as the per-runtime path/branding
post-stage step (preserves byte-output equivalence with the legacy
copyCommandsAs* pipeline, including Qwen/Hermes branding rewrites).
- Add _copyStaged, _removeGsdEntries kind-aware filesystem helpers.
- Add _runLegacyInstallMigrations, _runLegacyUninstallCleanup as thin
dispatchers over existing ADR-0008 legacy migrations (Hermes flat->nested
per #2841, dev-preferences-as-skill per #2973). For Hermes, also clean up
the intermediate skills/gsd/gsd-*/ layout that pre-Phase-2 installs left
on disk.
- Delete the 9 copyCommandsAs*Skills functions (Codex / Cursor / Windsurf /
Trae / CodeBuddy / Copilot / Claude / Antigravity / Augment) and the
_copyCommandsAsSkillsViaConverter helper. All test entry points migrated
to call installRuntimeArtifacts directly through the unified seam.
- Collapse the 9-branch uninstall ladder to one uninstallRuntimeArtifacts
call plus preserved non-layout side-effects (Codex TOML, Copilot
instructions, hooks).
- Unify install dispatcher: a single _isSkillsRuntime gate routes all 11
skills runtimes through installRuntimeArtifacts for both full and core/
minimal profiles. Removes 11 per-runtime if-else branches (3 minimal-mode
shim branches + 8 dead after-the-gate branches).
Net delta on bin/install.js: 11,495 -> 11,174 (-321 LOC).
New tests:
- tests/install-uninstall-layout-loop.test.cjs (34 tests) - per-runtime
fixture assertions on install/uninstall/legacy-migration ordering.
- tests/install-hermes-regressions.test.cjs (6 tests) - covers the six
defects surfaced by iterative review: Hermes upgrade leaves stale dirs,
--hermes --profile=core fall-through, --qwen --profile=core fall-through,
minimal-mode dev-preferences migration skipped (Hermes/Qwen/Claude-global),
and ordering bug in _runLegacyInstallMigrations.
Existing tests (10,038 prior + 40 new) all green: 10,078/10,078 pass.
Refs #3664
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After PR #3649 (merge 40a442b2), test (windows-latest, 24) failed at npm ci with zero stdout/stderr (16s opaque exit). Identical code passed on the PR. Root cause: the npm ci and npm run build:sdk steps in .github/workflows/test.yml (jobs test and coverage) lacked an effective shell — neither step-level nor via defaults.run.shell. On windows-latest, Actions defaults to pwsh; npm.cmd → node.exe → npm-cli.js child-process chain under pwsh can swallow stderr.
Pin shell: bash on the 4 outlier steps. Add tests/workflow-shell-pinning.test.cjs that scans every workflow file referencing windows-latest, computes effective shell with proper workflow- and job-level defaults.run.shell inheritance, and fails CI if any run: npm|npx … step lacks an effective shell.
Closes#3672
Replace the blunt kindPrefix !== '' guard in _syncGsdDir with a
manifest-membership discriminator. For non-empty prefix runtimes, behavior
is unchanged (prefix match). For Hermes (empty prefix), a directory is
GSD-owned iff its stem appears in the canonical manifest; only those dirs
are removal candidates when absent from the staged set. Dirs not in the
manifest (user-owned) are preserved unconditionally. When no manifest is
provided (legacy callers), the removal pass is skipped (conservative fallback).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the fs.existsSync(dest) guard in applySurface so _syncGsdDir is
always called. _syncGsdDir already does mkdirSync(..., { recursive: true })
so the destination is created when absent — recovering partially-initialized
or user-deleted runtime config dirs. Also threads manifest through to
_syncGsdDir as optional 4th arg (used by P1-2 Hermes fix).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the 2 empty try/finally wrappers (finally bodies contained only
comments, no cleanup actions). Inline the assertions directly; add a
comment noting stagedDir lifecycle ownership. No behavior change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all 13 try/finally blocks with t.after() per-test cleanup hooks.
Import createTempDir/cleanup from tests/helpers.cjs; use createTempDir
inside createFixtureSkillsDir and createFixtureAgentsDir. Remove os import
(now unused at top level).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When kindPrefix === '' (Hermes: destSubpath=skills/gsd, no per-skill prefix),
startsWith('') always returns true so the prior removal loop would delete any
dir not in the staged set — including user-owned skill dirs. Guard the entire
removal block behind kindPrefix !== '' so non-staged dirs are never pruned when
there is no prefix to distinguish GSD-owned from user-owned entries.
TDD: failing test added first asserting user-custom-skill is preserved through
a _syncGsdDir call with kindPrefix=''.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests verify that kind.stage(resolvedProfile) produces correct directory
structure: .md files for commands, agent .md files for agents, and
gsd-<stem>/SKILL.md dirs for skills kinds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RED-2/GREEN-2: wildcard skills='*' stages all *.md as <prefix><stem>/SKILL.md
with converter applied. Includes existence guard and error cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>