@matyah00/openpi 0.1.2

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.
Files changed (98) hide show
  1. package/README.md +117 -0
  2. package/agents/agent-chain.yaml +113 -0
  3. package/agents/backend.md +13 -0
  4. package/agents/basher.md +27 -0
  5. package/agents/builder.md +14 -0
  6. package/agents/code-searcher.md +27 -0
  7. package/agents/context-pruner.md +29 -0
  8. package/agents/directory-lister.md +25 -0
  9. package/agents/documenter.md +13 -0
  10. package/agents/editor.md +27 -0
  11. package/agents/file-picker.md +27 -0
  12. package/agents/frontend.md +14 -0
  13. package/agents/glob-matcher.md +25 -0
  14. package/agents/librarian.md +27 -0
  15. package/agents/loop-controller.md +41 -0
  16. package/agents/pi-pi/agent-expert.md +97 -0
  17. package/agents/pi-pi/cli-expert.md +41 -0
  18. package/agents/pi-pi/config-expert.md +63 -0
  19. package/agents/pi-pi/ext-expert.md +43 -0
  20. package/agents/pi-pi/keybinding-expert.md +134 -0
  21. package/agents/pi-pi/pi-orchestrator.md +57 -0
  22. package/agents/pi-pi/prompt-expert.md +70 -0
  23. package/agents/pi-pi/skill-expert.md +42 -0
  24. package/agents/pi-pi/theme-expert.md +40 -0
  25. package/agents/pi-pi/tui-expert.md +85 -0
  26. package/agents/plan-reviewer.md +22 -0
  27. package/agents/planner.md +14 -0
  28. package/agents/problem-architect.md +55 -0
  29. package/agents/red-team.md +13 -0
  30. package/agents/reviewer.md +14 -0
  31. package/agents/rule-verifier.md +35 -0
  32. package/agents/scout.md +14 -0
  33. package/agents/security-auditor.md +35 -0
  34. package/agents/ship-guard.md +34 -0
  35. package/agents/spec-reviewer.md +41 -0
  36. package/agents/teams.yaml +73 -0
  37. package/agents/tester.md +27 -0
  38. package/agents/thinker.md +26 -0
  39. package/agents/worker.md +27 -0
  40. package/damage-control-rules.yaml +277 -0
  41. package/extensions/agent-chain.ts +293 -0
  42. package/extensions/agent-team.ts +312 -0
  43. package/extensions/audit-tools.ts +260 -0
  44. package/extensions/commands.ts +169 -0
  45. package/extensions/damage-control-continue.ts +243 -0
  46. package/extensions/lib/packagePaths.ts +13 -0
  47. package/extensions/minimal.ts +34 -0
  48. package/extensions/openpi.ts +255 -0
  49. package/extensions/pure-focus.ts +24 -0
  50. package/extensions/purpose-gate.ts +84 -0
  51. package/extensions/search-tools.ts +277 -0
  52. package/extensions/state-tools.ts +276 -0
  53. package/extensions/system-select.ts +120 -0
  54. package/extensions/theme-cycler.ts +181 -0
  55. package/extensions/themeMap.ts +145 -0
  56. package/extensions/tool-counter-widget.ts +68 -0
  57. package/extensions/tool-counter.ts +102 -0
  58. package/extensions/workflow.ts +642 -0
  59. package/package.json +60 -0
  60. package/prompts/blueprint.md +66 -0
  61. package/prompts/clarify.md +26 -0
  62. package/prompts/compress.md +23 -0
  63. package/prompts/debate.md +23 -0
  64. package/prompts/deep.md +36 -0
  65. package/prompts/deps.md +24 -0
  66. package/prompts/explore.md +22 -0
  67. package/prompts/ghost-test.md +22 -0
  68. package/prompts/goal.md +26 -0
  69. package/prompts/parallel.md +42 -0
  70. package/prompts/plan-team.md +31 -0
  71. package/prompts/prime.md +17 -0
  72. package/prompts/review.md +23 -0
  73. package/prompts/sentinel.md +29 -0
  74. package/prompts/ship.md +30 -0
  75. package/prompts/snapshot.md +26 -0
  76. package/prompts/spec.md +58 -0
  77. package/prompts/test.md +13 -0
  78. package/prompts/validate.md +19 -0
  79. package/skills/bowser/SKILL.md +114 -0
  80. package/skills/env-scanner/SKILL.md +25 -0
  81. package/skills/security-guard/SKILL.md +24 -0
  82. package/skills/session-continuity/SKILL.md +20 -0
  83. package/skills/spec-driven/SKILL.md +25 -0
  84. package/skills/test-first/SKILL.md +23 -0
  85. package/skills/ultrathink/SKILL.md +27 -0
  86. package/themes/catppuccin-mocha.json +86 -0
  87. package/themes/cyberpunk.json +81 -0
  88. package/themes/dracula.json +81 -0
  89. package/themes/everforest.json +82 -0
  90. package/themes/gruvbox.json +80 -0
  91. package/themes/midnight-ocean.json +76 -0
  92. package/themes/nord.json +84 -0
  93. package/themes/ocean-breeze.json +83 -0
  94. package/themes/rose-pine.json +82 -0
  95. package/themes/synthwave.json +82 -0
  96. package/themes/tokyo-night.json +83 -0
  97. package/tsconfig.json +15 -0
  98. package/types/pi-shims.d.ts +102 -0
@@ -0,0 +1,293 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { Text } from "@mariozechner/pi-tui";
4
+ import { spawn } from "child_process";
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs";
6
+ import { join, resolve } from "path";
7
+ import { bundledAgentsDir } from "./lib/packagePaths.ts";
8
+
9
+ type ChainStep = { agent: string; prompt: string };
10
+ type ChainDef = { name: string; description: string; steps: ChainStep[] };
11
+ type AgentDef = { name: string; description: string; tools: string; systemPrompt: string };
12
+ type StepState = { agent: string; status: "pending" | "running" | "done" | "error"; elapsed: number; lastWork: string };
13
+
14
+ function displayName(name: string): string {
15
+ return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
16
+ }
17
+
18
+ function parseChainYaml(raw: string): ChainDef[] {
19
+ const chains: ChainDef[] = [];
20
+ let current: ChainDef | null = null;
21
+ let currentStep: ChainStep | null = null;
22
+
23
+ for (const line of raw.split("\n")) {
24
+ const chainMatch = line.match(/^(\S[^:]*):$/);
25
+ if (chainMatch) {
26
+ if (current && currentStep) current.steps.push(currentStep);
27
+ current = { name: chainMatch[1].trim(), description: "", steps: [] };
28
+ currentStep = null;
29
+ chains.push(current);
30
+ continue;
31
+ }
32
+ const descMatch = line.match(/^\s+description:\s+(.+)$/);
33
+ if (descMatch && current && !currentStep) {
34
+ current.description = descMatch[1].trim().replace(/^["']|["']$/g, "");
35
+ continue;
36
+ }
37
+ const agentMatch = line.match(/^\s+-\s+agent:\s+(.+)$/);
38
+ if (agentMatch && current) {
39
+ if (currentStep) current.steps.push(currentStep);
40
+ currentStep = { agent: agentMatch[1].trim(), prompt: "" };
41
+ continue;
42
+ }
43
+ const promptMatch = line.match(/^\s+prompt:\s+(.+)$/);
44
+ if (promptMatch && currentStep) {
45
+ currentStep.prompt = promptMatch[1].trim().replace(/^["']|["']$/g, "").replace(/\\n/g, "\n");
46
+ }
47
+ }
48
+ if (current && currentStep) current.steps.push(currentStep);
49
+ return chains;
50
+ }
51
+
52
+ function parseAgentFile(filePath: string): AgentDef | null {
53
+ const raw = readFileSync(filePath, "utf-8");
54
+ const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
55
+ if (!match) return null;
56
+ const frontmatter: Record<string, string> = {};
57
+ for (const line of match[1].split("\n")) {
58
+ const idx = line.indexOf(":");
59
+ if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
60
+ }
61
+ if (!frontmatter.name) return null;
62
+ return {
63
+ name: frontmatter.name,
64
+ description: frontmatter.description || "",
65
+ tools: frontmatter.tools || "read,grep,find,ls",
66
+ systemPrompt: match[2].trim(),
67
+ };
68
+ }
69
+
70
+ function scanAgentDirs(cwd: string): Map<string, AgentDef> {
71
+ const dirs = [join(cwd, ".pi", "agents"), join(cwd, "agents"), bundledAgentsDir];
72
+ const agents = new Map<string, AgentDef>();
73
+ for (const dir of dirs) {
74
+ if (!existsSync(dir)) continue;
75
+ for (const file of readdirSync(dir)) {
76
+ if (!file.endsWith(".md")) continue;
77
+ const def = parseAgentFile(resolve(dir, file));
78
+ if (def && !agents.has(def.name.toLowerCase())) agents.set(def.name.toLowerCase(), def);
79
+ }
80
+ }
81
+ return agents;
82
+ }
83
+
84
+ export default function (pi: ExtensionAPI) {
85
+ let allAgents = new Map<string, AgentDef>();
86
+ let chains: ChainDef[] = [];
87
+ let activeChain: ChainDef | null = null;
88
+ let sessionDir = "";
89
+ let widgetCtx: any;
90
+ let stepStates: StepState[] = [];
91
+ const agentSessions = new Map<string, string | null>();
92
+
93
+ function loadChains(cwd: string) {
94
+ sessionDir = join(cwd, ".pi", "agent-sessions");
95
+ mkdirSync(sessionDir, { recursive: true });
96
+ allAgents = scanAgentDirs(cwd);
97
+ agentSessions.clear();
98
+ for (const key of allAgents.keys()) {
99
+ const sessionFile = join(sessionDir, `chain-${key}.json`);
100
+ agentSessions.set(key, existsSync(sessionFile) ? sessionFile : null);
101
+ }
102
+ const chainPath = [join(cwd, ".pi", "agents", "agent-chain.yaml"), join(bundledAgentsDir, "agent-chain.yaml")].find((candidate) => existsSync(candidate));
103
+ chains = chainPath ? parseChainYaml(readFileSync(chainPath, "utf-8")) : [];
104
+ }
105
+
106
+ function activateChain(chain: ChainDef) {
107
+ activeChain = chain;
108
+ stepStates = chain.steps.map((step) => ({ agent: step.agent, status: "pending", elapsed: 0, lastWork: "" }));
109
+ updateWidget();
110
+ }
111
+
112
+ function updateWidget() {
113
+ if (!widgetCtx) return;
114
+ widgetCtx.ui.setWidget("openpi-chain", (_tui: any, theme: any) => ({
115
+ render(): string[] {
116
+ if (!activeChain) return [theme.fg("dim", "No chain active")];
117
+ const flow = stepStates.map((step) => `${step.status}:${displayName(step.agent)}`).join(" -> ");
118
+ return [theme.fg("muted", `Chain: ${activeChain.name}`), theme.fg("dim", flow)];
119
+ },
120
+ invalidate() {},
121
+ }));
122
+ }
123
+
124
+ function runAgent(agentDef: AgentDef, task: string, stepIndex: number, ctx: any): Promise<{ output: string; exitCode: number; elapsed: number }> {
125
+ const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
126
+ const agentKey = agentDef.name.toLowerCase().replace(/\s+/g, "-");
127
+ const agentSessionFile = join(sessionDir, `chain-${agentKey}.json`);
128
+ const args = [
129
+ "--mode", "json",
130
+ "-p",
131
+ "--no-extensions",
132
+ "--tools", agentDef.tools,
133
+ "--thinking", "off",
134
+ "--append-system-prompt", agentDef.systemPrompt,
135
+ "--session", agentSessionFile,
136
+ ];
137
+ if (model) args.splice(4, 0, "--model", model);
138
+ if (agentSessions.get(agentKey)) args.push("-c");
139
+ args.push(task);
140
+
141
+ const state = stepStates[stepIndex];
142
+ const start = Date.now();
143
+ const chunks: string[] = [];
144
+
145
+ return new Promise((resolveDone) => {
146
+ const proc = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
147
+ const timer = setInterval(() => {
148
+ state.elapsed = Date.now() - start;
149
+ updateWidget();
150
+ }, 1000);
151
+ let buffer = "";
152
+
153
+ proc.stdout!.setEncoding("utf-8");
154
+ proc.stdout!.on("data", (chunk: string) => {
155
+ buffer += chunk;
156
+ const lines = buffer.split("\n");
157
+ buffer = lines.pop() || "";
158
+ for (const line of lines) {
159
+ try {
160
+ const event = JSON.parse(line);
161
+ const delta = event.assistantMessageEvent;
162
+ if (event.type === "message_update" && delta?.type === "text_delta") {
163
+ chunks.push(delta.delta || "");
164
+ state.lastWork = chunks.join("").split("\n").filter(Boolean).pop() || "";
165
+ updateWidget();
166
+ }
167
+ } catch {}
168
+ }
169
+ });
170
+ proc.stderr!.setEncoding("utf-8");
171
+ proc.stderr!.on("data", () => {});
172
+ proc.on("close", (code) => {
173
+ clearInterval(timer);
174
+ state.elapsed = Date.now() - start;
175
+ if (code === 0) agentSessions.set(agentKey, agentSessionFile);
176
+ resolveDone({ output: chunks.join(""), exitCode: code ?? 1, elapsed: state.elapsed });
177
+ });
178
+ proc.on("error", (error) => {
179
+ clearInterval(timer);
180
+ resolveDone({ output: `Error spawning agent: ${error.message}`, exitCode: 1, elapsed: Date.now() - start });
181
+ });
182
+ });
183
+ }
184
+
185
+ async function runChain(task: string, ctx: any): Promise<{ output: string; success: boolean; elapsed: number }> {
186
+ if (!activeChain) return { output: "No chain active", success: false, elapsed: 0 };
187
+ const start = Date.now();
188
+ stepStates = activeChain.steps.map((step) => ({ agent: step.agent, status: "pending", elapsed: 0, lastWork: "" }));
189
+ updateWidget();
190
+
191
+ let input = task;
192
+ const original = task;
193
+ for (let i = 0; i < activeChain.steps.length; i++) {
194
+ const step = activeChain.steps[i];
195
+ stepStates[i].status = "running";
196
+ updateWidget();
197
+
198
+ const prompt = step.prompt.replace(/\$INPUT/g, input).replace(/\$ORIGINAL/g, original);
199
+ const agentDef = allAgents.get(step.agent.toLowerCase());
200
+ if (!agentDef) {
201
+ stepStates[i].status = "error";
202
+ return { output: `Agent "${step.agent}" not found`, success: false, elapsed: Date.now() - start };
203
+ }
204
+
205
+ const result = await runAgent(agentDef, prompt, i, ctx);
206
+ if (result.exitCode !== 0) {
207
+ stepStates[i].status = "error";
208
+ return { output: result.output, success: false, elapsed: Date.now() - start };
209
+ }
210
+ stepStates[i].status = "done";
211
+ input = result.output;
212
+ updateWidget();
213
+ }
214
+ return { output: input, success: true, elapsed: Date.now() - start };
215
+ }
216
+
217
+ pi.registerTool({
218
+ name: "run_chain",
219
+ label: "Run Chain",
220
+ description: "Run the active Pi-native agent chain. Each step feeds output into the next step.",
221
+ parameters: Type.Object({ task: Type.String({ description: "Task for the chain" }) }),
222
+ async execute(_toolCallId, params, _signal, onUpdate, ctx) {
223
+ const { task } = params as { task: string };
224
+ onUpdate?.({ content: [{ type: "text", text: `Starting chain ${activeChain?.name || "none"}...` }], details: { task, status: "running" } });
225
+ const result = await runChain(task, ctx);
226
+ const text = result.output.length > 8000 ? `${result.output.slice(0, 8000)}\n\n... [truncated]` : result.output;
227
+ return {
228
+ content: [{ type: "text", text: `[chain:${activeChain?.name || "none"}] ${result.success ? "done" : "error"} in ${Math.round(result.elapsed / 1000)}s\n\n${text}` }],
229
+ details: { chain: activeChain?.name, task, success: result.success, elapsed: result.elapsed, fullOutput: result.output },
230
+ };
231
+ },
232
+ renderCall(args, theme) {
233
+ return new Text(`${theme.fg("toolTitle", "run_chain ")}${theme.fg("accent", activeChain?.name || "?")} ${theme.fg("muted", (args as any).task || "")}`, 0, 0);
234
+ },
235
+ });
236
+
237
+ pi.registerCommand("chain", {
238
+ description: "Select active Pi agent chain",
239
+ handler: async (_args, ctx) => {
240
+ if (!chains.length) {
241
+ ctx.ui.notify("No chains defined", "warning");
242
+ return;
243
+ }
244
+ const options = chains.map((chain) => `${chain.name} - ${chain.description}`);
245
+ const choice = await ctx.ui.select("Select Chain", options);
246
+ if (!choice) return;
247
+ const chain = chains[options.indexOf(choice)];
248
+ activateChain(chain);
249
+ ctx.ui.setStatus("openpi-chain", `Chain: ${chain.name}`);
250
+ },
251
+ });
252
+
253
+ pi.registerCommand("chain-list", {
254
+ description: "List Pi agent chains",
255
+ handler: async () => {
256
+ const lines = ["# Chains", "", ...chains.map((chain) => `- **${chain.name}** - ${chain.description}`)];
257
+ pi.sendMessage({ customType: "openpi-chains", content: lines.join("\n"), display: true });
258
+ },
259
+ });
260
+
261
+ pi.on("before_agent_start", async (event) => {
262
+ if (!activeChain) return;
263
+ const flow = activeChain.steps.map((step) => displayName(step.agent)).join(" -> ");
264
+ return {
265
+ systemPrompt: `You have access to the run_chain tool for structured Pi-native workflows.
266
+
267
+ Active chain: ${activeChain.name}
268
+ Flow: ${flow}
269
+
270
+ Use run_chain for multi-step work that benefits from planning, implementation, or review. For simple questions, answer directly.
271
+
272
+ ${event.systemPrompt}`,
273
+ };
274
+ });
275
+
276
+ pi.on("session_start", async (_event, ctx) => {
277
+ widgetCtx = ctx;
278
+ const sessDir = join(ctx.cwd, ".pi", "agent-sessions");
279
+ if (existsSync(sessDir)) {
280
+ for (const file of readdirSync(sessDir)) {
281
+ if (file.startsWith("chain-") && file.endsWith(".json")) {
282
+ try { unlinkSync(join(sessDir, file)); } catch {}
283
+ }
284
+ }
285
+ }
286
+ loadChains(ctx.cwd);
287
+ if (chains[0]) activateChain(chains[0]);
288
+ if (ctx.hasUI) {
289
+ ctx.ui.setStatus("openpi-chain", activeChain ? `Chain: ${activeChain.name}` : "Chain: none");
290
+ ctx.ui.notify(activeChain ? `Chain: ${activeChain.name}\n/chain select chain\n/chain-list list chains` : "No chains found", "info");
291
+ }
292
+ });
293
+ }
@@ -0,0 +1,312 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { Text } from "@mariozechner/pi-tui";
4
+ import { spawn } from "child_process";
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs";
6
+ import { join, resolve } from "path";
7
+ import { bundledAgentsDir } from "./lib/packagePaths.ts";
8
+
9
+ type AgentDef = {
10
+ name: string;
11
+ description: string;
12
+ tools: string;
13
+ systemPrompt: string;
14
+ file: string;
15
+ };
16
+
17
+ type AgentState = {
18
+ def: AgentDef;
19
+ status: "idle" | "running" | "done" | "error";
20
+ task: string;
21
+ elapsed: number;
22
+ lastWork: string;
23
+ sessionFile: string | null;
24
+ };
25
+
26
+ function displayName(name: string): string {
27
+ return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
28
+ }
29
+
30
+ function parseTeamsYaml(raw: string): Record<string, string[]> {
31
+ const teams: Record<string, string[]> = {};
32
+ let current: string | null = null;
33
+ for (const line of raw.split("\n")) {
34
+ const teamMatch = line.match(/^(\S[^:]*):$/);
35
+ if (teamMatch) {
36
+ current = teamMatch[1].trim();
37
+ teams[current] = [];
38
+ continue;
39
+ }
40
+ const itemMatch = line.match(/^\s+-\s+(.+)$/);
41
+ if (itemMatch && current) teams[current].push(itemMatch[1].trim());
42
+ }
43
+ return teams;
44
+ }
45
+
46
+ function parseAgentFile(filePath: string): AgentDef | null {
47
+ const raw = readFileSync(filePath, "utf-8");
48
+ const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
49
+ if (!match) return null;
50
+
51
+ const frontmatter: Record<string, string> = {};
52
+ for (const line of match[1].split("\n")) {
53
+ const idx = line.indexOf(":");
54
+ if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
55
+ }
56
+ if (!frontmatter.name) return null;
57
+
58
+ return {
59
+ name: frontmatter.name,
60
+ description: frontmatter.description || "",
61
+ tools: frontmatter.tools || "read,grep,find,ls",
62
+ systemPrompt: match[2].trim(),
63
+ file: filePath,
64
+ };
65
+ }
66
+
67
+ function scanAgentDirs(cwd: string): AgentDef[] {
68
+ const dirs = [join(cwd, ".pi", "agents"), join(cwd, "agents"), bundledAgentsDir];
69
+ const agents: AgentDef[] = [];
70
+ const seen = new Set<string>();
71
+
72
+ for (const dir of dirs) {
73
+ if (!existsSync(dir)) continue;
74
+ for (const file of readdirSync(dir)) {
75
+ if (!file.endsWith(".md")) continue;
76
+ const def = parseAgentFile(resolve(dir, file));
77
+ if (!def) continue;
78
+ const key = def.name.toLowerCase();
79
+ if (seen.has(key)) continue;
80
+ seen.add(key);
81
+ agents.push(def);
82
+ }
83
+ }
84
+
85
+ return agents;
86
+ }
87
+
88
+ export default function (pi: ExtensionAPI) {
89
+ const agentStates = new Map<string, AgentState>();
90
+ let allAgentDefs: AgentDef[] = [];
91
+ let teams: Record<string, string[]> = {};
92
+ let activeTeamName = "";
93
+ let sessionDir = "";
94
+ let widgetCtx: any;
95
+
96
+ function loadAgents(cwd: string) {
97
+ sessionDir = join(cwd, ".pi", "agent-sessions");
98
+ mkdirSync(sessionDir, { recursive: true });
99
+ allAgentDefs = scanAgentDirs(cwd);
100
+
101
+ const teamsPath = [join(cwd, ".pi", "agents", "teams.yaml"), join(bundledAgentsDir, "teams.yaml")].find((candidate) => existsSync(candidate));
102
+ teams = teamsPath ? parseTeamsYaml(readFileSync(teamsPath, "utf-8")) : {};
103
+ if (Object.keys(teams).length === 0) teams = { all: allAgentDefs.map((agent) => agent.name) };
104
+ }
105
+
106
+ function activateTeam(teamName: string) {
107
+ activeTeamName = teamName;
108
+ const defsByName = new Map(allAgentDefs.map((agent) => [agent.name.toLowerCase(), agent]));
109
+ agentStates.clear();
110
+
111
+ for (const member of teams[teamName] || []) {
112
+ const def = defsByName.get(member.toLowerCase());
113
+ if (!def) continue;
114
+ const key = def.name.toLowerCase();
115
+ const sessionFile = join(sessionDir, `${key}.json`);
116
+ agentStates.set(key, {
117
+ def,
118
+ status: "idle",
119
+ task: "",
120
+ elapsed: 0,
121
+ lastWork: "",
122
+ sessionFile: existsSync(sessionFile) ? sessionFile : null,
123
+ });
124
+ }
125
+ }
126
+
127
+ function updateWidget() {
128
+ if (!widgetCtx) return;
129
+ widgetCtx.ui.setWidget("openpi-team", (_tui: any, theme: any) => ({
130
+ render(width: number): string[] {
131
+ const lines = [`Team: ${activeTeamName || "none"}`];
132
+ for (const state of agentStates.values()) {
133
+ const task = state.task ? ` - ${state.task.slice(0, Math.max(20, width - 40))}` : "";
134
+ lines.push(`${state.status.padEnd(7)} ${displayName(state.def.name)}${task}`);
135
+ }
136
+ return lines.map((line) => theme.fg("muted", line));
137
+ },
138
+ invalidate() {},
139
+ }));
140
+ }
141
+
142
+ function dispatchAgent(agentName: string, task: string, ctx: any): Promise<{ output: string; exitCode: number; elapsed: number }> {
143
+ const state = agentStates.get(agentName.toLowerCase());
144
+ if (!state) {
145
+ return Promise.resolve({
146
+ output: `Agent "${agentName}" not found. Available: ${Array.from(agentStates.values()).map((item) => item.def.name).join(", ")}`,
147
+ exitCode: 1,
148
+ elapsed: 0,
149
+ });
150
+ }
151
+ if (state.status === "running") {
152
+ return Promise.resolve({ output: `Agent "${state.def.name}" is already running.`, exitCode: 1, elapsed: 0 });
153
+ }
154
+
155
+ state.status = "running";
156
+ state.task = task;
157
+ state.elapsed = 0;
158
+ state.lastWork = "";
159
+ updateWidget();
160
+
161
+ const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
162
+ const agentKey = state.def.name.toLowerCase().replace(/\s+/g, "-");
163
+ const agentSessionFile = join(sessionDir, `${agentKey}.json`);
164
+ const args = [
165
+ "--mode", "json",
166
+ "-p",
167
+ "--no-extensions",
168
+ "--tools", state.def.tools,
169
+ "--thinking", "off",
170
+ "--append-system-prompt", state.def.systemPrompt,
171
+ "--session", agentSessionFile,
172
+ ];
173
+ if (model) args.splice(4, 0, "--model", model);
174
+ if (state.sessionFile) args.push("-c");
175
+ args.push(task);
176
+
177
+ const start = Date.now();
178
+ const chunks: string[] = [];
179
+
180
+ return new Promise((resolveDone) => {
181
+ const proc = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
182
+ const timer = setInterval(() => {
183
+ state.elapsed = Date.now() - start;
184
+ updateWidget();
185
+ }, 1000);
186
+ let buffer = "";
187
+
188
+ proc.stdout!.setEncoding("utf-8");
189
+ proc.stdout!.on("data", (chunk: string) => {
190
+ buffer += chunk;
191
+ const lines = buffer.split("\n");
192
+ buffer = lines.pop() || "";
193
+ for (const line of lines) {
194
+ try {
195
+ const event = JSON.parse(line);
196
+ const delta = event.assistantMessageEvent;
197
+ if (event.type === "message_update" && delta?.type === "text_delta") {
198
+ chunks.push(delta.delta || "");
199
+ state.lastWork = chunks.join("").split("\n").filter(Boolean).pop() || "";
200
+ updateWidget();
201
+ }
202
+ } catch {}
203
+ }
204
+ });
205
+ proc.stderr!.setEncoding("utf-8");
206
+ proc.stderr!.on("data", () => {});
207
+ proc.on("close", (code) => {
208
+ clearInterval(timer);
209
+ state.elapsed = Date.now() - start;
210
+ state.status = code === 0 ? "done" : "error";
211
+ if (code === 0) state.sessionFile = agentSessionFile;
212
+ updateWidget();
213
+ resolveDone({ output: chunks.join(""), exitCode: code ?? 1, elapsed: state.elapsed });
214
+ });
215
+ proc.on("error", (error) => {
216
+ clearInterval(timer);
217
+ state.status = "error";
218
+ state.lastWork = error.message;
219
+ updateWidget();
220
+ resolveDone({ output: `Error spawning agent: ${error.message}`, exitCode: 1, elapsed: Date.now() - start });
221
+ });
222
+ });
223
+ }
224
+
225
+ pi.registerTool({
226
+ name: "dispatch_agent",
227
+ label: "Dispatch Agent",
228
+ description: "Dispatch a task to a Pi-native specialist agent in the active team.",
229
+ parameters: Type.Object({
230
+ agent: Type.String({ description: "Agent name" }),
231
+ task: Type.String({ description: "Task for the specialist agent" }),
232
+ }),
233
+ async execute(_toolCallId, params, _signal, onUpdate, ctx) {
234
+ const { agent, task } = params as { agent: string; task: string };
235
+ onUpdate?.({ content: [{ type: "text", text: `Dispatching to ${agent}...` }], details: { agent, task, status: "running" } });
236
+ const result = await dispatchAgent(agent, task, ctx);
237
+ const text = result.output.length > 8000 ? `${result.output.slice(0, 8000)}\n\n... [truncated]` : result.output;
238
+ return {
239
+ content: [{ type: "text", text: `[${agent}] ${result.exitCode === 0 ? "done" : "error"} in ${Math.round(result.elapsed / 1000)}s\n\n${text}` }],
240
+ details: { agent, task, exitCode: result.exitCode, elapsed: result.elapsed, fullOutput: result.output },
241
+ };
242
+ },
243
+ renderCall(args, theme) {
244
+ return new Text(`${theme.fg("toolTitle", "dispatch_agent ")}${theme.fg("accent", (args as any).agent || "?")}`, 0, 0);
245
+ },
246
+ });
247
+
248
+ pi.registerCommand("agents-team", {
249
+ description: "Select active Pi agent team",
250
+ handler: async (_args, ctx) => {
251
+ const names = Object.keys(teams);
252
+ if (!names.length) {
253
+ ctx.ui.notify("No teams defined", "warning");
254
+ return;
255
+ }
256
+ const choice = await ctx.ui.select("Select Team", names);
257
+ if (!choice) return;
258
+ activateTeam(choice);
259
+ pi.setActiveTools(["dispatch_agent"]);
260
+ ctx.ui.setStatus("openpi-team", `Team: ${activeTeamName} (${agentStates.size})`);
261
+ updateWidget();
262
+ },
263
+ });
264
+
265
+ pi.registerCommand("agents-list", {
266
+ description: "List active Pi team agents",
267
+ handler: async () => {
268
+ const lines = ["# Active Agents", "", ...Array.from(agentStates.values()).map((state) => `- **${state.def.name}** (${state.status}) - ${state.def.description}`)];
269
+ pi.sendMessage({ customType: "openpi-agents", content: lines.join("\n"), display: true });
270
+ },
271
+ });
272
+
273
+ pi.on("before_agent_start", async () => {
274
+ const catalog = Array.from(agentStates.values())
275
+ .map((state) => `### ${displayName(state.def.name)}\nDispatch as: \`${state.def.name}\`\n${state.def.description}\nTools: ${state.def.tools}`)
276
+ .join("\n\n");
277
+ return {
278
+ systemPrompt: `You are a dispatcher agent. You do not work directly on the codebase. Use dispatch_agent for all substantive work.
279
+
280
+ Active team: ${activeTeamName}
281
+
282
+ ${catalog}
283
+
284
+ Rules:
285
+ - Break work into clear subtasks.
286
+ - Dispatch to the best specialist.
287
+ - Review results before answering the user.
288
+ - Do not use tools other than dispatch_agent.`,
289
+ };
290
+ });
291
+
292
+ pi.on("session_start", async (_event, ctx) => {
293
+ widgetCtx = ctx;
294
+ const sessDir = join(ctx.cwd, ".pi", "agent-sessions");
295
+ if (existsSync(sessDir)) {
296
+ for (const file of readdirSync(sessDir)) {
297
+ if (file.endsWith(".json")) {
298
+ try { unlinkSync(join(sessDir, file)); } catch {}
299
+ }
300
+ }
301
+ }
302
+ loadAgents(ctx.cwd);
303
+ const first = Object.keys(teams)[0];
304
+ if (first) activateTeam(first);
305
+ pi.setActiveTools(["dispatch_agent"]);
306
+ if (ctx.hasUI) {
307
+ ctx.ui.setStatus("openpi-team", `Team: ${activeTeamName} (${agentStates.size})`);
308
+ ctx.ui.notify(`Team: ${activeTeamName}\n/agents-team select team\n/agents-list list agents`, "info");
309
+ }
310
+ updateWidget();
311
+ });
312
+ }