Six independent windows-only failure clusters identified from the
windows-22 CI log on 1be0e4e2:
- scripts/command-contract-helpers.cjs: parseFrontmatter split on `\n`
→ on Windows checkout (autocrlf=true) every line carries trailing \r,
lines.indexOf('---', 1) returns -1, all fields read as missing. Drove
the bulk of three "67 subtests failed" suite-level errors covering
workstreams.md, workspace.md, verify-work.md, add-tests.md, etc.
Fix: split(/\r?\n/).
- tests/enh-3271-sdk-adr-structure.test.cjs: same CRLF pattern — H2
captures pulled '\r' into headings like "decision\r", breaking
equality checks on "## Decision" / "## Consequences". Fix: same.
- tests/runtime-bridge-sync-smoke.test.cjs: await import(BRIDGE_PATH)
passed a Windows absolute path to Node's ESM loader, which rejects
with "Only URLs with a scheme in: file, data, and node are
supported." Fix: wrap once at module scope with pathToFileURL.
pathToFileURL is a no-op for POSIX absolute paths.
- tests/bug-2957-claude-global-postinstall-message.test.cjs: hardcoded
'/tmp/gsd-test-settings.json' resolved to D:\tmp\... on Windows
where the parent dir doesn't exist → ENOENT on fs.writeFileSync
inside finishInstall. Fix: os.tmpdir() + pid suffix.
- tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs: the
"while/read loop" subtest and the "end-to-end against real git
worktrees" describe both assert POSIX shell behavior (process
substitution `< <(...)`, RUNNER~1 8.3-shortname mismatch). Skip on
win32 with explicit reasons (satisfies the
no-unconditional-win32-skip guard).
- tests/bug-2838-summary-rescue-gitignored-planning.test.cjs: entire
describe extracts bash rescue blocks from workflow .md files and
runs them; the shell contract itself is the test's point. Skip on
win32 with reason.
- tests/helpers.cjs cleanup(): bumped rmSync retry budget from
10×100ms to 20×250ms — 1s wasn't enough for Windows Defender's
deferred handle release; bumping to 5s should absorb the residual
EBUSY failures observed in bug-1736 / bug-2248 / bug-2698 after the
first retry bump landed in 1be0e4e2.
Validated: holodeck (ubuntu docker) 11224/0 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#2774): inclusion-based worktree cleanup to protect workspace .git
The cleanup blocks in execute-phase.md and quick.md used an exclusion
filter (`grep -v "$(pwd)$"`) to skip the current worktree before calling
`git worktree remove --force` on everything else. The exclusion fails
whenever the current workspace is itself a worktree of an upstream repo:
- multi-workspace setups where `git worktree list` reports the registry
path as a different absolute path than `$(pwd)`
- the cross-drive Windows case where the registry reports `E:/...` while
`$(pwd)` resolves to `C:/...` — the equality test never holds, every
other worktree (including the workspace itself) is removed, and the
workspace's `.git` pointer file is destroyed.
Switches both cleanup blocks to an inclusion-based filter that targets
only agent-spawned worktrees under `.claude/worktrees/agent-`, the
namespace Claude Code's `isolation="worktree"` always uses for executor
worktrees. The workspace path can never collide with that prefix.
Adds tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs covering:
- both workflow files use the inclusion filter
- neither falls back to the broken `grep -v "$(pwd)$"` guard
- end-to-end simulation of porcelain output with workspace + agent
worktrees yields only the agent worktree
Closes#2774
* test(#2774): replace source-grep with behavioral test of cleanup pipeline
* fix(#2774): whitespace-safe worktree iteration with while/read
CodeRabbit review on PR #2778 flagged that `for WT in $WORKTREES` splits
on whitespace. Any agent worktree path containing a space (e.g. a workspace
under '/Users/dev/My Workspace/') would be torn into broken half-paths,
`git -C` would fail on each fragment, and the executor branch would never
be deleted.
Switch both cleanup blocks (quick.md and execute-phase.md) to:
while IFS= read -r WT; do
[ -z "$WT" ] && continue
...
done < <(git worktree list --porcelain | grep ... | sed ...)
Process substitution feeds the pipeline output line-by-line — IFS= and -r
preserve every byte of the path including embedded spaces.
Also rename the misleading `makeBareTempGitRepo` helper to
`makeTempUpstreamRepo` (it does not pass --bare; it inits a normal repo
with an initial commit so worktree-add works).
Add two new behavioral tests:
- discovery pipeline yields whitespace paths intact on a single line
- the actual while/read loop iterates each whitespace-bearing path
exactly once (would fail with the previous `for WT in` form)
Tests: 5681 pass, 0 fail.