Skip to content

Add BioRouter Crew with shared daemon services and native CLI - #366

Merged
Broccolito merged 432 commits into
mainfrom
codex/biorouter-crew
Sep 27, 2026
Merged

Broccolito merged 432 commits into
mainfrom
codex/biorouter-crew

Conversation

@Broccolito

@Broccolito Broccolito commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

BioRouter Crew adds SSH workspaces with teams, channels, invitations, persistent messages, attachments and individually owned agents. Its Linux broker runs under an ordinary user without sudo or an exposed network listener. Native OpenSSH handles host verification, jump hosts and interactive authentication.

The desktop, biorouter crew, and explicit shared-daemon conversations use common daemon services for authentication, credentials, connections, transfers, observation, grants and tasks. Human approval remains separate from SSH login and provider credentials. Transfers verify hashes and retain restart receipts; uncertain submissions are not retried automatically.

Private SSH connections now require an institution ID. The workspace host confirms an immutable shared institution label. Local models can serve any institution; institutional models must match the private workspace. Admission and ongoing worker checks use the resolved provider affiliation, selected-source protection and policy epochs. A context change invalidates an earlier unprotected grant before output. Required institution provenance survives optional privacy settings being disabled. Crew conversation copying and divergence are refused until safe scope inheritance is supported.

Sending a message retains the verified room instead of flashing the join view. Enter sends; Shift-Enter inserts a newline, with composition and repeated-key guards. A development-only stdin path permits unattended synthetic QA approval input while preserving daemon identity/proof checks; installed applications retain their native prompts. Saved CLI conversations restore their provider through the trusted session store, and slow observer state admission no longer continually takes priority over queued messages.

Crew UI redesign campaign (2026-09-23 to 2026-09-25)

Evidence: redesign acceptance evidence, close-out handoff. Designs: UI redesign spec, naming design, plan §16. All 28 plan §16 work packages landed, plus the integrate-verify pass and four rounds of live-QA fixes.

What changed

  • Slack-like Crew redesign. The 2,175-line CrewView.tsx form is replaced by a layered interface built only from the design system: a sidebar of workspaces, teams, channels and members; channel header and menu; timeline; a composer that takes drop and paste; a details pane; "Ask my agent"; one dialog inventory; each error rendered once; no machine IDs on Crew screens.
  • Human-readable identity and naming (naming design D1–D17, slices S0–S3a). People, workspaces, teams and channels are shown and addressed by unique names. A person joins by invitation plus a device code (join_by_name_v1, on by default from 461f7899). The host or a team owner can add an admitted member directly. The broker enforces unique names and refuses system accounts at invite, including on the legacy token path. S4 (discovery, 8-digit codes) is deferred.
  • Revoke UI. Revoke is available from the chat's access pane and bar and from a channel's Agent access tab, with an inline confirm. A revoked chat holds Send and says why. When the workspace is unreachable it reads "Stopped on this device" and only Retry confirms. Past access keeps revoked grants.

Fixes found by live QA

  • Scope binding to sessions (SCOPE-BIND). A grant is bound to the chat's sessions.incarnation, so a reissued session ID never inherits it, and a deleted chat's grant restricts but never authorizes (0dcbc6fb, 9de11e76).
  • Journal checksum. The broker crate now declares serde_json/preserve_order itself. Without it, a standalone broker build refused every existing journal with checksum mismatch. The live upgrade, downgrade and re-upgrade then passed byte-identically. tests/journal_key_order.rs pins this (0cae537e).
  • Observer re-authorization and batching. A moved policy epoch is re-authorized rather than trusted. Each message in a batched page keeps its own source decision, and queued frames are decided again at admission (6f2b0b233).
  • Keepalive. The heartbeat is authenticated and re-dials use non-interactive credentials only. There is no re-dial after a refused heartbeat or for a revoked device, and whichever view first finds a drop re-dials it (3c81c7095, 3b2130ab1, 334cffc76, bd7cd6033).
  • One-click hosting (D-HOST, "Start it for me"). It runs exactly the dialog's fixed commands behind the user-action proof, and refuses extra fields and shell metacharacters (4cd2152ba, dcdffab8d, 0ee194605, b5e955bea, 29c78640d).
  • Trusted drop and paste (D-DROP). The path comes only from Electron's webUtils.getPathForFile, followed by a native confirmation that the renderer cannot answer (5edc20f6d, 284c54b48).
  • Credential floor on file registration. The BR-23 floor applies to every POST /crew/files registration in both directions, with links, case variants and hard links resolved. It adds the netrc, pgpass, git-credentials, docker, kube and gh stores, and refuses settings, login and autostart download destinations and executable overwrites (9708e78dc, b7585ba1a, 6aca6c18a, 7339e6d9d).
  • Hosted-CI fixes on this push. On Windows, an_include_resolves_like_openssh pinned the absolute Include arm with /etc/ssh/extra, which Windows does not treat as absolute. It now uses a path that is absolute on every OS (d1aaae4f). On Linux, SecretGuard::is_denied asked is_inside_root of a path's lowercase spelling. Under a root with a capital letter that spelling names nothing on a case-sensitive filesystem, so a project's own negation (!.netrc) always lost to the floor. Containment is now decided from the path as given, and the test roots its project at a directory with a capital letter, so it is deterministic (ec305ddf; the old code was reproduced failing on a case-sensitive APFS volume).

Live QA trend. Four rounds ran on a disposable AWS fixture with synthetic data only. Fresh novice, security and accessibility critics drove the real development apps on private Versa gpt-5.5-2026-04-24.

Round 1 Round 2 Round 3 Round 4
Build 5959ebdc 5b079379 ac02e57a b1c87edc
Unaided novice task rate 39% (15/38) 57% (28/49) 85% (51/60) 91% (51/56)
Findings P0 / P1 / P2 5 / 23 / 41 0 / 16 / 62 0 / 5 / 58 0 / 3 / 54
Design score about 4½ 7 8.5 9

Three brand-new host and joiner pairs completed their journeys: Alice with Bob, Carol, Dave and Erin; Henry with Gina; and Iris with Jack (21 of 22 tasks unaided). Every broker boundary the security critics tested held in every round, checked against the broker journal. The round-4 P1s (Q4-01 re-dial, Q4-02 stale "Can't connect", Q4-03 file-name truncation) were fixed before the final build and re-checked there.

Final acceptance on 461f7899 (seven lanes, 2026-09-25 14:26–15:11Z)

  • Three users, three files, three agents (G04/G13): 7 of 7 PASS.
  • Revoke end to end, including an unreachable server: every core assertion PASS.
  • Mixed GUI and CLI (G11): 11 of 11 PASS.
  • Institution gate on the fourth profile: PASS, with zero egress.
  • Round-4 P1 re-check by a fresh novice: PASS, 4 of 4 tasks unaided.
  • Live observer fairness and the six-command self-test: PASS.
  • Denial matrix (G12): 4 PASS; (b) passed with partial coverage, because the model never sent an out-of-schema method; (d) failed on wording. The refusal was correct, but the person saw the broker's raw grant_expired envelope (D-1).
  • Not established: the real native share confirmation's wording (the stage answered it automatically), the daemon worker allowlist exercised by a model, Windows and Linux desktops, MFA and jump hosts, load, and storage faults.
  • Open P2/P3 findings, none of which leaks data: D-1; a stale connect-error bar after reconnect (NEW-1); revoke F1 (Edit in place bypasses the revoked composer hold), F2 (no Revoke while offline), F3 (an unconfirmed revocation is not retried on reconnect), F4 (CLI grants list labels) and F5 (duplicate rows).

Hosted CI on ec305ddf (the head before the final polish; see Final polish below)

All green: Rust 36160256955 (guards, serve, macOS/Ubuntu/Windows tests, both cross-checks), Frontend 36160256954, Apps smoke 36160256951, Computer Use native payloads 36160257045, Check Commit Messages 36160256969 (pull_request) and 36160251513 (push). Before this, only Rust failed. Windows failed at ac02e57a and b1c87edc on the Include test; the 61e30f55 run was cancelled when its fix was pushed. Ubuntu then failed at d1aaae4f (36156654158) on the secret-guard test. No flaky re-run was needed. The local just check-everything verdict at 461f7899 is not on file; re-run it before merging.

Needs human security review before merge

CLAUDE.md requires a person to review authorization, credential, MCP-protocol and async code. Every change below had independent agent review only.

  • SCOPE-BIND: grants bound to sessions.incarnation (0dcbc6fb, 9de11e76). Check that a reissued session ID never inherits a grant, that a deleted chat's grant restricts but never authorizes, and the --no-session IDs.
  • Grant and revocation state (67082118). Check that a failed task setup revokes its run, and that connections.json writes are read-modify-write under a cross-process lock.
  • Live shell output routing (ec6a154e). Check progress-token routing in mcp_client.rs, the 256-slot channel, and that an unknown token is never broadcast.
  • Direct add and system-account refusal (500bf8ada, 5c1c5cdfe, 456337eb8, 3b577b111, 5e8760460). Check that only the host or a team owner can add, the expected_username, recycled-UID and former-member refusals, and the legacy token path.
  • Observer re-authorization and per-page batching (6f2b0b233).
  • Keepalive and re-dial (3c81c7095, 3b2130ab1, 334cffc76, bd7cd6033). Check that the heartbeat is authenticated, that re-dials use non-interactive credentials only, and that a refused heartbeat or revoked device never re-dials.
  • One-click host start, D-HOST (4cd2152ba, dcdffab8d, 0ee194605, b5e955bea, 29c78640d). Check the fixed commands, the user-action proof, and the refusal of extra fields and metacharacters.
  • Trusted drop and paste, D-DROP (5edc20f6d, 284c54b48). Check the path source, and that the development auto-confirm gate fails closed outside validated dev profiles.
  • File-registration credential floor and download destinations (9708e78dc, b7585ba1a, 6aca6c18a, 7339e6d9d), including the ~/Library cloud-drive exception.
  • Secret-guard case-fold containment (ec305ddf). Check that containment is decided from the path as given, not its lowercase form. This makes a project negation work on case-sensitive filesystems, and it can also refuse more: an uppercase path outside the project no longer borrows the project's negations.
  • join-by-name on by default (461f7899). Check that every shipping build carries join_by_name_v1 and that --no-default-features still builds a broker without S3a.
  • Automatic revocation retry, F3 (cd94f12fb, b88ea1476, cf02f2d03, 07c2a104f, dc2746551). Check that the retry is armed at every connect and backs off 5 s to 5 min while connected. Check that run.revoke is signed with the device key, that revocation is merged across processes without ever losing a confirmation, and that replaced_grants keeps an unconfirmed stop when its chat is granted again or its ID reissued without authorizing anything.
  • Policy-changed mapping, D-1 (cd94f12fb, b6226ac59, 9248dcbb0). Check that a broker grant_expired ends the grant locally as ended_by_workspace without a run.revoke, that every workspace refusal at turn start goes through agent_error_text so no raw envelope is shown, and that a timed-out run is not marked revoked.
  • Worker-allowlist test, B-1 (9f84b4f08). Check that it drives the real Agent::reply under a Private capability through the real grant path, that the fake broker receives none of the five person-only methods, and that it fails with the allowlist disabled.
  • Connection-save no-op and early refusal, P-1 (cd94f12fb, fb2c0f4a8, f3dd33761). Check that an unchanged PATCH is judged under the registry's cross-process locks against connections.json and writes nothing, and that every save refusal is asked before the bridge is dropped.
  • Re-send hold in a revoked chat, F1 (9248dcbb0). Check that every path in BaseChat.tsx that sends outside the composer asks the Crew hold first. The daemon's own hold is the next item.
  • Daemon history-rewrite hold, F1 (715c59b5d, last fixes). CrewManager::history_rewrite_refusal is asked before anything stored changes in three places: edit_message's in-place arm (after the reach gate, before the turn lock and snapshot), /reply's conversation_so_far write-back (before apply_client_writeback), and both diverge doors for a proven caller. Check that each refusal is a 403 with the turn's own sentence, and that an unproven caller meets the reach gate first and learns nothing about the grant. Check that moving the standing checks into the shared grant_stands did not loosen the turn's check_tier. The check is local only, so a grant the workspace ended that this daemon has not heard about still edits, and its turn is refused.
  • Deferred design decision (not a fix in this PR): page script holds the daemon secret and the user-action proof, so it can reach POST /crew/files in both directions and POST /agent/call_tool (the structural half of Q3-01/Q4-55). The credential floor and destination rules are defense in depth. A registration proof held only by the main process needs a security design decision.

Validation

The implementation status, validation report, protocol contract and CLI guide retain exact artifact scopes and limitations.

  • Institution baseline e53: 62 core Crew tests, 6 session-provenance tests, 36 broker integration tests and 37 GUI tests passed, with typecheck, generated API checks and independent Astra review. The journal target selected zero tests and supplies no evidence.
  • Current fixes: 18 provider-factory tests including the causal saved-marker regression, one resume-decision behavioral test, the privacy-order audit and queued-frame reauthorization passed. The latter ran in both server library and daemon test harnesses. The added live slow-admission regression remains ignored and is not counted.
  • source bin/activate-hermit && CARGO_BUILD_JOBS=2 CARGO_INCREMENTAL=0 just check-everything completed with exit 0 at 00:41:39 UTC on September 24, 2026. Independent Astra review found no actionable issues. This checkpoint is e11fe795; its hosted checks and rebuilt runtime replay are pending.
  • Real e53 GUI acceptance used four isolated local profiles and four ordinary Linux UIDs on a disposable Ubuntu fixture. Three UCSF users connected, enrolled, joined their team and exchanged messages. Carol's Enter message reached untouched Bob after 4,073 ms without refresh or the join-screen flash; Shift-Enter inserted a newline.
  • All three users invoked actual Versa GPT-5.5 tasks that read distinct CSV fixtures, computed and wrote new totals, reread them and attached the results. Each real job exited 0 under its owner's UID. Independent SSH checks matched file ownership, bytes and SHA-256. Alice downloaded Carol's attachment through the native Save dialog with an exact hash match. Initial room history delivery was slow; the observer correction needs its own runtime replay.
  • A fourth private workspace labelled foreign-synthetic refused a UCSF-affiliated Versa task. Its refusing CONNECT recorder remained empty before and after the attempt. No task/context receipt was visible; an additional native CLI corroboration was blocked by automatic approval review and is not counted.
  • Required e53 Rust jobs failed on two stale test fixtures; their corrections have focused local passes. Other e53 required checks and native packaging passed. New-head checks remain pending. The model self-test has a typed no-grant refusal, but its six help/version child commands previously selected stale binaries; fresh replay is required.
  • Earlier Linux fixtures were independently cleaned. The active synthetic fixture has supervised cleanup at 01:10:57 UTC on September 24, with independent verification scheduled. Cleanup is not yet claimed.

Remaining draft gates and limits

Complete the matching artifact replay, ordinary-chat MCP/selected-context scenarios, CLI model-resume self-test, remaining privacy/recovery cases and hosted checks. The new CLI and observer fixes are independently reviewed; this PR remains draft until applicable acceptance is complete.

Shared native CLI IPC is Unix-only. Windows desktop acceptance remains open. Canonical broker storage requires a supported local Linux filesystem; NFS/SMB hosting, arbitrary shells, automatic failover and retention/compaction/backup tooling remain unsupported. Narrows NFS home hosting is refused; Leo basic chat/files are not runtime-qualified and its recorded Landlock ABI cannot support the execution helper. Crew copy/diverge requires starting a fresh conversation and explicitly granting context instead. Synthetic testing and SSH encryption do not establish HIPAA compliance.

Source and curated text evidence are public. Credentials, screenshots and raw visual/transcript evidence remain local. No merge is requested.

Final polish (2026-09-25)

The final acceptance lanes on 461f7899 left the G12 (d) wording failure (D-1) and some P2/P3 findings open: NEW-1, revoke F1–F5, F-1 and P-1. The worker allowlist had also never been exercised (B-1).

Fixes. 15 commits ending at dc2746551 fix them, in two groups: daemon, core and CLI; and the renderer. Each group had an adversarial agent review; the daemon group's first review failed F3 and P-1, and round 2 fixed both.

  • D-1. A policy change now reads "Crew settings changed since access was granted. Grant access again from Crew." in both interfaces, and the grant is marked ended_by_workspace.
  • F3. The daemon confirms an unconfirmed revocation with the workspace by itself when the connection returns.
  • F1. Edit in place and every other re-send is held in a revoked chat.
  • F2. Crew offers Revoke while offline.
  • F5. Past access rows are dated.
  • NEW-1. A failed Connect's raw transport text no longer lingers.
  • F-1. A refused transfer ends failed with a plain reason.
  • P-1. An unchanged privacy save is a no-op.
  • B-1. A new offline integration test (crew_worker_allowlist) sends the five person-only methods through a real agent loop, and each is refused.

Local gate. ALL_GREEN on dc274655: --workspace --lib --bins 8540 passed, 0 failed; 13 Crew and privacy integration binaries; no OpenAPI drift; 9213 frontend tests.

Live re-check. Run on dc274655 on a new disposable AWS fixture: two accounts, private Versa GPT-5.5, synthetic data. It passed all nine items it covered: D-1, P-1, F3 (three times), NEW-1, F1, F2, F5, F-1, and no unrequested navigation to Crew. The fixture was torn down and independently verified.

New findings from the re-check, none leaking data:

  • NEW-2 (P2): Crew opened after a drop stays Offline after the daemon reconnects.
  • NEW-3 (P3): a daemon-confirmed revoke is undated in Past access.
  • NEW-4 (P3): "Confirmed." shows for about 7 s.

Not re-run on dc274655: the three-user, institution, mixed GUI and CLI, fairness and self-test lanes.

Hosted CI on 55a345a8 (dc274655 plus the docs commit that records the above) is all green on first attempt, with no re-runs:

Needs human security review. The five final-polish items are added to the checklist above.

Records. Details are in the Final polish section of docs/research/biorouter-crew/evidence/ui-redesign-acceptance-2026-09.md and in the handoff's close-out. This PR stays draft.

Last fixes (2026-09-25)

After the live re-check, six commits (b5a0caaa0…715c59b5d) fix NEW-2 (Crew now follows the daemon's re-dial when it opens on a connection that dropped elsewhere), NEW-3 (a revoke the daemon confirmed after a 503 is dated in Past access and keeps its row), NEW-4 ("Confirmed." stays until dismissed or the pane is left) and two re-check observations: the chat's Crew bar offers Revoke while offline, and a workspace-ended grant reads "Ended: Crew settings changed". The daemon now also holds F1 itself: Edit in place, /reply's history write-back and diverge on a chat whose Crew access has ended are refused with a 403 carrying the turn's sentence, before anything stored changes (new offline binary crew_history_rewrite_hold). These fixes are verified by regression tests that fail on each parent and by a local gate that was ALL_GREEN on 715c59b5d (--workspace --lib --bins 8542 passed, 0 failed; 18 integration binaries; no OpenAPI drift; 9229 frontend tests). No live fixture ran, so none of them has been checked in a running app. Still open: the offline consent form's "a channel you can't see", replaced_grants not listed in the desktop, and human review, whose checklist above now includes the daemon hold. Details are in the evidence record's "Last fixes (2026-09-25)" section.

Hosted CI on 7b5482f7 (715c59b5d plus its docs commit): all green on first attempt, with no re-runs. Rust 36200867724 (guards, serve, macOS, Ubuntu and Windows tests, both cross-checks); Frontend 36200867727; Apps smoke 36200867730; Computer Use native payloads 36200867747; Check Commit Messages 36200867751 (pull_request) and 36200864129 (push).

Type of Change

  • Feature
  • Refactor / Code quality
  • Documentation
  • Tests
  • Security fix
  • Build / Release

AI Assistance

  • This PR was created or reviewed with AI assistance

GPT-6 Astra implements and reviews product code. GPT-5.6 Luna writes tests, runs validation and drives the app.

Project lineage

The README, NOTICE, governance, the landing about page, the research reports, the release notes and the history docs now describe Biorouter as an independent project inspired by Goose and other agents (Claude Code, Codex CLI, Gemini CLI, OpenHands, Aider, Cline, OpenCode and ForgeCode), not as a fork (45da68c6b). NOTICE keeps the Block, Inc. attribution lines ("This product includes software developed by Block, Inc." and "Copyright 2024 Block, Inc.") verbatim, as a legal notice. The functional GOOSE_ environment prefixes in code are unchanged. The published v1.91.0 GitHub release body still contains the old "Biorouter is a fork of Block's Goose" sentence: only the repository copy, docs/releases/notes/v1.91.0.md, changed.

User manual

The Crew user manual is in docs/crew/: 11 pages for members, hosts and IT staff, covering joining and hosting a workspace, teams and channels, messages and files, agents and chat access, privacy, troubleshooting, the biorouter crew commands and server administration. The landing site's documentation has a matching Crew page (landing/docs.html, #crew). Both are written from the UI copy, the CLI help and the Crew source, and were made consistent across pages at 3811a9b8f, which also stops command blocks on the landing docs page from pushing a step past the right edge. Final word counts (wc -w): 26,878 words in total. README 611, getting-started 1,955, hosting-a-workspace 3,298, joining-a-workspace 2,687, teams-channels-and-people 2,012, messages-and-files 2,080, agents-and-chat-access 2,742, privacy-and-security 1,882, connections-and-troubleshooting 2,791, command-line 4,156, administration 2,664. The landing Crew page has about 4,081 words of text.

🤖 Generated with Claude Code

@Broccolito Broccolito changed the title Add BioRouter Crew shared SSH collaboration and scoped agents Add BioRouter Crew with shared daemon services and native CLI Sep 23, 2026
…vice code, and documents revoke's exit status
…ion and device code, and says which responses are unauthenticated
…n, and list SCOPE-BIND among the work packages
…record the fail-closed revoke in P09 and G12
…resolved, and put the UI-DIALOGS row back in the progress table
The hello v2 verification bullet claimed a relay that strips signature_v2 cannot alter the capabilities and that they are display-only. BrokerHello::unsigned keeps them as unauthenticated hints and join_status acts on join_by_name_v1, so a bridge-path relay can add or strip them. State that, and why it fails closed, matching the naming design's compatibility table.
… unsigned capability can be stripped

The contract said a client sends a new parameter only after the capability
that introduced it is present. Neither client does that for
expected_username, and following it would let the M2 relay strip
signature_v2 and human_names_v1 and so switch off target_mismatch. State that
capabilities gate methods and screens, never a defense, and fix the naming
design's matching sentence.
A journal record's checksum is verified by re-serializing the parsed
record, so parsing must keep the writer's key order. The shipped broker
only had preserve_order through workspace feature unification with
biorouter-server; a broker built on its own (cargo build -p
biorouter-crew, and the crate's own tests) refused every existing
journal with journal_corrupt: checksum mismatch. Measured on the live
QA fixture: the standalone build failed to open both workspaces, the
same commit with the feature replayed them byte-identically.

The new test fails without the feature and passes with it.
…shared drop set

displayText.ts restated the \p{Cc}/\p{Cf} drop set inside its own rejected
class, a second definition the annotationChannel guard forbids. utils/untrustedText
now exports stripHiddenCharacters as the one definition (sanitizeUntrustedLabel
calls it: strip, trim, slice, as before), and sanitizeDisplayText calls it after
removing only the classes it refuses beyond that set.

The extra classes run FIRST. The obvious order (strip, then the rest) is not
equivalent: deleting a format character between a lone high and a lone low
surrogate joins them into U+10000, which no later pass removes. Measured equal
to the single-class implementation over every code point (three wrappings) and
800k random strings; the new tests were run against the old implementation and
pass there too, and the surrogate test fails on the reversed order.
…s v flag

INSTITUTION_ID_PATTERN had a bare '-' in a character class. A browser compiles
a pattern attribute with the 'v' flag, where that is a syntax error, and a
pattern that fails to compile is dropped silently: the privacy popover and the
onboarding field accepted 'UCSF' and left the refusal to the daemon. The '-' is
now escaped; the flagless RegExp isInstitutionId builds reads '\-' as the same
literal '-'.

nameRules.ts carried a corrected copy under INSTITUTION_FIELD_PATTERN because of
this defect. It is now a re-export of the one pattern, under the same name the
dialogs import. Tests compile the pattern under 'v' and with no flag and check
the same accept/refuse table under both; they fail on the old pattern.
…p offering attachment as a resolvable kind

The chat named its Crew channel only from the Crew view's in-memory labels, so after a
restart it fell back to 'a channel in {workspace}' although the daemon records the
destination's name at grant time. grants.ts now parses labels (every field checked as
text, a label for a different channel ignored), and the chat prefers that name, then the
remembered one, then the recorded workspace. Access rows use it only when the snapshot no
longer shows the channel. CrewUnknownName carries the daemon's case-only did_you_mean, and
'attachment' is gone from the resolvable kinds, since the daemon refuses it with
crew_invalid_selector.
… the staged helper

Formatting only: prettier --write on the seven files format:check failed on
(line wrapping, and the trailing commas and parentheses wrapping implies);
three are unchanged with all whitespace removed, the rest differ only in that.
Their tests and ESLint pass unchanged.

src/computer-use/ is the native helper scripts/computer-use-resources.js
stages. The repo .gitignore ignores it, but Prettier run from ui/desktop reads
only that directory's .gitignore and .prettierignore, so format:check failed
on its manifest.json and pin.json on any machine that had built the helper.
…rewFiles

The /crew route renders only CrewApp, so crew/legacy/LegacyCrewLayout.tsx,
crew/crew.css, CrewFiles.tsx and its regression test are deleted rather than
left compiled (D16). CrewView stays as the controller root the test harnesses
mount, with layout now required. The stylesheet guards lose their legacy
exemptions, and integration/legacyStylesheet.test.ts now fails if a deleted
path exists again or if any source file or build config imports, mocks or
@imports one. The upload privacy handoff CrewFiles.regression covered is held
by files/useCrewUpload.test.tsx.
…le in the 360px pane

An access row put the icon, the title and destination, the status badge and
the actions on one unwrapping line, so in the 360px details pane the title
and destination were squeezed to zero width and the More actions button was
clipped (D12). The line now wraps, in authored crew-app.css rules rather than
new utilities: the text block keeps at least 10rem, and the badge and the
actions share one end group that moves onto its own line, never shrinks, and
wraps inside itself before it would overflow. Names and order are unchanged.
AccessTab.test asserts the structure the wrap depends on; measured in headless
Chromium, the text keeps 308px at 360px and the last button stays inside the
row down to 240px. crew-app.css's header also drops its legacy exemption note.
…odes in the dialogs

A broker refusal reached the dialogs as an older daemon's raw
'Crew broker refused request: {json}' envelope, and the broker's new
codes (rate_limited, target_mismatch, not_invited, identity_unavailable,
unknown_account, quota_exceeded, identity_conflict, device_conflict,
identity_mismatch, already_approved) had no words of their own.

parseRefusal now removes the legacy envelope before reading the code, so
both daemons read the same. A person-written broker sentence is shown
without its code; the broker's technical texts (the journal and
state-size quota, the challenge rate limit, the member identity_conflict,
device_conflict and the two enrollment identity_mismatch texts) get copy
deck sentences. quota_exceeded is matched on its words because the join
quota means something else. The canonical-spelling hint no longer
captures the sentence's full stop, and Let in answers already_approved
with Replace code, which re-sends the same code with replace: true.
Fixtures are now the broker's literal texts, with legacy-envelope cases.
…w only the privacy an invitation states

D9. Re-pasting an invitation for a workspace this computer already has returns the
existing connection. JoinDialog then PATCHed it with the Advanced choices (which
disconnects it) and, when that failed, removed it together with its device key. The
dialog now reads existing_connection_id from the preview and offers 'Open {connection}'
instead of saving; a submit snapshots the saved ids first, never updates a connection
that was already here, and rolls back only one it certainly created (when the list
can't be read, nothing is removed).

D15. The summary read the planned save values (mode, institution_id), which default to
Private for a legacy paste, as the workspace's stated privacy. It now reads
workspace_mode and workspace_institution_id; when the invitation doesn't say, it says
so and leaves Private/Public unchosen, and Join stays disabled until the person picks.
The preview also keeps fingerprint and missing (a paste that names no server asks for
the login up front), and a 409 crew_connection_exists or crew_invitation_conflict
offers to open the connection it names. CrewJoinClaim carries JoinClaimed's inviter,
workspace_name and add_device instead of a never-sent principal. The JoinDialog pattern
assertion now reads INSTITUTION_ID_PATTERN, which 8473c84 changed.
…, then offer Reconnect

GET and POST .../join answer 409 crew_not_connected while the connection is down, and
the join card showed that as a poll error on every tick, forever. It now stops polling,
connects once by itself, and asks again; if the route still can't reach the workspace it
shows a Reconnect button (a user-initiated connect, so Sign in opens when the server asks).
A claim refused the same way reconnects and claims again. The card also uses what the
daemon's JoinClaimed and join status carry: the inviter and workspace name are remembered
for the screens after joining, and an add-device invitation says it adds this computer to
the person's account.
…ames, run start times and broker capabilities to clients

- CrewRouteError: a broker refusal envelope now answers broker_code and the
  broker's own sentence instead of the transport's JSON text (D1).
- Observer messages frames carry remaining and page_size, so a client can
  tell when the opening backlog ends and how large a page really is (D5).
- Each messages frame carries the people and channel_names its page gave
  that message, so a departed author keeps a name (D6).
- RunView records started_at; owned runs sort newest first and a state
  frame lists live runs plus the 20 newest finished runs per channel (D10).
- State frames carry the connected broker's capabilities (D13).
…ply stays under its stack

check_provider_dispatch (binding + tier + standing + a context.manifest worker
request) was awaited inline from Agent::provider, which Agent::reply reaches
through the tool-surface preparation. Its future was laid out in reply's own
frame and overflowed the default 2 MiB test-thread stack in privacy_toggle's
the_master_toggle_governs_every_gate_in_both_directions. Both it and
standing() now run their bodies behind Box::pin; every call site and pub
signature is unchanged, and no stack size is raised anywhere.

Measured: privacy_toggle passes three times with RUST_MIN_STACK unset, and the
stack-margin binaries (soft_interrupt_agent_loop, turn_abort_tests,
subagent_delegation, steer_always_lands) and privacy_guard_wiring pass.
The CLI printed a forwarded broker refusal as 'Daemon returned 400: …',
and from an older daemon as the raw 'Crew broker refused request: {json}'
envelope.

DaemonRefusal now carries the daemon's broker_code (validated as a code
and capped at 128 characters), falling back to the code inside an older
daemon's envelope, whose message becomes the refusal's text. The text
stays terminal-safe and apostrophes stay unescaped.

biorouter crew prints a broker refusal as the sentence the desktop shows
for the same code, byte for byte (a test reads the desktop's copy deck),
followed by the command that acts on it: enroll approve --replace for
already_approved, enroll invite @<canonical> for identity_ambiguous,
enroll invite --add-device for already_member, and 'Choose another
name.' for a name_taken that does not already say so. JSON output keeps
code and adds broker_code. Exit codes and the revocation handling are
unchanged.
…'s count, and name departed authors

- crewApi: CrewHttpError carries the broker's code (brokerCode, from
  broker_code) and api/errors exports crewBrokerCode (D1).
- Messages frames validate remaining, page_size, people and channel_names;
  people and channel_names are built on a null prototype and never keep a
  __proto__/constructor/prototype key (D5, D6).
- The controller exposes optional backlogComplete, pageSize, people and
  capabilities. useOpening ends the opening on backlogComplete and keeps
  its quiet/stall timers only for a daemon that sends no remaining; the
  timeline measures full pages by pageSize (D5).
- An older page asks at the observer's page size, halves it on
  response_too_large down to 1, and records the size it loaded with. A 5xx
  or a size/shape refusal returns to the live tail with the error in the
  connection bar; access, privacy, stale-cursor and unclassified failures
  still clear the verified view (D5).
- Every message page's people merge into a per-channel map that the
  timeline, pane and channel presentations pass to usePeopleDirectory, so
  a former author reads 'Name · former member' (D6).
- The timeline scrolls to each new newest message while following, since a
  full tail does not grow, and auto mark-read measures that the newest row
  is on screen when the dwell ends (D4).
- ObservedRun gains started_at; unanchored task rows appear only for live
  runs or runs that started within the loaded messages, oldest first, and
  newestTaskIn prefers the latest start (D10).
- uniqueNamesSupported prefers the broker's unique_names_v1 capability, so
  a new S2 workspace with no teams offers Rename (D13).
- An expired pending join reads 'Invitation expired · Invite again…' with
  no Let in, in the sidebar and in workspace settings (D15).
- Drop the deprecated Invitation.status?: never (D16).
Live shell lines were lost, leaked between concurrent calls, or never
shown, for four reasons that compounded:

- The developer shell sent each line as an MCP logging notification with
  no link to its tool call. It now echoes the call's progress token in
  `data.progress_token` (additive; absent when the call had none), on
  output lines and on the foreground heartbeat, which names the command.
- The client subscribed every unpooled dispatch to a broadcast, so two
  concurrent shell calls on one developer client each received the
  other's lines. Every dispatch, pooled or not, now registers a route
  under its own token; a tokened logging line goes to that route only.
  A token no route holds is dropped, never broadcast. An untokened line
  keeps the old fallback: broadcast on an unpooled client, dropped on a
  pooled one.
- A pooled (routed_only) client dropped every logging line, and its
  per-dispatch token never reached the server at all: rmcp 0.14's
  send_request_with_option overwrites _meta.progressToken with its own
  counter before merging PeerRequestOptions::meta. The token now travels
  through options.meta, which rmcp applies last. Tokens are per-client
  decimal numbers minted for every request, so they cannot collide with
  a listing's token and the recorded MCP cassettes still match.
- Per-dispatch channels held 16 notifications and try_send dropped the
  rest silently. Capacity is now 256; try_send stays so a slow consumer
  never parks a task per line, and drops are counted per route and
  logged once when the route is removed (RAII, also on cancellation).

Tests: tokened routing on unpooled and pooled clients, unknown and
malformed tokens dropped rather than broadcast, a 200-line burst
arriving complete, overflow counting, two concurrent dispatches over a
real MCP connection each receiving only their own lines with the token
the server actually received, and the shell tool echoing the token.

This is MCP protocol and async-concurrency code; per HOWTOAI.md it
needs human review before merge.
…very write, and revoke a run whose setup failed

NEEDS HUMAN REVIEW: both changes are grant and revocation state.

D8. Every process sharing a profile (the daemon, a CLI run) loads
connections.json once, and every save wrote its whole in-memory copy back:
a connection another process saved vanished, and a grant another process
revoked came back live. All registry writes now go through one helper,
update_registry(_keeping): take the in-process registry lock, take an
advisory exclusive lock on <root>/connections.lock (std File::try_lock with a
bounded 5 s backoff, never blocking the runtime), re-read connections.json,
apply the edit to that copy, write it atomically only when it changed, and
make it this process's copy. When the file is exactly what this process last
read or wrote (tracked by digest), its own copy is the base, so single-process
behaviour is unchanged. When another process wrote, what the file never holds
for this process is carried over: each connection's live status and last
error, an in-memory binding of a legacy grant, and a stop (a grant expired
here stays expired for the same run, even if its save failed). prepare_device,
save/update, remove, adopt_verified_hello, the grant insert, revoke,
prune_stale_grant and retire_deleted_sessions all use it; edit_saved_scopes is
gone. Edits that always held in memory when the save failed (revoke above all)
still do. A chat delete in a profile that never used Crew still creates
nothing, and a new-connection refusal that needs no saved state is made before
the registry is touched.

D7. After run.create returned a run, a failed protected-context check,
missing credential, credential or registry save, or the first
messages.history left the run live at the workspace, and, once recorded, the
grant active here. Every failure after the run ID is known now abandons it:
a recorded grant is revoked as the person would (expired and saved here, then
run.revoke), falling back to a direct run.revoke if that could not ask the
workspace; an unrecorded one gets run.revoke and its freshly written run
credential deleted (new CredentialVault::delete, keyring/file/vault). The
original error is returned. grant_session shares the path, so a failed grant is
left expired, not active. routes/crew.rs is unchanged.

Tests: registry_lock_tests.rs (two managers on one root: save + revoke both
survive; a revoke is never written back live through remove/prepare/save/
revoke/retire; unsaved stop survives; process state carried; newer grant not
overridden; 24 concurrent writes all land; lock wait and bounded refusal;
no directory for a Crew-less profile; refused/no-op edits write nothing), and
in mod.rs a failed messages.history revokes the run and leaves the grant
expired, a pre-record refusal revokes and leaves no credential, and credential
deletion for file and vault backends. Mutation-checked: disabling the re-read
or the abandon fails 8 of them. scope_binding_tests: the retire test now also
expects the other process's grant in memory, since writes read it back.
…eason (F-1)

A removed member's download through the renderer or the raw route was accepted
(200 starting) and then ended needs_file_selection, "Transfer stopped. Reselect
the original local file or destination to resume", which no reselection could
ever fix. A transfer the workspace itself refused now ends in the terminal
failed state with the workspace's reason as a sentence (crew::refusal_sentence):
for a removed member, "That channel isn't available to you. It may be archived,
or you may not be in it.", the words the CLI already used. A stop the workspace
did not answer (a pause, a dropped connection, a locked vault) is still
needs_file_selection, a download stopped while publishing is still
publication_unconfirmed, and a failed receipt stays failed across a restart.
The text form of `biorouter crew grants list` printed a grant whose expires_at
had passed as Active, printed task grants as "Chat", and ignored the
conversation's title. It now names a task a task (kind), names the chat by its
session_name, and reads the state from expired, expires_at and the daemon's new
revocation word together, in the words the desktop's access list uses: Active
(with when it ends), Expired, Revoked, Ended for a task, "Stopped on this
device; the workspace hasn't confirmed yet" for an unconfirmed revocation, and
"Ended: Crew settings changed" for a run the workspace ended itself.
…ed by itself (F3)

The daemon now asks the workspace again for an unconfirmed revocation with no
Retry pressed, so a task the ledger left waiting on that confirmation would
have kept reading "Stop not confirmed" (or a finished task its "didn't confirm"
note) after the workspace confirmed. Listing a connection's runs now settles
each such task whose run the registry records as confirmed
(CrewManager::remote_revocation_confirmed): a stop reads cancelled, a failure
its own failure without the unconfirmed suffix, and a finished task loses the
note, as a confirmed Retry would have left them. Nothing else is touched.
…ver the transport record (NEW-1)

The workspace menu's header showed the last failure as the daemon wrote it —
a failed Connect's message, else the saved record's last_error, which for an
SSH drop is 'Crew SSH failure [ssh_eof; child_before_cleanup=exit_255]: ...'.
It now reads both through connectErrorText, the saved one by its typed
last_error_code: 'Can't reach lab-server.', 'Can't connect to …', and a
code prefix never reaches the person.
…e the allowlist refuses them (B-1)

G12 (b) passed live only because GPT-5.5 declined, in the model, to send a
method outside crew__request's schema enum, so the daemon's worker allowlist
was never exercised. This integration binary drives it deterministically with
no model: a real Agent::reply loop on a chat granted through the real grant
path (workspace.snapshot, run.create, the run credential), a Private model
capability affiliated with the workspace's institution, and a scripted provider
that sends crew__request for enrollment.approve, team.add_member,
channel.add_member, run.revoke and policy.set. Every call must come back as the
allowlist's refusal ("Operation unavailable to a scoped Crew worker"), and the
fake broker, which logs every frame, must receive none of those methods: after
the grant it sees only the turn's scoped context.manifest reads, all under the
run credential. The broker is this test binary itself, started by a fake ssh,
so the binary is offline and runs in CI's loopback-only integration step.
Checked against a disabled allowlist, which it fails.
…nnect (F3)

The 503 crew_revocation_unconfirmed answer said "reconnect and retry" and the
CLI said "reconnect and run this command again", which was the only way it ever
got confirmed. The daemon now asks the workspace again by itself whenever the
connection is back, so both say that, and the CLI names grants list as where to
see it confirmed. The revoke route test now also pins the grants list's
revocation: unconfirmed and remote_revocation_confirmed: false after the 503,
and null for an untouched grant.
… a transfer of it (F-1)

A transfer the workspace refused because the file itself is gone or out of reach (forbidden: attachment unavailable) now ends with "That file isn't available to you. It may have been removed, or you may not be in its channel." rather than the bare "Attachment unavailable."
…try, not this process's copy (P-1)

The no-op check compared the PATCH body with this process's in-memory connection, so a change another process had saved since (D8) could be mistaken for the setting it replaced and the person's save silently skipped. It now compares under the registry's locks against connections.json as it is now, and writes nothing; the unchanged-save test pins the cross-process case.
…ain or its id reissued (F3)

A chat's grant is stored under its session id, one per id, and the daemon's
own retries found unconfirmed stops only there. Two paths dropped such a stop
for good, with no run.revoke ever sent again: granting the same chat again
(begin_run_with_policy replaced the grant), and pruning a deleted chat's grant
once another chat held its id (prune_stale_grant). The old run stayed live at
the workspace until it lapsed, and grants list stopped showing it.

Both paths now move an unconfirmed stop to the registry's `replaced` list
(Registry::keep_replaced) and arm a retry pass for its connection. The list is:

- asked about by the same retries (unconfirmed_revocations reads it), and a
  confirmation is recorded on it (confirm_replaced), which the task ledger's
  remote_revocation_confirmed also reads;
- listed by session_grants as `replaced_grants`, apart from `grants`, so a
  reader that looks a chat up by id (the desktop's findSessionGrant) never
  takes an earlier grant for the chat's access; the route adds kind and a
  null session_name (the id may name another chat now), and the CLI text
  lists them under their own heading;
- merged across processes like any stop (carry_process_state): what the
  workspace said is never forgotten either way round, and an unconfirmed one
  the file never heard of is carried in while its connection is saved;
- dropped when its connection is removed, and otherwise only once settled:
  a week past its run's own end, or at confirmation when no end is recorded.

Only an unconfirmed stop is kept; a live grant, a confirmed stop and one the
workspace ended itself are replaced or pruned as before. Nothing reads the
list to decide what a chat may do.

Tests: the reviewer's scenario end to end against the scripted broker (revoke
offline, the workspace refuses the reconnect retry, grant again: run.revoke for
the old run is sent again at once, it is listed and saved as replaced and
unconfirmed, and the next reconnect confirms it while the new grant is never
touched); the reissued-id prune; which stops are kept; retention; the merge;
the route's replaced_grants row; the CLI text. The first two fail with
keep_replaced disabled.
…ping its bridge (P-1)

update() asked only validate_connection's refusals before disconnect_locked,
so an edit that save_inner refused later still dropped a working bridge: an
institution ID that does not normalize, and "Choose this private SSH
connection's institution before saving". Every refusal the save makes (those
two, aliases whose institutions differ, a prepared identity on an edit) is now
asked first, by running the save's own edit on a copy of the saved registry
that is then thrown away (refusal_before_saving). The save checks again, so a
change another process saves in between is still judged by the save.

The P-1 test now also edits with both refusals and pins that the bridge, the
status, the mode and the policy epoch are unchanged and no bridge was dialled
again; it fails with the early check removed.
…penAPI gates

07c2a10 left three gates red:

- clippy::too_many_lines: begin_run_with_policy grew to 109 lines. The F3
  recording (insert the new grant, keep an unconfirmed earlier stop, arm its
  retries) moves unchanged into CrewManager::record_grant in revocation.rs,
  beside keep_replaced. Same order: the edit holds in memory when the save
  fails, the retries are armed either way, and the file's error is returned.
- the id-reuse seam audit: keepalive_tests.rs now calls
  forget_minted_session_ids_for_test to reissue a deleted chat's id (F3's
  prune path). It is a #[cfg(test)] module; it gets an allowed row with its
  reason.
- OpenAPI drift: regenerated for the crew_profile_grants replaced_grants
  paragraph (07c2a10) and the crew_profile_revoke 503 sentence (cf02f2d).
  Doc comments only.
…dc27465

The final lanes on 461f789 left D-1 (the G12 (d) wording failure), NEW-1,
revoke F1-F5, F-1, P-1 and the B-1 coverage gap open. A final polish fixed
them in 15 commits ending at dc27465, passed the local gate there, and
re-checked nine items live on a new two-account fixture that was then torn
down and independently verified. This records it where each reader looks:

- the acceptance evidence gains a Final polish section: what was fixed, by
  which commits and tests, the reviewers' non-blocking notes, the gate, the
  live verdicts, the three new findings (NEW-2 P2, NEW-3 and NEW-4 P3) and
  the teardown receipts, and says which lanes were not re-run on dc27465;
- the status ledger updates P02, P09, G02, G08, G09 (now pass for every
  fixture of the campaign), G12 ((d) passes; (b) has a deterministic test,
  not a live model) and G15, and its close-out blockers;
- the handoff's close-out names the final code head, the new gate, and five
  final-polish rows for human review: the automatic revocation retry, the
  policy-changed mapping, the worker-allowlist test, the connection-save
  no-op and the re-send hold;
- plan section 16's final status says the same;
- the CLI guide's revoke section stops telling people to reconnect and run
  the command again, which the daemon now does by itself, and names the
  grant states.

Hosted CI on the pushed head is recorded in the PR, since a commit cannot
record its own.
…n that dropped elsewhere (NEW-2)

Crew armed its read-only offline follow only for a loss it was on screen
for, or after a failed Connect. A bridge that dropped while the person was
in a chat left Crew saying Offline after the daemon had re-dialled (11 min
34 s live), until a Connect replaced the working bridge.

The follow now starts whenever Crew mounts on, or selects, a connection the
daemon already calls disconnected. It still only reads GET /connections and
never connects. It is not started while a loss is being decided, for an
ended membership, or for a connection the person disconnected in this window
(the daemon never re-dials a Disconnect); seeing it connected again forgets
that Disconnect.
…ess, and keep its row (NEW-3)

Only a 200 was recorded for Show past access, so a revoke stopped on this
device (503) and confirmed later by the daemon itself (F3's path) read
plain Revoked, and vanished from the list once the chat was granted again.

Every run this window sees waiting (its own 503, or a grant list that says
revocation: unconfirmed) is now remembered, and the first list read that
says confirmed records it, dated then. The grant list's replaced_grants are
read for this too, so a stop confirmed after its chat was granted again is
dated as well. A run confirmed before this window saw it waiting gets no
invented time, a workspace-ended run is never recorded as a revoke, and a
200's own time is not moved. Display only.
…anged, as the CLI does

Past access labelled a grant the workspace ended itself (revocation:
ended_by_workspace, D-1) "Expired", while the CLI's grants list says
"Ended: Crew settings changed" and the chat says the settings changed.
Nobody revoked it and its time did not run out.

Access rows now read "Ended: Crew settings changed" for such a grant, chat
or task, and the chat-connect note and the Chat access pane say "Crew
settings changed since ... was given access." A grant whose time ran out
still reads Expired, and a finished task still reads Ended.
…or leaves the pane (NEW-4)

The note that the workspace confirmed a revoke this view saw waiting lived
in the pane body's component state. When Crew reconnected, the offline
screen gave way to the channel, the pane body mounted again and forgot it
had waited: live, "Confirmed." was visible for about seven seconds.

useConfirmedAfterWait now keeps what it saw per scope. The Chat access pane
passes its pane intent, which lives across that remount and ends when the
person closes the pane or opens another, so the note survives Crew
reconnecting and goes when the person leaves the pane. The note carries a
Dismiss control in the pane, the Access list and the chat's bar.
While the Crew connection was down, the chat's own access bar offered only
Connect in Crew, so stopping the chat meant going to Crew first. A revoke
needs no connection: the daemon stops the grant on this device at once
(503, "Stopped on this device") and confirms it with the workspace by
itself once the connection is back (F3).

The offline bar now offers Revoke access beside Connect, asks inline as the
connected bar does, and then shows the held chat with the unconfirmed note.
Revoking never navigates to Crew or connects anything.
…s ended (F1, daemon)

Edit in place truncated a chat, and /reply's conversation_so_far replaced its
history, whatever the chat's Crew grant said; the turn that followed was then
refused at dispatch. The renderer holds those paths first, but a window that
had not re-read the grant since the CLI or another window revoked it could
still cut the stored transcript for a turn that could never run.

CrewManager::history_rewrite_refusal answers, locally and without asking the
workspace, the sentence the chat's next turn would be refused with: removed,
ended by the workspace or a settings change (D-1), or past the run's recorded
end. The standing checks it shares with every turn now live in one function,
grant_stands, so the two can never word one grant differently.

The daemon asks it before anything changes: edit_message's in-place arm (after
the reach gate, before the turn lock and the snapshot), /reply's write-back
(before apply_client_writeback), and, for a caller holding the person's proof,
both diverge doors, which refused a Crew chat's copy as a bare 500. Each refusal
is 403 with the sentence as a plain-text body. A caller without the proof is
still refused by the reach gate first and never learns where the grant stands.

Tests: crew_history_rewrite_hold (new integration binary; 3 of its tests
failed before the fix: 200 + truncated, 500, 200 + written back) and two crew
unit tests pinning that the turn and the rewrite say the same sentence.
…daemon history-rewrite hold

NEW-2, NEW-3, NEW-4, two re-check observations and the daemon half of F1
landed in six commits ending at 715c59b. They are recorded as verified by
regression tests and an ALL_GREEN local gate only: no live fixture ran. The
handoff's close-out names the new final code head and adds the daemon's
history-rewrite hold to the human-review list.
landing/docs.html conflicted in five places: #373 updated the default
models and #366 rewrote the same sentences in plain style. The
resolution keeps every model fact from #373 in the wording from #366.
@Broccolito
Broccolito marked this pull request as ready for review September 27, 2026 06:42
@Broccolito
Broccolito merged commit 5f53ac4 into main Sep 27, 2026
24 checks passed
@Broccolito
Broccolito deleted the codex/biorouter-crew branch September 27, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant