- prd-swarm-multi-agent.md: mark Phase 1 and Teams (PT) as shipped in delivery table; update problem statement to reflect clineStack removal; add section 7b summarising the teams system and linking to the new reference - docs/teams.md: new full reference for .roo/teams/*.json config format, template variables, execution modes, conventions injection, orchestrator loop pattern, and the fullstack.json example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
21 KiB
PRD: Swarm / True Multi-Agent Execution in Moo Code
Status: In progress — Phase 1 shipped; Teams system shipped
Author: davincidreams
Branch: feat/swarm-prd
Created: 2026-04-25
Last updated: 2026-04-25
1. Problem Statement
Moo Code today runs tasks strictly sequentially. Phase 1 shipped — clineStack has
been replaced with tasks: Map<string, Task> and true concurrent fan-out is available via
spawn_parallel_tasks with concurrent: true. The remaining problem is the higher phases:
agent identity, cross-agent communication, permission bridging, and external process backends.
The original problem for context:
When
spawn_parallel_taskswas used, children executed one at a time via a queued delegation chain (parallelQueueonHistoryItem). The root cause was theclineStack: Task[]LIFO invariant inClineProvider, which enforced "only one task open at a time" and disposed the parent before the child started. A 4-task "parallel" job took 4× the wall-clock time of the longest subtask.
The goal of this PRD is to close the gap with claude-code's swarm/teammate architecture and deliver genuine multi-instance, multi-agent execution inside Moo Code.
2. Goals
| # | Goal | Success metric |
|---|---|---|
| G1 | True in-process concurrency | N tasks run simultaneously, wall time ≈ max(subtask time) |
| G2 | Swarm identity model | Each agent has stable ID, name, color; visible in UI |
| G3 | Agent-to-agent communication | Leader can send tasks/messages to named workers |
| G4 | Permission delegation | Worker tool-use approvals surfaced in leader's VS Code UI |
| G5 | External process backend | Spawn headless CLI workers or new VS Code windows |
| G6 | Fault isolation | One failing worker does not kill the swarm |
Non-goals (this version):
- Distributed execution across machines
- Custom agent personas / system-prompt authoring UI
- Persistent cross-session team identities (teams dissolve when the session ends)
3. Background: How claude-code Solves This
Reference implementation: C:\Users\lisam\laud code\claude-code\src\utils\swarm\
3.1 Concurrent task map
claude-code stores all running agents in a flat AppState.tasks: Record<taskId, TaskState> map.
There is no stack, no single-open invariant. Any number of InProcessTeammateTask objects can
be status: "running" simultaneously.
3.2 AsyncLocalStorage context isolation
Each in-process teammate runs inside runWithTeammateContext(context, fn) which uses Node.js
AsyncLocalStorage. Every async operation on any call stack descending from that invocation
sees the same TeammateContext — no cross-agent state bleed.
// teammateContext.ts
const teammateContextStorage = new AsyncLocalStorage<TeammateContext>()
export function runWithTeammateContext<T>(ctx: TeammateContext, fn: () => T): T {
return teammateContextStorage.run(ctx, fn)
}
Note: Moo Code Task instances are already class-isolated (each holds its own messages, API
client, history). AsyncLocalStorage is needed primarily for utility functions that look up
"current context" without being passed an explicit reference.
3.3 Two-level abort
Each teammate has:
abortController— kills the whole agent lifecyclecurrentWorkAbortController— aborts only the current LLM turn (like pressing Escape), leaving the agent alive and idle
3.4 Idle loop + mailbox polling
After finishing a turn, a worker does not complete. It enters an idle loop that polls a
file-based mailbox (~/.claude/teams/{team}/inboxes/{agent}.json) every 500 ms for:
- A new task message from the leader
- A shutdown request
- A permission response
3.5 File-based mailboxes with locking
~/.claude/teams/<teamName>/inboxes/<agentName>.json
Writes use proper-lockfile with retry logic to prevent concurrent corruption. Message types:
idle_notification, shutdown_request, task_assignment, permission_request,
permission_response.
3.6 Permission bridge
A module-level singleton (leaderPermissionBridge.ts) lets in-process workers inject a
WorkerPermissionRequest directly into the leader's ToolUseConfirm queue — the same dialog
the human uses — with a colored worker badge showing which agent is asking.
3.7 Backend registry
detectAndGetBackend():
priority 1: already inside tmux → TmuxBackend (native splits)
priority 2: iTerm2 with it2 CLI → ITermBackend (native splits)
priority 3: any tmux available → TmuxBackend (external session)
priority 4: headless / no terminal → InProcessBackend
4. Proposed Architecture
4.1 Phase 1 — Concurrent task map (foundation)
The single highest-impact change. Remove the clineStack: Task[] LIFO invariant.
// ClineProvider.ts — replace line 140
// FROM:
private clineStack: Task[] = []
// TO:
private tasks: Map<string, Task> = new Map()
private focusedTaskId?: string // which task the VS Code UI is showing
private leaderTaskId?: string // root leader of the current swarm session
Changes cascade through:
getCurrentTask()→ returnstasks.get(focusedTaskId)(UI focus)addClineToStack()→registerTask(task)— adds to map, emitsTaskFocusedremoveClineFromStack()→unregisterTask(taskId)— removes from map; if leader is unregistered, tears down swarmdelegateParentAndOpenChild()— stops disposing parent; instead setsfocusedTaskIdto child, parent remainsstatus: "delegated"but alive in mapcreateTask()— no longer enforces single-open invariant for swarm spawns
Estimated scope: ~400 LOC in ClineProvider.ts + updates to webviewMessageHandler.ts,
event emitters, and any call site that assumes clineStack.length <= 1.
4.2 Phase 2 — Swarm identity and registry
New file: src/core/swarm/SwarmRegistry.ts
export type AgentColorName = "red" | "blue" | "green" | "yellow" | "purple" | "orange" | "pink" | "cyan"
export interface AgentIdentity {
agentId: string // "<name>@<team>"
agentName: string
teamName: string
color: AgentColorName
isLeader: boolean
taskId: string // linked Moo Code task ID
}
export interface SwarmSession {
sessionId: string
leaderTaskId: string
teammates: Map<string, AgentIdentity> // keyed by agentId
taskList: string[] // unclaimed tasks (worker-pull model)
}
export class SwarmRegistry {
private sessions: Map<string, SwarmSession> = new Map()
private colorIndex = 0
private readonly COLORS: AgentColorName[] = [
"red","blue","green","yellow","purple","orange","pink","cyan"
]
createSession(leaderTaskId: string): SwarmSession { … }
assignColor(agentId: string): AgentColorName { … } // round-robin
registerTeammate(sessionId: string, identity: AgentIdentity): void { … }
unregisterTeammate(sessionId: string, agentId: string): void { … }
getSession(sessionId: string): SwarmSession | undefined { … }
destroySession(sessionId: string): void { … }
}
The registry lives on ClineProvider (one per workspace). The AgentIdentity is stored in
HistoryItem (extend schema) so it survives process restart.
4.3 Phase 3 — Mailbox communication
New file: src/core/swarm/MailboxService.ts
Two implementations behind one interface:
export interface IMailboxService {
send(to: string, msg: TeammateMessage): Promise<void>
read(agentName: string): Promise<TeammateMessage[]>
markRead(agentName: string, idx: number): Promise<void>
}
InMemoryMailbox— for in-process workers; uses aMap<agentName, TeammateMessage[]>with async mutex instead of file locks. Zero latency.FileMailbox— for cross-process/cross-window workers; stores at~/.roo/teams/<team>/inboxes/<agent>.jsonwithproper-lockfile.
Message types to implement first:
| Type | Direction | Purpose |
|---|---|---|
task_assignment |
leader → worker | Assign a new task |
idle_notification |
worker → leader | "I finished, ready for more" |
shutdown_request |
leader → worker | "Stop after current turn" |
permission_request |
worker → leader | Request tool-use approval |
permission_response |
leader → worker | Approval/rejection result |
4.4 Phase 4 — Idle worker loop
Extend Task.ts with a runSwarmWorkerLoop() method. After each LLM turn completes, instead of
marking the task done, the worker:
- Sends
idle_notificationto leader with summary. - Polls mailbox every 500 ms waiting for
task_assignmentorshutdown_request. - On
task_assignment: adds user message to conversation, re-entersrecursivelyMakeClineRequests. - On
shutdown_request: marks task completed after current turn. - On leader abort signal: hard-stops immediately.
This mirrors waitForNextPromptOrShutdown in inProcessRunner.ts:689.
Two-level abort (mirrors claude-code):
class Task {
readonly lifecycleAbortController = new AbortController() // kills whole worker
// currentRequestAbortController already exists — rename to turnAbortController
// (aborts only the ongoing LLM stream, worker stays alive)
}
4.5 Phase 5 — Permission bridge
New file: src/core/swarm/LeaderPermissionBridge.ts
export type WorkerPermissionRequest = {
requestId: string
workerTaskId: string
agentName: string
color: AgentColorName
toolName: string
toolUseId: string
input: Record<string, unknown>
description: string
onAllow(updatedInput?: Record<string, unknown>): void
onReject(reason?: string): void
}
// Module-level singleton — workers inject, leader UI reads
let pendingRequests: WorkerPermissionRequest[] = []
let notifyLeader: (() => void) | null = null
export function registerLeaderNotifier(fn: () => void): void {
notifyLeader = fn
}
export function submitPermissionRequest(req: WorkerPermissionRequest): void {
pendingRequests.push(req)
notifyLeader?.()
}
export function consumePendingRequests(): WorkerPermissionRequest[] {
const copy = [...pendingRequests]
pendingRequests = []
return copy
}
The VS Code UI renders pending worker permission requests as either:
- An inline badge in the existing tool-approval ask UI (preferred), or
- A VS Code
showInformationMessagewith Allow/Reject buttons (fallback)
4.6 Phase 6 — External process backend
For full OS-level isolation (separate heaps, separate git worktrees):
Option A — Headless CLI worker (lighter, recommended first):
moo-worker \
--agent-id researcher@my-team \
--team-name my-team \
--parent-session $SESSION_ID \
--worktree ~/.roo/worktrees/project-a1b2c3d4 \
--mailbox-dir ~/.roo/teams/my-team/inboxes \
--model claude-sonnet-4-6
The worker process reads its first task from its mailbox file and uses FileMailbox for
bidirectional communication. Mirrors claude-code's PaneBackendExecutor.spawn().
Option B — New VS Code window (full extension, heavier):
vscode.commands.executeCommand("vscode.openFolder", worktreeUri, {
forceNewWindow: true,
})
// + write initial task to mailbox before window opens
Backend registry (mirrors claude-code registry.ts):
export interface IWorkerBackend {
spawn(config: WorkerSpawnConfig): Promise<WorkerSpawnResult>
terminate(workerId: string): Promise<void>
isActive(workerId: string): Promise<boolean>
}
// Priority order:
// 1. InProcessBackend — default; no extra dependencies
// 2. CliWorkerBackend — opt-in; requires moo-worker binary
// 3. VsCodeWindowBackend — opt-in; spawns new VS Code window
5. Data Model Changes
5.1 HistoryItem extensions
// packages/types/src/history.ts — additions
agentId?: string // "<name>@<team>" if this task is a swarm worker
agentName?: string
agentColor?: string
teamName?: string
swarmSessionId?: string
isSwarmLeader?: boolean
isIdle?: boolean // worker is alive but waiting for next task
5.2 New types package exports
// packages/types/src/swarm.ts (new file)
export type AgentColorName = "red" | "blue" | "green" | "yellow" | "purple" | "orange" | "pink" | "cyan"
export interface AgentIdentity { … }
export interface SwarmSession { … }
export interface TeammateMessage { … }
export type TeammateMessageType =
| "task_assignment"
| "idle_notification"
| "shutdown_request"
| "permission_request"
| "permission_response"
5.3 New event names
// packages/types/src/events.ts — additions to RooCodeEventName
SwarmSessionStarted = "swarmSessionStarted"
SwarmSessionEnded = "swarmSessionEnded"
WorkerSpawned = "workerSpawned"
WorkerIdle = "workerIdle"
WorkerShutdown = "workerShutdown"
PermissionRequested = "permissionRequested" // worker→leader
PermissionResolved = "permissionResolved" // leader→worker
6. API surface (spawn_parallel_tasks evolution)
Today spawn_parallel_tasks runs tasks sequentially. Once Phase 1+2 land, it gains a
concurrent: true flag:
{
"tool": "spawn_parallel_tasks",
"tasks": [
{ "mode": "code", "message": "implement auth service", "worktree": "auto" },
{ "mode": "code", "message": "implement payment service", "worktree": "auto" },
{ "mode": "code", "message": "implement notification service", "worktree": "auto" }
],
"concurrent": true,
"abortOnChildFailure": false
}
When concurrent: true, all tasks start immediately against the task map (Phase 1). The parent
registers onIdle callbacks on each child and resumes only when all have called
idle_notification with a completed status.
A new spawn_swarm tool (Phase 3+) allows richer worker control:
{
"tool": "spawn_swarm",
"teamName": "feature-v2",
"workers": [
{ "name": "researcher", "mode": "architect", "color": "blue" },
{ "name": "coder-1", "mode": "code", "color": "green" },
{ "name": "reviewer", "mode": "code", "color": "yellow" }
],
"taskList": [
"Research existing auth implementations",
"Implement JWT refresh token flow",
"Review and test the JWT implementation"
]
}
Workers pull tasks from the taskList atomically; the leader monitors idle notifications and
can push new tasks dynamically.
7. UI Changes
7.1 Active workers panel
A new collapsible section in the Chat sidebar shows all running workers with their color dot, current status (running / idle), and latest tool in use. Mirrors how SubtaskRow renders child tasks today.
7.2 Worker permission badge
When a worker requests tool approval, the existing tool-ask dialog renders a colored badge
([researcher · blue]) above the file diff / bash command block.
7.3 Swarm activity log
The Output Channel gains a Moo Code Swarm channel that logs all cross-agent messages,
idle transitions, and permission events in real time.
7b. Teams System (Shipped)
The Teams system provides a lightweight, config-driven multi-agent workflow layer that sits on top of the Phase 1 concurrent task map. It is inspired by the Atlas-Agent-Teams CLI plugin.
How it works
- A team is defined in
.roo/teams/<slug>.json. The file describes an ordered list of phases; each phase runs one or more specialist agents (each in a Roo-Code mode). - An orchestrator agent (any mode with
run_team_phaseavailable) reads the config withread_file, then callsrun_team_phaseonce per phase in order. - The tool handles concurrent vs sequential dispatch, conventions injection, and template
interpolation — the orchestrator only needs to pass
taskand accumulatedcontext.
Key components
| File | Role |
|---|---|
packages/types/src/team.ts |
TeamConfig, TeamPhase, TeamAgentSpec interfaces |
src/services/teams/TeamsManager.ts |
Scans .roo/teams/*.json; caches configs by slug |
src/core/tools/RunTeamPhaseTool.ts |
Tool implementation |
src/core/prompts/tools/native-tools/run_team_phase.ts |
LLM tool schema |
.roo/teams/fullstack.json |
Sample 3-phase full-stack team |
Config format (summary)
{
"slug": "my-team",
"name": "My Team",
"orchestratorMode": "orchestrator",
"conventions": ".roo/teams/conventions/my-team.md",
"phases": [
{
"name": "discovery",
"concurrent": true,
"requireApproval": true,
"agents": [
{
"mode": "architect",
"role": "Backend Architect",
"instruction": "Analyze backend requirements for: {{task}}\n\nPrior context: {{context}}"
}
]
}
]
}
Template variables available in instruction and worktree fields: {{task}}, {{context}},
{{phase}}, {{team}}.
See docs/teams.md for the full reference.
8. Phased Delivery
| Phase | Name | Key deliverables | Status | Estimated effort |
|---|---|---|---|---|
| P1 | Concurrent task map | Replace clineStack with tasks: Map; remove single-open invariant; concurrent: true on spawn_parallel_tasks |
Shipped | Large (1–2 weeks) |
| PT | Teams system | run_team_phase tool; TeamsManager; .roo/teams/*.json config format; conventions injection; abortOnChildFailure |
Shipped | Medium (2–3 days) |
| P2 | Swarm identity | SwarmRegistry, AgentIdentity, color assignment; UI color dots |
Planned | Medium (3–5 days) |
| P3 | In-process mailbox | InMemoryMailbox, idle loop in Task, idle_notification / task_assignment messages |
Planned | Medium (3–5 days) |
| P4 | Permission bridge | LeaderPermissionBridge, worker badge in tool-ask UI |
Planned | Medium (3–5 days) |
| P5 | File mailbox | FileMailbox with lockfile; cross-process swarm works |
Planned | Small (2–3 days) |
| P6 | External backends | CliWorkerBackend; moo-worker entry point; spawn_swarm tool |
Planned | Large (1–2 weeks) |
9. Risks and Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| Removing single-open invariant breaks webview state assumptions | High | Audit all getCurrentTask() call sites; add getFocusedTask() as new name to make call sites obvious |
Concurrent tasks racing on taskHistoryStore writes |
Medium | Serialize writes via async mutex (one per taskId); reads are safe (immutable snapshots) |
| VS Code extension host is single-threaded — "concurrency" is cooperative | Medium | Tasks yield naturally at await points; long CPU-bound turns block others. Acceptable for I/O-bound LLM tasks |
| File mailbox lock contention with many workers | Low | In-process workers use InMemoryMailbox; file mailbox only for cross-process |
| External CLI worker binary distribution | Medium | Ship as optional; document manual install; in-process is always available |
10. Prior Art / References
- claude-code swarm source:
C:\Users\lisam\laud code\claude-code\src\utils\swarm\inProcessRunner.ts— idle loop, permission bridgeteammateContext.ts— AsyncLocalStorage patternbackends/registry.ts— pluggable backend selectionteammateMailbox.ts— file-based mailbox with lockfileleaderPermissionBridge.ts— module-level permission injection
- Current Moo Code parallel implementation:
src/core/webview/ClineProvider.ts—delegateParentAndOpenChild,reopenParentFromDelegationsrc/core/tools/SpawnParallelTasksTool.tspackages/types/src/history.ts—parallelQueue,parallelResults
- Previous improvements (merged in
moo-code-standalone):- Worktree orphan detection
abortOnChildFailureflaggetParallelTaskStatus()introspection API- Telemetry for worktree/parallel events