@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.
- package/extensions/phi/orchestrator.ts +470 -189
- package/package.json +1 -1
|
@@ -1,27 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Orchestrator Extension -
|
|
2
|
+
* Orchestrator Extension - Full-cycle project planning and execution
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
-
// ───
|
|
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
|
|
43
|
-
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.",
|
|
44
322
|
promptGuidelines: [
|
|
45
|
-
"When asked to plan a project:
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
68
|
-
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" })),
|
|
69
347
|
}),
|
|
70
348
|
|
|
71
|
-
async execute(_toolCallId, params, _signal, _onUpdate,
|
|
349
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
72
350
|
const p = params as {
|
|
73
|
-
title: string;
|
|
74
|
-
|
|
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
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
157
|
-
});
|
|
373
|
+
};
|
|
158
374
|
|
|
159
|
-
|
|
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
|
-
//
|
|
166
|
-
await
|
|
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
|
|
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
|
-
|
|
384
|
+
const summary = `**🏁 Project "${p.title}" — Complete!**
|
|
172
385
|
|
|
173
|
-
**
|
|
174
|
-
|
|
175
|
-
- \`${todoFile}\` — Actionable task list with priorities and agent assignments
|
|
386
|
+
**Plan files:** \`${specFile}\`, \`${todoFile}\`
|
|
387
|
+
**Progress:** \`${progressFile}\`
|
|
176
388
|
|
|
177
|
-
**
|
|
178
|
-
- ${
|
|
179
|
-
- ${
|
|
180
|
-
-
|
|
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
|
-
**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
400
|
+
All files in \`.phi/plans/\``;
|
|
190
401
|
|
|
191
402
|
return {
|
|
192
403
|
content: [{ type: "text", text: summary }],
|
|
193
|
-
details: {
|
|
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: "
|
|
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
|
-
**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
451
|
+
Project: ${description}
|
|
234
452
|
|
|
235
453
|
Instructions:
|
|
236
|
-
- Identify
|
|
237
|
-
- Break
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
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
|
|
528
|
+
description: "List all project plans and their execution status",
|
|
250
529
|
handler: async (_args, ctx) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
538
|
+
if (specs.length === 0) {
|
|
539
|
+
ctx.ui.notify("No plans found.", "info");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
266
542
|
|
|
267
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
}
|