@phi-code-admin/phi-code 0.85.0 ā 0.87.0
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/CHANGELOG.md +83 -0
- package/README.md +92 -123
- package/config/routing.example.json +129 -0
- package/config/routing.schema.json +58 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -6
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +9 -8
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts.map +1 -1
- package/dist/utils/pi-user-agent.js +4 -1
- package/dist/utils/pi-user-agent.js.map +1 -1
- package/docs/compaction.md +11 -11
- package/docs/custom-provider.md +4 -4
- package/docs/development.md +2 -2
- package/docs/extensions.md +47 -47
- package/docs/fork-policy.md +81 -0
- package/docs/index.md +7 -7
- package/docs/json.md +3 -3
- package/docs/keybindings.md +5 -5
- package/docs/models.md +6 -6
- package/docs/packages.md +37 -37
- package/docs/prompt-templates.md +4 -4
- package/docs/providers.md +9 -9
- package/docs/quickstart.md +25 -25
- package/docs/rpc.md +3 -3
- package/docs/sdk.md +34 -34
- package/docs/session-format.md +4 -4
- package/docs/sessions.md +11 -11
- package/docs/settings.md +9 -9
- package/docs/shell-aliases.md +2 -2
- package/docs/skills.md +9 -9
- package/docs/terminal-setup.md +7 -7
- package/docs/termux.md +6 -6
- package/docs/themes.md +9 -9
- package/docs/tmux.md +3 -3
- package/docs/tui.md +8 -8
- package/docs/usage.md +39 -39
- package/docs/windows.md +2 -2
- package/extensions/phi/agents.ts +12 -117
- package/extensions/phi/benchmark.ts +129 -49
- package/extensions/phi/browser.ts +10 -37
- package/extensions/phi/btw/btw.ts +1 -7
- package/extensions/phi/chrome/index.ts +1283 -741
- package/extensions/phi/commit.ts +9 -14
- package/extensions/phi/goal/index.ts +10 -33
- package/extensions/phi/init.ts +37 -47
- package/extensions/phi/keys.ts +2 -7
- package/extensions/phi/mcp/callback-server.ts +162 -165
- package/extensions/phi/mcp/config.ts +122 -136
- package/extensions/phi/mcp/errors.ts +18 -23
- package/extensions/phi/mcp/index.ts +322 -355
- package/extensions/phi/mcp/oauth-provider.ts +289 -289
- package/extensions/phi/mcp/server-manager.ts +390 -413
- package/extensions/phi/mcp/tool-bridge.ts +381 -415
- package/extensions/phi/memory.ts +2 -2
- package/extensions/phi/models.ts +27 -26
- package/extensions/phi/orchestrator.ts +343 -266
- package/extensions/phi/productivity.ts +4 -2
- package/extensions/phi/providers/agent-def.ts +128 -0
- package/extensions/phi/providers/alibaba.ts +56 -7
- package/extensions/phi/providers/context-window.ts +4 -1
- package/extensions/phi/providers/live-models.ts +5 -20
- package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
- package/extensions/phi/providers/phase-machine.ts +220 -0
- package/extensions/phi/setup.ts +196 -169
- package/extensions/phi/skill-loader.ts +18 -21
- package/extensions/phi/smart-router.ts +6 -6
- package/extensions/phi/web-search.ts +90 -50
- package/package.json +2 -1
|
@@ -16,19 +16,16 @@
|
|
|
16
16
|
* /plans ā List plans and their execution status
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { Type } from "@sinclair/typebox";
|
|
20
|
-
import type { ExtensionAPI } from "phi-code";
|
|
21
|
-
import { writeFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
22
|
-
import { join } from "node:path";
|
|
23
19
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
24
21
|
import { homedir } from "node:os";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
parsePhaseVerdict,
|
|
30
|
-
} from "./providers/orchestrator-helpers.js";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { Type } from "@sinclair/typebox";
|
|
24
|
+
import type { ExtensionAPI } from "phi-code";
|
|
25
|
+
import { type AgentDef, loadAgentDef } from "./providers/agent-def.js";
|
|
31
26
|
import { defaultExplorerSpecs, READONLY_EXPLORER_TOOLS, runExploreFanout } from "./providers/explore-fanout.js";
|
|
27
|
+
import { extractBlockingFindings, extractHandoff, parsePhaseVerdict } from "./providers/orchestrator-helpers.js";
|
|
28
|
+
import { analyzePhaseMessages, buildNextBrief, decidePhaseTransition } from "./providers/phase-machine.js";
|
|
32
29
|
|
|
33
30
|
// āāā Types āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
34
31
|
|
|
@@ -57,35 +54,51 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
57
54
|
// āāā Plan File Generators āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
58
55
|
|
|
59
56
|
function generateSpec(p: {
|
|
60
|
-
title: string;
|
|
61
|
-
|
|
57
|
+
title: string;
|
|
58
|
+
description: string;
|
|
59
|
+
goals: string[];
|
|
60
|
+
requirements: string[];
|
|
61
|
+
architecture?: string[];
|
|
62
|
+
constraints?: string[];
|
|
63
|
+
successCriteria?: string[];
|
|
64
|
+
tasks: TaskDef[];
|
|
62
65
|
}): string {
|
|
63
66
|
let spec = `# ${p.title}\n\n`;
|
|
64
67
|
spec += `**Created:** ${new Date().toLocaleString()}\n\n`;
|
|
65
68
|
spec += `## Description\n\n${p.description}\n\n`;
|
|
66
69
|
spec += `## Goals\n\n`;
|
|
67
|
-
p.goals.forEach((g, i) => {
|
|
70
|
+
p.goals.forEach((g, i) => {
|
|
71
|
+
spec += `${i + 1}. ${g}\n`;
|
|
72
|
+
});
|
|
68
73
|
spec += "\n## Requirements\n\n";
|
|
69
|
-
p.requirements.forEach(r => {
|
|
74
|
+
p.requirements.forEach((r) => {
|
|
75
|
+
spec += `- ${r}\n`;
|
|
76
|
+
});
|
|
70
77
|
spec += "\n";
|
|
71
78
|
if (p.architecture?.length) {
|
|
72
79
|
spec += `## Architecture\n\n`;
|
|
73
|
-
p.architecture.forEach(a => {
|
|
80
|
+
p.architecture.forEach((a) => {
|
|
81
|
+
spec += `- ${a}\n`;
|
|
82
|
+
});
|
|
74
83
|
spec += "\n";
|
|
75
84
|
}
|
|
76
85
|
if (p.constraints?.length) {
|
|
77
86
|
spec += `## Constraints\n\n`;
|
|
78
|
-
p.constraints.forEach(c => {
|
|
87
|
+
p.constraints.forEach((c) => {
|
|
88
|
+
spec += `- ${c}\n`;
|
|
89
|
+
});
|
|
79
90
|
spec += "\n";
|
|
80
91
|
}
|
|
81
92
|
if (p.successCriteria?.length) {
|
|
82
93
|
spec += `## Success Criteria\n\n`;
|
|
83
|
-
p.successCriteria.forEach(s => {
|
|
94
|
+
p.successCriteria.forEach((s) => {
|
|
95
|
+
spec += `- [ ] ${s}\n`;
|
|
96
|
+
});
|
|
84
97
|
spec += "\n";
|
|
85
98
|
}
|
|
86
99
|
spec += `## Task Overview\n\n| # | Task | Agent | Priority | Dependencies |\n|---|------|-------|----------|-------------|\n`;
|
|
87
100
|
p.tasks.forEach((t, i) => {
|
|
88
|
-
const deps = t.dependencies?.map(d => `#${d}`).join(", ") || "ā";
|
|
101
|
+
const deps = t.dependencies?.map((d) => `#${d}`).join(", ") || "ā";
|
|
89
102
|
spec += `| ${i + 1} | ${t.title} | ${t.agent || "code"} | ${t.priority || "medium"} | ${deps} |\n`;
|
|
90
103
|
});
|
|
91
104
|
spec += `\n---\n*Generated by Phi Code Orchestrator*\n`;
|
|
@@ -101,12 +114,15 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
101
114
|
const prioTag = t.priority === "high" ? " š“" : t.priority === "low" ? " š¢" : " š”";
|
|
102
115
|
const depsTag = t.dependencies?.length ? ` (after #${t.dependencies.join(", #")})` : "";
|
|
103
116
|
todo += `## Task ${i + 1}: ${t.title}${prioTag}${agentTag}${depsTag}\n\n- [ ] ${t.description}\n`;
|
|
104
|
-
if (t.subtasks)
|
|
117
|
+
if (t.subtasks)
|
|
118
|
+
t.subtasks.forEach((st) => {
|
|
119
|
+
todo += ` - [ ] ${st}\n`;
|
|
120
|
+
});
|
|
105
121
|
todo += "\n";
|
|
106
122
|
});
|
|
107
123
|
todo += `---\n\n## Progress\n\n- Total: ${tasks.length} tasks\n`;
|
|
108
|
-
todo += `- High priority: ${tasks.filter(t => t.priority === "high").length}\n`;
|
|
109
|
-
todo += `- Agents: ${[...new Set(tasks.map(t => t.agent || "code"))].join(", ")}\n`;
|
|
124
|
+
todo += `- High priority: ${tasks.filter((t) => t.priority === "high").length}\n`;
|
|
125
|
+
todo += `- Agents: ${[...new Set(tasks.map((t) => t.agent || "code"))].join(", ")}\n`;
|
|
110
126
|
return todo;
|
|
111
127
|
}
|
|
112
128
|
|
|
@@ -115,8 +131,10 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
115
131
|
pi.registerTool({
|
|
116
132
|
name: "orchestrate",
|
|
117
133
|
label: "Project Orchestrator",
|
|
118
|
-
description:
|
|
119
|
-
|
|
134
|
+
description:
|
|
135
|
+
"Create a structured project plan with spec, todo, and task breakdown. Forces the model to decompose the project into goals, requirements, architecture, constraints, and success criteria before execution.",
|
|
136
|
+
promptSnippet:
|
|
137
|
+
"Plan + structure projects using the prompt-architect pattern. Each task gets [CONTEXT] ā [TASK] ā [FORMAT] ā [CONSTRAINTS].",
|
|
120
138
|
promptGuidelines: [
|
|
121
139
|
// NOTE: The old guideline "When asked to plan or build a project, call the orchestrate tool"
|
|
122
140
|
// was REMOVED because it hijacked the /plan command flow. The orchestrate tool is now
|
|
@@ -129,30 +147,59 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
129
147
|
],
|
|
130
148
|
parameters: Type.Object({
|
|
131
149
|
title: Type.String({ description: "Concise project title" }),
|
|
132
|
-
description: Type.String({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
description: Type.String({
|
|
151
|
+
description: "Full project description: what to build, why, and any relevant context",
|
|
152
|
+
}),
|
|
153
|
+
goals: Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
154
|
+
description: "Measurable project goals (what success looks like)",
|
|
155
|
+
}),
|
|
156
|
+
requirements: Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
157
|
+
description: "Technical and functional requirements",
|
|
158
|
+
}),
|
|
159
|
+
architecture: Type.Optional(
|
|
160
|
+
Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
161
|
+
description: "Architecture decisions, tech stack choices, trade-offs",
|
|
162
|
+
}),
|
|
163
|
+
),
|
|
136
164
|
tasks: Type.Array(
|
|
137
165
|
Type.Object({
|
|
138
166
|
title: Type.String({ description: "Clear, action-oriented task title" }),
|
|
139
|
-
description: Type.String({
|
|
167
|
+
description: Type.String({
|
|
168
|
+
description: "SELF-CONTAINED task description. Include ALL context the sub-agent needs.",
|
|
169
|
+
}),
|
|
140
170
|
agent: Type.Optional(Type.String({ description: "Agent type: explore, plan, code, test, review" })),
|
|
141
171
|
priority: Type.Optional(Type.String({ description: "high, medium, low" })),
|
|
142
|
-
dependencies: Type.Optional(
|
|
143
|
-
|
|
172
|
+
dependencies: Type.Optional(
|
|
173
|
+
Type.Array(Type.Number(), { description: "Task numbers this depends on (1-indexed)" }),
|
|
174
|
+
),
|
|
175
|
+
subtasks: Type.Optional(
|
|
176
|
+
Type.Array(Type.String(), { description: "Specific sub-steps within this task" }),
|
|
177
|
+
),
|
|
178
|
+
}),
|
|
179
|
+
{ description: "Ordered list of tasks" },
|
|
180
|
+
),
|
|
181
|
+
constraints: Type.Optional(
|
|
182
|
+
Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
183
|
+
description: "Hard constraints: things to avoid, rules",
|
|
184
|
+
}),
|
|
185
|
+
),
|
|
186
|
+
successCriteria: Type.Optional(
|
|
187
|
+
Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
188
|
+
description: "How to verify the project is complete",
|
|
144
189
|
}),
|
|
145
|
-
{ description: "Ordered list of tasks" }
|
|
146
190
|
),
|
|
147
|
-
constraints: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Hard constraints: things to avoid, rules" })),
|
|
148
|
-
successCriteria: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "How to verify the project is complete" })),
|
|
149
191
|
}),
|
|
150
192
|
|
|
151
193
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
152
194
|
// Block if /plan orchestration is active ā prevent hijacking
|
|
153
195
|
if (orchestrationActive) {
|
|
154
196
|
return {
|
|
155
|
-
content: [
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: "ā ļø Orchestration is already active via /plan. Do NOT call orchestrate during /plan phases. Follow the phase instructions instead.",
|
|
201
|
+
},
|
|
202
|
+
],
|
|
156
203
|
details: undefined,
|
|
157
204
|
};
|
|
158
205
|
}
|
|
@@ -163,7 +210,11 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
163
210
|
const toArray = (v: any): string[] => {
|
|
164
211
|
if (!v) return [];
|
|
165
212
|
if (Array.isArray(v)) return v;
|
|
166
|
-
if (typeof v === "string")
|
|
213
|
+
if (typeof v === "string")
|
|
214
|
+
return v
|
|
215
|
+
.split("\n")
|
|
216
|
+
.map((s: string) => s.replace(/^[-ā¢*]\s*/, "").trim())
|
|
217
|
+
.filter(Boolean);
|
|
167
218
|
return [];
|
|
168
219
|
};
|
|
169
220
|
|
|
@@ -190,7 +241,8 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
190
241
|
await writeFile(join(plansDir, specFile), spec, "utf-8");
|
|
191
242
|
await writeFile(join(plansDir, todoFile), todo, "utf-8");
|
|
192
243
|
|
|
193
|
-
const summary =
|
|
244
|
+
const summary =
|
|
245
|
+
`š **Project "${p.title}" ā Structured plan created!**\n\n` +
|
|
194
246
|
`**Spec:** \`${specFile}\` | **Todo:** \`${todoFile}\`\n\n` +
|
|
195
247
|
`**Goals:** ${p.goals.length} | **Requirements:** ${p.requirements.length} | **Tasks:** ${p.tasks.length}\n\n` +
|
|
196
248
|
`The spec and todo files are ready in \`.phi/plans/\`. ` +
|
|
@@ -212,12 +264,6 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
212
264
|
|
|
213
265
|
// āāā Orchestration State āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
214
266
|
|
|
215
|
-
interface AgentDef {
|
|
216
|
-
name: string;
|
|
217
|
-
tools: string[];
|
|
218
|
-
systemPrompt: string;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
267
|
interface OrchestratorPhase {
|
|
222
268
|
key: string;
|
|
223
269
|
label: string;
|
|
@@ -239,7 +285,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
239
285
|
let phaseQueue: OrchestratorPhase[] = [];
|
|
240
286
|
let orchestrationActive = false;
|
|
241
287
|
let activeAgentPrompt: string | null = null;
|
|
242
|
-
let
|
|
288
|
+
let _activeAgentTools: string[] | null = null;
|
|
243
289
|
let savedTools: string[] | null = null;
|
|
244
290
|
let phasePending = false; // true while waiting for a phase to complete
|
|
245
291
|
let phaseTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -269,7 +315,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
269
315
|
try {
|
|
270
316
|
const f = join(process.cwd(), ".phi", "plans", `${key}-${ts}.md`);
|
|
271
317
|
if (existsSync(f)) return readFileSync(f, "utf-8");
|
|
272
|
-
} catch {
|
|
318
|
+
} catch {
|
|
319
|
+
/* ignore */
|
|
320
|
+
}
|
|
273
321
|
return null;
|
|
274
322
|
}
|
|
275
323
|
|
|
@@ -282,13 +330,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
282
330
|
}
|
|
283
331
|
function writeCheckpoint(nextBaseIndex: number): void {
|
|
284
332
|
try {
|
|
285
|
-
if (nextBaseIndex >= 5) {
|
|
333
|
+
if (nextBaseIndex >= 5) {
|
|
334
|
+
clearCheckpoint();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
286
337
|
writeFileSync(
|
|
287
338
|
checkpointPath(),
|
|
288
339
|
JSON.stringify({ description: currentDescription, ts: currentRunTs, nextBaseIndex }, null, 2),
|
|
289
340
|
"utf-8",
|
|
290
341
|
);
|
|
291
|
-
} catch {
|
|
342
|
+
} catch {
|
|
343
|
+
/* best effort */
|
|
344
|
+
}
|
|
292
345
|
}
|
|
293
346
|
function readCheckpoint(): { description: string; ts: string; nextBaseIndex: number } | null {
|
|
294
347
|
try {
|
|
@@ -298,14 +351,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
298
351
|
if (typeof c?.description === "string" && typeof c?.ts === "string" && typeof c?.nextBaseIndex === "number") {
|
|
299
352
|
return c;
|
|
300
353
|
}
|
|
301
|
-
} catch {
|
|
354
|
+
} catch {
|
|
355
|
+
/* ignore */
|
|
356
|
+
}
|
|
302
357
|
return null;
|
|
303
358
|
}
|
|
304
359
|
function clearCheckpoint(): void {
|
|
305
360
|
try {
|
|
306
361
|
const p = checkpointPath();
|
|
307
362
|
if (existsSync(p)) unlinkSync(p);
|
|
308
|
-
} catch {
|
|
363
|
+
} catch {
|
|
364
|
+
/* best effort */
|
|
365
|
+
}
|
|
309
366
|
}
|
|
310
367
|
|
|
311
368
|
/**
|
|
@@ -330,45 +387,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
330
387
|
\`\`\`
|
|
331
388
|
- **Internal orchestration data:** notes injected as "Previous phase summary" or budget/handoff reminders are for YOUR use only. Do not repeat them back to the user.`;
|
|
332
389
|
|
|
333
|
-
/**
|
|
334
|
-
* Parse agent .md file with YAML frontmatter
|
|
335
|
-
*/
|
|
336
|
-
function loadAgentDef(name: string): AgentDef | null {
|
|
337
|
-
const dirs = [
|
|
338
|
-
join(process.cwd(), ".phi", "agents"),
|
|
339
|
-
join(homedir(), ".phi", "agent", "agents"),
|
|
340
|
-
];
|
|
341
|
-
for (const dir of dirs) {
|
|
342
|
-
const filePath = join(dir, `${name}.md`);
|
|
343
|
-
try {
|
|
344
|
-
const content = readFileSync(filePath, "utf-8");
|
|
345
|
-
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
346
|
-
if (!fmMatch) continue;
|
|
347
|
-
const fields: Record<string, string> = {};
|
|
348
|
-
for (const line of fmMatch[1].split("\n")) {
|
|
349
|
-
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
350
|
-
if (m) fields[m[1]] = m[2].trim();
|
|
351
|
-
}
|
|
352
|
-
return {
|
|
353
|
-
name: fields.name || name,
|
|
354
|
-
tools: (fields.tools || "").split(",").map(t => t.trim()).filter(Boolean),
|
|
355
|
-
systemPrompt: fmMatch[2].trim(),
|
|
356
|
-
};
|
|
357
|
-
} catch { continue; }
|
|
358
|
-
}
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
390
|
/**
|
|
363
391
|
* Load a skill body (SKILL.md) so a phase instruction can embed it verbatim.
|
|
364
|
-
* Search order mirrors
|
|
365
|
-
*
|
|
366
|
-
*
|
|
392
|
+
* Search order mirrors agent-def.ts: project .phi/skills, then the global
|
|
393
|
+
* ~/.phi/agent/skills (postinstall copies bundled skills there), then the
|
|
394
|
+
* bundled <package>/skills for the repo layout. YAML frontmatter is
|
|
395
|
+
* stripped. Returns null when the skill is not installed.
|
|
367
396
|
*/
|
|
368
397
|
function loadSkillContent(name: string): string | null {
|
|
369
398
|
const dirs = [
|
|
370
399
|
join(process.cwd(), ".phi", "skills"),
|
|
371
400
|
join(homedir(), ".phi", "agent", "skills"),
|
|
401
|
+
join(__dirname, "..", "..", "skills"),
|
|
372
402
|
];
|
|
373
403
|
for (const dir of dirs) {
|
|
374
404
|
try {
|
|
@@ -376,7 +406,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
376
406
|
const fmMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
|
|
377
407
|
const body = (fmMatch ? fmMatch[1] : content).trim();
|
|
378
408
|
if (body) return body;
|
|
379
|
-
} catch {
|
|
409
|
+
} catch {
|
|
410
|
+
/* try next dir */
|
|
411
|
+
}
|
|
380
412
|
}
|
|
381
413
|
return null;
|
|
382
414
|
}
|
|
@@ -390,7 +422,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
390
422
|
let routing: any = { routes: {}, default: { model: "default" } };
|
|
391
423
|
try {
|
|
392
424
|
routing = JSON.parse(readFileSync(routingPath, "utf-8"));
|
|
393
|
-
} catch {
|
|
425
|
+
} catch {
|
|
426
|
+
/* no routing config */
|
|
427
|
+
}
|
|
394
428
|
|
|
395
429
|
function getModel(routeKey: string): { preferred: string; fallback: string } {
|
|
396
430
|
const route = routing.routes?.[routeKey];
|
|
@@ -409,9 +443,10 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
409
443
|
const ts = tsOverride || timestamp();
|
|
410
444
|
currentRunTs = ts; // shared by all phase report file names this run (or resumed)
|
|
411
445
|
// Inject runtime info so agents can adapt to the host OS
|
|
412
|
-
const shellNote =
|
|
413
|
-
|
|
414
|
-
|
|
446
|
+
const shellNote =
|
|
447
|
+
process.platform === "win32"
|
|
448
|
+
? `\nShell: bash (Git Bash), NOT cmd.exe. Always use Unix syntax: rm not del, test -f not if exist, / not \\\\`
|
|
449
|
+
: "";
|
|
415
450
|
const runtimeInfo = `\n\nRuntime: ${process.platform} (${process.arch})${shellNote}`;
|
|
416
451
|
|
|
417
452
|
// Embed the prompt-architect skill so the PLAN phase applies it to every
|
|
@@ -425,9 +460,13 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
425
460
|
|
|
426
461
|
const phases: OrchestratorPhase[] = [
|
|
427
462
|
{
|
|
428
|
-
key: "explore",
|
|
463
|
+
key: "explore",
|
|
464
|
+
label: "š Phase 1 ā EXPLORE",
|
|
465
|
+
model: explore.preferred,
|
|
466
|
+
fallback: explore.fallback,
|
|
429
467
|
agent: loadAgentDef("explore"),
|
|
430
|
-
instruction:
|
|
468
|
+
instruction:
|
|
469
|
+
`You are the EXPLORE agent. Analyze the project requirements and existing codebase.
|
|
431
470
|
|
|
432
471
|
**FIRST ACTION (MANDATORY):** Call \`memory_search\` with project-relevant keywords to check for prior context.
|
|
433
472
|
|
|
@@ -480,12 +519,18 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
|
|
|
480
519
|
- Every function fully implemented
|
|
481
520
|
- Follow existing patterns if codebase exists
|
|
482
521
|
- [Any other specific constraints]
|
|
483
|
-
\`\`\`` +
|
|
522
|
+
\`\`\`` +
|
|
523
|
+
runtimeInfo +
|
|
524
|
+
COMMON_PHASE_RULES,
|
|
484
525
|
},
|
|
485
526
|
{
|
|
486
|
-
key: "plan",
|
|
527
|
+
key: "plan",
|
|
528
|
+
label: "š Phase 2 ā PLAN",
|
|
529
|
+
model: plan.preferred,
|
|
530
|
+
fallback: plan.fallback,
|
|
487
531
|
agent: loadAgentDef("plan"),
|
|
488
|
-
instruction:
|
|
532
|
+
instruction:
|
|
533
|
+
`You are the PLAN agent. Design the architecture and create a detailed task list.
|
|
489
534
|
|
|
490
535
|
**Context Retrieval:**
|
|
491
536
|
1. Use \`ontology_query\` to retrieve all entities and relations from Phase 1
|
|
@@ -528,12 +573,18 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
|
|
|
528
573
|
- Dependencies: Task 1
|
|
529
574
|
\`\`\`
|
|
530
575
|
${promptArchitectSection}
|
|
531
|
-
Before finishing, use \`memory_write\` to save your plan summary with relevant tags for future reference.` +
|
|
576
|
+
Before finishing, use \`memory_write\` to save your plan summary with relevant tags for future reference.` +
|
|
577
|
+
runtimeInfo +
|
|
578
|
+
COMMON_PHASE_RULES,
|
|
532
579
|
},
|
|
533
580
|
{
|
|
534
|
-
key: "code",
|
|
581
|
+
key: "code",
|
|
582
|
+
label: "š» Phase 3 ā CODE",
|
|
583
|
+
model: code.preferred,
|
|
584
|
+
fallback: code.fallback,
|
|
535
585
|
agent: loadAgentDef("code"),
|
|
536
|
-
instruction:
|
|
586
|
+
instruction:
|
|
587
|
+
`You are the CODE agent. Implement the complete project.
|
|
537
588
|
|
|
538
589
|
**Context Retrieval:**
|
|
539
590
|
1. Use \`memory_search\` with project keywords to find notes from previous phases
|
|
@@ -577,12 +628,18 @@ After implementation, use \`memory_write\` to save a summary of what was built,
|
|
|
577
628
|
**CRITICAL RULES:**
|
|
578
629
|
- Write ONE file per tool call ā NEVER combine multiple files in a single response
|
|
579
630
|
- Keep each file under 500 lines. If longer, split into modules
|
|
580
|
-
- After writing ALL files, verify they exist with a single \`find . -name '*.ts' -o -name '*.js' -o -name '*.html' | sort\`` +
|
|
631
|
+
- After writing ALL files, verify they exist with a single \`find . -name '*.ts' -o -name '*.js' -o -name '*.html' | sort\`` +
|
|
632
|
+
runtimeInfo +
|
|
633
|
+
COMMON_PHASE_RULES,
|
|
581
634
|
},
|
|
582
635
|
{
|
|
583
|
-
key: "test",
|
|
636
|
+
key: "test",
|
|
637
|
+
label: "š§Ŗ Phase 4 ā TEST",
|
|
638
|
+
model: test.preferred,
|
|
639
|
+
fallback: test.fallback,
|
|
584
640
|
agent: loadAgentDef("test"),
|
|
585
|
-
instruction:
|
|
641
|
+
instruction:
|
|
642
|
+
`You are the TEST agent. Verify the implementation.
|
|
586
643
|
|
|
587
644
|
**Context Retrieval:**
|
|
588
645
|
1. Use \`memory_search\` to find implementation notes from the CODE phase
|
|
@@ -641,12 +698,18 @@ $ <command actually executed>
|
|
|
641
698
|
|
|
642
699
|
After testing, use \`memory_write\` to save test results, bugs found, and lessons learned.
|
|
643
700
|
|
|
644
|
-
**Ontology update:** Use \`ontology_add\` to update the project status (e.g., entity "test-results" type "Phase" with properties {passed: "N", failed: "M", coverage: "X%"}) and add a relation to the project entity.` +
|
|
701
|
+
**Ontology update:** Use \`ontology_add\` to update the project status (e.g., entity "test-results" type "Phase" with properties {passed: "N", failed: "M", coverage: "X%"}) and add a relation to the project entity.` +
|
|
702
|
+
runtimeInfo +
|
|
703
|
+
COMMON_PHASE_RULES,
|
|
645
704
|
},
|
|
646
705
|
{
|
|
647
|
-
key: "review",
|
|
706
|
+
key: "review",
|
|
707
|
+
label: "š Phase 5 ā REVIEW",
|
|
708
|
+
model: review.preferred,
|
|
709
|
+
fallback: review.fallback,
|
|
648
710
|
agent: loadAgentDef("review"),
|
|
649
|
-
instruction:
|
|
711
|
+
instruction:
|
|
712
|
+
`You are the REVIEW agent. Final quality review.
|
|
650
713
|
|
|
651
714
|
**FIRST ACTIONS (MANDATORY ā do these before anything else):**
|
|
652
715
|
1. Call \`memory_search\` to find all notes from previous phases (explore, plan, code, test)
|
|
@@ -698,10 +761,14 @@ Tag the note with relevant keywords for vector search.
|
|
|
698
761
|
**Ontology enrichment:** After your review, use \`ontology_add\` to save your key findings:
|
|
699
762
|
- Add a "review-report" entity with type "Document"
|
|
700
763
|
- Add relations to the project: "reviews" ā project, quality score as entity property
|
|
701
|
-
- Save any new architectural decisions or patterns discovered` +
|
|
764
|
+
- Save any new architectural decisions or patterns discovered` +
|
|
765
|
+
runtimeInfo +
|
|
766
|
+
COMMON_PHASE_RULES,
|
|
702
767
|
},
|
|
703
768
|
];
|
|
704
|
-
phases.forEach((p, i) => {
|
|
769
|
+
phases.forEach((p, i) => {
|
|
770
|
+
p.baseIndex = i;
|
|
771
|
+
});
|
|
705
772
|
return phases;
|
|
706
773
|
}
|
|
707
774
|
|
|
@@ -765,7 +832,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
765
832
|
/**
|
|
766
833
|
* Activate agent for a phase: set system prompt + restrict tools.
|
|
767
834
|
*/
|
|
768
|
-
function activateAgent(phase: OrchestratorPhase,
|
|
835
|
+
function activateAgent(phase: OrchestratorPhase, _ctx: any) {
|
|
769
836
|
// Fallback capture: savedTools is normally taken in the /plan handler at
|
|
770
837
|
// orchestration start, but capture it here too so restoration is always defined.
|
|
771
838
|
if (!savedTools) {
|
|
@@ -778,21 +845,21 @@ Tag the note with relevant keywords for vector search.
|
|
|
778
845
|
// Restrict tools to agent's allowed tools
|
|
779
846
|
if (agentDef.tools.length > 0) {
|
|
780
847
|
// Always include memory tools in orchestration phases
|
|
781
|
-
const memoryTools = [
|
|
782
|
-
const agentTools = [...agentDef.tools, ...memoryTools.filter(t => !agentDef.tools.includes(t))];
|
|
783
|
-
|
|
848
|
+
const memoryTools = ["memory_search", "memory_write", "memory_read", "ontology_add", "ontology_query"];
|
|
849
|
+
const agentTools = [...agentDef.tools, ...memoryTools.filter((t) => !agentDef.tools.includes(t))];
|
|
850
|
+
_activeAgentTools = agentTools;
|
|
784
851
|
pi.setActiveTools(agentTools);
|
|
785
852
|
} else if (savedTools) {
|
|
786
853
|
// Agent with no tool restriction: reset to the full toolset so the
|
|
787
854
|
// previous phase's narrower restriction does not leak into this one.
|
|
788
|
-
|
|
855
|
+
_activeAgentTools = null;
|
|
789
856
|
pi.setActiveTools(savedTools);
|
|
790
857
|
}
|
|
791
858
|
} else {
|
|
792
859
|
// No agent for this phase: restore the full toolset, otherwise the prior
|
|
793
860
|
// phase's restriction would persist.
|
|
794
861
|
activeAgentPrompt = null;
|
|
795
|
-
|
|
862
|
+
_activeAgentTools = null;
|
|
796
863
|
if (savedTools) {
|
|
797
864
|
pi.setActiveTools(savedTools);
|
|
798
865
|
}
|
|
@@ -804,7 +871,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
804
871
|
*/
|
|
805
872
|
function deactivateAgent() {
|
|
806
873
|
activeAgentPrompt = null;
|
|
807
|
-
|
|
874
|
+
_activeAgentTools = null;
|
|
808
875
|
if (savedTools) {
|
|
809
876
|
pi.setActiveTools(savedTools);
|
|
810
877
|
savedTools = null;
|
|
@@ -828,11 +895,16 @@ Tag the note with relevant keywords for vector search.
|
|
|
828
895
|
setOrchestrationActive(false);
|
|
829
896
|
phasePending = false;
|
|
830
897
|
deactivateAgent();
|
|
831
|
-
if (phaseTimeoutId) {
|
|
898
|
+
if (phaseTimeoutId) {
|
|
899
|
+
clearTimeout(phaseTimeoutId);
|
|
900
|
+
phaseTimeoutId = null;
|
|
901
|
+
}
|
|
832
902
|
if (originalModel) {
|
|
833
903
|
const toRestore = originalModel;
|
|
834
904
|
originalModel = null;
|
|
835
|
-
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
905
|
+
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
906
|
+
/* best effort */
|
|
907
|
+
});
|
|
836
908
|
}
|
|
837
909
|
}
|
|
838
910
|
|
|
@@ -847,18 +919,26 @@ Tag the note with relevant keywords for vector search.
|
|
|
847
919
|
if (originalModel) {
|
|
848
920
|
const toRestore = originalModel;
|
|
849
921
|
originalModel = null;
|
|
850
|
-
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
922
|
+
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
923
|
+
/* best effort */
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
if (phaseTimeoutId) {
|
|
927
|
+
clearTimeout(phaseTimeoutId);
|
|
928
|
+
phaseTimeoutId = null;
|
|
851
929
|
}
|
|
852
|
-
if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
|
|
853
930
|
// Generate global final summary using the real phase counters
|
|
854
931
|
const elapsed = phaseStartTime ? Math.round((Date.now() - phaseStartTime) / 1000) : 0;
|
|
855
932
|
const minutes = Math.floor(elapsed / 60);
|
|
856
933
|
const seconds = elapsed % 60;
|
|
857
934
|
const skippedNote = skippedPhases ? ` (${skippedPhases} skipped on timeout)` : "";
|
|
858
|
-
ctx.ui.notify(
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
935
|
+
ctx.ui.notify(
|
|
936
|
+
`\nš **Orchestration Summary**\n` +
|
|
937
|
+
` Phases: ${completedPhases}/5 completed${skippedNote}\n` +
|
|
938
|
+
` Duration: ${minutes}m ${seconds}s\n` +
|
|
939
|
+
` Check \`.phi/plans/\` for all reports`,
|
|
940
|
+
"info",
|
|
941
|
+
);
|
|
862
942
|
const allDone = completedPhases >= 5 && skippedPhases === 0;
|
|
863
943
|
const doneMsg = allDone
|
|
864
944
|
? `\nā
**All 5 phases complete!**`
|
|
@@ -867,7 +947,11 @@ Tag the note with relevant keywords for vector search.
|
|
|
867
947
|
ctx.ui.notify(doneMsg, "info");
|
|
868
948
|
} catch {
|
|
869
949
|
// Fallback: inject completion message into conversation stream
|
|
870
|
-
try {
|
|
950
|
+
try {
|
|
951
|
+
pi.sendUserMessage(`Orchestration finished: ${completedPhases}/5 phases completed${skippedNote}.`);
|
|
952
|
+
} catch {
|
|
953
|
+
/* best effort */
|
|
954
|
+
}
|
|
871
955
|
}
|
|
872
956
|
return;
|
|
873
957
|
}
|
|
@@ -894,7 +978,11 @@ Tag the note with relevant keywords for vector search.
|
|
|
894
978
|
// race a still-streaming phase. Mark it internal so the resulting
|
|
895
979
|
// agent_end is not treated as a user cancellation.
|
|
896
980
|
internalAbort = true;
|
|
897
|
-
try {
|
|
981
|
+
try {
|
|
982
|
+
ctx.abort();
|
|
983
|
+
} catch {
|
|
984
|
+
/* best effort */
|
|
985
|
+
}
|
|
898
986
|
// Retry the SAME phase once on the fallback model before skipping:
|
|
899
987
|
// a timed-out phase usually means the model/route stalled, and a
|
|
900
988
|
// different family often gets through (cap = one retry per phase).
|
|
@@ -902,7 +990,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
902
990
|
phase.retried = true;
|
|
903
991
|
phase.useFallback = true;
|
|
904
992
|
phaseQueue.unshift(phase);
|
|
905
|
-
ctx.ui.notify(
|
|
993
|
+
ctx.ui.notify(
|
|
994
|
+
`\nā° **Phase timed out** (${MAX_PHASE_DURATION_MS / 60000} min) ā retrying once on the fallback model.`,
|
|
995
|
+
"warning",
|
|
996
|
+
);
|
|
906
997
|
} else {
|
|
907
998
|
skippedPhases++;
|
|
908
999
|
ctx.ui.notify(`\nā° **Phase timed out again** ā skipping to next phase.`, "warning");
|
|
@@ -915,9 +1006,9 @@ Tag the note with relevant keywords for vector search.
|
|
|
915
1006
|
|
|
916
1007
|
// āāā System Prompt Injection ā Agent personas āāāāāāāāāāāāāāāāāāāā
|
|
917
1008
|
|
|
918
|
-
pi.on("before_agent_start", async (
|
|
1009
|
+
pi.on("before_agent_start", async (_event, _ctx) => {
|
|
919
1010
|
if (!orchestrationActive || !activeAgentPrompt) {
|
|
920
|
-
return {
|
|
1011
|
+
return {};
|
|
921
1012
|
}
|
|
922
1013
|
// Replace system prompt with the active agent's prompt
|
|
923
1014
|
return { systemPrompt: activeAgentPrompt };
|
|
@@ -937,15 +1028,21 @@ Tag the note with relevant keywords for vector search.
|
|
|
937
1028
|
// An internal abort (phase timeout) re-emits agent_end; that transition was
|
|
938
1029
|
// already handled by the timeout. Consume the flag and ignore this event so
|
|
939
1030
|
// it is not mistaken for a user cancellation below.
|
|
940
|
-
if (internalAbort) {
|
|
1031
|
+
if (internalAbort) {
|
|
1032
|
+
internalAbort = false;
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const messages = event.messages || [];
|
|
1037
|
+
// Analyze what happened, read the phase's report, and let the pure state
|
|
1038
|
+
// machine (providers/phase-machine.ts, unit tested) decide the transition.
|
|
1039
|
+
// This hook only interprets the decision: notifications, queue edits,
|
|
1040
|
+
// checkpoints, model switches.
|
|
1041
|
+
const analysis = analyzePhaseMessages(messages);
|
|
941
1042
|
|
|
942
1043
|
// User abort (Ctrl+C / ESC): the aborted run still emits agent_end, but it
|
|
943
|
-
// must NOT be treated as phase completion.
|
|
944
|
-
|
|
945
|
-
const userAborted = (event.messages || []).some(
|
|
946
|
-
(m: any) => m.role === 'assistant' && m.stopReason === 'aborted',
|
|
947
|
-
);
|
|
948
|
-
if (userAborted) {
|
|
1044
|
+
// must NOT be treated as phase completion.
|
|
1045
|
+
if (analysis.userAborted) {
|
|
949
1046
|
ctx.ui.notify(`\nš **Orchestration cancelled** by user. Remaining phases skipped.`, "warning");
|
|
950
1047
|
stopOrchestration();
|
|
951
1048
|
return;
|
|
@@ -961,163 +1058,119 @@ Tag the note with relevant keywords for vector search.
|
|
|
961
1058
|
}
|
|
962
1059
|
|
|
963
1060
|
// Clear phase timeout on normal completion
|
|
964
|
-
if (phaseTimeoutId) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
// Instead of raw LLM text, extract concrete actions: files created/modified,
|
|
968
|
-
// errors encountered, test results. This gives the next phase actionable context.
|
|
969
|
-
const messages = event.messages || [];
|
|
970
|
-
const filesWritten: string[] = [];
|
|
971
|
-
const filesEdited: string[] = [];
|
|
972
|
-
const errorsHit: string[] = [];
|
|
973
|
-
const testResults: string[] = [];
|
|
974
|
-
let toolCallCount = 0;
|
|
975
|
-
|
|
976
|
-
for (const rawMsg of messages) {
|
|
977
|
-
// Pi message variants are scanned dynamically; cast to a loose shape once.
|
|
978
|
-
const msg = rawMsg as { role?: string; content?: unknown; name?: string; toolName?: string; isError?: boolean };
|
|
979
|
-
// Pi uses role: "toolResult" instead of "tool"
|
|
980
|
-
if (msg.role === 'tool' || msg.role === 'function' || msg.role === 'toolResult') {
|
|
981
|
-
toolCallCount++;
|
|
982
|
-
const content = Array.isArray(msg.content)
|
|
983
|
-
? msg.content.map((c: any) => c.text || '').join('')
|
|
984
|
-
: String(msg.content || '');
|
|
985
|
-
const name = msg.name || msg.toolName || '';
|
|
986
|
-
// Track writes
|
|
987
|
-
if (name === 'write' && content.includes('Successfully wrote')) {
|
|
988
|
-
const match = content.match(/wrote \d+ bytes to (.+)/);
|
|
989
|
-
if (match) filesWritten.push(match[1]);
|
|
990
|
-
}
|
|
991
|
-
// Track edits ā the edit tool returns "Successfully replaced N block(s) in <path>."
|
|
992
|
-
// Anchor the path capture so it does not over-capture unrelated text.
|
|
993
|
-
if (name === 'edit' && !content.includes('ERR') && !msg.isError) {
|
|
994
|
-
const match = content.match(/replaced \d+ block\(s\) in (.+?)\.?$/m);
|
|
995
|
-
if (match) filesEdited.push(match[1]);
|
|
996
|
-
}
|
|
997
|
-
// Track errors ā but filter out edit retries (old_text mismatch = normal retry, not error)
|
|
998
|
-
if ((content.includes('ERR:') || content.includes('Error:') || content.includes('FAIL'))
|
|
999
|
-
&& !content.includes('old text must match')
|
|
1000
|
-
&& !content.includes('The old text')
|
|
1001
|
-
&& !content.includes('oldText not found')
|
|
1002
|
-
&& !content.includes('old_text not found')) {
|
|
1003
|
-
const preview = content.slice(0, 150).replace(/\n/g, ' ');
|
|
1004
|
-
errorsHit.push(`${name}: ${preview}`);
|
|
1005
|
-
}
|
|
1006
|
-
// Track test results
|
|
1007
|
-
if (content.includes('PASS') || content.includes('ā
') || content.includes('ā') || content.includes('ā')) {
|
|
1008
|
-
const lines = content.split('\n').filter((l: string) => /PASS|FAIL|ā
|ā|ā/.test(l));
|
|
1009
|
-
testResults.push(...lines.slice(0, 10));
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1061
|
+
if (phaseTimeoutId) {
|
|
1062
|
+
clearTimeout(phaseTimeoutId);
|
|
1063
|
+
phaseTimeoutId = null;
|
|
1012
1064
|
}
|
|
1013
1065
|
|
|
1014
|
-
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1066
|
+
const reportContent = currentPhase ? readPhaseReport(currentPhase.key, currentRunTs) : null;
|
|
1067
|
+
const verdict = reportContent ? parsePhaseVerdict(reportContent) : null;
|
|
1068
|
+
const decision = decidePhaseTransition({
|
|
1069
|
+
analysis,
|
|
1070
|
+
phase: currentPhase ? { key: currentPhase.key, retried: currentPhase.retried } : null,
|
|
1071
|
+
verdict,
|
|
1072
|
+
hasReport: reportContent !== null,
|
|
1073
|
+
reviewFixRounds,
|
|
1074
|
+
maxToolCallsPerPhase: MAX_TOOL_CALLS_PER_PHASE,
|
|
1018
1075
|
});
|
|
1019
|
-
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1076
|
+
|
|
1077
|
+
if (decision.action === "stop") {
|
|
1078
|
+
// Only a genuine auth failure (401) is fatal ā abort the whole workflow.
|
|
1079
|
+
ctx.ui.notify(
|
|
1080
|
+
`\nā **Orchestrator aborted:** API authentication error (401)\nCheck your API key and model configuration.`,
|
|
1081
|
+
"error",
|
|
1082
|
+
);
|
|
1022
1083
|
stopOrchestration();
|
|
1023
1084
|
return;
|
|
1024
1085
|
}
|
|
1025
1086
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
|
|
1087
|
+
if (decision.action === "retry-fallback" && currentPhase) {
|
|
1088
|
+
// Transient provider/proxy failure (timeout text / 5xx / 429 / broken
|
|
1089
|
+
// JSON tool call) that is NOT a 401: retry the SAME phase once on the
|
|
1090
|
+
// fallback model (a different family often gets through).
|
|
1031
1091
|
currentPhase.retried = true;
|
|
1032
1092
|
currentPhase.useFallback = true;
|
|
1033
1093
|
phaseQueue.unshift(currentPhase);
|
|
1034
1094
|
phasePending = false;
|
|
1035
|
-
ctx.ui.notify(
|
|
1095
|
+
ctx.ui.notify(
|
|
1096
|
+
`\nš **Transient provider error** in ${currentPhase.label} ā retrying once on the fallback model.`,
|
|
1097
|
+
"warning",
|
|
1098
|
+
);
|
|
1036
1099
|
sendNextPhase(ctx);
|
|
1037
1100
|
return;
|
|
1038
1101
|
}
|
|
1039
1102
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1103
|
+
if (decision.action === "pause-blocked" && currentPhase) {
|
|
1104
|
+
ctx.ui.notify(
|
|
1105
|
+
`\nāøļø **${currentPhase.label} reported BLOCKED.** Pausing /plan ā see \`.phi/plans/${currentPhase.key}-${currentRunTs}.md\`. Fix the blocker and re-run \`/plan\` to continue.`,
|
|
1106
|
+
"warning",
|
|
1107
|
+
);
|
|
1108
|
+
stopOrchestration();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (decision.action === "continue" && decision.zeroToolCalls && messages.length > 0) {
|
|
1113
|
+
// A phase that made 0 tool calls is NOT fatal: a model may legitimately
|
|
1114
|
+
// answer with text only (e.g. an inline plan). Warn and continue rather
|
|
1115
|
+
// than killing phases 2-5, mirroring the graceful phase-timeout path.
|
|
1116
|
+
ctx.ui.notify(
|
|
1117
|
+
`\nā ļø **Phase made 0 tool calls** ā it may have responded with text only. Continuing to the next phase.`,
|
|
1118
|
+
"warning",
|
|
1119
|
+
);
|
|
1045
1120
|
if (phaseQueue.length > 0) {
|
|
1046
1121
|
phaseQueue[0].instruction += `\n\n**Note:** The previous phase made 0 tool calls and may have responded with text only. Make sure you actually call the required tools.`;
|
|
1047
1122
|
}
|
|
1048
1123
|
}
|
|
1049
1124
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1125
|
+
if (decision.action === "continue" && decision.missingVerdict && currentPhase) {
|
|
1126
|
+
// The model deviated from the text contract: this phase must start its
|
|
1127
|
+
// report with "VERDICT: ..." and nothing parseable was found. Surface it
|
|
1128
|
+
// instead of silently treating the phase as passed.
|
|
1129
|
+
ctx.ui.notify(
|
|
1130
|
+
`\nā ļø **${currentPhase.label} wrote no parseable VERDICT line** in \`.phi/plans/${currentPhase.key}-${currentRunTs}.md\` ā treating it as passed. Check the report manually.`,
|
|
1131
|
+
"warning",
|
|
1132
|
+
);
|
|
1056
1133
|
}
|
|
1057
|
-
if (filesWritten.length > 0) summaryParts.push(`Files created/written: ${filesWritten.join(', ')}`);
|
|
1058
|
-
if (filesEdited.length > 0) summaryParts.push(`Files edited: ${filesEdited.join(', ')}`);
|
|
1059
|
-
if (testResults.length > 0) summaryParts.push(`Test results:\n${testResults.join('\n')}`);
|
|
1060
|
-
if (errorsHit.length > 0) summaryParts.push(`Errors encountered: ${errorsHit.length}\n${errorsHit.slice(0, 5).join('\n')}`);
|
|
1061
|
-
|
|
1062
|
-
// Verify mandatory tool usage
|
|
1063
|
-
const toolNames = messages
|
|
1064
|
-
.filter((m: any) => m.role === 'tool' || m.role === 'function' || m.role === 'toolResult')
|
|
1065
|
-
.map((m: any) => (m as any).name || (m as any).toolName || '');
|
|
1066
|
-
const hasMemorySearch = toolNames.includes('memory_search');
|
|
1067
|
-
const hasMemoryWrite = toolNames.includes('memory_write');
|
|
1068
|
-
if (!hasMemorySearch) summaryParts.push(`ā ļø Phase did NOT call memory_search (mandatory)`);
|
|
1069
|
-
if (!hasMemoryWrite) summaryParts.push(`ā ļø Phase did NOT call memory_write (mandatory)`);
|
|
1070
|
-
|
|
1071
|
-
const phaseSummary = summaryParts.join('\n');
|
|
1072
1134
|
|
|
1073
1135
|
// Prefer the phase's canonical "## HANDOFF" block (a deterministic text
|
|
1074
1136
|
// contract written to its report file) over the heuristic summary; fall back
|
|
1075
1137
|
// to the heuristic when the model did not write one.
|
|
1076
|
-
const reportContent = currentPhase ? readPhaseReport(currentPhase.key, currentRunTs) : null;
|
|
1077
1138
|
const handoff = reportContent ? extractHandoff(reportContent) : "";
|
|
1078
|
-
const nextBrief =
|
|
1079
|
-
|
|
1080
|
-
|
|
1139
|
+
const nextBrief = buildNextBrief(
|
|
1140
|
+
analysis,
|
|
1141
|
+
handoff,
|
|
1142
|
+
currentPhase?.label || "previous phase",
|
|
1143
|
+
MAX_TOOL_CALLS_PER_PHASE,
|
|
1144
|
+
);
|
|
1081
1145
|
if (nextBrief && phaseQueue.length > 0) {
|
|
1082
1146
|
phaseQueue[0].instruction += `\n\n**Previous phase summary:**\n${nextBrief}`;
|
|
1083
1147
|
}
|
|
1084
1148
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
...currentPhase,
|
|
1111
|
-
retried: false,
|
|
1112
|
-
useFallback: false,
|
|
1113
|
-
label: "š Re-REVIEW (after fix)",
|
|
1114
|
-
};
|
|
1115
|
-
// Run next: fix, then re-review.
|
|
1116
|
-
phaseQueue.unshift(reReviewPhase);
|
|
1117
|
-
phaseQueue.unshift(fixPhase);
|
|
1118
|
-
ctx.ui.notify(`\nš **REVIEW verdict: FAIL** ā running one targeted fix + re-review cycle.`, "warning");
|
|
1119
|
-
}
|
|
1120
|
-
} catch { /* verdict logic is best-effort */ }
|
|
1149
|
+
if (decision.action === "review-fix-cycle" && currentPhase) {
|
|
1150
|
+
// REVIEW FAIL: open ONE bounded fix -> re-review cycle.
|
|
1151
|
+
reviewFixRounds++;
|
|
1152
|
+
const blocking = (reportContent ? extractBlockingFindings(reportContent) : "").slice(0, 4000);
|
|
1153
|
+
const fixPhase: OrchestratorPhase = {
|
|
1154
|
+
key: "code",
|
|
1155
|
+
label: "š§ Fix ā CODE (review remediation)",
|
|
1156
|
+
model: currentPhase.model,
|
|
1157
|
+
fallback: currentPhase.fallback,
|
|
1158
|
+
agent: loadAgentDef("code"),
|
|
1159
|
+
instruction:
|
|
1160
|
+
`You are the CODE agent. REVIEW found BLOCKING issues. Fix ONLY these, root cause not workaround, then update \`.phi/plans/progress-${currentRunTs}.md\`.\n\n## BLOCKING findings to fix:\n${blocking || "(see the review report in .phi/plans/)"}\n` +
|
|
1161
|
+
COMMON_PHASE_RULES,
|
|
1162
|
+
};
|
|
1163
|
+
const reReviewPhase: OrchestratorPhase = {
|
|
1164
|
+
...currentPhase,
|
|
1165
|
+
retried: false,
|
|
1166
|
+
useFallback: false,
|
|
1167
|
+
label: "š Re-REVIEW (after fix)",
|
|
1168
|
+
};
|
|
1169
|
+
// Run next: fix, then re-review.
|
|
1170
|
+
phaseQueue.unshift(reReviewPhase);
|
|
1171
|
+
phaseQueue.unshift(fixPhase);
|
|
1172
|
+
ctx.ui.notify(`\nš **REVIEW verdict: FAIL** ā running one targeted fix + re-review cycle.`, "warning");
|
|
1173
|
+
}
|
|
1121
1174
|
|
|
1122
1175
|
// Checkpoint resume position after each base phase (synthetic fix/re-review
|
|
1123
1176
|
// phases have no baseIndex and do not advance it).
|
|
@@ -1168,7 +1221,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
1168
1221
|
const firstResume = remaining[0];
|
|
1169
1222
|
currentPhase = firstResume;
|
|
1170
1223
|
phaseQueue = remaining.slice(1);
|
|
1171
|
-
ctx.ui.notify(
|
|
1224
|
+
ctx.ui.notify(
|
|
1225
|
+
`š **Resuming orchestration** at ${firstResume.label} (${remaining.length} phase(s) left)\n`,
|
|
1226
|
+
"info",
|
|
1227
|
+
);
|
|
1172
1228
|
const { modelId, warning } = await switchModelForPhase(firstResume, ctx);
|
|
1173
1229
|
activateAgent(firstResume, ctx);
|
|
1174
1230
|
ctx.ui.notify(`${firstResume.label} ā \`${modelId}\``, "info");
|
|
@@ -1183,7 +1239,8 @@ Tag the note with relevant keywords for vector search.
|
|
|
1183
1239
|
const description = rawArgs.replace(/(^|\s)--fanout\b/g, " ").trim();
|
|
1184
1240
|
|
|
1185
1241
|
if (!description) {
|
|
1186
|
-
ctx.ui.notify(
|
|
1242
|
+
ctx.ui.notify(
|
|
1243
|
+
`**Usage:** \`/plan <project description>\` (or \`/plan --resume\` to continue a checkpointed run, \`/plan --fanout <desc>\` for parallel exploration)
|
|
1187
1244
|
|
|
1188
1245
|
**Examples:**
|
|
1189
1246
|
/plan Build a REST API for user authentication with JWT
|
|
@@ -1191,7 +1248,9 @@ Tag the note with relevant keywords for vector search.
|
|
|
1191
1248
|
/plan Add test coverage to the payment module
|
|
1192
1249
|
|
|
1193
1250
|
**Other commands:**
|
|
1194
|
-
/plans ā List all plans`,
|
|
1251
|
+
/plans ā List all plans`,
|
|
1252
|
+
"info",
|
|
1253
|
+
);
|
|
1195
1254
|
return;
|
|
1196
1255
|
}
|
|
1197
1256
|
|
|
@@ -1199,7 +1258,11 @@ Tag the note with relevant keywords for vector search.
|
|
|
1199
1258
|
await ensurePlansDir();
|
|
1200
1259
|
const ts = timestamp();
|
|
1201
1260
|
const specFile = `spec-${ts}.md`;
|
|
1202
|
-
await writeFile(
|
|
1261
|
+
await writeFile(
|
|
1262
|
+
join(plansDir, specFile),
|
|
1263
|
+
`# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`,
|
|
1264
|
+
"utf-8",
|
|
1265
|
+
);
|
|
1203
1266
|
|
|
1204
1267
|
// Build phases with model assignments + agent definitions
|
|
1205
1268
|
currentDescription = description;
|
|
@@ -1239,7 +1302,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
1239
1302
|
try {
|
|
1240
1303
|
const available = ctx.modelRegistry?.getAvailable?.() || [];
|
|
1241
1304
|
const exploreModelId = resolveModelRef(available, firstPhase.model)?.id || firstPhase.model;
|
|
1242
|
-
ctx.ui.notify(
|
|
1305
|
+
ctx.ui.notify(
|
|
1306
|
+
`\nš **Parallel exploration** (read-only, up to 2 concurrent) ā building a fuller map before phase 1...`,
|
|
1307
|
+
"info",
|
|
1308
|
+
);
|
|
1243
1309
|
const fan = await runExploreFanout(defaultExplorerSpecs(description), {
|
|
1244
1310
|
model: exploreModelId,
|
|
1245
1311
|
tools: READONLY_EXPLORER_TOOLS,
|
|
@@ -1249,14 +1315,22 @@ Tag the note with relevant keywords for vector search.
|
|
|
1249
1315
|
});
|
|
1250
1316
|
const okCount = fan.results.filter((r) => r.ok).length;
|
|
1251
1317
|
if (fan.merged.trim()) {
|
|
1252
|
-
firstPhase.instruction =
|
|
1253
|
-
|
|
1254
|
-
|
|
1318
|
+
firstPhase.instruction = `## Parallel exploration findings (${okCount} read-only sub-explorer(s))\nUse these as a starting map; verify and synthesize them into your brief, do not trust them blindly.\n\n${fan.merged}\n\n---\n${firstPhase.instruction}`;
|
|
1319
|
+
ctx.ui.notify(
|
|
1320
|
+
`\nš ${okCount} sub-exploration(s) merged into EXPLORE${fan.rateLimited ? " (rate limit hit ā ran the rest sequentially)" : ""}.`,
|
|
1321
|
+
"info",
|
|
1322
|
+
);
|
|
1255
1323
|
} else {
|
|
1256
|
-
ctx.ui.notify(
|
|
1324
|
+
ctx.ui.notify(
|
|
1325
|
+
`\nš Parallel exploration returned nothing usable${fan.rateLimited ? " (rate-limited)" : ""} ā running the normal single-agent EXPLORE.`,
|
|
1326
|
+
"warning",
|
|
1327
|
+
);
|
|
1257
1328
|
}
|
|
1258
1329
|
} catch {
|
|
1259
|
-
ctx.ui.notify(
|
|
1330
|
+
ctx.ui.notify(
|
|
1331
|
+
`\nš Parallel exploration skipped (error) ā running the normal single-agent EXPLORE.`,
|
|
1332
|
+
"warning",
|
|
1333
|
+
);
|
|
1260
1334
|
}
|
|
1261
1335
|
}
|
|
1262
1336
|
|
|
@@ -1293,7 +1367,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
1293
1367
|
}
|
|
1294
1368
|
|
|
1295
1369
|
const files = await readdir(plansDir);
|
|
1296
|
-
const specs = files
|
|
1370
|
+
const specs = files
|
|
1371
|
+
.filter((f) => f.startsWith("spec-") && f.endsWith(".md"))
|
|
1372
|
+
.sort()
|
|
1373
|
+
.reverse();
|
|
1297
1374
|
|
|
1298
1375
|
if (specs.length === 0) {
|
|
1299
1376
|
ctx.ui.notify("No plans found.", "info");
|