@@ -88,6 +88,24 @@ ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
8888[ -f " $CONFIG " ] && . " $CONFIG "
8989HOST=" ${CBM_VM_HOST:? set CBM_VM_HOST in ~/ .claude/ cbm-vm/ config} "
9090USER_=" ${CBM_VM_USER:- test} "
91+
92+ # ── Per-run checkout (the VM's last shared-state hazard) ─────────────────────
93+ # The VM holds ONE working tree at /c/cbm, and `update`/`sync` REPLACE it
94+ # (`git reset --hard` + `clean -fdx`). A second session syncing its own branch
95+ # therefore swaps the code under a running leg — observed 2026-08-06, where a
96+ # gate run on main compiled another branch's files and died on a header that
97+ # does not exist on Windows. That is worse than a red: it is a confident wrong
98+ # answer about the wrong tree.
99+ #
100+ # Setting CBM_CI_RUN_ID gives this run its own checkout, cloned from /c/cbm.
101+ # A local clone hardlinks .git/objects, so it costs seconds and little disk.
102+ # Unset (the default) keeps the historical single-tree behaviour.
103+ VM_BASE_REPO=" /c/cbm"
104+ if [ -n " ${CBM_CI_RUN_ID:- } " ]; then
105+ VM_REPO=" /c/cbm-run-${CBM_CI_RUN_ID} "
106+ else
107+ VM_REPO=" $VM_BASE_REPO "
108+ fi
91109HOST_KEY=" ${CBM_VM_HOST_KEY_SHA256:? set CBM_VM_HOST_KEY_SHA256 in ~/ .claude/ cbm-vm/ config} "
92110LOCAL_BRANCH=" $( git -C " $ROOT " branch --show-current) "
93111BRANCH=" ${CBM_VM_BRANCH:- ${LOCAL_BRANCH:- main} } "
@@ -117,6 +135,38 @@ SCP=(scp "${SSH_OPTIONS[@]}")
117135
118136vm () { local env=" $1 " ; shift
119137 " ${SSH[@]} " " C:\\ msys64\\ msys2_shell.cmd -defterm -no-start -${env} -c \" set -e -o pipefail; $* \" " ; }
138+
139+ # Create this run's own checkout if it does not exist yet. A LOCAL clone
140+ # hardlinks .git/objects, so it is seconds and near-zero disk rather than a
141+ # fresh network clone. No-op when running in the shared (default) tree.
142+ vm_ensure_run_checkout () {
143+ [ " $VM_REPO " = " $VM_BASE_REPO " ] && return 0
144+ # `git clone --local` points origin at the base PATH, so a later
145+ # `fetch origin <branch>` would resolve against whatever the base happens
146+ # to hold (i.e. another session's state) instead of GitHub. Inherit the
147+ # base's real origin URL so the isolated tree fetches independently.
148+ # The substitution is single-quoted here so it expands in the REMOTE shell
149+ # (same pattern as JOBS above); nested double quotes do not survive the
150+ # cmd.exe -> msys2_shell wrapper.
151+ local base_url_expr=' $(git -C ' " $VM_BASE_REPO " ' remote get-url origin)'
152+ vm clangarm64 " [ -d '$VM_REPO /.git' ] || git clone --local '$VM_BASE_REPO ' '$VM_REPO '"
153+ vm clangarm64 " git -C '$VM_REPO ' remote set-url origin $base_url_expr "
154+ }
155+
156+ # The VM tree must be the one the caller means. A concurrent session running
157+ # `update`/`sync` in the SHARED tree swaps it mid-flight, and the leg then
158+ # reports a confident verdict about someone else's branch. Callers assert the
159+ # expected HEAD; a mismatch fails loudly instead of producing that verdict.
160+ vm_assert_head () {
161+ local want=" $1 " got
162+ got=" $( vm clangarm64 " cd $VM_REPO && git rev-parse --verify HEAD" | tr -d ' \r' ) "
163+ if [ " $got " != " $want " ]; then
164+ echo " FATAL: VM tree $VM_REPO is at $got , expected $want ." >&2
165+ echo " Another session likely re-synced the shared checkout." >&2
166+ echo " Re-run with CBM_CI_RUN_ID set to get an isolated tree." >&2
167+ return 1
168+ fi
169+ }
120170vm_cmd () { " ${SSH[@]} " " $@ " ; } # plain cmd.exe (CI-shaped environment)
121171
122172# A GitHub runner is ephemeral: every job starts on a fresh image with a
@@ -157,13 +207,25 @@ esac
157207case " $cmd " in
158208status)
159209 " ${SSH[@]} " " echo VM_REACHABLE & ver"
160- vm clangarm64 " cd /c/cbm 2>/dev/null && git log --oneline -1 && ls -la build/c/codebase-memory-mcp.exe build/c/test-runner.exe 2>/dev/null || echo 'repo/build missing — run provision-windows.sh'"
210+ vm clangarm64 " cd $VM_REPO 2>/dev/null && git log --oneline -1 && ls -la build/c/codebase-memory-mcp.exe build/c/test-runner.exe 2>/dev/null || echo 'repo/build missing — run provision-windows.sh'"
161211 ;;
162212update)
163- vm clangarm64 " cd /c/cbm && git fetch origin ${BRANCH} && git reset --hard FETCH_HEAD && git clean -fdx && git log --oneline -1"
213+ vm_ensure_run_checkout
214+ vm clangarm64 " cd $VM_REPO && git fetch origin ${BRANCH} && git reset --hard FETCH_HEAD && git clean -fdx && git log --oneline -1"
164215 exec " $0 " build
165216 ;;
217+ drop-run-checkout)
218+ # Explicit tidy-up of THIS run's checkout (no-op in shared mode). The run
219+ # that created it removes it; nothing else may, since another session's
220+ # tree must never be deleted out from under it.
221+ if [ " $VM_REPO " = " $VM_BASE_REPO " ]; then
222+ echo " win.sh: shared checkout ($VM_BASE_REPO ) — nothing to drop" >&2
223+ else
224+ vm clangarm64 " rm -rf '$VM_REPO ' && echo dropped $VM_REPO "
225+ fi
226+ ;;
166227sync)
228+ vm_ensure_run_checkout
167229 local_head=" $( git -C " $ROOT " rev-parse --verify HEAD) "
168230 WIN_MANIFEST=" $( mktemp " ${TMPDIR:-/ tmp} /cbm-vm-manifest.XXXXXX" ) "
169231 WIN_ARCHIVE=" $( mktemp " ${TMPDIR:-/ tmp} /cbm-vm-worktree.XXXXXX.tar" ) "
@@ -172,25 +234,25 @@ sync)
172234 cbm_vm_write_untracked_manifest " $ROOT " " $WIN_MANIFEST "
173235 COPYFILE_DISABLE=1 tar --no-xattrs --no-mac-metadata \
174236 -C " $ROOT " --null -T " $WIN_MANIFEST " -cf " $WIN_ARCHIVE "
175- remote_head=" $( vm clangarm64 " cd /c/cbm && git rev-parse --verify HEAD" ) "
237+ remote_head=" $( vm clangarm64 " cd $VM_REPO && git rev-parse --verify HEAD" ) "
176238 remote_head=" ${remote_head// $' \r ' / } "
177239 if [ " $remote_head " != " $local_head " ]; then
178240 echo " FATAL: Windows VM is at $remote_head , expected local HEAD $local_head ; run win.sh update first." >&2
179241 exit 1
180242 fi
181243 vm clangarm64 \
182- " cd /c/cbm && git reset --hard '$local_head ' && git clean -fdx"
244+ " cd $VM_REPO && git reset --hard '$local_head ' && git clean -fdx"
183245 if [ -s " $WIN_PATCH " ]; then
184246 " ${SSH[@]} " \
185- ' C:\msys64\msys2_shell.cmd -defterm -no-start -clangarm64 -c "cd /c/cbm && git apply --binary --whitespace=nowarn -"' \
247+ ' C:\msys64\msys2_shell.cmd -defterm -no-start -clangarm64 -c "cd $VM_REPO && git apply --binary --whitespace=nowarn -"' \
186248 < " $WIN_PATCH "
187249 fi
188250 if [ -s " $WIN_MANIFEST " ]; then
189251 " ${SSH[@]} " \
190- ' C:\msys64\msys2_shell.cmd -defterm -no-start -clangarm64 -c "cd /c/cbm && tar -xf -"' \
252+ ' C:\msys64\msys2_shell.cmd -defterm -no-start -clangarm64 -c "cd $VM_REPO && tar -xf -"' \
191253 < " $WIN_ARCHIVE "
192254 fi
193- vm clangarm64 " cd /c/cbm && git status --short --branch"
255+ vm clangarm64 " cd $VM_REPO && git status --short --branch"
194256 win_cleanup
195257 exec " $0 " build
196258 ;;
@@ -201,7 +263,7 @@ build)
201263 # NOT via CC='ccache clang', which bypassed the verified-cache env layer.
202264 # The test-runner is no longer built here: the test leg (scripts/test.sh)
203265 # builds its own runner, same as CI's test jobs.
204- vm clangarm64 " cd /c/cbm && scripts/build.sh CC=clang CXX=clang++ > /tmp/win-build.log 2>&1 && echo BUILD_OK || (echo BUILD_FAIL; tail -20 /tmp/win-build.log; exit 1)"
266+ vm clangarm64 " cd $VM_REPO && scripts/build.sh CC=clang CXX=clang++ > /tmp/win-build.log 2>&1 && echo BUILD_OK || (echo BUILD_FAIL; tail -20 /tmp/win-build.log; exit 1)"
205267 ;;
206268test)
207269 [ $# -ge 1 ] || { echo " usage: win.sh test <suite...>" >&2 ; exit 2; }
@@ -210,7 +272,12 @@ test)
210272 # protected per-user temp root via vm-run-tests.sh, which streams FULL
211273 # output and refuses to report success without the runner's completion
212274 # summary. A `| tail -40` here once hid 40 real Windows failures.
213- vm clangarm64 " cd /c/cbm && bash test-infrastructure/vm/vm-run-tests.sh $* "
275+ # CBM_VM_EXPECT_HEAD=<sha> asserts the tree is the one the caller means
276+ # before spending a leg on it — cheap insurance against a concurrent
277+ # session having re-synced the shared checkout.
278+ [ -z " ${CBM_VM_EXPECT_HEAD:- } " ] || vm_assert_head " $CBM_VM_EXPECT_HEAD "
279+ vm clangarm64 " cd $VM_REPO && bash test-infrastructure/vm/vm-run-tests.sh $* "
280+ [ -z " ${CBM_VM_EXPECT_HEAD:- } " ] || vm_assert_head " $CBM_VM_EXPECT_HEAD "
214281 ;;
215282guards)
216283 # Match the Windows CI product build: a clean, embedded-UI product binary.
@@ -222,7 +289,7 @@ guards)
222289 # needs the official MSVC Node.js (/c/node, from provision-windows.sh):
223290 # MSVC-built npm native modules cannot resolve Node-API symbols against
224291 # MSYS2's mingw node.
225- vm clangarm64 " export PATH=/c/node:\$ PATH && cd /c/cbm && scripts/build.sh --with-ui CC=clang CXX=clang++ SANITIZE= BUILD_DIR=build/guards"
292+ vm clangarm64 " export PATH=/c/node:\$ PATH && cd $VM_REPO && scripts/build.sh --with-ui CC=clang CXX=clang++ SANITIZE= BUILD_DIR=build/guards"
226293 # The guards themselves run through plain cmd/PowerShell, exactly like the
227294 # CI job: under the MSYS2 shell TMP is C:\msys64\tmp, whose ancestry
228295 # grants mutation rights to Authenticated Users — the daemon's
@@ -238,14 +305,14 @@ smoke-install)
238305 # build/c (including any test-runner) just as CI's separate jobs imply;
239306 # ccache keeps the rebuild to minutes — parity was the explicit user call
240307 # over incremental speed here.
241- vm clangarm64 " cd /c/cbm && scripts/build.sh CC=clang CXX=clang++ && bash test-infrastructure/vm/vm-smoke.sh"
308+ vm clangarm64 " cd $VM_REPO && scripts/build.sh CC=clang CXX=clang++ && bash test-infrastructure/vm/vm-smoke.sh"
242309 ;;
243310smoke-artifact)
244311 # The release-archive flow with local bytes: canonical build → the ONE
245312 # package-release.sh → extract → vm-smoke.sh in artifact mode — the same
246313 # sequence _build.yml + _smoke.yml run remotely, so archive-layout bugs
247314 # surface here instead of in a release dry run.
248- vm clangarm64 " cd /c/cbm && bash scripts/ci/smoke-artifact.sh windows arm64 CC=clang CXX=clang++"
315+ vm clangarm64 " cd $VM_REPO && bash scripts/ci/smoke-artifact.sh windows arm64 CC=clang CXX=clang++"
249316 ;;
250317soak)
251318 duration=" ${1:- 10} "
@@ -256,7 +323,7 @@ soak)
256323 echo " usage: win.sh soak [positive-minutes]" >&2
257324 exit 2
258325 fi
259- vm clangarm64 " cd /c/cbm && CBM_VM_TEST_LOG=/tmp/win-soak.log bash \
326+ vm clangarm64 " cd $VM_REPO && CBM_VM_TEST_LOG=/tmp/win-soak.log bash \
260327 test-infrastructure/vm/vm-run-tests.sh --soak '$duration '"
261328 ;;
262329sh)
@@ -273,11 +340,11 @@ ubsan-build)
273340 # Validated: UBSan needs no interceptors, so it builds, runs, AND reports
274341 # correctly under emulation. (ASan does NOT: no aarch64 runtime exists and
275342 # the x86_64 runtime faults in emulated process-init — ASan stays CI-only.)
276- vm clang64 " cd /c/cbm && make -j${JOBS} -f Makefile.cbm CC=clang CXX=clang++ SANITIZE='-fsanitize=undefined -fno-omit-frame-pointer' build/c/test-runner > /tmp/win-ubsan-build.log 2>&1 && echo UBSAN_BUILD_OK || (echo UBSAN_BUILD_FAIL; tail -20 /tmp/win-ubsan-build.log; exit 1)"
343+ vm clang64 " cd $VM_REPO && make -j${JOBS} -f Makefile.cbm CC=clang CXX=clang++ SANITIZE='-fsanitize=undefined -fno-omit-frame-pointer' build/c/test-runner > /tmp/win-ubsan-build.log 2>&1 && echo UBSAN_BUILD_OK || (echo UBSAN_BUILD_FAIL; tail -20 /tmp/win-ubsan-build.log; exit 1)"
277344 ;;
278345ubsan-test)
279346 [ $# -ge 1 ] || { echo " usage: win.sh ubsan-test <suite...>" >&2 ; exit 2; }
280- vm clang64 " cd /c/cbm && CBM_VM_TEST_LOG=/tmp/win-ubsan-test.log bash test-infrastructure/vm/vm-run-tests.sh $* "
347+ vm clang64 " cd $VM_REPO && CBM_VM_TEST_LOG=/tmp/win-ubsan-test.log bash test-infrastructure/vm/vm-run-tests.sh $* "
281348 ;;
282349trap-ubsan-build)
283350 # NATIVE ARM64 UBSan via trap mode. -fsanitize-trap=undefined needs NO
@@ -289,13 +356,13 @@ trap-ubsan-build)
289356 # fired, reproduce under the emulated `win.sh ubsan-build`/`ubsan-test`,
290357 # which carries the full runtime + message. BUILD_DIR isolated so it never
291358 # clobbers the plain test-runner.
292- vm clangarm64 " cd /c/cbm && make -j${JOBS} -f Makefile.cbm CC='ccache clang' CXX='ccache clang++' SANITIZE='-fsanitize=undefined -fsanitize-trap=undefined -fstack-protector-strong -fno-omit-frame-pointer' BUILD_DIR=build/trap-ubsan build/trap-ubsan/test-runner > /tmp/win-trap-ubsan-build.log 2>&1 && echo TRAP_UBSAN_BUILD_OK || (echo TRAP_UBSAN_BUILD_FAIL; tail -20 /tmp/win-trap-ubsan-build.log; exit 1)"
359+ vm clangarm64 " cd $VM_REPO && make -j${JOBS} -f Makefile.cbm CC='ccache clang' CXX='ccache clang++' SANITIZE='-fsanitize=undefined -fsanitize-trap=undefined -fstack-protector-strong -fno-omit-frame-pointer' BUILD_DIR=build/trap-ubsan build/trap-ubsan/test-runner > /tmp/win-trap-ubsan-build.log 2>&1 && echo TRAP_UBSAN_BUILD_OK || (echo TRAP_UBSAN_BUILD_FAIL; tail -20 /tmp/win-trap-ubsan-build.log; exit 1)"
293360 ;;
294361trap-ubsan-test)
295362 [ $# -ge 1 ] || { echo " usage: win.sh trap-ubsan-test <suite...>" >&2 ; exit 2; }
296363 # A UB trap crashes the runner with SIGILL (exit 132); the harness reports
297364 # the failing suite so the emulated diagnosis loop can name the check.
298- vm clangarm64 " cd /c/cbm && CBM_VM_RUNNER=build/trap-ubsan/test-runner CBM_VM_TEST_LOG=/tmp/win-trap-ubsan-test.log bash test-infrastructure/vm/vm-run-tests.sh $* "
365+ vm clangarm64 " cd $VM_REPO && CBM_VM_RUNNER=build/trap-ubsan/test-runner CBM_VM_TEST_LOG=/tmp/win-trap-ubsan-test.log bash test-infrastructure/vm/vm-run-tests.sh $* "
299366 ;;
300367pageheap)
301368 # OS-level heap verification (page-granular overflow/UAF detection) for the
@@ -318,7 +385,7 @@ test-par)
318385 # under the CI-shaped protected temp root with FULL output (this leg once
319386 # ran under the MSYS-shared /tmp and piped through `tail -25` — the same
320387 # truncated-blindness class that hid 40 Windows failures from `test`).
321- vm clangarm64 " cd /c/cbm && CBM_VM_TEST_LOG=/tmp/win-test-par.log bash test-infrastructure/vm/vm-run-tests.sh --par"
388+ vm clangarm64 " cd $VM_REPO && CBM_VM_TEST_LOG=/tmp/win-test-par.log bash test-infrastructure/vm/vm-run-tests.sh --par"
322389 ;;
323390help | -h | --help)
324391 print_help
0 commit comments