pi-teams 0.9.5 → 0.9.6
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 +90 -0
- package/extensions/index.ts +213 -0
- package/package.json +13 -2
- package/src/utils/predefined-teams.test.ts +389 -0
- package/src/utils/predefined-teams.ts +261 -0
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ 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.
|
|
48
49
|
- **Isolated OS Windows**: Launch teammates in true separate OS windows instead of panes.
|
|
49
50
|
- **Persistent Window Titles**: Windows are automatically titled `[team-name]: [agent-name]` for easy identification in your window manager.
|
|
50
51
|
- **Plan Approval Mode**: Require teammates to submit their implementation plans for your approval before they touch any code.
|
|
@@ -108,6 +109,95 @@ Teammates in `planning` mode will use `task_submit_plan`. As the lead, review th
|
|
|
108
109
|
|
|
109
110
|
---
|
|
110
111
|
|
|
112
|
+
## 🏗️ Predefined Teams
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
### Define Team Templates
|
|
117
|
+
|
|
118
|
+
Create `~/.pi/teams.yaml` (global) or `.pi/teams.yaml` in your project:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
# Full development team
|
|
122
|
+
full:
|
|
123
|
+
- scout
|
|
124
|
+
- planner
|
|
125
|
+
- builder
|
|
126
|
+
- reviewer
|
|
127
|
+
- documenter
|
|
128
|
+
|
|
129
|
+
# Quick plan-build cycle
|
|
130
|
+
plan-build:
|
|
131
|
+
- planner
|
|
132
|
+
- builder
|
|
133
|
+
- reviewer
|
|
134
|
+
|
|
135
|
+
# Research and documentation
|
|
136
|
+
research:
|
|
137
|
+
- scout
|
|
138
|
+
- documenter
|
|
139
|
+
|
|
140
|
+
# Frontend specialists
|
|
141
|
+
frontend:
|
|
142
|
+
- planner
|
|
143
|
+
- builder
|
|
144
|
+
- bowser
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Define Agent Definitions
|
|
148
|
+
|
|
149
|
+
Create agent definitions in `~/.pi/agent/agents/` (global) or `.pi/agents/` (project-local):
|
|
150
|
+
|
|
151
|
+
**scout.md:**
|
|
152
|
+
```markdown
|
|
153
|
+
---
|
|
154
|
+
name: scout
|
|
155
|
+
description: Fast recon and codebase exploration
|
|
156
|
+
tools: read,grep,find,ls
|
|
157
|
+
---
|
|
158
|
+
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.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**builder.md:**
|
|
162
|
+
```markdown
|
|
163
|
+
---
|
|
164
|
+
name: builder
|
|
165
|
+
description: Implementation specialist
|
|
166
|
+
tools: read,write,edit,bash
|
|
167
|
+
model: claude-sonnet-4
|
|
168
|
+
thinking: medium
|
|
169
|
+
---
|
|
170
|
+
You are a builder agent. Implement code following the plan provided. Write clean, tested code.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Agent Definition Fields:**
|
|
174
|
+
- `name` (required): The agent's name
|
|
175
|
+
- `description` (required): What the agent does
|
|
176
|
+
- `tools` (optional): Comma or space-separated list of allowed tools
|
|
177
|
+
- `model` (optional): Model to use (e.g., `claude-sonnet-4`, `gpt-4o`)
|
|
178
|
+
- `thinking` (optional): Thinking level (`off`, `minimal`, `low`, `medium`, `high`)
|
|
179
|
+
|
|
180
|
+
### Use Predefined Teams
|
|
181
|
+
|
|
182
|
+
**List available team templates:**
|
|
183
|
+
> **You:** "List all predefined teams I can use."
|
|
184
|
+
|
|
185
|
+
**List available agent definitions:**
|
|
186
|
+
> **You:** "Show me all predefined agents."
|
|
187
|
+
|
|
188
|
+
**Create a team from a template:**
|
|
189
|
+
> **You:** "Create a team named 'my-project' from the 'plan-build' predefined team in the current directory."
|
|
190
|
+
|
|
191
|
+
This single command:
|
|
192
|
+
1. Creates the team
|
|
193
|
+
2. Spawns all agents defined in the template
|
|
194
|
+
3. Each agent gets its predefined prompt, tools, model, and thinking settings
|
|
195
|
+
|
|
196
|
+
**With options:**
|
|
197
|
+
> **You:** "Create a team named 'big-team' from 'full' predefined team using 'gpt-4o' as default model and separate windows."
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
111
201
|
## 📚 Learn More
|
|
112
202
|
|
|
113
203
|
- **[Full Usage Guide](docs/guide.md)** - Detailed examples, hook system, best practices, and troubleshooting
|
package/extensions/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ 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";
|
|
14
15
|
import { spawnSync } from "node:child_process";
|
|
@@ -897,4 +898,216 @@ export default function (pi: ExtensionAPI) {
|
|
|
897
898
|
};
|
|
898
899
|
},
|
|
899
900
|
});
|
|
901
|
+
|
|
902
|
+
pi.registerTool({
|
|
903
|
+
name: "list_predefined_teams",
|
|
904
|
+
label: "List Predefined Teams",
|
|
905
|
+
description: "List all available predefined team configurations from teams.yaml files. These are team templates that can be instantiated with create_predefined_team.",
|
|
906
|
+
parameters: Type.Object({}),
|
|
907
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
908
|
+
const projectDir = ctx.cwd;
|
|
909
|
+
const predefinedTeams = predefined.getAllPredefinedTeams(projectDir);
|
|
910
|
+
const agents = predefined.getAllAgentDefinitions(projectDir);
|
|
911
|
+
|
|
912
|
+
const result = predefinedTeams.map(team => {
|
|
913
|
+
const teamAgents = team.agents.map(agentName => {
|
|
914
|
+
const agentDef = agents.find(a => a.name === agentName);
|
|
915
|
+
return {
|
|
916
|
+
name: agentName,
|
|
917
|
+
description: agentDef?.description || "(agent definition not found)",
|
|
918
|
+
found: !!agentDef,
|
|
919
|
+
};
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
name: team.name,
|
|
924
|
+
agents: teamAgents,
|
|
925
|
+
};
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
930
|
+
details: { teams: result },
|
|
931
|
+
};
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
pi.registerTool({
|
|
936
|
+
name: "list_predefined_agents",
|
|
937
|
+
label: "List Predefined Agents",
|
|
938
|
+
description: "List all available predefined agent definitions from .md files. These can be used individually or as part of predefined teams.",
|
|
939
|
+
parameters: Type.Object({}),
|
|
940
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
941
|
+
const projectDir = ctx.cwd;
|
|
942
|
+
const agents = predefined.getAllAgentDefinitions(projectDir);
|
|
943
|
+
|
|
944
|
+
const result = agents.map(agent => ({
|
|
945
|
+
name: agent.name,
|
|
946
|
+
description: agent.description,
|
|
947
|
+
tools: agent.tools,
|
|
948
|
+
model: agent.model,
|
|
949
|
+
thinking: agent.thinking,
|
|
950
|
+
}));
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
954
|
+
details: { agents: result },
|
|
955
|
+
};
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
pi.registerTool({
|
|
960
|
+
name: "create_predefined_team",
|
|
961
|
+
label: "Create Predefined Team",
|
|
962
|
+
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.",
|
|
963
|
+
parameters: Type.Object({
|
|
964
|
+
team_name: Type.String({ description: "Name for the new team instance" }),
|
|
965
|
+
predefined_team: Type.String({ description: "Name of the predefined team template from teams.yaml" }),
|
|
966
|
+
cwd: Type.String({ description: "Working directory for spawned agents" }),
|
|
967
|
+
default_model: Type.Optional(Type.String({ description: "Default model for agents without a specified model" })),
|
|
968
|
+
separate_windows: Type.Optional(Type.Boolean({ default: false, description: "Open teammates in separate OS windows instead of panes" })),
|
|
969
|
+
}),
|
|
970
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
971
|
+
const projectDir = ctx.cwd;
|
|
972
|
+
const predefinedTeam = predefined.getPredefinedTeam(params.predefined_team, projectDir);
|
|
973
|
+
|
|
974
|
+
if (!predefinedTeam) {
|
|
975
|
+
const available = predefined.getAllPredefinedTeams(projectDir).map(t => t.name);
|
|
976
|
+
throw new Error(`Predefined team "${params.predefined_team}" not found. Available teams: ${available.join(", ") || "none"}`);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (!terminal) {
|
|
980
|
+
throw new Error("No terminal adapter detected.");
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Create the team
|
|
984
|
+
const config = teams.createTeam(params.team_name, "local-session", "lead-agent", `Predefined team: ${params.predefined_team}`, params.default_model, params.separate_windows);
|
|
985
|
+
registerLeadSession(params.team_name);
|
|
986
|
+
|
|
987
|
+
const agentDefinitions = predefined.getAllAgentDefinitions(projectDir);
|
|
988
|
+
const spawnResults: Array<{ name: string; status: string; error?: string }> = [];
|
|
989
|
+
|
|
990
|
+
// Spawn each agent in the predefined team
|
|
991
|
+
for (const agentName of predefinedTeam.agents) {
|
|
992
|
+
const agentDef = agentDefinitions.find(a => a.name === agentName);
|
|
993
|
+
|
|
994
|
+
if (!agentDef) {
|
|
995
|
+
spawnResults.push({ name: agentName, status: "skipped", error: "Agent definition not found" });
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
try {
|
|
1000
|
+
const safeName = paths.sanitizeName(agentName);
|
|
1001
|
+
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
1002
|
+
|
|
1003
|
+
let chosenModel = agentDef.model || params.default_model || config.defaultModel;
|
|
1004
|
+
|
|
1005
|
+
if (chosenModel && !chosenModel.includes('/')) {
|
|
1006
|
+
const resolved = resolveModelWithProvider(chosenModel);
|
|
1007
|
+
if (resolved) {
|
|
1008
|
+
chosenModel = resolved;
|
|
1009
|
+
} else if (config.defaultModel && config.defaultModel.includes('/')) {
|
|
1010
|
+
const [provider] = config.defaultModel.split('/');
|
|
1011
|
+
chosenModel = `${provider}/${chosenModel}`;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const useSeparateWindow = params.separate_windows ?? config.separateWindows ?? false;
|
|
1016
|
+
if (useSeparateWindow && !terminal.supportsWindows()) {
|
|
1017
|
+
throw new Error(`Separate windows mode is not supported in ${terminal.name}.`);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const member: Member = {
|
|
1021
|
+
agentId: `${safeName}@${safeTeamName}`,
|
|
1022
|
+
name: safeName,
|
|
1023
|
+
agentType: "teammate",
|
|
1024
|
+
model: chosenModel,
|
|
1025
|
+
joinedAt: Date.now(),
|
|
1026
|
+
tmuxPaneId: "",
|
|
1027
|
+
cwd: params.cwd,
|
|
1028
|
+
subscriptions: [],
|
|
1029
|
+
prompt: agentDef.prompt,
|
|
1030
|
+
color: "blue",
|
|
1031
|
+
thinking: agentDef.thinking,
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
await teams.addMember(safeTeamName, member);
|
|
1035
|
+
await messaging.sendPlainMessage(safeTeamName, "team-lead", safeName, agentDef.prompt, "Initial prompt from predefined team");
|
|
1036
|
+
|
|
1037
|
+
const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi";
|
|
1038
|
+
let piCmd = piBinary;
|
|
1039
|
+
|
|
1040
|
+
if (chosenModel) {
|
|
1041
|
+
if (agentDef.thinking) {
|
|
1042
|
+
piCmd = `${piBinary} --model ${chosenModel}:${agentDef.thinking}`;
|
|
1043
|
+
} else {
|
|
1044
|
+
piCmd = `${piBinary} --model ${chosenModel}`;
|
|
1045
|
+
}
|
|
1046
|
+
} else if (agentDef.thinking) {
|
|
1047
|
+
piCmd = `${piBinary} --thinking ${agentDef.thinking}`;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const env: Record<string, string> = {
|
|
1051
|
+
...process.env,
|
|
1052
|
+
PI_TEAM_NAME: safeTeamName,
|
|
1053
|
+
PI_AGENT_NAME: safeName,
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
let terminalId = "";
|
|
1057
|
+
let isWindow = false;
|
|
1058
|
+
|
|
1059
|
+
try {
|
|
1060
|
+
if (useSeparateWindow) {
|
|
1061
|
+
isWindow = true;
|
|
1062
|
+
terminalId = terminal.spawnWindow({
|
|
1063
|
+
name: safeName,
|
|
1064
|
+
cwd: params.cwd,
|
|
1065
|
+
command: piCmd,
|
|
1066
|
+
env: env,
|
|
1067
|
+
teamName: safeTeamName,
|
|
1068
|
+
});
|
|
1069
|
+
await teams.updateMember(safeTeamName, safeName, { windowId: terminalId });
|
|
1070
|
+
} else {
|
|
1071
|
+
if (terminal instanceof Iterm2Adapter) {
|
|
1072
|
+
const teammates = (await teams.readConfig(safeTeamName)).members.filter(m => m.agentType === "teammate" && m.tmuxPaneId.startsWith("iterm_"));
|
|
1073
|
+
const lastTeammate = teammates.length > 0 ? teammates[teammates.length - 1] : null;
|
|
1074
|
+
if (lastTeammate?.tmuxPaneId) {
|
|
1075
|
+
terminal.setSpawnContext({ lastSessionId: lastTeammate.tmuxPaneId.replace("iterm_", "") });
|
|
1076
|
+
} else {
|
|
1077
|
+
terminal.setSpawnContext({});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const leadMember = (await teams.readConfig(safeTeamName)).members.find(m => m.name === "team-lead");
|
|
1082
|
+
const anchorPaneId = terminal.name === "tmux"
|
|
1083
|
+
? leadMember?.tmuxPaneId || process.env.TMUX_PANE || undefined
|
|
1084
|
+
: undefined;
|
|
1085
|
+
|
|
1086
|
+
terminalId = terminal.spawn({
|
|
1087
|
+
name: safeName,
|
|
1088
|
+
cwd: params.cwd,
|
|
1089
|
+
command: piCmd,
|
|
1090
|
+
env: env,
|
|
1091
|
+
anchorPaneId,
|
|
1092
|
+
});
|
|
1093
|
+
await teams.updateMember(safeTeamName, safeName, { tmuxPaneId: terminalId });
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
spawnResults.push({ name: agentName, status: "spawned", error: undefined });
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
spawnResults.push({ name: agentName, status: "error", error: `Failed to spawn: ${e}` });
|
|
1099
|
+
}
|
|
1100
|
+
} catch (e) {
|
|
1101
|
+
spawnResults.push({ name: agentName, status: "error", error: String(e) });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const summary = spawnResults.map(r => `${r.name}: ${r.status}${r.error ? ` (${r.error})` : ""}`).join("\n");
|
|
1106
|
+
|
|
1107
|
+
return {
|
|
1108
|
+
content: [{ type: "text", text: `Team "${params.team_name}" created from predefined team "${params.predefined_team}".\n\nAgent spawn results:\n${summary}` }],
|
|
1109
|
+
details: { teamName: params.team_name, predefinedTeam: params.predefined_team, results: spawnResults },
|
|
1110
|
+
};
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
900
1113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-teams",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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"
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
parseAgentFrontmatter,
|
|
7
|
+
parseTeamsYaml,
|
|
8
|
+
discoverAgents,
|
|
9
|
+
discoverTeams,
|
|
10
|
+
getAllAgentDefinitions,
|
|
11
|
+
getAllPredefinedTeams,
|
|
12
|
+
getAgentDefinition,
|
|
13
|
+
getPredefinedTeam,
|
|
14
|
+
} from "./predefined-teams";
|
|
15
|
+
|
|
16
|
+
describe("parseAgentFrontmatter", () => {
|
|
17
|
+
it("parses a valid agent definition with all fields", () => {
|
|
18
|
+
const content = `---
|
|
19
|
+
name: scout
|
|
20
|
+
description: Fast recon and codebase exploration
|
|
21
|
+
tools: read,grep,find,ls
|
|
22
|
+
model: claude-sonnet-4
|
|
23
|
+
thinking: high
|
|
24
|
+
---
|
|
25
|
+
You are a scout agent. Investigate the codebase quickly.`;
|
|
26
|
+
|
|
27
|
+
const result = parseAgentFrontmatter(content, "/test/scout.md");
|
|
28
|
+
|
|
29
|
+
expect(result).not.toBeNull();
|
|
30
|
+
expect(result?.name).toBe("scout");
|
|
31
|
+
expect(result?.description).toBe("Fast recon and codebase exploration");
|
|
32
|
+
expect(result?.tools).toEqual(["read", "grep", "find", "ls"]);
|
|
33
|
+
expect(result?.model).toBe("claude-sonnet-4");
|
|
34
|
+
expect(result?.thinking).toBe("high");
|
|
35
|
+
expect(result?.prompt).toBe("You are a scout agent. Investigate the codebase quickly.");
|
|
36
|
+
expect(result?.filePath).toBe("/test/scout.md");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("parses an agent definition with space-separated tools", () => {
|
|
40
|
+
const content = `---
|
|
41
|
+
name: builder
|
|
42
|
+
description: Code builder
|
|
43
|
+
tools: read write edit bash
|
|
44
|
+
---
|
|
45
|
+
You build things.`;
|
|
46
|
+
|
|
47
|
+
const result = parseAgentFrontmatter(content, "/test/builder.md");
|
|
48
|
+
|
|
49
|
+
expect(result).not.toBeNull();
|
|
50
|
+
expect(result?.tools).toEqual(["read", "write", "edit", "bash"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("parses an agent definition without optional fields", () => {
|
|
54
|
+
const content = `---
|
|
55
|
+
name: simple
|
|
56
|
+
description: Simple agent
|
|
57
|
+
---
|
|
58
|
+
You are simple.`;
|
|
59
|
+
|
|
60
|
+
const result = parseAgentFrontmatter(content, "/test/simple.md");
|
|
61
|
+
|
|
62
|
+
expect(result).not.toBeNull();
|
|
63
|
+
expect(result?.name).toBe("simple");
|
|
64
|
+
expect(result?.description).toBe("Simple agent");
|
|
65
|
+
expect(result?.tools).toBeUndefined();
|
|
66
|
+
expect(result?.model).toBeUndefined();
|
|
67
|
+
expect(result?.thinking).toBeUndefined();
|
|
68
|
+
expect(result?.prompt).toBe("You are simple.");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns null for content without frontmatter", () => {
|
|
72
|
+
const content = "This is just regular markdown without frontmatter.";
|
|
73
|
+
const result = parseAgentFrontmatter(content, "/test/no-frontmatter.md");
|
|
74
|
+
expect(result).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns null for frontmatter without name", () => {
|
|
78
|
+
const content = `---
|
|
79
|
+
description: Missing name field
|
|
80
|
+
---
|
|
81
|
+
Some prompt`;
|
|
82
|
+
const result = parseAgentFrontmatter(content, "/test/no-name.md");
|
|
83
|
+
expect(result).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("parseTeamsYaml", () => {
|
|
88
|
+
it("parses a valid teams.yaml content", () => {
|
|
89
|
+
const content = `
|
|
90
|
+
full:
|
|
91
|
+
- scout
|
|
92
|
+
- planner
|
|
93
|
+
- builder
|
|
94
|
+
|
|
95
|
+
plan-build:
|
|
96
|
+
- planner
|
|
97
|
+
- builder
|
|
98
|
+
- reviewer
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const result = parseTeamsYaml(content);
|
|
102
|
+
|
|
103
|
+
expect(result).toHaveLength(2);
|
|
104
|
+
expect(result[0].name).toBe("full");
|
|
105
|
+
expect(result[0].agents).toEqual(["scout", "planner", "builder"]);
|
|
106
|
+
expect(result[1].name).toBe("plan-build");
|
|
107
|
+
expect(result[1].agents).toEqual(["planner", "builder", "reviewer"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("handles comments and empty lines", () => {
|
|
111
|
+
const content = `
|
|
112
|
+
# This is a comment
|
|
113
|
+
full:
|
|
114
|
+
- scout
|
|
115
|
+
# Another comment
|
|
116
|
+
- planner
|
|
117
|
+
|
|
118
|
+
# Empty line above
|
|
119
|
+
minimal:
|
|
120
|
+
- scout
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
const result = parseTeamsYaml(content);
|
|
124
|
+
|
|
125
|
+
expect(result).toHaveLength(2);
|
|
126
|
+
expect(result[0].agents).toEqual(["scout", "planner"]);
|
|
127
|
+
expect(result[1].agents).toEqual(["scout"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("returns empty array for empty content", () => {
|
|
131
|
+
expect(parseTeamsYaml("")).toEqual([]);
|
|
132
|
+
expect(parseTeamsYaml("# Just comments\n\n")).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("handles tab indentation", () => {
|
|
136
|
+
const content = `
|
|
137
|
+
full:
|
|
138
|
+
\t- scout
|
|
139
|
+
\t- planner
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const result = parseTeamsYaml(content);
|
|
143
|
+
|
|
144
|
+
expect(result).toHaveLength(1);
|
|
145
|
+
expect(result[0].agents).toEqual(["scout", "planner"]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("discoverAgents", () => {
|
|
150
|
+
const testDir = path.join(os.tmpdir(), "pi-teams-test-agents-" + Date.now());
|
|
151
|
+
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
if (fs.existsSync(testDir)) {
|
|
154
|
+
fs.rmSync(testDir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
if (fs.existsSync(testDir)) {
|
|
161
|
+
fs.rmSync(testDir, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("discovers agent definitions from markdown files", () => {
|
|
166
|
+
fs.writeFileSync(path.join(testDir, "scout.md"), `---
|
|
167
|
+
name: scout
|
|
168
|
+
description: Scout agent
|
|
169
|
+
---
|
|
170
|
+
Scout prompt`);
|
|
171
|
+
|
|
172
|
+
fs.writeFileSync(path.join(testDir, "builder.md"), `---
|
|
173
|
+
name: builder
|
|
174
|
+
description: Builder agent
|
|
175
|
+
---
|
|
176
|
+
Builder prompt`);
|
|
177
|
+
|
|
178
|
+
const result = discoverAgents(testDir);
|
|
179
|
+
|
|
180
|
+
expect(result).toHaveLength(2);
|
|
181
|
+
expect(result.find(a => a.name === "scout")).toBeDefined();
|
|
182
|
+
expect(result.find(a => a.name === "builder")).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("discovers agents from SKILL.md in subdirectories", () => {
|
|
186
|
+
const subDir = path.join(testDir, "special-agent");
|
|
187
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
188
|
+
|
|
189
|
+
fs.writeFileSync(path.join(subDir, "SKILL.md"), `---
|
|
190
|
+
name: special
|
|
191
|
+
description: Special agent
|
|
192
|
+
---
|
|
193
|
+
Special prompt`);
|
|
194
|
+
|
|
195
|
+
const result = discoverAgents(testDir);
|
|
196
|
+
|
|
197
|
+
expect(result).toHaveLength(1);
|
|
198
|
+
expect(result[0].name).toBe("special");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("returns empty array for non-existent directory", () => {
|
|
202
|
+
const result = discoverAgents("/non/existent/path");
|
|
203
|
+
expect(result).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("ignores files without valid frontmatter", () => {
|
|
207
|
+
fs.writeFileSync(path.join(testDir, "invalid.md"), "No frontmatter here");
|
|
208
|
+
fs.writeFileSync(path.join(testDir, "valid.md"), `---
|
|
209
|
+
name: valid
|
|
210
|
+
description: Valid agent
|
|
211
|
+
---
|
|
212
|
+
Valid prompt`);
|
|
213
|
+
|
|
214
|
+
const result = discoverAgents(testDir);
|
|
215
|
+
|
|
216
|
+
expect(result).toHaveLength(1);
|
|
217
|
+
expect(result[0].name).toBe("valid");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("discoverTeams", () => {
|
|
222
|
+
const testDir = path.join(os.tmpdir(), "pi-teams-test-teams-" + Date.now());
|
|
223
|
+
|
|
224
|
+
beforeEach(() => {
|
|
225
|
+
if (fs.existsSync(testDir)) {
|
|
226
|
+
fs.rmSync(testDir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
afterEach(() => {
|
|
232
|
+
if (fs.existsSync(testDir)) {
|
|
233
|
+
fs.rmSync(testDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("discovers teams from teams.yaml", () => {
|
|
238
|
+
fs.writeFileSync(path.join(testDir, "teams.yaml"), `
|
|
239
|
+
full:
|
|
240
|
+
- scout
|
|
241
|
+
- planner
|
|
242
|
+
`);
|
|
243
|
+
|
|
244
|
+
const result = discoverTeams(testDir);
|
|
245
|
+
|
|
246
|
+
expect(result).toHaveLength(1);
|
|
247
|
+
expect(result[0].name).toBe("full");
|
|
248
|
+
expect(result[0].agents).toEqual(["scout", "planner"]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("returns empty array when teams.yaml does not exist", () => {
|
|
252
|
+
const result = discoverTeams(testDir);
|
|
253
|
+
expect(result).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("getAllAgentDefinitions and getAllPredefinedTeams", () => {
|
|
258
|
+
const globalDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
259
|
+
const globalTeamsDir = path.join(os.homedir(), ".pi", "agent");
|
|
260
|
+
const projectDir = path.join(os.tmpdir(), "pi-teams-test-project-" + Date.now());
|
|
261
|
+
const projectAgentsDir = path.join(projectDir, ".pi", "agents");
|
|
262
|
+
const projectTeamsDir = path.join(projectDir, ".pi");
|
|
263
|
+
|
|
264
|
+
// Store original files to restore later
|
|
265
|
+
let originalGlobalAgents: string[] = [];
|
|
266
|
+
let originalGlobalTeams: string | null = null;
|
|
267
|
+
|
|
268
|
+
beforeEach(() => {
|
|
269
|
+
// Create project directory
|
|
270
|
+
if (fs.existsSync(projectDir)) {
|
|
271
|
+
fs.rmSync(projectDir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
fs.mkdirSync(projectAgentsDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
// Backup global files if they exist
|
|
276
|
+
if (fs.existsSync(globalDir)) {
|
|
277
|
+
originalGlobalAgents = fs.readdirSync(globalDir);
|
|
278
|
+
}
|
|
279
|
+
if (fs.existsSync(path.join(globalTeamsDir, "teams.yaml"))) {
|
|
280
|
+
originalGlobalTeams = fs.readFileSync(path.join(globalTeamsDir, "teams.yaml"), "utf-8");
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
afterEach(() => {
|
|
285
|
+
if (fs.existsSync(projectDir)) {
|
|
286
|
+
fs.rmSync(projectDir, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("combines global and project-local agents", () => {
|
|
291
|
+
// Create project-local agent
|
|
292
|
+
fs.writeFileSync(path.join(projectAgentsDir, "project-agent.md"), `---
|
|
293
|
+
name: project-agent
|
|
294
|
+
description: Project local agent
|
|
295
|
+
---
|
|
296
|
+
Project prompt`);
|
|
297
|
+
|
|
298
|
+
const result = getAllAgentDefinitions(projectDir);
|
|
299
|
+
|
|
300
|
+
// Should include project-local agent
|
|
301
|
+
expect(result.find(a => a.name === "project-agent")).toBeDefined();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("project-local agents override global agents", () => {
|
|
305
|
+
// Create project-local agent with same name as global
|
|
306
|
+
fs.writeFileSync(path.join(projectAgentsDir, "scout.md"), `---
|
|
307
|
+
name: scout
|
|
308
|
+
description: Project override scout
|
|
309
|
+
---
|
|
310
|
+
Project scout prompt`);
|
|
311
|
+
|
|
312
|
+
const result = getAllAgentDefinitions(projectDir);
|
|
313
|
+
const scout = result.find(a => a.name === "scout");
|
|
314
|
+
|
|
315
|
+
expect(scout).toBeDefined();
|
|
316
|
+
expect(scout?.description).toBe("Project override scout");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("combines global and project-local teams", () => {
|
|
320
|
+
// Create project-local teams.yaml
|
|
321
|
+
fs.writeFileSync(path.join(projectTeamsDir, "teams.yaml"), `
|
|
322
|
+
custom:
|
|
323
|
+
- agent1
|
|
324
|
+
- agent2
|
|
325
|
+
`);
|
|
326
|
+
|
|
327
|
+
const result = getAllPredefinedTeams(projectDir);
|
|
328
|
+
|
|
329
|
+
// Should include project-local team
|
|
330
|
+
expect(result.find(t => t.name === "custom")).toBeDefined();
|
|
331
|
+
expect(result.find(t => t.name === "custom")?.agents).toEqual(["agent1", "agent2"]);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("getAgentDefinition and getPredefinedTeam", () => {
|
|
336
|
+
const projectDir = path.join(os.tmpdir(), "pi-teams-test-get-" + Date.now());
|
|
337
|
+
const projectAgentsDir = path.join(projectDir, ".pi", "agents");
|
|
338
|
+
const projectTeamsDir = path.join(projectDir, ".pi");
|
|
339
|
+
|
|
340
|
+
beforeEach(() => {
|
|
341
|
+
if (fs.existsSync(projectDir)) {
|
|
342
|
+
fs.rmSync(projectDir, { recursive: true });
|
|
343
|
+
}
|
|
344
|
+
fs.mkdirSync(projectAgentsDir, { recursive: true });
|
|
345
|
+
|
|
346
|
+
fs.writeFileSync(path.join(projectAgentsDir, "test-agent.md"), `---
|
|
347
|
+
name: test-agent
|
|
348
|
+
description: Test agent
|
|
349
|
+
---
|
|
350
|
+
Test prompt`);
|
|
351
|
+
|
|
352
|
+
fs.writeFileSync(path.join(projectTeamsDir, "teams.yaml"), `
|
|
353
|
+
test-team:
|
|
354
|
+
- test-agent
|
|
355
|
+
`);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
afterEach(() => {
|
|
359
|
+
if (fs.existsSync(projectDir)) {
|
|
360
|
+
fs.rmSync(projectDir, { recursive: true });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("gets a specific agent definition by name", () => {
|
|
365
|
+
const result = getAgentDefinition("test-agent", projectDir);
|
|
366
|
+
|
|
367
|
+
expect(result).toBeDefined();
|
|
368
|
+
expect(result?.name).toBe("test-agent");
|
|
369
|
+
expect(result?.description).toBe("Test agent");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("returns undefined for non-existent agent", () => {
|
|
373
|
+
const result = getAgentDefinition("non-existent", projectDir);
|
|
374
|
+
expect(result).toBeUndefined();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("gets a specific predefined team by name", () => {
|
|
378
|
+
const result = getPredefinedTeam("test-team", projectDir);
|
|
379
|
+
|
|
380
|
+
expect(result).toBeDefined();
|
|
381
|
+
expect(result?.name).toBe("test-team");
|
|
382
|
+
expect(result?.agents).toEqual(["test-agent"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("returns undefined for non-existent team", () => {
|
|
386
|
+
const result = getPredefinedTeam("non-existent", projectDir);
|
|
387
|
+
expect(result).toBeUndefined();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents an agent definition from a .md file
|
|
7
|
+
*/
|
|
8
|
+
export interface AgentDefinition {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
tools?: string[];
|
|
12
|
+
model?: string;
|
|
13
|
+
thinking?: "off" | "minimal" | "low" | "medium" | "high";
|
|
14
|
+
prompt: string;
|
|
15
|
+
filePath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a predefined team from teams.yaml
|
|
20
|
+
*/
|
|
21
|
+
export interface PredefinedTeam {
|
|
22
|
+
name: string;
|
|
23
|
+
agents: string[];
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse frontmatter from a markdown file
|
|
29
|
+
*/
|
|
30
|
+
export function parseAgentFrontmatter(content: string, filePath: string): AgentDefinition | null {
|
|
31
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
32
|
+
if (!frontmatterMatch) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const frontmatterStr = frontmatterMatch[1];
|
|
37
|
+
const prompt = frontmatterMatch[2].trim();
|
|
38
|
+
const frontmatter: Record<string, string> = {};
|
|
39
|
+
|
|
40
|
+
// Parse YAML-like frontmatter (simple key: value format)
|
|
41
|
+
for (const line of frontmatterStr.split("\n")) {
|
|
42
|
+
const colonIndex = line.indexOf(":");
|
|
43
|
+
if (colonIndex > 0) {
|
|
44
|
+
const key = line.slice(0, colonIndex).trim();
|
|
45
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
46
|
+
frontmatter[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const name = frontmatter.name;
|
|
51
|
+
if (!name) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const description = frontmatter.description || "";
|
|
56
|
+
|
|
57
|
+
// Parse tools (comma-separated or space-separated)
|
|
58
|
+
let tools: string[] | undefined;
|
|
59
|
+
if (frontmatter.tools) {
|
|
60
|
+
tools = frontmatter.tools.split(/[,\s]+/).map(t => t.trim()).filter(t => t);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const thinking = frontmatter.thinking as AgentDefinition["thinking"];
|
|
64
|
+
const model = frontmatter.model;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
description,
|
|
69
|
+
tools,
|
|
70
|
+
model,
|
|
71
|
+
thinking,
|
|
72
|
+
prompt,
|
|
73
|
+
filePath,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Discover agent definitions from a directory
|
|
79
|
+
*/
|
|
80
|
+
export function discoverAgents(dir: string): AgentDefinition[] {
|
|
81
|
+
const agents: AgentDefinition[] = [];
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(dir)) {
|
|
84
|
+
return agents;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
88
|
+
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const fullPath = path.join(dir, entry.name);
|
|
91
|
+
|
|
92
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
93
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
94
|
+
const agent = parseAgentFrontmatter(content, fullPath);
|
|
95
|
+
if (agent) {
|
|
96
|
+
agents.push(agent);
|
|
97
|
+
}
|
|
98
|
+
} else if (entry.isDirectory()) {
|
|
99
|
+
// Check for SKILL.md style (agent-name/SKILL.md)
|
|
100
|
+
const skillPath = path.join(fullPath, "SKILL.md");
|
|
101
|
+
if (fs.existsSync(skillPath)) {
|
|
102
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
103
|
+
const agent = parseAgentFrontmatter(content, skillPath);
|
|
104
|
+
if (agent) {
|
|
105
|
+
agents.push(agent);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return agents;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse teams.yaml content into PredefinedTeam array
|
|
116
|
+
*/
|
|
117
|
+
export function parseTeamsYaml(content: string): PredefinedTeam[] {
|
|
118
|
+
const teams: PredefinedTeam[] = [];
|
|
119
|
+
const lines = content.split("\n");
|
|
120
|
+
let currentTeam: PredefinedTeam | null = null;
|
|
121
|
+
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
// Skip empty lines and comments
|
|
124
|
+
if (!line.trim() || line.trim().startsWith("#")) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check for team definition (no leading spaces, ends with colon)
|
|
129
|
+
if (!line.startsWith(" ") && !line.startsWith("\t") && line.endsWith(":")) {
|
|
130
|
+
// Save previous team
|
|
131
|
+
if (currentTeam && currentTeam.agents.length > 0) {
|
|
132
|
+
teams.push(currentTeam);
|
|
133
|
+
}
|
|
134
|
+
currentTeam = {
|
|
135
|
+
name: line.slice(0, -1).trim(),
|
|
136
|
+
agents: [],
|
|
137
|
+
};
|
|
138
|
+
} else if (currentTeam && (line.startsWith(" ") || line.startsWith("\t"))) {
|
|
139
|
+
// Agent entry (indented line starting with -)
|
|
140
|
+
const agentMatch = line.match(/^\s*-\s*(.+)$/);
|
|
141
|
+
if (agentMatch) {
|
|
142
|
+
currentTeam.agents.push(agentMatch[1].trim());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Save last team
|
|
148
|
+
if (currentTeam && currentTeam.agents.length > 0) {
|
|
149
|
+
teams.push(currentTeam);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return teams;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Discover predefined teams from teams.yaml files
|
|
157
|
+
*/
|
|
158
|
+
export function discoverTeams(dir: string): PredefinedTeam[] {
|
|
159
|
+
const teamsPath = path.join(dir, "teams.yaml");
|
|
160
|
+
|
|
161
|
+
if (!fs.existsSync(teamsPath)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const content = fs.readFileSync(teamsPath, "utf-8");
|
|
167
|
+
return parseTeamsYaml(content);
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get all agent definitions from all locations
|
|
175
|
+
* Priority: project-local > global
|
|
176
|
+
*/
|
|
177
|
+
export function getAllAgentDefinitions(projectDir?: string): AgentDefinition[] {
|
|
178
|
+
const agents: AgentDefinition[] = [];
|
|
179
|
+
const seenNames = new Set<string>();
|
|
180
|
+
|
|
181
|
+
// Global agent definitions
|
|
182
|
+
const globalDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
183
|
+
for (const agent of discoverAgents(globalDir)) {
|
|
184
|
+
if (!seenNames.has(agent.name)) {
|
|
185
|
+
seenNames.add(agent.name);
|
|
186
|
+
agents.push(agent);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Project-local agent definitions
|
|
191
|
+
if (projectDir) {
|
|
192
|
+
const projectAgentsDir = path.join(projectDir, ".pi", "agents");
|
|
193
|
+
for (const agent of discoverAgents(projectAgentsDir)) {
|
|
194
|
+
if (!seenNames.has(agent.name)) {
|
|
195
|
+
seenNames.add(agent.name);
|
|
196
|
+
agents.push(agent);
|
|
197
|
+
} else {
|
|
198
|
+
// Override global with project-local
|
|
199
|
+
const idx = agents.findIndex(a => a.name === agent.name);
|
|
200
|
+
if (idx >= 0) {
|
|
201
|
+
agents[idx] = agent;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return agents;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get all predefined teams from all locations
|
|
212
|
+
* Priority: project-local > global
|
|
213
|
+
*/
|
|
214
|
+
export function getAllPredefinedTeams(projectDir?: string): PredefinedTeam[] {
|
|
215
|
+
const teams: PredefinedTeam[] = [];
|
|
216
|
+
const seenNames = new Set<string>();
|
|
217
|
+
|
|
218
|
+
// Global teams
|
|
219
|
+
const globalDir = path.join(os.homedir(), ".pi", "agent");
|
|
220
|
+
for (const team of discoverTeams(globalDir)) {
|
|
221
|
+
if (!seenNames.has(team.name)) {
|
|
222
|
+
seenNames.add(team.name);
|
|
223
|
+
teams.push(team);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Project-local teams
|
|
228
|
+
if (projectDir) {
|
|
229
|
+
const projectDirPath = path.join(projectDir, ".pi");
|
|
230
|
+
for (const team of discoverTeams(projectDirPath)) {
|
|
231
|
+
if (!seenNames.has(team.name)) {
|
|
232
|
+
seenNames.add(team.name);
|
|
233
|
+
teams.push(team);
|
|
234
|
+
} else {
|
|
235
|
+
// Override global with project-local
|
|
236
|
+
const idx = teams.findIndex(t => t.name === team.name);
|
|
237
|
+
if (idx >= 0) {
|
|
238
|
+
teams[idx] = team;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return teams;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get a specific agent definition by name
|
|
249
|
+
*/
|
|
250
|
+
export function getAgentDefinition(name: string, projectDir?: string): AgentDefinition | undefined {
|
|
251
|
+
const agents = getAllAgentDefinitions(projectDir);
|
|
252
|
+
return agents.find(a => a.name === name);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get a specific predefined team by name
|
|
257
|
+
*/
|
|
258
|
+
export function getPredefinedTeam(name: string, projectDir?: string): PredefinedTeam | undefined {
|
|
259
|
+
const teams = getAllPredefinedTeams(projectDir);
|
|
260
|
+
return teams.find(t => t.name === name);
|
|
261
|
+
}
|