Skip to content
All docs

docs / The project

Decisions (ADR-lite)

Every architectural decision, with its reasons and the alternatives.

Status key: Decided (build on it), Default (chosen so work can proceed; owner may change), Open (needs owner input before the milestone named).

ID Decision Status Why Alternatives considered
D-01 TypeScript monorepo, pnpm + Turborepo, Node 22 Decided One language for web, API, adapters, SDK; owner's Mandate is TS Python backend (better ML libs, but splits the codebase)
D-02 Postgres (Neon) + Drizzle + pgvector; pg-boss queue; run state machine persisted in Postgres Decided Minimal infra, self-host friendly, matches Mandate Temporal (durable but heavy to self-host), Inngest (hosted dependency), Redis/BullMQ (extra infra) — revisit Temporal if run complexity explodes
D-03 BYOK-only vendor access; no consumer-UI automation; no token reuse Decided ToS, reliability, trust Browser-scraping consumer apps (rejected)
D-04 Full product, built in dependency order; no throwaway MVP Decided Owner preference; each milestone production-grade MVP-then-iterate (rejected by owner)
D-05 Claude Code commits locally; owner pushes Superseded by D-17 Owner rule —
D-06 API + orchestrator on Fastify in containers; web on Vercel; runners on Docker hosts Decided Long-running streams and sandboxes can't live on serverless All-in Next.js on Vercel (rejected: timeouts)
D-07 Governance: native packages/governance with Mandate-compatible schemas Decided; Mandate connection in D-97 (M9) Fewer moving parts; Mandate connectable later Delegate to a Mandate instance from day one
D-08 Memory graph in Postgres tables + pgvector (graph abstraction in packages/memory) Default One database FalkorDB/Neo4j (Relay uses FalkorDB) — swap behind interface if graph queries get heavy
D-09 Licence: open-core, AGPL-3.0 for core; hosted cloud commercial Default Mirrors Mandate's choice MIT (easier adoption, weaker moat), BSL
D-10 Router = Thompson sampling with cost/latency utility; Elo from pairwise judgments Decided Simple, explainable, learns online Learned ranking model (later, when data exists)
D-11 Cross-vendor review mandatory for non-trivial outputs Decided Core differentiator; errors correlate within a vendor Same-vendor self-review (cheaper, weaker)
D-12 Beachhead user: prosumer builder running 3+ agents; GitHub repos first Default Fastest feedback; owner fits persona Teams first, enterprise first
D-13 Product name is Brio (from "con brio", play with spirit); brand strings centralised in packages/brand Decided (23 Oct 2026) Short, easy to say, orchestra-related, no AI product conflict found Roster (placeholder), Orchestrion, Tutti (crowded), Consort, Fulcrum; owner to clear trademark and domain before public launch
D-14 Global opt-in scoreboard (anonymised cross-workspace priors) Superseded by D-96 (M9) Strong cold-start advantage; privacy implications Workspace-only scores
D-15 Platform keys (Brio pays vendors, resells with margin) in addition to BYOK Superseded by D-105 (M10) Lower friction for non-technical users BYOK only + platform fee
D-16 Relay as optional coordination backend for teams already on it Default: later (M9+) Interop goodwill Ignore
D-17 Claude Code may push to origin as the owner's logged-in account (Ivan825) when the owner authorises it, in small per-step pushes; no bot/AI identity, co-author trailer or AI GitHub Action ever touches the repo Decided (23 Oct 2026) Owner instruction at M0 start Owner pushes everything (D-05)
D-18 TypeScript 6.0.x, not 7.x Default (M0) TS 7 is the native (Go) compiler without the JS compiler API that Next.js, drizzle-kit and other tools still load TS 7 (revisit once the toolchain supports it)
D-19 Tailwind CSS 3.4 LTS Default (M0) brand/tailwind.preset.ts is a v3 JS preset (presets, darkMode); v3 uses it unchanged Tailwind 4 with @config compat layer
D-20 Internal workspace packages export TypeScript source (exports: ./src/index.ts), no per-package build Default (M0) No build ordering or stale dist/ during development; Next.js uses transpilePackages, the API runs through tsx Per-package tsc/tsup builds (add if a package is published to npm, e.g. sdk)
D-21 Ledger hash = sha256(prev_hash ‖ canonical_json({workspaceId, seq, kind, payload})), one chain per workspace, appends serialised by an advisory lock Decided (M0) Refines SECURITY_GOVERNANCE §8 (which hashes the payload only): binding seq, kind and workspace stops entries being renumbered, relabelled or moved between chains Hash the payload only
D-22 Events writer lives in packages/db and must be constructed with a redactor; seq comes from missions.last_event_seq under a row lock; live fan-out via Postgres LISTEN/NOTIFY, reads always from the table Decided (M0) Gapless per-mission seq without extra infra; SSE resume is a plain seq > Last-Event-ID query; no payload can be written unredacted Sequence per mission; Redis pub/sub
D-23 Workspaces are Better Auth organisations (organization/member/invitation mapped onto workspaces/members/invitations, session.activeOrganizationId → active_workspace_id); invite roles limited to admin/member until RBAC in M9 Decided (M0) One source of truth for membership and invitations; Better Auth handles accept/expiry Separate workspace tables synced from auth
D-24 The browser only talks to the web origin: Next.js rewrites /api/auth/* and /v1/* to the API Decided (M0) First-party auth cookies, no CORS, SSE works unchanged; the API stays a separate long-running service (D-06) Cross-origin API with CORS + SameSite=None cookies
D-25 SSE connections are closed after BRIO_SSE_MAX_CONNECTION_MS (default 5 min); clients reconnect with Last-Event-ID Decided (M0) Keeps connections fresh behind proxies/load balancers and continuously exercises the resume path Unbounded connections
D-26 M0 email is a local LogMailer (logged + dev outbox at GET /v1/dev/outbox when BRIO_DEV_OUTBOX=true, refused in production) Default (M0) Acceptance asks for invites logged locally; a real provider arrives with notifications in M9 Wire an email provider now
D-27 Vendor presets are data in registry/connectors.json (presets: base URL, regions, auth scheme, API styles, model-list shape, key check, pricing source, verified date); adapters are three families that read them Decided (M1) Adding or fixing a vendor is a data change with a test; CLAUDE.md rule 8 extended to endpoints One adapter class per vendor
D-28 Model-API families call vendors' native HTTP APIs with fetch, not the vendor SDK packages Decided (M1) One injectable transport for recorded fixtures, cancellation, retries and the URL guard; native features (Anthropic caching and thinking blocks, Gemini thought signatures, Responses reasoning items) are kept by echoing vendor content verbatim. Refines ADAPTER_SPEC §2.2 ("native SDKs") openai, @anthropic-ai/sdk, @google/genai
D-29 (Superseded by D-38) Until the runner and scheduler (M2/M3), a user can run one task with one chosen model-API agent in the API process ("direct run"); no sandbox, so file and shell tools are not offered (fail closed); confidential tasks may only use local models until data-routing policies (M9); the click is the approval for that dispatch Decided (M1) M1 acceptance needs a one-task mission against each preset; this is a real, metered, evented run, not a stub; M3 replaces it with the scheduler Wait for M3 to run anything
D-30 Pricing: global model_prices table seeded from registry/prices.json (vendor docs, dated) and refreshed live from OpenRouter's public list and from vendor model lists that carry prices; precedence agent override > preset row > OpenRouter mirror > free (local) > unknown, and unknown refuses the run. Vendor-reported cost (OpenRouter, xAI, LiteLLM header) is preferred over computed cost. Long-context tiers, cache writes, DeepSeek off-peak and non-USD prices (converted with BRIO_FX_USD_PER, else unknown) are modelled Decided (M1) Budgets need a price before dispatch (rule 6); every cost event records the price it used Hard-coded price table; OpenRouter only
D-31 AgentAdapter.healthCheck and listModels take the ConnectorConfig (base URL, preset) plus a fetch; keys are checked without spending money: model list, a key-info GET, or an empty-body probe (401/403 bad key, 400/422 good key) Decided (M1) Several vendors have no authenticated model list (GLM none; Sarvam and OpenRouter public) 1-token generation as a health check
D-32 Outbound connector HTTP goes through a URL guard: https only, no credentials in URLs, no redirects, cloud-metadata and link-local addresses always blocked, private networks only when BRIO_ALLOW_PRIVATE_URLS (default on outside production) Decided (M1) User-supplied base URLs (local models, proxies) would otherwise be an SSRF hole Allow-list of vendor hosts only (breaks self-hosted models)
D-33 Anthropic is called with anthropic-version: 2023-06-01 and Authorization: Bearer Decided (M1, verified 23 Oct 2026) 2026-10-21 rejects temperature/top_p/top_k on every model, which would break agents' passthrough options; 2023-06-01 remains valid Track the newest version
D-34 Meta Model API is shown as "Preview · region-limited" Decided (M1) The M1 prompt calls it US-only; Meta's geographic use policy (12 Oct 2026) now lists restricted territories instead. The label stays true either way "US only"
D-35 Agent answers are stored as content-addressed blobs (per workspace, blob:<sha256> refs) with a report artifact Decided (M1) Artifacts need bytes before object storage exists; the ref scheme survives a move to S3/R2 Object storage now; answer text in events only
D-36 Coding CLIs run from one sandbox image (apps/runner/images/sandbox-cli) with pinned versions checked on 23 Oct 2026 (Claude Code 2.1.309, Codex 0.164.0, Cursor Agent 2026.10.22, Antigravity 1.4.6, OpenCode 1.19.4, Aider 0.86.2, Goose 1.55.0); images are tagged by a hash of their source and built on first use Decided (M2) Reproducible sandboxes; a changed Dockerfile always rebuilds; deployments can pin pushed images (BRIO_IMAGE_*) latest tags; one image per CLI
D-37 Cursor Agent runs are unmetered: it reports no tokens or cost (billing is the user's Cursor plan), so its runs are bounded by time only, its key is checked by the first run, and the cost shows as $0 "unmetered" Decided (M2) Honest about what can be measured; budgets still bound time Refuse Cursor; guess tokens
D-38 Every attempt goes through the queue: the API resolves a dispatch plan (attempts.dispatch: rendered input, agent and connector config without secrets, price, limits, sandbox spec, repo) and queues it on pg-boss; a runner claims it atomically (queued → running), heartbeats it, and records the result. Supersedes D-29's in-process runs Decided (M2) One execution path for model APIs and CLIs; an auditable record of exactly what ran; duplicate jobs are harmless; crashes are reaped Keep model-API runs in the API process
D-39 Sandbox egress: the sandbox sits on an internal Docker network; a per-attempt egress proxy is its only route out and forwards only to allow-listed hosts (the agent's vendor; registries and git for code), never to private or metadata addresses. Model-API agents' sandboxes have no network at all (model calls leave from the runner) Decided (M2) Default deny (SECURITY_GOVERNANCE §3) with hostnames, which Docker networking cannot filter itself iptables rules on the host; a shared proxy
D-40 The runner never trusts the agent's .git: results are committed from the runner's own repository cache with a private index over the worktree files, hooks and fsmonitor disabled and global config ignored; symlinks are stored as links, never followed; .brio/ is excluded; Brio (not the agent) authors every commit Decided (M2) A hostile agent controls its clone's config and hooks; none of it may run on the host. Also keeps AI attribution out of commits (rule 9) Run git in the agent's clone
D-41 Jules and Devin are scheduled for M6 with the other hosted agents (the registry said M2; BUILD_PLAN puts hosted adapters in M6) Decided (M2) Data and plan disagreed; the plan's grouping is right Build them in M2
D-42 Claude Code runs with --setting-sources "" and --strict-mcp-config (not --bare) Decided (M2, verified 23 Oct 2026) A repository cannot bring its own hooks or MCP servers; --bare would also disable the hooks Brio needs for lease checks in M3 --bare; trust repository settings
D-43 brio-mailbox reaches coding CLIs as an MCP server (streamable HTTP, stateless, official SDK) hosted by the runner; each attempt gets its own bearer token, and the sandbox reaches it only through its egress proxy under the alias brio-mailbox:8080. Model-API agents get the same mailbox in-process, and messages addressed to them arrive between tool-loop turns Decided (M3) No extra network path out of the sandbox; an agent can only speak as itself; one implementation for both kinds of agent A stdio relay inside the image; unix sockets (do not cross Docker Desktop's VM)
D-44 Planning is an attempt: each mission has a planning task (plan version 0) dispatched to a model-API planner; its JSON answer is validated, retried with the errors (twice), linted with automatic fixes, and stored as a plan version. Routing is static until M4 (pin, else a coding CLI for code and a model API for the rest) and adapts to the number of agents: none blocks with a reason, one takes everything, multi-agent modes degrade to single and say so Decided (M3) The planner gets the same metering, events and sandbox rules as any agent; works with 0, 1 or many agents (owner requirement) A planner that calls models directly from the API
D-45 The M3 verifier runs command checks (tests, typecheck, lint, build) in a fresh sandbox on the attempt's branch, evaluates diff_scope, and sends every other check kind to a person until M5. Failed hard checks send the task back to the same agent with the failures (at most 2 revisions), then fail it Decided (M3) Missions can finish end to end now without pretending to run checks that do not exist yet Mark tasks done without checks
D-46 Leases are exclusive per footprint path or glob, granted under a per-workspace advisory lock, renewed while the task works and released when it ends; plan lint serialises parallel code tasks whose footprints may overlap, and a code task with no footprint runs alone Decided (M3) Two agents can never edit the same files at once, by construction Optimistic edits and merge conflicts
D-47 Attempt branches live in the runner's repository cache; a task starts from its direct upstream tasks' branches (merged in the trusted cache, conflict = blocked until merge tasks in M5). One runner pool shares BRIO_RUNNER_DIR until shared storage (M10) Decided (M3); the conflict part superseded by D-60 Downstream tasks see upstream changes without a merge step yet Push attempt branches to a remote
D-48 Handoff packets carry decisions down the whole chain: a packet includes the decisions its sender received, then its own Decided (M3) A settled decision two tasks upstream still binds the receiver (HANDOFF_PACKET.md: decisions are not reopened without asking) Direct-sender decisions only
D-49 The M0 stub planner is gone; a mission with no agent able to plan waits in draft with mission.blocked and a reason, and resumes on its own when one appears Decided (M3) Replaces TODO(milestone-3); fail-closed, and tells the user what to do Keep the stub as a fallback
D-50 Projects point at an https git URL; local repository paths only when BRIO_ALLOW_LOCAL_REPOS (default on outside production) Decided (M3) Self-hosters and local development work on local repos; a hosted Brio never reads its own disk on a user's say-so Remote URLs only
D-51 Thompson sampling gives each agent two forced tries per task type (warm-up) before pure sampling; convergence is tested over 200 seeds per priority with teams whose best agent leads by ≥ 0.15 utility: the best agent is identified in ≥ 90% of runs and is the most-picked over tasks 41–50 in ≥ 85% Decided (M4) Without a warm-up, one early failure (1 of 2) starves a strong agent for dozens of tasks; the cost is bounded (2 tries per agent and type) ε-greedy; UCB; a larger prior
D-52 Multi-agent modes are a pure step function (nextStep) over the task's attempts; the orchestrator applies it. The task runs as a whole; candidates, checkers, judges and mergers are concurrent attempts tagged with a role and purpose in their dispatch, and the runner leaves the task's status to the orchestrator. The chosen result (mode_params.result) is then verified like a single attempt; people are asked once, for the chosen result only Decided (M4) Restart-safe (state lives in attempts), one verifier path for every mode, no approval spam per candidate A sub-task per candidate; a mode state machine in the database
D-53 Judging: both orders, a disagreement or unreadable answer is a tie, and the cheaper passing result wins a tie; the judge (and best-of-n's tie-breaker) comes from outside the candidates, another vendor where possible. Elo moves once per judged pair. Reviews that cannot be parsed count as no review (the result stands on its checks) Decided (M4) Cancels position bias (ROUTING §4.3); tests outrank opinion; a broken reviewer cannot block a mission Trust a single ordering; re-ask until parseable
D-54 Scoreboard keys are <preset or CLI adapter>/<model>; every made attempt counts once (attempts.scored): the accepted one passes, a result a checker or a person sent back counts as revise (half), failed ones fail; in modes the losing candidates pass or fail on their own checks and lose on Elo. Merges, reviews and judgements are not scored. Ratings: one per person per attempt (+0.5 α or +1 β) Decided (M4) Pass rate measures "did the work pass", Elo measures "was it the better work"; nothing double-counts after a crash Score only the winner; score reviewers
D-55 A plan's suggestedMode: "single" is the planner's default, not a choice: the default mode rules apply. Any other mode from the planner, and any mode a person sets with the plan editor's mode edit (n, pipeline steps), is explicit and kept. A pin always runs single. Revisions return to the agent that made the result and build on its branch Decided (M4) The planner prompt's example says single for everything; treating that as a decision would switch off the modes Planner always decides; ignore the planner
D-56 The verifier decides what it can at once (diff scope, citations, media headers, JSON Schema conformance), hands commands, web audits, link checks and source fetching to the runner, and has agents decide rubric reviews, fact checks and contract reviews. Reviewer attempts join the task while it is verified and never change its status; their verdicts are review rows and check results Decided (M5) Every check kind runs, each where it is safe and cheapest; one checklist per result All checks as agent calls; all checks in the runner
D-57 A reviewer is never from the maker's vendor; when the workspace has none, the check goes to a person (property test). Confidential work is reviewed only by local models Decided (M5) ROUTING §4.2; errors correlate within a vendor Allow same-vendor review with a warning
D-58 a11y and lighthouse run in a web sandbox image (Chromium, Lighthouse 13.5.0, axe-core 4.14.0, puppeteer-core 25.13.0, verified 23 Oct 2026) against the attempt's files served from inside the container with no network; a check with a start command gets registries and git Decided (M5) Real audits, reproducible, no egress for static pages Hosted audit services; axe CLI with chromedriver
D-59 Verdicts: revise twice (same agent, with the failures, building on its branch), reroute once to the next-best agent excluding those that failed, then fail; a task that fails for good leads to one revised plan from the planner once the rest settles, and approving it carries over every done task kept unchanged (same key, type and contract) Decided (M5) ROUTING §4.2 "reroute … then failed → replan proposal"; finished work is never redone Fail the mission; replan from scratch
D-60 Merge engine: when every task is done, finished code branches merge in plan order (git merge-tree in the runner's cache) onto an integration branch and every hard command check runs on it; a conflict becomes a merge.integrate task working on the conflict markers, a failing check a fix task, for up to three rounds. A task whose upstream branches conflict waits for a merge task. One branch is its own integration Decided (M5) Verification after merge (BUILD_PLAN M5); supersedes D-47's "blocked until merge tasks in M5" Merge only at push time; ask a person
D-61 Missions take a routing strategy (learned, static, or one agent alone with no revisions, reroutes or replans) and may come with a plan made elsewhere. The eval harness runs the 30 golden missions under each agent alone, static and learned, in separate workspaces, scores results with hidden checks in a sandbox, and reports the PRD verdict. Offline runs use simulated agents of known skill per kind of work (no keys, reproducible) and run nightly in CI; live runs use the owner's agents and keys and never run in Actions Decided (M5) PRD §7 targets measurable on demand without spending money in CI; D-17 (no AI-driven Actions) Live evals in CI with stored keys
D-62 Service connectors (hosted agents, media, research, search, fetch, browsers) are registry data like the model presets: base URL with {setting} placeholders, auth scheme (bearer, header, AWS SigV4, Entra), install settings, a free key check, the billing unit and any fixed rate, the task types each takes. Behaviour lives in one adapter per vendor on a shared job pattern (start, poll, answer, cancel, collect) Decided (M6) Vendor facts drift; data is checked and edited in one place (rule 8). One job pattern gives every vendor the same limits, cost events, approvals and file handling One hand-written client per vendor
D-63 Mode candidates must be plausible: an agent joins best-of-n or an ensemble only if its sampled utility plus one standard deviation reaches the leader's minus a margin per priority (quality .25, balanced .15, cheap and fast .10); explicit modes are exempt. Judge ties go to the result whose agent is likelier right (scoreboard pass rate), cost first only for cheap missions Decided (M5, from the offline eval) The first eval showed weak candidates added to every mode and ties going to the cheaper, weaker result Always add the runner-up; cheaper wins ties
D-64 A hosted agent's question or approval goes to the mission's person through the mailbox and waits 10 minutes; no answer is no. A declined question asked again ends the run. Hosted results come back as a patch applied to the task's own worktree, so checks, merges and integration treat them like any other agent's Decided (M6) SECURITY_GOVERNANCE: irreversible actions need approval, fail closed; one verification path for every agent Auto-approve; trust the vendor's pull request
D-65 Media results are downloaded the moment they exist (vendor URLs expire), placed at the contract's output path in the worktree, and metered in the vendor's unit: as reported where the vendor reports cost (BFL, Runway, Recraft credits; Kling and Ideogram dollars; ElevenLabs characters), else at the registry's documented rate, which an agent can override. Each media service names the task types it takes (an image API is never routed a video) Decided (M6) The media_spec check reads the file itself; budgets hold for media like for tokens Keep vendor URLs; meter media at zero
D-66 Search and fetch services are tools, not agents: the workspace's oldest healthy connector of each kind backs every agent's web_search and fetch_url (in-process for model APIs, over the brio-mailbox MCP server for coding CLIs), each call metered on the attempt and held to its limit. No agent can be built on them, and confidential tasks get neither Decided (M6) ADAPTER_SPEC §2.9: one search surface for every agent makes results comparable; a confidential task's text never reaches a search engine A search "agent"; per-agent search keys
D-67 The eval harness can rate each result as its user would (--feedback: thumbs up or down by the hidden checks). Offline, with it, learned routing reached 70% (static 80%, best single agent 73%); without it 53–57% over three passes. The verifier only sees the contract's checks, so ratings are how the router learns what those miss; PRD targets are judged on live runs Decided (M5/M6) Honest measurement: verifier-only learning cannot separate agents whose failures the checks miss Score evals on visible checks only
D-68 Every task that works on the repository's files starts from its upstream tasks' branches (not only code and design), and integration merges every finished task that changed files: docs, copy and generated media as well as code Decided (M6) A voiceover reads the script a writer produced; a landing page's merged result needs its hero image. Refines D-47 and D-60 ("code branches") Pass files between tasks by handoff text
D-69 Browser agents run in a browser sandbox (virtual display, Chromium, xdotool, ImageMagick; worktree read-only) whose egress reaches only the sites the task names: the hosts of its url inputs and of the links in its objective and constraints, with their subdomains; never addresses or localhost. Claude computer use (computer_toolset_20260801) plans actions; Brio runs them there, answers every tool_use in order, prunes old screenshots and asks a person (ask_person) before anything irreversible; no answer is no Decided (M6) SECURITY_GOVERNANCE §3 (per-task URL allowlist), rule 5; the model never touches the network itself Let the agent browse freely; a hosted browser only
D-70 Cloud browsers (Browserbase, Steel) are infrastructure, not agents: a computer-use agent with options.browser = "cloud" runs on the workspace's session over CDP instead of the local sandbox, the task's allowlist enforced inside the browser (Fetch interception), the session released and metered by the second when the run ends. Session URLs that carry a key stay in memory Decided (M6) Sites that need a residential or long-lived browser, without giving up the allowlist Trust the cloud browser to browse freely
D-71 Browser Use Cloud runs in Browser Use's own browser, which Brio cannot restrict; it is capped at the attempt's remaining budget (maxCostUsd) and told to stay on the task's sites, and its reported cost is metered Decided (M6) Honest about what Brio can and cannot enforce on a vendor's browser Refuse Browser Use
D-72 Vendor webhooks are hints, never authority: a Manus webhook is registered only when Brio has a public https address, verified against Manus's signing key over the exact body, URL and a five-minute timestamp, and only adds a note to the attempt's board; runs still follow jobs by polling Decided (M6) A forged or lost webhook can neither finish nor fail a run Finish runs on webhooks
D-73 Judges, checkers and mergers come only from agents whose task types include that work (judge.pairwise, review.rubric, the task's own type for a merger) Decided (M6, a bug the landing-page acceptance test found) A voice model was eligible to judge code Any agent of another vendor
D-74 ACP drives coding CLIs that speak it through the official SDK, opt-in per agent (options.protocol = "acp"), Cursor first (cursor-agent acp, its key from the environment and the cursor_login method; never a browser sign-in). Every permission request is answered (the sandbox is the boundary). Headless stream-JSON stays the default until each CLI's ACP mode is verified live Decided (M7) ACP gives structured tool calls, plans and permissions, but its live behaviour per CLI is unverified (docs/vendors/protocols.md §3) ACP for every CLI at once
D-75 Brio's MCP server is stateless streamable HTTP at /mcp on the MCP SDK 1.x (protocol 2025-11-25, which current clients speak), with tools that act as the caller in one workspace through the same rules as the web app: delegate (auto-approves a small clean plan and waits up to an hour), plan_mission, approve_plan, mission_status, report, ask_agent, answer, get_brief, get_contract, request_review, list_missions. brio mcp serves the same tools over stdio by proxying to /mcp with an API token from the environment Decided (M7) One tool surface, one set of rules; stdio servers take credentials from the environment (MCP spec) Separate stdio tools; the 2.x SDK before clients move
D-76 Remote MCP clients sign in with OAuth 2.1 served by Better Auth (@better-auth/mcp, CIMD, and dynamic registration because Cursor documents only that): PKCE S256, JWT access tokens bound to /mcp (RFC 8707), consent in the web app, sign-in that resumes the authorization. /mcp verifies the JWT against Brio's own keys, issuer and audience, and acts in the workspace the client names or the person's last active one, held to it like an API token Decided (M7) MCP authorization spec; no token passthrough; one identity system A separate authorization server; API tokens only
D-77 Workspace API tokens (brio_pat_ + 40 base62, sha256 stored, shown once, optional expiry) for the CLI, stdio MCP and A2A clients. A token acts as its person in its own workspace only — enforced in requireMember whichever way a route found the workspace — and cannot create, delete or switch workspaces, invite people or mint tokens Decided (M7) Machine access without cookies, with a small blast radius Personal tokens across workspaces
D-78 Brio as an A2A v1.0 agent: Agent Card at /.well-known/agent-card.json, JSON-RPC with SSE at /a2a on the official SDK's request handler. Each message becomes a mission (auto-approved when small); the A2A task follows it (working, input-required for a question or a plan to approve, completed with the report and produced files as artifacts) and is stored in Postgres and refreshed from the mission when read, so it survives restarts. Follow-up messages answer the open question or approve the plan. API tokens only Decided (M7) PRD F40; durable tasks without a second state machine In-memory task store
D-79 Any A2A agent is a worker by its URL (registry service a2a, optional bearer): its card is read at install, its skills suggest the task types it takes (editable on the connector), each agent is its own vendor (by host) so A2A agents can review each other, runs are unmetered (bounded by time), and its questions reach the mission's person Decided (M7) PRD F10; honest about A2A carrying no cost One "a2a" vendor for all
D-80 MCP servers are tools, not agents (registry service mcp): checked by listing their tools, attached to agents (agents.tools), named in the dispatch without secrets; the runner adds their credentials from the vault in memory. Model-API agents call them in their tool loop as <server>__<tool>; coding CLIs get them in their MCP configuration and their sandbox may reach the server's host Decided (M7) ADAPTER_SPEC §2.6; secrets never stored with a dispatch MCP servers as agents
D-81 Handing work back is irreversible, so it waits on an approval: when a mission on a repository ends with its work integrated, Brio proposes a delivery (the integration commit pushed as brio/<title>-<id>, and a pull request when the repository is on GitHub) in the transaction that ends the mission; a person approves or denies it (never an API token); approved, a runner pushes to the repository URL itself (never the cache's mirror remote) and opens the pull request with the project's token, as its owner. Both side effects go to the ledger; a denied or failed delivery can be asked for again Decided (M8) CLAUDE.md rule 6; PRD's "approve PR action"; an agent holding a token must not approve its own push Push automatically on success; open PRs as a Brio bot (D-17)
D-82 A git token per project, sealed in the vault: the runner reveals it only to fetch a private repository into its cache and to push an approved delivery, through an http.extraHeader in the environment (never argv, the cache's config or a remote URL); dispatches name it, never hold it; agents' worktrees are local clones and never see it. A failed fetch is an error, never a reason to re-clone (the cache holds the mission's branches) Decided (M8) Private repositories without credentials on disk; a lost cache would lose work Tokens in clone URLs; a credential helper on the runner
D-83 brio login is the OAuth device flow (RFC 8628, Better Auth device authorization, client brio-cli): the person approves the terminal's code at /device and the CLI holds a session of its own (sent as a bearer token, 0600 config file), so it acts as that person — it may approve plans and pull requests, which API tokens may not. brio logout ends the session on the server Decided (M8) The gh-style login people expect; approvals stay with people Paste an API token; a localhost redirect
D-84 Missions start from more than a goal (PRD F1): files (≤ 10, ≤ 2 MB together) stored as blobs, and a GitHub issue or pull request read once at creation (with the project's token when the project is that repository); every attempt gets them as attachments (text inline, binaries by name) and as .brio/inputs/ files in its worktree. Other issue hosts are refused with a clear message Decided (M8) One input path for every agent; issue text is untrusted content like handoffs Re-read the issue per attempt; support every tracker at once
D-85 Crews: a saved set of agents with an agent per task type; a mission routed to a crew uses only its agents (reviews too) and the crew's pin unless the plan pins another; a deleted crew leaves the mission with no agents rather than quietly using everyone. Templates: a plan saved as a pipeline; a mission starts from it adapted (the planner keeps its shape for the new goal) or as is (its plan, minus pins to deleted agents) Decided (M8) PRD glossary (crew), F5 (templates) Crews as tags; templates as plain copies
D-86 The local runner (brio runner) opens one WebSocket to the API and keeps no database: the API's hub hands it its own work (queued attempts of agents bound to it, their command checks, merges and deliveries of projects that live on it), claims atomically, heartbeats its attempts and records outcomes with the same functions the cloud runner uses (attempt work and its recording are split for this). Agents bound to a runner work only on that runner's projects; file work on a runner's project goes only to its agents; planners and reviewers may stay in the cloud (the runner supplies the repo map). Web audits stay in cloud sandboxes; the mailbox is not offered to local attempts yet Decided (M8) ARCHITECTURE: hosted API + optional local runner, outbound only; one recording path for every runner Give the runner database credentials; run everything locally
D-87 A local runner runs in Docker by default; host mode (no container: a fresh worktree as /work, a cut-down environment, process-group kill, the wall clock as the only limit) only with its owner's consent, recorded with its words and time. API keys never reach a local runner: its agents are coding CLIs under the person's own sign-in, offered only where the vendor's terms allow it (registry localLogin: Antigravity "allowed"; Claude Code "own-use", which the person confirms; others not until verified), never metered here, the CLI's own settings and stored sign-in untouched and never read Decided (M8) Rule 1 and SECURITY_GOVERNANCE §1 (subscription use only in the local runner, per the vendor's terms); rule 5 (sandbox by default) Send vault keys to the person's machine; host mode by default
D-88 One rule tells a quick start from a plan waiting for a person (auto-approve asked for, no lint errors, no questions, ≤ $5 and ≤ 10 tasks); the MCP and A2A servers use it so a mission passing through awaiting_approval is not reported as waiting. Zod applies defaults inside partial(), so update bodies are explicit schemas without defaults Decided (M8, two bugs found by tests) A delegate returned early under load; an agent rename reset its prompt, limits, tools and price Poll twice; partial() of the create schema
D-89 Budgets (M9): monthly limits per workspace and per agent, plus each mission's and task's own; spend is the live attempts.cost_usd, written before each attempt.cost event. Before dispatch every scope that applies is checked and the attempt's maxCostUsd is capped to what is left; while it runs a guard on the event feed emits budget.warn (always first) and budget.stop, and cancels the attempt with a budget: reason (it ends LimitExceeded). A budget-stopped task waits, blocked, until a person raises the budget Decided (M9) Pre-dispatch caps stop most overspend before it happens; the live guard catches the rest within seconds without polling vendors Vendor-side spend caps only (not every vendor has them; no per-mission view)
D-90 The policy engine (packages/core/src/policy) is pure: data routing, connector allow/deny (deny wins), network and approval policies. Confidential work goes only to local models unless a data-routing policy allows a connector. It filters candidates where agents are chosen (planner, router, reviewers) and is checked again at dispatch (policy_denied); network policies shape the sandbox's allowlist Decided (M9) One rule set, enforced at choice and at dispatch, so no path routes around it; property-tested Checks only at dispatch (tasks would be routed and then fail)
D-91 Notifications go by email (Resend; the dev outbox without a key), web push (VAPID, sent through the guarded fetch) and the workspace's own Slack app (bot token and signing secret in the vault; Approve/Deny buttons; interactions verified by HMAC and mapped to a member by their Slack email). Each event notifies each person once per channel (a claimed row), by their preferences Decided (M9) People approve where they already are; BYO Slack app keeps Brio from holding a global Slack install A Brio-wide Slack app (one install for every customer, larger blast radius)
D-92 MCP clients decide approvals only with the brio:approve scope, asked for explicitly and granted on the consent page; it is never a default and API tokens never carry it Decided (M9) Approving money or a push is a step up from running missions; a person must see that on consent Any signed-in MCP client may approve
D-93 The ledger is readable, verifiable and exportable (JSONL, CSV) by owners and admins; verification recomputes the hash chain and names the first broken entry Decided (M9) Audit is only useful if someone can check it without database access Database access for auditors
D-94 Project memory is a graph in Postgres (packages/memory) built from events: missions, tasks, contracts, prompts, decisions (assumptions, answers), files. A brief for paths or a topic is handed to every attempt on the project (input.brief, .brio/brief.md); people add, correct and forget items, each in the ledger Decided (M9) Agents start knowing why the code is as it is; one database (D-08) Embeddings-only retrieval (loses the why behind a change)
D-95 Workspace roles: owners and admins administer (connectors, budgets, policies, ledger, people, sharing); members act (missions, agents, crews, projects, approvals); viewers only read. Only owners make owners and admins; a workspace always keeps an owner; every change is in the ledger Decided (M9) Matches how teams split spending power from day-to-day work Owner/member only
D-96 The global scoreboard is opt-in both ways (D-14): a workspace may add its outcomes (agent key, task type, verdict, cost, time; its id only as a salted hash, to count contributors) and, separately, start unseen agents from global numbers. Global rows are used or shown only with ≥ 20 outcomes from ≥ 3 workspaces; a workspace's own scores always come first Decided (M9) Cold-start advantage without exposing any one workspace's results Workspace-only scores; or sharing on by default
D-97 Governance stays native (D-07), and a workspace may also connect a Mandate: one USD mandate's agent token (mnd_…, checked with GET /api/agent/mandate, kept in the vault). Then every paid attempt asks POST /api/agent/authorize for its cap (merchant = the vendor that bills it, an idempotency key per request) after Brio's own budgets pass; the attempt's limit becomes what was held; the real cost is captured (or the hold voided) when it ends. Pending requests keep the task blocked and are asked again, identically, until a person decides in Mandate; declined or unreachable blocks too (fail closed). Creating, pausing and revoking mandates stay in Mandate's UI, which has no admin API Decided (M9) Owners who already govern agents' spending in Mandate keep one place for it, without Brio holding admin power over it Delegate all budgets to Mandate (loses per-mission and per-agent scopes); Mandate's API-key proxy (would route vendor keys through a second service)
D-98 Ledger entries are written in the transaction that does what they record, with seq and hashes empty, and chained afterwards in the order written — by the API worker within a fraction of a second, and by every reader before it reads. A trigger lets a pending entry be chained exactly once and nothing else ever change; verification is unchanged Decided (M10) Holding each workspace's chain lock until every dispatch and settlement committed serialised the workspace: under load, events waited seconds (docs/LOAD_TEST.md) Chain inside each transaction (correct, but one lock per workspace for all work); a separate pending table (rows would vanish from readers until chained)
D-99 The API runs in one of three roles (BRIO_API_ROLE): all (one process, the default and what Compose runs), or web (REST, live events, MCP, A2A, runners' sockets; as many as needed) and worker (orchestrator, budget guard, notifications, memory, Mandate, ledger chainer; exactly one). A web process records what people do; the worker reacts to the events those writes emit, with its sweep as the safety net Decided (M10) One process serving 500 attempts' worth of streams and requests while running every mission saturated its thread (p95 677 ms) Shard missions across several workers now (not needed at this scale; noted in LOAD_TEST.md)
D-100 A runner takes jobs with one claim loop per queue that asks for as many jobs as it has free slots Decided (M10) A pg-boss poller per slot made eight idle runners cost Postgres ~2,100 transactions a second; now ~70 Fewer slots per runner (caps throughput instead of fixing the cost)
D-101 Work reaches a person's local runner only for a mission they created, following a plan version they approved, and (a direct run) started by them; integrations, checks and deliveries there follow from such attempts Decided (M10, security review H2) Agents there run commands as that person, often in host mode; no one else's goal, edits or check commands may reach the machine Per-mission approval by the owner in the inbox (more prompts for the common case, the owner's own work)
D-102 Outbound requests to user-supplied addresses go only to addresses checked inside the connection's own DNS lookup (an undici agent), and IPv6 forms are judged by the IPv4 address they carry; git follows no redirects Decided (M10, security review M2) Checking a name and then letting fetch resolve it again allowed DNS rebinding; mapped/NAT64/6to4 forms slipped past string checks Resolve once and connect by IP with a Host header (breaks TLS SNI for https)
D-103 Prices from a vendor's model list are trusted (shared by every workspace) only from the vendor's own address in the registry; a list from an address someone typed in names models for that connector and sets no prices Decided (M10, security review H1) The price table is global; one workspace must not be able to set what everyone's work costs Per-workspace price tables (more storage and lookups for a case with no legitimate need)
D-104 Every result is scanned for instructions aimed at the agents that read it next; a finding holds the result for a person (a hard injection_scan check). Untrusted content enters prompts only through fence, whose fence is longer than any backtick run inside it Decided (M10) Reviewers, judges and the next task's agent read results as data; text that breaks out of its quote or addresses them should reach a person first Refuse such results outright (false positives on legitimate security work would fail missions)
D-105 Brio stays bring-your-own-keys at launch (D-15): no platform keys, no reselling of vendor usage. Usage is measured per workspace (GET /v1/usage: spend, attempts, outcomes and tokens by day, agent, vendor and task type) and, for an operator who hosts Brio for others, a billing hook sends each workspace's finished day once, signed with HMAC-SHA256, retried hourly on failure. A public status page reports each part's health with nothing about any workspace Decided (M10) Vendors bill people directly for what their agents use; an operator charges for Brio itself from the hook, with whatever plans it likes; no money flows through Brio Platform keys with a margin (lower friction for non-technical users; adds payments, fraud and reselling terms to Brio)
D-106 Email can go through any SMTP server as well as Resend: BRIO_SMTP_HOST/PORT/TLS/USER/PASSWORD, with the sender defaulting to the SMTP user. Resend wins when both are set. TLS is implicit on 465 and required STARTTLS otherwise; a password is never sent without TLS in production Decided (after M10) Resend only sends to other people from a domain you own; an owner with no domain can still send sign-in links and invitations from a mailbox they already have (Gmail or Workspace with an app password, Outlook, Zoho) Resend only (needs a bought domain); a hosted relay of ours (Brio would see every email and need an account with us)
D-107 BRIO_SIGNUP_ALLOW limits who may create an account: listed addresses and @domains, and anyone with a pending invitation. Existing accounts always sign in; every way an account is made (magic link, Google, passkey) is checked, and nobody else is sent a sign-in link. Unset, sign-up stays open Decided (after M10) An owner without a domain publishes a laptop through a tunnel; strangers who find the address should not get workspaces, sandboxes on that machine, or emails from the owner's own mailbox Closed by default (breaks the open self-host and hosted flows); an admin approval queue (more UI for a case a list covers)
D-108 The product website is its own app (apps/site): a static export in the Brio brand, published with the Vercel CLI from the owner's machine to a free *.vercel.app address. Its screenshots are captured from the real app, and tests tie its claims to the repository (connector names and count, load-test numbers, CLI commands, doc links) Decided (after M10) A product needs a front door; the owner has no domain; connecting the repository to Vercel would install a GitHub App on it, which rule 9 forbids A page inside the web app (couples marketing to app deploys, and needs the API running); GitHub Pages (needs an Actions bot to deploy)
D-109 Chat is a mission of kind chat that stays open. Each message the person sends is one task in its standing plan version, carrying the conversation so far as quoted, untrusted context, and answered by the router's pick, one picked agent, or several agents in a mode (together = ensemble, review = maker-checker, debate). One message at a time. A chat is never planned, done or failed: the scheduler sees only the turns still at work, a failed turn is revised or rerouted like any task and then stays failed, and a stopped turn stays stopped. A chat can be handed to a full mission with the conversation as context Decided (after M10) People want to talk to their agents the way they talk to one assistant, and have several agents build an answer together; reusing missions gives every reply routing, modes, budgets, policies, verification, the ledger and the scoreboard for free A separate chat engine calling adapters directly (a second path for money, policy and secrets to escape); token streaming (adapters answer whole turns; per-token events would flood the append-only log)
D-110 Ready-made images: after CI passes on main, a plain GitHub Actions job (no AI; contents: read, packages: write; it never writes to the repository) builds the api, runner and web images natively for amd64 and arm64 and publishes them to GitHub Packages as ghcr.io/<owner>/brio-*, tagged sha-<12 chars> and latest, only once all six builds succeed. ./setup.sh --prebuilt points a server at them; the updater moves a server to a new commit only when that commit's images exist, and pins BRIO_VERSION to them, so code and images always match Decided (after M10; the owner allowed this Actions job explicitly, a narrow exception to D-17's no-bot rule since it only publishes packages) Building needs ~3 GB of memory, running ~560 MB: with images built elsewhere, a free 1 GB server (Azure for Students) runs Brio, and updates take seconds instead of a rebuild Build on the server (needs a paid 4 GB server); build on the owner's laptop and push (uses the laptop the owner wants to keep free, and updates wait for it)
D-111 The CLI is published to npm as brio-orchestrator (command brio): apps/cli/scripts/pack.mjs bundles it with esbuild into one minified ES module with no dependencies, plus the sandbox image sources the local runner builds from, the README and the licence; the owner publishes it from their own machine with npm login and pnpm --filter @brio/cli publish:npm. Its version is apps/cli's, and brio --version prints it. A test builds the package and runs it outside the workspace Decided (after M10) People expect npx/npm install -g for a CLI; the name brio is taken on npm, and brio-orchestrator matches the website; one file with no dependencies installs in a second and cannot break on a dependency's update Publish the workspace packages (@brio/runner, @brio/db, …) as dependencies (dozens of packages to version and publish together); a scoped @brio/cli (needs the brio npm organisation); publish from Actions (a bot identity, D-17)
When you change or add a decision, add a row, keep the old one with status Superseded by D-xx, and note the commit.