pi-teams 0.5.2 → 0.7.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-teams 🚀
2
2
 
3
- **pi-teams** turns your single Pi agent into a coordinated software engineering team. It allows you to spawn multiple "Teammate" agents in separate terminal panes that work autonomously, communicate with each other, and manage a shared task board—all mediated through **tmux**.
3
+ **pi-teams** turns your single Pi agent into a coordinated software engineering team. It allows you to spawn multiple "Teammate" agents in separate terminal panes that work autonomously, communicate with each other, and manage a shared task board—all mediated through tmux, iTerm2, or Zellij.
4
4
 
5
5
  ### 🖥️ pi-teams in Action
6
6
 
@@ -16,95 +16,76 @@ Open your Pi terminal and type:
16
16
  pi install npm:pi-teams
17
17
  ```
18
18
 
19
- ---
19
+ ## 🚀 Quick Start
20
+
21
+ ```bash
22
+ # 1. Start a team (inside tmux, Zellij, or iTerm2)
23
+ "Create a team named 'my-team' using 'gpt-4o'"
24
+
25
+ # 2. Spawn teammates
26
+ "Spawn 'security-bot' to scan for vulnerabilities"
27
+ "Spawn 'frontend-dev' using 'haiku' for quick iterations"
28
+
29
+ # 3. Create and assign tasks
30
+ "Create a task for security-bot: 'Audit auth endpoints'"
31
+
32
+ # 4. Review and approve work
33
+ "List all tasks and approve any pending plans"
34
+ ```
20
35
 
21
36
  ## 🌟 What can it do?
22
37
 
38
+ ### Core Features
23
39
  - **Spawn Specialists**: Create agents like "Security Expert" or "Frontend Pro" to handle sub-tasks in parallel.
24
40
  - **Shared Task Board**: Keep everyone on the same page with a persistent list of tasks and their status.
25
41
  - **Agent Messaging**: Agents can send direct messages to each other and to you (the Team Lead) to report progress.
26
42
  - **Autonomous Work**: Teammates automatically "wake up," read their instructions, and poll their inboxes for new work while idle.
27
43
  - **Beautiful UI**: Optimized vertical splits in `tmux` with clear labels so you always know who is doing what.
28
44
 
29
- ---
45
+ ### Advanced Features
46
+ - **Plan Approval Mode**: Require teammates to submit their implementation plans for your approval before they touch any code.
47
+ - **Broadcast Messaging**: Send a message to the entire team at once for global coordination and announcements.
48
+ - **Quality Gate Hooks**: Automated shell scripts run when tasks are completed (e.g., to run tests or linting).
49
+ - **Thinking Level Control**: Set per-teammate thinking levels (`off`, `minimal`, `low`, `medium`, `high`) to balance speed vs. reasoning depth.
30
50
 
31
- ## 💬 How to use it (Examples)
32
-
33
- You don't need to learn complex code commands. Just talk to Pi in plain English!
51
+ ## 💬 Key Examples
34
52
 
35
53
  ### 1. Start a Team
36
54
  > **You:** "Create a team named 'my-app-audit' for reviewing the codebase."
37
55
 
38
- **Pro Tip:** You can also set a default model for the whole team!
56
+ **Set a default model for the whole team:**
39
57
  > **You:** "Create a team named 'Research' and use 'gpt-4o' for everyone."
40
58
 
41
- ### 2. Spawn a Teammate
59
+ ### 2. Spawn Teammate with Custom Settings
42
60
  > **You:** "Spawn a teammate named 'security-bot' in the current folder. Tell them to scan for hardcoded API keys."
43
61
 
44
- **Pro Tip:** You can specify a different model for a specific teammate!
62
+ **Use a different model:**
45
63
  > **You:** "Spawn a teammate named 'speed-bot' using 'haiku' to quickly run some benchmarks."
46
64
 
47
- ### 3. Assign a Task
48
- > **You:** "Create a task for security-bot: 'Check the .env.example file for sensitive defaults' and set it to in_progress."
49
-
50
- ### 4. Send a Message
51
- > **You:** "Tell security-bot to focus on the 'config/' directory first."
52
- > *The lead agent uses `send_message` to put an instruction in the teammate's inbox.*
53
-
54
- ### 5. Inter-Agent Communication
55
- > Teammates can also talk to each other! For example, a `frontend-bot` can message a `backend-bot` to coordinate on an API schema without your intervention.
65
+ **Require plan approval:**
66
+ > **You:** "Spawn a teammate named 'refactor-bot' and require plan approval before they make any changes."
56
67
 
57
- ### 6. Check on Progress
58
- > **You:** "How is the team doing? List all tasks and check my inbox for any messages."
68
+ **Customize model and thinking level:**
69
+ > **You:** "Spawn a teammate named 'architect-bot' using 'gpt-4o' with 'high' thinking level for deep reasoning."
59
70
 
60
- ### 7. Shut Down the Team
61
- > **You:** "We're done. Shut down the team and close the panes."
62
-
63
- ---
64
-
65
- ## 🛠 Available Tools
66
-
67
- Pi automatically uses these tools when you give instructions like the examples above.
68
-
69
- ### Team Management
70
- - `team_create`: Start a new team. (Optional: `default_model`)
71
- - `team_delete`: Delete a team and its data.
72
- - `read_config`: Get details about the team and its members.
73
-
74
- ### Teammates
75
- - `spawn_teammate`: Launch a new agent into a `tmux` pane with a role and instructions. (Optional: `model`)
76
- - `check_teammate`: See if a teammate is still running or has unread messages.
77
- - `force_kill_teammate`: Stop a teammate and remove them from the team.
78
- - `process_shutdown_approved`: Orderly shutdown for a finished teammate.
79
-
80
- ### Task Management
81
- - `task_create`: Create a new task.
82
- - `task_list`: List all tasks and their current status.
83
- - `task_get`: Get full details of a specific task.
84
- - `task_update`: Update a task's status or owner.
85
-
86
- ### Messaging
87
- - `send_message`: Send a message to a teammate or lead.
88
- - `read_inbox`: Read incoming messages for an agent.
71
+ ### 3. Assign Task & Get Approval
72
+ > **You:** "Create a task for security-bot: 'Check the .env.example file for sensitive defaults' and set it to in_progress."
89
73
 
90
- ---
74
+ Teammates in `planning` mode will use `task_submit_plan`. As the lead, review their work:
75
+ > **You:** "Review refactor-bot's plan for task 5. If it looks good, approve it. If not, reject it with feedback on the test coverage."
91
76
 
92
- ## 🤖 Automated Behavior
77
+ ### 4. Broadcast to Team
78
+ > **You:** "Broadcast to the entire team: 'The API endpoint has changed to /v2. Please update your work accordingly.'"
93
79
 
94
- - **Initial Greeting**: When a teammate is spawned, they will automatically send a message saying they've started and are checking their inbox.
95
- - **Idle Polling**: Teammates check for new messages every 30 seconds if they are idle.
96
- - **Context Injection**: Each teammate is given a custom system prompt that defines their role and instructions for the team environment.
80
+ ### 5. Shut Down Team
81
+ > **You:** "We're done. Shut down the team and close the panes."
97
82
 
98
83
  ---
99
84
 
100
- ## 📂 Configuration & Data
85
+ ## 📚 Learn More
101
86
 
102
- All team and task data is stored in your home directory:
103
- `~/.pi/teams/` and `~/.pi/tasks/`
104
-
105
- You can manually inspect these JSON files if you ever need to debug your team's configuration or message history.
106
-
107
- ---
87
+ - **[Full Usage Guide](docs/guide.md)** - Detailed examples, hook system, best practices, and troubleshooting
88
+ - **[Tool Reference](docs/reference.md)** - Complete documentation of all tools and parameters
108
89
 
109
90
  ## 🪟 Terminal Requirements: tmux, Zellij, or iTerm2
110
91
 
@@ -112,32 +93,29 @@ To show multiple agents on one screen, **pi-teams** requires a way to manage ter
112
93
 
113
94
  ### Option 1: tmux (Recommended)
114
95
 
115
- #### 1. Install tmux
96
+ Install tmux:
116
97
  - **macOS**: `brew install tmux`
117
98
  - **Linux**: `sudo apt install tmux`
118
99
 
119
- #### 2. How to run it
120
- Before you start a team, you **must** be inside a tmux session. Simply type:
100
+ How to run:
121
101
  ```bash
122
- tmux
102
+ tmux # Start tmux session
103
+ pi # Start pi inside tmux
123
104
  ```
124
- Then start `pi` inside that window.
125
105
 
126
106
  ### Option 2: Zellij
127
107
 
128
- If you prefer **Zellij**, simply start `pi` inside a Zellij session. **pi-teams** will detect it via the `ZELLIJ` environment variable and use `zellij run` to spawn teammates in new panes.
108
+ Simply start `pi` inside a Zellij session. **pi-teams** will detect it via the `ZELLIJ` environment variable and use `zellij run` to spawn teammates in new panes.
129
109
 
130
110
  ### Option 3: iTerm2 (macOS)
131
111
 
132
112
  If you are using **iTerm2** on macOS and are *not* inside tmux or Zellij, **pi-teams** will use AppleScript to automatically split your current window into an optimized layout (1 large Lead pane on the left, Teammates stacked on the right). It will also name the panes with the teammate's agent name for easy identification.
133
113
 
134
- ---
135
-
136
114
  ## 📜 Credits & Attribution
137
115
 
138
- This project is a port of the excellent [claude-code-teams-mcp](https://github.com/cs50victor/claude-code-teams-mcp) by [cs50victor](https://github.com/cs50victor).
116
+ This project is a port of the excellent [claude-code-teams-mcp](https://github.com/cs50victor/claude-code-teams-mcp) by [cs50victor](https://github.com/cs50victor).
139
117
 
140
- We have adapted the original MCP coordination protocol to work natively as a **Pi Package**, adding features like auto-starting teammates, balanced vertical UI layouts, and automatic inbox polling to make the experience seamless for Pi users.
118
+ We have adapted the original MCP coordination protocol to work natively as a **Pi Package**, adding features like auto-starting teammates, balanced vertical UI layouts, automatic inbox polling, plan approval mode, broadcast messaging, and quality gate hooks.
141
119
 
142
120
  ## 📄 License
143
121
  MIT
@@ -6,7 +6,8 @@ import * as teams from "../src/utils/teams";
6
6
  import * as tasks from "../src/utils/tasks";
7
7
  import * as messaging from "../src/utils/messaging";
8
8
  import { Member } from "../src/utils/models";
9
- import { execSync, spawnSync } from "node:child_process";
9
+ import { getTerminalAdapter } from "../src/adapters/terminal-registry";
10
+ import { Iterm2Adapter } from "../src/adapters/iterm2-adapter";
10
11
  import path from "node:path";
11
12
  import fs from "node:fs";
12
13
 
@@ -15,6 +16,9 @@ export default function (pi: ExtensionAPI) {
15
16
  const agentName = process.env.PI_AGENT_NAME || "team-lead";
16
17
  const teamName = process.env.PI_TEAM_NAME;
17
18
 
19
+ // Get the terminal adapter once at startup
20
+ const terminal = getTerminalAdapter();
21
+
18
22
  pi.on("session_start", async (_event, ctx) => {
19
23
  paths.ensureDirs();
20
24
  if (isTeammate) {
@@ -26,15 +30,9 @@ export default function (pi: ExtensionAPI) {
26
30
  // Use a shorter, more prominent status at the beginning if possible
27
31
  ctx.ui.setStatus("00-pi-teams", `[${agentName.toUpperCase()}]`);
28
32
 
29
- // Also set the tmux pane title for better visibility
30
- try {
31
- if (process.env.TMUX) {
32
- spawnSync("tmux", ["select-pane", "-T", agentName]);
33
- } else if (process.env.TERM_PROGRAM === "iTerm.app") {
34
- spawnSync("osascript", ["-e", `tell application "iTerm2" to tell current session of current window to set name to "${agentName}"`]);
35
- }
36
- } catch (e) {
37
- // ignore
33
+ // Set the terminal pane title for better visibility
34
+ if (terminal) {
35
+ terminal.setTitle(agentName);
38
36
  }
39
37
 
40
38
  // Auto-trigger the first turn for teammates
@@ -60,8 +58,27 @@ export default function (pi: ExtensionAPI) {
60
58
  pi.on("before_agent_start", async (event, ctx) => {
61
59
  if (isTeammate && firstTurn) {
62
60
  firstTurn = false;
61
+
62
+ // Get the teammate's model and thinking level from team config for accurate reporting
63
+ let modelInfo = "";
64
+ if (teamName) {
65
+ try {
66
+ const teamConfig = await teams.readConfig(teamName);
67
+ const member = teamConfig.members.find(m => m.name === agentName);
68
+ if (member && member.model) {
69
+ modelInfo = `\nYou are currently using model: ${member.model}`;
70
+ if (member.thinking) {
71
+ modelInfo += ` with thinking level: ${member.thinking}`;
72
+ }
73
+ modelInfo += `. When reporting your model or thinking level, use these exact values.`;
74
+ }
75
+ } catch (e) {
76
+ // If config can't be read, that's okay - proceed without model info
77
+ }
78
+ }
79
+
63
80
  return {
64
- systemPrompt: event.systemPrompt + `\n\nYou are teammate '${agentName}' on team '${teamName}'.\nYour lead is 'team-lead'.\nStart by calling read_inbox(team_name="${teamName}") to get your initial instructions.`,
81
+ systemPrompt: event.systemPrompt + `\n\nYou are teammate '${agentName}' on team '${teamName}'.\nYour lead is 'team-lead'.${modelInfo}\nStart by calling read_inbox(team_name="${teamName}") to get your initial instructions.`,
65
82
  };
66
83
  }
67
84
  });
@@ -80,32 +97,8 @@ export default function (pi: ExtensionAPI) {
80
97
  }
81
98
  }
82
99
 
83
- if (member.tmuxPaneId) {
84
- try {
85
- if (member.tmuxPaneId.startsWith("iterm_")) {
86
- const itermId = member.tmuxPaneId.replace("iterm_", "");
87
- const script = `tell application "iTerm2"
88
- repeat with aWindow in windows
89
- repeat with aTab in tabs of aWindow
90
- repeat with aSession in sessions of aTab
91
- if id of aSession is "${itermId}" then
92
- close aSession
93
- return "Closed"
94
- end if
95
- end repeat
96
- end repeat
97
- end repeat
98
- end tell`;
99
- spawnSync("osascript", ["-e", script]);
100
- } else if (member.tmuxPaneId.startsWith("zellij_")) {
101
- // Zellij is expected to close on process exit (using --close-on-exit)
102
- } else {
103
- // Use -t with the pane_id
104
- spawnSync("tmux", ["kill-pane", "-t", member.tmuxPaneId.trim()]);
105
- }
106
- } catch (e) {
107
- // ignore
108
- }
100
+ if (member.tmuxPaneId && terminal) {
101
+ terminal.kill(member.tmuxPaneId);
109
102
  }
110
103
  }
111
104
 
@@ -119,7 +112,7 @@ end tell`;
119
112
  description: Type.Optional(Type.String()),
120
113
  default_model: Type.Optional(Type.String()),
121
114
  }),
122
- async execute(toolCallId, params, signal, onUpdate, ctx) {
115
+ async execute(toolCallId, params: any, signal, onUpdate, ctx) {
123
116
  const config = teams.createTeam(params.team_name, "local-session", "lead-agent", params.description, params.default_model);
124
117
  return {
125
118
  content: [{ type: "text", text: `Team ${params.team_name} created.` }],
@@ -131,15 +124,17 @@ end tell`;
131
124
  pi.registerTool({
132
125
  name: "spawn_teammate",
133
126
  label: "Spawn Teammate",
134
- description: "Spawn a new teammate in a tmux pane.",
127
+ description: "Spawn a new teammate in a terminal pane.",
135
128
  parameters: Type.Object({
136
129
  team_name: Type.String(),
137
130
  name: Type.String(),
138
131
  prompt: Type.String(),
139
132
  cwd: Type.String(),
140
133
  model: Type.Optional(Type.String()),
134
+ thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high"])),
135
+ plan_mode_required: Type.Optional(Type.Boolean({ default: false })),
141
136
  }),
142
- async execute(toolCallId, params, signal, onUpdate, ctx) {
137
+ async execute(toolCallId, params: any, signal, onUpdate, ctx) {
143
138
  const safeName = paths.sanitizeName(params.name);
144
139
  const safeTeamName = paths.sanitizeName(params.team_name);
145
140
 
@@ -147,8 +142,28 @@ end tell`;
147
142
  throw new Error(`Team ${params.team_name} does not exist`);
148
143
  }
149
144
 
145
+ if (!terminal) {
146
+ throw new Error("No terminal adapter detected. Ensure you're running in tmux, iTerm2, or Zellij.");
147
+ }
148
+
150
149
  const teamConfig = await teams.readConfig(safeTeamName);
151
- const chosenModel = params.model || teamConfig.defaultModel;
150
+ let chosenModel = params.model || teamConfig.defaultModel;
151
+
152
+ // If model doesn't include provider prefix (provider/model), use the team's defaultModel or fallback
153
+ if (chosenModel && !chosenModel.includes('/')) {
154
+ // Check if team has a defaultModel with a provider prefix
155
+ if (teamConfig.defaultModel && teamConfig.defaultModel.includes('/')) {
156
+ const [provider] = teamConfig.defaultModel.split('/');
157
+ chosenModel = `${provider}/${chosenModel}`;
158
+ } else {
159
+ // Infer provider from model name
160
+ if (chosenModel.startsWith('glm-')) {
161
+ chosenModel = `zai/${chosenModel}`;
162
+ } else if (chosenModel.startsWith('claude-')) {
163
+ chosenModel = `anthropic/${chosenModel}`;
164
+ }
165
+ }
166
+ }
152
167
 
153
168
  const member: Member = {
154
169
  agentId: `${safeName}@${safeTeamName}`,
@@ -161,88 +176,58 @@ end tell`;
161
176
  subscriptions: [],
162
177
  prompt: params.prompt,
163
178
  color: "blue",
179
+ thinking: params.thinking,
180
+ planModeRequired: params.plan_mode_required,
164
181
  };
165
182
 
166
183
  await teams.addMember(safeTeamName, member);
167
184
  await messaging.sendPlainMessage(safeTeamName, "team-lead", safeName, params.prompt, "Initial prompt");
168
185
 
169
186
  const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi"; // Assumed on path
170
- const piCmd = piBinary;
171
-
172
- let paneId = "";
173
- try {
174
- if (process.env.ZELLIJ && !process.env.TMUX) {
175
- const zellijArgs = [
176
- "run",
177
- "--name", safeName,
178
- "--cwd", params.cwd,
179
- "--close-on-exit",
180
- "--",
181
- "env", `PI_TEAM_NAME=${safeTeamName}`, `PI_AGENT_NAME=${safeName}`,
182
- "sh", "-c", piCmd
183
- ];
184
- spawnSync("zellij", zellijArgs);
185
- paneId = `zellij_${safeName}`;
186
- } else if (process.env.TERM_PROGRAM === "iTerm.app" && !process.env.TMUX && !process.env.ZELLIJ) {
187
- const itermCmd = `cd '${params.cwd}' && PI_TEAM_NAME=${safeTeamName} PI_AGENT_NAME=${safeName} ${piCmd}`;
188
- const teammates = teamConfig.members.filter(m => m.agentType === "teammate" && m.tmuxPaneId.startsWith("iterm_"));
189
- const lastTeammate = teammates.length > 0 ? teammates[teammates.length - 1] : null;
190
-
191
- let script = "";
192
- if (!lastTeammate) {
193
- // First teammate: split current session vertically (side-by-side)
194
- script = `tell application "iTerm2"
195
- tell current session of current window
196
- set newSession to split vertically with default profile
197
- tell newSession
198
- write text "${itermCmd.replace(/"/g, '\\"')}"
199
- return id
200
- end tell
201
- end tell
202
- end tell`;
203
- } else {
204
- // Subsequent teammate: split the last teammate's session horizontally (stacking them)
205
- const lastSessionId = lastTeammate.tmuxPaneId.replace("iterm_", "");
206
- script = `tell application "iTerm2"
207
- repeat with aWindow in windows
208
- repeat with aTab in tabs of aWindow
209
- repeat with aSession in sessions of aTab
210
- if id of aSession is "${lastSessionId}" then
211
- tell aSession
212
- set newSession to split horizontally with default profile
213
- tell newSession
214
- write text "${itermCmd.replace(/"/g, '\\"')}"
215
- return id
216
- end tell
217
- end tell
218
- end if
219
- end repeat
220
- end repeat
221
- end repeat
222
- end tell`;
223
- }
224
- const result = spawnSync("osascript", ["-e", script]);
225
- if (result.status !== 0) throw new Error(`osascript failed with status ${result.status}: ${result.stderr.toString()}`);
226
- paneId = `iterm_${result.stdout.toString().trim()}`;
187
+ let piCmd = piBinary;
188
+
189
+ // Build model command with thinking level if specified
190
+ if (chosenModel) {
191
+ const [provider, ...modelParts] = chosenModel.split('/');
192
+ const modelName = modelParts.join('/');
193
+
194
+ if (params.thinking) {
195
+ piCmd = `${piBinary} --provider ${provider} --model ${modelName}:${params.thinking}`;
227
196
  } else {
228
- const tmuxArgs = [
229
- "split-window",
230
- "-h", "-dP",
231
- "-F", "#{pane_id}",
232
- "-c", params.cwd,
233
- "env", `PI_TEAM_NAME=${safeTeamName}`, `PI_AGENT_NAME=${safeName}`,
234
- "sh", "-c", piCmd
235
- ];
236
- const result = spawnSync("tmux", tmuxArgs);
237
- if (result.status !== 0) throw new Error(`tmux failed with status ${result.status}: ${result.stderr.toString()}`);
238
- paneId = result.stdout.toString().trim();
239
- spawnSync("tmux", ["set-window-option", "main-pane-width", "60%"]);
240
- spawnSync("tmux", ["select-layout", "main-vertical"]);
197
+ piCmd = `${piBinary} --provider ${provider} --model ${modelName}`;
241
198
  }
242
- } catch (e) {
243
- throw new Error(`Failed to spawn ${process.env.ZELLIJ && !process.env.TMUX ? "zellij" : (process.env.TERM_PROGRAM === "iTerm.app" ? "iTerm2" : "tmux")} pane: ${e}`);
199
+ } else if (params.thinking) {
200
+ piCmd = `${piBinary} --thinking ${params.thinking}`;
244
201
  }
245
202
 
203
+ const env: Record<string, string> = {
204
+ ...process.env,
205
+ PI_TEAM_NAME: safeTeamName,
206
+ PI_AGENT_NAME: safeName,
207
+ };
208
+
209
+ // For iTerm2, we need to handle the spawn context for proper layout
210
+ if (terminal instanceof Iterm2Adapter) {
211
+ const teammates = teamConfig.members.filter(m => m.agentType === "teammate" && m.tmuxPaneId.startsWith("iterm_"));
212
+ const lastTeammate = teammates.length > 0 ? teammates[teammates.length - 1] : null;
213
+ if (lastTeammate?.tmuxPaneId) {
214
+ terminal.setSpawnContext({ lastSessionId: lastTeammate.tmuxPaneId.replace("iterm_", "") });
215
+ } else {
216
+ terminal.setSpawnContext({});
217
+ }
218
+ }
219
+
220
+ let paneId = "";
221
+ try {
222
+ paneId = terminal.spawn({
223
+ name: safeName,
224
+ cwd: params.cwd,
225
+ command: piCmd,
226
+ env: env,
227
+ });
228
+ } catch (e) {
229
+ throw new Error(`Failed to spawn ${terminal.name} pane: ${e}`);
230
+ }
246
231
 
247
232
  // Update member with paneId
248
233
  await teams.updateMember(params.team_name, params.name, { tmuxPaneId: paneId });
@@ -264,10 +249,30 @@ end tell`;
264
249
  content: Type.String(),
265
250
  summary: Type.String(),
266
251
  }),
267
- async execute(toolCallId, params, signal, onUpdate, ctx) {
252
+ async execute(toolCallId, params: any, signal, onUpdate, ctx) {
268
253
  await messaging.sendPlainMessage(params.team_name, agentName, params.recipient, params.content, params.summary);
269
254
  return {
270
255
  content: [{ type: "text", text: `Message sent to ${params.recipient}.` }],
256
+ details: {},
257
+ };
258
+ },
259
+ });
260
+
261
+ pi.registerTool({
262
+ name: "broadcast_message",
263
+ label: "Broadcast Message",
264
+ description: "Broadcast a message to all team members except the sender.",
265
+ parameters: Type.Object({
266
+ team_name: Type.String(),
267
+ content: Type.String(),
268
+ summary: Type.String(),
269
+ color: Type.Optional(Type.String()),
270
+ }),
271
+ async execute(toolCallId, params: any, signal, onUpdate, ctx) {
272
+ await messaging.broadcastMessage(params.team_name, agentName, params.content, params.summary, params.color);
273
+ return {
274
+ content: [{ type: "text", text: `Message broadcasted to all team members.` }],
275
+ details: {},
271
276
  };
272
277
  },
273
278
  });
@@ -309,6 +314,43 @@ end tell`;
309
314
  },
310
315
  });
311
316
 
317
+ pi.registerTool({
318
+ name: "task_submit_plan",
319
+ label: "Submit Plan",
320
+ description: "Submit a plan for a task, updating its status to 'planning'.",
321
+ parameters: Type.Object({
322
+ team_name: Type.String(),
323
+ task_id: Type.String(),
324
+ plan: Type.String(),
325
+ }),
326
+ async execute(toolCallId, params: any, signal, onUpdate, ctx) {
327
+ const updated = await tasks.submitPlan(params.team_name, params.task_id, params.plan);
328
+ return {
329
+ content: [{ type: "text", text: `Plan submitted for task ${params.task_id}.` }],
330
+ details: { task: updated },
331
+ };
332
+ },
333
+ });
334
+
335
+ pi.registerTool({
336
+ name: "task_evaluate_plan",
337
+ label: "Evaluate Plan",
338
+ description: "Evaluate a submitted plan for a task.",
339
+ parameters: Type.Object({
340
+ team_name: Type.String(),
341
+ task_id: Type.String(),
342
+ action: StringEnum(["approve", "reject"]),
343
+ feedback: Type.Optional(Type.String({ description: "Required for rejection" })),
344
+ }),
345
+ async execute(toolCallId, params: any, signal, onUpdate, ctx) {
346
+ const updated = await tasks.evaluatePlan(params.team_name, params.task_id, params.action as any, params.feedback);
347
+ return {
348
+ content: [{ type: "text", text: `Plan for task ${params.task_id} has been ${params.action}d.` }],
349
+ details: { task: updated },
350
+ };
351
+ },
352
+ });
353
+
312
354
  pi.registerTool({
313
355
  name: "task_list",
314
356
  label: "List Tasks",
@@ -332,10 +374,10 @@ end tell`;
332
374
  parameters: Type.Object({
333
375
  team_name: Type.String(),
334
376
  task_id: Type.String(),
335
- status: Type.Optional(StringEnum(["pending", "in_progress", "completed", "deleted"])),
377
+ status: Type.Optional(StringEnum(["pending", "planning", "in_progress", "completed", "deleted"])),
336
378
  owner: Type.Optional(Type.String()),
337
379
  }),
338
- async execute(toolCallId, params, signal, onUpdate, ctx) {
380
+ async execute(toolCallId, params: any, signal, onUpdate, ctx) {
339
381
  const updated = await tasks.updateTask(params.team_name, params.task_id, {
340
382
  status: params.status as any,
341
383
  owner: params.owner,
@@ -414,7 +456,7 @@ end tell`;
414
456
  pi.registerTool({
415
457
  name: "force_kill_teammate",
416
458
  label: "Force Kill Teammate",
417
- description: "Forcibly kill a teammate's tmux target.",
459
+ description: "Forcibly kill a teammate's terminal pane.",
418
460
  parameters: Type.Object({
419
461
  team_name: Type.String(),
420
462
  agent_name: Type.String(),
@@ -448,33 +490,8 @@ end tell`;
448
490
  if (!member) throw new Error(`Teammate ${params.agent_name} not found`);
449
491
 
450
492
  let alive = false;
451
- if (member.tmuxPaneId) {
452
- try {
453
- if (member.tmuxPaneId.startsWith("zellij_")) {
454
- // Assume alive if it's zellij for now
455
- alive = true;
456
- } else if (member.tmuxPaneId.startsWith("iterm_")) {
457
- const itermId = member.tmuxPaneId.replace("iterm_", "");
458
- const script = `tell application "iTerm2"
459
- repeat with aWindow in windows
460
- repeat with aTab in tabs of aWindow
461
- repeat with aSession in sessions of aTab
462
- if id of aSession is "${itermId}" then
463
- return "Alive"
464
- end if
465
- end repeat
466
- end repeat
467
- end repeat
468
- end tell`;
469
- const result = spawnSync("osascript", ["-e", script]);
470
- alive = result.stdout.toString().includes("Alive");
471
- } else {
472
- execSync(`tmux has-session -t ${member.tmuxPaneId}`);
473
- alive = true;
474
- }
475
- } catch (e) {
476
- alive = false;
477
- }
493
+ if (member.tmuxPaneId && terminal) {
494
+ alive = terminal.isAlive(member.tmuxPaneId);
478
495
  }
479
496
 
480
497
  const unreadCount = (await messaging.readInbox(params.team_name, params.agent_name, true, false)).length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-teams",
3
- "version": "0.5.2",
3
+ "version": "0.7.3",
4
4
  "description": "Agent teams for pi, ported from claude-code-teams-mcp",
5
5
  "repository": "github:burggraf/pi-teams",
6
6
  "author": "Mark Burggraf",