Native-client runtime API routes: files, artifacts, jobs, git, LSP, secrets, targets - #6229
Conversation
…tive clients (#6163) GPUI's Files and Preview modules probed GET /v1/artifacts and GET /v1/files and found neither. Reconciled against the route table: file suggestions (/v1/workspace/files/search) and /v1/workspace/status already existed, but there was no listing, no bounded read, and no revision-checked write over HTTP; artifacts in Core are session-scoped ArtifactRecords stored under sessions/<id>/artifacts/, which no route exposed. Add GET /v1/workspace/files (bounded, sandboxed directory listing; .git is never served and symlinks are listed by kind but never followed), GET /v1/workspace/files/read (a bounded byte window with a whole-file SHA-256 revision, utf-8 or base64), and PUT /v1/workspace/files (atomic write through the existing confined WorkspaceFile opener; 201 on create, 200 on an overwrite whose expected_revision matches, 409 on drift naming the current revision, 413 above 4 MiB with a route-level body limit sized so the handler answers instead of dropping the connection). Add GET /v1/sessions/{id}/artifacts and GET /v1/sessions/{id}/artifacts/{artifact_id} over the existing records, rooted at the server's sessions dir. No second file store, cache or index; the workspace root is the server's configured workspace. Gates: cargo fmt --check clean; clippy (CI flags) exit 0; runtime_api:: under scripts/with-hermetic-test-home.sh + nextest ci profile: 241 passed, 0 failed; npm test 66+14+470 passed; npm run check:web exit 0; check-versions.sh exit 0. Live probe against an isolated app-server built from this tree: 15 requests, every status as documented, nothing written outside the workspace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it, diagnostics, targets, LSP, and voice GPUI's terminals, usage panel, settings, review, and diagnostics modules named HTTP contracts that did not exist beyond the workspace file/artifact routes landed in 1758b78. This slice implements them against existing Core authorities rather than standing up parallel backends. Jobs: GET/POST /v1/threads/{id}/jobs, per-job status, non-consuming cursor-based output reads (stream/cursor/max_bytes/wait_ms/format, with exact dropped-byte accounting and stale-job tail snapshots), stdin writes, and kill, plus a flat GET /v1/jobs. Each thread now owns one shared ShellManager handed to its engine on load, so the API and the model see one job set and background work survives engine LRU eviction; the entry drops with the thread. API-created jobs inherit the same sandbox posture projection a turn applies. Commands: GET /v1/commands projects the live command registry (aliases, usage, localized description, discovery flags, argument requirements) at the server's resolved locale. Context: GET /v1/threads/{id}/context answers via a new engine Op (GetContextBudget, with a wire-op twin) so the live estimate, billed input tokens, route window, ceilings, and pressure come from the engine itself; live:false with static route info when the engine is unavailable. Secrets: PUT /v1/providers/{id}/key is write-only — the credential write path moved to codewhale-config (same transactional store+metadata logic the CLI uses), the response carries backend/configPath/credentialState metadata only, and the in-memory config mirrors the persisted auth marker so readiness readback reflects the write immediately. 4 KiB key cap, ~5 KiB body limit, no echo of key or length anywhere. Git: GET /v1/git, /v1/changes, /v1/diff, /v1/workspace/diff, /v1/git/graph, and POST /v1/git/{stage,unstage,discard,commit,push,branch} over the existing hardened git wrapper and workspace confinement; bounded outputs, refreshed status in every mutation response. Diagnostics: GET /v1/logs{,/{name}}, /v1/crashes{,/{name}} (both the .codewhale and legacy .deepseek crash roots), and /v1/process — bounded, basename-validated reads; no telemetry upload surface. Targets/remote: GET /v1/targets reports this runtime as its own sole target; POST /v1/targets{,/switch} 501 (client-owned registry). GET /v1/remote reports bind posture; POST /v1/remote/connect probes a candidate's unauthenticated /v1/runtime/info, refuses URLs with credentials, and never forwards tokens. GET /v1/ssh and /v1/cloud answer supported:false owned by the control plane; their POSTs 501. LSP: GET /v1/lsp plus /v1/diagnostics, /v1/definition, /v1/references, /v1/symbols over one lazily-built API-owned LspManager confined to the server workspace; normal no-server/timeout states return 200 with ok:false and a machine-readable reason. Voice: GET /v1/voice capability plus POST /v1/voice/{dictate,send,control} delegating to the existing TUI record-to-ASR pipeline through a headless dictate_once (same whisper/Groq/provider dispatch, same send-suffix and voice-control behavior, serialized mic access). CODEWHALE_DISABLE_VOICE is an operator kill-switch that makes every voice surface fail closed. docs/RUNTIME_API.md now documents these families with their actual shapes, limits, status codes, and ownership boundaries. Gates: cargo fmt --all -- --check clean; runtime_api::tests under scripts/with-hermetic-test-home.sh + scripts/dev-cargo.sh: 250 passed, 0 failed; codewhale-config lib tests 704 passed 0 failed; codewhale-cli lib tests 390 passed 0 failed. No provider calls, no remote actions. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| .map_err(|error| ApiError::internal(format!("file open failed: {error}")))?; | ||
| file.seek(SeekFrom::Start(offset)) | ||
| .map_err(|error| ApiError::internal(format!("file seek failed: {error}")))?; | ||
| let mut window = Vec::with_capacity(limit.min(64 * 1024)); |
| let base = format!("http://{addr}"); | ||
|
|
||
| let status = client | ||
| .get(format!("{base}/v1/sessions/{session_id}/artifacts")) |
| .status(); | ||
| assert_eq!(status, StatusCode::UNAUTHORIZED); | ||
| let listing: Value = client | ||
| .get(format!("{base}/v1/sessions/{session_id}/artifacts")) |
| .get(format!( | ||
| "{base}/v1/sessions/{session_id}/artifacts/{}", | ||
| served.id | ||
| )) |
| .get(format!( | ||
| "{base}/v1/sessions/{session_id}/artifacts/{}?offset=5&limit=3", | ||
| served.id | ||
| )) |
| .get(format!( | ||
| "{base}/v1/sessions/{session_id}/artifacts/{artifact_id}" | ||
| )) |
| .get(format!( | ||
| "{base}/v1/sessions/{session_id}/artifacts/{}?limit=0", | ||
| served.id | ||
| )) |
Brings the branch up to the 51-commit v0.9.14 slice-2 merge (9cdfa92) so these routes can land. Three textual conflicts, two of which hid problems a textual merge cannot see. Conflicts, all HEAD-vs-main additive collisions: - `core/engine.rs` — both sides added a name to the same `use` list (`SessionContextBudget` here, `TurnSpec` on main). Kept both. - `runtime_api/workspace.rs` — this branch's #6163 workspace-files block against main's #6168 instruction-sources block. No symbol collisions between the two (34 vs 5 items, checked), so both kept — but the conflict boundary cut through `write_workspace_file`, whose closing brace lived on main's side. Keeping both sides verbatim produced an unclosed delimiter; the brace is restored. - `runtime_api.rs` — the files routes against the instructions route. Both kept. Two further defects the compiler could not see: - **`GET /v1/commands` was implemented twice.** Both sides added it, and axum rejects that at runtime ("Overlapping method route"). They are not duplicates: main's carries user-registered commands and shadowing, this branch's carries localized descriptions, argument-shape semantics and composer hints. Choosing between them is a public API contract decision, not a merge mechanic, so main's shipped handler stays (#6178 closed against it) and this branch's route, module and test are removed. The lost fields are written up in #6230 with the full comparison; everything is recoverable from 060711b. - **`OP_KINDS` never listed `get_context_budget`.** This branch added the `GetContextBudget` variant and its `kind_str()` arm but not the const array, which `protocol_covers_engine_ops` checks — and `op.rs` has its own test asserting "OP_KINDS must list every variant in order". Pre-existing on this branch rather than a merge artifact: it would have failed CI on its own. Added in variant order. Also fixed one pre-existing clippy error on this branch that the release gate would have caught: `assert_eq!(..., false)` on a bool expression in the voice route test. Checks: `./scripts/dev-test.sh tui` — 12713 tests run, 12713 passed, 19 skipped (from 12712/1 failed before the OP_KINDS fix). `./scripts/dev-test.sh protocol` — 68 passed. CI clippy gate (`--workspace --all-targets --all-features -D warnings`) exits 0. `cargo fmt --all -- --check` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWzjx9Q7Mw2G7K8rpiJy9p
|
Merged current Conflicts, and the two things a textual merge could not seeThree textual conflicts, all additive collisions — kept both sides in each. But two hid real problems: The
They are not duplicates. Main's carries user-registered commands and shadowing; this branch's carries localized Picking between them is a public API decision, not a merge mechanic, so I kept main's shipped handler (#6178 is closed against it) and removed this branch's route, module and test. The lost capability is written up field-by-field in #6230, and everything is recoverable from One pre-existing defect on this branch
Verification
I did not write the original two commits; I brought them up to date and verified them. CI here is still their first hosted check. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Claude finished @Hmbown's task in 3m 15s —— View job Claude is reviewing this PR
|
…pass
`Test (windows-latest)` is a required check and this was the only test failing
it: 15280 run, 1 failed, green on ubuntu and macOS.
assertion `left == right` failed
left: "v1\r\n"
right: "v1\n"
`git_routes_drive_a_real_workspace_repo` writes `tracked.txt` as `v1\n`, drives
`POST /v1/git/discard`, then reads the file back. Windows git defaults to
`core.autocrlf=true`, so the checkout that discard performs rewrites LF to CRLF
and the file comes back `v1\r\n`.
That is git behaving as configured, not the route misbehaving, so the fixture
is what needed pinning. The repo already sets `user.email` and `user.name` for
determinism; `core.autocrlf=false` belongs in the same place. The assertion is
unchanged and not weakened — the test still requires the exact bytes back.
Checks: `./scripts/dev-test.sh tui git_routes_drive_a_real_workspace_repo` — 1
passed on macOS, where it passed before too. The Windows half rests on git's
documented `core.autocrlf` behaviour and on CI, since this host cannot build for
`x86_64-pc-windows-msvc`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWzjx9Q7Mw2G7K8rpiJy9p
|
Claude finished @Hmbown's task in 4m 20s —— View job Claude finished reviewing this PR
|
…6179) The credential route could set a key but never clear one, and served no way to tell "you have no key" apart from "your key is owned elsewhere and this control cannot change it". Both halves of that gap are what kept first-run provider setup in the desktop dependent on the TUI. DELETE /v1/providers/{id}/key clears through a shared owner rather than a second implementation: crates/config/src/credentials.rs gains clear_provider_api_key, and `codewhale auth clear` now calls it, which deletes its inline body and its clear_provider_api_key_from_keyring helper. That move fixes a real defect on the CLI path: the secret-store delete was `let _ = secrets.delete(...)`, so `auth clear` printed success while the key could still be sitting in the keyring. The shared owner returns the backend error instead, and both callers report it. Config semantics are unchanged - only xAI clears auth_mode/consent/OAuth generation alongside the key, exactly as before, because every other route is still an API-key route that simply has no key now. GET /v1/providers gains credentialSource, credentialWritable and a reason. The classification is structural: declared auth mode, consent state, and the *kind* of any configured api_key value. It never resolves a secret, an environment value or an auth command, and it reports a class - never a value, a path, or an environment variable name - so ProviderEntry's standing promise to carry no credential, endpoint, path or consent-source metadata still holds. Its doc comment now says why the field is compatible with that promise rather than leaving the two to contradict each other. Both verbs refuse a credential Codewhale does not own with 409 and that same reason. The case worth naming: a literal key in a config file still wins at request time, so writing the secret store would have reported a success the user could never observe - the exact "must not appear successfully overwritten" the issue asks for. Tests: four unit tests pin the classifier, including that the __KEYRING__ sentinel is routing metadata and not mistaken for a file-owned literal, and that a refusal reason can never carry the value it refused. One integration test round-trips set -> catalog metadata -> clear -> readback through the live config mirror, asserts the clear receipt echoes no key bytes, and asserts a repeated clear is not an error, because a client retrying a revoke must not be told something went wrong. runtime_api::secrets::tests 4 passed; 0 failed runtime_api::tests::provider_key* 2 passed; 0 failed codewhale-cli --lib 390 passed; 0 failed cargo fmt --all -- --check clean cargo clippy --workspace --all-targets --all-features --locked (CI's allow list) clean Closes #6179 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AJENKJ2smviQW4FVGzUTk9
…6149) `main` is red right now, and this is why: CI's blocking-calls gate is advisory on pull requests and blocking on pushes to main, so `crates/tui/src/runtime_api/workspace.rs: std_fs sites 1 > budget 0` failed the Lint job at 9cdfa92. The site is real, not a lint artifact - `workspace_instructions` called `std::fs::canonicalize` in its async body, after awaiting the spawn_blocking it already had. It now rides that same closure; no new blocking scope was needed. This branch had added four more of the same class, which the gate would have turned into a worse red on merge: - `list_logs` walked directories and stat'd every file inline; `list_crashes` did the same through `list_files`; `process_info` read /proc and resolved `current_exe` inline, on a route polled for live health. Each now has a sync half called from `spawn_blocking`. - Four LSP handlers called `resolve_workspace_file` straight from their async bodies, and that helper canonicalizes and stats every path component. It is now async and does the work on the blocking pool. **The gate could not have caught that last one**, and the next person should know why: the scanner is per-file and lexical, so the `std::fs` calls it counts live in `workspace.rs` while the handler calling through to them lives in `lsp.rs`. A helper is invisible at its call site. Recorded in the doc comment on `resolve_workspace_file`. The same lexical rule cuts the other way: a sync `fn` holding `std::fs` is counted even when every caller is already inside `spawn_blocking`. The eight remaining sites (`workspace.rs` 3, `diagnostics.rs` 5) are all that case - `confined_directory`, `list_workspace_directory`, `precheck_file_target`, `list_files`, `read_named_window`, `rss_bytes` - and every caller of each was read before budgeting rather than trusting the count. That is the "--update if the site can only run on synchronous code" path the script's own message names. Also updates the provider-catalog non-secret projection guard, which correctly failed on #6179's new fields. It is an allow-list, so widening it is an argument, not a formality: the new fields are admissible because `credentialSource` is one of four fixed enum spellings that can never interpolate a value, a path or an environment variable name, and `credentialWritable` is a boolean. The test now asserts that closed vocabulary rather than only the key set, so the field cannot later become a place a value hides. runtime_api:: 258 passed; 0 failed (--test-threads=2) cargo fmt --all -- --check clean cargo clippy --workspace --all-targets --all-features --locked (CI's allow list) clean check-blocking-calls-budget.py 624 sites across 180 files, within budget check-dead-code-budget.py PASS, 254 attributes, exactly at budget Note on the two `compatibility_stream_*` tests that failed an earlier run on this head: they pass here at --test-threads=2 and pass isolated. They use a 2-second subscription deadline that is only scaled when `CI` is set, and the earlier run was at load average 17 on 14 cores. Load flake, not regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AJENKJ2smviQW4FVGzUTk9
|
Claude finished @Hmbown's task in 3m 40s —— View job Claude finished reviewing this PR
|
| // instead; the config no longer advertises a key the backend may hold. | ||
| if let Some(error) = &outcome.secret_store_error { | ||
| println!( | ||
| "cleared API key for {slot} from config, but the secret store refused the delete: {error}" |
CodeQL triage — 10 high-severity alerts, all assessedThe CodeQL check reports 6 ×
|
…ually serve #6229 documented `GET /v1/commands` as `{commands, locale}` with `description`, `requires_argument`, `requires_required_argument`, `composer_wants_trailing_space`, `palette_runs_directly`, `show_in_empty_discovery` and `unlisted`. The route that landed (#6178) serves none of those: no `locale`, and per entry `summary`, `usage`, `subcommands`, `takes_arguments`, `kind`, `binding`, `discovery`, `hidden`, `shadowed_by` and `shadowed_aliases`. It also serves user commands, which the doc said were intentionally absent. Rewritten from the served response — probed against a build of the merged branch on a throwaway store, 105 commands — plus the two rules a client has to respect: a `binding: "host"` row is never submitted as a model prompt, and a user command shadowing a builtin wins that spelling. Found while building the GPUI palette against this contract, which is what a contract document is for. Docs only; no code, no tests affected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Native-client runtime API routes for the GPUI desktop: jobs, workspace files
and session artifacts, git, diagnostics, context, targets/SSH/cloud, LSP, voice,
and a write-only provider credential route. 28 files, ~+5,900 lines, of which
1,758 are tests.
docs/RUNTIME_API.mddocuments the surface.Why
The GPUI desktop is the product client (
CURRENT_DECISIONS.md§14). It isEngine-complete for reads but could not drive long-running jobs, review a diff,
export diagnostics, switch targets, or complete first-run provider setup without
dropping the user back into the TUI. Each of those needed a Codewhale-owned
route rather than a second registry in the desktop.
Closes
GET /v1/jobsplusGET|POST /v1/threads/{id}/jobs,/jobs/{job_id},/output,/stdin,/kill. Jobs created here carry anapi:{thread_id}owner scope, so a foreign or reused id is rejected ratherthan controlled. Covered by
jobs_api_lists_creates_streams_stdin_and_kills.DELETE /v1/providers/{id}/key, pluscredentialSource/credentialWritableonGET /v1/providers. See below.It also turns
maingreenmainis red at9cdfa92bc: CI's blocking-calls gate is advisory on pullrequests and blocking on pushes to main, and the Lint job failed on
crates/tui/src/runtime_api/workspace.rs: std_fs sites 1 > budget 0.That site is real.
workspace_instructionscalledstd::fs::canonicalizeinits async body, after awaiting the
spawn_blockingit already had — ablocking syscall on a Tokio worker. It now rides that same closure.
This branch had added four more of the same class, which would have made the
red worse on merge:
list_logs,list_crashesandprocess_infodiddirectory walks, per-file stats and a
/procread inline, and four LSPhandlers called
resolve_workspace_file— which canonicalizes and stats everypath component — straight from their async bodies. All now use the blocking
pool.
The gate could not have caught the LSP one. The scanner is per-file and
lexical: the
std::fscalls it counts live inworkspace.rs, while thehandlers calling through to them live in
lsp.rs, so a helper is invisible atits call site. Worth knowing before anyone reads a passing budget as proof.
The same lexical rule cuts the other way, which is why eight sites are
budgeted rather than rewritten: a sync
fnholdingstd::fsis counted evenwhen every caller is already inside
spawn_blocking. Every caller ofconfined_directory,list_workspace_directory,precheck_file_target,list_files,read_named_windowandrss_byteswas read before budgeting.#6179 — clearing a credential, and saying who owns one
The credential route could set a key but never clear one, and served no way to
tell "you have no key" apart from "your key is owned elsewhere and this control
cannot change it".
DELETE /v1/providers/{id}/keyclears through a shared owner rather than asecond implementation:
crates/config/src/credentials.rsgainsclear_provider_api_key, andcodewhale auth clearnow calls it — whichdeletes its inline body and a helper. That move fixes a real CLI defect on the
way past: the secret-store delete was
let _ = secrets.delete(...), soauth clearprinted success while the key could still be in the keyring. Bothcallers now report the backend error.
GET /v1/providersgainscredentialSource,credentialWritableand areason, so a client disables its control with a truthful explanation instead of
letting a write fail late. Both verbs refuse a credential Codewhale does not
own with
409— notably a literal key in a config file, which still wins atrequest time, so writing the secret store would have reported a success the
user could never observe. That is the "must not appear successfully
overwritten" the issue asks for.
The provider-catalog non-secret projection guard correctly failed on the new
fields. Widening an allow-list is an argument, not a formality: the fields are
admissible because
credentialSourceis one of four fixed enum spellings thatcan never interpolate a value, a path or an environment variable name. The test
now asserts that closed vocabulary, not just the key set.
Partially addresses, and says so
#6179 is not closed by this PR.
PUT /v1/providers/{id}/keysatisfies theset half: it persists through the same transactional path as
codewhale auth set, caps the body at 4 KiB, rejects control characters,returns a redacted receipt (backend + post-write
credentialState), and hasdeliberately no GET — a route that can return a secret can leak one. Still
missing from that issue's acceptance criteria: clear/revoke,
sourceandwritablemetadata, and the named refusal for environment-managed credentials.Tracked there, not silently folded in here.
Two defects the merge surfaced, and what was done about them
The merge of
origin/main(51-commit v0.9.14 slice-2) hit three textualconflicts, two of which hid problems a textual merge cannot see:
GET /v1/commandswas implemented twice. Both sides added it and axumrejects that at runtime ("Overlapping method route"). They are not duplicates:
main's carries user-registered commands and shadowing; this branch's carried
localized descriptions, argument-shape semantics and composer hints. That is a
public API contract decision, not a merge mechanic, so main's shipped handler
stays and this branch's route, module and test were removed. The lost fields
are written up in GET /v1/commands: two contracts collided — fold the composer/argument-shape fields into the shipped one and settle the localization policy #6230 with the full comparison and are recoverable from
060711b2f1.OP_KINDSnever listedget_context_budget. TheGetContextBudgetvariant and its
kind_str()arm were added without the const array, whichprotocol_covers_engine_opschecks. Pre-existing on this branch rather than amerge artifact — it would have failed CI on its own. Added in variant order.
Evidence
Re-run on this exact head (
0285df1ee), not inherited from the handoff:cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features --locked(CI's allow list)codewhale-config --libtest result: ok. 704 passed; 0 failed; 1 ignoredcodewhale-cli --libtest result: ok. 390 passed; 0 failed; 0 ignoredruntime_apisync-changelog.sh+derive-changelog.mjsregenerate with no diffThe handoff claimed
runtime_api"250 passed". The first run here reportedtest result: FAILED. 251 passed; 2 failed; 0 ignored; 0 measured; 12494 filtered out.The two failures were
compatibility_stream_closes_losslessly_across_replay_live_handoffandcompatibility_stream_exposes_and_resolves_user_input_without_answer_echo,both "deadline has elapsed" on a subscription handshake.
They are not a regression, and the reason is worth recording: both tests
arrived on
origin/mainin #5717, older than this branch, and both useci_scaled(Duration::from_secs(2))— 2s locally, 8s only whenCIis set. Thatrun happened on a 14-core host at load average 17 with three concurrent cargo
lanes. Re-run isolated on the same head:
And the whole module at
--test-threads=2on the current head:test result: ok. 258 passed; 0 failed; 0 ignored; 0 measured; 12494 filtered out.So: green isolated and green at low concurrency, flaky under parallel load. That is a different claim from
"green", and the local gate being load-fragile is worth someone's attention
independently of this PR.
Lint gates:
check-provider-registry.py,test_check_command_crate_boundaries.py,test_check_command_migration_manifest.py,check-dead-code-budget.py,check-readme-translations.py,check-tui-locale-parity.py,check-persistence-backlog-budget.py,check-readme-locales.sh,check-tui-product-vocabulary.sh,web/scripts/check-locales.mjs— all pass.check-runtime-contract-budget.pyfails, and not because of this branch:Neither symbol appears anywhere in this branch's diff. Both landed on
mainineefdcb42f5(#5715) without a matching update toscripts/runtime-contract-budget.json, which still lists 50 Act-full tool namesand neither of these. This is exactly the case
ci.yml:459-462anticipates —"a branch can fail it for debt it inherited rather than added" — so the gate is
advisory on pull requests and blocking on pushes to
main. Flagging rather thanfolding a manifest edit into an unrelated PR.
Windows:
git_routes_drive_a_real_workspace_repowas the only test failing therequired
Test (windows-latest)check (15280 run, 1 failed). Windows gitdefaults to
core.autocrlf=true, so the checkout thatPOST /v1/git/discardperforms rewrote
v1\ntov1\r\n. That is git behaving as configured, so thefixture pins
core.autocrlf=falsealongside theuser.email/user.nameitalready set. The assertion is unchanged and not weakened. This host cannot build
x86_64-pc-windows-msvc, so that half rests on CI.CI owns the exhaustive run;
cargo nextest --workspacewas not run locally.🤖 Generated with Claude Code