@phi-code-admin/phi-code 0.58.1 → 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,27 +1,60 @@
1
1
  /**
2
- * Orchestrator Extension - Project planning and task management
2
+ * Orchestrator Extension - Full-cycle project planning and execution
3
3
  *
4
- * Provides tools for the LLM to create structured project plans:
5
- * - /plan: Interactive planning command
6
- * - /plans: List and manage existing plans
7
- * - 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.
8
7
  *
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.
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
13
12
  *
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.
13
+ * Sub-agent execution:
14
+ * Each task spawns a separate `phi` CLI process with:
15
+ * - Its own system prompt (from the agent .md file)
16
+ * - Its own model (from routing.json)
17
+ * - Its own context (isolated, no shared history)
18
+ * - Its own tool access (read, write, edit, bash, etc.)
19
+ * Results are collected into progress.md and reported to the user.
18
20
  */
19
21
 
20
22
  import { Type } from "@sinclair/typebox";
21
23
  import type { ExtensionAPI } from "phi-code";
22
- import { writeFile, mkdir, readdir, readFile, access } from "node:fs/promises";
24
+ import { writeFile, mkdir, readdir, readFile } from "node:fs/promises";
23
25
  import { join } from "node:path";
24
- import { existsSync } from "node:fs";
26
+ import { existsSync, readFileSync } from "node:fs";
27
+ import { execFile } from "node:child_process";
28
+ import { homedir } from "node:os";
29
+
30
+ // ─── Types ───────────────────────────────────────────────────────────────
31
+
32
+ interface TaskDef {
33
+ title: string;
34
+ description: string;
35
+ agent?: string;
36
+ priority?: string;
37
+ dependencies?: number[];
38
+ subtasks?: string[];
39
+ }
40
+
41
+ interface TaskResult {
42
+ taskIndex: number;
43
+ title: string;
44
+ agent: string;
45
+ status: "success" | "error" | "skipped";
46
+ output: string;
47
+ durationMs: number;
48
+ }
49
+
50
+ interface AgentDef {
51
+ name: string;
52
+ description: string;
53
+ tools: string;
54
+ systemPrompt: string;
55
+ }
56
+
57
+ // ─── Extension ───────────────────────────────────────────────────────────
25
58
 
26
59
  export default function orchestratorExtension(pi: ExtensionAPI) {
27
60
  const plansDir = join(process.cwd(), ".phi", "plans");
@@ -34,57 +67,289 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
34
67
  return new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
35
68
  }
36
69
 
37
- // ─── Orchestrate Tool ────────────────────────────────────────────
70
+ // ─── Agent Discovery ─────────────────────────────────────────────
71
+
72
+ function loadAgentDefs(): Map<string, AgentDef> {
73
+ const agents = new Map<string, AgentDef>();
74
+ const dirs = [
75
+ join(process.cwd(), ".phi", "agents"),
76
+ join(homedir(), ".phi", "agent", "agents"),
77
+ join(__dirname, "..", "..", "..", "agents"),
78
+ ];
79
+
80
+ for (const dir of dirs) {
81
+ if (!existsSync(dir)) continue;
82
+ try {
83
+ const files = require("fs").readdirSync(dir) as string[];
84
+ for (const file of files) {
85
+ if (!file.endsWith(".md")) continue;
86
+ const name = file.replace(".md", "");
87
+ if (agents.has(name)) continue;
88
+
89
+ try {
90
+ const content = readFileSync(join(dir, file), "utf-8");
91
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
92
+ if (!fmMatch) continue;
93
+
94
+ const frontmatter = fmMatch[1];
95
+ const body = fmMatch[2].trim();
96
+ const desc = frontmatter.match(/description:\s*(.+)/)?.[1] || "";
97
+ const tools = frontmatter.match(/tools:\s*(.+)/)?.[1] || "";
98
+
99
+ agents.set(name, { name, description: desc, tools, systemPrompt: body });
100
+ } catch { /* skip */ }
101
+ }
102
+ } catch { /* skip */ }
103
+ }
104
+
105
+ return agents;
106
+ }
107
+
108
+ function resolveAgentModel(agentType: string): string | null {
109
+ const routingPath = join(homedir(), ".phi", "agent", "routing.json");
110
+ try {
111
+ const config = JSON.parse(readFileSync(routingPath, "utf-8"));
112
+ for (const [_cat, route] of Object.entries(config.routes || {})) {
113
+ const r = route as any;
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;
124
+ }
125
+ return config.default?.model || null;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ function findPhiBinary(): string {
132
+ const bundledCli = join(__dirname, "..", "..", "..", "dist", "cli.js");
133
+ if (existsSync(bundledCli)) return bundledCli;
134
+ try {
135
+ const which = require("child_process").execSync("which phi 2>/dev/null", { encoding: "utf-8" }).trim();
136
+ if (which) return which;
137
+ } catch { /* not in PATH */ }
138
+ return "npx";
139
+ }
140
+
141
+ // ─── Sub-Agent Execution ─────────────────────────────────────────
142
+
143
+ function executeTask(
144
+ task: TaskDef,
145
+ agentDefs: Map<string, AgentDef>,
146
+ cwd: string,
147
+ timeoutMs: number = 300000,
148
+ ): Promise<TaskResult> {
149
+ return new Promise((resolve) => {
150
+ const agentType = task.agent || "code";
151
+ const agentDef = agentDefs.get(agentType);
152
+ const model = resolveAgentModel(agentType);
153
+ const phiBin = findPhiBinary();
154
+ const startTime = Date.now();
155
+
156
+ let taskPrompt = `Task: ${task.title}\n\n${task.description}`;
157
+ if (task.subtasks && task.subtasks.length > 0) {
158
+ taskPrompt += "\n\nSub-tasks:\n" + task.subtasks.map((st, i) => `${i + 1}. ${st}`).join("\n");
159
+ }
160
+ taskPrompt += "\n\nComplete this task. Be thorough and precise. Report what you did.";
161
+
162
+ const args: string[] = [];
163
+ if (phiBin === "npx") args.push("@phi-code-admin/phi-code");
164
+
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");
169
+ args.push(taskPrompt);
170
+
171
+ const cmd = phiBin === "npx" ? "npx" : "node";
172
+ const cmdArgs = phiBin === "npx" ? args : [phiBin, ...args];
173
+
174
+ execFile(cmd, cmdArgs, {
175
+ cwd,
176
+ timeout: timeoutMs,
177
+ maxBuffer: 10 * 1024 * 1024,
178
+ env: { ...process.env },
179
+ }, (error, stdout, stderr) => {
180
+ const durationMs = Date.now() - startTime;
181
+ if (error) {
182
+ resolve({
183
+ taskIndex: 0, title: task.title, agent: agentType,
184
+ status: "error", output: `Error: ${error.message}\n${stderr || ""}`.trim(), durationMs,
185
+ });
186
+ } else {
187
+ resolve({
188
+ taskIndex: 0, title: task.title, agent: agentType,
189
+ status: "success", output: stdout.trim(), durationMs,
190
+ });
191
+ }
192
+ });
193
+ });
194
+ }
195
+
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) ──────────────────────
38
316
 
39
317
  pi.registerTool({
40
318
  name: "orchestrate",
41
319
  label: "Project Orchestrator",
42
- 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.",
43
- 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.",
44
322
  promptGuidelines: [
45
- "When asked to plan a project: first analyze the request thoroughly, then call orchestrate with structured data.",
46
- "Use the prompt-architect skill patterns (ROLE/CONTEXT/TASK/FORMAT/CONSTRAINTS) when structuring specifications.",
47
- "Break tasks into small, actionable items. Each task should be completable by a single sub-agent.",
48
- "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.",
49
327
  ],
50
328
  parameters: Type.Object({
51
329
  title: Type.String({ description: "Project title" }),
52
330
  description: Type.String({ description: "Full project description with context" }),
53
331
  goals: Type.Array(Type.String(), { description: "List of project goals" }),
54
332
  requirements: Type.Array(Type.String(), { description: "Technical and functional requirements" }),
55
- architecture: Type.Optional(Type.Array(Type.String(), { description: "Architecture decisions and tech stack" })),
333
+ architecture: Type.Optional(Type.Array(Type.String(), { description: "Architecture decisions" })),
56
334
  tasks: Type.Array(
57
335
  Type.Object({
58
336
  title: Type.String({ description: "Task title" }),
59
- description: Type.String({ description: "Detailed task description" }),
60
- 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" })),
61
339
  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" })),
340
+ dependencies: Type.Optional(Type.Array(Type.Number(), { description: "IDs of prerequisite tasks (1-indexed)" })),
63
341
  subtasks: Type.Optional(Type.Array(Type.String(), { description: "Sub-task descriptions" })),
64
342
  }),
65
- { description: "Ordered list of tasks" }
343
+ { description: "Ordered list of tasks to execute" }
66
344
  ),
67
- constraints: Type.Optional(Type.Array(Type.String(), { description: "Project constraints and limitations" })),
68
- 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" })),
69
347
  }),
70
348
 
71
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
349
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
72
350
  const p = params as {
73
- title: string;
74
- description: string;
75
- goals: string[];
76
- requirements: string[];
77
- architecture?: string[];
78
- tasks: Array<{
79
- title: string;
80
- description: string;
81
- agent?: string;
82
- priority?: string;
83
- dependencies?: number[];
84
- subtasks?: string[];
85
- }>;
86
- constraints?: string[];
87
- successCriteria?: string[];
351
+ title: string; description: string; goals: string[]; requirements: string[];
352
+ architecture?: string[]; tasks: TaskDef[]; constraints?: string[]; successCriteria?: string[];
88
353
  };
89
354
 
90
355
  try {
@@ -93,104 +358,54 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
93
358
  const specFile = `spec-${ts}.md`;
94
359
  const todoFile = `todo-${ts}.md`;
95
360
 
96
- // ─── Generate spec.md ─────────────────────────────────
97
- let spec = `# ${p.title}\n\n`;
98
- spec += `**Created:** ${new Date().toLocaleString()}\n\n`;
99
- spec += `## Description\n\n${p.description}\n\n`;
100
-
101
- spec += `## Goals\n\n`;
102
- p.goals.forEach((g, i) => { spec += `${i + 1}. ${g}\n`; });
103
- spec += "\n";
104
-
105
- spec += `## Requirements\n\n`;
106
- p.requirements.forEach(r => { spec += `- ${r}\n`; });
107
- spec += "\n";
108
-
109
- if (p.architecture && p.architecture.length > 0) {
110
- spec += `## Architecture\n\n`;
111
- p.architecture.forEach(a => { spec += `- ${a}\n`; });
112
- spec += "\n";
113
- }
114
-
115
- if (p.constraints && p.constraints.length > 0) {
116
- spec += `## Constraints\n\n`;
117
- p.constraints.forEach(c => { spec += `- ${c}\n`; });
118
- spec += "\n";
119
- }
120
-
121
- if (p.successCriteria && p.successCriteria.length > 0) {
122
- spec += `## Success Criteria\n\n`;
123
- p.successCriteria.forEach(s => { spec += `- [ ] ${s}\n`; });
124
- spec += "\n";
125
- }
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");
126
366
 
127
- spec += `## Task Overview\n\n`;
128
- spec += `| # | Task | Agent | Priority | Dependencies |\n`;
129
- spec += `|---|------|-------|----------|-------------|\n`;
130
- p.tasks.forEach((t, i) => {
131
- const deps = t.dependencies?.map(d => `#${d}`).join(", ") || "—";
132
- spec += `| ${i + 1} | ${t.title} | ${t.agent || "code"} | ${t.priority || "medium"} | ${deps} |\n`;
133
- });
134
- spec += "\n";
135
-
136
- spec += `---\n*Generated by Phi Code Orchestrator*\n`;
137
-
138
- // ─── Generate todo.md ─────────────────────────────────
139
- let todo = `# TODO: ${p.title}\n\n`;
140
- todo += `**Created:** ${new Date().toLocaleString()}\n`;
141
- todo += `**Tasks:** ${p.tasks.length}\n\n`;
142
-
143
- p.tasks.forEach((t, i) => {
144
- const agentTag = t.agent ? ` [${t.agent}]` : "";
145
- const prioTag = t.priority === "high" ? " 🔴" : t.priority === "low" ? " 🟢" : " 🟡";
146
- const depsTag = t.dependencies?.length ? ` (after #${t.dependencies.join(", #")})` : "";
147
-
148
- todo += `## Task ${i + 1}: ${t.title}${prioTag}${agentTag}${depsTag}\n\n`;
149
- todo += `- [ ] ${t.description}\n`;
150
-
151
- if (t.subtasks) {
152
- t.subtasks.forEach(st => {
153
- todo += ` - [ ] ${st}\n`;
154
- });
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 }] });
155
372
  }
156
- todo += "\n";
157
- });
373
+ };
158
374
 
159
- todo += `---\n\n## Progress\n\n`;
160
- todo += `- Total: ${p.tasks.length} tasks\n`;
161
- todo += `- High priority: ${p.tasks.filter(t => t.priority === "high").length}\n`;
162
- 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`;
375
+ notify(`📋 Plan created: **${p.title}** (${p.tasks.length} tasks)\nNow executing with sub-agents...`, "info");
164
376
 
165
- // Write files
166
- await writeFile(join(plansDir, specFile), spec, "utf-8");
167
- await writeFile(join(plansDir, todoFile), todo, "utf-8");
377
+ // Auto-execute all tasks
378
+ const { results, progressFile } = await executePlan(p.tasks, todoFile, notify);
168
379
 
169
- const summary = `**✅ Project plan created!**
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);
170
383
 
171
- 📋 **${p.title}**
384
+ const summary = `**🏁 Project "${p.title}" — Complete!**
172
385
 
173
- **Files:**
174
- - \`${specFile}\` — Full specification (goals, requirements, architecture, constraints)
175
- - \`${todoFile}\` — Actionable task list with priorities and agent assignments
386
+ **Plan files:** \`${specFile}\`, \`${todoFile}\`
387
+ **Progress:** \`${progressFile}\`
176
388
 
177
- **Summary:**
178
- - ${p.goals.length} goals
179
- - ${p.requirements.length} requirements
180
- - ${p.tasks.length} tasks (${p.tasks.filter(t => t.priority === "high").length} high priority)
181
- - Agents: ${[...new Set(p.tasks.map(t => t.agent || "code"))].join(", ")}
389
+ **Results:**
390
+ - ✅ Succeeded: ${succeeded}/${results.length}
391
+ - ❌ Failed: ${failed}
392
+ - ⏱️ Total time: ${(totalTime / 1000).toFixed(1)}s
182
393
 
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
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")}
188
399
 
189
- Files saved in \`.phi/plans/\``;
400
+ All files in \`.phi/plans/\``;
190
401
 
191
402
  return {
192
403
  content: [{ type: "text", text: summary }],
193
- 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
+ },
194
409
  };
195
410
  } catch (error) {
196
411
  return {
@@ -201,96 +416,162 @@ Files saved in \`.phi/plans/\``;
201
416
  },
202
417
  });
203
418
 
204
- // ─── /plan Command ───────────────────────────────────────────────
419
+ // ─── /plan Command — Full workflow ───────────────────────────────
205
420
 
206
421
  pi.registerCommand("plan", {
207
- description: "Create a structured project plan (the LLM analyzes your description, then creates spec + todo files)",
422
+ description: "Plan AND execute a project: creates spec + todo, then runs each task with isolated sub-agents",
208
423
  handler: async (args, ctx) => {
209
424
  const description = args.trim();
210
425
 
211
426
  if (!description) {
212
427
  ctx.ui.notify(`**Usage:** \`/plan <project description>\`
213
428
 
214
- **Examples:**
215
- /plan Build a REST API for user authentication with JWT tokens
216
- /plan Migrate the frontend from React to Next.js App Router
217
- /plan Add comprehensive test coverage to the payment module
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
218
435
 
219
- 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/
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
224
440
 
225
- 💡 The more detail you provide, the better the plan.`, "info");
441
+ **Other commands:**
442
+ /run — Re-execute an existing plan
443
+ /plans — List all plans and status`, "info");
226
444
  return;
227
445
  }
228
446
 
229
- // Send as user message to trigger the LLM to analyze and call orchestrate
230
447
  ctx.sendUserMessage(
231
- `Please analyze this project request and create a structured plan using the orchestrate tool.
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.
232
450
 
233
- Project description: ${description}
451
+ Project: ${description}
234
452
 
235
453
  Instructions:
236
- - Identify clear goals, requirements, and architecture decisions
237
- - Break the work into small, actionable tasks (each doable by one agent)
238
- - Assign the best agent type to each task: explore (analysis), plan (design), code (implementation), test (validation), review (quality)
239
- - 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`
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 ─────────────────────
465
+
466
+ pi.registerCommand("run", {
467
+ description: "Re-execute an existing plan's tasks with sub-agents",
468
+ handler: async (args, ctx) => {
469
+ if (!existsSync(plansDir)) {
470
+ ctx.ui.notify("No plans found. Use `/plan <description>` to create and execute one.", "warning");
471
+ return;
472
+ }
473
+
474
+ const files = (await readdir(plansDir)).sort().reverse();
475
+ const todoFiles = files.filter(f => f.startsWith("todo-") && f.endsWith(".md"));
476
+
477
+ if (todoFiles.length === 0) {
478
+ ctx.ui.notify("No todo files found. Use `/plan <description>` first.", "warning");
479
+ return;
480
+ }
481
+
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(/- \[ \] (.+)/);
493
+ const subtasks: string[] = [];
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
+ });
504
+ }
505
+ }
506
+
507
+ if (tasks.length === 0) {
508
+ ctx.ui.notify("Could not parse tasks from todo file.", "error");
509
+ return;
510
+ }
511
+
512
+ const confirmed = await ctx.ui.confirm(
513
+ "Re-execute Plan",
514
+ `${tasks.length} tasks found in \`${todoFile}\`.\nEach will spawn an isolated sub-agent.\n\nProceed?`
242
515
  );
516
+ if (!confirmed) {
517
+ ctx.ui.notify("Cancelled.", "info");
518
+ return;
519
+ }
520
+
521
+ await executePlan(tasks, todoFile, (msg, type) => ctx.ui.notify(msg, type));
243
522
  },
244
523
  });
245
524
 
246
525
  // ─── /plans Command ──────────────────────────────────────────────
247
526
 
248
527
  pi.registerCommand("plans", {
249
- description: "List existing project plans",
528
+ description: "List all project plans and their execution status",
250
529
  handler: async (_args, ctx) => {
251
- try {
252
- if (!existsSync(plansDir)) {
253
- ctx.ui.notify("No plans yet. Use `/plan <description>` to create one.", "info");
254
- return;
255
- }
256
-
257
- const files = await readdir(plansDir);
258
- 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
+ }
259
534
 
260
- if (specs.length === 0) {
261
- ctx.ui.notify("No plans found. Use `/plan <description>` to create one.", "info");
262
- return;
263
- }
535
+ const files = await readdir(plansDir);
536
+ const specs = files.filter(f => f.startsWith("spec-") && f.endsWith(".md")).sort().reverse();
264
537
 
265
- let output = `📁 **Project Plans** (${specs.length})\n\n`;
538
+ if (specs.length === 0) {
539
+ ctx.ui.notify("No plans found.", "info");
540
+ return;
541
+ }
266
542
 
267
- for (const specFile of specs) {
268
- const ts = specFile.replace("spec-", "").replace(".md", "");
269
- const todoFile = `todo-${ts}.md`;
543
+ let output = `📁 **Project Plans** (${specs.length})\n\n`;
270
544
 
271
- try {
272
- const content = await readFile(join(plansDir, specFile), "utf-8");
273
- const titleMatch = content.match(/^# (.+)$/m);
274
- const title = titleMatch ? titleMatch[1] : specFile;
275
- const taskCount = (content.match(/\| \d+ \|/g) || []).length;
276
- const date = ts.replace(/_/g, " ").substring(0, 10);
277
-
278
- const hasTodo = files.includes(todoFile);
279
-
280
- output += `📋 **${title}** (${date})\n`;
281
- output += ` Spec: \`${specFile}\`${hasTodo ? ` | Todo: \`${todoFile}\`` : ""}\n`;
282
- if (taskCount > 0) output += ` Tasks: ${taskCount}\n`;
283
- output += "\n";
284
- } catch {
285
- output += `📋 \`${specFile}\`\n\n`;
286
- }
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`;
287
570
  }
288
-
289
- output += `_Use \`read .phi/plans/<file>\` to view a plan._`;
290
- ctx.ui.notify(output, "info");
291
- } catch (error) {
292
- ctx.ui.notify(`Failed to list plans: ${error}`, "error");
293
571
  }
572
+
573
+ output += `_Commands: \`/plan\` (create+execute), \`/run\` (re-execute), \`read .phi/plans/<file>\` (view)_`;
574
+ ctx.ui.notify(output, "info");
294
575
  },
295
576
  });
296
577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.58.1",
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": {