@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 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
+ }