From ba5a9e696b1147192e8eee3091d9bb8179f3a8af Mon Sep 17 00:00:00 2001 From: Destin Date: Wed, 2 Sep 2026 06:48:16 -0700 Subject: [PATCH 1/2] tooling: the workbench boot check refuses a dead port; verify.sh checks tests/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tooling repairs, all measured before and after. 1. `scripts/workbench-boot-check.mjs` reported "All 16 workbench routes mount cleanly" and exited 0 with NOTHING serving the port — reproduced against a deliberately dead port. Chrome renders its own ERR_CONNECTION_REFUSED page, which has no "failed to start" text, no #boot spinner and throws no exception, so all three of the script's probes read clean. CLAUDE.md tells every session to trust this check, so a dead dev server read as a passing app. Three assertions now stand between a route and `ok`, each proven to fire on its own: * a preflight HTTP request, before Chrome is discovered or launched (dead port -> exit 2, naming run-workbench.sh) * Page.navigate's errorText and the main document's HTTP status (a 500 -> "server answered HTTP 500 for the page") * #root, which index.html ships inline (a live server that is not the workbench -> "#root is not in the DOM") Verified green against a real workbench: 16/16, exit 0. Guarded by scripts/workbench-boot-check.test.mjs, which needs no Chrome, and by a new Workspace CI step. CLAUDE.md's route count corrected 12 -> 16. 2. `scripts/verify.sh` now runs `tsc -p tsconfig.tests.json` beside the src one, printing how many test files are still excluded, and its SCOPE header no longer says the test tree is unchecked. It also warns loudly when desktop/node_modules is a SYMLINK: the test-runner half of that is fixed in youcoded#384, but npm ci and Gradle still follow the link and empty the shared copy, and a green run must not imply the setup is fine. 3. Roadmap and reports brought in line with what was measured overnight. Two investigations archived; the fixed-sleeps one re-measured. The three suites filed as flaking under parallel load (subagent-view, mcp-startup-wiring, project-watcher) did NOT fail in any of 27 full local runs — that entry is re-scoped rather than closed, since a trigger nobody has reproduced is not the same as a fixed bug. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01T9SRtMoZJNF4sJrodQa1N1 --- .github/workflows/workspace-ci.yml | 9 +++ CLAUDE.md | 2 +- ROADMAP.md | 2 +- ...026-09-01-desktop-tests-not-typechecked.md | 20 ----- ...9-01-fixed-sleeps-and-mcp-wiring-import.md | 20 ++++- ...026-09-01-desktop-tests-not-typechecked.md | 38 ++++++++++ ...26-09-01-workbench-boot-check-dead-port.md | 13 +++- docs/roadmap/dev-workspace.md | 39 +++++----- docs/roadmap/shipped.md | 3 + scripts/verify.sh | 37 ++++++++- scripts/workbench-boot-check.mjs | 76 ++++++++++++++++++- scripts/workbench-boot-check.test.mjs | 52 +++++++++++++ 12 files changed, 257 insertions(+), 54 deletions(-) delete mode 100644 docs/active/investigations/2026-09-01-desktop-tests-not-typechecked.md create mode 100644 docs/archive/investigations/2026-09-01-desktop-tests-not-typechecked.md rename docs/{active => archive}/investigations/2026-09-01-workbench-boot-check-dead-port.md (72%) create mode 100644 scripts/workbench-boot-check.test.mjs diff --git a/.github/workflows/workspace-ci.yml b/.github/workflows/workspace-ci.yml index 21d5b5d7..af61c4e3 100644 --- a/.github/workflows/workspace-ci.yml +++ b/.github/workflows/workspace-ci.yml @@ -85,6 +85,15 @@ jobs: if: ${{ !cancelled() }} run: node --test .claude/hooks/context-inject.test.mjs .claude/hooks/glob-guard.test.mjs .claude/hooks/roadmap-edit-check.test.mjs + # The workbench boot check reported "All 16 workbench routes mount cleanly" + # against a port nothing was serving — verified 2026-09-02 on a dead port, + # exit 0. CLAUDE.md tells every session to trust that check. These two cases + # need no Chrome (the preflight now runs before the browser launches), so + # they belong on this runner. + - name: Test the workbench boot check + if: ${{ !cancelled() }} + run: node --test scripts/workbench-boot-check.test.mjs + # The review deck — the tool Destin approves every UI change on — had ten # test files and no caller: not this workflow, not a script, not its README. # Asked for the first time on 2026-08-31 they were not merely unrun but RED diff --git a/CLAUDE.md b/CLAUDE.md index 1518b898..3692985b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -130,7 +130,7 @@ When designing new features or making changes to user-facing app interfaces, the ### UI Workbench -`bash scripts/run-workbench.sh` boots the **real renderer** in a browser tab (Vite only — no Electron, no PTY) against a fake `window.claude`, on port 5233. Every menu is clickable and stateful, so **new feature UI is built here before its backend exists** — channels with no backend go in `MOCK_ONLY`, which is then the backend to-do list. Toolbar switches scenario (`default`/`empty`/`no-providers`/`refused`/`stress`), fake IPC latency, narrow viewport, and the tool gallery that replaced `?mode=tool-sandbox`. Use `run-dev.sh` instead when you need real event ordering, PTY, or main-process behaviour. **After any change to the mock shim run `node scripts/workbench-boot-check.mjs`** — it loads every registered workbench route headless (12 today) and fails on a console error; the unit suite passed while the app crashed at boot three times running. Rule: `.claude/rules/react-renderer.md`; spec: `docs/archive/specs/2026-07-29-ui-workbench-design.md`. +`bash scripts/run-workbench.sh` boots the **real renderer** in a browser tab (Vite only — no Electron, no PTY) against a fake `window.claude`, on port 5233. Every menu is clickable and stateful, so **new feature UI is built here before its backend exists** — channels with no backend go in `MOCK_ONLY`, which is then the backend to-do list. Toolbar switches scenario (`default`/`empty`/`no-providers`/`refused`/`stress`), fake IPC latency, narrow viewport, and the tool gallery that replaced `?mode=tool-sandbox`. Use `run-dev.sh` instead when you need real event ordering, PTY, or main-process behaviour. **After any change to the mock shim run `node scripts/workbench-boot-check.mjs`** — it loads every registered workbench route headless (16 today) and fails on a console error, and refuses (exit 2) when nothing is serving the port; the unit suite passed while the app crashed at boot three times running. Rule: `.claude/rules/react-renderer.md`; spec: `docs/archive/specs/2026-07-29-ui-workbench-design.md`. ### UI review (autonomous screenshot sweep) diff --git a/ROADMAP.md b/ROADMAP.md index 29aac125..132f5545 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -43,7 +43,7 @@ Target: `v1.3` | Area | Open | Needs verify | Decisions | Parked | |---|---|---|---|---| | [native-harness](docs/roadmap/native-harness.md) — the app's own agent doing work | 51 | 8 | 0 | 16 | -| [dev-workspace](docs/roadmap/dev-workspace.md) — building the app, not the app | 41 | 19 | 0 | 9 | +| [dev-workspace](docs/roadmap/dev-workspace.md) — building the app, not the app | 39 | 19 | 0 | 8 | | [user-interface](docs/roadmap/user-interface.md) — shared primitives, chrome, layout, copy | 27 | 17 | 0 | 5 | | [files](docs/roadmap/files.md) — documents the user opens, edits or organises | 21 | 5 | 0 | 9 | | [marketplace](docs/roadmap/marketplace.md) — finding, installing and rating plugins and themes | 18 | 6 | 0 | 3 | diff --git a/docs/active/investigations/2026-09-01-desktop-tests-not-typechecked.md b/docs/active/investigations/2026-09-01-desktop-tests-not-typechecked.md deleted file mode 100644 index 8beae737..00000000 --- a/docs/active/investigations/2026-09-01-desktop-tests-not-typechecked.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -date: 2026-09-01 -status: active -type: investigation -topic: desktop/tests/ is neither type-checked nor linted — tsconfig's include stops at src/ ---- - -# `desktop/tests/` (~350 files) is neither type-checked nor linted - -**Mechanism.** `youcoded/desktop/tsconfig.json`'s `include` is `src/**/*` only, and there is no -second tsconfig for the test tree, so `tsc --noEmit` never sees it — vitest executes those files with -esbuild stripping types without checking them. The ESLint config is scoped to `src/**` for the same -reason: type-aware rules need the files in a TS project. - - -**Fix.** One tests tsconfig unlocks both at once. `scripts/verify.sh` states this limitation in its -header; this is the fix for it. Expect a first run to surface a backlog of type errors in tests — -land the config and the fixes together so the gate never ships red. - -**History.** Filed 2026-08-06; re-verified 2026-09-01 (still a single tsconfig). diff --git a/docs/active/investigations/2026-09-01-fixed-sleeps-and-mcp-wiring-import.md b/docs/active/investigations/2026-09-01-fixed-sleeps-and-mcp-wiring-import.md index ab0cb4ea..38510fb3 100644 --- a/docs/active/investigations/2026-09-01-fixed-sleeps-and-mcp-wiring-import.md +++ b/docs/active/investigations/2026-09-01-fixed-sleeps-and-mcp-wiring-import.md @@ -37,5 +37,23 @@ sync-spaces-engine debounce item (`2026-09-01-sync-engine-debounce-macos-flake.m (youcoded#366) the macOS leg went red three times on one unchanged tree with a different victim each run while Ubuntu and Windows passed all three. +**Update 2026-09-02 — six converted, and the eight-concurrent claim did not reproduce.** + +*Sleeps:* 108 counted, now 102. The six taken were the ones that were not merely letting time pass: +five copies of `send('go')` followed by `await new Promise(r => setTimeout(r, 20))` in +`native-session-host.test.ts` — a guess that a child's turn had started, which is the exact bug +youcoded#363 fixed once in that same file and left a helper for (`waitForTurnInFlight`, previously +used at one site out of six) — and one whose own comment said "poll for it" above a `setTimeout(r, 30)`. +The rest were read and left: most of the large ones (120/150/80/80/150 ms) are NEGATIVE assertions — +"wait a bounded time and prove nothing happened" — which have no signal to wait on by construction, +and the `setTimeout(r, 10)` majority sit inside fake model streams, where they ARE the simulated work. + +*MCP startup wiring:* did not exceed its budget in any of 27 full local runs on 2026-09-02 — 1 alone, +6 concurrent, 4 pinned to 4 cores with `taskset`, and two 8-way concurrent sweeps. The 8-way sweeps +DID break the suite, just not here: `harness-eval-orchestrator` (3/8 runs) and `harness-review-runner` +(4/8) hit 30,000 ms, and two React tests (`comment-list`, `feedback-section`) failed on effect timing. +All four are fixed. So the residual recorded above is still open in principle but has never been +measured failing; the file's in-body `await import()` remains unhoistable for the reason stated. + **History.** Filed 2026-08-28 as the deliberate residual of #362/#363; re-verified 2026-09-01 (the -five in-body imports and the `os` mock are unchanged). +five in-body imports and the `os` mock are unchanged); re-measured 2026-09-02. diff --git a/docs/archive/investigations/2026-09-01-desktop-tests-not-typechecked.md b/docs/archive/investigations/2026-09-01-desktop-tests-not-typechecked.md new file mode 100644 index 00000000..140ac9af --- /dev/null +++ b/docs/archive/investigations/2026-09-01-desktop-tests-not-typechecked.md @@ -0,0 +1,38 @@ +--- +date: 2026-09-01 +status: shipped +type: investigation +topic: desktop/tests/ is neither type-checked nor linted — tsconfig's include stops at src/ +--- + +# `desktop/tests/` (~350 files) is neither type-checked nor linted + +**Mechanism.** `youcoded/desktop/tsconfig.json`'s `include` is `src/**/*` only, and there is no +second tsconfig for the test tree, so `tsc --noEmit` never sees it — vitest executes those files with +esbuild stripping types without checking them. The ESLint config is scoped to `src/**` for the same +reason: type-aware rules need the files in a TS project. + + +**Fix.** One tests tsconfig unlocks both at once. `scripts/verify.sh` states this limitation in its +header; this is the fix for it. Expect a first run to surface a backlog of type errors in tests — +land the config and the fixes together so the gate never ships red. + +**FIXED 2026-09-02.** `desktop/tsconfig.tests.json` extends the base and changes exactly three +things, each measured rather than guessed: `moduleResolution: "bundler"` (vitest resolves through +Vite, and classic node resolution alone produced 12 phantom "cannot find module" errors for `vite` +and `@vitejs/plugin-react`), `allowJs` + `checkJs: false` (several suites import the untyped +`test-engine/*.mjs` orchestrator — 57 implicit-any errors without it), and `DOM.Iterable` in `lib` +(iterating a `querySelectorAll` result, 5 errors). After those three the tree still held **201 real +type errors in 57 files**; those files are listed one per line in the config's `exclude` so the gate +ships green, and `scripts/verify.sh` prints the remaining count on every run. 514 of 571 files are +type-checked today. One error was fixed rather than excluded — `tests/helpers/chat-store-harness.ts` +is imported by three suites, and `exclude` does not apply to a file another included file imports. + +The lint half landed in the same change: a `tests/**` block in `eslint.config.mjs` carrying the +syntactic (project-free) half of the src rule set. It found 5 errors across 4 files, all false +positives on deliberate code, each now carrying a named `eslint-disable` with its reason. The +type-aware rules are deliberately absent until the exclude list is empty — pointing them at the +tests project would fail all 57 excluded files with "not found in project", a config error dressed +up as a lint finding. + +**History.** Filed 2026-08-06; re-verified 2026-09-01 (still a single tsconfig); fixed 2026-09-02. diff --git a/docs/active/investigations/2026-09-01-workbench-boot-check-dead-port.md b/docs/archive/investigations/2026-09-01-workbench-boot-check-dead-port.md similarity index 72% rename from docs/active/investigations/2026-09-01-workbench-boot-check-dead-port.md rename to docs/archive/investigations/2026-09-01-workbench-boot-check-dead-port.md index 3ced2ee2..23eaad26 100644 --- a/docs/active/investigations/2026-09-01-workbench-boot-check-dead-port.md +++ b/docs/archive/investigations/2026-09-01-workbench-boot-check-dead-port.md @@ -1,6 +1,6 @@ --- date: 2026-09-01 -status: active +status: shipped type: investigation topic: workbench-boot-check.mjs reports every route "ok" when nothing is serving the port --- @@ -26,4 +26,13 @@ of the app exists in the DOM; fail loudly when the port refuses a connection. Th anything reachable only by clicking into a panel (its header says it exercises MOUNT only) — see the Backup & Sync workbench crash report for that class. -**History.** Filed 2026-08-27 (hit while building the download-resume UI); re-verified 2026-09-01. +**FIXED 2026-09-02.** All three assertions from the fix shape landed, each proven to fire on its +own: a preflight HTTP request (dead port -> exit 2, "nothing is serving the workbench on port N", +before Chrome is even launched); the `Page.navigate` errorText plus the main document's HTTP status +(a server answering 500 -> "server answered HTTP 500 for the page"); and `#root`, which +`index.html` ships inline (a server that is up but is not the workbench -> "#root is not in the DOM"). +Verified green against a real workbench: 16/16 routes, exit 0. Guarded by +`scripts/workbench-boot-check.test.mjs`, which needs no Chrome, and run by Workspace CI. + +**History.** Filed 2026-08-27 (hit while building the download-resume UI); re-verified 2026-09-01; +fixed 2026-09-02. diff --git a/docs/roadmap/dev-workspace.md b/docs/roadmap/dev-workspace.md index 9905d3f2..29b6b97a 100644 --- a/docs/roadmap/dev-workspace.md +++ b/docs/roadmap/dev-workspace.md @@ -10,30 +10,29 @@ seen-on is always n/a here. and a missing git are all unpinned. None is a known failure `n/a` `needs-verify` `checked 2026-09-02` -- [ ] About a hundred fixed sleeps still stand in for real signals across the desktop suite, and - the MCP startup-wiring test blows even the 30 s budget once eight full suites run at once - (times out at 5 s under lighter load) — left on purpose after youcoded#362/#363 - `n/a` `confirmed` `checked 2026-09-01` → docs/active/investigations/2026-09-01-fixed-sleeps-and-mcp-wiring-import.md - -- [ ] The desktop test tree (~350 files) is neither type-checked nor linted — a broken type in a - test only shows up when the test runs - `n/a` `confirmed` `checked 2026-09-01` → docs/active/investigations/2026-09-01-desktop-tests-not-typechecked.md - -- [ ] verify.sh's related-tests fail with Vite "Denied ID …?inline" in any worktree whose - node_modules is a symlink to the main checkout; the hardlink-farm copy (`cp -al`) is now the - documented convention and sidesteps it, so a config-level fix is deliberately not now - `n/a` `parked` `checked 2026-09-01` +- [ ] 102 fixed sleeps still stand in for real signals across the desktop suite (was 108; the six + worst in native-session-host — five copies of "guess 20 ms that the child's turn started", + the bug youcoded#363 already fixed once in that file, plus one whose own comment said "poll" + while it slept — now wait on the real event). The MCP startup-wiring test did not blow its + budget in any of 27 local runs on 2026-09-02, including two 8-way concurrent sweeps + `n/a` `confirmed` `checked 2026-09-02` → docs/active/investigations/2026-09-01-fixed-sleeps-and-mcp-wiring-import.md + +- [ ] 57 test files are excluded from the new test typecheck — they hold the 201 type errors it + found on the day it was switched on, mostly fixtures built as partial objects. Named one per + line in `desktop/tsconfig.tests.json`; verify.sh prints the remaining count every run + `n/a` `confirmed` `checked 2026-09-02` - [ ] The sync-spaces engine test goes red on the macOS CI leg every week or two — on branches that touch nothing in sync, and on untouched master — with zero watcher events delivered; Ubuntu and Windows pass the same commit, and every fire also skips macOS packaging `n/a` `needs-verify` `checked 2026-09-01` → docs/active/investigations/2026-09-01-sync-engine-debounce-macos-flake.md -- [ ] Three suites still flake under parallel load and pass alone: subagent-view, mcp-startup-wiring - (5 s timeout), and project-watcher ("expected [add] to include edit", ubuntu, youcoded#375 - run 33553046644 on 2026-09-01); the fourth member, sync-warning-self-clear, was fixed by - youcoded#317 - `n/a` `needs-verify` `checked 2026-09-01` +- [ ] subagent-view, mcp-startup-wiring and project-watcher were filed as the suites that flake + under parallel load, but 27 full local runs on 2026-09-02 (1 alone, 6 concurrent, 4 pinned to + 4 cores, 2 x 8 concurrent) never failed any of the three — the four that DID fail at 8-way + concurrency were different files and are fixed. Either these three need a different trigger + (the project-watcher hit was Ubuntu CI, not local) or they are already fixed + `n/a` `needs-verify` `checked 2026-09-02` - [ ] The lint gate only enables rules already at zero; the deferred list at the bottom of the ESLint config still fires — 79 renderer floating promises and 43 exhaustive-deps hits (the @@ -68,10 +67,6 @@ seen-on is always n/a here. records rounds, Destin-seconds, reopens and rows that failed at acceptance `n/a` `in-flight` `checked 2026-09-02` → docs/active/plans/2026-09-01-feature-flow-plan.md -- [ ] The workbench boot check prints "ok" for all 12 routes and exits 0 when nothing is serving - the port at all — a green run does not prove the app mounted - `n/a` `confirmed` `checked 2026-09-01` → docs/active/investigations/2026-09-01-workbench-boot-check-dead-port.md - - [ ] Opening Settings → Backup & Sync in the workbench takes the whole thing down to "YouCoded failed to start"; the boot check cannot see it and the review sweep counts the error state as covered diff --git a/docs/roadmap/shipped.md b/docs/roadmap/shipped.md index 33f74440..28c31799 100644 --- a/docs/roadmap/shipped.md +++ b/docs/roadmap/shipped.md @@ -502,3 +502,6 @@ Every `[x]` item from the single-file roadmap as it stood at the migration base, - [x] Bundled write-guard never actually blocked (exit-1/stdout vs CC's exit-2/stderr deny contract) `bug` `#hooks` (added 2026-07-15) Found during issue youcoded#86 verification (CC only denies PreToolUse on exit 2 + stderr; the guard exited 1 + stdout → no-op in EVERY permission mode). Fixed 2026-07-15: youcoded-core PR #119 (source copy, verified with sandboxed claude -p probes against CC v2.1.211) + youcoded PR #144 (both app-bundled copies + a parity/contract pinning test). CC-command-list refresh + red-stall fixtures landed the same day in youcoded PR #143 (issues #85/#87). - [x] 2026-09-02 native-harness — The assistant can hand you a link as a deliverable (live web, localhost or LAN IP) — a tile in the Deliverables card that opens in your browser on click; works in Claude Code sessions too, via a per-session MCP server the app attaches with `--mcp-config` (nothing written to `~/.claude.json`), and on Android (youcoded#381 `e4bcf98e`) → `docs/archive/plans/2026-09-02-send-user-link-deliverables.md` +- [x] 2026-09-02 dev-workspace — `workbench-boot-check.mjs` said "All 16 workbench routes mount cleanly" with nothing serving the port; it now preflights the port (exit 2), checks the navigation resolved and the document's HTTP status, and requires `#root` in the DOM (youcoded-dev, `scripts/workbench-boot-check.test.mjs` + a CI step) +- [x] 2026-09-02 dev-workspace — The desktop test tree was neither type-checked nor linted; `desktop/tsconfig.tests.json` + an eslint block cover it now (514 of 571 files type-checked, all 571 linted at zero errors) +- [x] 2026-09-02 dev-workspace — verify.sh's related-tests failed with Vite `Denied ID …?inline` in a symlinked-node_modules worktree (60 of 84 files died at import); vitest.config.ts now allows the resolved node_modules and verify.sh says out loud that a symlink is still unsafe for `npm ci` diff --git a/scripts/verify.sh b/scripts/verify.sh index fc52fb48..5edee56e 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -24,9 +24,11 @@ # mapping to drive them from. A green run here says nothing about Android. # * The marketplace worker has its own CI (wecoded-marketplace/.github/) and is # not run here either. -# * `tsc --noEmit` uses desktop/tsconfig.json, whose `include` is `src/**/*`. -# Test files under tests/ are therefore NOT type-checked by it — vitest -# executes them, but esbuild strips types without checking them. +# * `tsc --noEmit` runs TWICE: desktop/tsconfig.json (src/**) and +# desktop/tsconfig.tests.json (the test tree). Until 2026-09-02 only the +# first existed and nothing type-checked a single test file. The second one +# still EXCLUDES the files that were already failing when it was introduced +# — the count is printed on every run so the debt cannot go quiet. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -78,6 +80,24 @@ DESKTOP="$CHECKOUT/desktop" exit 2 } +# A SYMLINKED node_modules is not a supported shape, and it used to fail +# silently: Vite resolved through the link to the main checkout, its file guard +# denied the resulting path, and ~60 suites died at import with +# `Denied ID .../github-dark.css?inline` while the summary said only "tests +# failed". vitest.config.ts now allows the resolved directory, so the suites run +# — but the OTHER hazard is unfixable from here and worse: `npm ci` and Gradle's +# bundleWebUi follow the link and empty the MAIN checkout's node_modules for +# every worktree at once (workspace CLAUDE.md, verified 2026-08-13). Say so +# loudly rather than letting a green run imply the setup is fine. +if [[ -L "$DESKTOP/node_modules" ]]; then + echo "WARNING: $DESKTOP/node_modules is a SYMLINK to $(readlink "$DESKTOP/node_modules")" >&2 + echo " Tests will run, but do NOT run 'npm ci' or any Gradle task in this" >&2 + echo " checkout — both follow the link and wipe the shared copy." >&2 + echo " Replace it with a hardlink farm:" >&2 + echo " rm '$DESKTOP/node_modules' && cp -al /desktop/node_modules '$DESKTOP/node_modules'" >&2 + echo "" >&2 +fi + # Default base ref: prefer a local master, fall back to the remote. A worktree # created straight from origin/master may have no local master ref at all. if [[ -z "$BASE" ]]; then @@ -173,6 +193,7 @@ echo "" if [[ $DRY -eq 1 ]]; then echo "would run:" echo " npx tsc --noEmit -p tsconfig.json" + echo " npx tsc --noEmit -p tsconfig.tests.json" echo " npm run knip" echo " npm run lint" if [[ $RUN_FULL -eq 1 ]]; then @@ -185,6 +206,14 @@ if [[ $DRY -eq 1 ]]; then fi start types "types (tsc --noEmit)" npx tsc --noEmit -p tsconfig.json +# The test tree is its own TS project (different module resolution, allowJs for +# the .mjs orchestrator). Separate check so a failure names which tree broke. +# Older checkouts have no tsconfig.tests.json; skip rather than fail on them. +if [[ -f "$DESKTOP/tsconfig.tests.json" ]]; then + TESTS_EXCLUDED=$(grep -cE '^ *"tests/.*\.tsx?"' "$DESKTOP/tsconfig.tests.json" || true) + start testtypes "types in tests/ (tsc --noEmit, ${TESTS_EXCLUDED} file(s) still excluded)" \ + npx tsc --noEmit -p tsconfig.tests.json +fi start knip "dead code (knip)" npm run knip --silent # eslint is the bug gate, not a style gate — it catches the classes tsc/knip # structurally cannot (conditional React hooks, floating promises in main, @@ -203,7 +232,7 @@ fi start invariants "invariants (ast-grep)" bash "$ROOT/scripts/ast-grep/check.sh" "$DESKTOP/src" FAILED=0 -for key in types tests knip lint invariants; do +for key in types testtypes tests knip lint invariants; do [[ -n "${PID[$key]:-}" ]] || continue wait "${PID[$key]}"; rc=$? if [[ $rc -eq 0 ]]; then diff --git a/scripts/workbench-boot-check.mjs b/scripts/workbench-boot-check.mjs index 8b21a2de..7557a837 100755 --- a/scripts/workbench-boot-check.mjs +++ b/scripts/workbench-boot-check.mjs @@ -27,6 +27,20 @@ // // Requires a Chrome/Chromium binary; it launches its own headless instance on a // scratch profile and cleans it up. +// +// FALSE-GREEN THIS SCRIPT USED TO HAVE (fixed 2026-09-02): with NOTHING serving +// the port, Chrome rendered its own net::ERR_CONNECTION_REFUSED page for every +// route. That page has no "failed to start" text, no #boot spinner and throws no +// exception -- so all sixteen routes printed `ok` and the script exited 0 saying +// "All 16 workbench routes mount cleanly". CLAUDE.md tells every session to trust +// this check, so a dead server read as a passing app. Three things now have to be +// true before a route counts as ok, and each is asserted separately so the failure +// says which one broke: +// 1. a preflight HTTP request to the workbench port succeeds (else exit 2); +// 2. the navigation itself resolved -- CDP reports no `errorText`, and the main +// document response was not 4xx/5xx; +// 3. #root is present in the DOM. index.html ships #root inline, so its absence +// means the bytes we loaded were not the workbench at all. import { spawn, spawnSync } from 'node:child_process'; import { mkdtempSync, rmSync } from 'node:fs'; @@ -84,6 +98,29 @@ const ROUTES = [ ['app · first-run wizard', '&child=1&scenario=default&firstRun=AUTHENTICATE'], ]; +// A dead port is the failure mode this script exists to catch and used to miss. +// Prove something answers HTTP before spending two minutes rendering Chrome's +// error page sixteen times. +async function preflight() { + let last = null; + for (let i = 0; i < 3; i += 1) { + try { + const res = await fetch(BASE, { method: 'GET' }); + if (res.ok) return; + last = `HTTP ${res.status}`; + } catch (err) { + last = err?.cause?.code ?? err?.message ?? String(err); + } + await new Promise((r) => setTimeout(r, 500)); + } + console.error(`boot-check: nothing is serving the workbench on port ${PORT} (${last}).`); + console.error(' Start it first: bash scripts/run-workbench.sh '); + console.error(' Refusing to report routes as passing against a dead port.'); + process.exit(2); +} + +await preflight(); + const CHROME = ['google-chrome-stable', 'google-chrome', 'chromium', 'chromium-browser']; function findChrome() { @@ -134,6 +171,11 @@ async function checkRoute(url) { let id = 0; const pending = new Map(); const errors = []; + // Main-document HTTP status, filled in by Network.responseReceived. A Vite dev + // server that is up but has lost the entry (404) or blown up (500) is a boot + // failure we used to render as `ok` -- the error body simply has no #boot. + let docStatus = null; + let netFailure = null; const send = (method, params = {}) => new Promise((res) => { const i = ++id; pending.set(i, res); @@ -148,11 +190,27 @@ async function checkRoute(url) { errors.push(msg.params.exceptionDetails?.exception?.description ?? msg.params.exceptionDetails?.text ?? 'unknown exception'); } + // Only the top-level document decides pass/fail here; a 404 on some optional + // asset is not a boot failure, and Runtime.exceptionThrown already covers a + // missing module. + if (msg.method === 'Network.responseReceived' && msg.params.type === 'Document' + && docStatus === null) { + docStatus = msg.params.response.status; + } + if (msg.method === 'Network.loadingFailed' && msg.params.type === 'Document' + && !netFailure) { + netFailure = msg.params.errorText ?? 'document load failed'; + } }; await send('Runtime.enable'); await send('Page.enable'); - await send('Page.navigate', { url }); + await send('Network.enable'); + // Page.navigate's OWN result carries errorText when the navigation never + // resolved (connection refused, DNS failure, bad scheme). This is the single + // strongest signal that the port is dead, and it was previously discarded. + const nav = await send('Page.navigate', { url }); + const navError = nav?.errorText ?? null; // Long enough for the 3s firstRun timeout and the classifier's first tick. await new Promise((r) => setTimeout(r, 6000)); @@ -161,13 +219,17 @@ async function checkRoute(url) { failedToStart: document.body.innerText.includes('failed to start'), message: document.body.innerText.slice(0, 200), stillBooting: !!document.getElementById('boot'), + hasRoot: !!document.getElementById('root'), })`, returnByValue: true, }); ws.close(); await fetch(`http://127.0.0.1:${CDP_PORT}/json/close/${target.id}`); - return { ...JSON.parse(probe.result.value ?? '{}'), errors }; + // A throw inside Runtime.evaluate (or a page we could not reach at all) must not + // read as "every assertion passed" -- default hasRoot to false, not undefined. + const dom = { hasRoot: false, ...JSON.parse(probe?.result?.value ?? '{}') }; + return { ...dom, errors, navError, netFailure, docStatus }; } await waitForCdp(); @@ -177,10 +239,18 @@ for (const [label, query] of ROUTES) { const r = await checkRoute(BASE + query); // `stillBooting` catches the case the error boundary never runs because the // module graph failed to load at all — the #boot spinner is still in the DOM. - const bad = r.failedToStart || r.stillBooting || r.errors.length > 0; + // The navigation/#root assertions catch the case we never loaded the workbench + // at all, which every one of these routes used to report as `ok`. + const httpBad = typeof r.docStatus === 'number' && r.docStatus >= 400; + const bad = r.navError || r.netFailure || httpBad || !r.hasRoot + || r.failedToStart || r.stillBooting || r.errors.length > 0; if (bad) { failed += 1; console.log(`FAIL ${label}`); + if (r.navError) console.log(` navigation failed: ${r.navError}`); + if (r.netFailure) console.log(` document did not load: ${r.netFailure}`); + if (httpBad) console.log(` server answered HTTP ${r.docStatus} for the page`); + if (!r.hasRoot) console.log(' #root is not in the DOM — this page is not the workbench'); if (r.failedToStart) console.log(` error boundary: ${r.message.replace(/\n/g, ' | ')}`); if (r.stillBooting) console.log(' never mounted (boot spinner still present)'); for (const e of [...new Set(r.errors)].slice(0, 5)) { diff --git a/scripts/workbench-boot-check.test.mjs b/scripts/workbench-boot-check.test.mjs new file mode 100644 index 00000000..5befe14c --- /dev/null +++ b/scripts/workbench-boot-check.test.mjs @@ -0,0 +1,52 @@ +// Guard for the false green that made this script worthless. +// +// Until 2026-09-02, `workbench-boot-check.mjs` run against a port NOTHING was +// serving printed `ok` for all sixteen routes and exited 0 with "All 16 +// workbench routes mount cleanly" — Chrome rendered its own +// net::ERR_CONNECTION_REFUSED page, which has no "failed to start" text, no +// #boot spinner, and throws no exception. CLAUDE.md tells every session to trust +// this check, so a dead dev server read as a passing app. +// +// This asserts the refusal, and does it WITHOUT Chrome: the preflight now runs +// before the browser is discovered or launched, so the whole check is one HTTP +// request when the port is dead. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { createServer } from 'node:http'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'workbench-boot-check.mjs'); + +/** A port nothing is listening on: bind one, read it, close it. */ +async function freePort() { + const srv = createServer(); + await new Promise((r) => srv.listen(0, '127.0.0.1', r)); + const { port } = srv.address(); + await new Promise((r) => srv.close(r)); + return port; +} + +test('refuses loudly when nothing is serving the workbench port', async () => { + const port = await freePort(); + const { status, stderr } = spawnSync(process.execPath, [SCRIPT, String(port)], { + encoding: 'utf8', + timeout: 60_000, + }); + assert.equal(status, 2, `expected exit 2, got ${status}\n${stderr}`); + assert.match(stderr, /nothing is serving the workbench on port/); + assert.match(stderr, /run-workbench\.sh/); + // The old false green, spelled out so a regression is unmistakable. + assert.doesNotMatch(stderr, /routes mount cleanly/); +}); + +test('does not report routes as passing against a dead port', async () => { + const port = await freePort(); + const { stdout } = spawnSync(process.execPath, [SCRIPT, String(port)], { + encoding: 'utf8', + timeout: 60_000, + }); + assert.doesNotMatch(stdout, /^ok /m); + assert.doesNotMatch(stdout, /mount cleanly/); +}); From 7315cd609ca0aa028b5cf5c27090eda92e8a8830 Mon Sep 17 00:00:00 2001 From: Destin Date: Wed, 2 Sep 2026 06:50:48 -0700 Subject: [PATCH 2/2] =?UTF-8?q?docs(handoff):=20overnight=20session=20C=20?= =?UTF-8?q?=E2=80=94=20what=20reproduced,=20what=20was=20already=20fixed,?= =?UTF-8?q?=20what=20is=20left?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01T9SRtMoZJNF4sJrodQa1N1 --- .../handoffs/2026-09-02-session-c-report.md | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 docs/active/handoffs/2026-09-02-session-c-report.md diff --git a/docs/active/handoffs/2026-09-02-session-c-report.md b/docs/active/handoffs/2026-09-02-session-c-report.md new file mode 100644 index 00000000..6ae70863 --- /dev/null +++ b/docs/active/handoffs/2026-09-02-session-c-report.md @@ -0,0 +1,285 @@ +--- +date: 2026-09-02 +status: shipped +type: handoff +topic: overnight session C — making the verification tooling honest +--- + +# Session C — the verification tooling now tells the truth about itself + +**PRs:** youcoded#384 (the app's test tree) · youcoded-dev#19 (the workspace tooling). +Headless throughout — no dev window, no `run-dev.sh`. + +The short version: **three of the eight filed items were already fixed on `master`** and only +looked broken because the shared checkout is ~100 commits behind. Two were real and are fixed. +One was real but its *named victims* were wrong, and it is re-scoped rather than closed. Every +claim below has the command output behind it. + +--- + +## 1. The workbench boot check — REAL, fixed + +**Reproduced on a clean checkout, first thing:** + +``` +$ node scripts/workbench-boot-check.mjs 5999 # nothing listening on 5999 +ok parent frame (toolbar) +ok app · default +… (14 more) +All 16 workbench routes mount cleanly. +$ echo $? +0 +``` + +Chrome renders its own `ERR_CONNECTION_REFUSED` page. That page has no "failed to start" text, +no `#boot` spinner, and throws no exception — so all three of the script's probes read clean and +every route scored `ok`. `CLAUDE.md` tells every session to run this after any mock-shim change, +so a dead dev server read as a passing app. + +**Fixed with three assertions, each proven to fire on its own** (not inferred — each was driven +by a purpose-built server): + +| Assertion | How it was proven | +|---|---| +| Preflight HTTP request to the port | dead port → `exit 2`, `nothing is serving the workbench on port 5999 (ECONNREFUSED)` | +| `Page.navigate` errorText + document HTTP status | a server answering 500 → `server answered HTTP 500 for the page` | +| `#root` present in the DOM | a live server that is not the workbench → `#root is not in the DOM — this page is not the workbench` | + +**No false red:** against a real workbench on port 5263, `16/16 routes, exit 0`. + +The preflight was moved **above** the Chrome launch, which has a second payoff: the whole +dead-port case is now one HTTP request, so `scripts/workbench-boot-check.test.mjs` can guard it +on a runner with no browser. Wired into Workspace CI. `CLAUDE.md`'s stale route count +(12) corrected to 16. + +--- + +## 2. `harness-eval-orchestrator.test.ts` — the brief was stale; the flake is real but different + +**The filed symptom did not reproduce.** The brief said the test at ~:1747 expects 2 transcript +files and gets 1. Run alone on clean `master`: + +``` +$ npx vitest run tests/harness-eval-orchestrator.test.ts + Test Files 1 passed (1) + Tests 86 passed (86) +``` + +**No bisect was run, because there was nothing red to bisect.** `docs/roadmap/shipped.md` already +carries `- [x] 2026-09-01 dev-workspace — harness-eval-orchestrator.test.ts has a red test on +master (youcoded 780de530 + 6cab56b9 in #362/#363; 86/86 green on master f2d229e4)`. The +transcript-count failure was the concurrency bug that the `YOUCODED_EVAL_RUNS_DIR` override fixed +on 2026-08-28 — before that, the suite wrote into the real workspace and two concurrent runs +snapshot/restored over each other. + +**The second half of the brief WAS real.** *"does not advertise models a `--only` run will not +touch"* times out — see §4. + +--- + +## 3. `ipc-handlers.test.ts` import-time flake — already fixed; the live one is a different file + +`docs/roadmap/shipped.md:280` closes it: `SkillConfigStore.save()` writes a per-writer temp name +(`.${pid}.${seq}.tmp`) since `f05b2711` (2026-08-05), so the ENOENT rename collision cannot +happen. The surviving relative is `mcp-startup-wiring.test.ts`, which `await import()`s all of +`ipc-handlers.ts` inside five test bodies. **It did not fail once in 27 full local runs**, +including two 8-way concurrent sweeps. Its investigation says not to raise the budget again +without a real CI failure, and I did not. + +--- + +## 4. The parallel-load flakes — REAL, four of them, none of them the three that were filed + +**How they were forced.** Twenty-seven full suite runs on this tree, escalating load: + +| Load | Runs | Result | +|---|---|---| +| 1 suite alone | 1 | green, 37 s | +| 6 concurrent full suites | 6 | 6/6 green | +| 4 concurrent, pinned to 4 cores (`taskset -c 0-3`) | 4 | 4/4 green | +| **8 concurrent full suites** | 8 | **7 of 8 runs failed** | +| 8 concurrent, after the fixes | 8 | **8/8 green**, 7,828 tests each, 166 s per run | + +**The shared cause of two of the four:** a fixed wall-clock per-test budget standing in for "did +it hang?", on tests whose work is spawning real processes or driving 200 real turns. That budget +was measured on an idle machine; under contention it measures the machine, not the code. + +- `harness-eval-orchestrator` › *does not advertise models a `--only` run will not touch* — + `Test timed out in 30000ms` in **3/8** runs. It spawns **two** real node processes; most tests + in the file already pass an explicit `}, 60_000)` and this one did not. The file went from ~17 s + alone to 138 s under 8-way load. +- `harness-review-runner` › *survives the max_steps gate up to STEP_GATE_ALLOWANCE* — same + timeout, **4/8**. It drives `STEP_GATE_ALLOWANCE * BATTERY_STEP_BUDGET` = 200 tool calls through + a real `HarnessSession`, and it sits **above** the comment in its own file that tells every test + below it to pass `HEAVY_RUN_TIMEOUT_MS` (120 s). 157 s under load. + + Both fixed with a **file-level** `vi.setConfig`, not another hand-applied constant — relying on + each author to remember is precisely what failed. + +- `comment-list` › *shows a held comment once…* — `expected "vi.fn()" to be called…, Number of + calls: 0`. `onHeldListed` fires from a passive effect, one beat after the commit the test waited + on. Now inside a `waitFor`. +- `feedback-section` › *posts a comment and makes the thread re-read* — `expected +0 not to be +0`. + It snapshotted the props array into a local and then waited on the **snapshot**, which can never + change. Now reads the live array inside the `waitFor`. (This one only surfaced *after* the first + three were fixed — it had been masked by whichever suite failed first.) + +**The three suites that were filed — `subagent-view`, `mcp-startup-wiring`, `project-watcher` — +never failed in any of the 27 runs.** That entry is re-scoped, not closed: the `project-watcher` +sighting was Ubuntu CI, not local, so the trigger may simply be a different machine shape. Saying +"fixed" about something I could not make fail would be exactly the dishonesty this session was +about. + +--- + +## 5. `desktop/tests/` type-checking and linting — REAL, landed with a named debt + +`tsconfig.json`'s `include` was `src/**/*`, so **nothing had ever type-checked a test file**. + +Three compiler options were needed before the real errors were visible, each measured rather than +guessed: `moduleResolution: "bundler"` (12 phantom "cannot find module" errors for `vite` and +`@vitejs/plugin-react` under the base's classic node resolution), `allowJs`+`checkJs:false` (57 +implicit-any errors from importing the untyped `test-engine/*.mjs`), and `DOM.Iterable` (5, from +iterating a `querySelectorAll` result). 258 → 246 → **201 real errors in 57 files.** + +**Those 57 files are excluded, by name, one per line**, so the gate ships green — the same +green-gate rule the ESLint config states. `scripts/verify.sh` prints the count on every run: + +``` +PASS types in tests/ (tsc --noEmit, 57 file(s) still excluded) +``` + +**514 of 571 files are type-checked today.** One error was fixed rather than excluded: +`tests/helpers/chat-store-harness.ts` is imported by three suites, and `exclude` does not apply to +a file an included file imports — a useful thing to know before assuming the list is a wall. + +The lint half found only **5 errors in 4 files**, every one a false positive on deliberate code +(three literal `${…}` strings that are the subject of their test, a helper named `use` that builds +a tool-*use* event, and a deliberately >2^53 integer). Each now carries a named `eslint-disable` +with its reason, so the rules stay on for future tests. Type-aware rules are deliberately absent +from the tests block: pointing them at a project that excludes 57 files fails all 57 with "not +found in project", which is a config error dressed up as a lint finding. + +--- + +## 6. The audit-staleness reminder — ALREADY FIXED on master; proven firing + +No code change was needed. `git log` shows `5598f69 fix(hooks): audit-staleness reminder skips +baseline files instead of picking the newest name`, and `.claude/hooks/context-inject.test.mjs` +already carries three cases for it. **The reason it "never fired" is that the shared youcoded-dev +checkout is ~100 commits behind `origin/master`.** + +Proven firing by running the real hook against a probe report: + +``` +### ⚠️ Audit staleness +Latest audit (2026-09-02-PROBE.md) is 243 days old. Consider running `/audit`. + +### ⚠️ Unapplied audit findings +3 open item(s) in 2026-09-02-PROBE.md. Review the ## Residue section. +``` + +It is silent today for the right reason: the newest non-baseline report is `2026-09-01.md`, one +day old, `residue: 0`. + +--- + +## 7. Anchors and the four "already done" entries — all already closed + +``` +$ node scripts/audit-anchors.mjs --no-diff +anchors: 394/394 ok · MAP paths: 345/345 ok · eager ≈7992 tokens (limit 10000) +MECHANICAL PASS: OK +``` + +All four entries the difficulty ranking flagged are already `[x]` in `docs/roadmap/shipped.md`: +the CI anchor cron (closed 2026-09-01), the two doc anchors (2026-09-01), the curated-defaults +dead id (2026-09-01), and `conversation-triage.mjs`, which was **dropped** on 2026-09-02 with the +reason recorded ("the /wrap-up skill makes each session report its own friction"). I confirmed the +file is in neither git nor the working tree and did **not** re-create it — re-creating a 526-line +tool that a decision already replaced would be building something nobody asked for. + +The roadmap tool then found an entry I *had* fixed, by its own anchor: + +``` +### Claims — 101 checked, 1 broken +- dev-workspace:70 The workbench boot check prints "ok" for all 12 routes and exits 0 … +``` + +Closed and archived. Now `100 checked, 0 broken`. + +--- + +## 8a. `verify.sh` in a symlinked worktree — REAL, fixed properly rather than warned around + +**Reproduced** in a throwaway worktree whose `desktop/node_modules` was a symlink to the main +checkout: + +``` +Caused by: Error: Denied ID …/highlight.js/styles/github-dark.css?inline + Test Files 60 failed | 24 passed (84) + Tests 255 passed (255) +``` + +Note the shape: **60 files failed and 0 assertions failed.** Vite resolves through the symlink to +the real path, then its dev-server file guard denies anything outside the project root. The only +imports that pass through that guard are Vite-transformed asset URLs, so the failure is not +"module not found" but a denial thrown at module load — and the summary blames your diff. + +Fixed in `vitest.config.ts` by naming the resolved `node_modules` in `server.fs.allow`. Same +worktree, after: **`Test Files 84 passed (84)`**. This is test-config only; the app's +`vite.config.ts` is untouched. + +**This does not make a symlinked `node_modules` safe** — `npm ci` and Gradle's `bundleWebUi` still +follow it and empty the shared copy for every worktree at once. So `verify.sh` now *also* prints a +loud warning naming that hazard and the `cp -al` replacement command. Real fix for the lie, loud +warning for the danger that cannot be fixed from there. + +## 8b. Fixed sleeps standing in for signals — 6 of 108 converted + +`108` counted (not "about a hundred"): 34 in `native-session-host.test.ts` alone. **Six were +taken, and the choice of which six is the point.** + +Five were literally the same line, five times: `send('go')` followed by +`await new Promise(r => setTimeout(r, 20))` — a guess that a child's turn had started. That is the +exact bug youcoded#363 fixed once in this same file, and it left a helper behind +(`waitForTurnInFlight`, which matches on `data.agentId` because a child's events arrive re-stamped +under the parent's session id). **The helper was being used at one site out of six.** The sixth had +a comment reading "The ledger write is fire-and-forget — poll for it" directly above a +`setTimeout(r, 30)`; it now polls. + +The rest were read and deliberately left. Most of the large ones (120/150/80/80/150 ms) are +**negative** assertions — "wait a bounded time and prove nothing happened" — which have no signal to +wait on by construction, and the `setTimeout(r, 10)` majority sit inside fake model streams where +they *are* the simulated work. Converting those would be churn at best and a weakened test at +worst. + +--- + +## What is green now that was not + +- `node scripts/workbench-boot-check.mjs ` → exit 2 with a real explanation, instead of + exit 0 and "All 16 routes mount cleanly". +- `tsc -p desktop/tsconfig.tests.json` → 0 errors over 514 test files that nothing had ever + checked; `npm run lint` covers all 571. +- 8 concurrent full suites → 8/8 green, where the same load was 1/8 before. +- `npx vitest related` in a symlinked-node_modules worktree → 84/84, where it was 24/84. +- `scripts/verify.sh` reports six checks instead of five, and states its own remaining blind spot + (57 excluded files) in the pass line. + +## What was left behind, and why + +1. **201 type errors in 57 test files.** Filed as its own roadmap entry with the list living in + `tsconfig.tests.json`. They are mostly fixtures built as partial objects; each needs a judgment + call about what the fixture should assert, and making 201 of those unsupervised at 6 a.m. is how + a test gets quietly weakened. Deleting a line from the exclude list and fixing that file is the + intended way to pay it down. +2. **The three filed flaky suites stay open, re-scoped.** They did not fail locally in 27 runs. The + next step is a CI-shaped runner (2 cores), not more local concurrency. +3. **`mcp-startup-wiring`'s in-body `await import()`** is untouched, per its investigation's own + instruction — it never failed here. +4. **96 remaining fixed sleeps**, characterised above so the next session can pick without + re-reading all of them. +5. **Type-aware ESLint rules on `tests/**`** — blocked on (1). +6. **`docs/roadmap/dev-workspace.md`'s macOS sync-spaces flake** was not touched; it needs the + macOS CI leg, which this machine is not.