@tmustier/pi-agent-teams 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/docs/claude-parity.md +109 -0
- package/docs/field-notes-teams-setup.md +105 -0
- package/extensions/teams/README.md +23 -0
- package/extensions/teams/cleanup.ts +31 -0
- package/extensions/teams/fs-lock.ts +71 -0
- package/extensions/teams/index.ts +18 -0
- package/extensions/teams/leader.ts +1640 -0
- package/extensions/teams/mailbox.ts +106 -0
- package/extensions/teams/paths.ts +22 -0
- package/extensions/teams/task-store.ts +529 -0
- package/extensions/teams/tasks.ts +95 -0
- package/extensions/teams/team-config.ts +228 -0
- package/extensions/teams/teammate-rpc.ts +203 -0
- package/extensions/teams/worker.ts +488 -0
- package/extensions/teams/worktree.ts +106 -0
- package/package.json +29 -0
- package/scripts/e2e-rpc-test.mjs +277 -0
- package/scripts/smoke-test.mjs +199 -0
- package/scripts/start-tmux-team.sh +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Mustier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# pi-agent-teams
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
> **Status:** MVP (command-driven + status widget). See [`docs/claude-parity.md`](docs/claude-parity.md) for the full roadmap.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
Core agent-teams primitives, matching Claude's design:
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- **Auto-claim** — idle teammates automatically pick up the next unassigned, unblocked task. No manual dispatching required (disable with `PI_TEAMS_DEFAULT_AUTO_CLAIM=0`).
|
|
13
|
+
- **Direct messages and broadcast** — send a message to one teammate or all of them at once, via file-based mailboxes.
|
|
14
|
+
- **Graceful lifecycle** — spawn, stop, shutdown (with handshake), or kill teammates. The leader tracks who's online, idle, or streaming.
|
|
15
|
+
- **LLM-callable delegate tool** — the model can spawn teammates and create/assign tasks in a single tool call, no slash commands needed.
|
|
16
|
+
- **Team cleanup** — tear down all team artifacts (tasks, mailboxes, sessions, worktrees) when you're done.
|
|
17
|
+
|
|
18
|
+
Additional Pi-specific capabilities:
|
|
19
|
+
|
|
20
|
+
- **Git worktrees** — optionally give each teammate its own worktree so they work on isolated branches without conflicting edits.
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
**Option A — install from npm:**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pi install npm:@tmustier/pi-agent-teams
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Option B — load directly (dev):**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pi -e ~/projects/pi-agent-teams/extensions/teams/index.ts
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Option C — install from a local folder:**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pi install ~/projects/pi-agent-teams
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then run `pi` normally; the extension auto-discovers.
|
|
44
|
+
|
|
45
|
+
Verify with `/team id` — it should print the current team info.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
# In a Pi session with the extension loaded:
|
|
51
|
+
|
|
52
|
+
/team spawn alice # spawn a teammate (fresh session, shared workspace)
|
|
53
|
+
/team spawn bob branch worktree # spawn with leader context + isolated worktree
|
|
54
|
+
|
|
55
|
+
/team task add alice: Fix failing tests # create a task and assign it to alice
|
|
56
|
+
/team task add Refactor auth module # unassigned — auto-claimed by next idle teammate
|
|
57
|
+
|
|
58
|
+
/team dm alice Check the edge cases too # direct message
|
|
59
|
+
/team broadcast Wrapping up soon # message everyone
|
|
60
|
+
|
|
61
|
+
/team shutdown alice # graceful shutdown (handshake)
|
|
62
|
+
/team cleanup # remove team artifacts when done
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or let the model drive it with the delegate tool:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"action": "delegate",
|
|
70
|
+
"contextMode": "branch",
|
|
71
|
+
"workspaceMode": "worktree",
|
|
72
|
+
"teammates": ["alice", "bob"],
|
|
73
|
+
"tasks": [
|
|
74
|
+
{ "text": "Fix failing unit tests" },
|
|
75
|
+
{ "text": "Refactor auth module" }
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
All commands live under `/team`.
|
|
83
|
+
|
|
84
|
+
### Teammates
|
|
85
|
+
|
|
86
|
+
| Command | Description |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `/team spawn <name> [fresh\|branch] [shared\|worktree]` | Start a teammate |
|
|
89
|
+
| `/team list` | List teammates and their status |
|
|
90
|
+
| `/team send <name> <msg>` | Send a prompt over RPC |
|
|
91
|
+
| `/team steer <name> <msg>` | Redirect an in-flight run |
|
|
92
|
+
| `/team dm <name> <msg>` | Send a mailbox message |
|
|
93
|
+
| `/team broadcast <msg>` | Message all teammates |
|
|
94
|
+
| `/team stop <name> [reason]` | Abort current work (resets task to pending) |
|
|
95
|
+
| `/team shutdown <name> [reason]` | Graceful shutdown (handshake) |
|
|
96
|
+
| `/team shutdown` | Shutdown leader + all teammates |
|
|
97
|
+
| `/team kill <name>` | Force-terminate |
|
|
98
|
+
| `/team cleanup [--force]` | Delete team artifacts |
|
|
99
|
+
| `/team id` | Print team/task-list IDs and paths |
|
|
100
|
+
| `/team env <name>` | Print env vars to start a manual worker |
|
|
101
|
+
|
|
102
|
+
### Tasks
|
|
103
|
+
|
|
104
|
+
| Command | Description |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `/team task add <text>` | Create a task (prefix with `name:` to assign) |
|
|
107
|
+
| `/team task assign <id> <agent>` | Assign a task |
|
|
108
|
+
| `/team task unassign <id>` | Remove assignment |
|
|
109
|
+
| `/team task list` | Show tasks with status, deps, blocks |
|
|
110
|
+
| `/team task show <id>` | Full description + result |
|
|
111
|
+
| `/team task dep add <id> <depId>` | Add a dependency |
|
|
112
|
+
| `/team task dep rm <id> <depId>` | Remove a dependency |
|
|
113
|
+
| `/team task dep ls <id>` | Show deps and blocks |
|
|
114
|
+
| `/team task clear [completed\|all]` | Delete task files |
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
| Environment variable | Purpose | Default |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| `PI_TEAMS_ROOT_DIR` | Storage root (absolute or relative to `~/.pi/agent`) | `~/.pi/agent/teams` |
|
|
121
|
+
| `PI_TEAMS_DEFAULT_AUTO_CLAIM` | Whether spawned teammates auto-claim tasks | `1` (on) |
|
|
122
|
+
|
|
123
|
+
## Storage layout
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
<teamsRoot>/<teamId>/
|
|
127
|
+
config.json # team metadata + members
|
|
128
|
+
tasks/<taskListId>/
|
|
129
|
+
1.json, 2.json, ... # one file per task
|
|
130
|
+
.highwatermark # next task ID
|
|
131
|
+
mailboxes/<namespace>/inboxes/
|
|
132
|
+
<agent>.json # per-agent inbox
|
|
133
|
+
sessions/ # teammate session files
|
|
134
|
+
worktrees/<agent>/ # git worktrees (when enabled)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
### Smoke test (no API keys)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
node scripts/smoke-test.mjs
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Filesystem-level test of the task store, mailbox, and team config.
|
|
146
|
+
|
|
147
|
+
### E2E RPC test (spawns pi + one teammate)
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
node scripts/e2e-rpc-test.mjs
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Starts a leader in RPC mode, spawns a worker, runs a shutdown handshake, verifies cleanup. Sets `PI_TEAMS_ROOT_DIR` to a temp directory so nothing touches `~/.pi/agent/teams`.
|
|
154
|
+
|
|
155
|
+
### tmux dogfooding
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
./scripts/start-tmux-team.sh pi-teams alice bob
|
|
159
|
+
tmux attach -t pi-teams
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Starts a leader + one tmux window per worker for interactive testing.
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT (see [`LICENSE`](LICENSE)).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Claude Agent Teams parity roadmap (pi-agent-teams)
|
|
2
|
+
|
|
3
|
+
Last updated: 2026-02-07
|
|
4
|
+
|
|
5
|
+
This document tracks **feature parity gaps** between:
|
|
6
|
+
|
|
7
|
+
- Claude Code **Agent Teams** (official docs)
|
|
8
|
+
- https://code.claude.com/docs/en/agent-teams#control-your-agent-team
|
|
9
|
+
- https://code.claude.com/docs/en/interactive-mode#task-list
|
|
10
|
+
|
|
11
|
+
…and this repository’s implementation:
|
|
12
|
+
|
|
13
|
+
- `pi-agent-teams` (Pi extension)
|
|
14
|
+
|
|
15
|
+
## Scope / philosophy
|
|
16
|
+
|
|
17
|
+
- Target the **same coordination primitives** as Claude Teams:
|
|
18
|
+
- shared task list
|
|
19
|
+
- mailbox messaging
|
|
20
|
+
- long-lived teammates
|
|
21
|
+
- Prefer **inspectable, local-first artifacts** (files + lock files).
|
|
22
|
+
- Avoid guidance that bypasses Claude feature gating; we only document behavior.
|
|
23
|
+
- Accept that some Claude UX (terminal keybindings + split-pane integration) may not be achievable in Pi without deeper TUI/terminal integration.
|
|
24
|
+
|
|
25
|
+
## Parity matrix (docs-oriented)
|
|
26
|
+
|
|
27
|
+
Legend: ✅ implemented • 🟡 partial • ❌ missing
|
|
28
|
+
|
|
29
|
+
| Area | Claude docs behavior | Pi Teams status | Notes / next step | Priority |
|
|
30
|
+
| --- | --- | --- | --- | --- |
|
|
31
|
+
| Enablement | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` + settings | N/A | Pi extension is always available when installed/loaded. | — |
|
|
32
|
+
| Team config | `~/.claude/teams/<team>/config.json` w/ members | ✅ | Implemented via `extensions/teams/team-config.ts` (stored under `~/.pi/agent/teams/...` or `PI_TEAMS_ROOT_DIR`). | P0 |
|
|
33
|
+
| Task list (shared) | `~/.claude/tasks/<taskListId>/` + states + deps | ✅ | File-per-task + deps (`blockedBy`/`blocks`); `/team task dep add|rm|ls`; self-claim skips blocked tasks. | P0 |
|
|
34
|
+
| Self-claim | Teammates can self-claim next unassigned, unblocked task; file locking | ✅ | Implemented: `claimNextAvailableTask()` + locks; enabled by default (`PI_TEAMS_DEFAULT_AUTO_CLAIM=1`). | P0 |
|
|
35
|
+
| Explicit assign | Lead assigns task to teammate | ✅ | `/team task assign` sets owner + pings via mailbox. | P0 |
|
|
36
|
+
| “Message” vs “broadcast” | Send to one teammate or all teammates | ✅ | `/team dm` + `/team broadcast` use mailbox; `/team send` uses RPC. Broadcast recipients = team config workers + RPC-spawned map + active task owners; manual tmux workers self-register into `config.json` on startup. | P0 |
|
|
37
|
+
| Teammate↔teammate messaging | Teammates can message each other directly | ❌ | Worker needs peer discovery (read team config) + send command/tool. | P1 |
|
|
38
|
+
| Display modes | In-process selection (Shift+Up/Down); split panes (tmux/iTerm) | ❌ | Pi has a widget + commands, but no terminal-level teammate navigation/panes. | P2 |
|
|
39
|
+
| Delegate mode | Lead restricted to coordination-only tools | ❌ | Add a leader “delegate mode” switch that blocks edit/write/bash tools (soft or enforced). | P1 |
|
|
40
|
+
| Plan approval | Teammate can be “plan required” and needs lead approval to implement | ❌ | Likely implement by spawning with read-only tool set until approved, then restart worker with full tools. | P1 |
|
|
41
|
+
| Shutdown handshake | Lead requests shutdown; teammate can approve/reject | 🟡 | Implemented `shutdown_request` → `shutdown_approved` via mailbox + `/team shutdown <name>` (auto-approve; no reject yet). | P1 |
|
|
42
|
+
| Cleanup team | “Clean up the team” removes shared resources after teammates stopped | ✅ | `/team cleanup [--force]` deletes only `<teamsRoot>/<teamId>` after safety checks. | P1 |
|
|
43
|
+
| Hooks / quality gates | `TeammateIdle`, `TaskCompleted` hooks | ❌ | Add optional hook runner in leader on idle/task-complete events (script execution + exit-code gating). | P2 |
|
|
44
|
+
| Task list UX | Ctrl+T toggle; show all/clear tasks by asking | 🟡 | Widget + `/team task list` show blocked/deps; `/team task show <id>`; `/team task clear [completed|all]`. No Ctrl+T toggle yet. | P0 |
|
|
45
|
+
| Shared task list across sessions | `CLAUDE_CODE_TASK_LIST_ID=...` | 🟡 | Pi supports `PI_TEAMS_TASK_LIST_ID` env (worker side) but leader doesn’t expose a stable “named task list id” workflow yet. | P1 |
|
|
46
|
+
|
|
47
|
+
## Prioritized roadmap
|
|
48
|
+
|
|
49
|
+
### P0 (done): collaboration primitives parity
|
|
50
|
+
|
|
51
|
+
1) **Task dependency commands + UX** ✅
|
|
52
|
+
- `/team task dep add <id> <depId>` / `dep rm ...` / `dep ls <id>`
|
|
53
|
+
- `task list` output shows blocked status + deps/blocks summary
|
|
54
|
+
- `/team task show <id>` shows full description + `metadata.result`
|
|
55
|
+
|
|
56
|
+
2) **Broadcast messaging** ✅
|
|
57
|
+
- `/team broadcast <msg...>` (mailbox broadcast)
|
|
58
|
+
|
|
59
|
+
3) **Task list hygiene** ✅
|
|
60
|
+
- `/team task clear [completed|all] [--force]` (safe delete within `teamsRoot/teamId`)
|
|
61
|
+
|
|
62
|
+
### P1: governance + lifecycle parity
|
|
63
|
+
|
|
64
|
+
4) **Shutdown handshake** 🟡
|
|
65
|
+
- Mailbox protocol: `shutdown_request` → `shutdown_approved` (no reject yet)
|
|
66
|
+
- Leader command: `/team shutdown <name> [reason...]` (graceful), keep `/team kill` as force
|
|
67
|
+
|
|
68
|
+
5) **Plan approval**
|
|
69
|
+
- Spawn option: `--plan-required` / `/team spawn <name> plan` (naming TBD)
|
|
70
|
+
- Worker flow: produce plan → send approval request → wait → implement after approval
|
|
71
|
+
- Enforcement idea: start worker with tools excluding write/edit/bash, then restart same session with full tool set after approval
|
|
72
|
+
|
|
73
|
+
6) **Delegate mode (leader)**
|
|
74
|
+
- A toggle (env or command) that prevents the leader from doing code edits and focuses it on coordination.
|
|
75
|
+
- In Pi, likely implemented as: leader tool wrapper refuses `bash/edit/write` while delegate mode is on.
|
|
76
|
+
|
|
77
|
+
7) **Cleanup** ✅
|
|
78
|
+
- `/team cleanup [--force]` deletes only `<teamsRoot>/<teamId>` after safety checks.
|
|
79
|
+
- Refuses if RPC teammates are running or there are `in_progress` tasks unless `--force`.
|
|
80
|
+
|
|
81
|
+
### P2: UX + “product-level” parity
|
|
82
|
+
|
|
83
|
+
8) **Better teammate interaction UX**
|
|
84
|
+
- Explore whether Pi’s TUI API can support:
|
|
85
|
+
- selecting a teammate from the widget
|
|
86
|
+
- “entering” a teammate transcript view
|
|
87
|
+
- (Optional) tmux integration for split panes.
|
|
88
|
+
|
|
89
|
+
9) **Hooks / quality gates**
|
|
90
|
+
- Support scripts that run on idle/task completion (similar to Claude hooks).
|
|
91
|
+
|
|
92
|
+
10) **Join/attach flow**
|
|
93
|
+
- Allow a running session to attach to an existing team (discover + approve join).
|
|
94
|
+
|
|
95
|
+
## Where changes would land (code map)
|
|
96
|
+
|
|
97
|
+
- Leader orchestration + commands + tool: `extensions/teams/leader.ts`
|
|
98
|
+
- Worker mailbox polling + self-claim + protocols: `extensions/teams/worker.ts`
|
|
99
|
+
- Task store + locking: `extensions/teams/task-store.ts`, `extensions/teams/fs-lock.ts`
|
|
100
|
+
- Mailbox store + locking: `extensions/teams/mailbox.ts`
|
|
101
|
+
- Team config: `extensions/teams/team-config.ts`
|
|
102
|
+
- Optional workspace isolation: `extensions/teams/worktree.ts`
|
|
103
|
+
|
|
104
|
+
## Testing strategy
|
|
105
|
+
|
|
106
|
+
- Keep tests hermetic by setting `PI_TEAMS_ROOT_DIR` to a temp directory.
|
|
107
|
+
- Extend:
|
|
108
|
+
- `scripts/smoke-test.mjs` for filesystem-only behaviors (deps, claiming, locking)
|
|
109
|
+
- `scripts/e2e-rpc-test.mjs` for protocol flows (shutdown handshake, plan approval)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Field notes: using `pi-agent-teams` for real (setup + surprises)
|
|
2
|
+
|
|
3
|
+
Date: 2026-02-07
|
|
4
|
+
|
|
5
|
+
Goal: dogfood the Teams extension to implement its own roadmap (Claude parity), and capture anything that surprised/confused us while setting it up.
|
|
6
|
+
|
|
7
|
+
## Test run: test1
|
|
8
|
+
|
|
9
|
+
Decisions: tmux session `pi-teams-test1`; `PI_TEAMS_ROOT_DIR=~/projects/pi-agent-teams/test1`; `teamId=0baaa0e6-8020-4d9a-bf33-c1a65f99a2f7`; workers started manually in tmux (not `/team spawn`).
|
|
10
|
+
|
|
11
|
+
First impressions:
|
|
12
|
+
- Manual tmux workers are usable. Initially the leader showed “(no teammates)” because it only tracked RPC-spawned workers; now manual workers **upsert themselves into `config.json` on startup**, and the leader widget renders online workers from team config.
|
|
13
|
+
- Pinning `PI_TEAMS_ROOT_DIR` made reruns/id discovery predictable (no “find the new folder” step).
|
|
14
|
+
- tmux workflow feels close to Claude-style split panes; bootstrap ergonomics still need smoothing.
|
|
15
|
+
- Surprise: `/team spawn <name> branch` failed once with `Entry <id> not found` (branch-from leaf missing on disk); `/team spawn <name> fresh` worked.
|
|
16
|
+
- Surprise (automation): when driving the leader via `tmux send-keys`, back-to-back `/team ...` commands sometimes only executed the first one unless we inserted a small delay.
|
|
17
|
+
|
|
18
|
+
## Setup (tmux-based)
|
|
19
|
+
|
|
20
|
+
### Why tmux?
|
|
21
|
+
|
|
22
|
+
- Pi sessions are long-lived and interactive.
|
|
23
|
+
- Our harness (and many CI environments) dislike background processes that keep stdio open.
|
|
24
|
+
- tmux gives us:
|
|
25
|
+
- a stable place to run a leader session
|
|
26
|
+
- optional separate panes/windows for worker sessions
|
|
27
|
+
- the ability to attach/detach without killing the team
|
|
28
|
+
|
|
29
|
+
### Environment knobs used
|
|
30
|
+
|
|
31
|
+
- `PI_TEAMS_ROOT_DIR` — isolate Teams artifacts from `~/.pi/agent/teams` while experimenting.
|
|
32
|
+
- Recommendation: use a *fresh, empty* temp directory per run so it’s easy to discover the current teamId by listing the directory.
|
|
33
|
+
|
|
34
|
+
### Session bootstrap (manual)
|
|
35
|
+
|
|
36
|
+
(There is also a helper script now: `./scripts/start-tmux-team.sh`.)
|
|
37
|
+
|
|
38
|
+
1. Pick a temp Teams root:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export PI_TEAMS_ROOT_DIR="/tmp/pi-teams-$(date +%Y%m%d-%H%M%S)"
|
|
42
|
+
mkdir -p "$PI_TEAMS_ROOT_DIR"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Start leader in tmux:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
tmux new -s pi-teams -c ~/projects/pi-agent-teams \
|
|
49
|
+
"PI_TEAMS_ROOT_DIR=$PI_TEAMS_ROOT_DIR pi -e ~/projects/pi-agent-teams/extensions/teams/index.ts"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. In the leader session:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
/team help
|
|
56
|
+
/team spawn alice branch
|
|
57
|
+
/team spawn bob branch
|
|
58
|
+
/team task add alice: add /team broadcast command
|
|
59
|
+
/team task add bob: add task dependency commands
|
|
60
|
+
/team task list
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
4. Optional: start **interactive worker panes** (instead of leader-spawned RPC workers).
|
|
64
|
+
|
|
65
|
+
This currently requires discovering the leader’s `teamId` first (see “Surprises” below).
|
|
66
|
+
|
|
67
|
+
## Surprises / confusion points (so far)
|
|
68
|
+
|
|
69
|
+
- **TeamId discoverability**: the leader uses `sessionId` as `teamId`. That’s convenient internally, but not obvious externally.
|
|
70
|
+
- Workaround used: point `PI_TEAMS_ROOT_DIR` at a fresh directory and `ls` it to find the generated `<teamId>` folder.
|
|
71
|
+
- Note: the team directory isn’t necessarily created instantly on process start (we saw a short delay), so scripts may need a small retry loop.
|
|
72
|
+
- Update: implemented `/team id` and `/team env <name>` (prints env vars + a one-liner to start a manual worker).
|
|
73
|
+
|
|
74
|
+
- **tmux vs `/team spawn`**: `/team spawn` uses `pi --mode rpc` subprocesses.
|
|
75
|
+
- Pros: simple, managed lifecycle.
|
|
76
|
+
- Cons: you don’t see a full interactive teammate UI like Claude’s split-pane mode.
|
|
77
|
+
- We manually started workers in separate tmux windows (setting `PI_TEAMS_WORKER=1`, `PI_TEAMS_TEAM_ID=...`, etc). This now shows up in the leader widget because workers upsert themselves into `config.json`, and the leader renders online workers from team config.
|
|
78
|
+
- Update: leader now renders teammates from `team config` and also auto-adds unknown senders on idle notifications (so manual tmux workers feel first-class).
|
|
79
|
+
- Improvement idea: optional spawn mode that starts a worker in a new tmux pane/window.
|
|
80
|
+
|
|
81
|
+
- **Two messaging paths** (`/team send` vs `/team dm`):
|
|
82
|
+
- `/team send` = RPC prompt (immediate “user message”)
|
|
83
|
+
- `/team dm` = mailbox message (Claude-style)
|
|
84
|
+
- Improvement idea: clearer naming and/or a single “message” command with a mode flag.
|
|
85
|
+
|
|
86
|
+
- **Runaway tasks / timeboxing**: a vague task prompt can turn into a long “research spiral”.
|
|
87
|
+
- In manual-tmux mode, there isn’t a great way (yet) for the leader to *steer* an in-flight run (unlike `/team steer` for RPC-spawned teammates).
|
|
88
|
+
- Improvement idea: add a mailbox-level “steer” protocol message that workers can treat as an in-flight follow-up if they’re currently running.
|
|
89
|
+
|
|
90
|
+
- **Failure semantics are underspecified**: tool failures show up in the worker UI, but our task store currently only supports `pending|in_progress|completed`.
|
|
91
|
+
- Update: `/team stop <name>` now sends a mailbox `abort_request`; workers treat aborts as aborts and reset the task back to `pending` (keeping the `owner`) instead of marking it `completed` with an empty result.
|
|
92
|
+
- Improvement idea: add `failed` status (and have workers write `metadata.failureReason` + include it in idle notifications), and only mark `completed` when we have an explicit success signal.
|
|
93
|
+
|
|
94
|
+
- **Worker shutdown + self-claim interaction**: when a worker receives SIGTERM it unassigns its non-completed tasks; other idle workers may immediately self-claim those now-unowned tasks.
|
|
95
|
+
- This is good for liveness, but surprising the first time you see task ownership “jump”.
|
|
96
|
+
|
|
97
|
+
- **Nice surprise: results are persisted with the task**: on completion, the worker writes `metadata.result` + `metadata.completedAt` into the task JSON file.
|
|
98
|
+
- This made it easy to recover outputs even after closing tmux windows.
|
|
99
|
+
|
|
100
|
+
## Next notes to capture
|
|
101
|
+
|
|
102
|
+
- How easy it is to recover after restarting the leader
|
|
103
|
+
- How often we hit file/lock contention
|
|
104
|
+
- Whether auto-claim behavior matches expectations in mixed assigned/unassigned task lists
|
|
105
|
+
- Whether worktree mode is essential in practice (and what breaks when no git repo exists)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# pi-teams (extension)
|
|
2
|
+
|
|
3
|
+
This directory contains the **Teams** extension entrypoint:
|
|
4
|
+
|
|
5
|
+
- `index.ts` (leader/worker dispatch)
|
|
6
|
+
|
|
7
|
+
The full project README (usage, commands, tests) lives at the repo root:
|
|
8
|
+
|
|
9
|
+
- `../../README.md`
|
|
10
|
+
|
|
11
|
+
## Storage root override
|
|
12
|
+
|
|
13
|
+
By default, all Teams artifacts are stored under the Pi agent directory:
|
|
14
|
+
|
|
15
|
+
- `~/.pi/agent/teams/<teamId>/...`
|
|
16
|
+
|
|
17
|
+
For tests/CI (or if you want to keep Teams state separate), set:
|
|
18
|
+
|
|
19
|
+
- `PI_TEAMS_ROOT_DIR=/absolute/path`
|
|
20
|
+
|
|
21
|
+
Then the extension will store:
|
|
22
|
+
|
|
23
|
+
- `<PI_TEAMS_ROOT_DIR>/<teamId>/...`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function assertTeamDirWithinTeamsRoot(teamsRootDir: string, teamDir: string): {
|
|
5
|
+
teamsRootAbs: string;
|
|
6
|
+
teamDirAbs: string;
|
|
7
|
+
} {
|
|
8
|
+
const teamsRootAbs = path.resolve(teamsRootDir);
|
|
9
|
+
const teamDirAbs = path.resolve(teamDir);
|
|
10
|
+
|
|
11
|
+
const rel = path.relative(teamsRootAbs, teamDirAbs);
|
|
12
|
+
// rel === "" => same path (would delete the whole root)
|
|
13
|
+
// rel starts with ".." or is absolute => outside root
|
|
14
|
+
if (!rel || rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Refusing to operate on path outside teams root. teamsRootDir=${teamsRootAbs} teamDir=${teamDirAbs}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { teamsRootAbs, teamDirAbs };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively delete the given teamDir, but only if it's safely inside teamsRootDir.
|
|
25
|
+
*
|
|
26
|
+
* Uses fs.rm({ recursive: true, force: true }) so it's idempotent.
|
|
27
|
+
*/
|
|
28
|
+
export async function cleanupTeamDir(teamsRootDir: string, teamDir: string): Promise<void> {
|
|
29
|
+
const { teamDirAbs } = assertTeamDirWithinTeamsRoot(teamsRootDir, teamDir);
|
|
30
|
+
await fs.promises.rm(teamDirAbs, { recursive: true, force: true });
|
|
31
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
function sleep(ms: number): Promise<void> {
|
|
4
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LockOptions {
|
|
8
|
+
/** How long to wait to acquire the lock before failing. */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
/** If lock file is older than this, consider it stale and remove it. */
|
|
11
|
+
staleMs?: number;
|
|
12
|
+
/** Poll interval while waiting for lock. */
|
|
13
|
+
pollMs?: number;
|
|
14
|
+
/** Optional label to help debugging (written into lock file). */
|
|
15
|
+
label?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function withLock<T>(lockFilePath: string, fn: () => Promise<T>, opts: LockOptions = {}): Promise<T> {
|
|
19
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
20
|
+
const staleMs = opts.staleMs ?? 60_000;
|
|
21
|
+
const pollMs = opts.pollMs ?? 50;
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
|
|
24
|
+
let fd: number | null = null;
|
|
25
|
+
|
|
26
|
+
while (fd === null) {
|
|
27
|
+
try {
|
|
28
|
+
fd = fs.openSync(lockFilePath, "wx");
|
|
29
|
+
const payload = {
|
|
30
|
+
pid: process.pid,
|
|
31
|
+
createdAt: new Date().toISOString(),
|
|
32
|
+
label: opts.label,
|
|
33
|
+
};
|
|
34
|
+
fs.writeFileSync(fd, JSON.stringify(payload));
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
if (err?.code !== "EEXIST") throw err;
|
|
37
|
+
|
|
38
|
+
// Stale lock handling
|
|
39
|
+
try {
|
|
40
|
+
const st = fs.statSync(lockFilePath);
|
|
41
|
+
const age = Date.now() - st.mtimeMs;
|
|
42
|
+
if (age > staleMs) {
|
|
43
|
+
fs.unlinkSync(lockFilePath);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore: stat/unlink failures fall through to wait
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Date.now() - start > timeoutMs) {
|
|
51
|
+
throw new Error(`Timeout acquiring lock: ${lockFilePath}`);
|
|
52
|
+
}
|
|
53
|
+
await sleep(pollMs);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await fn();
|
|
59
|
+
} finally {
|
|
60
|
+
try {
|
|
61
|
+
if (fd !== null) fs.closeSync(fd);
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
fs.unlinkSync(lockFilePath);
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { runLeader } from "./leader.js";
|
|
3
|
+
import { runWorker } from "./worker.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* pi-teams
|
|
7
|
+
*
|
|
8
|
+
* Two roles in one extension (Claude-style):
|
|
9
|
+
* - Leader process: spawn teammates, manage task list + mailbox UI/commands
|
|
10
|
+
* - Worker process: poll mailbox + auto-claim tasks from shared task list
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const IS_WORKER = process.env.PI_TEAMS_WORKER === "1";
|
|
14
|
+
|
|
15
|
+
export default function (pi: ExtensionAPI) {
|
|
16
|
+
if (IS_WORKER) runWorker(pi);
|
|
17
|
+
else runLeader(pi);
|
|
18
|
+
}
|