Windows: make dev build, compile and test on Windows - #62
Conversation
Windows is a shipping platform and nothing in this pipeline compiled for it. The cost was exact: `browser_host.rs` reached for `ns_window()` — macOS-only — in a module `lib.rs` declares unconditionally, and the default Windows build stopped linking. Every job was green through review and merge. `shell: bash` on the scripted steps, because the Windows runner defaults to PowerShell where `if ! cmd; then`, `mkdir -p` and `: >` are syntax errors, and that image ships Git Bash. The externalBin stub gets its `.exe` name there. The bundled-Chromium type-check stays macOS-only: that engine's backend IS AppKit, so asking Windows to build it would fail on an honest absence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es anyway `ns_window()` exists only on macOS, and this file is compiled on every platform because `lib.rs` declares the module unconditionally — so without a gate the default Windows build did not compile at all, whether or not CEF was enabled. `None` off macOS rather than an HWND: the trait already says a window that is not there is a NORMAL answer, and there is no NSWindow to parent into off macOS. The backend behind this host is AppKit throughout, so handing it an HWND would buy a compile and not a working view. Gmail keeps working on Windows through the OS webview, which is the path `cef::is_ready()` already falls back to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CallDetector` and `DetectorConfig` are used only by the macOS-gated `CallDetector::start` below, so off macOS they were two unused imports — a warning, and a hard error under CI's `-D warnings`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o avoid Two halves of one bug, both of which left a dev build signed out. The dev-token file was looked up under `HOME` alone. That is a Unix name, set on Windows only when something like Git Bash synthesises it — a GUI launch has just `USERPROFILE`, so the lookup resolved to nothing and skipped the dev token: the same binary worked under `npm run dev` from bash and not when double-clicked. Reading both is what makes this agree with `brains_storage::default_root()`. The early `return Ok(None)` is now macOS-only, and the gate is the point. The dialog it exists to avoid is a macOS mechanism — keychain items carry a per-binary ACL there, and every rebuild is a new binary. Windows Credential Manager has no such ACL and never prompts; a read is silent. So off macOS that return bought nothing and cost the only source that works, and a Windows dev build with a token sitting in Credential Manager reported "no brains token" on every launch — signed out of boards, mini-sites and the MCP tool list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GNU tar — what Git for Windows puts first on PATH — reads an absolute `C:\...`
argument as a `host:path` remote spec and aborts ("Cannot connect to C:").
bsdtar, which macOS ships, does not care either way, so relative paths plus an
explicit `cwd` is the one form that works on both.
This runs inside `beforeBuildCommand`, so getting it wrong failed the whole
build rather than just the recording assets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…le without it `:` was hardcoded as the PATH list separator. That does not merely fail to split on Windows — it splits in the WRONG PLACES, because every entry carries a drive letter. `C:\Windows\system32` came apart into `C` and `\Windows\system32`, neither of them a directory, so every existence-checked entry was dropped and the composed PATH came back empty. `resolve_claude_path` then searched it for a CLI it could never find, and the app reported the CLI as missing on a machine where it was installed and on PATH. One constant, used by both providers — codex already shares `find_in_dirs` from here and split its own PATH the same wrong way. The regression test asserts through a resolution rather than a round trip, and that is deliberate: splitting on `:` and re-joining on `:` reassembles the same string, so only a check that resolves the entries can tell the difference. It fails against the old separator; the three existing compose tests spelled the separator into their fixtures and now ask the platform. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`encoded_cwds` flattened `/` and `\` and left `:` alone, so a Windows cwd encoded to `C:-Users-me-ws` — still drive-qualified. Windows `Path::join` REPLACES its base when the argument is drive-qualified, so `projects.join(encoded)` did not join: it resolved against the current directory on C:. The guard counted transcripts somewhere that was never the CLI's project directory, found none, and reported no prior use — the one answer this module exists to get right, because "no prior use" turns a wiped data dir into a fresh start instead of a recovery offer. `C-` is what the CLI itself writes: `C:\Repos\brains` becomes `C--Repos-brains`, the colon and the separator each contributing one dash. The suite kept its OWN copy of the encoder, which is how this hid: the copy spelled the colon straight through exactly as the code under test did, so the two agreed and the test passed while writing its fixture into whatever directory C: was pointing at — 18 of them, in the source tree, one per run. The test now calls the production encoder and asserts the fixture landed under the temp dir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same reason `keychain.rs` reads both: `HOME` is a Unix name Windows sets only under Git Bash, so on a GUI launch the guard's home lookup answered "no home" and the migration guard silently never fired. Its test asked for `HOME` too, and panicked on Windows before it could assert anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lsewhere
`tests/headless_sandbox.rs` gated its TESTS on macOS but not the fixtures they
lean on, and `probing_cli` writes a `#!/bin/sh` and chmods it through
`std::os::unix` — which does not exist off Unix. The binary failed to build and
took `cargo test --workspace` down with it, so nothing behind it ran either.
Behind it were four more. All of them reach `sandbox::policy()`, which off macOS
is `Err(Unsupported)` by design — there is no seatbelt to ask, and a scheduled
run fails closed rather than running unconfined. So they were not finding a
broken contract, they were staging one the platform never reaches:
* the two `sandbox::tests` are built on macOS facts — `/tmp` resolving through
a symlink, and a root error that `policy()` never gets far enough to return;
* `actor_runner`'s stages a SPAWN failure, which means getting past the
sandbox first; the earlier refusal has its own cover in `engine::tests`;
* `runner_settings`' is gated at the LAST STEP ONLY. The live-settings claim
above it holds everywhere and now keeps running everywhere — gating the
whole test would have thrown away the portable half to buy nothing.
What stays portable is the one check that reads `sandbox::PROFILE` as a string.
That is deliberate: the absence it asserts — no `mach-lookup` grant — is the one
a careless edit reopens, and it should fail on whichever machine made the edit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`node_path()` already appends `.exe` there; only the test asked for the Unix name. `Path::ends_with` matches whole components, and `recall-node` is not `recall-node.exe`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The walk built paths with `join`, so the seven builders came back `src\layout\…` on Windows and failed against a list written the way every other path in this tree is written. Same files, same content, one character. A phantom failure only a Windows dev sees is worse than no net at all, because it teaches them to distrust the one that works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ignored ~37MB of generated SDK archive, and the one staging directory the list above it missed. Untracked is not ignored: it sat in every `git status` waiting for one `git add -A` to commit the vendored SDK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dev's full-repo hygiene sweep (#49) and the per-app workspace work (#48) touched five of the files this branch changes. Conflicts resolved by keeping dev's shape and re-applying only the Windows fix on top: * claude/path.rs — dev's rustfmt shape, `PATH_SEP` instead of `:` * settings.rs — dev's `is_legacy_relic` naming and shortened comment, plus the `USERPROFILE` fallback * keychain.rs — the `dev_root()` helper this branch adds, which is what the other two call sites already use * actor_runner.rs — dev dropped that test's doc comment; kept the drop, kept the macOS gate * runner_settings.rs — dev's `runner_with_dynamic_workspace` rewrite taken whole, with only the `spawn_headless` tail gated Verified on Windows after the merge: `cargo test --workspace` exit 0, clippy `-D warnings` clean, `cargo fmt --check` clean, and the size/imports/manifests lints pass. One frontend test fails, and it is dev's, not this branch's: `frame/__tests__/shortcuts.test.ts > swallows nothing while the setup gate is up` times out at 5s on a clean `origin/dev` checkout too, verified in a separate worktree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both where #55 rewrote what this branch had touched: * `package-runtime.mjs` — #55 dropped the staged COPY entirely (copying a macOS framework rewrites the symlinks its signature depends on, and Apple rejects the result) and now tars the SDK in place. Took that whole, and re-applied only the Windows half on top: the paths stay RELATIVE with an explicit `cwd`, because GNU tar — what Git for Windows puts first on PATH — reads an absolute `C:\...` as a `host:path` remote spec. Re-checked both forms against the local GNU tar 1.35 on a synthetic tree: the absolute form still aborts with "Cannot connect to C: resolve failed", the relative one produces the same `desktop-sdk/` layout. Tarring in place is unaffected — `-C` still lands on the SDK's own parent, just spelled from the repo root. * `ci.yml` — #55 replaced the inline externalBin stubbing with `stub-bundle-artifacts.mjs`, which reads the paths out of tauri.conf.json instead of hardcoding them AND already appends `.exe` on win32. Strictly better than the inline stub this branch had, so the script wins and the Windows matrix entry stays. Also gated `actor_runner::tests::runner` and its `Path` import: #55's sibling work left `runner_settings` building its own runner, so the shared helper's only caller is now the macOS-gated test, and it was a `dead_code` warning off macOS. Re-verified on Windows: `cargo test --workspace` exit 0 and warning-free, clippy `-D warnings` clean, `cargo fmt --check` clean, size lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`a_sidecar_that_exits_fails_the_call_instead_of_blocking` went red on this branch's macOS leg, and the branch changes nothing in this path — on macOS `PATH_SEP` is `:`, every gate added is `target_os`-shaped, and the recording change was a test assertion about `.exe`. It is a race, and it will bite the next branch just as arbitrarily. `Sidecar::call` reports a dead child at three ordered points: `try_wait` before it writes, the write itself, then the read. The fake sidecar here is `echo not-protocol; exit 0`, so it is already exiting while `call` runs, and WHICH point catches it is timing: 1. "the sidecar exited before prepare" — try_wait won 2. "send the prepare request: Broken pipe" — the write hit a closed pipe 3. "the sidecar closed while handling prepare" — the read hit EOF The assertion accepted 1 and 3. CI got 2. All three are the same claim, which is the one in the test's own name: the call FAILED rather than hung. Nothing about the recorder was wrong — only the list of accepted wordings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stefan-ssv-labs
left a comment
There was a problem hiding this comment.
✅ review-pr: clean — no blockers or criticals found.
#45 merged first, which inverts the sequencing agreed in its thread (that PR was to drop its job once this matrix existed). Same outcome, done from this side: the separate `Windows (recording)` job is gone and the `rust` matrix is the one Windows mechanism. That is a real removal, not a formality, so it was checked rather than assumed. The job ran three commands and the matrix leg covers all three: cargo clippy -p brains-recording --all-targets ← already a step on dev, now runs on both legs cargo test -p brains-recording ← subsumed by --workspace cargo build -p brains-recording ← built by both of the above It only becomes redundant because of this PR: the job's own comment named `--workspace` (red on Windows via sandbox.rs) and `brains-desktop` (did not link at all) as the reasons it had to stay narrow, and both are fixed here. Its load-bearing note — never `continue-on-error`, because a job that cannot go red reads as coverage — moved onto the matrix, where the next person will look. The other three conflicts are the same story twice over: both branches fixed the same Windows bug independently. * package-runtime.mjs — both made tar's paths relative. Took dev's, which routes through host.mjs, because that helper also carries the npm.cmd fix (Node refuses to execFile a .cmd at all since the 2024 argument-injection change). Kept this branch's note that it runs inside beforeBuildCommand, so getting it wrong fails the whole build, not just the recording assets. * src-tauri/src/lib.rs — a TRAP worth naming. This branch imported CallDetector/DetectorConfig at the top under a macOS gate; dev moved them inside the macOS block instead (that file is at its size ceiling). Keeping both would leave the outer import UNUSED on every non-macOS build — a hard error under `-D warnings`, which is the exact failure this branch's comment existed to prevent. Took dev's arrangement verbatim; it is green on dev's macOS leg, which is the only leg that compiles that path. * engines/recording/src/lib.rs — both fixed the same node_path assertion. Took the union: dev's extra check that the binary resolves next to the executable, with this branch's clearer failure message. Verified on Windows 11: `cargo test -p brains-recording` 63 passed, `cargo clippy -p brains-recording --all-targets -- -D warnings` clean, and `cargo check -p brains-desktop` finishes — the claim this PR is about. `npm run lint:size` green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#45 landed first — the reconciliation happened here insteadThe sequencing agreed in #45's thread was: this PR's matrix lands, then #45 drops its separate The removal was checked, not assumed. That job ran three commands, and the matrix leg covers all three:
And it only becomes redundant because of this PR: the job's own comment named The other three conflicts were both branches fixing the same Windows bug independently. One is worth naming because keeping both sides would have broken the build:
Verified on Windows 11 before pushing: |
stefan-ssv-labs
left a comment
There was a problem hiding this comment.
✅ review-pr: clean — no blockers or criticals found.
…dev's BRNS-DESK-034 was fixed twice, independently: here, and in Amir's Windows PR (#62, commit e87d525), which reported the bug in the first place. Both do the identical thing — import relative+sep from node:path and normalize each walked path through relative(process.cwd(), p).split(sep).join("/") in place of the process.cwd().length + 1 slice. Dev's version wins outright. It names the normalization repoPath() instead of inlining it in the .map, and it carries the explanation of why a phantom Windows-only failure is worse than no net at all. This branch's copy of that file is now byte-identical to dev, so the ledger-net half of this PR is empty and what remains is BRNS-DESK-033 alone. .github/workflows/ci.yml merged clean: #62 restructured the Rust job above the Build macOS upload, which moved but did not touch it.
…nd the cache caches what cargo writes (#71) Two CI truth problems, both in the same three workflow files. ## The Windows path fix was never proven on Windows (BRNS-DESK-034, AC4) `ledger-net.test.ts` walks the tree with `join()` and compares the result against forward-slash literals, which makes it the one spec in this repository whose result depends on the path separator. It failed on every entry at once on Windows until #62 normalized the discovered side, and nothing re-proved that afterwards: the matrix #62 added is a cargo matrix, and cargo does not run vitest. The `Frontend` job that does run vitest is ubuntu-only. So the fix for a Windows bug was a claim rather than a test, and AC4 stayed open. The cheapest true closer is a Windows-gated step inside the existing `rust` job, which already installs node 22 and runs `npm ci`. The check costs seconds there; a second Windows frontend job would cost a job. It is that job's LAST step, deliberately. A failing step aborts the leg, so a frontend spec placed ahead of the cargo steps would let one red assertion delete that leg's clippy, its `--workspace` tests and its Windows link check — exactly the coverage the matrix exists to provide, erased by an unrelated failure. `fail-fast: false` guards sibling legs, not sibling steps. Running last costs nothing, because the spec's walker skips `target` and `node_modules` by name. Both flags carry weight. `--passWithNoTests=false` pins the behavior that makes the hardcoded path load-bearing — vitest exits 1 on a filter matching nothing, so a rename cannot silently retire the step. That is vitest's default today, but it is vitest's, and one line in `vitest.config.ts` would flip it for the whole repo. `--no-install` stops `npm exec` from falling back to a registry download of an unpinned vitest if the local binary ever goes missing; a step whose value is determinism should fail loudly instead. Verified locally: the spec passes 26/26, a stale filter exits 1, and `--passWithNoTests=true` exits 0 on that same stale filter, which proves the flag is read rather than ignored. Not `npm test`: that chains `drive:test` and `eval:test` behind vitest and neither has ever run on Windows, so widening to the full frontend gate would be a different change with a real chance of landing red for unrelated reasons. ## The Rust cache pointed at a directory cargo no longer writes (BRNS-DESK-035) `Swatinem/rust-cache` caches `<workspaces>/target` and defaults `workspaces` to `.`. Six sites overrode that with `src-tauri`, so the action cached a path cargo does not write: `./Cargo.toml` is the workspace root and lists the seven engine crates and `src-tauri` as members, so every artifact lands in repo-root `target/`. Removing the input at all six sites is the whole fix. The lesson is NOT that the input was always wrong, and the comments say so plainly, because the true version is the useful one. It entered at e152464 (2026-03-01), when this tree had no root `Cargo.toml` and `src-tauri` carried its own manifest and lockfile — src-tauri genuinely was the cargo root, and `src-tauri/target/` genuinely was where cargo wrote. Commit 2e0e247 (2026-08-06) added the root workspace and deleted `src-tauri/Cargo.lock` in one move, and the input silently became stale. Nothing lints a workspace-root move against the CI inputs naming the old root. That is the failure mode worth remembering, and an earlier draft of these comments asserted the opposite. The key was also wrong, in a way that is easy to miss: the action hashes `<workspaces>/Cargo.lock` only if it exists, and `src-tauri/Cargo.lock` had been deleted, so the lockfile dropped out of the key entirely and a `cargo update` stopped invalidating the entry. That is why the first run after this change re-keys and saves rather than no-opping. `save-if` on the two ci.yml jobs that run on every PR is the other half of the fix, not a separate opinion. These entries stop being a ~741 MB registry and become a real target directory, and a PR-scoped save is readable only by re-runs of that same PR while still counting against the 10 GB repository budget that evicts by last use. Writing only on `main` keeps one warm entry every PR can restore from, instead of per-PR copies nobody reads that evict the shared one and the npm caches beside it. `build-macos` is deliberately excluded: it is label-gated on a pull request and never runs on `main`, so restricting its save would mean it never writes and never restores. `dev-release.yml` gets `key: universal-apple-darwin-dev`. Both release workflows name the job `build` and run it on `macos-15`, and the action builds its key from the job id, the runner OS and arch, and the `key` input — not from the workflow name. With the same key literal, a manual `release.yml` dispatch on `dev` lands in the same ref scope with the same key, and cache entries are immutable: the second writer is silently skipped and one channel serves the other channel's target directory. That was harmless while both held nothing but the registry; it is not harmless now that they hold real builds, since the two channels pin different Recall SDKs and carry different bundle identities. Two expectations are tempered in the comments rather than overstated. A restore returns third-party dependency artifacts only — the action prunes workspace members before saving, so our own eight crates relink every run. And `release.yml` triggers on `v*` tags, where Actions caches are ref-scoped: a tag run restores from its own new ref or from the default branch, which never runs that job id, so its entries are written and in the normal flow never read. Fixing the path there is still right, but warming the release lane needs its own change and its own ticket. `dev-release.yml` is the one lane this genuinely warms, since it runs on pushes to `dev` and restores its own previous save. Verified: `actionlint .github/workflows/*.yml` exits 0, all three files parse, no `workspaces:` input remains, and the vitest spec passes under the exact command string CI will run. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…t the shell crate (#73) * style: cargo fmt --all, so the widened gate can land green (BRNS-DESK-036) Seven hunks across six files in two crates, and every one is rustfmt's own line-breaking rather than a change in what the code does. This is the commit `ci.yml` asked for in the note above its format check — "Do the reformat as its own commit, then add --all." The widening itself is the last commit here. `src/engines/model/src/codex/models.rs:5` is an import reorder; the other six are wrapping decisions. Two of those are mirror images, which is worth a word: `claude/path.rs` collapses a broken-out `ok_or(...)` back onto the call line while `codex/path.rs` expands the same construct outward. rustfmt is not being inconsistent between them — the two sites differ by the width of the error type's name, `PathError` against `CodexPathError`, and each is being moved to the shape the width rule dictates for it. Both had been hand-formatted the other way. Those two files are also why this reformat is worth more than tidiness. #62 rewrote exactly these lines to fix the Windows PATH separator and left both unformatted, because the format gate is scoped to `src-tauri`: it went green over the two files that PR had just changed, and the PR body reports `cargo fmt --check` clean in good faith. The debt here is this small only because #45 paid the rest of it — 364 of the 369 hunks that existed when the note above the format check was written. What remains is what arrived afterwards, through a gate that could not see it. rustfmt does not change semantics, and nothing here is near a macro or a conditional-compilation boundary where that generalization gets interesting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(src-tauri): two field_reassign_with_default in test code (BRNS-DESK-036) These are the only two clippy findings in the whole workspace, and both are in the one crate CI already lints. Why they survived is worth naming before the next commit widens anything, because it is not only `--manifest-path`: the clippy step also has no `--all-targets`, so even inside the single package it selects, only the lib target is linted — and both of these live in `#[cfg(test)]` code. Widening the package selector alone would have stayed green over both. Each fix is the one clippy suggests: move the single plain field assignment into the struct literal, leave everything else alone. The `set_app_enabled` calls have to remain after the initializer in both cases — they are methods rather than fields, so `..Default::default()` cannot absorb them — and `boot_tests.rs` keeps its loop over the manifest's apps for the same reason. `Settings::default()` is untouched, so both sites construct exactly the value they constructed before. That equivalence is the lint's own point: the two-step form reads as though it might not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: fmt and clippy cover the workspace, not just the shell crate (BRNS-DESK-036) `--manifest-path src-tauri/Cargo.toml` selects that package alone, and `src-tauri` is the index — CLAUDE.md's word, and it holds no domain logic. So two of the three Rust gates were pointed at the crate with the least code in it while `cargo test` had already been widened out of that exact trap, with a comment above it explaining the mechanism. Both now match the tests: `cargo fmt --all --check` and `cargo clippy --workspace --all-targets -- -D warnings`. The hint the format step echoes on failure changes with it, from `cargo fmt --manifest-path src-tauri/Cargo.toml` to `cargo fmt --all` — a failure message naming a command that reproduces nothing is its own small defect. The two gates were not failing the same way, and the difference decides which flag matters: rustfmt really was blind to the engines. It formats the packages it is given and nothing else, so all seven went unformatted — which is how #62 fixed the Windows PATH separator in `claude/path.rs` and `codex/path.rs`, left both unformatted, and watched this step go green over the two files it had just changed. Clippy was not blind to them. Cargo applies clippy's workspace wrapper to every workspace member it builds, and `src-tauri` depends on all seven engines, so their lib targets have been linted all along through the narrow step. The flag that was missing is `--all-targets`: no crate's test, bench or example target was linted anywhere, which is why both of the workspace's only two real findings were sitting in `src-tauri`'s own test code — inside the single crate the step already selected. Widening the package selector alone would have stayed green over both. That also explains the `-p brains-recording --all-targets` step this commit removes, and corrects the note that stood on it. It read "the engine crates are NOT covered by the step above", but recording was already a `src-tauri` dependency when that step was added, so the six clippy errors nobody saw cannot have been in its lib — they were test-only, which is what "tests included" in the step's own name was saying. `--workspace --all-targets` subsumes it exactly: same target selector, and `brains-recording` is one of the eight members `--workspace` resolves to. `--workspace` itself is explicit rather than load-bearing, and the review of this branch is what established that: the root manifest is virtual and declares no `default-members`, so `cargo metadata` resolves `workspace_default_members` to all eight members and a bare `cargo clippy --all-targets` would already select the same set. The flag stays to say the scope out loud, and to hold it if anyone adds `default-members` later — not because it widens anything today. The fmt step's old note refused this widening on a hunk count, and that note is the reason to keep arithmetic out of a workflow comment. At 34cc669, where it was written, the workspace measured 369 fmt-dirty hunks, 111 of them in `src-tauri`. #45 — the same PR — then paid 364 of them and nobody came back to edit the note that had just been written. On `dev` at ee2984a what was left measured 7 hunks across 6 files in 2 crates, and that is the reformat commit before this one. Every one of those figures is basis-specific; the comment now carries the mechanism and points here for the numbers. `--all-targets` leaves one hole and cannot close it, so the comment says so rather than claiming coverage it does not have. `brains-browser` declares `[[bin]] brains_cef_helper` with `required-features = ["chromium"]`, and cargo drops a target with unmet required features out of a wildcard selection silently — it errors only when the target is named. The same feature gates the engine modules inside that crate's lib, and the one step that turns the feature on is `cargo check`, not `cargo clippy`. That gated code is linted by nothing, before this change and after it; it needs its own ticket. `CONTRIBUTING.md` moves with the gate. Its pre-commit list is the only checked-in one, it promises to be "the whole safety net", and it named neither `cargo fmt` nor `cargo clippy` — survivable while the gate covered one crate, misleading now that it covers the tree most Rust changes land in. Both commands are added in the form CI runs them, with a note on why the flags are not optional. Neither gate changes which legs it runs on. Neither carries an `if:` today, so both already ran on macOS and Windows; this changes scope, not platform reach. Clippy on both is load-bearing — #62 fixed two Windows-only unused imports that failed `-D warnings`, a class of finding the macOS leg structurally cannot see. Verified locally on macOS: `cargo fmt --all --check` and `cargo clippy --workspace --all-targets -- -D warnings` both exit 0, and `actionlint .github/workflows/*.yml` is clean. Both halves of the gap were measured rather than assumed, by planting a deliberate lint and re-running both command forms: in an engine's lib the old clippy step already caught it, in an engine's test target the old step exits 0 while the new one exits 101, and for rustfmt the old step exits 0 on an engine violation the new one flags. The Windows leg's clippy cannot be measured from here, so this PR's own run is that measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Picks up the Discord flag that
devdoes not build on Windows, and the failures hiding behind it. Base isdev.What was reported
1. Won't compile on Windows.
browser_host.rscalledwindow.ns_window(), which Tauri only exposes on macOS, from a modulelib.rsdeclares unconditionally — so the default Windows build failed regardless of whether CEF was enabled.Fixed, but not with the suggested HWND branch. The engine behind this host is AppKit the whole way down (
app_mac,clip, theobjc2-app-kitviews inengine.rs), so wrapping an HWND intoWindowPtrwould buy a compile and not a working view. TheHosttrait already documents "no window to parent into" as a normal answer, soNoneoff macOS is the honest one. Gmail keeps working on Windows through the OS webview, as reported.2. Two unused imports failing
clippy -D warnings— gated under the sametarget_osas theCallDetector::startcall they serve.3.
ledger-net.test.tsbuilt paths withjoin()and compared them against a hardcoded forward-slash list. Normalized.The structural fix
CI's Rust job was
os: [macos-latest]. Nothing in the pipeline compiled for Windows, which is why all three passed review and merge. The matrix is now[macos-latest, windows-latest], scripted steps carryshell: bash(the Windows runner defaults to PowerShell, whereif ! cmd; then,mkdir -pand: >are syntax errors), and the externalBin stub gets its.exename. The bundled-Chromium type-check stays macOS-only — that backend is AppKit, so asking Windows to build it would fail on an honest absence rather than on anything wrong.What was hiding behind them
tests/headless_sandbox.rsalso failed to compile on Windows (probing_clichmods throughstd::os::unix, ungated while its tests were gated), which tookcargo test --workspacedown entirely — so nothing after it ran either. Once it built, nine more failures. Two were real bugs, not test artifacts:PATH composition hardcoded the Unix separator
compose_pathsplit and joined on:. On Windows that does not merely fail to split, it splits in the wrong places, because every entry carries a drive letter:C:\Windows\system32came apart intoCand\Windows\system32, neither of them a directory. Every existence-checked entry was dropped and the composed PATH came back empty, soresolve_claude_pathsearched it for a CLI it could never find — and the app reported the claude CLI as missing on a machine where it was installed and on PATH. Fixed for both claude and codex, which shared the same split.The regression test asserts through a resolution rather than a round trip, deliberately: splitting on
:and re-joining on:reassembles the same string, so only a check that resolves the entries can tell the difference. It fails against the old separator.The wipe guard looked for transcripts in the wrong place
encoded_cwdsflattened/and\and left:alone, so a Windows cwd encoded toC:-Users-me-ws— still drive-qualified. WindowsPath::joinreplaces its base when the argument is drive-qualified, soprojects.join(encoded)resolved against the current directory on C: instead of~/.claude/projects. The guard counted zero transcripts and reported no prior use — the one answer this module exists to get right, because "no prior use" turns a wiped data dir into a fresh start instead of a recovery offer.The suite kept its own copy of the encoder carrying the same bug, so the two agreed and the test passed while writing its fixture into whatever directory C: was pointing at — 18 of them, in the source tree, one per run. The test now calls the production encoder and asserts the fixture landed under the temp dir.
The rest were test-only
Seatbelt tests asserting macOS facts (
/tmpresolving through a symlink; a root errorpolicy()never gets far enough to return); arecall-nodevsrecall-node.exeassertion; aHOMElookup Windows does not set. Where a test had a portable half, only the macOS-dependent step is gated —runner_settings' live-settings claim still runs everywhere. The one check that readssandbox::PROFILEas a string stays portable on purpose: the absence it asserts (nomach-lookupgrant) is the one a careless edit reopens, and it should fail on whichever machine made the edit.Also folded in, found on the way: a dev build on Windows never found its token (
HOME-only lookup, plus a macOS-shaped keychain early-return — Windows Credential Manager has no per-binary ACL and never prompts, so that return bought nothing and cost the only source that works);targiven absolute paths, which GNU tar on Windows reads as a remotehost:pathspec; andsrc-tauri/recall-runtime/was untracked but not ignored, ~37MB onegit add -Afrom being committed.Verification
Run on Windows 11:
cargo test --workspace— exit 0 (was: did not compile)cargo clippy -- -D warnings— cleancargo fmt --check— cleannpx vitest run— 138 files, 1659 tests, 0 failureslint:size,lint:imports,lint:manifests,lint:css-vars,lint:resources— cleanBehaviour on macOS is unchanged: every gate added is
target_os-shaped, and the PATH separator resolves to:there.Branch is 6 commits behind
devand not rebased, so the merge check runs against currentdev.🤖 Generated with Claude Code