@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,642 @@
1
+ /**
2
+ * openpi Workflow Extension
3
+ *
4
+ * Adds a Pi-native orchestration surface without changing Pi internals:
5
+ * - /add, /fix, /review commands that inject structured workflow prompts
6
+ * - spawn_agents tool that runs isolated Pi subprocesses for role agents
7
+ *
8
+ * Security posture:
9
+ * - Project agents are only loaded from .pi/agents under the current project
10
+ * - Per-agent cwd overrides must stay inside the current project directory
11
+ * - No API keys or secrets are accepted as parameters
12
+ */
13
+
14
+ import { spawn } from "node:child_process";
15
+ import * as fs from "node:fs";
16
+ import * as os from "node:os";
17
+ import * as path from "node:path";
18
+ import type { Message } from "@earendil-works/pi-ai";
19
+ import { StringEnum } from "@earendil-works/pi-ai";
20
+ import { type ExtensionAPI, getAgentDir, parseFrontmatter, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
21
+ import { Text } from "@earendil-works/pi-tui";
22
+ import { Type } from "typebox";
23
+
24
+ type AgentScope = "user" | "project" | "both";
25
+ type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
26
+ type ExecutionMode = "parallel" | "sequential";
27
+
28
+ const MAX_AGENTS_PER_CALL = 8;
29
+ const MAX_PARALLEL_CONCURRENCY = 4;
30
+ const OUTPUT_PREVIEW_CHARS = 6_000;
31
+ const ROLE_ALIASES: Record<string, string> = {
32
+ "file_picker": "file-picker",
33
+ "file-picker": "file-picker",
34
+ scout: "scout",
35
+ planner: "planner",
36
+ editor: "editor",
37
+ worker: "worker",
38
+ tester: "tester",
39
+ reviewer: "reviewer",
40
+ "code_searcher": "code-searcher",
41
+ "code-searcher": "code-searcher",
42
+ "directory_lister": "directory-lister",
43
+ "directory-lister": "directory-lister",
44
+ "glob_matcher": "glob-matcher",
45
+ "glob-matcher": "glob-matcher",
46
+ basher: "basher",
47
+ thinker: "thinker",
48
+ "context_pruner": "context-pruner",
49
+ "context-pruner": "context-pruner",
50
+ librarian: "librarian",
51
+ "security_auditor": "security-auditor",
52
+ "security-auditor": "security-auditor",
53
+ "problem_architect": "problem-architect",
54
+ "problem-architect": "problem-architect",
55
+ "spec_reviewer": "spec-reviewer",
56
+ "spec-reviewer": "spec-reviewer",
57
+ "ship_guard": "ship-guard",
58
+ "ship-guard": "ship-guard",
59
+ "rule_verifier": "rule-verifier",
60
+ "rule-verifier": "rule-verifier",
61
+ "loop_controller": "loop-controller",
62
+ "loop-controller": "loop-controller",
63
+ };
64
+ const THINKING_LEVELS = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh"]);
65
+
66
+ interface AgentConfig {
67
+ name: string;
68
+ description: string;
69
+ tools?: string[];
70
+ model?: string;
71
+ thinking?: ThinkingLevel;
72
+ systemPrompt: string;
73
+ source: "user" | "project";
74
+ filePath: string;
75
+ }
76
+
77
+ interface AgentRunRequest {
78
+ agent?: string;
79
+ agent_type?: string;
80
+ prompt?: string;
81
+ task?: string;
82
+ cwd?: string;
83
+ model?: string;
84
+ thinking?: ThinkingLevel;
85
+ tools?: string[];
86
+ }
87
+
88
+ interface UsageStats {
89
+ input: number;
90
+ output: number;
91
+ cacheRead: number;
92
+ cacheWrite: number;
93
+ cost: number;
94
+ contextTokens: number;
95
+ turns: number;
96
+ }
97
+
98
+ interface AgentRunResult {
99
+ agent: string;
100
+ agentSource: "user" | "project" | "unknown";
101
+ cwd: string;
102
+ prompt: string;
103
+ exitCode: number;
104
+ output: string;
105
+ stderr: string;
106
+ usage: UsageStats;
107
+ model?: string;
108
+ thinking?: ThinkingLevel;
109
+ stopReason?: string;
110
+ errorMessage?: string;
111
+ }
112
+
113
+ interface SpawnAgentsDetails {
114
+ mode: ExecutionMode;
115
+ agentScope: AgentScope;
116
+ projectAgentsDir: string | null;
117
+ results: AgentRunResult[];
118
+ }
119
+
120
+ function normalizeRoleName(raw: string | undefined): string {
121
+ const value = (raw ?? "").trim();
122
+ return ROLE_ALIASES[value] ?? value;
123
+ }
124
+
125
+ function truncateText(text: string, maxChars = OUTPUT_PREVIEW_CHARS): string {
126
+ if (text.length <= maxChars) return text;
127
+ return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} chars]`;
128
+ }
129
+
130
+ function formatUsage(usage: UsageStats): string {
131
+ const parts: string[] = [];
132
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`);
133
+ if (usage.input) parts.push(`in ${usage.input}`);
134
+ if (usage.output) parts.push(`out ${usage.output}`);
135
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
136
+ return parts.join(", ");
137
+ }
138
+
139
+ function isInside(parent: string, candidate: string): boolean {
140
+ const relative = path.relative(parent, candidate);
141
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
142
+ }
143
+
144
+ function resolveCwd(projectCwd: string, requested?: string): string {
145
+ if (!requested?.trim()) return projectCwd;
146
+ const resolved = path.resolve(projectCwd, requested);
147
+ const projectRoot = path.resolve(projectCwd);
148
+ if (!isInside(projectRoot, resolved)) {
149
+ throw new Error(`cwd must stay inside project root: ${requested}`);
150
+ }
151
+ return resolved;
152
+ }
153
+
154
+ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
155
+ if (!fs.existsSync(dir)) return [];
156
+
157
+ let entries: fs.Dirent[];
158
+ try {
159
+ entries = fs.readdirSync(dir, { withFileTypes: true });
160
+ } catch {
161
+ return [];
162
+ }
163
+
164
+ const agents: AgentConfig[] = [];
165
+ for (const entry of entries) {
166
+ if (!entry.name.endsWith(".md")) continue;
167
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
168
+
169
+ const filePath = path.join(dir, entry.name);
170
+ let content: string;
171
+ try {
172
+ content = fs.readFileSync(filePath, "utf-8");
173
+ } catch {
174
+ continue;
175
+ }
176
+
177
+ const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
178
+ if (!frontmatter.name || !frontmatter.description) continue;
179
+
180
+ const tools = frontmatter.tools
181
+ ?.split(",")
182
+ .map((tool) => tool.trim())
183
+ .filter(Boolean);
184
+
185
+ const maybeThinking = frontmatter.thinking?.trim() as ThinkingLevel | undefined;
186
+ const thinking = maybeThinking && THINKING_LEVELS.has(maybeThinking) ? maybeThinking : undefined;
187
+
188
+ agents.push({
189
+ name: frontmatter.name,
190
+ description: frontmatter.description,
191
+ tools: tools && tools.length > 0 ? tools : undefined,
192
+ model: frontmatter.model,
193
+ thinking,
194
+ systemPrompt: body,
195
+ source,
196
+ filePath,
197
+ });
198
+ }
199
+
200
+ return agents;
201
+ }
202
+
203
+ function findNearestProjectAgentsDir(cwd: string): string | null {
204
+ let currentDir = path.resolve(cwd);
205
+ while (true) {
206
+ const candidate = path.join(currentDir, ".pi", "agents");
207
+ try {
208
+ if (fs.statSync(candidate).isDirectory()) return candidate;
209
+ } catch {
210
+ // keep walking
211
+ }
212
+
213
+ const parent = path.dirname(currentDir);
214
+ if (parent === currentDir) return null;
215
+ currentDir = parent;
216
+ }
217
+ }
218
+
219
+ function discoverAgents(cwd: string, scope: AgentScope): { agents: AgentConfig[]; projectAgentsDir: string | null } {
220
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
221
+ const userAgentsDir = path.join(getAgentDir(), "agents");
222
+ const agentsByName = new Map<string, AgentConfig>();
223
+
224
+ if (scope === "user" || scope === "both") {
225
+ for (const agent of loadAgentsFromDir(userAgentsDir, "user")) agentsByName.set(agent.name, agent);
226
+ }
227
+ if ((scope === "project" || scope === "both") && projectAgentsDir) {
228
+ for (const agent of loadAgentsFromDir(projectAgentsDir, "project")) agentsByName.set(agent.name, agent);
229
+ }
230
+
231
+ return { agents: Array.from(agentsByName.values()), projectAgentsDir };
232
+ }
233
+
234
+ function getFinalAssistantText(messages: Message[]): string {
235
+ for (let i = messages.length - 1; i >= 0; i--) {
236
+ const message = messages[i];
237
+ if (message.role !== "assistant") continue;
238
+ for (const part of message.content) {
239
+ if (part.type === "text") return part.text;
240
+ }
241
+ }
242
+ return "";
243
+ }
244
+
245
+ async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
246
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openpi-agent-"));
247
+ const safeName = agentName.replace(/[^\w.-]+/g, "_");
248
+ const filePath = path.join(dir, `system-${safeName}.md`);
249
+ await withFileMutationQueue(filePath, async () => {
250
+ await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
251
+ });
252
+ return { dir, filePath };
253
+ }
254
+
255
+ function getPiInvocation(args: string[]): { command: string; args: string[] } {
256
+ const currentScript = process.argv[1];
257
+ const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
258
+ if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
259
+ return { command: process.execPath, args: [currentScript, ...args] };
260
+ }
261
+
262
+ const execName = path.basename(process.execPath).toLowerCase();
263
+ const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
264
+ if (!isGenericRuntime) return { command: process.execPath, args };
265
+
266
+ return { command: "pi", args };
267
+ }
268
+
269
+ async function runSingleAgent(
270
+ projectCwd: string,
271
+ availableAgents: AgentConfig[],
272
+ request: AgentRunRequest,
273
+ signal: AbortSignal | undefined,
274
+ ): Promise<AgentRunResult> {
275
+ const requestedAgent = normalizeRoleName(request.agent ?? request.agent_type);
276
+ const prompt = (request.prompt ?? request.task ?? "").trim();
277
+
278
+ if (!requestedAgent) throw new Error("spawn_agents item requires agent or agent_type");
279
+ if (!prompt) throw new Error(`spawn_agents item for ${requestedAgent} requires prompt or task`);
280
+
281
+ const agent = availableAgents.find((candidate) => candidate.name === requestedAgent);
282
+ const cwd = resolveCwd(projectCwd, request.cwd);
283
+ const baseResult: AgentRunResult = {
284
+ agent: requestedAgent,
285
+ agentSource: agent?.source ?? "unknown",
286
+ cwd,
287
+ prompt,
288
+ exitCode: 1,
289
+ output: "",
290
+ stderr: "",
291
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
292
+ model: request.model ?? agent?.model,
293
+ thinking: request.thinking ?? agent?.thinking,
294
+ };
295
+
296
+ if (!agent) {
297
+ const names = availableAgents.map((candidate) => candidate.name).sort().join(", ") || "none";
298
+ return { ...baseResult, errorMessage: `Unknown agent: ${requestedAgent}. Available: ${names}` };
299
+ }
300
+
301
+ const args = ["--mode", "json", "-p", "--no-session"];
302
+ const model = request.model ?? agent.model;
303
+ const thinking = request.thinking ?? agent.thinking;
304
+ const tools = request.tools ?? agent.tools;
305
+
306
+ if (model) args.push("--model", model);
307
+ if (thinking) args.push("--thinking", thinking);
308
+ if (tools && tools.length > 0) args.push("--tools", tools.join(","));
309
+
310
+ let tmpPromptDir: string | undefined;
311
+ let tmpPromptPath: string | undefined;
312
+ const messages: Message[] = [];
313
+
314
+ try {
315
+ if (agent.systemPrompt.trim()) {
316
+ const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
317
+ tmpPromptDir = tmp.dir;
318
+ tmpPromptPath = tmp.filePath;
319
+ args.push("--append-system-prompt", tmpPromptPath);
320
+ }
321
+
322
+ args.push(`Task: ${prompt}`);
323
+
324
+ let wasAborted = false;
325
+ const exitCode = await new Promise<number>((resolve) => {
326
+ const invocation = getPiInvocation(args);
327
+ const proc = spawn(invocation.command, invocation.args, {
328
+ cwd,
329
+ shell: false,
330
+ stdio: ["ignore", "pipe", "pipe"],
331
+ });
332
+ let buffer = "";
333
+
334
+ const processLine = (line: string) => {
335
+ if (!line.trim()) return;
336
+ let event: any;
337
+ try {
338
+ event = JSON.parse(line);
339
+ } catch {
340
+ return;
341
+ }
342
+
343
+ if (event.type === "message_end" && event.message) {
344
+ const message = event.message as Message;
345
+ messages.push(message);
346
+ if (message.role === "assistant") {
347
+ baseResult.usage.turns += 1;
348
+ const usage = message.usage;
349
+ if (usage) {
350
+ baseResult.usage.input += usage.input || 0;
351
+ baseResult.usage.output += usage.output || 0;
352
+ baseResult.usage.cacheRead += usage.cacheRead || 0;
353
+ baseResult.usage.cacheWrite += usage.cacheWrite || 0;
354
+ baseResult.usage.cost += usage.cost?.total || 0;
355
+ baseResult.usage.contextTokens = usage.totalTokens || 0;
356
+ }
357
+ if (!baseResult.model && message.model) baseResult.model = message.model;
358
+ if (message.stopReason) baseResult.stopReason = message.stopReason;
359
+ if (message.errorMessage) baseResult.errorMessage = message.errorMessage;
360
+ }
361
+ }
362
+
363
+ if (event.type === "tool_result_end" && event.message) {
364
+ messages.push(event.message as Message);
365
+ }
366
+ };
367
+
368
+ proc.stdout.on("data", (data) => {
369
+ buffer += data.toString();
370
+ const lines = buffer.split("\n");
371
+ buffer = lines.pop() ?? "";
372
+ for (const line of lines) processLine(line);
373
+ });
374
+
375
+ proc.stderr.on("data", (data) => {
376
+ baseResult.stderr += data.toString();
377
+ });
378
+
379
+ proc.on("close", (code) => {
380
+ if (buffer.trim()) processLine(buffer);
381
+ resolve(code ?? 0);
382
+ });
383
+
384
+ proc.on("error", (error) => {
385
+ baseResult.errorMessage = error.message;
386
+ resolve(1);
387
+ });
388
+
389
+ if (signal) {
390
+ const killProc = () => {
391
+ wasAborted = true;
392
+ proc.kill("SIGTERM");
393
+ setTimeout(() => {
394
+ if (!proc.killed) proc.kill("SIGKILL");
395
+ }, 5_000);
396
+ };
397
+ if (signal.aborted) killProc();
398
+ else signal.addEventListener("abort", killProc, { once: true });
399
+ }
400
+ });
401
+
402
+ if (wasAborted) {
403
+ return { ...baseResult, exitCode, errorMessage: "Subagent was aborted" };
404
+ }
405
+
406
+ return {
407
+ ...baseResult,
408
+ exitCode,
409
+ output: getFinalAssistantText(messages),
410
+ };
411
+ } finally {
412
+ if (tmpPromptPath) await fs.promises.rm(tmpPromptPath, { force: true }).catch(() => undefined);
413
+ if (tmpPromptDir) await fs.promises.rm(tmpPromptDir, { recursive: true, force: true }).catch(() => undefined);
414
+ }
415
+ }
416
+
417
+ async function mapWithConcurrencyLimit<TIn, TOut>(
418
+ items: TIn[],
419
+ concurrency: number,
420
+ fn: (item: TIn, index: number) => Promise<TOut>,
421
+ ): Promise<TOut[]> {
422
+ const results: TOut[] = new Array(items.length);
423
+ let nextIndex = 0;
424
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
425
+ const workers = new Array(workerCount).fill(null).map(async () => {
426
+ while (true) {
427
+ const current = nextIndex;
428
+ nextIndex += 1;
429
+ if (current >= items.length) return;
430
+ results[current] = await fn(items[current], current);
431
+ }
432
+ });
433
+ await Promise.all(workers);
434
+ return results;
435
+ }
436
+
437
+ function summarizeSpawnResults(results: AgentRunResult[]): string {
438
+ return results
439
+ .map((result, index) => {
440
+ const ok = result.exitCode === 0 && !result.errorMessage;
441
+ const usage = formatUsage(result.usage);
442
+ const meta = [result.model, result.thinking ? `thinking:${result.thinking}` : undefined, usage]
443
+ .filter(Boolean)
444
+ .join(" | ");
445
+ const body = result.output || result.errorMessage || result.stderr || "(no output)";
446
+ return [
447
+ `## ${index + 1}. ${ok ? "OK" : "FAIL"} ${result.agent}`,
448
+ meta ? `_${meta}_` : undefined,
449
+ "",
450
+ truncateText(body),
451
+ ]
452
+ .filter((part) => part !== undefined)
453
+ .join("\n");
454
+ })
455
+ .join("\n\n---\n\n");
456
+ }
457
+
458
+ function buildWorkflowPrompt(kind: "add" | "fix" | "review", userRequest: string): string {
459
+ const trimmed = userRequest.trim();
460
+ if (kind === "review") {
461
+ return `Use the openpi workflow to review the current changes.\n\nUser request: ${trimmed || "Review the current git diff."}\n\nProcess:\n1. Use spawn_agents with agent_type=\"reviewer\" to inspect the current diff.\n2. If validation context is needed, use spawn_agents with agent_type=\"tester\" for read-only/test commands only.\n3. Do not edit files during review unless I explicitly ask for fixes afterward.\n4. Return verifiable data only: files reviewed, file:line findings, test commands/output if any, verdict, and remaining risks.\n\nDo not reveal hidden chain-of-thought; provide concise audit evidence instead.`;
462
+ }
463
+
464
+ const label = kind === "add" ? "implement this feature" : "fix this bug";
465
+ return `Use the openpi workflow to ${label}.\n\nUser request: ${trimmed}\n\nRequired process:\n1. Use spawn_agents with agent_type=\"file-picker\" to find relevant files and line ranges.\n2. Use spawn_agents with agent_type=\"planner\" to create a concrete plan from the file-picker output.\n3. If the plan has blocking ambiguity, ask me before editing. Otherwise implement with the available tools or spawn_agents agent_type=\"editor\" for isolated edits.\n4. Use spawn_agents with agent_type=\"tester\" to run targeted validation.\n5. Use spawn_agents with agent_type=\"reviewer\" after edits to audit the diff.\n6. Fix critical test/review issues once, then summarize.\n\nFinal response must include:\n- Files inspected and changed\n- Key file:line references\n- Test commands and exact outcomes\n- Review verdict\n- Remaining risks or follow-ups\n\nDo not rely on or expose hidden chain-of-thought; present verifiable audit data only.`;
466
+ }
467
+
468
+ const AgentRequestSchema = Type.Object({
469
+ agent: Type.Optional(Type.String({ description: "Pi agent name, e.g. file-picker, planner, editor, tester, reviewer" })),
470
+ agent_type: Type.Optional(Type.String({ description: "Role alias for agent" })),
471
+ prompt: Type.Optional(Type.String({ description: "Prompt to send to the agent" })),
472
+ task: Type.Optional(Type.String({ description: "Alias for prompt" })),
473
+ cwd: Type.Optional(Type.String({ description: "Optional project-relative working directory. Must stay inside project root." })),
474
+ model: Type.Optional(Type.String({ description: "Optional model override for this agent run" })),
475
+ thinking: Type.Optional(
476
+ StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const, {
477
+ description: "Optional Pi thinking level override for this agent run",
478
+ }),
479
+ ),
480
+ tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool override for this agent process" })),
481
+ });
482
+
483
+ export default function workflowExtension(pi: ExtensionAPI) {
484
+ pi.registerTool({
485
+ name: "spawn_agents",
486
+ label: "Spawn Agents",
487
+ description:
488
+ "Run role agents as isolated Pi subprocesses. Agents come from ~/.pi/agent/agents and/or project .pi/agents. Returns structured outputs, not hidden reasoning.",
489
+ promptSnippet: "Delegate file discovery, planning, editing, testing, or review to isolated role agents.",
490
+ promptGuidelines: [
491
+ "Use spawn_agents for openpi workflows: file-picker before planner, editor only after context, tester and reviewer after edits.",
492
+ "When using spawn_agents, ask for verifiable outputs: file paths, line ranges, diffs, commands run, exact test output, findings, and assumptions.",
493
+ ],
494
+ parameters: Type.Object({
495
+ agents: Type.Array(AgentRequestSchema, {
496
+ description: "Agents to run. Parallel by default; use mode=sequential for dependent steps.",
497
+ }),
498
+ mode: Type.Optional(
499
+ StringEnum(["parallel", "sequential"] as const, {
500
+ description: "Run agents in parallel or sequentially. Default: parallel.",
501
+ default: "parallel",
502
+ }),
503
+ ),
504
+ agentScope: Type.Optional(
505
+ StringEnum(["user", "project", "both"] as const, {
506
+ description: "Agent discovery scope. Default: both, so project role agents override user agents.",
507
+ default: "both",
508
+ }),
509
+ ),
510
+ }),
511
+ prepareArguments(args) {
512
+ if (!args || typeof args !== "object") return args;
513
+ const input = args as { agents?: unknown; tasks?: unknown; mode?: unknown; agentScope?: unknown };
514
+ if (input.agents === undefined && Array.isArray(input.tasks)) {
515
+ return { ...input, agents: input.tasks };
516
+ }
517
+ return args;
518
+ },
519
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
520
+ const requests = params.agents as AgentRunRequest[];
521
+ if (requests.length === 0) throw new Error("spawn_agents requires at least one agent");
522
+ if (requests.length > MAX_AGENTS_PER_CALL) {
523
+ throw new Error(`spawn_agents supports at most ${MAX_AGENTS_PER_CALL} agents per call`);
524
+ }
525
+
526
+ const mode = (params.mode ?? "parallel") as ExecutionMode;
527
+ const agentScope = (params.agentScope ?? "both") as AgentScope;
528
+ const discovery = discoverAgents(ctx.cwd, agentScope);
529
+ const makeDetails = (results: AgentRunResult[]): SpawnAgentsDetails => ({
530
+ mode,
531
+ agentScope,
532
+ projectAgentsDir: discovery.projectAgentsDir,
533
+ results,
534
+ });
535
+
536
+ const results: AgentRunResult[] = [];
537
+ const emitProgress = () => {
538
+ onUpdate?.({
539
+ content: [{ type: "text", text: `spawn_agents: ${results.length}/${requests.length} complete` }],
540
+ details: makeDetails([...results]),
541
+ });
542
+ };
543
+
544
+ if (mode === "sequential") {
545
+ for (const request of requests) {
546
+ const result = await runSingleAgent(ctx.cwd, discovery.agents, request, signal);
547
+ results.push(result);
548
+ emitProgress();
549
+ if (result.exitCode !== 0 || result.errorMessage) break;
550
+ }
551
+ } else {
552
+ const parallelResults = await mapWithConcurrencyLimit(
553
+ requests,
554
+ MAX_PARALLEL_CONCURRENCY,
555
+ async (request) => {
556
+ const result = await runSingleAgent(ctx.cwd, discovery.agents, request, signal);
557
+ results.push(result);
558
+ emitProgress();
559
+ return result;
560
+ },
561
+ );
562
+ results.splice(0, results.length, ...parallelResults);
563
+ }
564
+
565
+ const successCount = results.filter((result) => result.exitCode === 0 && !result.errorMessage).length;
566
+ return {
567
+ content: [
568
+ {
569
+ type: "text",
570
+ text: `spawn_agents: ${successCount}/${results.length} succeeded\n\n${summarizeSpawnResults(results)}`,
571
+ },
572
+ ],
573
+ details: makeDetails(results),
574
+ };
575
+ },
576
+ renderCall(args, theme) {
577
+ const count = Array.isArray(args.agents) ? args.agents.length : 0;
578
+ const mode = args.mode ?? "parallel";
579
+ return new Text(
580
+ `${theme.fg("toolTitle", theme.bold("spawn_agents "))}${theme.fg("accent", `${count} ${mode}`)}`,
581
+ 0,
582
+ 0,
583
+ );
584
+ },
585
+ renderResult(result, _options, theme) {
586
+ const details = result.details as SpawnAgentsDetails | undefined;
587
+ if (!details) {
588
+ const first = result.content[0];
589
+ return new Text(first?.type === "text" ? first.text : "spawn_agents complete", 0, 0);
590
+ }
591
+ const successCount = details.results.filter((item) => item.exitCode === 0 && !item.errorMessage).length;
592
+ const icon = successCount === details.results.length ? theme.fg("success", "OK") : theme.fg("warning", "PARTIAL");
593
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("spawn_agents "))}${theme.fg("accent", `${successCount}/${details.results.length}`)} ${theme.fg("muted", details.mode)}`;
594
+ for (const run of details.results) {
595
+ const runIcon = run.exitCode === 0 && !run.errorMessage ? theme.fg("success", "OK") : theme.fg("error", "FAIL");
596
+ const usage = formatUsage(run.usage);
597
+ text += `\n ${runIcon} ${theme.fg("accent", run.agent)}${usage ? theme.fg("dim", ` (${usage})`) : ""}`;
598
+ }
599
+ return new Text(text, 0, 0);
600
+ },
601
+ });
602
+
603
+ const sendWorkflow = (kind: "add" | "fix" | "review", args: string, ctx: { isIdle(): boolean; ui: { notify(message: string, level?: "info" | "warning" | "error"): void } }) => {
604
+ if (kind !== "review" && !args.trim()) {
605
+ ctx.ui.notify(`Usage: /${kind} <request>`, "warning");
606
+ return;
607
+ }
608
+ const prompt = buildWorkflowPrompt(kind, args);
609
+ if (ctx.isIdle()) pi.sendUserMessage(prompt);
610
+ else pi.sendUserMessage(prompt, { deliverAs: "followUp" });
611
+ };
612
+
613
+ pi.registerCommand("add", {
614
+ description: "openpi add workflow: discover files, plan, edit, test, review",
615
+ handler: async (args, ctx) => sendWorkflow("add", args, ctx),
616
+ });
617
+
618
+ pi.registerCommand("fix", {
619
+ description: "openpi fix workflow: inspect bug, plan minimal fix, test, review",
620
+ handler: async (args, ctx) => sendWorkflow("fix", args, ctx),
621
+ });
622
+
623
+ pi.registerCommand("review", {
624
+ description: "openpi review workflow over current diff or supplied scope",
625
+ handler: async (args, ctx) => sendWorkflow("review", args, ctx),
626
+ });
627
+
628
+ pi.registerCommand("openpi-agents", {
629
+ description: "List openpi role agents available to spawn_agents",
630
+ handler: async (_args, ctx) => {
631
+ const discovery = discoverAgents(ctx.cwd, "both");
632
+ const spawnToolStatus = pi.getAllTools().some((tool) => tool.name === "spawn_agents")
633
+ ? "spawn_agents tool: registered"
634
+ : "spawn_agents tool: missing";
635
+ const agents = discovery.agents
636
+ .map((agent) => `${agent.name} [${agent.source}]${agent.thinking ? ` thinking:${agent.thinking}` : ""} - ${agent.description}`)
637
+ .sort()
638
+ .join("\n");
639
+ ctx.ui.notify(`${spawnToolStatus}\n${agents || "No agents found"}`, "info");
640
+ },
641
+ });
642
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@matyah00/openpi",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "description": "Pi-native commands, skills, agents, and workflows.",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-coding-agent",
9
+ "commands",
10
+ "skills",
11
+ "agents",
12
+ "workflows"
13
+ ],
14
+ "homepage": "https://github.com/haytamAroui/OpenPi#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/haytamAroui/OpenPi.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/haytamAroui/OpenPi/issues"
21
+ },
22
+ "license": "MIT",
23
+ "files": [
24
+ "agents",
25
+ "extensions",
26
+ "prompts",
27
+ "skills",
28
+ "themes",
29
+ "types",
30
+ "damage-control-rules.yaml",
31
+ "README.md",
32
+ "tsconfig.json"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "yaml": "^2.8.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@earendil-works/pi-ai": "*",
42
+ "@earendil-works/pi-coding-agent": "*",
43
+ "@earendil-works/pi-tui": "*",
44
+ "typebox": "*"
45
+ },
46
+ "pi": {
47
+ "extensions": [
48
+ "./extensions/openpi.ts"
49
+ ],
50
+ "skills": [
51
+ "./skills"
52
+ ],
53
+ "prompts": [
54
+ "./prompts"
55
+ ],
56
+ "themes": [
57
+ "./themes"
58
+ ]
59
+ }
60
+ }