pi-teams 0.9.5 → 0.9.8
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 +143 -0
- package/extensions/index.ts +382 -1
- package/package.json +13 -2
- package/src/utils/predefined-teams.test.ts +389 -0
- package/src/utils/predefined-teams.ts +509 -0
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ pi install npm:pi-teams
|
|
|
45
45
|
- **Beautiful UI**: Optimized vertical splits in `tmux` with clear labels so you always know who is doing what.
|
|
46
46
|
|
|
47
47
|
### Advanced Features
|
|
48
|
+
- **Predefined Teams**: Define team templates in `teams.yaml` and spawn entire teams with a single command.
|
|
49
|
+
- **Save Teams as Templates**: Convert any runtime team into a reusable template with a single command.
|
|
48
50
|
- **Isolated OS Windows**: Launch teammates in true separate OS windows instead of panes.
|
|
49
51
|
- **Persistent Window Titles**: Windows are automatically titled `[team-name]: [agent-name]` for easy identification in your window manager.
|
|
50
52
|
- **Plan Approval Mode**: Require teammates to submit their implementation plans for your approval before they touch any code.
|
|
@@ -106,6 +108,147 @@ Teammates in `planning` mode will use `task_submit_plan`. As the lead, review th
|
|
|
106
108
|
### 5. Shut Down Team
|
|
107
109
|
> **You:** "We're done. Shut down the team and close the panes."
|
|
108
110
|
|
|
111
|
+
**Automatic Cleanup:**
|
|
112
|
+
When you shut down a team, pi-teams automatically cleans up orphaned agent session folders from `~/.pi/agent/teams/` that are older than 1 hour. This prevents accumulation of stale session data over time.
|
|
113
|
+
|
|
114
|
+
**Manual Cleanup:**
|
|
115
|
+
If you need to clean up agent sessions without shutting down a team, or want to use a different age threshold:
|
|
116
|
+
> **You:** "Clean up agent session folders older than 24 hours."
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 🏗️ Predefined Teams
|
|
121
|
+
|
|
122
|
+
Predefined teams let you define reusable team templates in a `teams.yaml` file. This is perfect for common workflows where you always want the same set of specialists.
|
|
123
|
+
|
|
124
|
+
### Define Team Templates
|
|
125
|
+
|
|
126
|
+
Create `~/.pi/teams.yaml` (global) or `.pi/teams.yaml` in your project:
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
# Full development team
|
|
130
|
+
full:
|
|
131
|
+
- scout
|
|
132
|
+
- planner
|
|
133
|
+
- builder
|
|
134
|
+
- reviewer
|
|
135
|
+
- documenter
|
|
136
|
+
|
|
137
|
+
# Quick plan-build cycle
|
|
138
|
+
plan-build:
|
|
139
|
+
- planner
|
|
140
|
+
- builder
|
|
141
|
+
- reviewer
|
|
142
|
+
|
|
143
|
+
# Research and documentation
|
|
144
|
+
research:
|
|
145
|
+
- scout
|
|
146
|
+
- documenter
|
|
147
|
+
|
|
148
|
+
# Frontend specialists
|
|
149
|
+
frontend:
|
|
150
|
+
- planner
|
|
151
|
+
- builder
|
|
152
|
+
- bowser
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Define Agent Definitions
|
|
156
|
+
|
|
157
|
+
Create agent definitions in `~/.pi/agent/agents/` (global) or `.pi/agents/` (project-local):
|
|
158
|
+
|
|
159
|
+
**scout.md:**
|
|
160
|
+
```markdown
|
|
161
|
+
---
|
|
162
|
+
name: scout
|
|
163
|
+
description: Fast recon and codebase exploration
|
|
164
|
+
tools: read,grep,find,ls
|
|
165
|
+
---
|
|
166
|
+
You are a scout agent. Investigate the codebase quickly and report findings concisely. Do NOT modify any files. Focus on structure, patterns, and key entry points.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**builder.md:**
|
|
170
|
+
```markdown
|
|
171
|
+
---
|
|
172
|
+
name: builder
|
|
173
|
+
description: Implementation specialist
|
|
174
|
+
tools: read,write,edit,bash
|
|
175
|
+
model: claude-sonnet-4
|
|
176
|
+
thinking: medium
|
|
177
|
+
---
|
|
178
|
+
You are a builder agent. Implement code following the plan provided. Write clean, tested code.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Agent Definition Fields:**
|
|
182
|
+
- `name` (required): The agent's name
|
|
183
|
+
- `description` (required): What the agent does
|
|
184
|
+
- `tools` (optional): Comma or space-separated list of allowed tools
|
|
185
|
+
- `model` (optional): Model to use (e.g., `claude-sonnet-4`, `gpt-4o`)
|
|
186
|
+
- `thinking` (optional): Thinking level (`off`, `minimal`, `low`, `medium`, `high`)
|
|
187
|
+
|
|
188
|
+
### Use Predefined Teams
|
|
189
|
+
|
|
190
|
+
**List available team templates:**
|
|
191
|
+
> **You:** "List all predefined teams I can use."
|
|
192
|
+
|
|
193
|
+
**List available agent definitions:**
|
|
194
|
+
> **You:** "Show me all predefined agents."
|
|
195
|
+
|
|
196
|
+
**Create a team from a template:**
|
|
197
|
+
> **You:** "Create a team named 'my-project' from the 'plan-build' predefined team in the current directory."
|
|
198
|
+
|
|
199
|
+
This single command:
|
|
200
|
+
1. Creates the team
|
|
201
|
+
2. Spawns all agents defined in the template
|
|
202
|
+
3. Each agent gets its predefined prompt, tools, model, and thinking settings
|
|
203
|
+
|
|
204
|
+
**With options:**
|
|
205
|
+
> **You:** "Create a team named 'big-team' from 'full' predefined team using 'gpt-4o' as default model and separate windows."
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 💾 Save Teams as Templates
|
|
210
|
+
|
|
211
|
+
Sometimes you create a team with custom prompts and settings that you'd like to reuse later. Instead of manually creating `teams.yaml` and agent definition files, you can save any runtime team as a template.
|
|
212
|
+
|
|
213
|
+
### The Workflow
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
CREATE → USE → SAVE → REUSE
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
1. **Create** a team with custom teammates and prompts
|
|
220
|
+
2. **Use** the team for your task
|
|
221
|
+
3. **Save** the team as a reusable template
|
|
222
|
+
4. **Reuse** the template later (even on different projects)
|
|
223
|
+
|
|
224
|
+
### List Runtime Teams
|
|
225
|
+
|
|
226
|
+
See which teams you have that can be saved:
|
|
227
|
+
|
|
228
|
+
> **You:** "List all runtime teams."
|
|
229
|
+
|
|
230
|
+
### Save a Team as a Template
|
|
231
|
+
|
|
232
|
+
> **You:** "Save team 'my-modularization-team' as template 'code-modularization'"
|
|
233
|
+
|
|
234
|
+
This creates:
|
|
235
|
+
- Agent definition files in `~/.pi/agent/agents/` for each teammate
|
|
236
|
+
- Updates `~/.pi/teams.yaml` with the new template
|
|
237
|
+
|
|
238
|
+
### Save to Project-Local Scope
|
|
239
|
+
|
|
240
|
+
To save a template that's specific to the current project:
|
|
241
|
+
|
|
242
|
+
> **You:** "Save team 'my-frontend-team' as template 'frontend-sprint' with scope 'project'"
|
|
243
|
+
|
|
244
|
+
This creates files in `.pi/agents/` and `.pi/teams.yaml` in the current project directory.
|
|
245
|
+
|
|
246
|
+
### Reuse Your Template
|
|
247
|
+
|
|
248
|
+
Once saved, use it just like any predefined team:
|
|
249
|
+
|
|
250
|
+
> **You:** "Create a team named 'auth-refactor' from the 'code-modularization' template in the current directory"
|
|
251
|
+
|
|
109
252
|
---
|
|
110
253
|
|
|
111
254
|
## 📚 Learn More
|
package/extensions/index.ts
CHANGED
|
@@ -9,8 +9,10 @@ import * as runtime from "../src/utils/runtime";
|
|
|
9
9
|
import { Member } from "../src/utils/models";
|
|
10
10
|
import { getTerminalAdapter } from "../src/adapters/terminal-registry";
|
|
11
11
|
import { Iterm2Adapter } from "../src/adapters/iterm2-adapter";
|
|
12
|
+
import * as predefined from "../src/utils/predefined-teams";
|
|
12
13
|
import * as path from "node:path";
|
|
13
14
|
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
14
16
|
import { spawnSync } from "node:child_process";
|
|
15
17
|
|
|
16
18
|
// Cache for available models
|
|
@@ -262,6 +264,45 @@ function cleanupStaleTeam(teamName: string, terminal: any): boolean {
|
|
|
262
264
|
return false;
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Clean up orphaned agent session folders from ~/.pi/agent/teams/
|
|
269
|
+
* These are created by the pi core system when agents are spawned.
|
|
270
|
+
* We remove folders that are older than 24 hours to avoid deleting active sessions.
|
|
271
|
+
* Returns the number of folders cleaned up.
|
|
272
|
+
*/
|
|
273
|
+
function cleanupAgentSessionFolders(maxAgeMs: number = 24 * 60 * 60 * 1000): number {
|
|
274
|
+
const agentTeamsDir = path.join(os.homedir(), ".pi", "agent", "teams");
|
|
275
|
+
if (!fs.existsSync(agentTeamsDir)) return 0;
|
|
276
|
+
|
|
277
|
+
let cleaned = 0;
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
|
|
280
|
+
for (const dir of fs.readdirSync(agentTeamsDir)) {
|
|
281
|
+
const sessionDir = path.join(agentTeamsDir, dir);
|
|
282
|
+
const configFile = path.join(sessionDir, "config.json");
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
// Check if this is a directory with a config.json
|
|
286
|
+
if (!fs.statSync(sessionDir).isDirectory()) continue;
|
|
287
|
+
if (!fs.existsSync(configFile)) continue;
|
|
288
|
+
|
|
289
|
+
// Read the config to check the creation time
|
|
290
|
+
const config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
291
|
+
const createdAt = config.createdAt ? new Date(config.createdAt).getTime() : 0;
|
|
292
|
+
|
|
293
|
+
// If the folder is older than maxAgeMs, delete it
|
|
294
|
+
if (createdAt > 0 && (now - createdAt) > maxAgeMs) {
|
|
295
|
+
fs.rmSync(sessionDir, { recursive: true });
|
|
296
|
+
cleaned++;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// Ignore errors for individual folders
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return cleaned;
|
|
304
|
+
}
|
|
305
|
+
|
|
265
306
|
export default function (pi: ExtensionAPI) {
|
|
266
307
|
const isTeammate = !!process.env.PI_AGENT_NAME;
|
|
267
308
|
const agentName = process.env.PI_AGENT_NAME || "team-lead";
|
|
@@ -793,13 +834,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
793
834
|
const tasksDir = paths.taskDir(teamName);
|
|
794
835
|
if (fs.existsSync(tasksDir)) fs.rmSync(tasksDir, { recursive: true });
|
|
795
836
|
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
796
|
-
|
|
837
|
+
|
|
838
|
+
// Clean up orphaned agent session folders (older than 1 hour)
|
|
839
|
+
const cleanedSessions = cleanupAgentSessionFolders(60 * 60 * 1000);
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
content: [{
|
|
843
|
+
type: "text",
|
|
844
|
+
text: `Team ${teamName} shut down.${cleanedSessions > 0 ? ` Cleaned up ${cleanedSessions} orphaned agent session folder(s).` : ""}`
|
|
845
|
+
}],
|
|
846
|
+
details: { cleanedSessions }
|
|
847
|
+
};
|
|
797
848
|
} catch (e) {
|
|
798
849
|
throw new Error(`Failed to shutdown team: ${e}`);
|
|
799
850
|
}
|
|
800
851
|
},
|
|
801
852
|
});
|
|
802
853
|
|
|
854
|
+
pi.registerTool({
|
|
855
|
+
name: "cleanup_agent_sessions",
|
|
856
|
+
label: "Cleanup Agent Sessions",
|
|
857
|
+
description: "Clean up orphaned agent session folders from ~/.pi/agent/teams/ that are older than a specified age.",
|
|
858
|
+
parameters: Type.Object({
|
|
859
|
+
max_age_hours: Type.Optional(Type.Number()),
|
|
860
|
+
}),
|
|
861
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
862
|
+
const maxAgeHours = params.max_age_hours ?? 24;
|
|
863
|
+
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
|
864
|
+
const cleaned = cleanupAgentSessionFolders(maxAgeMs);
|
|
865
|
+
return {
|
|
866
|
+
content: [{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: `Cleaned up ${cleaned} orphaned agent session folder(s) older than ${maxAgeHours} hour(s).`
|
|
869
|
+
}],
|
|
870
|
+
details: { cleaned, maxAgeHours }
|
|
871
|
+
};
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
|
|
803
875
|
pi.registerTool({
|
|
804
876
|
name: "task_read",
|
|
805
877
|
label: "Read Task",
|
|
@@ -897,4 +969,313 @@ export default function (pi: ExtensionAPI) {
|
|
|
897
969
|
};
|
|
898
970
|
},
|
|
899
971
|
});
|
|
972
|
+
|
|
973
|
+
pi.registerTool({
|
|
974
|
+
name: "list_predefined_teams",
|
|
975
|
+
label: "List Predefined Teams",
|
|
976
|
+
description: "List all available predefined team configurations from teams.yaml files. These are team templates that can be instantiated with create_predefined_team.",
|
|
977
|
+
parameters: Type.Object({}),
|
|
978
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
979
|
+
const projectDir = ctx.cwd;
|
|
980
|
+
const predefinedTeams = predefined.getAllPredefinedTeams(projectDir);
|
|
981
|
+
const agents = predefined.getAllAgentDefinitions(projectDir);
|
|
982
|
+
|
|
983
|
+
const result = predefinedTeams.map(team => {
|
|
984
|
+
const teamAgents = team.agents.map(agentName => {
|
|
985
|
+
const agentDef = agents.find(a => a.name === agentName);
|
|
986
|
+
return {
|
|
987
|
+
name: agentName,
|
|
988
|
+
description: agentDef?.description || "(agent definition not found)",
|
|
989
|
+
found: !!agentDef,
|
|
990
|
+
};
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
name: team.name,
|
|
995
|
+
agents: teamAgents,
|
|
996
|
+
};
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
return {
|
|
1000
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1001
|
+
details: { teams: result },
|
|
1002
|
+
};
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
pi.registerTool({
|
|
1007
|
+
name: "list_predefined_agents",
|
|
1008
|
+
label: "List Predefined Agents",
|
|
1009
|
+
description: "List all available predefined agent definitions from .md files. These can be used individually or as part of predefined teams.",
|
|
1010
|
+
parameters: Type.Object({}),
|
|
1011
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
1012
|
+
const projectDir = ctx.cwd;
|
|
1013
|
+
const agents = predefined.getAllAgentDefinitions(projectDir);
|
|
1014
|
+
|
|
1015
|
+
const result = agents.map(agent => ({
|
|
1016
|
+
name: agent.name,
|
|
1017
|
+
description: agent.description,
|
|
1018
|
+
tools: agent.tools,
|
|
1019
|
+
model: agent.model,
|
|
1020
|
+
thinking: agent.thinking,
|
|
1021
|
+
}));
|
|
1022
|
+
|
|
1023
|
+
return {
|
|
1024
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1025
|
+
details: { agents: result },
|
|
1026
|
+
};
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
pi.registerTool({
|
|
1031
|
+
name: "create_predefined_team",
|
|
1032
|
+
label: "Create Predefined Team",
|
|
1033
|
+
description: "Create a team from a predefined team configuration. Spawns all agents defined in the team template from teams.yaml. Each agent is spawned with its predefined prompt, tools, and settings.",
|
|
1034
|
+
parameters: Type.Object({
|
|
1035
|
+
team_name: Type.String({ description: "Name for the new team instance" }),
|
|
1036
|
+
predefined_team: Type.String({ description: "Name of the predefined team template from teams.yaml" }),
|
|
1037
|
+
cwd: Type.String({ description: "Working directory for spawned agents" }),
|
|
1038
|
+
default_model: Type.Optional(Type.String({ description: "Default model for agents without a specified model" })),
|
|
1039
|
+
separate_windows: Type.Optional(Type.Boolean({ default: false, description: "Open teammates in separate OS windows instead of panes" })),
|
|
1040
|
+
}),
|
|
1041
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
1042
|
+
const projectDir = ctx.cwd;
|
|
1043
|
+
const predefinedTeam = predefined.getPredefinedTeam(params.predefined_team, projectDir);
|
|
1044
|
+
|
|
1045
|
+
if (!predefinedTeam) {
|
|
1046
|
+
const available = predefined.getAllPredefinedTeams(projectDir).map(t => t.name);
|
|
1047
|
+
throw new Error(`Predefined team "${params.predefined_team}" not found. Available teams: ${available.join(", ") || "none"}`);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (!terminal) {
|
|
1051
|
+
throw new Error("No terminal adapter detected.");
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Create the team
|
|
1055
|
+
const config = teams.createTeam(params.team_name, "local-session", "lead-agent", `Predefined team: ${params.predefined_team}`, params.default_model, params.separate_windows);
|
|
1056
|
+
registerLeadSession(params.team_name);
|
|
1057
|
+
|
|
1058
|
+
const agentDefinitions = predefined.getAllAgentDefinitions(projectDir);
|
|
1059
|
+
const spawnResults: Array<{ name: string; status: string; error?: string }> = [];
|
|
1060
|
+
|
|
1061
|
+
// Spawn each agent in the predefined team
|
|
1062
|
+
for (const agentName of predefinedTeam.agents) {
|
|
1063
|
+
const agentDef = agentDefinitions.find(a => a.name === agentName);
|
|
1064
|
+
|
|
1065
|
+
if (!agentDef) {
|
|
1066
|
+
spawnResults.push({ name: agentName, status: "skipped", error: "Agent definition not found" });
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
try {
|
|
1071
|
+
const safeName = paths.sanitizeName(agentName);
|
|
1072
|
+
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
1073
|
+
|
|
1074
|
+
let chosenModel = agentDef.model || params.default_model || config.defaultModel;
|
|
1075
|
+
|
|
1076
|
+
if (chosenModel && !chosenModel.includes('/')) {
|
|
1077
|
+
const resolved = resolveModelWithProvider(chosenModel);
|
|
1078
|
+
if (resolved) {
|
|
1079
|
+
chosenModel = resolved;
|
|
1080
|
+
} else if (config.defaultModel && config.defaultModel.includes('/')) {
|
|
1081
|
+
const [provider] = config.defaultModel.split('/');
|
|
1082
|
+
chosenModel = `${provider}/${chosenModel}`;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const useSeparateWindow = params.separate_windows ?? config.separateWindows ?? false;
|
|
1087
|
+
if (useSeparateWindow && !terminal.supportsWindows()) {
|
|
1088
|
+
throw new Error(`Separate windows mode is not supported in ${terminal.name}.`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const member: Member = {
|
|
1092
|
+
agentId: `${safeName}@${safeTeamName}`,
|
|
1093
|
+
name: safeName,
|
|
1094
|
+
agentType: "teammate",
|
|
1095
|
+
model: chosenModel,
|
|
1096
|
+
joinedAt: Date.now(),
|
|
1097
|
+
tmuxPaneId: "",
|
|
1098
|
+
cwd: params.cwd,
|
|
1099
|
+
subscriptions: [],
|
|
1100
|
+
prompt: agentDef.prompt,
|
|
1101
|
+
color: "blue",
|
|
1102
|
+
thinking: agentDef.thinking,
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
await teams.addMember(safeTeamName, member);
|
|
1106
|
+
await messaging.sendPlainMessage(safeTeamName, "team-lead", safeName, agentDef.prompt, "Initial prompt from predefined team");
|
|
1107
|
+
|
|
1108
|
+
const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi";
|
|
1109
|
+
let piCmd = piBinary;
|
|
1110
|
+
|
|
1111
|
+
if (chosenModel) {
|
|
1112
|
+
if (agentDef.thinking) {
|
|
1113
|
+
piCmd = `${piBinary} --model ${chosenModel}:${agentDef.thinking}`;
|
|
1114
|
+
} else {
|
|
1115
|
+
piCmd = `${piBinary} --model ${chosenModel}`;
|
|
1116
|
+
}
|
|
1117
|
+
} else if (agentDef.thinking) {
|
|
1118
|
+
piCmd = `${piBinary} --thinking ${agentDef.thinking}`;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const env: Record<string, string> = {
|
|
1122
|
+
...process.env,
|
|
1123
|
+
PI_TEAM_NAME: safeTeamName,
|
|
1124
|
+
PI_AGENT_NAME: safeName,
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
let terminalId = "";
|
|
1128
|
+
let isWindow = false;
|
|
1129
|
+
|
|
1130
|
+
try {
|
|
1131
|
+
if (useSeparateWindow) {
|
|
1132
|
+
isWindow = true;
|
|
1133
|
+
terminalId = terminal.spawnWindow({
|
|
1134
|
+
name: safeName,
|
|
1135
|
+
cwd: params.cwd,
|
|
1136
|
+
command: piCmd,
|
|
1137
|
+
env: env,
|
|
1138
|
+
teamName: safeTeamName,
|
|
1139
|
+
});
|
|
1140
|
+
await teams.updateMember(safeTeamName, safeName, { windowId: terminalId });
|
|
1141
|
+
} else {
|
|
1142
|
+
if (terminal instanceof Iterm2Adapter) {
|
|
1143
|
+
const teammates = (await teams.readConfig(safeTeamName)).members.filter(m => m.agentType === "teammate" && m.tmuxPaneId.startsWith("iterm_"));
|
|
1144
|
+
const lastTeammate = teammates.length > 0 ? teammates[teammates.length - 1] : null;
|
|
1145
|
+
if (lastTeammate?.tmuxPaneId) {
|
|
1146
|
+
terminal.setSpawnContext({ lastSessionId: lastTeammate.tmuxPaneId.replace("iterm_", "") });
|
|
1147
|
+
} else {
|
|
1148
|
+
terminal.setSpawnContext({});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const leadMember = (await teams.readConfig(safeTeamName)).members.find(m => m.name === "team-lead");
|
|
1153
|
+
const anchorPaneId = terminal.name === "tmux"
|
|
1154
|
+
? leadMember?.tmuxPaneId || process.env.TMUX_PANE || undefined
|
|
1155
|
+
: undefined;
|
|
1156
|
+
|
|
1157
|
+
terminalId = terminal.spawn({
|
|
1158
|
+
name: safeName,
|
|
1159
|
+
cwd: params.cwd,
|
|
1160
|
+
command: piCmd,
|
|
1161
|
+
env: env,
|
|
1162
|
+
anchorPaneId,
|
|
1163
|
+
});
|
|
1164
|
+
await teams.updateMember(safeTeamName, safeName, { tmuxPaneId: terminalId });
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
spawnResults.push({ name: agentName, status: "spawned", error: undefined });
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
spawnResults.push({ name: agentName, status: "error", error: `Failed to spawn: ${e}` });
|
|
1170
|
+
}
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
spawnResults.push({ name: agentName, status: "error", error: String(e) });
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const summary = spawnResults.map(r => `${r.name}: ${r.status}${r.error ? ` (${r.error})` : ""}`).join("\n");
|
|
1177
|
+
|
|
1178
|
+
return {
|
|
1179
|
+
content: [{ type: "text", text: `Team "${params.team_name}" created from predefined team "${params.predefined_team}".\n\nAgent spawn results:\n${summary}` }],
|
|
1180
|
+
details: { teamName: params.team_name, predefinedTeam: params.predefined_team, results: spawnResults },
|
|
1181
|
+
};
|
|
1182
|
+
},
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
pi.registerTool({
|
|
1186
|
+
name: "save_team_as_template",
|
|
1187
|
+
label: "Save Team as Template",
|
|
1188
|
+
description: "Save a runtime team as a reusable predefined team template. Creates agent definition files and updates teams.yaml. Use this when you've created a team with custom prompts and want to reuse it later.",
|
|
1189
|
+
parameters: Type.Object({
|
|
1190
|
+
team_name: Type.String({ description: "Name of the runtime team to save" }),
|
|
1191
|
+
template_name: Type.String({ description: "Name for the template (e.g., 'modularization', 'frontend-team')" }),
|
|
1192
|
+
description: Type.Optional(Type.String({ description: "Description for the template" })),
|
|
1193
|
+
scope: Type.Optional(StringEnum(["user", "project"], { description: "Where to save: 'user' for global (~/.pi), 'project' for project-local (.pi). Defaults to 'user'." })),
|
|
1194
|
+
}),
|
|
1195
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
1196
|
+
const teamName = params.team_name;
|
|
1197
|
+
|
|
1198
|
+
// Verify the team exists
|
|
1199
|
+
if (!teams.teamExists(teamName)) {
|
|
1200
|
+
throw new Error(`Team "${teamName}" does not exist. Use list_runtime_teams to see available teams.`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Read the team configuration
|
|
1204
|
+
const config = await teams.readConfig(teamName);
|
|
1205
|
+
|
|
1206
|
+
// Check that there are teammates to save
|
|
1207
|
+
const teammates = config.members.filter(m => m.agentType === "teammate");
|
|
1208
|
+
if (teammates.length === 0) {
|
|
1209
|
+
throw new Error(`Team "${teamName}" has no teammates to save. Only teams with spawned teammates can be saved as templates.`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Save the team as a template
|
|
1213
|
+
const result = predefined.saveTeamTemplate(config, {
|
|
1214
|
+
templateName: params.template_name,
|
|
1215
|
+
description: params.description,
|
|
1216
|
+
scope: params.scope || "user",
|
|
1217
|
+
projectDir: ctx.cwd,
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// Build summary message
|
|
1221
|
+
const agentSummary = result.savedAgents.map(a =>
|
|
1222
|
+
` - ${a.name}: ${a.existed ? "updated" : "created"} at ${a.path}`
|
|
1223
|
+
).join("\n");
|
|
1224
|
+
|
|
1225
|
+
const message = `Team "${teamName}" saved as template "${params.template_name}".
|
|
1226
|
+
|
|
1227
|
+
Agents saved:
|
|
1228
|
+
${agentSummary}
|
|
1229
|
+
|
|
1230
|
+
Template location: ${result.teamsYamlPath}
|
|
1231
|
+
|
|
1232
|
+
You can now use this template with:
|
|
1233
|
+
create_predefined_team({ team_name: "new-team", predefined_team: "${params.template_name}", cwd: "..." })`;
|
|
1234
|
+
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{ type: "text", text: message }],
|
|
1237
|
+
details: {
|
|
1238
|
+
teamName,
|
|
1239
|
+
templateName: params.template_name,
|
|
1240
|
+
agentsDir: result.agentsDir,
|
|
1241
|
+
teamsYamlPath: result.teamsYamlPath,
|
|
1242
|
+
savedAgents: result.savedAgents,
|
|
1243
|
+
templateExisted: result.templateExisted,
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
},
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
pi.registerTool({
|
|
1250
|
+
name: "list_runtime_teams",
|
|
1251
|
+
label: "List Runtime Teams",
|
|
1252
|
+
description: "List all runtime team configurations that can be saved as templates. These are active or saved teams from ~/.pi/teams/.",
|
|
1253
|
+
parameters: Type.Object({}),
|
|
1254
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
1255
|
+
const runtimeTeams = predefined.listRuntimeTeams();
|
|
1256
|
+
|
|
1257
|
+
if (runtimeTeams.length === 0) {
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{ type: "text", text: "No runtime teams found. Create a team with team_create first." }],
|
|
1260
|
+
details: { teams: [] },
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const result = runtimeTeams.map(team => ({
|
|
1265
|
+
name: team.name,
|
|
1266
|
+
description: team.description,
|
|
1267
|
+
memberCount: team.memberCount,
|
|
1268
|
+
createdAt: team.createdAt ? new Date(team.createdAt).toISOString() : undefined,
|
|
1269
|
+
}));
|
|
1270
|
+
|
|
1271
|
+
const summary = result.map(t =>
|
|
1272
|
+
`- ${t.name}: ${t.memberCount} teammate(s)${t.description ? ` - ${t.description}` : ""}`
|
|
1273
|
+
).join("\n");
|
|
1274
|
+
|
|
1275
|
+
return {
|
|
1276
|
+
content: [{ type: "text", text: `Runtime teams:\n${summary}` }],
|
|
1277
|
+
details: { teams: result },
|
|
1278
|
+
};
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
900
1281
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-teams",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Agent teams for pi, ported from claude-code-teams-mcp",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,7 +9,18 @@
|
|
|
9
9
|
"author": "Mark Burggraf",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"keywords": [
|
|
12
|
-
"pi-package"
|
|
12
|
+
"pi-package",
|
|
13
|
+
"ai-agent",
|
|
14
|
+
"multi-agent",
|
|
15
|
+
"agent-teams",
|
|
16
|
+
"agent-coordination",
|
|
17
|
+
"autonomous-agents",
|
|
18
|
+
"task-management",
|
|
19
|
+
"team-collaboration",
|
|
20
|
+
"agent-orchestration",
|
|
21
|
+
"coding-assistant",
|
|
22
|
+
"terminal",
|
|
23
|
+
"tmux"
|
|
13
24
|
],
|
|
14
25
|
"scripts": {
|
|
15
26
|
"test": "vitest run"
|