0055 — Multi-team orchestration: federated boards over A2A (scale-out)
- Status: Accepted
- Date: 2026-06-17
- Builds on: ADR 0042 (fleet — slug-routed agents, remote members,
delegate_toover A2A, mDNS/tailnet discovery), ADR 0024/0025 (CLI coding agents over ACP + the unified delegate registry), ADR 0039 (plugin event bus), ADR 0043 (plugin consumption SDK), and theproject_boardplugin (beads-backed coding orchestration).
Context
We want one Project-Manager agent to orchestrate work across multiple project teams — many boards, each backed by its own repo, dispatching coding agents scoped to that repo, so a PM can run a portfolio of delivery teams rather than a single board bound to one repo.
Today the model is 1 board = 1 repo = 1 coder pool:
delegates(ACP spawn) is already multi-repo-capable: the registry holds many delegates with distinctworkdirs;Delegate.workdiris overridable per-call; the ACP client cache keys onworkdir(so one coder fans out to N concurrent worktrees);worktree.pyis fully repo-parameterized.project_boardis a hard singleton: a process-wide_BOARD(store.py:497,get_store()ignores its kwargs after first init), oneconfig_section→ one repo/coder/beads-db, oneBoardLoop, board tools take no board argument, and the bead carries no repo field (repo lives only on the singleton). It also auto-discovers.beads/from the process cwd, so a board pollutes whatever repo it happens to run in (the dev-env isolation gap, found 2026-06-17).
Two further constraints shaped the decision: (1) a single agent cannot hold N boards × M features in context — the data must be streamed/projected, not dumped; (2) governance ("which agent may touch which repo / secrets / budget") is a first-class requirement, not an afterthought.
Decision
Scale out, not up. Multiplicity is the fleet, not an in-process board registry:
- Each repo/team is its own protoAgent instance running a single
project_boardfor its repo (one board per instance — the singleton stays; no in-process board registry). - The PM is a portfolio orchestrator that federates over the team-agents via A2A, reusing ADR 0042's remote-member +
delegate_tospine. A "board" the PM addresses resolves to a remote team-agent, not a local store. project_boardstays single-board. We do not build aboards:list or a board registry inside the plugin. The plugin's job shrinks to "run this team's board well"; cross-team is the fleet's job.
Sub-decisions
Deterministic board pinning (P0). A board's beads location must be explicit and instance-pinned — resolved from config /
operator.project_dir/ the instance data root, never cwd auto-discovery. This makes each team-agent's board deterministically its repo's, and fixes the isolation gap. (Thedb_pathconfig must actually wire through, and the_BOARDsingleton must re-read it on reload.)Portfolio addressing. The PM treats each team-agent as a remote board. Reuse fleet discovery + the remote-member registry (ADR 0042); a board id maps to
{agent, repo}. Dispatching a feature = adelegate_to(team-agent, …)A2A call; the team-agent runs its own board loop and ships the PR. The PM does not reach into any repo directly.Context at scale = projection + deltas, never raw reads. The PM holds a portfolio rollup (per board:
{ready, in_progress, in_review, blocked}+ blocked/critical-path items only). It drills into a single board's features on demand over A2A. Team-agents push deltas (PR merged, feature blocked, coder failed) into the PM's activity feed via the event bus / A2A (ADR 0039) — streamed, bounded context. The "board = projection over beads" design makes the rollup cheap.Governance is per-instance + natural. A team-agent only has its repo, secrets, and budget — isolation by construction. The PM's authority is "which team-agents it may dispatch to" (its delegate/remote-member set). No new cross-repo trust surface in the PM process.
Plugin-SDK / ecosystem.
project_boardcomposesdelegatesvia a direct import today (loop.py:_resolve_delegatereaches intoplugins.delegates.*). Move this to the ADR 0043 consumption SDK seam (named lookup viagraph.sdk), so plugins compose through a contract, not internals. The portfolio layer is a composition of fleet × project_board × delegates — the first real multi-plugin orchestration, and the template for "how plugins tie together."
Consequences
- Leverages the existing fleet/A2A substrate — remote members, slug routing,
delegate_to, discovery, turn-finished toasts — instead of new machinery. project_boardis simplified, not expanded — the singleton is correct for one-board-per-instance; we delete the cwd-coupling, not add a registry.- Deployment model: a team-agent runs where its repo lives (or with that repo checked out + pinned); the PM runs anywhere and federates. Matches "multiple project teams" literally.
- Repo identity must live on the board/instance record (and ideally the bead) so merge reconciliation and rollups are unambiguous across team-agents.
- New work concentrates in: (P0) board pinning, (P1) the PM portfolio capability (address team-agents as boards over A2A), (P2) rollup + delta streaming, (P3) a cross-repo dependency graph (the Zenflow-class capability).
Alternatives considered
- Scale-up: an in-process board registry (
boards:list, NBeadsBoards in one PM process). Rejected: one process owns every repo's worktrees + beads (blast radius, single box), it doesn't model "teams," and it ignores the fleet spine we already built. Kept as a possible co-located optimization later, not the primary. - Hybrid (uniform local+remote board addressing). Deferred — start pure scale-out; the addressing abstraction can later admit local boards if a single agent ever needs to hold co-located ones.
Phasing
- P0 ✅ (shipped) — deterministic per-instance board pinning in
project_board(explicit beads location;db_pathwires through; per-(db,repo) store;brruns in the configured repo). Also fixed the dev-env isolation gap. projectBoard-plugin v0.19.0 (#46) + pm-stack pin (#6) + the host fix protoAgent #1105 (scoped instances now resolve installed-plugin config from the unscoped plugins dir). - P1 ✅ (shipped + live-verified) — PM portfolio capability: the
portfolioplugin (#1107) —portfolio_boards/portfolio_dispatch(A2A) /portfolio_board_read. Pure composition of fleet × delegates × project_board. Live-verified end-to-end against a real team-agent over A2A (a dispatched feature was created on the team's own isolated board and read back structured). The plugin is a standalone repo (protoLabsAI/portfolio-plugin), shipped in the pm-stack bundle alongsideproject_board— extracted from the host so the PM ecosystem (pm · project_board · portfolio) is uniformly external + composable. - P2 — rollup + delta streaming (the bounded-context layer).
Slice 1 ✅ (shipped) —
portfolio_rollup(#1110): bounded cross-board view — per-board lane counts + only the blocked/critical-path items, never raw boards.Slice 2 ✅ (shipped) — board deltas via
portfolio_diff+portfolio_watch: a PM-side pull-diff. The PM snapshots each board (feature→state/blocked) under its instance data root and reports only the transitions (merged / newly-blocked / unblocked / new);portfolio_watchseeds a baseline and hands back aschedule_taskcron so the diff runs on a schedule and arrives as a turn — the system polls for the PM, not the PM's reasoning loop.Why pull-diff, not push (decided after a substrate scout): A2A push notifications (
pushNotificationConfig) are task-scoped — they express "my dispatched task finished," not "this board changed," which is the wrong granularity. The event bus (ADR 0039) is in-process per instance with no cross-instance bridge. And remote registration is one-directional (PM→team viaremotes.json) — a team-agent has no back-reference to its PM, so a push would need new credential distribution + a team-side emit hook in the standaloneproject_boardrepo. Pull-diff reuses the slice-1 read path verbatim, auth already flows PM→team, and nothing changes on the team side. Deferred push upgrade (P2.5+): a team-side bus→/api/inboxbridge once a team↔PM handshake exists — the inbox (POST /api/inbox,priority:"now"fires a turn) is the right inbound sink, but it's a larger lift for marginal latency gain.
- P3 ✅ (slice 1 shipped) — cross-repo dependency graph + program-level sequencing:
portfolio_link+portfolio_plan(plugin v0.4.0). Cross-board edges are PM-side state (portfolio_links.json, scoped) —{from_board, from_feature, to_board, to_feature}meaning from is blocked until to is done. Features are keyed by(board, feature_id)(ids are board-local).portfolio_planfetches every board and marks each edge satisfied (the depended-on feature merged→done, the board's one Done signal), blocking, unknown (board unreachable — fail-closed, never assumed satisfied), or dangling (feature/board gone), then returnsready_to_dispatch(afromwhose every blocker is satisfied and that hasn't started)blocked. Cycles are rejected at link time (DFS). Why PM-side: beads'blocksedges are single-DB (no foreign ids), a team-agent doesn't know its dependents, and sequencing is the PM's concern — same rationale as P2. Auto-dispatch (plugin v0.5.0): a link can carry the dependent's spec (a planned dispatch — the work is held, not created on its board);portfolio_autodispatchcreates it once the blocker ships, idempotently (adispatchedflag) so it's schedulable — the PM genuinely gates cross-repo dispatch rather than relying on advisory status. Deferred: bead-side annotation so an edge travels with the work, full cycle-aware multi-hop topological sequencing, and a console graph view. Improves on the Studio comp (project-level edges, manualresolve) with feature-level edges + automatic merge detection.