Land the thirteen finished branches as one tree (#229 #240 #247 #250 #256 #257 #261 #267 #268 #273 #276 #277 #278) - #275
Merged
Merged
Conversation
…vate model biorouter serve spawns biorouterd with no user-action key (SD-7), and the new-chat gate demanded that key's proof before binding a private default, so every POST /agent/start on a serve daemon configured with a private provider was refused 409 — the 2026-09-10 QA run's F1. The configured default is the person's choice, made out of band with biorouter configure (open question 24 put the raise at the write), so on a daemon that holds no key the new-chat bind no longer asks for a proof nobody can give. A daemon that holds a key (the desktop's) still refuses a proof-less first bind: the renderer sends the proof for free, and a model holding the recovered secret should not mint a private chat. The exemption covers one provider at one moment. On a keyless daemon, /agent/update_provider now measures every move onto a private model from Public (raise_baseline), so a new chat cannot be moved sideways to a private model nobody configured.
the_documented_closure_is_the_one_the_code_performs took the FIRST TierRaiseNeedsUser in routes/agent.rs. Since eb594de that has been the new-chat gate, not update_agent_provider's, so deleting the proof check from update_provider left the scan green (measured). The scan now starts at the handler AR-15 is about and is bounded by the next route.
A failed POST /agent/start reached console.error and nothing else on the Home composer (the QA run's F1): the text vanished and nothing appeared. One pure notice, startChatFailureNotice, now words every failure for a person (the daemon's own text stays behind Copy error), and every surface that starts a chat reports through it: Home, a fresh tab, a workflow window, the launcher, both Ask Biorouter buttons and Workflows. A source scan keeps an eighth surface from picking its own way to fail. Also fixed on the way: a workflow window whose start failed re-ran its creation effect in a loop, and a failed workflow start replaced the Workflows list with a list-load error. A browser tab now states the host's configured model (X-Caller-Provider) where the desktop proves the person: measured, a chat started on a private host model 403s its next request once its first reply makes it private, and 200s with the host provider stated.
The notice set a traceback only when it replaced the daemon's words, so the commonest failure on a serve host — the configured model has no credential there — showed no Copy error. It is always set now.
… a proof Records the ruling behind the new-chat fix: what it exempts (the configured default, at creation, on a daemon with no user-action key), what it does not (any other private model; any daemon that holds a key), the two halves that keep it from opening more, and the consequence to accept, with the privacy checklist's two questions answered as an enumeration. browser-access.md no longer promises a private-provider serve host works while it refused every chat, and says what a browser chat on one does. programmatic-session-access.md names the browser as a sender of X-Caller-Provider and the one proof-less bind. SD-1 named a route that does not exist (/config/provider); it is /config/set_provider. The two 409 descriptions in the OpenAPI spec now say which daemon refuses.
It said the daemon refuses every request that raises a session's privacy capability. A new chat binding the configured provider is no longer one of them, so an operator reading it would conclude a private model cannot work.
…r a knowledge base QA on merged main 7c96d79 (2026-09-10) measured four places where a caller holding nothing but the daemon secret - which a public chat's shell recovers with ps eww - was answered by the daemon while the tool path refused it: - H2: every /knowledge/bases/{id}/... route served a private base's pages, graph, history, location and a .brkb export; GET /knowledge/bases listed it. - M1: GET /sessions returned every chat on the machine, private ones titled. - M2: GET /agent/tools?session_id=<private> returned private-extension tool names while add_extension on the same chat refused. - F0: DELETE /sessions/{id} deleted a private chat the read refused, 4 of 4. Each is now the singular read's own gate (session_reach's pure decision), composed rather than re-derived: - Knowledge bases: one route_layer (session_reach::gate_knowledge_base) on a sub-router holding exactly the routes that name a base by {id}, reads and writes alike. An absent or malformed id is answered as a private one. The bases list and /knowledge/active omit what the caller cannot reach, and a selection write cannot move what its caller cannot see (KnowledgeService::set_selection_within). - Chats: session_reach on DELETE, rename, workflow values, the in-place edit arm (it truncates), extensions, usage, /agent/tools, callable_tool_count, /workflows/create and /skills/session; every refusal is GET /sessions/{id}'s byte for byte. GET /sessions, /sessions/sidebar and /schedule/{id}/sessions filter to the rows that gate admits (the sidebar scans so paging stays whole). Conversation ingest checks every named chat. - biorouter serve: its own interface (the served document's cookie) keeps the operator's configured-provider tier on listings and knowledge bases, which were open to it before; the transcript gate never reads it. Nothing refused before is permitted now. Tests show each route failing before and passing after; the wiring census names every new call site.
It claimed 'not a widening'. It adds no reach for a caller holding the daemon secret, but a tab on a private-model host now opens private chats, including desktop-started ones — which SD-9 records.
…covers The daemon now answers a request without X-User-Action as a public model on every route that names a chat or a knowledge base (previous commit), so the desktop - the person at the keyboard - says so on each of them: - Knowledge view: knowledgeFetch and the ingest/lint SSE stream attach the proof centrally; listBases, getActive, getGraph, getLocation, getPageBody, previewState, listHistory, restoreState, getKbTier and deleteBase carry it. The listing matters twice: KnowledgeContext prunes its selection against it, so a filtered list would read as deleted bases. - Chats: the session list cache, sidebar, first-run privacy notice, delete, rename, workflow values, in-place edit, extensions, usage, tool count, skills, schedule runs and create-workflow. - History's Export also sends it: the export route was gated in an earlier sweep and exporting a private chat from History had no proof to show. sessionListCache captures include_subagents before the proof's async hop, so an orphaned request still asks for the list it was issued for. Tests that pinned exact call arguments now assert the proof is sent. Docs: privacy-tiers.md records what shipped and what did not change; the execution plan's 'the Knowledge view is the user' scope note is marked superseded and open question 15(b) answered; serve-decisions.md gains SD-9 (the served interface keeps its operator's reach on listings and knowledge bases, and gains no transcript); programmatic-session-access.md's route tables match the tree; session_reach.rs answers its open listing question. OpenAPI spec and TS client regenerated for the new 403 responses.
…ere CI looks CI runs cargo test --workspace --lib --bins, so the integration binaries that held the H2 route sweep (tests/knowledge_routes.rs) and the served-operator standing (tests/serve_operator_reach.rs) were not what kept either door shut. Both now have a copy in routes::session_reach's lib tests, driven through routes::configure — the tree the daemon serves. The serve decision record becomes SD-10 (SD-9 is claimed by PR #229), and its transcript bullet is reworded so it holds whether or not the interface states a capability: the cookie never reaches a transcript, and a stated capability is judged at those routes exactly as any caller's is.
…bin also links routes::session_reach is compiled into the biorouterd bin as well as the library, and the bin has no auth module of its own, so the new served-operator test named crate::auth and failed to build there (clippy caught it; cargo test --lib alone did not). It now goes through biorouter_server::auth, which is the static http_caller reads in either binary.
F3 (provider QA, 2026-09-10): the per-chat binding already crossed windows on `biorouter:session-binding`, but the app-wide selection — BIOROUTER_PROVIDER / BIOROUTER_MODEL, the pair `/agent/start` binds a new chat to — had no announcement at all. The module header claimed each window "picks it up from its own config read"; each window read it once, at mount. Add `announceAppModelSelection` / `subscribeAppModelSelectionChanges` on the same channel, told apart from a binding by shape. The message is a nudge with no provider and no model: two windows' writes can be announced in the opposite order from the one they landed in, so a receiver must re-read the daemon rather than apply a payload. Local listeners run synchronously so the writing window re-reads too.
…(F3)
F3 (provider QA, 2026-09-10, HIGH). Change the app-wide model in window 1 and
window 2's chip never moved. Window 2 read `gpt-5.5-2026-04-24 (Private model,
UCSF)` at the instant of send; the chat it created bound `claude_code`, was
classified public — correctly — and its turn went to a consumer subscription
with no BAA. `ModelAndProviderContext` read BIOROUTER_PROVIDER/BIOROUTER_MODEL
once, on mount, while `/agent/start` binds whatever those keys say on the
daemon at that instant.
- ModelAndProviderContext re-reads the pair (a pure read, never the fallback
seeding) on the app-wide announcement and when its window regains focus or
becomes visible. Every statement of the selection — mount read, re-read, own
switch — is ticketed and publishes only if nothing issued after it has been
published, compared against what was last APPLIED (a failed newer read must
not condemn an older good one). A read that returns no body keeps the label
rather than erasing it.
- Every renderer write of the two keys announces: `changeModel`, the
first-run default seeding, and ConfigContext's `upsert`/`remove` — which
covers onboarding's local and coding-agent cards, Lead/Worker and reset,
none of which updated even their own window's chip before.
- A switch made from inside a chat now changes THAT chat only, unless the new
"Also use for new chats" box is ticked (unticked by default). This is
privacy-tiers §14.3 P4's recommended decoupling: QA F bound Claude Code in
one chat for one check and the next chat it opened came up public. With no
chat (Home, a chat not yet started, Settings, onboarding) a switch sets the
model new chats start on, and the dialog, the success toast and the chip's
dropdown ("Model for new chats") now say so, including "in every window".
- Both new-chat composers (Home and a not-yet-started chat) re-read the pair
immediately before `createSession`. If the chip was stale — a
`biorouter configure` in the terminal docked inside the window never takes
its focus — the send is refused, the fresh model and its tier go on screen,
a toast names what changed, and the composer gets the text back.
The pin still outranks a stale row: the app-wide selection touches neither.
… ledger New docs/desktop-ui/model-selection-across-windows.md: the two facts a model chip can state (a chat's binding vs the app-wide selection /agent/start binds), what a switch changes from each surface and why (privacy-tiers §14.3 P4), how each window stays current (announcements, focus re-reads, ticketed reads), the last look before a new chat, what it does not cover, the tests, and how to check it in the running app. Indexed in docs/desktop-ui/README.md. privacy-tiers.md's "What shipped" ledger gains a dated line for P4, so §14.3 no longer reads as open.
The Todo capability could always keep a checklist; nothing made the model
keep one. The only trigger was an advisory paragraph in system.md, the todo
MOIM rendered nothing while the list was empty, the old "don't stop with
unchecked todos" gate had been removed, and under Code Execution mode the
todo tools were reachable only from inside a script. Three multi-step QA
sessions on 2026-09-10 produced zero checklists.
This makes the checklist a harness behaviour rather than a prompt hint:
- Code Execution filter: the five todo__* tools stay directly callable
(exact names via todo_extension::TODO_TOOL_NAMES, never a prefix).
- agents::planning_gate: a small tested classifier reads the user's prompt
(numbered/bulleted list of actions, 3+ instructions, or 2+ joined by
then/after that/finally/steps; how-to questions and code never count).
For a multi-step turn with an empty checklist:
* the MOIM carries a "write the checklist first" reminder until it exists;
* the first non-todo tool batch is refused with a pointer to todo_write,
once per turn, as a synthetic inspection denial in
inspect_and_gate_tool_requests (not a registered inspector: the bridge
and approval relay would run it too, and the bridge flattens reasons);
* a turn that created or changed the list cannot end while items are
unfinished unless the final message names each one; at most
STOP_HOOK_BLOCK_CAP blocks per turn, on a count tools do not reset.
- Scope is one predicate (enforcement_applies) read by the turn and by the
system prompt, so the prompt's new clause describes exactly what runs:
not Chat mode, not subagents, not coding-agent bridge providers, only
with todo__todo_write on the roster. Todo disabled: none of it fires.
- The Stop-hook arms move out of the reply_internal generator into
decide_turn_stop (checklist check, then hooks), shrinking its poll frame.
- Docs: todo.md, code-execution.md and hooks-reference.md say what is
enforced; stale sessionTodos/ChatSummary comments corrected; a ChatSummary
test pins a direct todo_write appearing and ticking in the summary.
…race the stop check Every offset was a regex match boundary or a find() result, so a char boundary, but the workspace denies clippy::string_slice; str::get states the same thing without an index that could panic. The stop check now logs its block and give-up decisions, so a runtime run can be read from the daemon log.
Found by driving the running app. `Checkbox` draws its square beside an `sr-only` input, and the switcher's label was a SIBLING (`htmlFor`), so only the words toggled the box — a click on the square itself did nothing. Wrap the box and its words in one `label`, as SessionListView's checkbox already is. The new test clicks the square (Checkbox's own target) and fails against the sibling-label markup with `expect(element).toBeChecked()`.
Correct the sandbox store path (`<run>/data/sessions/sessions.db`), add the recipe for seeing the last look refuse an unannounced hand edit, record the toast-class trap (`TOAST_SURFACE_CLASS_NAME`, not `Toastify__toast`), and the 2026-09-11 measurements: 310 ms and 390 ms cross-window lag, both turns' token_events equal to the chip at send, the last look refusing both directions within 100 ms with no focus event. Also records, under "What this does not cover", that `/config/set_provider` writes provider then model as two writes — `config.yaml` held a mixed pair for ~55 ms — which a `/agent/start` in the gap would bind. Daemon work.
A private provider's request log is metadata-only, so neither the reminder nor the prompt clause can be read back from it, and the first live run could not tell a gated turn from a model that planned on its own. The turn now logs one line saying whether the gate is out of scope, in scope, or armed (with the signal's kind and counts, never the prompt), and debug lines when the reminder is added or the checklist disarms it.
Only the naming of an open item is checked, not the reason, so the intro no longer says the agent 'tells you which ones and why'.
…s on it # Conflicts: # docs/deployment/serve-decisions.md
…3f61d # Conflicts: # crates/biorouter/src/agents/agent.rs
main moved 60 commits (19 QA-fix PRs). One textual conflict, in ConfigContext.tsx's imports: main widened the privacy-tiers import for H3's master-switch record (`PRIVACY_TIERS_RECORD_KEY`, `privacyTiersRecordFromConfig`, `PrivacyTiersRecord`); this branch added the `sessionBindingSync` import beside it. Both kept. Auto-merged and checked by hand: SwitchModelModal.tsx (#222's derived `validation` + `aria-describedby` alongside this branch's scope copy, "Also use for new chats" box and `changeModel` options), its privacy test (#222's three new tests use call counts; the one exact-argument assertion is this branch's), Hub.tsx and BaseChat.tsx (#227's PrivacyTiersOffNote mount beside this branch's pre-send check), and privacy-tiers.md (both ledger additions). The renderer's writers of BIOROUTER_PROVIDER/BIOROUTER_MODEL are unchanged by main, so every one still announces.
serve-decisions.md conflicted: main's #226 added SD-9 (the launch token works until the daemon stops) and made the same SD-1 route fix this branch made. SD-1 takes main's wording; main's SD-9 stays SD-9; this branch's record becomes SD-12, after it. SD-10 and SD-11 are claimed by open #237 and #240, so SD-12 duplicates no number on main or in any open PR. Every reference to this branch's record moves with it (code comments, the two OpenAPI 409 descriptions, the daemon's keyless warning, the docs and their anchors, CLAUDE.md). References to main's SD-9 are untouched. CLAUDE.md's '(SD-1..SD-9)' range is replaced by no number at all, since every record that lands would make it stale.
main's H3 note (PrivacyTiersOffNote) now renders in Hub and needs a router and two ConfigContext hooks this test's mocks do not provide; it has nothing to do with starting a chat.
…primary The create-workflow modal computes the captured default in two places: from the chat's own selection read, and from the generated workflow's `knowledge_bases` block. Both fell back to `visible[0]` when the chat named no primary, so the saved workflow's `default` was the first visible base. `apply_knowledge_selection` maps `default: Some(id)` to `PrimaryUpdate::Set(id)`, so every chat the workflow started got a primary, the target of KB-less writes, that the chat it was captured from never had. The daemon's rule is the opposite. `plan_knowledge_selection` never infers the primary from `visible`, and the doc on `plan_workflow_knowledge_selection` says why: a promoted primary turns "I did not say where to write" into a commit into someone's base. With no primary, a KB-less write fails and names the candidates. Both paths now go through `primaryAmong`: the named primary if it is among the bases the workflow will see, and otherwise `null`. The daemon's block for a chat with no primary omits `default` entirely (`skip_serializing_if`), and that reads as `null` too. A primary outside `visible` becomes `null`, not unioned in the way the daemon unions an author's `default`. The daemon does that because somebody wrote the workflow and meant it. A captured primary outside its own set is an inconsistent read. The block is one locked snapshot and never is one, but the modal's own read is two requests (the base list and the selection), and a base created or deleted between their answers leaves the selection naming a base the list lacks. Unioning could save a deleted base as the default, which `set_visible_kbs` refuses, so every chat the workflow starts would fail with a 400. After a failed list it would save the primary as the only visible base, hiding every other one. Tests: the read path with no generated block; the generated block with `default: null` and with `default` absent (the daemon's own wire shape); and a primary outside the visible bases, once from the read and once from the block. All five fail before this change: each saved the first visible base.
…wledge base The previous commit stops the create-workflow modal from inventing a primary when it captures a chat's selection. Editing the selection afterwards still invented one, in two places: - The picker (`WorkflowResourcePicker`). Switching a base on made it the default whenever none was set, and switching the default off handed the role to `next[0]`. - The modal's `onKnowledgeBaseIdsChange`. It promoted `ids[0]` on any change while no default was set, including switching off an unrelated base. So a user who captured a chat with no primary and then removed one base got the first remaining base as the write target of every chat the workflow starts. These are user gestures, not captures, which is the case for keeping them. They go anyway. The gesture is "this workflow may search this base", and the promotion adds "and KB-less writes go here", which the user did not ask for. It is the promotion the daemon removed in 3a6e688 ("never promote a sole visible base to the primary"): "exactly one candidate" was treated as consent, which it is not, and the author who wants that base as the write target has a field to say so. In the picker, that field is the Default control, one click away on every selected row. - Switching a base on changes the selection only. - Switching the default off leaves no default, rather than passing it on. - The modal's handler keeps only the membership rule: a named default stays while its base is selected and becomes `null` when it is not. - The Default control is now a toggle (`aria-pressed`), so pressing the current default clears it. Without that, "these bases, and no default", the state a chat with no primary now captures, could not be restored once any base had been made the default, short of switching it off and on again. Each control is named for its row ("Default KB: lab-notes") so the pressed state says which base it belongs to. Tests, in the modal: switching a base on, switching another off, and switching the primary off each save no primary. All three fail on the previous commit. Switching a base on still fails with only the handler fixed (the picker names the base) or only the picker fixed (the handler names `ids[0]`). In the picker: switching on leaves the default unset, switching the default off clears it, and the Default control names a base and clears it when pressed again. All three fail before this change. A fourth, switching another base off leaves the default alone, passes either way as a guard against clearing it on every toggle.
Both sides appended a decision record to serve-decisions.md; keep both, in number order (SD-11 from #260, then this branch's SD-13).
# Conflicts: # CLAUDE.md # crates/biorouter-server/src/commands/agent.rs # docs/deployment/serve-decisions.md
…kflow-kb-and-docs # Conflicts: # crates/biorouter-server/src/routes/config_management.rs
Each is a pair of PRs that were green apart and wrong together. 1. `commands/serve.rs` did not compile. #273 replaced the browser token's `Option<String>` with the `BrowserToken` enum and does the env set/remove in its own `match browser_token.value()` above the spawn; #229's branch still carried the older `.envs(browser_token.iter()...)` line, which the merge kept beside it. Dropped the superseded line — #273's match is strictly better, since it also REMOVES an inherited token for `--no-token`. 2. `components/extensions/ExtensionsView.tsx` was an eighth chat-start surface reporting a failure a fifth way. #229's census in `utils/startChatFailure.test.ts` scans `src/` for anything calling `createSession|startNewSession|startAgent` and requires the shared notice; main's `ExtensionLoadFailureNotice` (an "Ask Biorouter" button) starts a chat and let the rejection go unhandled, so the notice stayed on screen and nothing said the chat had not opened. Wired to `startChatFailureNotice`, as `GroupedExtensionLoadingToast` already was. The census was doing exactly what it was written to do. 3. `chatStreamStore.browserSubagent.test.tsx` reached the network. #260's spec sets `BROWSER_SURFACE_MARKER`; #229 made `userActionHeaders()` state the host's `BIOROUTER_PROVIDER" on a browser surface, so the spec now reads `/config/read`. Stubbed `readConfig` in the spec's own mock list. vitest: startChatFailure 14/14, chatStreamStore.browserSubagent 7/7.
Picks up PR #262, which fixes the harness defect this branch's CI kept tripping over: `MentionPopover.privateChat.test.tsx` installed a jsdom `scrollIntoView` stub in `beforeEach` and deleted it in `afterEach`, but the scroll runs from a PASSIVE effect that React flushes during setup.ts's `cleanup()` — which vitest runs AFTER the spec's own `afterEach`. Measured: the polyfill is on `origin/main` (99bf415) and on neither PR branch, and both branches still carried the per-test install/delete at lines 77-78 and 109-111. One scheduler turn wide, so it only opens on a loaded runner. Conflicts, and how each was settled: * CLAUDE.md, docs/deployment/serve-decisions.md, docs/deployment/programmatic-session-access.md — both sides appended a numbered record or a table row. Kept BOTH, in number order: this branch's SD-10 then main's SD-11, and main's `/agent/cancel` row beside the `authorize_agent_control` row it shares a mechanism with. The SD range in CLAUDE.md becomes the union, SD-1..SD-11. * routes/session_reach.rs — the module doc's gated list and its "two spellings, one list" paragraph. Union of both sides' rows; the count is now seven routes reached through a helper (four via `authorize_agent_control`, two more via `authorize_turn_control` into it, and `/active_work/{id}/cancel` via `work_reach`). The over-read control list drops `get_session_extensions`, which this branch GATED and which therefore can no longer serve as a control, and keeps main's extra `reply::routes` control. * routes/agent.rs — both branches gated `GET /agent/callable_tool_count`, independently. Kept main's body (it handles an initializing child and places the gate before the agent is minted) inside this branch's wrapper, because the wrapper is what makes the refusal PLAIN TEXT through `SessionOutOfReach::into_response` instead of JSON through the route's `ErrorResponse`. That is not cosmetic: `one boundary has one body` is measured by two tests in `routes::session_reach`, and `?` failed both. This branch's `GET /agent/tools` gate is untouched by the merge and its `/agent/tools` row replaces main's now-false "deliberately not here" note — reach and Gate-E filtering are different questions, and only the second stays deliberate there. * tests/privacy_guard_wiring.rs — one census row per file, extended not added: agent.rs is `c(6, 6, 0)`, this branch's two new gates plus the four that were already there, with main's SD-11 note that the turn-control gate adds no call. Verified by running the census rather than by counting. * ui/desktop KnowledgeContext.tsx, useHistory.ts, useKnowledgeBases.ts — main's refactor already sends the user-action proof this branch was adding (`fetchKnowledgeSelection`, `refreshBases`), so main's side stands, keeping only this branch's proof header on the base delete. The `setPrimaryKbId(null)` beside it goes: the docstring directly above it says there must not be one, because the delete is itself the repair (D2). * openapi.json / types.gen.ts — kept in step with the utoipa annotation. Also fixes the one genuine, deterministic failure on this branch: `crates/biorouter-server/tests/privacy_ar15_is_retired.rs` refused the new line `the secret as "not a human" for changing a tier (AR-11/AR-15)` in docs/security/privacy-tiers-execution-plan.md — a bare citation of a closed risk with no disclosure word within 250 bytes, which reads as a live one. Fixed in the prose: both risks are now links, and the sentence says AR-15 is retired and that the tier route's posture is *why*. Measured after the merge: cargo test -p biorouter-server --test privacy_ar15_is_retired 7 passed cargo test -p biorouter --test privacy_guard_wiring 3 passed cargo test -p biorouter-server --lib -- routes::session_reach 47 passed
…re two counts Follow-up to merging 133 commits of main into this branch. Nothing here changes behaviour a user can see; each item is a claim in the six commits that the merge made false. **The canonical matcher moved, so the port's three citations were dead.** PR #266 renamed `crates/biorouter/src/marketplace/search.rs` to `crates/biorouter/src/catalog_search.rs`. `landing/marketplace-search.js`, `landing/scripts/baam-search.test.mjs` and `landing/baam.html` each named the old path as the module to read for the reasoning -- the one pointer a future reader of a port most needs. All three now name the new path and say when it moved, the way `ui/desktop/src/components/baam/search.ts` already does on main. **And PR #266 did not only move it -- it changed a rule the port mirrors.** The verbatim-phrase bonus became `written_in`: present as a substring AND not buried inside a longer word at either end, with the boundary imposed only by an end that is itself a word character. The port still asked `indexOf(...) !== -1`, so it had drifted from the module it claims to mirror rule for rule. Ported, with that module's own six assertions run against it. Worth being exact about the blast radius, in both directions: the shelves call `matching`, which collapses `rank` to a membership set, so **nothing on the page moves** -- a visitor sees the same cards in the same order either way. That is also why the drift could have sat there indefinitely, and why the rule is now pinned by a test rather than left to the next reader to notice. **`declared_model_names`' own doc contradicted the comment 100 lines below it.** The route's `Ok(None)` arm still said "six of those nine ship a curated `with_models(...)` catalog" -- the figure produced by the instrument the function's doc-comment exists to warn against, which undercounts by three because `ProviderMetadata::new` also takes a `model_names` list. In a commit whose subject is copy that does not contradict itself, that is the wrong comment to leave standing. It now reads nine, measured off `known_models`, and points at the function that explains the measurement. **The subagent count moved 197 -> 198 while this branch waited.** The whole point of that CLAUDE.md line is that a wrong figure is worse than none, because this repo asserts "pre + N" against it. Re-measured on the merged tree: BIOROUTER_DISABLE_KEYRING=true cargo test -p biorouter --lib -- subagent test result: ok. 198 passed; 0 failed; 3794 filtered out so the line carries 198 and today's date, and now also records that it moved by one in a single week -- which is the argument for re-measuring rather than for trusting any figure written here, this one included. Verified: node --test landing/scripts/baam-search.test.mjs 17/17 (the browser half drives the real page, so the baam.html edit is covered); baam-privacy-facet.test.mjs 14/14; build-registry.mjs --check reports all three outputs current, and no registry datum is touched.
No conflicts. The point of the merge is PR #262, which fixes the harness defect this branch's `Unit tests (vitest)` run tripped over: FAIL src/components/MentionPopover.privateChat.test.tsx > offers this chat's knowledge bases, not every base, and names its primary TypeError: selectedElement.scrollIntoView is not a function at src/components/MentionPopover.tsx:819 (commitHookPassiveMountEffects) Not this branch's feature and not an assertion: the spec installed a jsdom `scrollIntoView` stub in `beforeEach` and DELETED it in `afterEach`, but the palette's scroll runs from a PASSIVE effect that React flushes when setup.ts's `cleanup()` unmounts — and vitest runs the spec's own `afterEach` BEFORE that. The property is already gone when the queued effect fires, so the throw lands inside React and fails a test that had already asserted its point. The window is one scheduler turn wide, which is why it opens on a loaded CI runner and reads as flakiness. Measured rather than assumed: * the polyfill now lives once, process-wide, in ui/desktop/src/test/setup.ts with a regression guard in src/test/scrollIntoViewPolyfill.test.tsx — both present on origin/main at 99bf415 (PR #262) and on NEITHER PR branch, which merged an older main; * this branch still carried the per-test install/delete at MentionPopover.privateChat.test.tsx:77-78 and :109-111, and the merge replaces the whole spec with main's; * vitest.config.ts sets no `pool`/`isolate`, so the defaults (forks, isolate: true) apply and cross-FILE pollution is structurally impossible — the leak was always inside this one file. After the merge, on this branch: npx vitest run --testTimeout=30000 --hookTimeout=30000 (excluding src/utils/artifactCdnAssets.browser.test.ts, whose afterAll browser.close() exceeds 30 s under load on pristine main too) Test Files 458 passed (458) Tests 5179 passed | 1 skipped (5180)
PR #242 and its port PR #255 stopped searching the `license` FIELD, because every BAAM entry is Apache-2.0 and a licence separates nothing. The defect survived, one field over: the registry publishes the licence again as one of each entry's own TAG chips — and, for a skill, a third time among its keywords — and labels are searched, rightly. A test asserting "the license is not searched" passed while `PACS` still listed most of the catalog. Measured by driving the real Browse-extensions modal against the live 37-entry registry, with the field already gone: (empty) 37 PACS 31 pac 31 apache 31 Apache-2.0 32 zzzznope 0 `PACS` ≡ `pac` ≡ `apache` = 31 identifies the path: `PACS` → its singular `pac` → inside `apache` → the `Apache-2.0` chip on 31 rows, none of them about PACS (BenchlingAgent, DNAnexusAgent, OMEROAgent…). One rule, in all three copies: a label that says nothing its entry's own licence does not is dropped when the searchable text is assembled. By WORDS, not by equality — the tag is `Apache-2.0` and the keyword is `apache`, so an equality test drops the tag and leaves the keyword matching 49 skills. Over all 166 live entries the rule drops the 129 licence labels and nothing else. Counts after, identical on both matchers (Rust / TypeScript): (empty) 37 -> 37 ext, 129 -> 129 skill PACS 31 -> 1 ext, 51 -> 7 skill pac 31 -> 1 ext, 51 -> 7 skill apache 31 -> 0 ext, 49 -> 0 skill Apache-2.0 32 -> 1 ext, 49 -> 0 skill Every legitimate query is byte-identical before and after: `R scripting ggplot visualization`, `r-scripting`, `SPOKE knowledge graph`, `ggplot`, `heatmap`, `python`. Differential parity re-run over 1,192 shared queries × 2 catalogs: 0 set mismatches, before and after (the 391 tie-order differences are the one divergence the port's header already documents — Rust breaks ties by id, the document by registry order). The website shelf shared the defect and was worse, because its `data-license` is on every card rather than only the tagged ones: `apache`, `Apache-2.0` and `pac` each matched all 37 extension cards and all 132 skill cards. Registry data is untouched; only the haystack changed.
`./scripts/clippy-lint.sh` fails on `main` today, and this tree added a third violation to the two already there. All three are extractions, not rewrites: no statement moved relative to another, and each helper keeps the comments that say why its guards exist. - `commands/agent.rs` `run` (101/100) — the block this tree grew. The proof-of-user read, the level of its report and SD-12's launch-state pin move into `install_user_action_proof`, which keeps them beside the read they all depend on. The pin still lands before `AppState::new()` and before any route is mounted, because the one call site is where the inline block was. - `agents/code_execution_extension.rs` `handle_execute_code` (102/100, from #246) — the `_meta` assembly moves into `collected_meta`. Assembly only; every key there has a reader in the desktop artifact panel. - `session/session_manager.rs` `backfill_privacy_from_recorded_provenance` (105/100) — the shape guards move into `prepare_backfill_shape`, which answers `None` for "skip the backfill" and `Some(turn_ledger)" for "run it, and here is whether the second evidence source is readable". Each guard in it is a failed startup that happened; the comments saying so travel with them. `./scripts/clippy-lint.sh`: all baseline checks pass. `cargo fmt --check` clean. `cargo check --workspace --all-targets` clean.
`clippy::string_slice` is `warn` in the workspace `Cargo.toml`, so under
`scripts/clippy-lint.sh`'s `-D warnings` the new
`every_failure_before_a_new_chat_is_returned_discards_it` was a hard error:
`&body[start..end]` panics when either bound falls inside a UTF-8 character.
It cannot here -- both bounds come from `str::find` -- but "cannot" is the
argument the lint exists to stop being made in a comment, so the bounds are taken
through `get` and the reasoning moved into the `expect`.
Measured on the merged tree:
cargo test -p biorouter-server --lib -- routes::agent
test result: ok. 63 passed; 0 failed
cargo clippy --all-targets -- -D warnings \
-A clippy::result_large_err -A clippy::string_slice
exit 0, zero findings
⚠ Those two `-A` flags are NOT this branch's debt and must not be read as a
waiver of it. Both fire on files this branch does not touch, and pristine
origin/main was measured failing the same way:
* `result_large_err` at routes/reply.rs:1863 (`authorize_steer`) -- reproduced
on a detached origin/main worktree, `cargo clippy -p biorouter-server --lib
-- -D warnings`, exit 101, same line;
* `string_slice` at commands/agent.rs:480 (`&digest[..62]`) -- the line is
byte-identical on origin/main and absent from this branch's diff.
Allowing exactly those two is what lets the sweep reach every other target and
show that this branch adds nothing. CI is unaffected either way: rust.yml's
clippy step is `cargo clippy --workspace --all-targets --locked` with no
`-D warnings` and is labelled informational, so both are warnings there.
#276 and #277 both change how the website's shelves are searched, from different ends, and the merge needs both: * #277 replaces the whole-phrase `hay.indexOf(q)` with the tokenised, ranked matcher in `landing/marketplace-search.js` (`hits.has(c)`), a port of `catalog_search.rs`. * #276 removes the LICENCE from what is searched — it reaches the haystack as `data-license`, as an `Apache-2.0` chip inside `textContent`, and as a tag. Taken naively the merge keeps #277's matcher and silently loses #276's fix, because #277 assembles its own fields in `cardFields` rather than through #276's `searchHaystack`. Composed instead: - The three conflicted call sites take #277's `hits.has(c)`. `searchHaystack` stays, because the FACET loop still reads `hay` and must read the licence-free one. - `cardFields` drops the licence by WORDS, not by equality: `apache` and `Apache-2.0` are one licence spelled two ways, and the skills shelf carries both. `namesOnlyTheLicense` is #276's own predicate. - And `data-tags` is not the tag list — it is `name + organization + version + description + tags`, so it also carried the VERSION. `v0.2.0` tokenises to `v0`, `2`, `0`, and a bare `2` or `0` — which is exactly what "Apache-2.0" tokenises to — matched **35 of 37** cards through their version numbers alone. Version tokens are dropped there and stripped from the searched `.ext-org` label. Neither `catalog_search.rs` nor the desktop port searches a version, so this is what keeps the three in step. That took `Apache-2.0` from 35 hits to 1, and the one that remains is `ucsfomopagent`, whose description reads "v0.2.0 adds built-in OMOP/SQL-Server context" — the digits, not the licence. The Rust and desktop matchers answer this query with the same single entry. So #276's website assertion was rewritten rather than satisfied. "nothing matches Apache-2.0" is a property of a whole-phrase matcher; under a token matcher the honest claim is that **the licence explains none of it**, and the test now asserts the hits for `Apache-2.0` equal the hits for `2 0`. `apache` and `APACHE` still assert exactly `[]`. landing: baam-privacy-facet 16/16, baam-search 17/17, build-registry 56/56, check-docs-privacy 21/21, `build-registry.mjs --check` current (37 extensions, 129 skills), `check-consistency.mjs --check` clean. No registry datum touched.
…tise its own base `bind_allowed` is asymmetric on purpose: Gate A refuses only the DOWNWARD bind, because putting a MORE private model in front of a conversation cannot leak that conversation. Gate H inherited that predicate for every alternate-provider construction site, and on one of them the premise does not hold. `build_model_ref_provider` (`agents/knowledge_tool.rs`) builds a provider whose tier does not stop in this process: it becomes `SourceIngestArgs::caller_capability` / `ConversationIngestArgs::caller_capability`, crosses to `caller_is_private`, and lands in `knowledge::tier::raise_unlocked` — a permanent, monotone ratchet on a knowledge base. So the "harmless" upward choice let a PUBLIC chat name a PRIVATE model in `platform__ingest_source`'s `model` argument and mark its own base private for good, after which every KB read choke point refused that chat. The ratchet fires in `prepare_ingest_base`, before the sub-agent runs, so a curation that failed outright still cost the user the base — and the tool answered with an ordinary per-source report rather than an error. Both knowledge paths reach an alternate provider through that one function — the `model` argument (a provider name the MODEL wrote) and a base's stored `default_model` on a scheduled digest — so the gate goes there and a third knowledge path cannot be added without passing it. It now asks `assert_alt_provider_matches_session`: Gate A's rule AND DR-16's, i.e. the tiers must match. The laxer `assert_alt_provider_allowed` stays correct for CLI plan mode and prompt hooks, whose provider is named by a person's own configuration and whose tier stops in this process. The new half asks `privacy::tool_bind_allowed` rather than re-spelling `is_private() == is_private()`. That predicate arrived on main after this work was written, for the same reason on the sibling surface (`workspace_set_tools`' provider switch), and DR-16's rule must not have two sets of cells; the census rows are what keep the two sites asking one predicate. Every construction site was re-enumerated, not assumed: grep the workspace for `providers::create` / `create_from_persisted` / `create_with_named_model`, keep the calls where the provider is not the one the session row records AND a session's content reaches it. The result is the three sites already documented, plus two KB-keyed exemptions (the HTTP macro route and its CLI twin, which have a base id and no session). Everything else falls out as a bind (Gate A), the spawn (which already refuses both directions), a canned probe with no session content, or DR-21's app bind. The method is written into the module doc so the next reader can redo it instead of trusting a summary. Also recorded, because a stale record is worse than none: `privacy-tiers.md`'s "Did not ship" item 3 claimed DR-16's raise was HTTP-only and that `workspace_set_tools` had no self-target guard. Both were already false on main and are now corrected there, with this finding appended as the second half of the same story. This replaces a WIP commit whose own message said its ~419 insertions were unverified. What was measured, on the tree merged with main at 9096d37, every cargo invocation under BIOROUTER_DISABLE_KEYRING=true: cargo check -p biorouter --lib --tests clean, 0 warnings cargo test -p biorouter --lib privacy:: 246 passed, 0 failed cargo test -p biorouter --test privacy_capability --test privacy_guard_wiring --test privacy_toggle --test privacy_disclosure_toggle 4 / 3 / 4 / 1 passed, 0 failed cargo test -p biorouter-mcp --lib knowledge::tier 31 passed, 0 failed cargo test -p biorouter --lib -- knowledge_source_tool knowledge_tool 25 passed, 0 failed cargo fmt --check clean (the WIP was unformatted in three places; fixed) ./scripts/clippy-lint.sh strict clippy clean on -p biorouter -p biorouter-mcp The instrument was verified rather than trusted. Neutering the new predicate, and separately removing its master-switch read, each turned `the_master_toggle_governs_every_gate_in_both_directions` RED at the armed and the quiet assertion respectively. Reverting the call site to the laxer sibling turned both behavioural tests RED. The toggle test gained the new gate in both directions, which the WIP had left untested: the ratcheting half has its own `privacy_tiers_enabled()` read, so the sibling's assertion said nothing about it. `./scripts/clippy-lint.sh` still fails the whole workspace on `result_large_err` at `routes/reply.rs:1863` (`authorize_steer`, from SD-11's 5c399eb). That is pre-existing: the file is byte-identical to origin/main, and the same error was reproduced on a detached control worktree at 9096d37. CI's clippy step passes no `-D warnings` and is labelled informational, which is why main can carry it. Two `too_many_lines` baseline warnings are pre-existing the same way, in files this branch does not touch.
…11, not the draft `the_terminals_empty_steer_is_answered_by_the_gate_and_touches_nothing` failed on the merged tree — `test (ubuntu-latest)` on #275, `turn_control_no_user_key.rs:625`, `9 passed; 1 failed`. Neither side is broken; the test encodes a ruling that changed under it, and only an integration binary could see it (CI runs `tests/*.rs` on ubuntu only, so #261 and #240 were both green apart). #261 was written when SD-11 read "Stop **and steering** work on a daemon with no key": `/interrupt` would pass the reach gate, so an empty steer reached the text check and answered **400** for a chat the gate admitted. SD-11 then settled narrower — `/interrupt` asks for the proof on BOTH kinds of daemon, because `/reply` is refused 409 by the BR-33 single-turn lock in exactly the state where a steer lands, so admitting it would add silent mid-turn injection that nothing else there can do. `reply::steer_refusal` therefore answers from the HEADERS, before the body is parsed and before any chat is resolved: every row is a 403 carrying `STEER_NO_KEY`. What the terminal needs is unchanged, and is what the test still asserts: never the EMPTY 403 that means "this daemon holds a key" (which would send a `serve` user to find one that does not exist), always a sentence it can print instead (`key_verdict` → `Refused`), and the question touches neither the turn nor the agent's queue — now trivially, since nothing is reached. The five rows are kept rather than collapsed, so a change that admits the steer for SOME chat fails here instead of passing quietly. Also sharpened `steer_gate_question`'s doc in the CLI: it listed all three answer shapes correctly but let a reader expect the 400 from a keyless daemon, where it is now unreachable.
…ining it The last copy of the pattern PR #262 removed. `src/test/setup.ts` installs `Element.prototype.scrollIntoView` ONCE for the process and its own comment says why a per-test install-and-delete is a race the polyfill cannot win: the strip calls it from a PASSIVE effect, and vitest runs a spec's own `afterEach` BEFORE the shared `cleanup()` whose unmount flushes that effect. `ChatTabStrip.reveal` still did it, and its `delete` was unconditional — so it also removed the shared polyfill for the rest of that file's process, which is the wider half of the bug. Now `vi.spyOn(...).mockRestore()`, exactly as `setup.ts` prescribes and as `IngestPanel.test.tsx` already did. Swept the rest of `ui/desktop/src`: this was the only remaining redefiner. `boot-splash.test.ts` deletes `Element.prototype.animate`, which `setup.ts` does not install, so it restores jsdom's own state and is not the same hazard. `ChatTabStrip.reveal` + `scrollIntoViewPolyfill`: 5/5. Prettier clean.
Broccolito
added a commit
that referenced
this pull request
Sep 12, 2026
…anch already holds Content-free by construction: `git diff b1d3232 origin/main` is empty, because #275 merged the integration branch this PR was already merged with. Recorded so the branch CONTAINS main in its ancestry and not merely in its content, which is what stops a later read of this PR coming back DIRTY. Tree unchanged — verified by comparing the tree hash either side of this commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands every finished branch that was still open, as one tree, because the alternative was not working: each of these PRs appends a record to
docs/deployment/serve-decisions.mdorCLAUDE.md, so merging any one of them conflicted the rest. Serially that is one conflict resolution and one full CI cycle per PR; merged as one tree each branch conflicts exactly once, against an accumulating resolution, and CI runs once. Every branch's own head is a parent here, so each PR closes as merged.What is in it
fix/serve-private-default-new-chatclaude/nostalgic-fermi-6569afErrfix/f3-app-model-cross-windowclaude/sad-brattain-13f61dclaude/great-panini-672c85claude/ecstatic-einstein-dd05d6/active_workshows and stops only the work of chats the caller may reachclaude/nice-villani-42c71bclaude/gifted-shtern-1b6949biorouter webserves no transcripts, and gates the turn that reaches a chatfix/ui-a11y-and-approval-cardfix/serve-token-apps-sigterm-and-cli-gapsfix/license-tag-still-searchedfix/provider-copy-workflow-kb-and-docsfix/gate-h-alt-provider-tierFive defects only the merged tree has
Each is a pair of PRs that were green apart and wrong together. None is visible from either side alone, which is the argument for integrating before landing rather than after.
commands/serve.rsdid not compile. Four serve / apps serve / CLI lifecycle defects: an ignored browser token, an orphaned daemon, a stale cancellation warning, and an ungated scheduled run #273 replaced the browser token'sOption<String>with theBrowserTokenenum and does the env set/remove in its ownmatch browser_token.value()above the spawn; fix(serve): a new chat starts on the host's configured private model (QA F1) #229's branch still carried the older.envs(browser_token.iter()…)line, and the merge kept both. Dropped the superseded line — Four serve / apps serve / CLI lifecycle defects: an ignored browser token, an orphaned daemon, a stale cancellation warning, and an ungated scheduled run #273's match is strictly better, because it also removes an inherited token for--no-token, which the old line could not.ui/desktop/src/utils/startChatFailure.test.tsscanssrc/for anything callingcreateSession|startNewSession|startAgentand requires the shared notice. Main'sExtensionLoadFailureNotice("Ask Biorouter") starts a chat and let the rejection go unhandled: the notice stayed on screen and nothing said the chat had not opened. Wired tostartChatFailureNotice, asGroupedExtensionLoadingToastalready was. The census did exactly what it was written to do.chatStreamStore.browserSubagent.test.tsxsetsBROWSER_SURFACE_MARKER; fix(serve): a new chat starts on the host's configured private model (QA F1) #229 madeuserActionHeaders()state the host'sBIOROUTER_PROVIDERon a browser surface, so the spec now reads/config/read. StubbedreadConfigin that spec's own mock list — the network guard is what caught it.catalog_search.rs. Composed,data-tagsturned out to bename + organization + version + description + tags, sov0.2.0tokenises to2and0— andApache-2.0tokenises to exactly those two digits. Measured: 35 of 37 extension cards matched "Apache-2.0" through their version numbers alone. Version tokens are now dropped there and stripped from the searched.ext-orglabel, which also restores parity: Rust, the desktop port and the website now answer that query with the same single entry (ucsfomopagent, whose description reads "v0.2.0 adds built-in OMOP/SQL-Server context").the_terminals_empty_steer_is_answered_by_the_gate_and_touches_nothingfailed on the merged tree only — CI runstests/*.rson ubuntu alone, so fix(cli): the terminal asks the daemon before it asks you for the user-action key (SD-11 follow-up) #261 and fix(serve): Stop and steering work on a daemon with no user-action key (SD-11) #240 were both green apart. fix(cli): the terminal asks the daemon before it asks you for the user-action key (SD-11 follow-up) #261 was written when SD-11 read "Stop and steering work on a keyless daemon"; SD-11 then settled narrower, andreply::steer_refusalnow answers from the headers before any chat is resolved. The test's expectations were updated to the settled ruling, and what the terminal actually needs is unchanged and still asserted: never the EMPTY 403 (which would send aserveuser to find a key that does not exist), always a sentence it can print, and the question touches neither the turn nor the queue.Conflict resolutions, and what each preserved
docs/deployment/serve-decisions.md— five branches append anSD-nrecord. Kept every record, in number order: SD-10 (fix(privacy): /active_work shows and stops only the work of chats the caller could open #257), SD-11 (fix(serve): Stop and steering work on a daemon with no user-action key (SD-11) #240/Browser: a subagent's tab is read-only, and says so before you try (SD-8) #260), SD-12 (fix(serve): a new chat starts on the host's configured private model (QA F1) #229), SD-13 (fix(web): biorouter web serves no transcripts, gates the turn that reaches a chat, and stops reflecting the URL into script context (#56) #267). The header blurb was composed to name them rather than whichever branch merged last.commands/agent.rs— fix(serve): Stop and steering work on a daemon with no user-action key (SD-11) #240 logs SD-11's classification (NoUserActionKey::warning(), four arms, each naming what a keyless daemon does differently); fix(serve): a new chat starts on the host's configured private model (QA F1) #229 splits the same state by whether the launcher declared a key and pins the launch configuration. Composed rather than picked:warning()still carries the cause and the SD-11 consequence, fix(serve): a new chat starts on the host's configured private model (QA F1) #229's new-chat sentence is appended, and the level is the launcher's declaration — a deployment that can hold no key warns, a launcher that dropped one errors.record_launch_statestill runs beforeAppState::new(). Both PRs' own tests pass in that filter (12/12), includinga_launcher_that_promised_a_key_does_not_inherit_the_keyless_exemptionanddrift_since_launch_costs_the_keyless_exemption_and_nothing_else. fix(privacy): /active_work shows and stops only the work of chats the caller could open #257 added a third new function at the same point (served_operator_capability); all three are kept.components/Hub.tsx— union of imports; Desktop: every window's model chip follows the app-wide selection, and a chat's switch stays in the chat (F3) #247'sif (!(await confirmNewChatModel())) return false;still sits aboveclearExtensionOverrides(), so a refused send consumes nothing, and main's doc comment explaining thefalsecontract toChatInputis kept.routes/session_reach.rs— the module header's route table takes fix(privacy): /active_work shows and stops only the work of chats the caller could open #257's version (a superset, including the/active_work/{id}/cancelrow) withreply::authorize_steerrenamed to the function that exists,reply::steer_refusal. The anchor comment takes fix(privacy): /active_work shows and stops only the work of chats the caller could open #257's reasoning, which matches the code below it (get_session_insights/running_sessionsreplacedget_session_extensionsas the ungated controls once QA gated it).agents/mod.rs— bothpub(crate) mod script_call_gate;(fix(code-execution): every call a script makes faces its own permission decision (QA F7) #246, via Enforce the checklist for multi-step turns (planning gate) #250's base) andpub use schedule_tool::describe_cron;(Four serve / apps serve / CLI lifecycle defects: an ignored browser token, an orphaned daemon, a stale cancellation warning, and an ungated scheduled run #273); kept both, with the re-export beside the module it re-exports.Also fixed here:
./scripts/clippy-lint.shwas failing onmainThree
clippy::too_many_linesviolations, two already onmain(102/100 inhandle_execute_code, from #246; 105/100 inbackfill_privacy_from_recorded_provenance) and one this tree added (101/100 incommands/agent.rs::run). All three are extractions, not rewrites — no statement moved relative to another, and each helper keeps the comments that say why its guards exist. The gate now passes.Verified locally on this tree
cargo fmt --checkclean;cargo check --workspace --all-targetsclean;./scripts/clippy-lint.sh— all baseline checks pass.npx vitest run(excludingartifactCdnAssets.browser, whoseafterAllbrowser.close()exceeds 30 s on pristinemaintoo): 469 files, 5291 passed, 1 skipped, 0 failed.npm run lint:check:tsc --noEmit, ESLint,check:themes, 332 contrast assertions,check:tokens— all pass.npx prettier --check "src/**/*.{ts,tsx}": 0 failing files, measured 2026-09-12. Nothing in CI runs Prettier, so this is a measurement, not a gate.landing/:baam-privacy-facet16/16,baam-search17/17,build-registry56/56,check-docs-privacy21/21,build-registry.mjs --checkcurrent (37 extensions, 129 skills),check-consistency.mjs --checkclean. No registry datum touched.--lib --binsand everytests/*.rsintegration binary CI runs (thesandboxandui_example_appsexclusions aside) — see the check runs on this PR.Not in it
.brxtto biorouter.ucsf.edu with no digest pinning. That is an operator's decision, not an agent's.main.🤖 Generated with Claude Code