@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +92 -123
  3. package/config/routing.example.json +129 -0
  4. package/config/routing.schema.json +58 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +1 -1
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/sdk.d.ts.map +1 -1
  12. package/dist/core/sdk.js +1 -1
  13. package/dist/core/sdk.js.map +1 -1
  14. package/dist/core/system-prompt.d.ts.map +1 -1
  15. package/dist/core/system-prompt.js +6 -6
  16. package/dist/core/system-prompt.js.map +1 -1
  17. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/config-selector.js +1 -1
  19. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  20. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.js +2 -2
  22. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  23. package/dist/package-manager-cli.d.ts.map +1 -1
  24. package/dist/package-manager-cli.js +9 -8
  25. package/dist/package-manager-cli.js.map +1 -1
  26. package/dist/utils/pi-user-agent.d.ts.map +1 -1
  27. package/dist/utils/pi-user-agent.js +4 -1
  28. package/dist/utils/pi-user-agent.js.map +1 -1
  29. package/docs/compaction.md +11 -11
  30. package/docs/custom-provider.md +4 -4
  31. package/docs/development.md +2 -2
  32. package/docs/extensions.md +47 -47
  33. package/docs/fork-policy.md +81 -0
  34. package/docs/index.md +7 -7
  35. package/docs/json.md +3 -3
  36. package/docs/keybindings.md +5 -5
  37. package/docs/models.md +6 -6
  38. package/docs/packages.md +37 -37
  39. package/docs/prompt-templates.md +4 -4
  40. package/docs/providers.md +9 -9
  41. package/docs/quickstart.md +25 -25
  42. package/docs/rpc.md +3 -3
  43. package/docs/sdk.md +34 -34
  44. package/docs/session-format.md +4 -4
  45. package/docs/sessions.md +11 -11
  46. package/docs/settings.md +9 -9
  47. package/docs/shell-aliases.md +2 -2
  48. package/docs/skills.md +9 -9
  49. package/docs/terminal-setup.md +7 -7
  50. package/docs/termux.md +6 -6
  51. package/docs/themes.md +9 -9
  52. package/docs/tmux.md +3 -3
  53. package/docs/tui.md +8 -8
  54. package/docs/usage.md +39 -39
  55. package/docs/windows.md +2 -2
  56. package/extensions/phi/agents.ts +12 -117
  57. package/extensions/phi/benchmark.ts +129 -49
  58. package/extensions/phi/browser.ts +10 -37
  59. package/extensions/phi/btw/btw.ts +1 -7
  60. package/extensions/phi/chrome/index.ts +1283 -741
  61. package/extensions/phi/commit.ts +9 -14
  62. package/extensions/phi/goal/index.ts +10 -33
  63. package/extensions/phi/init.ts +37 -47
  64. package/extensions/phi/keys.ts +2 -7
  65. package/extensions/phi/mcp/callback-server.ts +162 -165
  66. package/extensions/phi/mcp/config.ts +122 -136
  67. package/extensions/phi/mcp/errors.ts +18 -23
  68. package/extensions/phi/mcp/index.ts +322 -355
  69. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  70. package/extensions/phi/mcp/server-manager.ts +390 -413
  71. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  72. package/extensions/phi/memory.ts +2 -2
  73. package/extensions/phi/models.ts +27 -26
  74. package/extensions/phi/orchestrator.ts +343 -266
  75. package/extensions/phi/productivity.ts +4 -2
  76. package/extensions/phi/providers/agent-def.ts +128 -0
  77. package/extensions/phi/providers/alibaba.ts +56 -7
  78. package/extensions/phi/providers/context-window.ts +4 -1
  79. package/extensions/phi/providers/live-models.ts +5 -20
  80. package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
  81. package/extensions/phi/providers/phase-machine.ts +220 -0
  82. package/extensions/phi/setup.ts +196 -169
  83. package/extensions/phi/skill-loader.ts +18 -21
  84. package/extensions/phi/smart-router.ts +6 -6
  85. package/extensions/phi/web-search.ts +90 -50
  86. 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
- extractBlockingFindings,
27
- extractHandoff,
28
- isTransientError,
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; description: string; goals: string[]; requirements: string[];
61
- architecture?: string[]; constraints?: string[]; successCriteria?: string[]; tasks: TaskDef[];
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) => { spec += `${i + 1}. ${g}\n`; });
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 => { spec += `- ${r}\n`; });
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 => { spec += `- ${a}\n`; });
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 => { spec += `- ${c}\n`; });
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 => { spec += `- [ ] ${s}\n`; });
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) t.subtasks.forEach(st => { todo += ` - [ ] ${st}\n`; });
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: "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.",
119
- promptSnippet: "Plan + structure projects using the prompt-architect pattern. Each task gets [CONTEXT] → [TASK] → [FORMAT] → [CONSTRAINTS].",
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({ description: "Full project description: what to build, why, and any relevant context" }),
133
- goals: Type.Union([Type.Array(Type.String()), Type.String()], { description: "Measurable project goals (what success looks like)" }),
134
- requirements: Type.Union([Type.Array(Type.String()), Type.String()], { description: "Technical and functional requirements" }),
135
- architecture: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Architecture decisions, tech stack choices, trade-offs" })),
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({ description: "SELF-CONTAINED task description. Include ALL context the sub-agent needs." }),
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(Type.Array(Type.Number(), { description: "Task numbers this depends on (1-indexed)" })),
143
- subtasks: Type.Optional(Type.Array(Type.String(), { description: "Specific sub-steps within this task" })),
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: [{ type: "text", text: "āš ļø Orchestration is already active via /plan. Do NOT call orchestrate during /plan phases. Follow the phase instructions instead." }],
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") return v.split("\n").map((s: string) => s.replace(/^[-•*]\s*/, "").trim()).filter(Boolean);
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 = `šŸ“‹ **Project "${p.title}" — Structured plan created!**\n\n` +
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 activeAgentTools: string[] | null = null;
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 { /* ignore */ }
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) { clearCheckpoint(); return; }
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 { /* best effort */ }
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 { /* ignore */ }
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 { /* best effort */ }
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 loadAgentDef: project .phi/skills first, then the
365
- * global ~/.phi/agent/skills (where postinstall copies the bundled skills).
366
- * YAML frontmatter is stripped. Returns null when the skill is not installed.
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 { /* try next dir */ }
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 { /* no routing config */ }
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 = process.platform === 'win32'
413
- ? `\nShell: bash (Git Bash), NOT cmd.exe. Always use Unix syntax: rm not del, test -f not if exist, / not \\\\`
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", label: "šŸ” Phase 1 — EXPLORE", model: explore.preferred, fallback: explore.fallback,
463
+ key: "explore",
464
+ label: "šŸ” Phase 1 — EXPLORE",
465
+ model: explore.preferred,
466
+ fallback: explore.fallback,
429
467
  agent: loadAgentDef("explore"),
430
- instruction: `You are the EXPLORE agent. Analyze the project requirements and existing codebase.
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
- \`\`\`` + runtimeInfo + COMMON_PHASE_RULES,
522
+ \`\`\`` +
523
+ runtimeInfo +
524
+ COMMON_PHASE_RULES,
484
525
  },
485
526
  {
486
- key: "plan", label: "šŸ“ Phase 2 — PLAN", model: plan.preferred, fallback: plan.fallback,
527
+ key: "plan",
528
+ label: "šŸ“ Phase 2 — PLAN",
529
+ model: plan.preferred,
530
+ fallback: plan.fallback,
487
531
  agent: loadAgentDef("plan"),
488
- instruction: `You are the PLAN agent. Design the architecture and create a detailed task list.
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.` + runtimeInfo + COMMON_PHASE_RULES,
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", label: "šŸ’» Phase 3 — CODE", model: code.preferred, fallback: code.fallback,
581
+ key: "code",
582
+ label: "šŸ’» Phase 3 — CODE",
583
+ model: code.preferred,
584
+ fallback: code.fallback,
535
585
  agent: loadAgentDef("code"),
536
- instruction: `You are the CODE agent. Implement the complete project.
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\`` + runtimeInfo + COMMON_PHASE_RULES,
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", label: "🧪 Phase 4 — TEST", model: test.preferred, fallback: test.fallback,
636
+ key: "test",
637
+ label: "🧪 Phase 4 — TEST",
638
+ model: test.preferred,
639
+ fallback: test.fallback,
584
640
  agent: loadAgentDef("test"),
585
- instruction: `You are the TEST agent. Verify the implementation.
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.` + runtimeInfo + COMMON_PHASE_RULES,
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", label: "šŸ” Phase 5 — REVIEW", model: review.preferred, fallback: review.fallback,
706
+ key: "review",
707
+ label: "šŸ” Phase 5 — REVIEW",
708
+ model: review.preferred,
709
+ fallback: review.fallback,
648
710
  agent: loadAgentDef("review"),
649
- instruction: `You are the REVIEW agent. Final quality review.
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` + runtimeInfo + COMMON_PHASE_RULES,
764
+ - Save any new architectural decisions or patterns discovered` +
765
+ runtimeInfo +
766
+ COMMON_PHASE_RULES,
702
767
  },
703
768
  ];
704
- phases.forEach((p, i) => { p.baseIndex = 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, ctx: any) {
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 = ['memory_search', 'memory_write', 'memory_read', 'ontology_add', 'ontology_query'];
782
- const agentTools = [...agentDef.tools, ...memoryTools.filter(t => !agentDef.tools.includes(t))];
783
- activeAgentTools = agentTools;
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
- activeAgentTools = null;
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
- activeAgentTools = null;
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
- activeAgentTools = null;
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) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
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(() => { /* best effort */ });
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(() => { /* best effort */ });
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(`\nšŸ“Š **Orchestration Summary**\n` +
859
- ` Phases: ${completedPhases}/5 completed${skippedNote}\n` +
860
- ` Duration: ${minutes}m ${seconds}s\n` +
861
- ` Check \`.phi/plans/\` for all reports`, "info");
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 { pi.sendUserMessage(`Orchestration finished: ${completedPhases}/5 phases completed${skippedNote}.`); } catch { /* best effort */ }
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 { ctx.abort(); } catch { /* best effort */ }
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(`\nā° **Phase timed out** (${MAX_PHASE_DURATION_MS / 60000} min) — retrying once on the fallback model.`, "warning");
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 (event, _ctx) => {
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) { internalAbort = false; return; }
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. Detect the aborted assistant
944
- // message and stop the whole workflow instead of chaining the next phase.
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) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
965
-
966
- // Build a structured summary of what happened in this phase
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
- // Detect API errors (401, auth failures) — abort workflow if found
1015
- const hasAuthError = messages.some((msg: any) => {
1016
- const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content || '');
1017
- return content.includes('401') && (content.includes('invalid access token') || content.includes('token expired') || content.includes('Unauthorized'));
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
- // Only a genuine auth failure (401) is fatal — abort the whole workflow.
1020
- if (hasAuthError) {
1021
- ctx.ui.notify(`\nāŒ **Orchestrator aborted:** API authentication error (401)\nCheck your API key and model configuration.`, "error");
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
- // Transient provider/proxy failure (timeout text / 5xx / 429 / broken JSON
1027
- // tool call) that is NOT a 401: retry the SAME phase once on the fallback
1028
- // model (a different family often gets through) before chaining onward.
1029
- if (currentPhase && !currentPhase.retried && isTransientError(messages)) {
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(`\nšŸ” **Transient provider error** in ${currentPhase.label} — retrying once on the fallback model.`, "warning");
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
- // A phase that made 0 tool calls is NOT fatal: a model may legitimately
1041
- // answer with text only (e.g. an inline plan). Warn and continue rather
1042
- // than killing phases 2-5, mirroring the graceful phase-timeout path.
1043
- if (toolCallCount === 0 && messages.length > 0) {
1044
- ctx.ui.notify(`\nāš ļø **Phase made 0 tool calls** — it may have responded with text only. Continuing to the next phase.`, "warning");
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
- // Build the summary
1051
- const summaryParts: string[] = [];
1052
- summaryParts.push(`Tool calls: ${toolCallCount}`);
1053
- // Anti-loop guard: warn if tool calls are excessive
1054
- if (toolCallCount > MAX_TOOL_CALLS_PER_PHASE) {
1055
- summaryParts.push(`āš ļø WARNING: Phase used ${toolCallCount} tool calls (limit: ${MAX_TOOL_CALLS_PER_PHASE}). Possible loop detected.`);
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 = handoff
1079
- ? `Tool calls: ${toolCallCount}\n## HANDOFF (from ${currentPhase?.label || "previous phase"})\n${handoff}`
1080
- : phaseSummary;
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
- // Verdict-driven control (best-effort, never breaks the chain):
1086
- // - BLOCKED: the phase could not run; pause the run for the user.
1087
- // - REVIEW FAIL: open ONE bounded fix -> re-review cycle.
1088
- try {
1089
- const verdict = reportContent ? parsePhaseVerdict(reportContent) : null;
1090
- const looped = toolCallCount > MAX_TOOL_CALLS_PER_PHASE;
1091
-
1092
- if (verdict === "BLOCKED" && currentPhase) {
1093
- ctx.ui.notify(`\nāøļø **${currentPhase.label} reported BLOCKED.** Pausing /plan — see \`.phi/plans/${currentPhase.key}-${currentRunTs}.md\`. Fix the blocker and re-run \`/plan\` to continue.`, "warning");
1094
- stopOrchestration();
1095
- return;
1096
- }
1097
-
1098
- if (currentPhase?.key === "review" && verdict === "FAIL" && reviewFixRounds < 1 && !looped) {
1099
- reviewFixRounds++;
1100
- const blocking = (reportContent ? extractBlockingFindings(reportContent) : "").slice(0, 4000);
1101
- const fixPhase: OrchestratorPhase = {
1102
- key: "code",
1103
- label: "šŸ”§ Fix — CODE (review remediation)",
1104
- model: currentPhase.model,
1105
- fallback: currentPhase.fallback,
1106
- agent: loadAgentDef("code"),
1107
- instruction: `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` + COMMON_PHASE_RULES,
1108
- };
1109
- const reReviewPhase: OrchestratorPhase = {
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(`šŸ“‹ **Resuming orchestration** at ${firstResume.label} (${remaining.length} phase(s) left)\n`, "info");
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(`**Usage:** \`/plan <project description>\` (or \`/plan --resume\` to continue a checkpointed run, \`/plan --fanout <desc>\` for parallel exploration)
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`, "info");
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(join(plansDir, specFile), `# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`, "utf-8");
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(`\nšŸ”­ **Parallel exploration** (read-only, up to 2 concurrent) — building a fuller map before phase 1...`, "info");
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
- `## 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}`;
1254
- ctx.ui.notify(`\nšŸ”­ ${okCount} sub-exploration(s) merged into EXPLORE${fan.rateLimited ? " (rate limit hit — ran the rest sequentially)" : ""}.`, "info");
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(`\nšŸ”­ Parallel exploration returned nothing usable${fan.rateLimited ? " (rate-limited)" : ""} — running the normal single-agent EXPLORE.`, "warning");
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(`\nšŸ”­ Parallel exploration skipped (error) — running the normal single-agent EXPLORE.`, "warning");
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.filter(f => f.startsWith("spec-") && f.endsWith(".md")).sort().reverse();
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");