Skip to content

Architecture Decision Records ​

ADRs capture significant architectural decisions — the context, the options considered, the decision, and its consequences — so the why survives the people who made it.

Format: lightweight MADR-style. One file per decision, numbered, never deleted (supersede instead).

#TitleStatus
0001Extensibility & Plugin ArchitectureAccepted
0002Reusable Subagent WorkflowsAccepted
0003Reactive Agent: Activity Thread, Event Bus & Inbound InboxAccepted
0004Multi-Instance Data ScopingAccepted
0005Tool Pollution & Progressive Tool DisclosureAccepted
0006Observability & the Self-Improving FlywheelAccepted
0007Directory-Aware Operator Primitives (enabling a "Roxy" fork)Accepted
0008Sandboxing posture & NVIDIA OpenShellAccepted
0009The Studio control stack (goals · workflows · subagents · skills)Accepted
0010Headless setup & UI deployment tiers (lighter stack)Accepted
0011Deep-research workflow with adversarial reviewAccepted
0012Eval strategy: model-tagged tracking & model comparisonAccepted
0013Console data layer: TanStack Query + Suspense + ErrorBoundaryAccepted
0014A2A 0.3 → 1.0: adopt a2a-sdk + protolabs-a2aAccepted (shipped #453)
0015Optional native Discord surface (ingress + outbound)Accepted (shipped as plugins/discord)
0016In-app Discord configuration (token, admin list, live connect)Accepted
0017In-app Google (Gmail + Calendar) connect flowAccepted
0018Plugins contribute surfaces, routes & subagentsAccepted
0019Plugins contribute config, settings & secretsAccepted
0020Console IA: run from Chat, manage from surfacesAccepted
0021Agent memory: extract, don't dumpAccepted
0022Activity is a provenance feed, not a second chatAccepted
0023Decompose server.py: AppState + composition rootAccepted
0024Spawn CLI coding agents over ACP (code_with)Superseded by 0025 (code_with removed; ACP lives on as an acp delegate)
0025Unified delegate registry + hot-swappable panel (delegate_to)Accepted (complete; PR1–PR4)
0026Plugin-contributed console surfaces (rail views + tabs)Accepted (complete; PR1–PR3)
0027Install plugins from a git URL (shareable plugin repos)Accepted (sliced)
0028Plugin-contributed goal verifiers (+ safe programmatic goals)Accepted (sliced)
0029A standard for communication (chat-surface) pluginsAccepted
0030Monitor goals — cadence-evaluated, hook-reactive objectives (closes 0028 D6)Accepted (sliced)
0031Pluggable knowledge backend (register_knowledge_store + Protocol + selector)Accepted
0032Pluggable middleware (register_middleware + per-request metadata contextvar)Accepted
0033Pluggable agent runtime (ACP executor) — runtime≠model axis, operator-tools MCP bus, context contract + cachingAccepted
0034Plugin UI as first-class React (Module Federation) — ui: react remotes + shared singletons + plugin-ui SDK + trust gate; Notes is the reference portSuperseded by 0038
0035Console layout — symmetric dual-rail (swappable surfaces), one tab style, mobile-first (bottom quick-bar + hamburger), persisted UI state (Zustand)Accepted
0036Unified context-menu system (right-click) — Zustand store + ContextType registry + one renderer; plugins contribute items via the plugin-ui SDK (trust-gated); replaces ad-hoc affordancesAccepted
0037Design-system foundation — Tailwind + @protolabsai/design preset + shadcn/Radix (themed by brand tokens); supersedes 0035 S5, flips 0036 D5; shared to plugin remotesAccepted
0038Generative-UI artifacts (sandboxed iframe, à la Claude/Open WebUI) + two-mode plugin UI (iframe-sandbox for untrusted, build-time src/ext registry for forks); retires Module FederationAccepted
0039Plugin event bus — decoupled topic pub/sub (extends ADR 0003) with in-process + iframe subscribers and a no-cross-plugin-dependency clause (namespaced publish, gated); notification dots as the first consumerAccepted
0040Plugin bundles — install a curated set of plugins as one (plugin install <url> of a bundle manifest; builds on ADR 0027 git-installable plugins)Accepted
0041Workspaces & tiered stores — the fleet-on-one-host model (per-agent workspace dirs + config/data scoping)Accepted (v0.31.0)
0042Fleet supervisor & unified console — background agents, slug-routed in-place switching (/agents/<slug>/*), per-agent layout/theme/chatAccepted (v0.31.0)
0043Plugin consumption SDK (graph/sdk.py — run_subagent/subagent_types/config) vs the register_* contribution API; proven by extracting workflows to an opt-in plugin (enabled:false, engine taps core via the SDK) with the Studio surface via src/ext + requiresPluginAccepted
0044Plugin-driven console navigation — plugins contribute nav entries/surfaces into the console railsProposed
0045The chat panel is a slot — fork (src/ext id chat) or plugin (views: slot: "chat") replaces the built-in surface, inheriting the always-mounted contract (#613); the canonical chat-panel protocol is A2A 1.0 + the protolabs DataParts (no TS SDK, no AI-SDK core); records the spike's deliberate deferralsAccepted
0046Pluggable utility bar — an AppShell.utilityBar slot + declarative plugin widgets (indicator / ticker / action) rendered natively (not iframes), fed by the event bus (ADR 0039); safe-for-untrusted, overflow to a MenuProposed
0047Layered settings cascade — per-field App→Host→Agent override (Field.scope, git-style), built on the FIELDS single-source; host-config.yaml (scope_leaf'd) holds box-shared gateway/model/routing/telemetry defaults, agents override freely; remotes stay delegate refs (#839)Accepted
0048Settings IA — scope is the primary axis: two homes, Host / App (box-shared, the host = first agent's inherited defaults) + Workspace (the focused agent, folding in its makeup — Identity/SOUL/Tools/MCP/Subagents/Skills/Middleware — plus its agent-scoped knobs). Surfaces ADR 0047's Field.scope; dissolves the category tabs + the bolted-on "Host defaults" cross-cutAccepted
0049Bundle pin lifecycle — a bundle pin means "last verified working": pin release tags not raw SHAs (annotated-tag peel fixed in _ls_remote_sha), record verified_against: core version, and a verify-and-bump CI loop (install pin set into a scratch agent → probe every declared view → auto-PR tag bumps) keeps it true; reference template in examples/bundles/template/Accepted
0050Background subagents & reactive notifications — a delegation marked run_in_background runs detached as a self-POSTed A2A turn (reusing the scheduler's substrate → durable task store, lifecycle, telemetry for free), tracked in a notified-gated registry keyed to the spawning chat session; completion is drained into that session's next turn as a <task-notification> (exactly-once) + a background.completed bus event. The chat stays live instead of blocking on long sub-work. Phase 1 of a 4-phase plan (reactivity/UX/control follow)Accepted
0051A2A realtime streaming & component rendering — an executor progress/lifecycle hook (mirroring set_terminal_hook) taps a turn's frames → a background.progress bus channel + live per-tool job card; real stop_task (the SDK's CancelTask genuinely stops the turn — a memory/docstring correction) + task_output + foreground→auto-background; cancel-path telemetry. Plus a typed component-v1 DataPart for richer component rendering over the A2A envelope, and alignment polish. Slice 1 builds on ADR 0050Accepted
0052User-facing skills (slash commands) — an opt-in user_facing flag (+ optional slash token) makes a skill invokable as /<slash> [args] in chat; dispatch rewrites the turn to inject the skill's prompt_template as a directive and falls through to the normal lead-agent turn (no worker, no short-circuit → every streaming/HITL/goal invariant holds, zero ChatSurface change). FTS schema v3→v4 (user_facing/slash), user_facing_skills() reader (layered: commons ∪ private), advertised via /api/chat/commands. Precedence: goal > workflow > subagent > skill. Mirrors ADR 0020/0002Accepted
0053wait: yield-and-resume instead of busy-polling — a core wait(seconds, then) tool schedules a one-shot scheduler resume (ADR 0003) and a WaitYieldMiddleware (before_model + can_jump_to=["end"]) ends the turn once wait has run, so the agent stops polling a status tool in a loop (which was burning the whole 200-step recursion budget). Resumes in the durable Activity thread with the agent's then instruction → long-horizon "do X, wait, do Y" without spinning. Lead-only (scheduler-gated); no-op on turns that didn't call wait. Follow-up: same-session resumeAccepted
0054dream & distill — scheduled self-curation subagents (/dream /distill, schedulable via the existing scheduler). dream consolidates durable facts into memory and prunes stale/duplicate/superseded ones (forget_memory by id; memory_list now leads with #id). distill mines recent activity for repeated workflows → hybrid output: auto-create high-confidence skills (save_skill, additive-only, source="distilled" → curator-managed), propose the rest as beads. New scoped, mostly read-only tools (recent_activity, list_skills, save_skill, forget_memory) — no shell / raw SQL (avoids MiMo-Code's writable-trajectory-DB risk). Out-of-graph runner now mirrors the lead's full tool set so allowlisted names don't silently dropAccepted
0055Multi-team orchestration — scale OUT: each repo/team is its own protoAgent instance running a single project_board for its repo; a PM agent federates over A2A (reusing ADR 0042 fleet/remote-members/delegate_to), treating each team-agent as a remote board. project_board stays single-board (multiplicity = the fleet, not an in-process registry). Context at scale = portfolio rollups + event/A2A deltas, never raw reads; governance is per-instance. P0 = deterministic per-instance board pinning (kills the cwd .beads auto-discovery gap); cross-plugin composition moves to the ADR 0043 SDK seamAccepted
0056Unified dockable-view model — one addressable View (kind: surface|session|plugin|ext) that can live in any container (a rail as a top-level panel, or a panel's tab strip as a tab), with one dockLayout store + a single shared DndContext. Makes the validated reorder / tear-out / re-dock gestures (POC spike/dock-dnd-tabs) one operation instead of a conversion between the uiStore.railOrder surfaces and the chatStore.sessions tabs. The crux is session-as-View (sessions become placeable peers → side-by-side multi-session). Sequenced after the DS TabBar reorder (protoContent #264); needs an AppShell "dock mode" (shared context) for the cross-container dragProposed
0057Command palette (⌘⇧K) — consume the DS @protolabsai/ui/command-palette substrate (protoContent ADR 0006) behind a thin web adapter: every surface auto-becomes a navigate command via useUI().setSurface (free for plugin views), plus a new declarative commands: manifest contribution type (mirrors views:; _parse_commands → runtime status → adapter compiles action→run, web is the only dispatcher → no plugin code in the bundle). Inline plugin views reuse pluginView() + consoleTheme()/authToken(); live search via a provider: route. Forces ADR 0056's viewFor(id). ⌘⇧K in-app (overlay) + Tauri global shortcut (desktop). Greenfield (no palette today)Accepted
0058Runtime plugin install in the frozen desktop app — a git-less HTTPS archive fetch + a bundled-dep gate (requires_pip ⊆ bundled, checked via find_spec) extend ADR 0027 so external plugins install at runtime inside the read-only PyInstaller bundle (the loader already discovers + importlib-loads live-root plugins in frozen mode; only git/pip were missing). First consumer: Discord becomes an external comms plugin — opt-in everywhere (removed from the default bundle), added via Settings (no onboarding step), behavior identical once enabled (same code relocated, frontend affordances stay in core). Gated cut: don't remove from the bundle until runtime-install is parity-verified on a signed dmg + the repo is pinned. Epic bd-3uhAccepted
0059Unified plugin manager — collapse the split (PluginsSurface's Local/Market/Download tabs + Settings▸Plugins config) into one surface with two sections: Discover (an in-app official-plugin directory rendered from a host-served config/plugin-catalog.json → GET /api/plugins/catalog, browse + one-click install that works on every surface incl. the frozen desktop app via ADR 0058) and Installed (each plugin row folds in enable/disable/update/uninstall + its config + Test/Connect inline; Settings▸Plugins → a pointer). The curated index ADR 0027 D9 / 0040 gestured at, now thin because runtime-install is the primitive. Subsumes the comms-channel one-click install. Epic bd-23aAccepted
0060Skills: progressive disclosure — replace per-turn BM25 retrieval (which injected top-k full skill bodies every model call, built from a query that included the agent's own recent output → generic turns re-summoned every skill and pinned it in context) with advertise cheaply, load deliberately: an always-on <available_skills> index (name + description of up to skills.top_k discoverable skills, query-independent, +N more → list_skills) plus a lead-agent load_skill(name) tool returning one skill's full procedure on demand (visible as a tool card). Greenfield-deletes load_skills/SkillRecord/_format_learned_skills/_build_skills_query and the skills_loaded chip end-to-end; skills.top_k → "skills listed in index", skills.announce/max_tokens removed. Mirrors Anthropic Agent Skills / Cursor rule tiers. ACP external-brain feed lists the same indexAccepted
0061Frontend extension registries — give the console the backend's extend-without-editing-core, update-safe property. Extend the src/ext/ fork seam (ADR 0038 D3) with behavior registries mirroring registerSurface (static, first-wins, HMR-safe): registerSlashCommand (shipped) lets a fork/core own a client-side /<name> (claim-the-token, the frontend twin of register_chat_command #1334) — core's /new//clear//effort now register through it, no hardcoded verbs. Plus registerComposerAction (composer actions slot), registerPaletteCommand (root command palette — core deep-links dogfooded onto it; carries icon/hint/disabled plus a keybinding id whose LIVE combo the row advertises (a literal shortcut string would lie the moment the user rebinds), last-wins by id with a guarded unregister, and declarative flag/hostOnly gates read at RENDER time through visiblePaletteCommands — the same axes settings sections use, so a flag that fails closed while /api/flags is in flight can't hide a row forever; registerPaletteSource serves read-time dynamic rows, and subscribePaletteCommands/paletteCommandsVersion back a useSyncExternalStore read), and createUISlice (a fork's own namespaced, per-agent-persisted store — not a merge into core UIState). A fork extends console behavior by dropping a src/ext/ module — no core edit. Surfaced by the GitHub→plugin extraction (#1336/#1337).Accepted
0062Full-screen document viewer — one app-wide reader (openDocument(spec) → a root-mounted full-screen DS Dialog rendering console markdown), mirroring the context-menu store+host+imperative-open pattern (src/docviewer/). DocumentSpec is generic (body resolves render() → load() async markdown → inline content; + title/subtitle) so any surface opens the SAME reader. Fixes background-agent reports landing truncated in chat (server carries a 2000-char preview, ADR 0050) — a "Read full report" card button loads the FULL report by job id; the Activity feed opens entries into the same viewer. Extensible for future long-content views (tool output, knowledge, artifacts). DS gap: Dialog has no size="fullscreen" (achieved via a .doc-viewer CSS override)Accepted
0063Scoped, user-rebindable keybinding system — a dedicated layer for global app commands (DS-internal + contextual composer keys stay as-is). registerKeybinding seam (src/ext/, last-wins by id) + one global keydown host that resolves a combo (mod=⌘/Ctrl) honoring focus scope (a panel marks data-kb-scope; the host walks the focused chain; most-specific scoped binding wins — so the same combo differs per panel), a typing gate (allowInInput), and GLOBAL user overrides (protoagent.keybindings, not per-agent). Settings ▸ Keyboard rebinds (record/reset/conflict-detect). Core defaults dogfood the seam: ⌘⇧K palette (adopted off the DS usePaletteHotkey → intents store), ⌘, Settings, / focus-composer (global); VS Code-style panel toggles ⌘B/⌘⌥B/⌘J (left/right/bottom); ⌘T new / ⌘K clear / ⌃Tab / ⌘1–9 (scope "chat"). Caveat: ⌘T/⌘1–9/⌃Tab are browser-reserved (work in the desktop app; rebind in a browser).Accepted
0064coder — execution-grounded code-solve: the missing verifier-grounded rung in the Lead Engineer board loop (not a new tier; not a PM concern). A solve() orchestrator over the delegates registry + code_exec, reached as a coder_solve tool (the deterministic ladder can't live in a prompted subagent) with a thin subagent wrapper (lead-agent ad-hoc, Subagents panel) and a board face (projectBoard-plugin's loop calls solve() per-feature, replacing the bare single acp shot). A difficulty-gated ladder gated on test pass/fail (greedy → best-of-k + execution-select → tree-search refine-on-failing-tests → fusion candidates + execute, ~3× paid only at the top). The board's Ready-gate EARS acceptance (already collected, today just prompt text) is compiled to the runnable oracle; no oracle ⇒ honest degrade to greedy (never best-of-k-with-LLM-judge by default — that's the judge-of-code ceiling this escapes). Composes existing substrate (disposable worktrees + forget_session/evict_client teardown, max_concurrent, protolabs/fusion as an openai delegate); composes with the board's existing model-tier coders ladder (search within a tier; escalate the tier when search stalls). gens-spent surfaced to portfolio_rollup (cost-v1). Issue #1440Accepted
0065Two-tier instance paths (box / instance) + single resolution rule — split the storage identity into a box root (per machine) and an instance id (per agent), both resolved from env (PROTOAGENT_BOX_ROOT / PROTOAGENT_INSTANCE, infra/paths.py) by one rule, not from the config file — so every store nests predictably under <box>/<instance>/ and the dev sandbox instance is isolated from prod. Underpins fleet robustness + the multi-instance test harness (zombie-reap #1474).Accepted
0066Goal trust-gate Phase 2: federation token + operator /api channel — an optional second credential auth.federation_token (A2A_FEDERATION_TOKEN) for semi-trusted A2A peers, confined to the /a2a + /v1 consumer surfaces and denied the /api operator surface (plugin install, config/SOUL rewrite, subagent runs, goal/watch set-paths) with 403 — the R1 path ceiling. Opt-in (unset ⇒ single-token mode). The /api ceiling, not per-verb gating, is the control (D4). Follow-ups tracked in #1504.Accepted
0067Standalone watch primitive (many concurrent watches) — resolves the fork ADR 0030 left open: a first-class watch that polls a condition on a cadence and, when it trips, runs a follow-up agent turn (via sdk.run_in_session, #1494) and/or fires hooks — the passive counterpart to a goal (which the agent drives). Unlike a goal (one-per-session) you can hold many. Retires the monitor-goal disposition (goals are drive-only).Accepted
0068Developer flags & the Developer panel — a small local/static feature-flag system (off·dev·beta·on) to gate pre-release functionality, built by reusing existing machinery: a single backend registry (runtime/flags.py, shaped like the config FIELDS list) exposed via GET /api/flags; a runtime channel (dev instance / import.meta.env.DEV / developer.channel) the tier is measured against; resolution precedence mirroring plugin-config env > query-param > panel > channel; a Settings ▸ Developer panel (CONSOLE_SECTIONS, device-local, gated to ≥ beta) to view/toggle/reset flags; and a cleanup contract (remove_by + a staleness test) so graduating to on deletes the flag + old path in one PR. A flag is not a plugin (load-time capability) nor a setting (permanent user config). Non-goals: A/B, %-rollouts, remote flag service. Issue #1506Accepted
0069Memory delivery layer — extends ADR 0021 from what is stored to how memory reaches the prompt: replace the raw pooled <prior_sessions> dump with an attributed digest (one line/session + recall_session tool), wrap ALL auto-injected memory (digest, hot, RAG) in an untrusted-reference envelope, namespace/incognito scoping, identity hygiene (api-default, unknown.json, prefix split). Phase 2: provenance stamping (fact→source session), a per-turn injection record, console memory inspector (a security control — how SpAIware-class poisoning gets detected). Phase 3: trust tiers by source_type, supersede-don't-delete staleness (invalidated_at, deterministic — no LLM freshness judging), memory-regression evals (knowledge-update/abstention/poisoning-replay). Anchored in OWASP ASI06 + product convergence (Claude project isolation, Copilot citation-validation, Gemini RESTRICTED-by-default, Claude Code v2.1.50)Accepted
0070Background results: push-resume, indexed reports, disposable workers — amends ADR 0050 pull-only delivery: on completion the server self-nudges the origin session (Claude Code-style push-inject-and-wake; notified-gated drain keeps exactly-once; background.auto_resume default on) so the agent briefs the operator; the full report is indexed into the KB keyed to the origin session (source_type="background_report", tier 2) with a summary-sized drain + search/docviewer pointer; background:* workers become disposable (no session summary, no digest line, no harvest — jobs DB is the system of record); report card → real card (raised bg, shadow, clamped excerpt + bottom fade, Open CTA) + GET /api/background/{id}Accepted
0071Plugin permissions: a trust-and-consent model, not a runtime sandbox — consolidates the locked ComfyUI posture into an explicit permissions model. In-process plugins run with full runtime privileges; the only hard boundaries are the enable gate + plugins.disabled, tools.disabled (by tool name), route auth default-deny + namespace-scoped public_paths, requires_env/min_version load gates, and install-time source allowlist + SHA pinning. Everything else (fs, network egress, cross-section secret reads, internal APIs, apply_settings) is unenforceable in-process. Decision: (D1) trust not sandbox — untrusted code → MCP, and stop re-proposing an in-process sandbox; (D2) present only the boundary we enforce — reframe manifest capabilities: as disclosure, not enforcement; (D3) formalize provenance-based consent (sources.official default github.com/protoLabsAI/* fork-overridable · sources.acked · trust_unverified) gating auto-enable (#899) and inherited by auto-update (#1720). Design-only; consent layer is a tracked follow-up. Issue #1725Accepted
0072Fleet seed: declare a team in config (team-via-config) — a fleet supervisor can seed a whole team from config instead of hand-registering each member, building on the unified delegate registry (ADR 0025), plugin bundles (0040), tiered stores (0041), and the fleet supervisor/console (0042).Proposed
0073Goal completion contracts — an optional, structured completion contract on GoalState (outcome / constraints / boundaries / stop_when) layered OVER the existing verifier surface (ADR 0028). The contract is re-stated in the drive-loop continuation prompt every turn (Hermes-Judgment-style contract-directed continuation) but is directive only: DONE is still decided by the goal's real, deterministic verifier — never an LLM-judge-as-arbiter (our edge). All fields default-empty (existing goals unaffected); plumbed through POST /api/goals + set_goal_operator/set_goal_safe; stop_when v1 is prompt-injected (agent self-parks via abandon_goal). FE goal-creation form is a follow-up.Accepted
0074System lifecycle events — a plugin seam for host lifecycle reactions (on_app_loaded / on_agent_active / on_system_wake) via register_lifecycle_hook, so a plugin can run work when the app loads, the agent becomes active, or the machine wakes, without editing core.Accepted
0075External interfaces: a first-class protoagent CLI + tightened MCP / API / model onboarding — treats the CLI, operator MCP, and HTTP API as one operation model with three faithful projections. Retires python -m server <sub> for an installable protoagent command (re-parenting the hidden plugin/workspace/fleet/skills/config dispatchers + new up/down/status/serve/setup); a shared ops/ layer ends REST-only orchestration glue; the operator MCP grows the full admin surface behind curated profiles + ADR 0071 consent + the ADR 0066 /api ceiling (and stops HITL tools hanging a foreign client); the REST surface deduplicates split namespaces (goal[s], memory/knowledge/injections), fills real /v1 usage, and publishes a /api/operations catalog; plus fast local-LLM onboarding (Ollama/HF/LM Studio/vLLM discovery + protoagent model use + a HuggingFace "Use this model" local-app registration — a CLI snippet, not a deep-link, matching the hermes-agent/openclaw/pi pattern). proto stays the A2A chat client — the two meet at the wire, not in one binary. Ships uv tool/pipx + zero-Python frozen-binary curl|sh.Accepted
0076Managed git for acp delegates — framework-owned branch/commit/push/PR behind an opt-in manage_git delegate field (+ base_branch, branch_prefix, and an item_id arg on delegate_to): the coder edits files and runs tests; a deterministic harness (plugins/coding_agent/git_harness.py, on run_command/run_gh) mints the branch (<prefix>/<slug>-<last7(item_id)>, no LLM naming), branches off fresh origin/<base>, then isolation-guards (stranded_on_base) → secret/scratch-scans → commits on the coder's behalf (idempotent to partial git: adopt unpushed commits / fixup already-pushed) → rebases (conflict ⇒ abort+push+report) → pushes --force-with-lease with lock-contention retry + remote-SHA verify → idempotent gh pr create (0-ahead skip, pre-check, already-exists recovery). An event-loop-atomic single-claim registry on item_id (default sha1(query)[:12]) dedups fan-out — duplicate PRs are the #1 empirical agent-PR rejection cause. Ported from source-verified protoMaker; plan + due diligence in docs/plans/coding-agent-deterministic-git.mdAccepted
0077Adversarial code-review workflow + the findings convention — a first-class code-review recipe (4 parallel review-finder angles: correctness / removed-behavior / cross-file / conventions → review-synthesizer dedup/rank → ONE verifier pass over the merged list, github-tool-grounded → final report ending in a canonical fenced findings JSON) plus the convention itself in graph/review/findings.py: schema [{file, line, severity, category, claim, evidence, verdict?}], FINDINGS_CONTRACT prompt snippet (schema written once), tolerant parse_findings (JSON-in-prose, coerced fields, verifier-vocabulary normalization), render_findings_markdown. The craft /code-review skill becomes a thin driver; the board review gate (M5) consumes the same recipe headlessly via run_workflow. Per-finding verify fan-out deliberately parked (needs a data-driven engine primitive).Accepted
0078Fleet PR review as a protoAgent tier — harvest of protoWorkstacean's Quinn × protoPatch system around the ADR 0077 engine: four-layer placement (core engine · github-plugin formal Review-API verdict tools + path_exists with the CI-terminal guard enforced INSIDE the tool · new pr-reviewer-plugin for webhook dispatch/structural trigger/approve-on-green pure function + sweep/protoPatch client/eval · new qaEngineer bundle with archetype + persona). Guards ported verbatim from Quinn's failure ledger (#863 CI-terminal, #748 deterministic approve, #891 server-computed trigger, #858/#903 unresolved-threads gate); fail-closed at every judgement boundary (a partial panel is not a review; Gaps are never severities; never review own PRs); GitHub-native review memory (prior_findings delta re-reviews); shadow mode against Quinn's live traffic before per-repo cutover.Accepted
0079Autonomous operating model — goals · tasks · scheduling · watches unified into one OODA loopAccepted
0080External secrets manager: env hydration with pluggable providers, Infisical first — a SecretsProvider contract + registry in infra/secrets/ whose orchestrator owns every os.environ write (existing-env-wins, ownership/provenance so refresh only touches manager-set vars, protected bootstrap + PROTOAGENT_* identity vars, redaction registration); hydration runs in from_yaml before the parse so the env fallback tier (gateway key, A2A_AUTH_TOKEN, plugin requires_env, MCP child env) sees manager values on every load path — plugins load after config (ADR 0019), which is exactly why a plugin-provided backend is a non-goal. Infisical ships built-in over raw REST (universal-auth login → v3-raw list; no SDK — infisicalsdk drags boto3 and does no token renewal anyway); bootstrap machine identity is the one local secret (SECRET_PATHS + settings-schema secret fields → secrets.yaml, env fallback INFISICAL_CLIENT_ID/_SECRET). Refresh loop + /api/secrets status/sync/test; fail-open by default, required: true = fail-fast boot; TTL-gated in-process cache, no disk cache; infisical run stays the documented ops alternative.Accepted
0081Self-authored persona — a guarded edit_soul tool that lets the lead agent rewrite sections of its own SOUL.md. Off by default behind soul.self_edit_enabled (a ui_hidden settings field); section-scoped (replace/append, never whole-file) and persona-only by contract, upholding ADR 0079's persona/doctrine split and the ADR 0066 operator-trust ceiling (subagents never get it). Writes go through config_io.write_soul, so every edit snapshots to soul-history (#1691) and is restorable from Settings ▸ Identity; a 64 KB cap keeps the prompt prefix bounded. Take-effect is live next turn via the server's _reload_langgraph_agent injected (not imported) as a reload_callback through create_agent_graph → get_all_tools, so tools//graph/ keep clear of server/ (import-linter green); the reload rebinds STATE.graph atomically so the current turn is unaffected. Degrades to next-reload semantics when no callback is wired. Every edit is never silent — publishes a persona.self_edited event so the operator sees it (OpenClaw's "tell the user" made real). Due-diligenced against prior art: Hermes keeps SOUL.md operator-only (users want evolution, unshipped), OpenClaw invites unguarded self-edit (a prompt-injection surface), Letta added a read-only persona guard after unconstrained self-edits degraded identity — edit_soul is the guarded middle.Accepted
0082Per-chat ACP runtime — let an operator pick an ACP coding agent from a chat tab's model dropdown and run that chat on it, hot-swapping the runtime the way a gateway model already hot-swaps. Extends ADR 0033's runtime/model split: today the native-vs-ACP fork reads the global agent_runtime at the top of the turn (server/chat.py), before the per-tab model is consulted, so the two never meet — and naively feeding acp:<agent> into the model picker would hit create_llm's text-only aux relay (Trap A), not the real AcpRuntime. Decision: (D1) a per-turn resolve_turn_runtime(request_metadata, config) — an acp:<agent> per-tab model routes the turn to _acp_drive_turn; (D2) re-key the ACP session registry by (thread, agent) so a swap builds a fresh session while the old agent's survives; (D3) guard so acp:* never reaches the native lead-model path; (D4) Option C continuity — allow the mid-chat swap but emit an explicit context-boundary system-note and start fresh (history is split — native in the checkpointer, ACP in the agent's own stateful session — and never synced; two-way history bridging is Option B, deferred); (D5) agent registration shipped in #1993; (D6) the only real FE change is ComposerModelSelect.tsx (chat-store/api already carry an arbitrary model string). Out of scope v1: history bridging, and /effort//compact/HITL/skill parity under ACP.Accepted
0083Cowork mode — a prepackaged knowledge-worker archetype for Claude Cowork graduates: cowork soul preset + one catalog row pointing at the cowork-archetype bundle (formerly cowork-stack; cowork-plugin skill pack + artifact + notes + execute_code + google-plugin + claude-bridge), shipped as data over the existing ADR 0042 seams — zero new core seams in v1. Grounded in verified research (Cowork = Claude Code headless in a VM; usage = 33.4% biz-ops / 16.4% content; ceilings we exceed: no cron, cloud tasks can't touch local files, siloed projects-only memory, no model choice, remote-default = files processed on Anthropic servers). D3 records the licensing finding: Anthropic's doc skills are all-rights-reserved (public repo ≠ redistributable) → cowork-plugin ships clean-room docx/xlsx/pptx/pdf over python-docx/openpyxl/python-pptx/pypdf, and the claude-bridge importer must skip creatorType: anthropic. D5 defers the missing seams (bundle mcp: field, rw-no-delete fence, approval tiers, env seeding) to filed issues.Accepted
0084Computer use — an out-of-process cua-driver MCP wrapper, and the fence it voids. Records the posture first: computer use is the first capability that voids our containment story wholesale — the ADR 0008 egress allowlist lives inside fetch_url and the ADR 0007 filesystem fence is an in-process path check, so an agent that moves a mouse routes around both by never touching them (opens Safari → no fetch_url call → no allowlist). Not weakened; absent from the code path. Per ADR 0071 the risk is a presented model implying containment the enforced model lacks, so D2 makes the plugin say so plainly. Decision: (D1) option A — a standalone cua-plugin whose register_mcp_server factory spawns cua-driver mcp over stdio; zero venv packages, frozen-build-safe, the exact shape ADR 0027 D1 mandates for dangerous code; the factory probes for the binary and returns None when absent, so we never install it and the macOS TCC grant stays a human GUI action. (D3) allowlist the driver's tools to the documented loop — 17 of 38 (ADR 0005). (D4) ship a SKILL.md — the driver is accessibility-tree-first, not screenshot-first and the snapshot-before-and-after invariant is load-bearing (element_index is per-(pid, window_id) and stale across turns); note screenshot was removed upstream (cua PR #1692) → get_window_state capture_mode:"vision". (D5) defer cua-sandbox on evidence — resolving it against our real tree goes 114 → 294 packages (+154 pyobjc-framework-* via cua-auto → pywinctl → pyobjc, Twisted via vncdotool, exact grpcio==1.78.0/protobuf==6.33.6 pins), cloud is sales-gated, and the cua meta-package's ≥3.12 floor breaks ours; revisit scoped to local Docker only, since a VM is the one path to OS-enforced isolation (the inversion: driver-on-host = least contained tool we ship, sandbox-in-VM = best). Reject the hand-rolled wire client (unversioned 0.1.x tax) and cua-agent as a tool (a competing loop; belongs behind the ADR 0025 delegate registry if ever).Accepted
0085Managed Node.js runtime, provisioned on demand — the "works on download" answer to the desktop npx gap. The npx-based ACP coding agents (Claude Code, Codex) and most catalog MCP servers need node/npx on PATH, which the desktop app (Python-sidecar-only bundle) doesn't ship; the existing PATH machinery (#1299 Tauri login-shell PATH, acp_client._discovered_node_dirs) can only find a Node the user already installed. Rather than bundle Node in the installer (a second binary to notarize, paid for by users who never use it) or bundle it + vendor the adapters (100 MB+, owning the Claude SDK cadence), option 3: protoagent runtime install-node downloads a pinned Node (NODE_VERSION + an in-repo _SHA256 table — a real integrity gate, not trust-on-first-use) into the box-shared box_root/runtime/node/current (one machine provisions once; every instance/dev-sandbox/fleet member shares it). Two halves split by layer: infra/node_runtime.py (light, importable everywhere) exposes the single consumption seam augment_path_with_managed_node() — a boot-time (server.agent_init) PATH append (append so a user's own Node wins; process-level so ACP, MCP, delegates, and gh inherit it uniformly, not one patch per launch site), re-run on install so a live server hot-adopts without restart; runtime/node_install.py (download/verify/extract/swap, old install kept until the new is in place) is consumed by the CLI + runtime list status, with a one-click console button deferred. npx -y still fetch-caches the adapter on first launch (we deliberately don't vendor it).Accepted
0086Chat-first mobile shell (amends ADR 0035 D6)Accepted
0087Device pairing — per-device revocable tokens + short-TTL QR codesAccepted
0088Per-finding inline review threads — finding-granular, GitHub-native review memoryProposed
0089Intra-instance trust boundary — the hub authenticates, members trust a fleet service token — fixes "all plugins broken on sister agents": each fleet member has its own instance_root (ADR 0065), so its own devices.json (ADR 0087) and auth.token, and the hub reverse-proxy (ADR 0042) forwards the caller's Authorization verbatim to a local member — which re-authenticates the external credential against its own isolated state, so a hub-minted device token is 401'd by every sister (structural: per-device tokens can't be shared). It only ever "worked" because members run open on loopback (supervisor.py strips the token) — itself the local-process RCE hole. Decision: adopt the API-gateway model — (D1) hub is the single auth boundary, members are backends that never consult the device registry; (D2) one internal fleet service token generated once, injected into every member at spawn via env (PROTOAGENT_FLEET_TOKEN, never config/URL), accepted as operator by every agent; (D3) the proxy swaps the caller's Authorization for it on local-peer targets, operator-tier-only + preserves the #1890 member_public anonymization; (D4) every in-instance caller (proxy · delegate_to · portfolio-PM board dispatch) presents it; (D5, phased) members then stop running open, closing the RCE hole. Rejects a signed per-request assertion (loopback needs no per-request crypto; door left open) and a box-tier shared registry (breaks ADR 0087 dev/prod isolation). The a.symbol white-screen is a downstream unguarded-401 in the plugin view (D7 follow-up).Accepted
0090Console-created goals drive in a dedicated chat tab (attach/detach) — the console surface for the ADR 0079 drive loop. A panel-set goal used to run headless (a run_in_session kick in the operator session), so the loop worked in the background but was never in the forefront. Decision: (D1) a Work-panel goal opens a focused chat tab, sets the goal with kick:false, and fires a hidden kickoff so the inline drive loop streams live into the tab (chat-set goals stay in chat); (D2) attach/detach — closing the tab prompts keep running (/resume a headless continuation, keep the server session/context) or stop (DELETE …?close_tasks=true — clear + close the goal's task backlog); (D3) a detail drawer surfaces the plan artifact, contract read-back (ADR 0073), evidence, and a per-iteration timeline, with rearm lifecycle (extend/restart); (D4) creation is operator-tier (ADR 0066 — any verifier); (D5) robustness — no-progress works for fuzzy llm goals (plan-hash), and the best-effort kick is guarded. Trade-off: driving from the tab means closing aborts the in-flight turn (detach re-kicks) — chosen over headless+observe (typing indicator only).Accepted
0091Agent snapshot portability — export an agent as a portable, SECRET-FREE zip and rehydrate a fresh agent from it. An agent = one instance root (~25 stores). A raw instance-root dump fails: secret-laden (secrets.yaml + .fleet-token + secret VALUES across ~15 sqlite DBs) and version-brittle — the docker commit/committed-.tfstate anti-pattern. Decision: the snapshot is a declarative bundle + optional seed, not a state dump. (D1) manifest modeled on the existing protoagent.bundle.yaml — SOUL + secret-stripped config + plugins.lock SHA pins + subagents + MCP + archetype ref; (D2) secret-free by the 12-factor litmus — reuse strip_secrets_from_doc/secret_paths, ALSO null mcp.servers[].env/headers (inline, uncovered), carry a required_secrets schema (names, no values — à la Letta Agent File nulling + A2A securitySchemes); (D3) rehydrate via a new secret-free entry into workspaces/manager.create() (NOT its secret-copying from_config clone), prompting for required_secrets; (D4) stateless definition always, runtime history seeded empty, knowledge/memory an opt-in operator-reviewed seed via knowledge_add; (D5) snapshot (portability) ≠ backup (DR encrypted tar, out of scope). ~80% reuse. Slice plan: export → import/rehydrate → knowledge seed → console UX. Slices 1–3 SHIPPED (#2103 export, #2104 import, #2105 opt-in knowledge seed — which RETRACTS the publishable claim in its review, since secret-free ≠ safe-to-share; memory is never seeded) — with one correction: the exporter deliberately does NOT reuse strip_secrets_from_doc, which fails OPEN (leaves a secret inline when relocation fails, #1645) and writes to secrets.yaml; export uses a pure fail-CLOSED redactor plus a pattern-level second layer.Accepted
0092Desktop document-generation baseline + versioned file artifacts (D1 doc stack; D2 save_file_artifact)Accepted
0093Pip-less wheel installer for frozen-app plugin deps (#1631 Scope A) — opens the open half of ADR 0058 D2. A third-party plugin whose requires_pip isn't bundled is no longer refused on the desktop app: behind the opt-in plugins.allow_unbundled_deps, its pure-Python deps install as wheels into a writable per-instance plugin-deps/<id> dir prepended to sys.path (the same mechanism that makes the live plugins root work when frozen), where the plugin's own module imports them. Mirrors ADR 0058 D1's git-less HTTPS fetch — closes the remaining pip gap the same way: (D3) PyPI JSON resolve → download the wheel over httpx → sha256-verify vs the PyPI digest → unpack; (D5) transitive resolution with real packaging marker/specifier eval, extra-gated/inapplicable deps dropped, already-bundled short-circuit, depth+cycle guard; (D6) each (name, version, sha256) pinned in plugins.lock, spec rails kept. (D4) P1 = pure wheels (shipped #2151); P2 = platform-wheel tag matching via packaging.tags (deferred #2155 — the big compiled deps are already bundled by ADR 0092, so P2 is the third-party long tail). Composes with ADR 0094 P2 on the frozen install_deps seam: host wheel-deps dir (this ADR, module imports) + managed runtime (0094, execute_code skills), each behind its own opt-in. Opt-in + consent + hash-pin, not a sandbox (ADR 0071). Freeze-verified: a plugin declaring requires_pip: [humanize] installed + imported on a real frozen build.Accepted
0094Managed Python runtime — execute_code on the frozen desktop app (closes #2137, completes ADR 0092's stated outcome). The engine spawns sys.executable, which in a PyInstaller build is the frozen server binary, so a hard gate leaves the tool unregistered on desktop — and any capability that computes through code is silently inert on the flagship distribution (cowork's docx/xlsx/pptx/pdf skills author through execute_code, so ADR 0092 D1 made cowork install but not produce). Two constraining facts: PyInstaller packs pure-Python libs into the PYZ (an external interpreter can never import the D1 bundle), and the child's library surface has always been the interpreter's own site-packages (scrubbed env, no PYTHONPATH). Decision: apply the ADR 0085 pattern to CPython — (D1) pinned CPython 3.12.x from python-build-standalone, in-repo SHA256 table, guarded extract, atomic swap into box-shared runtime/python/current, python_status(); (D2) frozen → managed interpreter ONLY (no system-Python fallback — deps roulette reproduces the invisible-breakage class), source → sys.executable unchanged; (D3) provisioning pip-installs the doc baseline from requirements-docs.txt (ADR 0092's single source of truth) into the runtime's own site-packages, reproducing source-run semantics; (D4) honest state everywhere — the tool registers on frozen and says "runtime not provisioned — install under Settings ▸ Tools (~35 MB)" instead of today's copy that implies the toggle works; (D5) /api/runtime/python + install (202+progress), protoagent runtime install-python, one-click button beside the toggle (consent = the click; plugin stays disabled-by-default per ADR 0071); (D6) out: PATH exposure (scoped to the execute_code spawn), superseding ADR 0093 (complementary — 0093 = frozen HOST imports, 0094 = child interpreter + deps; together the desktop compute tier), sandbox changes (subprocess + scrubbed env + timeout posture unchanged). Rejects in-process exec (credentials readable in-env, no crash isolation, no reliable kill), fixed first-party doc tools (docs-only, kills skill flexibility), system-Python discovery, and bundling an interpreter in the installer (~40 MB for users who never enable it).Accepted
0095Managed projects registry — one first-class top-level projects: section replacing the four places protoAgent declares a project today (filesystem.projects fence · operator_project_dir · project_board.repo · github-plugin's default_repo/repos). Registering one repo means typing its path in one box, its owner/name in a second, and its path again in a third, with nothing cross-checking that they name the same thing or detecting drift when one moves. Decision: (D1) an entry is a project's binding on this host — {name, path, github, default_branch, write, no_delete, fs}, keyed on name because the fs tools already address projects by it (no separate slug — a stable-id field would only earn its keep joining against an external system, and none exists); planning state stays with whatever board owns the work. (D2) the existing keys become projections with explicit config still winning, so no config migrates on upgrade; the fence has one insertion point (effective_filesystem_projects(), already the seam both production consumers route through), while operator_project_dir is deliberately left alone rather than conflating "what the console is looking at" with "what the agent may touch". (D3) registered ⇒ fenced read-write by default — the reverse of the cautious-looking default, recorded with its reasoning because it invites re-litigation: the case for "grants nothing until separately opted in" rests on entries arriving from somewhere other than the operator, and no such source exists, so a second opt-in for a directory you just hand-registered buys no risk reduction while reproducing #2251 inverted (register six projects, fence none, whole fs toolset unbinds with no visible cause); fs: false covers the genuine no-reach case, and any future imported entry must land fs: false. (D5) /api/projects + a Settings ▸ Capabilities panel reusing the #2264 server-side PathPicker (the console routinely configures a machine the browser isn't running on), showing each row's access mode and flagging unresolvable paths so a wrong entry is visible when made rather than as missing tools later. Out of scope: any external sync (no source exists), a file-explorer/IDE console view (plausible separately; this registry is what keeps it from becoming a fifth declaration site), multi-host path maps. Rejects extending filesystem.projects in place (cheaper, but an fs: false project inside filesystem: is incoherent), a plugin-owned registry (inverts the lint-imports contract — the fence is core config consumed at graph build), and doing nothing (the drift is silent — a moved checkout leaves the board pointing at the old path while the fence points at the new one).Proposed
0096The self-building loop — plugin-devkit closes design → scaffold → edit → test → hot-swap, the product thesis ("an app that builds itself around the user") given its missing architectural record. A 2026-08 audit found every mechanical piece shipped (devkit scaffold+live-enable #902/#913 · full hot-reload · ADR 0025 delegates with the per-call workdir seam · the board loop) but the loop open in eight places: no tool can edit what scaffold writes (the skill instructs an edit no tool can perform; the plugins dir is outside the ADR 0007 fence), no tool runs the scaffolded pytest suite, load failures are str(exc) with no traceback (+ reload_plugins reports every disabled plugin as a failure), the lanes never meet (devkit↔delegates↔board: zero cross-references), no runtime write path into the ADR 0095 registry, console refresh is poll-luck on agent-initiated changes, _apply_settings_changes event-loop safety is accidental, frozen with_tests raises FileNotFoundError. Decision: one spine, three lanes (in-place · delegated · board-driven) owned by the devkit — (D2) file tools fenced to the plugins root, NOT the operator fs fence; (D3) test_plugin on the ADR 0094 interpreter-resolution pattern (source sys.executable, frozen managed-runtime-or-actionable-refusal), testkit embedded as data; (D4) tracebacks + enabled-only failure reporting + rollback honesty; (D5) develop_plugin dispatches the configured ACP delegate into the plugin dir (manage_git off), then auto test+reload; (D6) git_init scaffolds repos-from-birth + the registry's first runtime write, scoped to the plugins root (the agent never widens its own fence outside its domain); (D7) board stays single-repo, graduation = config, the five naive-wiring breaks recorded, no outside-work auto-ingestion; (D8) agent-initiated changes push console invalidation + the guide view becomes live build status; (D9) explicit reload offload with a regression test; (D10) guardrails deferred by operator decision, ADR 0081's seven-guardrail set recorded as the follow-up shape. Devkit enabled: false IS the consent gate until ADR 0071 D3 lands.Proposed
0097Native OAuth-subscription providers for Claude and ChatGPT/CodexProposed
0098One process-tree lifecycle: infra/proc anchors and kills every child treeProposed
0099Chat-bundle format v1 — a structured, artifact-aware export for the P2 hosted viewer (#2179). P1 (#2158/#2181) shipped Markdown-only, no structured/versioned format and no artifact support; #2179's hosted viewer needs to mirror the console's own chat rendering, which a flat string can't drive. Decision: (D1) JSON manifest mirroring ChatMessage/ChatPart/ToolCall — a tool_call part nests {id, name, input, output} as one unit, not a separate message; (D2) sibling to export_op, reusing its role_of/text_of/tool_calls_of/redact() rather than forking them; (D3) artifacts resolve through an injected callback (plugins.artifact.resolve_for_bundle), never a direct plugin import — zero precedent for graph/server importing a specific plugin; (D4) which artifact VERSION to bundle is the hard part — show_artifact/rewrite_artifact read the call's own args directly (zero trim risk), update_artifact indexes into the version chain by the reported number, but that number is only trustworthy until the artifact's version count has ever exceeded its retention cap (two different commits can then report the SAME post-trim length) — detected via a new version_count lifetime counter, not timestamp comparison (which a real test caught colliding at millisecond resolution); once trimmed, every numbered lookup for that artifact reports unavailable rather than risk the wrong revision on a public link; (D5) binary file-kind artifacts are a placeholder (kind/size/filename) in v1, never bytes; (D6) packaged as a zip (manifest.json + REVIEW.md) like ADR 0091's snapshot bundle. #2682/#2683 (pre-publish review + publish client) now wire it in: (D7) infra/publish/client.py mirrors ADR 0080's infra/secrets shape — typed never-raise PublishResult/PublishErrorKind, httpx + a transport test seam; publish.endpoint_url empty (the default, since #2685's hosted service doesn't exist yet) reports not_configured without a network attempt, the same honest-state shape ADR 0094 uses; (D8) preview (GET .../publish/preview, read-only) and publish (POST .../publish, rebuilds fresh + sends) are separate routes rather than one route with a confirm flag, both behind the new chat.publish developer flag (tier off — the feature is incomplete, not destructive); (D9) the console's PublishDialog reuses the real Message/Markdown/ToolCalls primitives rather than a second renderer, but deliberately skips the full ChatMessageView (built for live turns, not a static preview) and gives artifacts a small purpose-built summary card, not a sandboxed render.Proposed
0100Agent archetypes, ratified — the starter-agent-type system that shipped incrementally through July 2026 (data-driven catalog #1565 · preview #2016 · tier #2241 · requires #2186 · requires_tools capability contract #2277/#2315 · bundle-block soul_preset + key validation #2715) finally gets its own decision record; ADR 0042 §F's four lines were the only prior spec. An archetype is a persona + optional bundle, data never code, merged from TWO sources (config/archetype-catalog.json, live-dir overridable with a Basic+Custom fallback; plus every installed bundle whose lock-cached archetype: block has a label — deduped by id + normalized URL, catalog wins, custom always last). Ratifies the full row schema (soul/soul_preset resolution incl. inline-wins + warn-and-fallback on unknown preset; standard/advanced tier; advisory requires choose-time warning; requires_tools persisted to workspace.yaml and boot-checked, advisory per ADR 0071), what each consumer applies (wizard → host install with Configure-step seeding #2714; new-agent picker → workspace create with soul/bundle/config/mcp/secrets seeding + rmtree rollback), and the boundaries: not a fork (operator personas stay downstream, the #2537→#2539 precedent), not a team (ADR 0072's member primitive), not a snapshot (ADR 0091 is the second picker source). Fixes terminology: bundle = the mechanism, stack = a published archetype-carrying bundle repo.Accepted
0101Context lifecycle — log, surface, pressure (umbrella #2772): the first decision record governing how a session's context grows, shrinks, and gets priced, prompted by a 30.9% measured cache hit ratio (cache-disciplined harnesses hit ~99.9%) and a $58.83/38-call turn. The stable prefix was already byte-stable by design — the payoff was thrown away by three gaps: no cache breakpoints on message history, the per-call volatile context block sitting BETWEEN cached prefix and history, and a cache-less subagent stack. Decisions: (D1) the prefix is a contract — 1 breakpoint slot on the stable system block + up to 2 rolling slots on the history tail, subagents cached (#2777/#2778); (D2) volatile context (RAG/memory/working_state/skills) moves out of the system message into the turn's input frame, composed per-TURN not per-call — ADR 0069's envelope moves intact (#2776/#2779); (D3) every tool result bounded (new mcp.max_result_chars — MCP was completely uncapped) and prunable in history at ~60% pressure via head+marker+tail stubs, batched-late to amortize cache invalidation (#2781/#2782); (D4) relief order = prune → summarize → catch overflow once (compact+retry — today nothing catches it and ModelFallback re-sends the same oversized prompt, #2783); (D5) fold-never-destroy — auto-compaction becomes archive-first reusing /compact's machinery, proceed+loud-log on archive failure (manual /compact stays strict), the past-remove_by chat.compact flag is removed (#2784/#2785); (D6) pressure persisted + projected (TelemetryStore columns, context-v1 projection, console series — #2773/#2787); (D7) 1h cache TTL for fleet/desktop profiles (#2780); (D8) #2710 round-count circuit breaker adopted — D1 makes rounds cheap, D8 keeps them coherent. Deferred to its own ADR: derived-view compaction via ModelRequest.override(messages=...) (#2786).Accepted
0102The trajectory — append-only session log + derived surface (umbrella #2806; the log half of ADR 0101's deferred log/surface split, giving #2786 its home). The audit's sharpest forensic finding survives the Endless Context work: "what did the model see on turn N" is unanswerable — prompt snapshots hold system prompts only, audit keeps 200-char summaries, and the checkpoint is a current-state snapshot destructively rewritten by compaction/pruning/rewind/fork. Decisions: (D1) a per-session append-only JSONL of REFERENCES (message ids + content hashes + sizes, stable-prefix hash, tools hash; surface_op events for every history rewrite) — the log is a spine over existing stores, never a second copy of every byte; (D2) "model-visible means logged" honestly bounded — reconstruction joins refs against checkpoint + chat-archive, PROVES what was sent (hash+size) even when pruning destroyed bytes, and says so; opt-in trajectory.full_text dev flag for deep forensics + true fork-from-any-point; (D3) writer = wrap_model_call middleware after PromptCache + one-line emits at the four rewrite sites, best-effort, checkpoint-TTL retention; (D4) read API (/api/trajectory/...) with per-message availablearchived
0103Programmatic tool calls (PTC) — agent tools callable from execute_code (umbrella #2807, spike-gated). ADR 0101 made agentic rounds cheap and coherent; the round COUNT is still the structure — a 16-issue sweep is 16 model rounds because each result needs a model look before the next call. DSH's "code mode" (the CodeAct pattern) collapses that: the model writes a script that batch-calls tools, and only what it prints/returns re-enters context. protoAgent has half already (execute_code, opt-in subprocess engine, 6k stdout cap); this ADR designs the missing bridge WITHOUT reopening ADR 0089's intra-instance trust boundary. Decisions: (D1) opt-in twice — tools.ptc.enabled off by default, S1 behind a dev flag until the spike's numbers justify the surface; (D2) loopback RPC with a single-RUN bearer token minted at spawn, dispatched through the SAME binding-layer path a model call takes (tool_map after drop_disabled_tools, enforcement, fences) — the token is the capability, loopback grants nothing; (D3) curated read-mostly default allowlist (read_file/search/fetch/web_search/memory_recall/...), operator-extendable by explicit name, HITL tools hard-denied (an interrupt can't park a subprocess — the subagent rule); (D4) a generated protoagent_tools.py stub per run from the run's actual allowlist — what's importable IS what's callable; (D5) only stdout/return re-enters MODEL context while every bridged call still hits audit/trajectory (via: ptc) + telemetry + per-tool caps; (D6) out: task() from code, cross-instance, MCP in the default set, streaming tools. S1 spike measures rounds/tokens/wall-clock on the 16-issue-sweep shape — the ADR graduates or dies on those numbers. AMENDED 2026-08-18: the bridge already existed — execute_code ships a socket-RPC tools proxy (per-run token, ephemeral port; a smaller wire shape than the ADR's HTTP route, which moves to Rejected). S1 closed the LIVE gap instead: the default bridge set was EVERYTHING registered — now a curated read-mostly default + structural _NEVER_BRIDGED denial (HITL/task/self, even over explicit config), plus the measurement harness (deterministic ten-reads-one-round repo test + scripts/ptc_bench.py for model-in-the-loop numbers). S2 (signature-rendered stubs) + S3 (ptc: audit/metrics rows) shipped in v0.138.0; GRADUATED 2026-08-18 on the pre-registered bench (rounds 5.5×, clean-rep wall 2.4×, correctness 4/4). S4 closed binding parity: late factories receive the denylist-FINAL toolset (outputs swept too), bridged calls meet the turn's subagent_fence + an enforcement gate from the same config fields at dispatch (rate limits per-path by construction), denials land as failed ptc: audit rows. GA surface = plugin enablement + execute_code.tools — tools.ptc.enabled was never implemented and is NOT added (a second switch for the same door). Rejects in-process exec (0094's reasons), a separate tool-server process, bridge-everything, and MCP-in-the-middle.Accepted
0104Durable chat recovery (Swap & Resume S5). A bounded GET /api/chat/sessions index discovers recent chat- contexts without transcript content; the bounded /turns reader returns the newest chronological tail as raw A2A status/artifacts/history for the console's existing replay path. localStorage remains primary, while boot hydration restores only missing or empty tabs with capped fan-out, vetoes stale local edits, and reattaches nonterminal turns. Explicit deletion removes current rows and writes a durable, non-evicting tombstone against late saves; the console clears with retire=false only when no producer is known active, while Keep running persists a local dismissal instead of retiring the session. Recovery inherits the task store's 24-hour terminal retention.Accepted
0105Fleet-shared delegates. #2977 copied the picked coder into a new member — a snapshot that drifts (rotated key, moved binary) and never reaches existing members; the ADR 0047 Host layer can't carry a raw list. Decision: a delegate has a scope — agent (per-instance, default) or host (the box's host-config.yaml delegates: + an owner-only host-secrets.yaml); every instance reads agent ∪ host (agent shadows host by name, secrets merge the same way); only the hub writes the host layer (members get 403 and may shadow instead); console: a Share with fleet switch on the hub, a fleet badge read-only on members; the create-time copy stays for agent-scoped picks.Accepted
0106Providers are a registry; every model reference names its providerAccepted
0107Unified, policy-bounded self-improvement reviews — one off-by-default settings surface; achieved goals and closed tasks enqueue a bounded same-session reviewer whose tool allowlist mechanically enforces off / propose / auto; automatic skill mutation is private-store-only with faithful rollback snapshots, and privileged persona edits retain soul-history + live reload. Memory stays read-only in this policy.Proposed
0108Context Architecture v2 — typed memory, prompt projection, and runtime context. Governs the four invariants: (D1) durable state and model-visible context are separate — a request-time projector derives the model surface without persisting it; (D2) request-only projection replaces checkpoint-persisted derived frames; (D3) capability-derived prompt contracts; (D4) typed memory attributes (kind, provenance, scope, review state, lifecycle) over one physical backend. Migration in five phases, each independently rollbackable.Proposed
0109project_board: one board, one store, N repos — the board stays a per-instance singleton (non-goal: no in-process board registry) but is no longer bound to a single repo: a feature carries its target repo and the plugin routes each dispatch (worktree, coder, PR) to it. Cross-repo routing is the plugin's job; cross-team federation stays the fleet's (ADRs 0042/0055). Pinning follows the D3 instance-store db_path: the beads store lives in the instance data root, never a repo checkout. Supersedes, in part and in place, ADR 0055's single-board sub-decision — "single board" stands, "for its repo" does notAccepted
0110Plugin setup steps — a setup-gap fix that is a command ("download the CLI", "install Chrome") becomes a banner button the host runs. A new closed action kind plugin_setup names a step the reporting plugin registered with registry.register_setup_step(step, fn); the action stays data (validated id, target forced to the reporting plugin). The console POSTs the core route POST /api/plugin-setup/{plugin_id}/{step}, which runs only the callable held for exactly that (plugin, step), off the event loop, audit-logged, 404 otherwise. The route is operator-only, not agent-callable, and deliberately outside /api/plugins/<id>/, where manifest public_paths / federation_paths could lower its gate. Steps are cleared on disable, uninstall and failed reload, and a per-plugin generation counter drops a stale load's late reports. Rejects pip-only Install deps, a plugin-owned route in the exemptable namespace, and auto-running on a tool call. First users: agent_browser's download-cli (pinned, SHA-256-verified) and install-chrome (#3464)Accepted
0111Zed as the operator's editor. Three surfaces: (D1) protoagent-acp, a standalone stdio ACP agent (integrations/zed-acp/, uvx-run, outside the core import graph and wheel), that is a pure A2A client of a running instance, so Zed's Agent Panel can drive any local, fleet or remote protoAgent; it maps prompt/cancel to SendStreamingMessage/CancelTask, text/reasoning to message/thought chunks, tool-call-v1 to tool_call with kind + absolute locations (follow-the-agent), and approval parks to session/request_permission; proven live against an unmodified v0.172.0 member (D2). (D3) records the missing stream data: args are an 800-char preview string, so no edit diffs, and roots need /api/fs/roots. (D4) writes stay protoAgent's (report-only), and editor-routed writes are a later opt-in HITL fs_write park. (D5) zed://file/…:line / zed://agent?prompt= deep-links and the operator MCP as a Zed context server are recorded, not built. (D6) registry path: PyPI + uvx, terminal auth method. Rejects a WASM extension (agent extensions deprecated, no UI), an in-core ACP server, and ACP-over-HTTP (Zed has no HTTP client yet). Amends ADR 0075's ACP-server framingProposed
0112Console code pane (amended 2026-09-25: an opt-in toolset, filesystem.code_pane, off by default) — a read-only navigator surface the agent points into, motivated by evidence that watching an agent work lowers comprehension (arXiv 2607.26375; Anthropic's AI-assistance coding-skills study). (D1) core code surface, File | Diff tabs + a Recent trail, opened on the dock NOT holding chat; (D2) GET /api/fs/file — the read_file fence, \n-delimited lines with endings preserved, 2 MB / 20k lines / 2k chars-per-line caps with paging, binary → metadata; (D3) GET /api/fs/diff — hardened git (no ext diff / textconv / filter driver / fsmonitor / hook / submodule recursion; all GIT_* env scrubbed), scoped to the project root, untracked files as Python-built patches, 1 MB cap, 10 s → 504; (D4) tools/fs_secrets.py secret-like-name deny list for display surfaces (403 before existence; read_file unchanged — a follow-up); (D5) show_code fs tool → a strictly validated code-ref component; (D6) console: chip, live-only auto-open on desktop, never on mobile, opt-in debounced follow mode with pin, pane-by-default links; highlighting library decided in the console PR. Amends 0035/0051/0056/0062/0086.Proposed
0113Agent pairing for remote fleet members. A hub pairs with a remote the way a phone does (ADR 0087): the remote shows a code (Settings ▸ Devices ▸ Pair an agent, or protoagent pair), and the hub claims it through the existing unauthenticated /api/pairing/claim, storing the minted per-hub device token (revocable on the remote) as the remote's bearer. (D2) agent codes are 10-char Crockford base32 (~50 bits), 5-min TTL, single-use, under the shared 5-miss lockout, which resets on each new code; the kind is fixed when the code is minted. (D4) a delegate to a remote routes through the hub's loopback proxy (fleet token → hub → paired token), so there is one token and one registry. (D5) an authenticated probe reports auth: ok|rejected. (D6) remote WebSockets are re-enabled, and the hub never lends a stored credential: it only swaps a hub-verified operator ?token=, and passes tickets through. (D8) the settings.devices flag is removed after a desktop-app test. (D9) mDNS advertises only on a reachable bind. (D10) a credential goes over plain http only to loopback or a tailnet address, or with an explicit opt-in. Rejects hub-requests/remote-approves and HTTPS discovery. Amends 0042 §I and 0087.Proposed
0115Model calls share a bounded, priority-ordered in-flight lane per endpoint and model (off by default). Nothing caps how many chat-model calls are in flight at once (graph/ has no semaphore; the per-fan-out caps don't know about each other), so under load every caller times out and retries together (the #209 review-gate incident). A new in-process limiter in graph/llm_limiter.py keyed by (base_url, model) gives each lane a bounded queue: model.max_inflight (default 0 = off), model.inflight_queue_timeout (300 s, separate from request_timeout), model.inflight_interactive_reserve (slots only interactive callers take). Waiters are served strict-priority (interactive > default > bulk) with 60 s aging, acquire OUTSIDE the stream-stall guard so wait time never counts as backend latency, and a queue timeout raises GatewayQueueTimeout (not retried, never mislabelled a stall). Per-process only (ACP coders / clawpatch / other instances stay out, D7); gateway-side limits remain the cross-process backstop. Adds queue-depth/wait Prometheus metrics + GET /api/telemetry/llm-lanes + sdk.llm_lanes(). Builds on 0006, keyed per 0106, boundary per 0025. Refs #3760.Proposed
0116A local-first data analyst: DuckDB over files in place, charts as Vega-Lite specs, and a plugin-service call seam. (D1) a data plugin in its own repo (protoLabsAI/data-plugin) — data_connect/sources/schema/query/profile/chart/export + two skills; the archetype bundle comes later. (D2) embedded DuckDB views over CSV/Parquet/JSON in place; SQLite/XLSX snapshotted to Parquet (their extensions would INSTALL from the network). (D3) read-only enforced BY THE ENGINE — a fresh in-memory connection per query with exact-file allowed_paths, enable_external_access=false, a private temp dir, extension autoload/autoinstall off, lock_configuration — plus a single-SELECT statement guard and row/time/memory caps. (D4) the scope is the operator-only data_dirs (spawns: true), campaign's upload fence (symlinks resolved first, credential names, agent home, hardlinks, too-broad roots), re-validated per query. (D5) a core vega-lite artifact kind: vendored SRI-pinned vega/vega-lite/vega-embed, a nonce CSP with no network, the CSP-safe expression interpreter (no eval), a deny-everything loader (+ usermeta.embedOptions stripped), inline-data checks at write time, themed from --pl-color-chart-* with live re-theme. (D6) plugin services — registry.register_service / sdk.service(name), rebound wholesale on reload; first service artifact.show. (D7) exports only into the agent workspace.Proposed

Part of the protoLabs autonomous development studio.