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).
| # | Title | Status |
|---|---|---|
| 0001 | Extensibility & Plugin Architecture | Accepted |
| 0002 | Reusable Subagent Workflows | Accepted |
| 0003 | Reactive Agent: Activity Thread, Event Bus & Inbound Inbox | Accepted |
| 0004 | Multi-Instance Data Scoping | Accepted |
| 0005 | Tool Pollution & Progressive Tool Disclosure | Accepted |
| 0006 | Observability & the Self-Improving Flywheel | Accepted |
| 0007 | Directory-Aware Operator Primitives (enabling a "Roxy" fork) | Accepted |
| 0008 | Sandboxing posture & NVIDIA OpenShell | Accepted |
| 0009 | The Studio control stack (goals · workflows · subagents · skills) | Accepted |
| 0010 | Headless setup & UI deployment tiers (lighter stack) | Accepted |
| 0011 | Deep-research workflow with adversarial review | Accepted |
| 0012 | Eval strategy: model-tagged tracking & model comparison | Accepted |
| 0013 | Console data layer: TanStack Query + Suspense + ErrorBoundary | Accepted |
| 0014 | A2A 0.3 → 1.0: adopt a2a-sdk + protolabs-a2a | Accepted (shipped #453) |
| 0015 | Optional native Discord surface (ingress + outbound) | Accepted (shipped as plugins/discord) |
| 0016 | In-app Discord configuration (token, admin list, live connect) | Accepted |
| 0017 | In-app Google (Gmail + Calendar) connect flow | Accepted |
| 0018 | Plugins contribute surfaces, routes & subagents | Accepted |
| 0019 | Plugins contribute config, settings & secrets | Accepted |
| 0020 | Console IA: run from Chat, manage from surfaces | Accepted |
| 0021 | Agent memory: extract, don't dump | Accepted |
| 0022 | Activity is a provenance feed, not a second chat | Accepted |
| 0023 | Decompose server.py: AppState + composition root | Accepted |
| 0024 | Spawn CLI coding agents over ACP (code_with) | Superseded by 0025 (code_with removed; ACP lives on as an acp delegate) |
| 0025 | Unified delegate registry + hot-swappable panel (delegate_to) | Accepted (complete; PR1–PR4) |
| 0026 | Plugin-contributed console surfaces (rail views + tabs) | Accepted (complete; PR1–PR3) |
| 0027 | Install plugins from a git URL (shareable plugin repos) | Accepted (sliced) |
| 0028 | Plugin-contributed goal verifiers (+ safe programmatic goals) | Accepted (sliced) |
| 0029 | A standard for communication (chat-surface) plugins | Accepted |
| 0030 | Monitor goals — cadence-evaluated, hook-reactive objectives (closes 0028 D6) | Accepted (sliced) |
| 0031 | Pluggable knowledge backend (register_knowledge_store + Protocol + selector) | Accepted |
| 0032 | Pluggable middleware (register_middleware + per-request metadata contextvar) | Accepted |
| 0033 | Pluggable agent runtime (ACP executor) — runtime≠model axis, operator-tools MCP bus, context contract + caching | Accepted |
| 0034 | Plugin UI as first-class React (Module Federation) — ui: react remotes + shared singletons + plugin-ui SDK + trust gate; Notes is the reference port | Superseded by 0038 |
| 0035 | Console layout — symmetric dual-rail (swappable surfaces), one tab style, mobile-first (bottom quick-bar + hamburger), persisted UI state (Zustand) | Accepted |
| 0036 | Unified context-menu system (right-click) — Zustand store + ContextType registry + one renderer; plugins contribute items via the plugin-ui SDK (trust-gated); replaces ad-hoc affordances | Accepted |
| 0037 | Design-system foundation — Tailwind + @protolabsai/design preset + shadcn/Radix (themed by brand tokens); supersedes 0035 S5, flips 0036 D5; shared to plugin remotes | Accepted |
| 0038 | Generative-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 Federation | Accepted |
| 0039 | Plugin 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 consumer | Accepted |
| 0040 | Plugin bundles — install a curated set of plugins as one (plugin install <url> of a bundle manifest; builds on ADR 0027 git-installable plugins) | Accepted |
| 0041 | Workspaces & tiered stores — the fleet-on-one-host model (per-agent workspace dirs + config/data scoping) | Accepted (v0.31.0) |
| 0042 | Fleet supervisor & unified console — background agents, slug-routed in-place switching (/agents/<slug>/*), per-agent layout/theme/chat | Accepted (v0.31.0) |
| 0043 | Plugin 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 + requiresPlugin | Accepted |
| 0044 | Plugin-driven console navigation — plugins contribute nav entries/surfaces into the console rails | Proposed |
| 0045 | The 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 deferrals | Accepted |
| 0046 | Pluggable 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 Menu | Proposed |
| 0047 | Layered 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 |
| 0048 | Settings 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-cut | Proposed |
| 0049 | Bundle 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 |
| 0050 | Background 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 |
| 0051 | A2A 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 0050 | Accepted |
| 0052 | User-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/0002 | Accepted |
| 0053 | wait: 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 resume | Accepted |
| 0054 | dream & 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 drop | Accepted |
| 0055 | Multi-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 seam | Accepted |
| 0056 | Unified 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 drag | Proposed |
| 0057 | Command 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) | Proposed |
| 0058 | Runtime 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-3uh | Proposed |
| 0059 | Unified 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-23a | Proposed |
| 0060 | Skills: 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 index | Accepted |
| 0061 | Frontend 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 ⌘K — core deep-links dogfooded onto it), 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 |
| 0062 | Full-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 |
| 0063 | Scoped, 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 |
| 0064 | coder — 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 #1440 | Accepted |
| 0065 | Two-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 |
| 0066 | Goal 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 |
| 0067 | Standalone 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 |
| 0068 | Developer 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 #1506 | Proposed |
| 0069 | Memory 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 |
| 0070 | Background 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 |
| 0071 | Plugin 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 #1725 | Accepted |
| 0072 | Fleet 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 |
| 0073 | Goal 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 |
| 0074 | System 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 |
| 0075 | External 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 |
| 0076 | Managed 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.md | Proposed |
| 0077 | Adversarial 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). | Proposed |
| 0078 | Fleet 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. | Proposed |
| 0080 | External 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 |
| 0081 | Self-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 |
| 0082 | Per-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 |
| 0083 | Cowork mode — a prepackaged knowledge-worker archetype for Claude Cowork graduates: cowork soul preset + one catalog row pointing at the cowork-stack bundle (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 |
| 0085 | Managed 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 |
| 0084 | Computer 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 |
| 0089 | Intra-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 |
| 0090 | Console-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 |
| 0091 | Agent 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. | Proposed |
| 0094 | Managed 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 |
| 0093 | Pip-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 |