@phi-code-admin/phi-code 0.58.2 → 0.58.3

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,16 +1,19 @@
1
1
  /**
2
- * Orchestrator Extension - Project planning and automatic task execution
2
+ * Orchestrator Extension - Full-cycle project planning and execution
3
3
  *
4
- * Provides tools for the LLM to create structured project plans and execute them:
5
- * - /plan: Interactive planning commandcreates spec.md + todo.md
6
- * - /plans: List and manage existing plans
7
- * - /run: Execute plan tasks using sub-agents (each with own context + model)
8
- * - orchestrate tool: Create spec.md + todo.md from structured input
4
+ * WORKFLOW (single command):
5
+ * /plan <description> LLM analyzes orchestrate tool → spec + todo → auto-execute → progress
6
+ * Everything happens in one shot. No manual steps.
7
+ *
8
+ * Commands:
9
+ * /plan — Full workflow: plan + execute with sub-agents
10
+ * /run — Re-execute an existing plan (e.g. after fixes)
11
+ * /plans — List plans and their execution status
9
12
  *
10
13
  * Sub-agent execution:
11
14
  * Each task spawns a separate `phi` CLI process with:
12
15
  * - Its own system prompt (from the agent .md file)
13
- * - Its own model (from routing.json or current model)
16
+ * - Its own model (from routing.json)
14
17
  * - Its own context (isolated, no shared history)
15
18
  * - Its own tool access (read, write, edit, bash, etc.)
16
19
  * Results are collected into progress.md and reported to the user.
@@ -18,14 +21,23 @@
18
21
 
19
22
  import { Type } from "@sinclair/typebox";
20
23
  import type { ExtensionAPI } from "phi-code";
21
- import { writeFile, mkdir, readdir, readFile, access } from "node:fs/promises";
22
- import { join, dirname } from "node:path";
24
+ import { writeFile, mkdir, readdir, readFile } from "node:fs/promises";
25
+ import { join } from "node:path";
23
26
  import { existsSync, readFileSync } from "node:fs";
24
27
  import { execFile } from "node:child_process";
25
28
  import { homedir } from "node:os";
26
29
 
27
30
  // ─── Types ───────────────────────────────────────────────────────────────
28
31
 
32
+ interface TaskDef {
33
+ title: string;
34
+ description: string;
35
+ agent?: string;
36
+ priority?: string;
37
+ dependencies?: number[];
38
+ subtasks?: string[];
39
+ }
40
+
29
41
  interface TaskResult {
30
42
  taskIndex: number;
31
43
  title: string;
@@ -57,10 +69,6 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
57
69
 
58
70
  // ─── Agent Discovery ─────────────────────────────────────────────
59
71
 
60
- /**
61
- * Load agent definitions from .md files.
62
- * Searches: project .phi/agents/ → global ~/.phi/agent/agents/ → bundled agents/
63
- */
64
72
  function loadAgentDefs(): Map<string, AgentDef> {
65
73
  const agents = new Map<string, AgentDef>();
66
74
  const dirs = [
@@ -76,7 +84,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
76
84
  for (const file of files) {
77
85
  if (!file.endsWith(".md")) continue;
78
86
  const name = file.replace(".md", "");
79
- if (agents.has(name)) continue; // Priority: project > global > bundled
87
+ if (agents.has(name)) continue;
80
88
 
81
89
  try {
82
90
  const content = readFileSync(join(dir, file), "utf-8");
@@ -85,32 +93,34 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
85
93
 
86
94
  const frontmatter = fmMatch[1];
87
95
  const body = fmMatch[2].trim();
88
-
89
96
  const desc = frontmatter.match(/description:\s*(.+)/)?.[1] || "";
90
97
  const tools = frontmatter.match(/tools:\s*(.+)/)?.[1] || "";
91
98
 
92
99
  agents.set(name, { name, description: desc, tools, systemPrompt: body });
93
- } catch { /* skip unparseable files */ }
100
+ } catch { /* skip */ }
94
101
  }
95
- } catch { /* skip inaccessible dirs */ }
102
+ } catch { /* skip */ }
96
103
  }
97
104
 
98
105
  return agents;
99
106
  }
100
107
 
101
- /**
102
- * Resolve the model for an agent type from routing.json.
103
- */
104
108
  function resolveAgentModel(agentType: string): string | null {
105
109
  const routingPath = join(homedir(), ".phi", "agent", "routing.json");
106
110
  try {
107
111
  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 || {})) {
112
+ for (const [_cat, route] of Object.entries(config.routes || {})) {
110
113
  const r = route as any;
111
- if (r.agent === agentType) {
112
- return r.preferredModel || null;
113
- }
114
+ if (r.agent === agentType) return r.preferredModel || null;
115
+ }
116
+ // Map agent type to route category
117
+ const categoryMap: Record<string, string> = {
118
+ code: "code", explore: "explore", plan: "plan",
119
+ test: "test", review: "review", debug: "debug",
120
+ };
121
+ const category = categoryMap[agentType];
122
+ if (category && config.routes?.[category]) {
123
+ return config.routes[category].preferredModel || null;
114
124
  }
115
125
  return config.default?.model || null;
116
126
  } catch {
@@ -118,39 +128,23 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
118
128
  }
119
129
  }
120
130
 
121
- /**
122
- * Find the phi CLI binary path.
123
- */
124
131
  function findPhiBinary(): string {
125
- // 1. Try the dist/cli.js from our package
126
132
  const bundledCli = join(__dirname, "..", "..", "..", "dist", "cli.js");
127
133
  if (existsSync(bundledCli)) return bundledCli;
128
-
129
- // 2. Try global phi binary
130
134
  try {
131
135
  const which = require("child_process").execSync("which phi 2>/dev/null", { encoding: "utf-8" }).trim();
132
136
  if (which) return which;
133
137
  } catch { /* not in PATH */ }
134
-
135
- // 3. Fallback to npx
136
138
  return "npx";
137
139
  }
138
140
 
139
141
  // ─── Sub-Agent Execution ─────────────────────────────────────────
140
142
 
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
143
  function executeTask(
150
- task: { title: string; description: string; agent?: string; subtasks?: string[] },
144
+ task: TaskDef,
151
145
  agentDefs: Map<string, AgentDef>,
152
146
  cwd: string,
153
- timeoutMs: number = 300000, // 5 minutes per task
147
+ timeoutMs: number = 300000,
154
148
  ): Promise<TaskResult> {
155
149
  return new Promise((resolve) => {
156
150
  const agentType = task.agent || "code";
@@ -159,115 +153,203 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
159
153
  const phiBin = findPhiBinary();
160
154
  const startTime = Date.now();
161
155
 
162
- // Build the task prompt
163
156
  let taskPrompt = `Task: ${task.title}\n\n${task.description}`;
164
157
  if (task.subtasks && task.subtasks.length > 0) {
165
158
  taskPrompt += "\n\nSub-tasks:\n" + task.subtasks.map((st, i) => `${i + 1}. ${st}`).join("\n");
166
159
  }
167
160
  taskPrompt += "\n\nComplete this task. Be thorough and precise. Report what you did.";
168
161
 
169
- // Build the command
170
162
  const args: string[] = [];
171
- if (phiBin === "npx") {
172
- args.push("@phi-code-admin/phi-code");
173
- }
163
+ if (phiBin === "npx") args.push("@phi-code-admin/phi-code");
174
164
 
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
165
+ args.push("--print");
166
+ if (model && model !== "default") args.push("--model", model);
167
+ if (agentDef?.systemPrompt) args.push("--system-prompt", agentDef.systemPrompt);
168
+ args.push("--no-save");
184
169
  args.push(taskPrompt);
185
170
 
186
171
  const cmd = phiBin === "npx" ? "npx" : "node";
187
172
  const cmdArgs = phiBin === "npx" ? args : [phiBin, ...args];
188
173
 
189
- const child = execFile(cmd, cmdArgs, {
174
+ execFile(cmd, cmdArgs, {
190
175
  cwd,
191
176
  timeout: timeoutMs,
192
- maxBuffer: 10 * 1024 * 1024, // 10MB
177
+ maxBuffer: 10 * 1024 * 1024,
193
178
  env: { ...process.env },
194
179
  }, (error, stdout, stderr) => {
195
180
  const durationMs = Date.now() - startTime;
196
-
197
181
  if (error) {
198
182
  resolve({
199
- taskIndex: 0,
200
- title: task.title,
201
- agent: agentType,
202
- status: "error",
203
- output: `Error: ${error.message}\n${stderr || ""}`.trim(),
204
- durationMs,
183
+ taskIndex: 0, title: task.title, agent: agentType,
184
+ status: "error", output: `Error: ${error.message}\n${stderr || ""}`.trim(), durationMs,
205
185
  });
206
186
  } else {
207
187
  resolve({
208
- taskIndex: 0,
209
- title: task.title,
210
- agent: agentType,
211
- status: "success",
212
- output: stdout.trim(),
213
- durationMs,
188
+ taskIndex: 0, title: task.title, agent: agentType,
189
+ status: "success", output: stdout.trim(), durationMs,
214
190
  });
215
191
  }
216
192
  });
217
193
  });
218
194
  }
219
195
 
220
- // ─── Orchestrate Tool ────────────────────────────────────────────
196
+ // ─── Execute All Tasks ───────────────────────────────────────────
197
+
198
+ async function executePlan(
199
+ tasks: TaskDef[],
200
+ todoFile: string,
201
+ notify: (msg: string, type: "info" | "error" | "warning") => void,
202
+ ): Promise<{ results: TaskResult[]; progressFile: string }> {
203
+ const agentDefs = loadAgentDefs();
204
+ const progressFile = todoFile.replace("todo-", "progress-");
205
+ const progressPath = join(plansDir, progressFile);
206
+ let progress = `# Progress: ${todoFile}\n\n`;
207
+ progress += `**Started:** ${new Date().toLocaleString()}\n`;
208
+ progress += `**Tasks:** ${tasks.length}\n\n`;
209
+ await writeFile(progressPath, progress, "utf-8");
210
+
211
+ notify(`🚀 Executing ${tasks.length} tasks with sub-agents...`, "info");
212
+
213
+ const results: TaskResult[] = [];
214
+
215
+ for (let i = 0; i < tasks.length; i++) {
216
+ const task = tasks[i];
217
+ const agentType = task.agent || "code";
218
+ notify(`⏳ Task ${i + 1}/${tasks.length}: **${task.title}** [${agentType}]`, "info");
219
+
220
+ const result = await executeTask(task, agentDefs, process.cwd());
221
+ result.taskIndex = i + 1;
222
+ results.push(result);
223
+
224
+ const icon = result.status === "success" ? "✅" : "❌";
225
+ const duration = (result.durationMs / 1000).toFixed(1);
226
+ const outputPreview = result.output.length > 500 ? result.output.slice(0, 500) + "..." : result.output;
227
+ notify(`${icon} Task ${i + 1}: **${task.title}** (${duration}s)\n${outputPreview}`,
228
+ result.status === "success" ? "info" : "error");
229
+
230
+ progress += `## Task ${i + 1}: ${task.title}\n\n`;
231
+ progress += `- **Status:** ${result.status}\n`;
232
+ progress += `- **Agent:** ${result.agent}\n`;
233
+ progress += `- **Duration:** ${duration}s\n`;
234
+ progress += `- **Output:**\n\n\`\`\`\n${result.output.slice(0, 3000)}\n\`\`\`\n\n`;
235
+ await writeFile(progressPath, progress, "utf-8");
236
+ }
237
+
238
+ const succeeded = results.filter(r => r.status === "success").length;
239
+ const failed = results.filter(r => r.status === "error").length;
240
+ const totalTime = results.reduce((sum, r) => sum + r.durationMs, 0);
241
+
242
+ progress += `---\n\n## Summary\n\n`;
243
+ progress += `- **Completed:** ${new Date().toLocaleString()}\n`;
244
+ progress += `- **Succeeded:** ${succeeded}/${results.length}\n`;
245
+ progress += `- **Failed:** ${failed}\n`;
246
+ progress += `- **Total time:** ${(totalTime / 1000).toFixed(1)}s\n`;
247
+ await writeFile(progressPath, progress, "utf-8");
248
+
249
+ notify(
250
+ `\n🏁 **Execution complete!**\n` +
251
+ `✅ ${succeeded}/${results.length} succeeded | ❌ ${failed} failed | ⏱️ ${(totalTime / 1000).toFixed(1)}s\n` +
252
+ `Progress: \`${progressFile}\``,
253
+ failed === 0 ? "info" : "warning"
254
+ );
255
+
256
+ return { results, progressFile };
257
+ }
258
+
259
+ // ─── Generate Plan Files ─────────────────────────────────────────
260
+
261
+ function generateSpec(p: {
262
+ title: string; description: string; goals: string[]; requirements: string[];
263
+ architecture?: string[]; constraints?: string[]; successCriteria?: string[]; tasks: TaskDef[];
264
+ }): string {
265
+ let spec = `# ${p.title}\n\n`;
266
+ spec += `**Created:** ${new Date().toLocaleString()}\n\n`;
267
+ spec += `## Description\n\n${p.description}\n\n`;
268
+ spec += `## Goals\n\n`;
269
+ p.goals.forEach((g, i) => { spec += `${i + 1}. ${g}\n`; });
270
+ spec += "\n## Requirements\n\n";
271
+ p.requirements.forEach(r => { spec += `- ${r}\n`; });
272
+ spec += "\n";
273
+ if (p.architecture?.length) {
274
+ spec += `## Architecture\n\n`;
275
+ p.architecture.forEach(a => { spec += `- ${a}\n`; });
276
+ spec += "\n";
277
+ }
278
+ if (p.constraints?.length) {
279
+ spec += `## Constraints\n\n`;
280
+ p.constraints.forEach(c => { spec += `- ${c}\n`; });
281
+ spec += "\n";
282
+ }
283
+ if (p.successCriteria?.length) {
284
+ spec += `## Success Criteria\n\n`;
285
+ p.successCriteria.forEach(s => { spec += `- [ ] ${s}\n`; });
286
+ spec += "\n";
287
+ }
288
+ spec += `## Task Overview\n\n| # | Task | Agent | Priority | Dependencies |\n|---|------|-------|----------|-------------|\n`;
289
+ p.tasks.forEach((t, i) => {
290
+ const deps = t.dependencies?.map(d => `#${d}`).join(", ") || "—";
291
+ spec += `| ${i + 1} | ${t.title} | ${t.agent || "code"} | ${t.priority || "medium"} | ${deps} |\n`;
292
+ });
293
+ spec += `\n---\n*Generated by Phi Code Orchestrator*\n`;
294
+ return spec;
295
+ }
296
+
297
+ function generateTodo(title: string, tasks: TaskDef[]): string {
298
+ let todo = `# TODO: ${title}\n\n`;
299
+ todo += `**Created:** ${new Date().toLocaleString()}\n`;
300
+ todo += `**Tasks:** ${tasks.length}\n**Status:** executing\n\n`;
301
+ tasks.forEach((t, i) => {
302
+ const agentTag = t.agent ? ` [${t.agent}]` : "";
303
+ const prioTag = t.priority === "high" ? " 🔴" : t.priority === "low" ? " 🟢" : " 🟡";
304
+ const depsTag = t.dependencies?.length ? ` (after #${t.dependencies.join(", #")})` : "";
305
+ todo += `## Task ${i + 1}: ${t.title}${prioTag}${agentTag}${depsTag}\n\n- [ ] ${t.description}\n`;
306
+ if (t.subtasks) t.subtasks.forEach(st => { todo += ` - [ ] ${st}\n`; });
307
+ todo += "\n";
308
+ });
309
+ todo += `---\n\n## Progress\n\n- Total: ${tasks.length} tasks\n`;
310
+ todo += `- High priority: ${tasks.filter(t => t.priority === "high").length}\n`;
311
+ todo += `- Agents: ${[...new Set(tasks.map(t => t.agent || "code"))].join(", ")}\n`;
312
+ return todo;
313
+ }
314
+
315
+ // ─── Orchestrate Tool (plan + auto-execute) ──────────────────────
221
316
 
222
317
  pi.registerTool({
223
318
  name: "orchestrate",
224
319
  label: "Project Orchestrator",
225
- description: "Create structured project plan files (spec.md + todo.md) from analyzed project requirements. Call this AFTER you have analyzed the user's request and structured it into goals, requirements, architecture decisions, and tasks.",
226
- promptSnippet: "Create spec.md + todo.md project plans. Use prompt-architect patterns to structure specs before calling.",
320
+ description: "Create a project plan AND automatically execute all tasks with sub-agents. Each agent gets its own isolated context, model, and system prompt. Call this after analyzing the user's project request.",
321
+ promptSnippet: "Plan + execute projects. Creates spec/todo, then runs each task with an isolated sub-agent.",
227
322
  promptGuidelines: [
228
- "When asked to plan a project: first analyze the request thoroughly, then call orchestrate with structured data.",
229
- "Use the prompt-architect skill patterns (ROLE/CONTEXT/TASK/FORMAT/CONSTRAINTS) when structuring specifications.",
230
- "Break tasks into small, actionable items. Each task should be completable by a single sub-agent.",
231
- "Assign agent types to tasks: 'explore' for analysis, 'plan' for design, 'code' for implementation, 'test' for validation, 'review' for quality.",
323
+ "When asked to plan or build a project: analyze the request, then call orchestrate. It will plan AND execute automatically.",
324
+ "Break tasks into small, actionable items. Each task is executed by an isolated sub-agent.",
325
+ "Assign agent types: 'explore' (analysis), 'plan' (design), 'code' (implementation), 'test' (validation), 'review' (quality).",
326
+ "Order tasks by dependency. Sub-agents execute sequentially, respecting the order.",
232
327
  ],
233
328
  parameters: Type.Object({
234
329
  title: Type.String({ description: "Project title" }),
235
330
  description: Type.String({ description: "Full project description with context" }),
236
331
  goals: Type.Array(Type.String(), { description: "List of project goals" }),
237
332
  requirements: Type.Array(Type.String(), { description: "Technical and functional requirements" }),
238
- architecture: Type.Optional(Type.Array(Type.String(), { description: "Architecture decisions and tech stack" })),
333
+ architecture: Type.Optional(Type.Array(Type.String(), { description: "Architecture decisions" })),
239
334
  tasks: Type.Array(
240
335
  Type.Object({
241
336
  title: Type.String({ description: "Task title" }),
242
- description: Type.String({ description: "Detailed task description" }),
243
- agent: Type.Optional(Type.String({ description: "Recommended agent: explore, plan, code, test, or review" })),
337
+ description: Type.String({ description: "Detailed task description with enough context for the agent to work independently" }),
338
+ agent: Type.Optional(Type.String({ description: "Agent type: explore, plan, code, test, or review" })),
244
339
  priority: Type.Optional(Type.String({ description: "high, medium, or low" })),
245
- dependencies: Type.Optional(Type.Array(Type.Number(), { description: "IDs of tasks this depends on (1-indexed)" })),
340
+ dependencies: Type.Optional(Type.Array(Type.Number(), { description: "IDs of prerequisite tasks (1-indexed)" })),
246
341
  subtasks: Type.Optional(Type.Array(Type.String(), { description: "Sub-task descriptions" })),
247
342
  }),
248
- { description: "Ordered list of tasks" }
343
+ { description: "Ordered list of tasks to execute" }
249
344
  ),
250
- constraints: Type.Optional(Type.Array(Type.String(), { description: "Project constraints and limitations" })),
251
- successCriteria: Type.Optional(Type.Array(Type.String(), { description: "How to verify the project is done" })),
345
+ constraints: Type.Optional(Type.Array(Type.String(), { description: "Project constraints" })),
346
+ successCriteria: Type.Optional(Type.Array(Type.String(), { description: "Completion criteria" })),
252
347
  }),
253
348
 
254
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
349
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
255
350
  const p = params as {
256
- title: string;
257
- description: string;
258
- goals: string[];
259
- requirements: string[];
260
- architecture?: string[];
261
- tasks: Array<{
262
- title: string;
263
- description: string;
264
- agent?: string;
265
- priority?: string;
266
- dependencies?: number[];
267
- subtasks?: string[];
268
- }>;
269
- constraints?: string[];
270
- successCriteria?: string[];
351
+ title: string; description: string; goals: string[]; requirements: string[];
352
+ architecture?: string[]; tasks: TaskDef[]; constraints?: string[]; successCriteria?: string[];
271
353
  };
272
354
 
273
355
  try {
@@ -276,99 +358,54 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
276
358
  const specFile = `spec-${ts}.md`;
277
359
  const todoFile = `todo-${ts}.md`;
278
360
 
279
- // ─── Generate spec.md ─────────────────────────────────
280
- let spec = `# ${p.title}\n\n`;
281
- spec += `**Created:** ${new Date().toLocaleString()}\n\n`;
282
- spec += `## Description\n\n${p.description}\n\n`;
283
-
284
- spec += `## Goals\n\n`;
285
- p.goals.forEach((g, i) => { spec += `${i + 1}. ${g}\n`; });
286
- spec += "\n";
287
-
288
- spec += `## Requirements\n\n`;
289
- p.requirements.forEach(r => { spec += `- ${r}\n`; });
290
- spec += "\n";
291
-
292
- if (p.architecture && p.architecture.length > 0) {
293
- spec += `## Architecture\n\n`;
294
- p.architecture.forEach(a => { spec += `- ${a}\n`; });
295
- spec += "\n";
296
- }
297
-
298
- if (p.constraints && p.constraints.length > 0) {
299
- spec += `## Constraints\n\n`;
300
- p.constraints.forEach(c => { spec += `- ${c}\n`; });
301
- spec += "\n";
302
- }
303
-
304
- if (p.successCriteria && p.successCriteria.length > 0) {
305
- spec += `## Success Criteria\n\n`;
306
- p.successCriteria.forEach(s => { spec += `- [ ] ${s}\n`; });
307
- spec += "\n";
308
- }
361
+ // Generate and write plan files
362
+ const spec = generateSpec(p);
363
+ const todo = generateTodo(p.title, p.tasks);
364
+ await writeFile(join(plansDir, specFile), spec, "utf-8");
365
+ await writeFile(join(plansDir, todoFile), todo, "utf-8");
309
366
 
310
- spec += `## Task Overview\n\n`;
311
- spec += `| # | Task | Agent | Priority | Dependencies |\n`;
312
- spec += `|---|------|-------|----------|-------------|\n`;
313
- p.tasks.forEach((t, i) => {
314
- const deps = t.dependencies?.map(d => `#${d}`).join(", ") || "—";
315
- spec += `| ${i + 1} | ${t.title} | ${t.agent || "code"} | ${t.priority || "medium"} | ${deps} |\n`;
316
- });
317
- spec += "\n";
318
-
319
- spec += `---\n*Generated by Phi Code Orchestrator*\n`;
320
-
321
- // ─── Generate todo.md ─────────────────────────────────
322
- let todo = `# TODO: ${p.title}\n\n`;
323
- todo += `**Created:** ${new Date().toLocaleString()}\n`;
324
- todo += `**Tasks:** ${p.tasks.length}\n`;
325
- todo += `**Status:** pending\n\n`;
326
-
327
- p.tasks.forEach((t, i) => {
328
- const agentTag = t.agent ? ` [${t.agent}]` : "";
329
- const prioTag = t.priority === "high" ? " 🔴" : t.priority === "low" ? " 🟢" : " 🟡";
330
- const depsTag = t.dependencies?.length ? ` (after #${t.dependencies.join(", #")})` : "";
331
-
332
- todo += `## Task ${i + 1}: ${t.title}${prioTag}${agentTag}${depsTag}\n\n`;
333
- todo += `- [ ] ${t.description}\n`;
334
-
335
- if (t.subtasks) {
336
- t.subtasks.forEach(st => {
337
- todo += ` - [ ] ${st}\n`;
338
- });
367
+ // Notify plan created
368
+ const notify = (msg: string, type: "info" | "error" | "warning") => {
369
+ // Use onUpdate for streaming progress to the user
370
+ if (_onUpdate) {
371
+ _onUpdate({ content: [{ type: "text", text: msg }] });
339
372
  }
340
- todo += "\n";
341
- });
373
+ };
342
374
 
343
- todo += `---\n\n## Progress\n\n`;
344
- todo += `- Total: ${p.tasks.length} tasks\n`;
345
- todo += `- High priority: ${p.tasks.filter(t => t.priority === "high").length}\n`;
346
- todo += `- Agents needed: ${[...new Set(p.tasks.map(t => t.agent || "code"))].join(", ")}\n\n`;
347
- todo += `*Run \`/run\` to execute tasks automatically with sub-agents.*\n`;
375
+ notify(`📋 Plan created: **${p.title}** (${p.tasks.length} tasks)\nNow executing with sub-agents...`, "info");
348
376
 
349
- // Write files
350
- await writeFile(join(plansDir, specFile), spec, "utf-8");
351
- await writeFile(join(plansDir, todoFile), todo, "utf-8");
377
+ // Auto-execute all tasks
378
+ const { results, progressFile } = await executePlan(p.tasks, todoFile, notify);
379
+
380
+ const succeeded = results.filter(r => r.status === "success").length;
381
+ const failed = results.filter(r => r.status === "error").length;
382
+ const totalTime = results.reduce((sum, r) => sum + r.durationMs, 0);
352
383
 
353
- const summary = `**✅ Project plan created!**
384
+ const summary = `**🏁 Project "${p.title}" — Complete!**
354
385
 
355
- 📋 **${p.title}**
386
+ **Plan files:** \`${specFile}\`, \`${todoFile}\`
387
+ **Progress:** \`${progressFile}\`
356
388
 
357
- **Files:**
358
- - \`${specFile}\` — Full specification
359
- - \`${todoFile}\` — Task list with agent assignments
389
+ **Results:**
390
+ - ✅ Succeeded: ${succeeded}/${results.length}
391
+ - ❌ Failed: ${failed}
392
+ - ⏱️ Total time: ${(totalTime / 1000).toFixed(1)}s
360
393
 
361
- **Summary:**
362
- - ${p.goals.length} goals, ${p.requirements.length} requirements
363
- - ${p.tasks.length} tasks (${p.tasks.filter(t => t.priority === "high").length} high priority)
364
- - Agents: ${[...new Set(p.tasks.map(t => t.agent || "code"))].join(", ")}
394
+ **Task details:**
395
+ ${results.map(r => {
396
+ const icon = r.status === "success" ? "" : "❌";
397
+ return `${icon} Task ${r.taskIndex}: ${r.title} [${r.agent}] (${(r.durationMs / 1000).toFixed(1)}s)`;
398
+ }).join("\n")}
365
399
 
366
- **Execute:** Run \`/run\` to launch sub-agents and execute tasks automatically.
367
- Each agent gets its own context, model, and system prompt.`;
400
+ All files in \`.phi/plans/\``;
368
401
 
369
402
  return {
370
403
  content: [{ type: "text", text: summary }],
371
- details: { specFile, todoFile, taskCount: p.tasks.length, title: p.title },
404
+ details: {
405
+ specFile, todoFile, progressFile,
406
+ taskCount: p.tasks.length, succeeded, failed,
407
+ totalTimeMs: totalTime, title: p.title,
408
+ },
372
409
  };
373
410
  } catch (error) {
374
411
  return {
@@ -379,14 +416,58 @@ Each agent gets its own context, model, and system prompt.`;
379
416
  },
380
417
  });
381
418
 
382
- // ─── /run Command — Execute plan with sub-agents ─────────────────
419
+ // ─── /plan Command — Full workflow ───────────────────────────────
420
+
421
+ pi.registerCommand("plan", {
422
+ description: "Plan AND execute a project: creates spec + todo, then runs each task with isolated sub-agents",
423
+ handler: async (args, ctx) => {
424
+ const description = args.trim();
425
+
426
+ if (!description) {
427
+ ctx.ui.notify(`**Usage:** \`/plan <project description>\`
428
+
429
+ **Full workflow in one command:**
430
+ 1. LLM analyzes your description
431
+ 2. Creates spec.md + todo.md
432
+ 3. Executes each task with an isolated sub-agent
433
+ 4. Each agent has its own context, model, and system prompt
434
+ 5. Results saved to progress.md
435
+
436
+ **Examples:**
437
+ /plan Build a REST API for user authentication with JWT
438
+ /plan Add test coverage to the payment module
439
+ /plan Refactor the frontend to use TypeScript
440
+
441
+ **Other commands:**
442
+ /run — Re-execute an existing plan
443
+ /plans — List all plans and status`, "info");
444
+ return;
445
+ }
446
+
447
+ ctx.sendUserMessage(
448
+ `Analyze this project request and execute it using the orchestrate tool.
449
+ The orchestrate tool will create the plan AND execute all tasks automatically with sub-agents.
450
+
451
+ Project: ${description}
452
+
453
+ Instructions:
454
+ - Identify goals, requirements, architecture decisions
455
+ - Break into small tasks (each executable by one sub-agent independently)
456
+ - Each task description must contain FULL context — the sub-agent has NO shared history
457
+ - Assign agent types: explore (analysis), plan (design), code (implementation), test (validation), review (quality)
458
+ - Set priorities and dependencies
459
+ - Call the orchestrate tool — it handles everything from there`
460
+ );
461
+ },
462
+ });
463
+
464
+ // ─── /run Command — Re-execute existing plan ─────────────────────
383
465
 
384
466
  pi.registerCommand("run", {
385
- description: "Execute the latest plan's tasks using sub-agents (each with own context and model)",
467
+ description: "Re-execute an existing plan's tasks with sub-agents",
386
468
  handler: async (args, ctx) => {
387
- // Find the latest todo file
388
469
  if (!existsSync(plansDir)) {
389
- ctx.ui.notify("No plans found. Use `/plan <description>` to create one first.", "warning");
470
+ ctx.ui.notify("No plans found. Use `/plan <description>` to create and execute one.", "warning");
390
471
  return;
391
472
  }
392
473
 
@@ -398,219 +479,99 @@ Each agent gets its own context, model, and system prompt.`;
398
479
  return;
399
480
  }
400
481
 
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;
482
+ const todoFile = todoFiles[0];
483
+ const todoContent = await readFile(join(plansDir, todoFile), "utf-8");
484
+
485
+ // Parse tasks
486
+ const tasks: TaskDef[] = [];
487
+ const sections = todoContent.split(/## Task \d+:/);
488
+ for (let i = 1; i < sections.length; i++) {
489
+ const section = sections[i];
490
+ const titleMatch = section.match(/^(.+?)(?:\s*🔴|\s*🟡|\s*🟢)/);
491
+ const agentMatch = section.match(/\[(\w+)\]/);
492
+ const descMatch = section.match(/- \[ \] (.+)/);
412
493
  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]);
494
+ const stMatches = section.matchAll(/ - \[ \] (.+)/g);
495
+ for (const m of stMatches) subtasks.push(m[1]);
496
+
497
+ if (titleMatch && descMatch) {
498
+ tasks.push({
499
+ title: titleMatch[1].trim(),
500
+ agent: agentMatch?.[1] || "code",
501
+ description: descMatch[1].trim(),
502
+ subtasks: subtasks.length > 0 ? subtasks : undefined,
503
+ });
417
504
  }
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
505
  }
427
506
 
428
507
  if (tasks.length === 0) {
429
- ctx.ui.notify("Could not parse tasks from the todo file. Check the format.", "error");
508
+ ctx.ui.notify("Could not parse tasks from todo file.", "error");
430
509
  return;
431
510
  }
432
511
 
433
- // Load agent definitions
434
- const agentDefs = loadAgentDefs();
435
- const availableAgents = [...agentDefs.keys()].join(", ") || "none loaded";
436
-
437
- // Confirm execution
438
512
  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?`
513
+ "Re-execute Plan",
514
+ `${tasks.length} tasks found in \`${todoFile}\`.\nEach will spawn an isolated sub-agent.\n\nProceed?`
444
515
  );
445
-
446
516
  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
-
520
- // ─── /plan Command ───────────────────────────────────────────────
521
-
522
- pi.registerCommand("plan", {
523
- description: "Create a structured project plan (the LLM analyzes your description, then creates spec + todo files)",
524
- handler: async (args, ctx) => {
525
- const description = args.trim();
526
-
527
- if (!description) {
528
- ctx.ui.notify(`**Usage:** \`/plan <project description>\`
529
-
530
- **Examples:**
531
- /plan Build a REST API for user authentication with JWT tokens
532
- /plan Migrate the frontend from React to Next.js App Router
533
- /plan Add comprehensive test coverage to the payment module
534
-
535
- The LLM will:
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
540
-
541
- Each sub-agent gets its own isolated context, model, and system prompt.`, "info");
517
+ ctx.ui.notify("Cancelled.", "info");
542
518
  return;
543
519
  }
544
520
 
545
- ctx.sendUserMessage(
546
- `Please analyze this project request and create a structured plan using the orchestrate tool.
547
-
548
- Project description: ${description}
549
-
550
- Instructions:
551
- - Identify clear goals, requirements, and architecture decisions
552
- - Break the work into small, actionable tasks (each doable by one agent)
553
- - Assign the best agent type to each task: explore (analysis), plan (design), code (implementation), test (validation), review (quality)
554
- - Set priorities (high/medium/low) and dependencies between tasks
555
- - Define success criteria for the project`
556
- );
521
+ await executePlan(tasks, todoFile, (msg, type) => ctx.ui.notify(msg, type));
557
522
  },
558
523
  });
559
524
 
560
525
  // ─── /plans Command ──────────────────────────────────────────────
561
526
 
562
527
  pi.registerCommand("plans", {
563
- description: "List existing project plans and their execution status",
528
+ description: "List all project plans and their execution status",
564
529
  handler: async (_args, ctx) => {
565
- try {
566
- if (!existsSync(plansDir)) {
567
- ctx.ui.notify("No plans yet. Use `/plan <description>` to create one.", "info");
568
- return;
569
- }
570
-
571
- const files = await readdir(plansDir);
572
- const specs = files.filter(f => f.startsWith("spec-") && f.endsWith(".md")).sort().reverse();
530
+ if (!existsSync(plansDir)) {
531
+ ctx.ui.notify("No plans yet. Use `/plan <description>` to create and execute one.", "info");
532
+ return;
533
+ }
573
534
 
574
- if (specs.length === 0) {
575
- ctx.ui.notify("No plans found. Use `/plan <description>` to create one.", "info");
576
- return;
577
- }
535
+ const files = await readdir(plansDir);
536
+ const specs = files.filter(f => f.startsWith("spec-") && f.endsWith(".md")).sort().reverse();
578
537
 
579
- let output = `📁 **Project Plans** (${specs.length})\n\n`;
538
+ if (specs.length === 0) {
539
+ ctx.ui.notify("No plans found.", "info");
540
+ return;
541
+ }
580
542
 
581
- for (const specFile of specs) {
582
- const ts = specFile.replace("spec-", "").replace(".md", "");
583
- const todoFile = `todo-${ts}.md`;
584
- const progressFile = `progress-${ts}.md`;
543
+ let output = `📁 **Project Plans** (${specs.length})\n\n`;
585
544
 
586
- try {
587
- const content = await readFile(join(plansDir, specFile), "utf-8");
588
- const titleMatch = content.match(/^# (.+)$/m);
589
- const title = titleMatch ? titleMatch[1] : specFile;
590
- const taskCount = (content.match(/\| \d+ \|/g) || []).length;
591
- const date = ts.replace(/_/g, " ").substring(0, 10);
592
-
593
- const hasTodo = files.includes(todoFile);
594
- const hasProgress = files.includes(progressFile);
595
- const status = hasProgress ? "🟢 executed" : hasTodo ? "🟡 planned" : "⚪ spec only";
596
-
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";
602
- if (taskCount > 0) output += ` Tasks: ${taskCount}\n`;
603
- output += "\n";
604
- } catch {
605
- output += `📋 \`${specFile}\`\n\n`;
606
- }
545
+ for (const specFile of specs) {
546
+ const ts = specFile.replace("spec-", "").replace(".md", "");
547
+ const todoFile = `todo-${ts}.md`;
548
+ const progressFile = `progress-${ts}.md`;
549
+
550
+ try {
551
+ const content = await readFile(join(plansDir, specFile), "utf-8");
552
+ const titleMatch = content.match(/^# (.+)$/m);
553
+ const title = titleMatch ? titleMatch[1] : specFile;
554
+ const taskCount = (content.match(/\| \d+ \|/g) || []).length;
555
+ const date = ts.replace(/_/g, " ").substring(0, 10);
556
+
557
+ const hasTodo = files.includes(todoFile);
558
+ const hasProgress = files.includes(progressFile);
559
+ const status = hasProgress ? "🟢 executed" : hasTodo ? "🟡 planned" : "⚪ spec only";
560
+
561
+ output += `📋 **${title}** (${date}) ${status}\n`;
562
+ output += ` Spec: \`${specFile}\``;
563
+ if (hasTodo) output += ` | Todo: \`${todoFile}\``;
564
+ if (hasProgress) output += ` | Progress: \`${progressFile}\``;
565
+ output += "\n";
566
+ if (taskCount > 0) output += ` Tasks: ${taskCount}\n`;
567
+ output += "\n";
568
+ } catch {
569
+ output += `📋 \`${specFile}\`\n\n`;
607
570
  }
608
-
609
- output += `_Commands: \`/plan\` (create), \`/run\` (execute), \`read .phi/plans/<file>\` (view)_`;
610
- ctx.ui.notify(output, "info");
611
- } catch (error) {
612
- ctx.ui.notify(`Failed to list plans: ${error}`, "error");
613
571
  }
572
+
573
+ output += `_Commands: \`/plan\` (create+execute), \`/run\` (re-execute), \`read .phi/plans/<file>\` (view)_`;
574
+ ctx.ui.notify(output, "info");
614
575
  },
615
576
  });
616
577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.58.2",
3
+ "version": "0.58.3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {