# pi-agent-teams

An experimental [Pi](https://pi.dev) extension that brings [Claude Code agent teams](https://code.claude.com/docs/en/agent-teams) to Pi. Spawn teammates, share a task list, and coordinate work across multiple Pi sessions.

> **Status:** MVP (command-driven + status widget). See [`docs/claude-parity.md`](docs/claude-parity.md) for the full roadmap.

## Features

Core agent-teams primitives, matching Claude's design:

- **Shared task list** — file-per-task on disk with three states (pending / in-progress / completed) and dependency tracking so blocked tasks stay blocked until their prerequisites finish.
- **Auto-claim** — idle teammates automatically pick up the next unassigned, unblocked task. No manual dispatching required (disable with `PI_TEAMS_DEFAULT_AUTO_CLAIM=0`).
- **Direct messages and broadcast** — send a message to one teammate or all of them at once, via file-based mailboxes. Urgent messages can interrupt active turns via steering.
- **Graceful lifecycle** — spawn, stop, shutdown (with handshake), or kill teammates. The leader tracks who's online, idle, or streaming.
- **LLM-callable teams tool** — the model can spawn teammates, delegate tasks, mutate task assignment/status/dependencies, message teammates, and run lifecycle actions in tool calls (no slash commands needed).
- **Team done + cleanup** — `/team done` ends a run (stops teammates, hides the widget, notifies with a summary); the widget auto-detects when all tasks are complete and shows a hint. `/team cleanup` tears down artifacts afterward.

Additional Pi-specific capabilities:

- **Git worktrees** — optionally give each teammate its own worktree so they work on isolated branches without conflicting edits.
- **Session branching** — clone the leader's conversation context into a teammate so it starts with full awareness of the work so far, instead of from scratch.
- **Completion notifications** — when a teammate finishes or fails a task, the leader LLM receives a structured `[Team]` message with task ID, subject, result summary, and progress counters so it can orchestrate autonomously without human intervention. When quality-gate hooks are active, the message warns that task states may still change.
- **Hooks / quality gates** — optional leader-side hooks on idle / task completion to run scripts (opt-in).

## UI style (terminology + naming)

Built-in styles:

- **normal** (default): "Team leader" + "Teammate <name>" (spawn requires explicit name)
- **soviet**: "Chairman" + "Comrade <name>" (spawn can auto-pick names)
- **pirate**: "Captain" + "Matey <name>" (spawn can auto-pick names)

Configure via:
- env: `PI_TEAMS_STYLE=<name>`
- command: `/team style <name>` (see: `/team style list`)

### Custom styles

You can add your own styles by creating JSON files under:

- `~/.pi/agent/teams/_styles/<style>.json`

The file can override strings and naming rules.

Strings include both terminology **and lifecycle copy**, e.g. `killedVerb`, `shutdownRequestedVerb`, `shutdownCompletedVerb`, `shutdownRefusedVerb`, `abortRequestedVerb`, plus templates like `teamEndedAllStopped`.

Example:

```json
{
  "extends": "pirate",
  "strings": {
    "memberTitle": "Deckhand",
    "memberPrefix": "Deckhand "
  },
  "naming": {
    "requireExplicitSpawnName": false,
    "autoNameStrategy": { "kind": "pool", "pool": ["pegleg", "parrot"], "fallbackBase": "deckhand" }
  }
}
```

## Install

**Option A — install from npm:**

```bash
pi install npm:@tmustier/pi-agent-teams
```

**Option B — load directly (dev):**

```bash
pi -e ~/projects/pi-agent-teams/extensions/teams/index.ts
```

**Option C — install from a local folder:**

```bash
pi install ~/projects/pi-agent-teams
```

Then run `pi` normally; the extension auto-discovers.

Verify with `/team id` — it should print the current team info.

## Quick start

The fastest way to get going is `/swarm`:

```
/swarm build the auth module               # agent spawns a team and coordinates the work
/swarm                                     # agent asks you what to do, then swarms on it
```

Or drive it manually:

```
/team spawn alice                          # spawn a teammate (fresh session, shared workspace)
/team spawn bob branch worktree            # spawn with leader context + isolated worktree

/team attach list                          # discover existing teams under ~/.pi/agent/teams
/team attach <teamId> [--claim]            # attach this session to an existing team workspace (force takeover with --claim)
/team detach                               # return to this session's own team

/team task add alice: Fix failing tests    # create a task and assign it to alice
/team task add Refactor auth module        # unassigned — auto-claimed by next idle teammate

/team dm alice Check the edge cases too    # direct message
/team broadcast Wrapping up soon           # message everyone

/tw                                        # open the interactive widget panel

/team done                                 # end run: stop teammates + hide widget
/team done --force                         # end run even with in-progress tasks

/team shutdown alice                       # graceful shutdown (handshake)
/team shutdown                             # stop all teammates (leader session remains active)
/team cleanup                              # remove team artifacts (worktrees, branches, team dir)
/team gc --dry-run                         # preview stale team dirs that would be removed
```

Or let the model drive it with the delegate tool:

```json
{
  "action": "delegate",
  "contextMode": "branch",
  "workspaceMode": "worktree",
  "model": "openai-codex/gpt-5.1-codex-mini",
  "thinking": "high",
  "teammates": ["alice", "bob"],
  "tasks": [
    { "text": "Fix failing unit tests" },
    { "text": "Refactor auth module" }
  ]
}
```

### Teams tool action reference (agent-run)

| Action | Required fields | Purpose |
| --- | --- | --- |
| `delegate` | `tasks` | Spawn teammates as needed and create/assign tasks. |
| `task_assign` | `taskId`, `assignee` | Assign/reassign a task owner. |
| `task_unassign` | `taskId` | Clear owner (resets to pending for non-completed tasks). |
| `task_set_status` | `taskId`, `status` | Set status to `pending`, `in_progress`, or `completed`. |
| `task_dep_add` | `taskId`, `depId` | Add dependency edge (`taskId` depends on `depId`). |
| `task_dep_rm` | `taskId`, `depId` | Remove dependency edge. |
| `task_dep_ls` | `taskId` | Inspect dependency/block graph for one task. |
| `message_dm` | `name`, `message` | Send mailbox DM to one teammate. Set `urgent=true` to interrupt their active turn. |
| `message_broadcast` | `message` | Send mailbox message to all discovered workers. Set `urgent=true` to interrupt active turns. |
| `message_steer` | `name`, `message` | Send steer instruction to a running RPC teammate. |
| `member_spawn` | `name` | Spawn one teammate (supports context/workspace/model/thinking/plan options). |
| `member_status` | optional `name` | Real-time worker status: activity, time in state, stall detection, tool use, tokens, last message. Omit name for all-worker summary. |
| `member_shutdown` | `name` or `all=true` | Request graceful shutdown via mailbox handshake. |
| `member_kill` | `name` | Force-stop one RPC teammate and unassign active tasks. |
| `member_prune` | _(none)_ | Mark stale non-RPC workers offline (`all=true` to force). |
| `team_done` | _(none)_ | End team run: stop all teammates, mark workers offline, hide widget (`all=true` to force with in-progress tasks). |
| `plan_approve` | `name` | Approve pending plan for a plan-required teammate. |
| `plan_reject` | `name` | Reject pending plan (`feedback` optional). |
| `hooks_policy_get` | _(none)_ | Read team hooks policy (configured + effective with env fallback). |
| `hooks_policy_set` | one or more of: `hookFailureAction`, `hookMaxReopensPerTask`, `hookFollowupOwner` | Update team hooks policy at runtime (`hooksPolicyReset=true` clears team overrides first). |
| `model_policy_get` | _(none)_ | Inspect teammate model policy and default inheritance behavior for the current leader model. |
| `model_policy_check` | optional `model` | Validate a model override before spawning (`<provider>/<modelId>` or `<modelId>`). |

Example calls:

```json
{ "action": "task_assign", "taskId": "12", "assignee": "alice" }
{ "action": "task_dep_add", "taskId": "12", "depId": "7" }
{ "action": "message_broadcast", "message": "Sync: finishing this milestone" }
{ "action": "message_dm", "name": "alice", "message": "Stop using lib X, use Y instead", "urgent": true }
{ "action": "member_kill", "name": "alice" }
{ "action": "plan_approve", "name": "alice" }
{ "action": "hooks_policy_get" }
{ "action": "hooks_policy_set", "hookFailureAction": "reopen_followup", "hookMaxReopensPerTask": 2, "hookFollowupOwner": "member" }
{ "action": "model_policy_get" }
{ "action": "team_done" }
{ "action": "model_policy_check", "model": "openai-codex/gpt-5.1-codex-mini" }
```

## Commands

### Shortcuts

| Command | Description |
| --- | --- |
| `/swarm [task]` | Tell the agent to spawn a team and work on a task |
| `/tw` | Open the interactive widget panel |
| `/team-widget` | Open the interactive widget panel (alias for `/tw`) |

### Team management

All management commands live under `/team`.

| Command | Description |
| --- | --- |
| `/team spawn <name> [fresh\|branch] [shared\|worktree] [plan] [--model <provider>/<modelId>] [--thinking <level>]` | Start a teammate |
| `/team list` | List teammates and their status |
| `/team status [name]` | Real-time worker state: stall detection, time in state, activity (omit name for summary) |
| `/team panel` | Interactive widget panel (same as `/tw`) |
| `/team attach list` | Discover existing team workspaces under `<teamsRoot>` |
| `/team attach <teamId> [--claim]` | Attach this session to an existing team workspace (`--claim` force-takes over an active claim) |
| `/team detach` | Return to this session's own team workspace |
| `/team style` | Show current style + usage |
| `/team style list` | List available styles (built-in + custom) |
| `/team style init <name> [extends <base>]` | Create a custom style template under `~/.pi/agent/teams/_styles/` |
| `/team style <name>` | Set style (built-in or custom) |
| `/team send <name> <msg>` | Send a prompt over RPC |
| `/team steer <name> <msg>` | Redirect an in-flight run |
| `/team dm <name> [--urgent] <msg>` | Send a mailbox message (`--urgent` interrupts active turns) |
| `/team broadcast [--urgent] <msg>` | Message all teammates (`--urgent` interrupts active turns) |
| `/team stop <name> [reason]` | Abort current work (resets task to pending) |
| `/team shutdown <name> [reason]` | Graceful shutdown (handshake) |
| `/team shutdown` | Stop all teammates (RPC + best-effort manual) (leader session remains active) |
| `/team prune [--all]` | Mark stale manual teammates offline (hides them in widget) |
| `/team kill <name>` | Force-terminate |
| `/team done [--force]` | End run: stop teammates + hide widget (auto-detects when all tasks complete) |
| `/team cleanup [--force]` | Delete team artifacts (worktrees, branches, team dir) |
| `/team gc [--dry-run] [--force] [--max-age-hours=N]` | Garbage-collect stale team dirs older than N hours (default: 24) |
| `/team id` | Print team/task-list IDs and paths |
| `/team env <name>` | Print env vars to start a manual teammate |

Model inheritance note:

- If the leader is running a deprecated model id (e.g. Sonnet 4 non-4.5 variants), teammates will **not** inherit that id by default.
- Explicit deprecated `--model` overrides are rejected.
- Agents can introspect/check this at runtime via `teams` actions: `{ "action": "model_policy_get" }` and `{ "action": "model_policy_check", "model": "..." }`.

### Policy visibility

Both the persistent widget and the interactive panel (`/tw`) show a compact policy
summary below the header:

- **Hooks**: on/off status, `failureAction`, `maxReopensPerTask`, `followupOwner` (effective values from team config + env fallback)
- **Model**: leader model with deprecation warning when applicable, teammate selection source (`inherit`/`override`/`default`)

These values reflect the same resolution as the `hooks_policy_get` and `model_policy_get`
tool actions, but are continuously visible — no extra tool calls needed.

### Ergonomic worker status

The widget and panel show real-time worker state at a glance:

- **Time in state**: how long a worker has been in its current status (e.g. `3m12s`)
- **Stall detection**: when a streaming worker hasn't emitted any agent event for > 5 minutes, status changes to `⚠ stalled` (configurable via `PI_TEAMS_STALL_THRESHOLD_MS`)
- **Last message summary**: most recent assistant text (first 80–100 chars) visible in the panel's selected-worker detail section
- **Model per worker**: shown in the panel detail view when available
- **Current activity**: tool verb (e.g. `running…`, `editing…`) displayed inline

The `member_status` tool action provides the same information programmatically for agent-driven orchestration — no need to parse JSONL files or check file modification times.

### Panel shortcuts (`/tw` / `/team panel`)

- `↑/↓` or `w/s`: select teammate / scroll transcript
- `1..9`: jump directly to teammate in overview
- `enter`: open selected teammate transcript (shows tool args inline: file paths, commands, patterns; errors marked with ✗)
- `t` or `shift+t`: open selected teammate task list (task-centric view with deps/blocks); in task view, toggle back (`esc`/`t`/`shift+t`)
- task view: `c` complete, `p` pending, `i` in-progress, `u` unassign, `r` reassign selected task
- `m` or `d`: compose message to selected teammate
- `a`: request abort
- `k`: kill (SIGTERM)
- `esc`: back/close panel
- attached mode shows a banner (`attached: ...`) with `/team detach` hint

### Tasks

| Command | Description |
| --- | --- |
| `/team task add <text>` | Create a task (prefix with `name:` to assign) |
| `/team task assign <id> <agent>` | Assign a task |
| `/team task unassign <id>` | Remove assignment |
| `/team task list` | Show tasks with status, deps, blocks |
| `/team task show <id>` | Full description + result |
| `/team task dep add <id> <depId>` | Add a dependency |
| `/team task dep rm <id> <depId>` | Remove a dependency |
| `/team task dep ls <id>` | Show deps and blocks |
| `/team task clear [completed\|all]` | Delete task files |

## Configuration

| Environment variable | Purpose | Default |
| --- | --- | --- |
| `PI_TEAMS_ROOT_DIR` | Storage root (absolute or relative to `~/.pi/agent`) | `~/.pi/agent/teams` |
| `PI_TEAMS_DEFAULT_AUTO_CLAIM` | Whether spawned teammates auto-claim tasks | `1` (on) |
| `PI_TEAMS_STYLE` | UI style id (built-in: `normal`, `soviet`, `pirate`, or custom) | `normal` |
| `PI_TEAMS_HOOKS_ENABLED` | Enable leader-side hooks/quality gates | `0` (off) |
| `PI_TEAMS_HOOKS_DIR` | Hooks directory (absolute or relative to `PI_TEAMS_ROOT_DIR`) | `<teamsRoot>/_hooks` |
| `PI_TEAMS_HOOK_TIMEOUT_MS` | Hook execution timeout (ms) | `60000` |
| `PI_TEAMS_HOOKS_FAILURE_ACTION` | Hook-failure policy: `warn`, `followup`, `reopen`, `reopen_followup` | `warn` |
| `PI_TEAMS_HOOKS_MAX_REOPENS_PER_TASK` | Reopen cap per task when failure action includes `reopen` (`0` disables auto-reopen) | `3` |
| `PI_TEAMS_HOOKS_FOLLOWUP_OWNER` | Follow-up owner policy: `member`, `lead`, `none` | `member` |
| `PI_TEAMS_HOOKS_CREATE_TASK_ON_FAILURE` | Legacy shortcut for `PI_TEAMS_HOOKS_FAILURE_ACTION=followup` | `0` (off) |
| `PI_TEAMS_STALL_THRESHOLD_MS` | Threshold (ms) before a streaming worker with no events is flagged as "stalled" | `300000` (5 min) |

## Storage layout

```
<teamsRoot>/<teamId>/
  config.json                          # team metadata + members
  tasks/<taskListId>/
    1.json, 2.json, ...                # one file per task
    .highwatermark                      # next task ID
  mailboxes/<namespace>/inboxes/
    <agent>.json                        # per-agent inbox
  sessions/                             # teammate session files
  worktrees/<agent>/                    # git worktrees (when enabled)

<teamsRoot>/_hooks/
  on_idle.{js,sh}                       # optional hook (see below)
  on_task_completed.{js,sh}             # optional quality gate
  on_task_failed.{js,sh}                # optional hook
```

## Hooks / quality gates (optional)

Enable hooks:

```bash
export PI_TEAMS_HOOKS_ENABLED=1
```

Then create hook scripts under:

- `<teamsRoot>/_hooks/` (default: `~/.pi/agent/teams/_hooks/`)

Recognized hook names:

- `on_idle.(js|mjs|sh)`
- `on_task_completed.(js|mjs|sh)`
- `on_task_failed.(js|mjs|sh)`

Hooks run with working directory = the **leader session cwd** and receive context via env vars:

- `PI_TEAMS_HOOK_EVENT`
- `PI_TEAMS_HOOK_CONTEXT_VERSION` (currently `1`)
- `PI_TEAMS_HOOK_CONTEXT_JSON` (stable JSON payload for agent scripts)
- `PI_TEAMS_TEAM_ID`, `PI_TEAMS_TEAM_DIR`, `PI_TEAMS_TASK_LIST_ID`
- `PI_TEAMS_STYLE`
- `PI_TEAMS_MEMBER`
- `PI_TEAMS_TASK_ID`, `PI_TEAMS_TASK_SUBJECT`, `PI_TEAMS_TASK_OWNER`, `PI_TEAMS_TASK_STATUS`

See [`docs/hook-contract.md`](docs/hook-contract.md) for the full versioned contract, JSON schema, and compatibility policy.

Hook policy can be controlled by agents at runtime via `teams` tool actions:

- `{ "action": "hooks_policy_get" }`
- `{ "action": "hooks_policy_set", ... }`

Team-level policy in `config.json` overrides env defaults for that team.

Hook failure policy (for `task_completed` / `task_failed` hooks):

```bash
# default behavior: notify + annotate task metadata
export PI_TEAMS_HOOKS_FAILURE_ACTION=warn

# create follow-up remediation task
export PI_TEAMS_HOOKS_FAILURE_ACTION=followup

# reopen completed task to pending (re-blocks downstream dependencies)
export PI_TEAMS_HOOKS_FAILURE_ACTION=reopen

# both reopen and create a follow-up task
export PI_TEAMS_HOOKS_FAILURE_ACTION=reopen_followup

# safety cap for auto-reopen loops (0 = disable auto-reopen)
export PI_TEAMS_HOOKS_MAX_REOPENS_PER_TASK=3

# owner for auto-created follow-up tasks
# member (default), lead, or none
export PI_TEAMS_HOOKS_FOLLOWUP_OWNER=member
```

Agent-first intent:

- Hook failures are remediated by agents (reopen/follow-up/assignment + teammate notification).
- The user should not need to manually clear task gate state.

Legacy shortcut still supported:

```bash
export PI_TEAMS_HOOKS_CREATE_TASK_ON_FAILURE=1
```

## Development

### Quality gate

```bash
npm run check
```

Runs strict TypeScript typechecking (`npm run typecheck`) and ESLint (`npm run lint`).

### Smoke test (no API keys)

```bash
npm run smoke-test
# or: npx tsx scripts/smoke-test.mts
```

Filesystem-level smoke test of the task store, mailbox, team config, and protocol parsers.

### E2E RPC test (spawns pi + one teammate)

```bash
node scripts/e2e-rpc-test.mjs
```

Starts a leader in RPC mode, spawns a teammate, runs a shutdown handshake, verifies cleanup. Sets `PI_TEAMS_ROOT_DIR` to a temp directory so nothing touches `~/.pi/agent/teams`.

### Integration: hooks remediation loop

```bash
npm run integration-hooks-remediation-test
```

Deterministic leader-side integration flow that verifies failed `on_task_completed` hook handling end-to-end:

- task is reopened when policy includes `reopen`
- follow-up task is created/assigned when policy includes `followup`
- remediation mailbox nudge is emitted for the responsible teammate

### Integration: cleanup and garbage collection

```bash
npm run integration-cleanup-test
```

Tests worktree/branch cleanup lifecycle, full team directory removal, GC age/activity filtering, and filesystem fallback when no git context is available.

### tmux dogfooding

```bash
./scripts/start-tmux-team.sh pi-teams alice bob
tmux attach -t pi-teams
```

Starts a leader + one tmux window per teammate for interactive testing.

## License

MIT (see [`LICENSE`](LICENSE)).
