A faithful port of Claude Code's team-mode mode to the pi coding agent. Named workers are spawned as pi subprocesses by default, the coordinator ends its turn after launching, and completion arrives as a <task-notification> user-role message that wakes the coordinator event-driven — no polling, no leader subprocess.
Sibling of
pi-mono-team-mode.team-modeis leader-driven (a coordinator subprocess runs on its own task graph).team-modemaps 1:1 to Claude Code's semantics instead.
Everything below mirrors claude-code/src/ behavior (coordinator/coordinatorMode.ts, tools/AgentTool, tools/SendMessageTool, tools/Task*Tool, utils/swarm/teammatePromptAddendum.ts).
| Claude Code | team-mode |
|---|---|
Agent({ description, prompt, name?, team_name?, subagent_type?, isolation?, run_in_background? }) |
agent(...) — same schema plus optional runtime selector |
subagent({ tasks })-style fan-out |
delegate({ tasks: [...] }) |
subagent({ chain })-style sequencing |
delegate({ task, chain: [...] }) |
SendMessage({ to, message }) |
send_message(...) |
TaskStop({ task_id }) |
task_stop(...) |
TaskOutput({ task_id }) |
task_output(...) |
TaskCreate({ subject, description, activeForm?, metadata? }) |
task_create(...) |
TaskUpdate({ task_id, status?, owner?, addBlocks?, addBlockedBy?, ... }) |
task_update(...) |
TaskGet({ task_id }) |
task_get(...) |
TaskList({ status?, owner? }) |
task_list(...) |
TeamCreate / TeamDelete |
team_create / team_delete |
<task-notification> XML wakes coordinator |
Emitted via pi.sendMessage({ triggerTurn: true }) on teammate end |
Coordinator system prompt (CLAUDE_CODE_COORDINATOR_MODE=1) |
PI_TEAM_MATE_COORDINATOR=1 |
Teammate prompt addendum (TEAMMATE_SYSTEM_PROMPT_ADDENDUM) |
Prepended to every teammate's system prompt |
Task ids namespaced agent-* |
Same namespace; task_stop and send_message accept it |
Workers are event-driven, not polled. You don't write "spawn, wait, spawn the next" loops:
Turn 1 (coordinator):
agent({ description: "research auth", prompt: "..." }) → task_id: agent-r-ab1
agent({ description: "research billing", prompt: "..." })→ task_id: agent-b-c3d
"Investigating both — I'll report back." [turn ends]
...workers run in parallel; zero coordinator tokens burned...
Between turns:
<task-notification>
<task-id>agent-r-ab1</task-id>
<status>completed</status>
<summary>Agent "research auth" completed</summary>
<result>Found null pointer in src/auth/validate.ts:42...</result>
<usage><duration_ms>14230</duration_ms></usage>
</task-notification>
Turn 2 (coordinator, woken by the notification):
"Found the bug. I'll fix it."
send_message({ to: "agent-r-ab1", message: "Fix src/auth/validate.ts:42..." })
[turn ends]
For a single coherent task, the coordinator should resolve it directly without creating a TODO item. Creating exactly one task_create entry is overhead, not coordination.
For a DAG of dependent tasks, the coordinator:
- Seeds the TODO list via
task_create(each task withblocks/blockedByset viatask_update). - Spawns a worker for each immediately-unblocked task, setting
task_update({ owner })on the matching TODO. - Ends its turn.
- On each
<task-notification>, marks the TODO completed, checks what's newly unblocked, spawns the next wave.
The coordinator never waits — it runs briefly, reactively, on each notification.
pi install npm:pi-mono-team-mode
# or load locally
pi -e /path/to/pi-extensions/extensions/team-mode/index.tsSet PI_TEAM_MATE_COORDINATOR=1 on the pi session you want to act as coordinator:
PI_TEAM_MATE_COORDINATOR=1 piThis injects the coordinator system prompt at every turn via the before_agent_start hook. The prompt teaches the LLM the <task-notification> flow, the delegation discipline, and the "synthesize — don't hand off understanding" rule lifted straight from Claude Code.
Drop a markdown file into .pi/teammates/<role>.md (or .claude/teammates/<role>.md):
---
name: reviewer
description: reviews diffs for bugs and style violations
modelTier: deep
thinkingLevel: high
tools: read, bash, grep
---
You are a careful code reviewer. ...Frontmatter fields: name, description, needsWorktree, hasMemory, modelTier, thinkingLevel, tools (comma-separated). The body becomes the teammate's system prompt. The Claude Code teammate addendum (send_message instructions) is prepended automatically.
agent(...) and each delegate task accept both model and thinking (thinking_level is accepted as an alias):
agent({
description: "review diff",
prompt: "Review the current branch",
subagent_type: "reviewer",
model: "deep",
thinking: "high",
});Use @@model in the editor to delegate a prompt directly to one or more selected models:
@@claude-sonnet-4-5 review current changes
@@gpt-5.4 what do you think about the proposal above?
An @@model mention selects a worker model; it does not classify the task. The parent orchestrator interprets the request using its full conversation, generates a self-contained prompt with the relevant context, execution boundaries, and expected output, and launches the selected worker. Worker prompts omit the routing mention and prohibit recursive delegation. This keeps references such as “the last message” and “above” meaningful without treating words such as review as repository-specific instructions.
Valid thinking levels are off, minimal, low, medium, high, and xhigh. Team-mode passes the selected level to the teammate subprocess as pi --thinking <level>. Token budgets remain pi's responsibility via ~/.pi/agent/settings.json thinkingBudgets.
model-config.json can define compact role/tier defaults:
Built-in provider catalogs map xs/sm to small models, md to default models, and lg/xl to large models. You can still override providers if you want exact model IDs per tier. Legacy roleTiers, tierThinkingLevels, and roleThinkingLevels remain supported.
Thinking resolution order is: explicit tool thinking, teammate spec thinkingLevel, roles/roleTiers tier metadata (tiers[tier].thinkingLevel), a legacy :<thinking> model suffix such as gpt-5.4:high, legacy tierThinkingLevels, then defaultThinkingLevel. If none applies, pi inherits its normal default.
agent and delegate accept an explicit runtime selector:
agent({
description: "quick summary",
prompt: "Summarize README.md",
runtime: "transient",
});
delegate({
runtime: "transient",
tasks: [
{ description: "scan docs", prompt: "Find docs gaps" },
{ description: "scan tests", prompt: "Find missing coverage" },
],
});runtime: "subprocess"(default): durable, resumable workers backed bypi --session; supportssend_message, names, teams, worktree isolation, background notifications, transcripts, and persistent records.runtime: "transient": fast one-shot in-processcreateAgentSession()runs; returns output directly to the current tool call; does not create teammate records, cannot be resumed, and does not share the parent LLM context window.
Transient prompts must be fully self-contained. They only share the parent Node.js process/runtime infrastructure, not the coordinator conversation. Initial transient mode rejects isolation: "worktree", run_in_background: true, team_name, and name because those imply durable teammate semantics.
Use transient mode for quick disposable research/summarization fan-out. Use subprocess mode for implementation work, resumable teammates, background work, worktrees, or anything that needs a stable task_id for later send_message.
~/.pi/agent/extensions/team-mode/
├── model-config.json # provider/tier/role + taskCompletedHook
├── teammates/<agent-id>/record.json
├── teammates/<agent-id>/sessions/<id>.jsonl # pi --session target
├── teams/<team-id>/record.json
├── tasks/<parent-session-id>/<task-id>.json # shared TODO list
└── runtime/<parent-session-id>/index.json # teammate name → agent-id
Override with PI_TEAM_MATE_STORAGE_ROOT for tests.
Add to model-config.json:
{ "taskCompletedHook": "pnpm test --run" }When a task transitions to completed, the hook runs in the task's cwd. Non-zero exit reverts the task to failed and attaches hook output (stdout + stderr, first 8 KB) to result. 2-minute timeout. Stale PID-based lock files are auto-recovered after 10 s.
- Each task is its own file under
tasks/<parent-session-id>/<task-id>.json. task_updatetakes an exclusive filesystem lock (<task-id>.lockviaopen(..., "wx")) and bumps a CASversioncounter.- Stale locks (>10 s) are reclaimed automatically — safe across teammate subprocesses.
delegate({ tasks: [...] })runs bounded parallel workers and returns aggregated output blocks.delegate({ task, chain: [...] })runs sequential chains with{task},{previous},{chain_dir}substitution and optional innerparallelfan-out.- Per-step
outputandreadslet chain steps exchange large artifacts through files in{chain_dir}. - Parallel caps:
PI_TEAM_MATE_MAX_PARALLEL(default8) andPI_TEAM_MATE_PARALLEL_CONCURRENCY(default4). - Live TUI widget now renders a multi-line ● Agents panel (spinner, turns/tool-uses/tokens/elapsed, activity hint, queued tail).
<task-notification>messages are rendered as styled completion boxes (status glyph, counters, duration, transcript path).
/teammate list | status <name> | stop <name>/team list | create <name> | delete <id>/tasks list | show <id> | clear- Ctrl+Shift+T — show the shared task list (pi's built-in Ctrl+T toggles thinking blocks)
- Broadcast
to: "*"is not implemented. Swarm/persistent teammate pattern with mailboxes isn't needed for the one-shot worker model; eachsend_messageresumes a teammate viapi --session, but injecting into an actively-running worker turn is not supported. - Tool restriction in coordinator mode is prompt-only, not enforced. Claude Code's coordinator mode actually strips Bash/Edit/Write from the tool pool; pi's extension API doesn't expose tool removal from the main session. The coordinator prompt tells the LLM not to use those tools, but the LLM could still call them.
TaskOutputon a running worker returns the teammate's last-saved record, not live stdout streaming. Completed workers return their final message faithfully.
cd extensions/team-mode
npm test
{ "defaultTier": "md", "tiers": { "sm": { "name": "Small", "thinkingLevel": "low", "description": "Simple tasks, deterministic outputs. Use for formatting, rewriting, classification", }, "md": { "name": "Medium", "thinkingLevel": "medium", "description": "Handles moderate complexity. Use for workflows, APIs, structured tasks", }, "lg": { "name": "Large", "thinkingLevel": "high", "description": "Strong reasoning, multi-step tasks. Use for reasoning, planning, debugging, decision support", }, "xl": { "name": "Deep", "thinkingLevel": "xhigh", "description": "Near-frontier capability, complex domains. Complex planning, abstraction, ambiguous problems", }, }, "roles": { "researcher": "sm", "docs": "xs", "backend": "md", "frontend": "md", "tester": "md", "planner": "lg", "reviewer": "md", }, }