Problem
Five shell suites fail when run locally under Windows Git Bash on current main and pass in CI on Linux. CI's Windows lane (.github/workflows/test-windows.yml) does not run any of them, so nothing in CI catches the gap. Every agent working on Windows sees these red when it verifies a change to playbooks or source-control locally. It then has to work out that the failures came before its change, or push to CI to get a trustworthy signal.
Most of the failures fall into two classes. In the first, a path is compared in POSIX /tmp/... form against the /c/Users/.../AppData/Local/Temp/... form Git Bash resolves. In the second, CRLF/LF line endings or a trailing \r break a byte-exact comparison. #5899 (head 4cf65d5fb) first reported them. They reproduce unchanged on today's main.
Reproduction
On Windows 11 with Git Bash, using a detached worktree at origin/main f605a5963 (2026-10-03), run bash <suite> for each suite:
| suite |
result |
failing assertions |
plugins/playbooks/skills/repo-sweep/scripts/tick.test.sh |
3 FAILED |
in-progress: body bytes, done lines: body bytes, filed: body bytes. The expected bytes have run:\r\n- [ ] and the actual have run:\n- [ ], so a CRLF body is rewritten with LF. |
plugins/playbooks/skills/repo-sweep/scripts/state.test.sh |
4 FAILED |
reconcile: committed lines, web-UI tick, exact skill sets, base excluded, CRLF body from a subdirectory: same report, other branch, one open sweep: exit 14 names the branch, other branch, two open sweeps: exit 15 lists them. For the two exit-14/15 cases, expected and actual print identically, even under cat -A with no \r visible. The difference is in something the test compares but does not print, and its cause is not found yet. |
plugins/source-control/scripts/worktree-claim.test.sh |
5 FAIL |
[4] report names the plain-add path, [19] foreign claim names the path, [31] unclaimed entry names the path. Each expects /tmp/tmp.X/external/... and gets /c/Users/KyleSexton/AppData/Local/Temp/tmp.X/external/.... Also [73] stale: a scan that fails mid-way is not provable (exit 1) (got 0) and [74] stale: the refusal says the scan failed. |
plugins/source-control/scripts/lib/worktree-facts.test.sh |
6 of 21 FAIL |
claimed after a bare add: the claim written is the claim read (actual is empty), a reason wrapped in double quotes round-trips unchanged, a literal backslash-n and backslash pair round-trip unchanged, list prints the raw quoted reason in the lock_reason column, list prints the raw backslash reason in the lock_reason column, list: a reasonless lock reads locked=yes, lock_reason=- |
plugins/source-control/scripts/worktree-root-scan.test.sh |
2 FAIL |
[14] symlink is not proposed: expected linked\tsymlink\tno, got linked\tempty\tyes. Git Bash without developer-mode symlinks copies instead of linking (assumption). Also [24] default root scans the configured root: the same /tmp vs /c/Users/.../Temp path form as above. |
The other source-control worktree suites pass on the same checkout: worktree-add-claim-gate, worktree-add-containment-gate, worktree-create-gate, worktree-create, worktree-root-doctor, and skills/worktree/reattach-slice. worktree-gates-spawn-budget skips because strace is unavailable.
Impact
- Local verification time on Windows. An agent cannot get a green local run for
playbooks or source-control changes, so it either investigates failures that existed before its change or pushes to CI to find out. Both add a CI round to time-to-green.
- The worktree-claim and worktree-facts failures cover claim parsing on the platform where most lanes run. If any of these failures is a real Windows behaviour gap rather than a test-fixture gap (unverified), the Windows behaviour of the claim gate is untested.
Proposed fix
- Path form. In the three source-control suites, normalise both sides before comparing: resolve the fixture root once with
cd "$TMP" && pwd -P (or cygpath -u when it is present) and build the expected paths from that, instead of the literal /tmp/... that mktemp returned.
- Line endings. In
tick.test.sh, and in state.test.sh once its hidden difference is found, find out whether tick.sh/state.sh should preserve CRLF bodies, which the "CRLF body" case suggests. If so, fix the script, which is likely a sed, awk or read that drops \r. If not, build the expected fixture bytes with the same line ending the script emits. Fix it in the script if the web-UI tick path really receives CRLF bodies from GitHub.
- worktree-facts claim round-trip. Find out why the claim read after a bare add is empty on Windows: whether the lock-reason file path,
git worktree lock --reason, or quoting differs.
- Symlink case. Skip
[14] with a SKIP: line when ln -s produces a copy (test with [ -L ] after creating it), the same way worktree-gates-spawn-budget skips without strace.
- Add these five suites to the Windows lane in
.github/workflows/test-windows.yml, scoped by path like its existing entries, so a regression is caught in CI.
Acceptance criteria
- On Windows Git Bash at a fresh
main, all five suites exit 0, or print SKIP: only for a capability the host lacks (symlinks).
test-windows.yml runs the five suites when their plugin paths change.
- Linux CI results for the five suites are unchanged.
Problem
Five shell suites fail when run locally under Windows Git Bash on current
mainand pass in CI on Linux. CI's Windows lane (.github/workflows/test-windows.yml) does not run any of them, so nothing in CI catches the gap. Every agent working on Windows sees these red when it verifies a change toplaybooksorsource-controllocally. It then has to work out that the failures came before its change, or push to CI to get a trustworthy signal.Most of the failures fall into two classes. In the first, a path is compared in POSIX
/tmp/...form against the/c/Users/.../AppData/Local/Temp/...form Git Bash resolves. In the second, CRLF/LF line endings or a trailing\rbreak a byte-exact comparison. #5899 (head4cf65d5fb) first reported them. They reproduce unchanged on today'smain.Reproduction
On Windows 11 with Git Bash, using a detached worktree at
origin/mainf605a5963(2026-10-03), runbash <suite>for each suite:plugins/playbooks/skills/repo-sweep/scripts/tick.test.shin-progress: body bytes,done lines: body bytes,filed: body bytes. The expected bytes haverun:\r\n- [ ]and the actual haverun:\n- [ ], so a CRLF body is rewritten with LF.plugins/playbooks/skills/repo-sweep/scripts/state.test.shreconcile: committed lines, web-UI tick, exact skill sets, base excluded,CRLF body from a subdirectory: same report,other branch, one open sweep: exit 14 names the branch,other branch, two open sweeps: exit 15 lists them. For the two exit-14/15 cases, expected and actual print identically, even undercat -Awith no\rvisible. The difference is in something the test compares but does not print, and its cause is not found yet.plugins/source-control/scripts/worktree-claim.test.sh[4] report names the plain-add path,[19] foreign claim names the path,[31] unclaimed entry names the path. Each expects/tmp/tmp.X/external/...and gets/c/Users/KyleSexton/AppData/Local/Temp/tmp.X/external/.... Also[73] stale: a scan that fails mid-way is not provable (exit 1)(got 0) and[74] stale: the refusal says the scan failed.plugins/source-control/scripts/lib/worktree-facts.test.shclaimed after a bare add: the claim written is the claim read(actual is empty),a reason wrapped in double quotes round-trips unchanged,a literal backslash-n and backslash pair round-trip unchanged,list prints the raw quoted reason in the lock_reason column,list prints the raw backslash reason in the lock_reason column,list: a reasonless lock reads locked=yes, lock_reason=-plugins/source-control/scripts/worktree-root-scan.test.sh[14] symlink is not proposed: expectedlinked\tsymlink\tno, gotlinked\tempty\tyes. Git Bash without developer-mode symlinks copies instead of linking (assumption). Also[24] default root scans the configured root: the same/tmpvs/c/Users/.../Temppath form as above.The other source-control worktree suites pass on the same checkout:
worktree-add-claim-gate,worktree-add-containment-gate,worktree-create-gate,worktree-create,worktree-root-doctor, andskills/worktree/reattach-slice.worktree-gates-spawn-budgetskips because strace is unavailable.Impact
playbooksorsource-controlchanges, so it either investigates failures that existed before its change or pushes to CI to find out. Both add a CI round to time-to-green.Proposed fix
cd "$TMP" && pwd -P(orcygpath -uwhen it is present) and build the expected paths from that, instead of the literal/tmp/...thatmktempreturned.tick.test.sh, and instate.test.shonce its hidden difference is found, find out whethertick.sh/state.shshould preserve CRLF bodies, which the "CRLF body" case suggests. If so, fix the script, which is likely ased,awkorreadthat drops\r. If not, build the expected fixture bytes with the same line ending the script emits. Fix it in the script if the web-UI tick path really receives CRLF bodies from GitHub.git worktree lock --reason, or quoting differs.[14]with aSKIP:line whenln -sproduces a copy (test with[ -L ]after creating it), the same wayworktree-gates-spawn-budgetskips without strace..github/workflows/test-windows.yml, scoped by path like its existing entries, so a regression is caught in CI.Acceptance criteria
main, all five suites exit 0, or printSKIP:only for a capability the host lacks (symlinks).test-windows.ymlruns the five suites when their plugin paths change.