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 +49 -71
- package/extensions/index.ts +161 -144
- package/package.json +1 -1
- package/src/adapters/iterm2-adapter.ts +158 -0
- package/src/adapters/terminal-registry.ts +92 -0
- package/src/adapters/tmux-adapter.ts +77 -0
- package/src/adapters/zellij-adapter.ts +62 -0
- package/src/utils/hooks.test.ts +75 -0
- package/src/utils/hooks.ts +35 -0
- package/src/utils/lock.test.ts +1 -0
- package/src/utils/lock.ts +4 -2
- package/src/utils/messaging.test.ts +36 -1
- package/src/utils/messaging.ts +35 -0
- package/src/utils/models.ts +4 -1
- package/src/utils/tasks.test.ts +66 -1
- package/src/utils/tasks.ts +78 -6
- package/src/utils/terminal-adapter.ts +85 -0
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
|
|
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
|
-
## 💬
|
|
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
|
-
**
|
|
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
|
|
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
|
-
**
|
|
62
|
+
**Use a different model:**
|
|
45
63
|
> **You:** "Spawn a teammate named 'speed-bot' using 'haiku' to quickly run some benchmarks."
|
|
46
64
|
|
|
47
|
-
|
|
48
|
-
> **You:** "
|
|
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
|
-
|
|
58
|
-
> **You:** "
|
|
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
|
-
###
|
|
61
|
-
> **You:** "
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
##
|
|
85
|
+
## 📚 Learn More
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
96
|
+
Install tmux:
|
|
116
97
|
- **macOS**: `brew install tmux`
|
|
117
98
|
- **Linux**: `sudo apt install tmux`
|
|
118
99
|
|
|
119
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
package/extensions/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
243
|
-
|
|
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
|
|
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
|
-
|
|
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;
|