@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.
- package/extensions/phi/orchestrator.ts +334 -373
- package/package.json +1 -1
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Orchestrator Extension -
|
|
2
|
+
* Orchestrator Extension - Full-cycle project planning and execution
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
22
|
-
import { join
|
|
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;
|
|
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
|
|
100
|
+
} catch { /* skip */ }
|
|
94
101
|
}
|
|
95
|
-
} catch { /* skip
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
144
|
+
task: TaskDef,
|
|
151
145
|
agentDefs: Map<string, AgentDef>,
|
|
152
146
|
cwd: string,
|
|
153
|
-
timeoutMs: number = 300000,
|
|
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
|
-
|
|
176
|
-
args.push("--
|
|
177
|
-
if (
|
|
178
|
-
|
|
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
|
-
|
|
174
|
+
execFile(cmd, cmdArgs, {
|
|
190
175
|
cwd,
|
|
191
176
|
timeout: timeoutMs,
|
|
192
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ───
|
|
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
|
|
226
|
-
promptSnippet: "
|
|
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:
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
"
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
251
|
-
successCriteria: Type.Optional(Type.Array(Type.String(), { description: "
|
|
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,
|
|
349
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
255
350
|
const p = params as {
|
|
256
|
-
title: string;
|
|
257
|
-
|
|
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
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
341
|
-
});
|
|
373
|
+
};
|
|
342
374
|
|
|
343
|
-
|
|
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
|
-
//
|
|
350
|
-
await
|
|
351
|
-
|
|
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 =
|
|
384
|
+
const summary = `**🏁 Project "${p.title}" — Complete!**
|
|
354
385
|
|
|
355
|
-
|
|
386
|
+
**Plan files:** \`${specFile}\`, \`${todoFile}\`
|
|
387
|
+
**Progress:** \`${progressFile}\`
|
|
356
388
|
|
|
357
|
-
**
|
|
358
|
-
-
|
|
359
|
-
-
|
|
389
|
+
**Results:**
|
|
390
|
+
- ✅ Succeeded: ${succeeded}/${results.length}
|
|
391
|
+
- ❌ Failed: ${failed}
|
|
392
|
+
- ⏱️ Total time: ${(totalTime / 1000).toFixed(1)}s
|
|
360
393
|
|
|
361
|
-
**
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
// ─── /
|
|
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: "
|
|
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
|
|
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];
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
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
|
-
"
|
|
440
|
-
|
|
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("
|
|
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.
|
|
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
|
|
528
|
+
description: "List all project plans and their execution status",
|
|
564
529
|
handler: async (_args, ctx) => {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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
|
-
|
|
538
|
+
if (specs.length === 0) {
|
|
539
|
+
ctx.ui.notify("No plans found.", "info");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
580
542
|
|
|
581
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
}
|