@phi-code-admin/phi-code 0.58.0 → 0.58.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.
@@ -1,27 +1,48 @@
1
1
  /**
2
- * Orchestrator Extension - Project planning and task management
2
+ * Orchestrator Extension - Project planning and automatic task execution
3
3
  *
4
- * Provides tools for the LLM to create structured project plans:
5
- * - /plan: Interactive planning command
4
+ * Provides tools for the LLM to create structured project plans and execute them:
5
+ * - /plan: Interactive planning command → creates spec.md + todo.md
6
6
  * - /plans: List and manage existing plans
7
+ * - /run: Execute plan tasks using sub-agents (each with own context + model)
7
8
  * - orchestrate tool: Create spec.md + todo.md from structured input
8
9
  *
9
- * Architecture:
10
- * The LLM analyzes the user's request (using prompt-architect skill patterns
11
- * if available), then calls the orchestrate tool with structured data.
12
- * The tool writes files to disk. The LLM does the thinking, the tool does the I/O.
13
- *
14
- * Integration with Prompt Architect skill:
15
- * When the prompt-architect skill is loaded, the LLM uses its patterns
16
- * (ROLE/CONTEXT/TASK/FORMAT/CONSTRAINTS) to structure the spec.
17
- * The skill-loader extension handles this automatically.
10
+ * Sub-agent execution:
11
+ * Each task spawns a separate `phi` CLI process with:
12
+ * - Its own system prompt (from the agent .md file)
13
+ * - Its own model (from routing.json or current model)
14
+ * - Its own context (isolated, no shared history)
15
+ * - Its own tool access (read, write, edit, bash, etc.)
16
+ * Results are collected into progress.md and reported to the user.
18
17
  */
19
18
 
20
19
  import { Type } from "@sinclair/typebox";
21
20
  import type { ExtensionAPI } from "phi-code";
22
21
  import { writeFile, mkdir, readdir, readFile, access } from "node:fs/promises";
23
- import { join } from "node:path";
24
- import { existsSync } from "node:fs";
22
+ import { join, dirname } from "node:path";
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { execFile } from "node:child_process";
25
+ import { homedir } from "node:os";
26
+
27
+ // ─── Types ───────────────────────────────────────────────────────────────
28
+
29
+ interface TaskResult {
30
+ taskIndex: number;
31
+ title: string;
32
+ agent: string;
33
+ status: "success" | "error" | "skipped";
34
+ output: string;
35
+ durationMs: number;
36
+ }
37
+
38
+ interface AgentDef {
39
+ name: string;
40
+ description: string;
41
+ tools: string;
42
+ systemPrompt: string;
43
+ }
44
+
45
+ // ─── Extension ───────────────────────────────────────────────────────────
25
46
 
26
47
  export default function orchestratorExtension(pi: ExtensionAPI) {
27
48
  const plansDir = join(process.cwd(), ".phi", "plans");
@@ -34,6 +55,168 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
34
55
  return new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
35
56
  }
36
57
 
58
+ // ─── Agent Discovery ─────────────────────────────────────────────
59
+
60
+ /**
61
+ * Load agent definitions from .md files.
62
+ * Searches: project .phi/agents/ → global ~/.phi/agent/agents/ → bundled agents/
63
+ */
64
+ function loadAgentDefs(): Map<string, AgentDef> {
65
+ const agents = new Map<string, AgentDef>();
66
+ const dirs = [
67
+ join(process.cwd(), ".phi", "agents"),
68
+ join(homedir(), ".phi", "agent", "agents"),
69
+ join(__dirname, "..", "..", "..", "agents"),
70
+ ];
71
+
72
+ for (const dir of dirs) {
73
+ if (!existsSync(dir)) continue;
74
+ try {
75
+ const files = require("fs").readdirSync(dir) as string[];
76
+ for (const file of files) {
77
+ if (!file.endsWith(".md")) continue;
78
+ const name = file.replace(".md", "");
79
+ if (agents.has(name)) continue; // Priority: project > global > bundled
80
+
81
+ try {
82
+ const content = readFileSync(join(dir, file), "utf-8");
83
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
84
+ if (!fmMatch) continue;
85
+
86
+ const frontmatter = fmMatch[1];
87
+ const body = fmMatch[2].trim();
88
+
89
+ const desc = frontmatter.match(/description:\s*(.+)/)?.[1] || "";
90
+ const tools = frontmatter.match(/tools:\s*(.+)/)?.[1] || "";
91
+
92
+ agents.set(name, { name, description: desc, tools, systemPrompt: body });
93
+ } catch { /* skip unparseable files */ }
94
+ }
95
+ } catch { /* skip inaccessible dirs */ }
96
+ }
97
+
98
+ return agents;
99
+ }
100
+
101
+ /**
102
+ * Resolve the model for an agent type from routing.json.
103
+ */
104
+ function resolveAgentModel(agentType: string): string | null {
105
+ const routingPath = join(homedir(), ".phi", "agent", "routing.json");
106
+ try {
107
+ const config = JSON.parse(readFileSync(routingPath, "utf-8"));
108
+ // Find the route that maps to this agent
109
+ for (const [_category, route] of Object.entries(config.routes || {})) {
110
+ const r = route as any;
111
+ if (r.agent === agentType) {
112
+ return r.preferredModel || null;
113
+ }
114
+ }
115
+ return config.default?.model || null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Find the phi CLI binary path.
123
+ */
124
+ function findPhiBinary(): string {
125
+ // 1. Try the dist/cli.js from our package
126
+ const bundledCli = join(__dirname, "..", "..", "..", "dist", "cli.js");
127
+ if (existsSync(bundledCli)) return bundledCli;
128
+
129
+ // 2. Try global phi binary
130
+ try {
131
+ const which = require("child_process").execSync("which phi 2>/dev/null", { encoding: "utf-8" }).trim();
132
+ if (which) return which;
133
+ } catch { /* not in PATH */ }
134
+
135
+ // 3. Fallback to npx
136
+ return "npx";
137
+ }
138
+
139
+ // ─── Sub-Agent Execution ─────────────────────────────────────────
140
+
141
+ /**
142
+ * Execute a task using a sub-agent process.
143
+ * Each agent gets its own isolated phi process with:
144
+ * - Its system prompt (from agent .md)
145
+ * - Its model (from routing.json)
146
+ * - No saved session (--no-save)
147
+ * - JSON output mode (--json)
148
+ */
149
+ function executeTask(
150
+ task: { title: string; description: string; agent?: string; subtasks?: string[] },
151
+ agentDefs: Map<string, AgentDef>,
152
+ cwd: string,
153
+ timeoutMs: number = 300000, // 5 minutes per task
154
+ ): Promise<TaskResult> {
155
+ return new Promise((resolve) => {
156
+ const agentType = task.agent || "code";
157
+ const agentDef = agentDefs.get(agentType);
158
+ const model = resolveAgentModel(agentType);
159
+ const phiBin = findPhiBinary();
160
+ const startTime = Date.now();
161
+
162
+ // Build the task prompt
163
+ let taskPrompt = `Task: ${task.title}\n\n${task.description}`;
164
+ if (task.subtasks && task.subtasks.length > 0) {
165
+ taskPrompt += "\n\nSub-tasks:\n" + task.subtasks.map((st, i) => `${i + 1}. ${st}`).join("\n");
166
+ }
167
+ taskPrompt += "\n\nComplete this task. Be thorough and precise. Report what you did.";
168
+
169
+ // Build the command
170
+ const args: string[] = [];
171
+ if (phiBin === "npx") {
172
+ args.push("@phi-code-admin/phi-code");
173
+ }
174
+
175
+ // Add flags
176
+ args.push("--print"); // Non-interactive, output only
177
+ if (model && model !== "default") {
178
+ args.push("--model", model);
179
+ }
180
+ if (agentDef?.systemPrompt) {
181
+ args.push("--system-prompt", agentDef.systemPrompt);
182
+ }
183
+ args.push("--no-save"); // Don't create a session
184
+ args.push(taskPrompt);
185
+
186
+ const cmd = phiBin === "npx" ? "npx" : "node";
187
+ const cmdArgs = phiBin === "npx" ? args : [phiBin, ...args];
188
+
189
+ const child = execFile(cmd, cmdArgs, {
190
+ cwd,
191
+ timeout: timeoutMs,
192
+ maxBuffer: 10 * 1024 * 1024, // 10MB
193
+ env: { ...process.env },
194
+ }, (error, stdout, stderr) => {
195
+ const durationMs = Date.now() - startTime;
196
+
197
+ if (error) {
198
+ resolve({
199
+ taskIndex: 0,
200
+ title: task.title,
201
+ agent: agentType,
202
+ status: "error",
203
+ output: `Error: ${error.message}\n${stderr || ""}`.trim(),
204
+ durationMs,
205
+ });
206
+ } else {
207
+ resolve({
208
+ taskIndex: 0,
209
+ title: task.title,
210
+ agent: agentType,
211
+ status: "success",
212
+ output: stdout.trim(),
213
+ durationMs,
214
+ });
215
+ }
216
+ });
217
+ });
218
+ }
219
+
37
220
  // ─── Orchestrate Tool ────────────────────────────────────────────
38
221
 
39
222
  pi.registerTool({
@@ -59,7 +242,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
59
242
  description: Type.String({ description: "Detailed task description" }),
60
243
  agent: Type.Optional(Type.String({ description: "Recommended agent: explore, plan, code, test, or review" })),
61
244
  priority: Type.Optional(Type.String({ description: "high, medium, or low" })),
62
- dependencies: Type.Optional(Type.Array(Type.Number(), { description: "IDs of tasks this depends on" })),
245
+ dependencies: Type.Optional(Type.Array(Type.Number(), { description: "IDs of tasks this depends on (1-indexed)" })),
63
246
  subtasks: Type.Optional(Type.Array(Type.String(), { description: "Sub-task descriptions" })),
64
247
  }),
65
248
  { description: "Ordered list of tasks" }
@@ -138,7 +321,8 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
138
321
  // ─── Generate todo.md ─────────────────────────────────
139
322
  let todo = `# TODO: ${p.title}\n\n`;
140
323
  todo += `**Created:** ${new Date().toLocaleString()}\n`;
141
- todo += `**Tasks:** ${p.tasks.length}\n\n`;
324
+ todo += `**Tasks:** ${p.tasks.length}\n`;
325
+ todo += `**Status:** pending\n\n`;
142
326
 
143
327
  p.tasks.forEach((t, i) => {
144
328
  const agentTag = t.agent ? ` [${t.agent}]` : "";
@@ -160,7 +344,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
160
344
  todo += `- Total: ${p.tasks.length} tasks\n`;
161
345
  todo += `- High priority: ${p.tasks.filter(t => t.priority === "high").length}\n`;
162
346
  todo += `- Agents needed: ${[...new Set(p.tasks.map(t => t.agent || "code"))].join(", ")}\n\n`;
163
- todo += `*Update [ ] to [x] when tasks are completed.*\n`;
347
+ todo += `*Run \`/run\` to execute tasks automatically with sub-agents.*\n`;
164
348
 
165
349
  // Write files
166
350
  await writeFile(join(plansDir, specFile), spec, "utf-8");
@@ -171,22 +355,16 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
171
355
  📋 **${p.title}**
172
356
 
173
357
  **Files:**
174
- - \`${specFile}\` — Full specification (goals, requirements, architecture, constraints)
175
- - \`${todoFile}\` — Actionable task list with priorities and agent assignments
358
+ - \`${specFile}\` — Full specification
359
+ - \`${todoFile}\` — Task list with agent assignments
176
360
 
177
361
  **Summary:**
178
- - ${p.goals.length} goals
179
- - ${p.requirements.length} requirements
362
+ - ${p.goals.length} goals, ${p.requirements.length} requirements
180
363
  - ${p.tasks.length} tasks (${p.tasks.filter(t => t.priority === "high").length} high priority)
181
364
  - Agents: ${[...new Set(p.tasks.map(t => t.agent || "code"))].join(", ")}
182
365
 
183
- **Next steps:**
184
- 1. Review the spec and todo files
185
- 2. Start with high-priority tasks
186
- 3. Follow dependency order
187
- 4. Mark tasks [x] when done
188
-
189
- Files saved in \`.phi/plans/\``;
366
+ **Execute:** Run \`/run\` to launch sub-agents and execute tasks automatically.
367
+ Each agent gets its own context, model, and system prompt.`;
190
368
 
191
369
  return {
192
370
  content: [{ type: "text", text: summary }],
@@ -201,6 +379,144 @@ Files saved in \`.phi/plans/\``;
201
379
  },
202
380
  });
203
381
 
382
+ // ─── /run Command — Execute plan with sub-agents ─────────────────
383
+
384
+ pi.registerCommand("run", {
385
+ description: "Execute the latest plan's tasks using sub-agents (each with own context and model)",
386
+ handler: async (args, ctx) => {
387
+ // Find the latest todo file
388
+ if (!existsSync(plansDir)) {
389
+ ctx.ui.notify("No plans found. Use `/plan <description>` to create one first.", "warning");
390
+ return;
391
+ }
392
+
393
+ const files = (await readdir(plansDir)).sort().reverse();
394
+ const todoFiles = files.filter(f => f.startsWith("todo-") && f.endsWith(".md"));
395
+
396
+ if (todoFiles.length === 0) {
397
+ ctx.ui.notify("No todo files found. Use `/plan <description>` first.", "warning");
398
+ return;
399
+ }
400
+
401
+ const todoFile = todoFiles[0]; // Most recent
402
+ const todoPath = join(plansDir, todoFile);
403
+ const todoContent = await readFile(todoPath, "utf-8");
404
+
405
+ // Parse tasks from todo.md
406
+ const taskRegex = /## Task (\d+): (.+?)(?:\s*🔴|\s*🟡|\s*🟢)?\s*(?:\[(\w+)\])?\s*(?:\(after.*?\))?\n\n- \[ \] (.+?)(?:\n(?: - \[ \] .+?\n)*)?/g;
407
+ const tasks: Array<{ index: number; title: string; agent: string; description: string; subtasks: string[] }> = [];
408
+
409
+ let match;
410
+ while ((match = taskRegex.exec(todoContent)) !== null) {
411
+ const subtaskRegex = / - \[ \] (.+)/g;
412
+ const subtasks: string[] = [];
413
+ const taskBlock = todoContent.slice(match.index, taskRegex.lastIndex + 500);
414
+ let stMatch;
415
+ while ((stMatch = subtaskRegex.exec(taskBlock)) !== null) {
416
+ subtasks.push(stMatch[1]);
417
+ }
418
+
419
+ tasks.push({
420
+ index: parseInt(match[1]),
421
+ title: match[2].trim(),
422
+ agent: match[3] || "code",
423
+ description: match[4].trim(),
424
+ subtasks,
425
+ });
426
+ }
427
+
428
+ if (tasks.length === 0) {
429
+ ctx.ui.notify("Could not parse tasks from the todo file. Check the format.", "error");
430
+ return;
431
+ }
432
+
433
+ // Load agent definitions
434
+ const agentDefs = loadAgentDefs();
435
+ const availableAgents = [...agentDefs.keys()].join(", ") || "none loaded";
436
+
437
+ // Confirm execution
438
+ const confirmed = await ctx.ui.confirm(
439
+ "Execute Plan",
440
+ `Found ${tasks.length} tasks in \`${todoFile}\`.\n` +
441
+ `Available agents: ${availableAgents}\n` +
442
+ `Each task will spawn an isolated phi process with its own context.\n\n` +
443
+ `Proceed?`
444
+ );
445
+
446
+ if (!confirmed) {
447
+ ctx.ui.notify("Execution cancelled.", "info");
448
+ return;
449
+ }
450
+
451
+ // Create progress file
452
+ const progressFile = todoFile.replace("todo-", "progress-");
453
+ const progressPath = join(plansDir, progressFile);
454
+ let progress = `# Progress: ${todoFile}\n\n`;
455
+ progress += `**Started:** ${new Date().toLocaleString()}\n`;
456
+ progress += `**Tasks:** ${tasks.length}\n\n`;
457
+ await writeFile(progressPath, progress, "utf-8");
458
+
459
+ ctx.ui.notify(`🚀 Starting execution of ${tasks.length} tasks...`, "info");
460
+
461
+ // Execute tasks in order (respecting dependencies)
462
+ const results: TaskResult[] = [];
463
+ const completed = new Set<number>();
464
+
465
+ for (const task of tasks) {
466
+ ctx.ui.notify(`\n⏳ **Task ${task.index}: ${task.title}** [${task.agent}]`, "info");
467
+
468
+ // Execute the task
469
+ const result = await executeTask(
470
+ { title: task.title, description: task.description, agent: task.agent, subtasks: task.subtasks },
471
+ agentDefs,
472
+ process.cwd(),
473
+ );
474
+ result.taskIndex = task.index;
475
+
476
+ results.push(result);
477
+ completed.add(task.index);
478
+
479
+ // Report result
480
+ const icon = result.status === "success" ? "✅" : "❌";
481
+ const duration = (result.durationMs / 1000).toFixed(1);
482
+ ctx.ui.notify(
483
+ `${icon} **Task ${task.index}: ${task.title}** (${duration}s)\n` +
484
+ `Agent: ${task.agent} | Output: ${result.output.slice(0, 500)}${result.output.length > 500 ? "..." : ""}`,
485
+ result.status === "success" ? "info" : "error"
486
+ );
487
+
488
+ // Update progress file
489
+ progress += `## Task ${task.index}: ${task.title}\n\n`;
490
+ progress += `- **Status:** ${result.status}\n`;
491
+ progress += `- **Agent:** ${result.agent}\n`;
492
+ progress += `- **Duration:** ${duration}s\n`;
493
+ progress += `- **Output:**\n\n\`\`\`\n${result.output.slice(0, 2000)}\n\`\`\`\n\n`;
494
+ await writeFile(progressPath, progress, "utf-8");
495
+ }
496
+
497
+ // Final summary
498
+ const succeeded = results.filter(r => r.status === "success").length;
499
+ const failed = results.filter(r => r.status === "error").length;
500
+ const totalTime = results.reduce((sum, r) => sum + r.durationMs, 0);
501
+
502
+ progress += `---\n\n## Summary\n\n`;
503
+ progress += `- **Completed:** ${new Date().toLocaleString()}\n`;
504
+ progress += `- **Succeeded:** ${succeeded}/${results.length}\n`;
505
+ progress += `- **Failed:** ${failed}\n`;
506
+ progress += `- **Total time:** ${(totalTime / 1000).toFixed(1)}s\n`;
507
+ await writeFile(progressPath, progress, "utf-8");
508
+
509
+ ctx.ui.notify(
510
+ `\n🏁 **Execution complete!**\n\n` +
511
+ `✅ Succeeded: ${succeeded}/${results.length}\n` +
512
+ `❌ Failed: ${failed}\n` +
513
+ `⏱️ Total time: ${(totalTime / 1000).toFixed(1)}s\n\n` +
514
+ `Progress saved to \`${progressFile}\``,
515
+ failed === 0 ? "info" : "warning"
516
+ );
517
+ },
518
+ });
519
+
204
520
  // ─── /plan Command ───────────────────────────────────────────────
205
521
 
206
522
  pi.registerCommand("plan", {
@@ -217,16 +533,15 @@ Files saved in \`.phi/plans/\``;
217
533
  /plan Add comprehensive test coverage to the payment module
218
534
 
219
535
  The LLM will:
220
- 1. Analyze your description using prompt-architect patterns
221
- 2. Identify goals, requirements, architecture decisions
222
- 3. Break down into tasks with agent assignments and priorities
223
- 4. Create spec.md + todo.md files in .phi/plans/
536
+ 1. Analyze your description
537
+ 2. Break down into tasks with agent assignments
538
+ 3. Create spec.md + todo.md files in .phi/plans/
539
+ 4. Run \`/run\` to execute with sub-agents
224
540
 
225
- 💡 The more detail you provide, the better the plan.`, "info");
541
+ Each sub-agent gets its own isolated context, model, and system prompt.`, "info");
226
542
  return;
227
543
  }
228
544
 
229
- // Send as user message to trigger the LLM to analyze and call orchestrate
230
545
  ctx.sendUserMessage(
231
546
  `Please analyze this project request and create a structured plan using the orchestrate tool.
232
547
 
@@ -237,8 +552,7 @@ Instructions:
237
552
  - Break the work into small, actionable tasks (each doable by one agent)
238
553
  - Assign the best agent type to each task: explore (analysis), plan (design), code (implementation), test (validation), review (quality)
239
554
  - Set priorities (high/medium/low) and dependencies between tasks
240
- - Define success criteria for the project
241
- - If the prompt-architect skill is available, use its ROLE/CONTEXT/TASK/FORMAT patterns to structure the specification`
555
+ - Define success criteria for the project`
242
556
  );
243
557
  },
244
558
  });
@@ -246,7 +560,7 @@ Instructions:
246
560
  // ─── /plans Command ──────────────────────────────────────────────
247
561
 
248
562
  pi.registerCommand("plans", {
249
- description: "List existing project plans",
563
+ description: "List existing project plans and their execution status",
250
564
  handler: async (_args, ctx) => {
251
565
  try {
252
566
  if (!existsSync(plansDir)) {
@@ -267,6 +581,7 @@ Instructions:
267
581
  for (const specFile of specs) {
268
582
  const ts = specFile.replace("spec-", "").replace(".md", "");
269
583
  const todoFile = `todo-${ts}.md`;
584
+ const progressFile = `progress-${ts}.md`;
270
585
 
271
586
  try {
272
587
  const content = await readFile(join(plansDir, specFile), "utf-8");
@@ -276,9 +591,14 @@ Instructions:
276
591
  const date = ts.replace(/_/g, " ").substring(0, 10);
277
592
 
278
593
  const hasTodo = files.includes(todoFile);
594
+ const hasProgress = files.includes(progressFile);
595
+ const status = hasProgress ? "🟢 executed" : hasTodo ? "🟡 planned" : "⚪ spec only";
279
596
 
280
- output += `📋 **${title}** (${date})\n`;
281
- output += ` Spec: \`${specFile}\`${hasTodo ? ` | Todo: \`${todoFile}\`` : ""}\n`;
597
+ output += `📋 **${title}** (${date}) ${status}\n`;
598
+ output += ` Spec: \`${specFile}\``;
599
+ if (hasTodo) output += ` | Todo: \`${todoFile}\``;
600
+ if (hasProgress) output += ` | Progress: \`${progressFile}\``;
601
+ output += "\n";
282
602
  if (taskCount > 0) output += ` Tasks: ${taskCount}\n`;
283
603
  output += "\n";
284
604
  } catch {
@@ -286,7 +606,7 @@ Instructions:
286
606
  }
287
607
  }
288
608
 
289
- output += `_Use \`read .phi/plans/<file>\` to view a plan._`;
609
+ output += `_Commands: \`/plan\` (create), \`/run\` (execute), \`read .phi/plans/<file>\` (view)_`;
290
610
  ctx.ui.notify(output, "info");
291
611
  } catch (error) {
292
612
  ctx.ui.notify(`Failed to list plans: ${error}`, "error");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.58.0",
3
+ "version": "0.58.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -58,7 +58,7 @@
58
58
  "phi-code-ai": "^0.56.3",
59
59
  "phi-code-tui": "^0.56.3",
60
60
  "proper-lockfile": "^4.1.2",
61
- "sigma-agents": "^0.1.1",
61
+ "sigma-agents": "^0.1.2",
62
62
  "sigma-memory": "^0.1.0",
63
63
  "sigma-skills": "^0.1.0",
64
64
  "strip-ansi": "^7.1.0",