@phi-code-admin/phi-code 0.86.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 (33) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/docs/fork-policy.md +18 -0
  3. package/extensions/phi/agents.ts +7 -4
  4. package/extensions/phi/benchmark.ts +129 -49
  5. package/extensions/phi/browser.ts +10 -37
  6. package/extensions/phi/btw/btw.ts +1 -7
  7. package/extensions/phi/chrome/index.ts +1283 -741
  8. package/extensions/phi/commit.ts +9 -14
  9. package/extensions/phi/goal/index.ts +10 -33
  10. package/extensions/phi/init.ts +37 -47
  11. package/extensions/phi/keys.ts +2 -7
  12. package/extensions/phi/mcp/callback-server.ts +162 -165
  13. package/extensions/phi/mcp/config.ts +122 -136
  14. package/extensions/phi/mcp/errors.ts +18 -23
  15. package/extensions/phi/mcp/index.ts +322 -355
  16. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  17. package/extensions/phi/mcp/server-manager.ts +390 -413
  18. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  19. package/extensions/phi/memory.ts +2 -2
  20. package/extensions/phi/models.ts +27 -26
  21. package/extensions/phi/orchestrator.ts +338 -229
  22. package/extensions/phi/productivity.ts +4 -2
  23. package/extensions/phi/providers/agent-def.ts +1 -1
  24. package/extensions/phi/providers/alibaba.ts +56 -7
  25. package/extensions/phi/providers/context-window.ts +4 -1
  26. package/extensions/phi/providers/live-models.ts +5 -20
  27. package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
  28. package/extensions/phi/providers/phase-machine.ts +220 -0
  29. package/extensions/phi/setup.ts +196 -169
  30. package/extensions/phi/skill-loader.ts +14 -17
  31. package/extensions/phi/smart-router.ts +6 -6
  32. package/extensions/phi/web-search.ts +90 -50
  33. package/package.json +1 -1
@@ -16,20 +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";
31
- import { defaultExplorerSpecs, READONLY_EXPLORER_TOOLS, runExploreFanout } from "./providers/explore-fanout.js";
22
+ import { join } from "node:path";
23
+ import { Type } from "@sinclair/typebox";
24
+ import type { ExtensionAPI } from "phi-code";
32
25
  import { type AgentDef, loadAgentDef } from "./providers/agent-def.js";
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";
33
29
 
34
30
  // ─── Types ───────────────────────────────────────────────────────────────
35
31
 
@@ -58,35 +54,51 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
58
54
  // ─── Plan File Generators ────────────────────────────────────────
59
55
 
60
56
  function generateSpec(p: {
61
- title: string; description: string; goals: string[]; requirements: string[];
62
- 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[];
63
65
  }): string {
64
66
  let spec = `# ${p.title}\n\n`;
65
67
  spec += `**Created:** ${new Date().toLocaleString()}\n\n`;
66
68
  spec += `## Description\n\n${p.description}\n\n`;
67
69
  spec += `## Goals\n\n`;
68
- p.goals.forEach((g, i) => { spec += `${i + 1}. ${g}\n`; });
70
+ p.goals.forEach((g, i) => {
71
+ spec += `${i + 1}. ${g}\n`;
72
+ });
69
73
  spec += "\n## Requirements\n\n";
70
- p.requirements.forEach(r => { spec += `- ${r}\n`; });
74
+ p.requirements.forEach((r) => {
75
+ spec += `- ${r}\n`;
76
+ });
71
77
  spec += "\n";
72
78
  if (p.architecture?.length) {
73
79
  spec += `## Architecture\n\n`;
74
- p.architecture.forEach(a => { spec += `- ${a}\n`; });
80
+ p.architecture.forEach((a) => {
81
+ spec += `- ${a}\n`;
82
+ });
75
83
  spec += "\n";
76
84
  }
77
85
  if (p.constraints?.length) {
78
86
  spec += `## Constraints\n\n`;
79
- p.constraints.forEach(c => { spec += `- ${c}\n`; });
87
+ p.constraints.forEach((c) => {
88
+ spec += `- ${c}\n`;
89
+ });
80
90
  spec += "\n";
81
91
  }
82
92
  if (p.successCriteria?.length) {
83
93
  spec += `## Success Criteria\n\n`;
84
- p.successCriteria.forEach(s => { spec += `- [ ] ${s}\n`; });
94
+ p.successCriteria.forEach((s) => {
95
+ spec += `- [ ] ${s}\n`;
96
+ });
85
97
  spec += "\n";
86
98
  }
87
99
  spec += `## Task Overview\n\n| # | Task | Agent | Priority | Dependencies |\n|---|------|-------|----------|-------------|\n`;
88
100
  p.tasks.forEach((t, i) => {
89
- const deps = t.dependencies?.map(d => `#${d}`).join(", ") || "—";
101
+ const deps = t.dependencies?.map((d) => `#${d}`).join(", ") || "—";
90
102
  spec += `| ${i + 1} | ${t.title} | ${t.agent || "code"} | ${t.priority || "medium"} | ${deps} |\n`;
91
103
  });
92
104
  spec += `\n---\n*Generated by Phi Code Orchestrator*\n`;
@@ -102,12 +114,15 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
102
114
  const prioTag = t.priority === "high" ? " 🔴" : t.priority === "low" ? " 🟢" : " 🟡";
103
115
  const depsTag = t.dependencies?.length ? ` (after #${t.dependencies.join(", #")})` : "";
104
116
  todo += `## Task ${i + 1}: ${t.title}${prioTag}${agentTag}${depsTag}\n\n- [ ] ${t.description}\n`;
105
- if (t.subtasks) t.subtasks.forEach(st => { todo += ` - [ ] ${st}\n`; });
117
+ if (t.subtasks)
118
+ t.subtasks.forEach((st) => {
119
+ todo += ` - [ ] ${st}\n`;
120
+ });
106
121
  todo += "\n";
107
122
  });
108
123
  todo += `---\n\n## Progress\n\n- Total: ${tasks.length} tasks\n`;
109
- todo += `- High priority: ${tasks.filter(t => t.priority === "high").length}\n`;
110
- 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`;
111
126
  return todo;
112
127
  }
113
128
 
@@ -116,8 +131,10 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
116
131
  pi.registerTool({
117
132
  name: "orchestrate",
118
133
  label: "Project Orchestrator",
119
- 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.",
120
- 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].",
121
138
  promptGuidelines: [
122
139
  // NOTE: The old guideline "When asked to plan or build a project, call the orchestrate tool"
123
140
  // was REMOVED because it hijacked the /plan command flow. The orchestrate tool is now
@@ -130,30 +147,59 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
130
147
  ],
131
148
  parameters: Type.Object({
132
149
  title: Type.String({ description: "Concise project title" }),
133
- description: Type.String({ description: "Full project description: what to build, why, and any relevant context" }),
134
- goals: Type.Union([Type.Array(Type.String()), Type.String()], { description: "Measurable project goals (what success looks like)" }),
135
- requirements: Type.Union([Type.Array(Type.String()), Type.String()], { description: "Technical and functional requirements" }),
136
- 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
+ ),
137
164
  tasks: Type.Array(
138
165
  Type.Object({
139
166
  title: Type.String({ description: "Clear, action-oriented task title" }),
140
- 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
+ }),
141
170
  agent: Type.Optional(Type.String({ description: "Agent type: explore, plan, code, test, review" })),
142
171
  priority: Type.Optional(Type.String({ description: "high, medium, low" })),
143
- dependencies: Type.Optional(Type.Array(Type.Number(), { description: "Task numbers this depends on (1-indexed)" })),
144
- 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",
145
189
  }),
146
- { description: "Ordered list of tasks" }
147
190
  ),
148
- constraints: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Hard constraints: things to avoid, rules" })),
149
- successCriteria: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "How to verify the project is complete" })),
150
191
  }),
151
192
 
152
193
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
153
194
  // Block if /plan orchestration is active — prevent hijacking
154
195
  if (orchestrationActive) {
155
196
  return {
156
- 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
+ ],
157
203
  details: undefined,
158
204
  };
159
205
  }
@@ -164,7 +210,11 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
164
210
  const toArray = (v: any): string[] => {
165
211
  if (!v) return [];
166
212
  if (Array.isArray(v)) return v;
167
- 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);
168
218
  return [];
169
219
  };
170
220
 
@@ -191,7 +241,8 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
191
241
  await writeFile(join(plansDir, specFile), spec, "utf-8");
192
242
  await writeFile(join(plansDir, todoFile), todo, "utf-8");
193
243
 
194
- const summary = `📋 **Project "${p.title}" — Structured plan created!**\n\n` +
244
+ const summary =
245
+ `📋 **Project "${p.title}" — Structured plan created!**\n\n` +
195
246
  `**Spec:** \`${specFile}\` | **Todo:** \`${todoFile}\`\n\n` +
196
247
  `**Goals:** ${p.goals.length} | **Requirements:** ${p.requirements.length} | **Tasks:** ${p.tasks.length}\n\n` +
197
248
  `The spec and todo files are ready in \`.phi/plans/\`. ` +
@@ -234,7 +285,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
234
285
  let phaseQueue: OrchestratorPhase[] = [];
235
286
  let orchestrationActive = false;
236
287
  let activeAgentPrompt: string | null = null;
237
- let activeAgentTools: string[] | null = null;
288
+ let _activeAgentTools: string[] | null = null;
238
289
  let savedTools: string[] | null = null;
239
290
  let phasePending = false; // true while waiting for a phase to complete
240
291
  let phaseTimeoutId: ReturnType<typeof setTimeout> | null = null;
@@ -264,7 +315,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
264
315
  try {
265
316
  const f = join(process.cwd(), ".phi", "plans", `${key}-${ts}.md`);
266
317
  if (existsSync(f)) return readFileSync(f, "utf-8");
267
- } catch { /* ignore */ }
318
+ } catch {
319
+ /* ignore */
320
+ }
268
321
  return null;
269
322
  }
270
323
 
@@ -277,13 +330,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
277
330
  }
278
331
  function writeCheckpoint(nextBaseIndex: number): void {
279
332
  try {
280
- if (nextBaseIndex >= 5) { clearCheckpoint(); return; }
333
+ if (nextBaseIndex >= 5) {
334
+ clearCheckpoint();
335
+ return;
336
+ }
281
337
  writeFileSync(
282
338
  checkpointPath(),
283
339
  JSON.stringify({ description: currentDescription, ts: currentRunTs, nextBaseIndex }, null, 2),
284
340
  "utf-8",
285
341
  );
286
- } catch { /* best effort */ }
342
+ } catch {
343
+ /* best effort */
344
+ }
287
345
  }
288
346
  function readCheckpoint(): { description: string; ts: string; nextBaseIndex: number } | null {
289
347
  try {
@@ -293,14 +351,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
293
351
  if (typeof c?.description === "string" && typeof c?.ts === "string" && typeof c?.nextBaseIndex === "number") {
294
352
  return c;
295
353
  }
296
- } catch { /* ignore */ }
354
+ } catch {
355
+ /* ignore */
356
+ }
297
357
  return null;
298
358
  }
299
359
  function clearCheckpoint(): void {
300
360
  try {
301
361
  const p = checkpointPath();
302
362
  if (existsSync(p)) unlinkSync(p);
303
- } catch { /* best effort */ }
363
+ } catch {
364
+ /* best effort */
365
+ }
304
366
  }
305
367
 
306
368
  /**
@@ -344,7 +406,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
344
406
  const fmMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
345
407
  const body = (fmMatch ? fmMatch[1] : content).trim();
346
408
  if (body) return body;
347
- } catch { /* try next dir */ }
409
+ } catch {
410
+ /* try next dir */
411
+ }
348
412
  }
349
413
  return null;
350
414
  }
@@ -358,7 +422,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
358
422
  let routing: any = { routes: {}, default: { model: "default" } };
359
423
  try {
360
424
  routing = JSON.parse(readFileSync(routingPath, "utf-8"));
361
- } catch { /* no routing config */ }
425
+ } catch {
426
+ /* no routing config */
427
+ }
362
428
 
363
429
  function getModel(routeKey: string): { preferred: string; fallback: string } {
364
430
  const route = routing.routes?.[routeKey];
@@ -377,9 +443,10 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
377
443
  const ts = tsOverride || timestamp();
378
444
  currentRunTs = ts; // shared by all phase report file names this run (or resumed)
379
445
  // Inject runtime info so agents can adapt to the host OS
380
- const shellNote = process.platform === 'win32'
381
- ? `\nShell: bash (Git Bash), NOT cmd.exe. Always use Unix syntax: rm not del, test -f not if exist, / not \\\\`
382
- : '';
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
+ : "";
383
450
  const runtimeInfo = `\n\nRuntime: ${process.platform} (${process.arch})${shellNote}`;
384
451
 
385
452
  // Embed the prompt-architect skill so the PLAN phase applies it to every
@@ -393,9 +460,13 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
393
460
 
394
461
  const phases: OrchestratorPhase[] = [
395
462
  {
396
- 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,
397
467
  agent: loadAgentDef("explore"),
398
- 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.
399
470
 
400
471
  **FIRST ACTION (MANDATORY):** Call \`memory_search\` with project-relevant keywords to check for prior context.
401
472
 
@@ -448,12 +519,18 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
448
519
  - Every function fully implemented
449
520
  - Follow existing patterns if codebase exists
450
521
  - [Any other specific constraints]
451
- \`\`\`` + runtimeInfo + COMMON_PHASE_RULES,
522
+ \`\`\`` +
523
+ runtimeInfo +
524
+ COMMON_PHASE_RULES,
452
525
  },
453
526
  {
454
- 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,
455
531
  agent: loadAgentDef("plan"),
456
- 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.
457
534
 
458
535
  **Context Retrieval:**
459
536
  1. Use \`ontology_query\` to retrieve all entities and relations from Phase 1
@@ -496,12 +573,18 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
496
573
  - Dependencies: Task 1
497
574
  \`\`\`
498
575
  ${promptArchitectSection}
499
- 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,
500
579
  },
501
580
  {
502
- 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,
503
585
  agent: loadAgentDef("code"),
504
- instruction: `You are the CODE agent. Implement the complete project.
586
+ instruction:
587
+ `You are the CODE agent. Implement the complete project.
505
588
 
506
589
  **Context Retrieval:**
507
590
  1. Use \`memory_search\` with project keywords to find notes from previous phases
@@ -545,12 +628,18 @@ After implementation, use \`memory_write\` to save a summary of what was built,
545
628
  **CRITICAL RULES:**
546
629
  - Write ONE file per tool call — NEVER combine multiple files in a single response
547
630
  - Keep each file under 500 lines. If longer, split into modules
548
- - 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,
549
634
  },
550
635
  {
551
- 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,
552
640
  agent: loadAgentDef("test"),
553
- instruction: `You are the TEST agent. Verify the implementation.
641
+ instruction:
642
+ `You are the TEST agent. Verify the implementation.
554
643
 
555
644
  **Context Retrieval:**
556
645
  1. Use \`memory_search\` to find implementation notes from the CODE phase
@@ -609,12 +698,18 @@ $ <command actually executed>
609
698
 
610
699
  After testing, use \`memory_write\` to save test results, bugs found, and lessons learned.
611
700
 
612
- **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,
613
704
  },
614
705
  {
615
- 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,
616
710
  agent: loadAgentDef("review"),
617
- instruction: `You are the REVIEW agent. Final quality review.
711
+ instruction:
712
+ `You are the REVIEW agent. Final quality review.
618
713
 
619
714
  **FIRST ACTIONS (MANDATORY — do these before anything else):**
620
715
  1. Call \`memory_search\` to find all notes from previous phases (explore, plan, code, test)
@@ -666,10 +761,14 @@ Tag the note with relevant keywords for vector search.
666
761
  **Ontology enrichment:** After your review, use \`ontology_add\` to save your key findings:
667
762
  - Add a "review-report" entity with type "Document"
668
763
  - Add relations to the project: "reviews" → project, quality score as entity property
669
- - 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,
670
767
  },
671
768
  ];
672
- phases.forEach((p, i) => { p.baseIndex = i; });
769
+ phases.forEach((p, i) => {
770
+ p.baseIndex = i;
771
+ });
673
772
  return phases;
674
773
  }
675
774
 
@@ -733,7 +832,7 @@ Tag the note with relevant keywords for vector search.
733
832
  /**
734
833
  * Activate agent for a phase: set system prompt + restrict tools.
735
834
  */
736
- function activateAgent(phase: OrchestratorPhase, ctx: any) {
835
+ function activateAgent(phase: OrchestratorPhase, _ctx: any) {
737
836
  // Fallback capture: savedTools is normally taken in the /plan handler at
738
837
  // orchestration start, but capture it here too so restoration is always defined.
739
838
  if (!savedTools) {
@@ -746,21 +845,21 @@ Tag the note with relevant keywords for vector search.
746
845
  // Restrict tools to agent's allowed tools
747
846
  if (agentDef.tools.length > 0) {
748
847
  // Always include memory tools in orchestration phases
749
- const memoryTools = ['memory_search', 'memory_write', 'memory_read', 'ontology_add', 'ontology_query'];
750
- const agentTools = [...agentDef.tools, ...memoryTools.filter(t => !agentDef.tools.includes(t))];
751
- 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;
752
851
  pi.setActiveTools(agentTools);
753
852
  } else if (savedTools) {
754
853
  // Agent with no tool restriction: reset to the full toolset so the
755
854
  // previous phase's narrower restriction does not leak into this one.
756
- activeAgentTools = null;
855
+ _activeAgentTools = null;
757
856
  pi.setActiveTools(savedTools);
758
857
  }
759
858
  } else {
760
859
  // No agent for this phase: restore the full toolset, otherwise the prior
761
860
  // phase's restriction would persist.
762
861
  activeAgentPrompt = null;
763
- activeAgentTools = null;
862
+ _activeAgentTools = null;
764
863
  if (savedTools) {
765
864
  pi.setActiveTools(savedTools);
766
865
  }
@@ -772,7 +871,7 @@ Tag the note with relevant keywords for vector search.
772
871
  */
773
872
  function deactivateAgent() {
774
873
  activeAgentPrompt = null;
775
- activeAgentTools = null;
874
+ _activeAgentTools = null;
776
875
  if (savedTools) {
777
876
  pi.setActiveTools(savedTools);
778
877
  savedTools = null;
@@ -796,11 +895,16 @@ Tag the note with relevant keywords for vector search.
796
895
  setOrchestrationActive(false);
797
896
  phasePending = false;
798
897
  deactivateAgent();
799
- if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
898
+ if (phaseTimeoutId) {
899
+ clearTimeout(phaseTimeoutId);
900
+ phaseTimeoutId = null;
901
+ }
800
902
  if (originalModel) {
801
903
  const toRestore = originalModel;
802
904
  originalModel = null;
803
- Promise.resolve(pi.setModel(toRestore)).catch(() => { /* best effort */ });
905
+ Promise.resolve(pi.setModel(toRestore)).catch(() => {
906
+ /* best effort */
907
+ });
804
908
  }
805
909
  }
806
910
 
@@ -815,18 +919,26 @@ Tag the note with relevant keywords for vector search.
815
919
  if (originalModel) {
816
920
  const toRestore = originalModel;
817
921
  originalModel = null;
818
- 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;
819
929
  }
820
- if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
821
930
  // Generate global final summary using the real phase counters
822
931
  const elapsed = phaseStartTime ? Math.round((Date.now() - phaseStartTime) / 1000) : 0;
823
932
  const minutes = Math.floor(elapsed / 60);
824
933
  const seconds = elapsed % 60;
825
934
  const skippedNote = skippedPhases ? ` (${skippedPhases} skipped on timeout)` : "";
826
- ctx.ui.notify(`\n📊 **Orchestration Summary**\n` +
827
- ` Phases: ${completedPhases}/5 completed${skippedNote}\n` +
828
- ` Duration: ${minutes}m ${seconds}s\n` +
829
- ` 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
+ );
830
942
  const allDone = completedPhases >= 5 && skippedPhases === 0;
831
943
  const doneMsg = allDone
832
944
  ? `\n✅ **All 5 phases complete!**`
@@ -835,7 +947,11 @@ Tag the note with relevant keywords for vector search.
835
947
  ctx.ui.notify(doneMsg, "info");
836
948
  } catch {
837
949
  // Fallback: inject completion message into conversation stream
838
- 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
+ }
839
955
  }
840
956
  return;
841
957
  }
@@ -862,7 +978,11 @@ Tag the note with relevant keywords for vector search.
862
978
  // race a still-streaming phase. Mark it internal so the resulting
863
979
  // agent_end is not treated as a user cancellation.
864
980
  internalAbort = true;
865
- try { ctx.abort(); } catch { /* best effort */ }
981
+ try {
982
+ ctx.abort();
983
+ } catch {
984
+ /* best effort */
985
+ }
866
986
  // Retry the SAME phase once on the fallback model before skipping:
867
987
  // a timed-out phase usually means the model/route stalled, and a
868
988
  // different family often gets through (cap = one retry per phase).
@@ -870,7 +990,10 @@ Tag the note with relevant keywords for vector search.
870
990
  phase.retried = true;
871
991
  phase.useFallback = true;
872
992
  phaseQueue.unshift(phase);
873
- 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
+ );
874
997
  } else {
875
998
  skippedPhases++;
876
999
  ctx.ui.notify(`\n⏰ **Phase timed out again** — skipping to next phase.`, "warning");
@@ -883,9 +1006,9 @@ Tag the note with relevant keywords for vector search.
883
1006
 
884
1007
  // ─── System Prompt Injection — Agent personas ────────────────────
885
1008
 
886
- pi.on("before_agent_start", async (event, _ctx) => {
1009
+ pi.on("before_agent_start", async (_event, _ctx) => {
887
1010
  if (!orchestrationActive || !activeAgentPrompt) {
888
- return { };
1011
+ return {};
889
1012
  }
890
1013
  // Replace system prompt with the active agent's prompt
891
1014
  return { systemPrompt: activeAgentPrompt };
@@ -905,15 +1028,21 @@ Tag the note with relevant keywords for vector search.
905
1028
  // An internal abort (phase timeout) re-emits agent_end; that transition was
906
1029
  // already handled by the timeout. Consume the flag and ignore this event so
907
1030
  // it is not mistaken for a user cancellation below.
908
- 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);
909
1042
 
910
1043
  // User abort (Ctrl+C / ESC): the aborted run still emits agent_end, but it
911
- // must NOT be treated as phase completion. Detect the aborted assistant
912
- // message and stop the whole workflow instead of chaining the next phase.
913
- const userAborted = (event.messages || []).some(
914
- (m: any) => m.role === 'assistant' && m.stopReason === 'aborted',
915
- );
916
- if (userAborted) {
1044
+ // must NOT be treated as phase completion.
1045
+ if (analysis.userAborted) {
917
1046
  ctx.ui.notify(`\n🛑 **Orchestration cancelled** by user. Remaining phases skipped.`, "warning");
918
1047
  stopOrchestration();
919
1048
  return;
@@ -929,163 +1058,119 @@ Tag the note with relevant keywords for vector search.
929
1058
  }
930
1059
 
931
1060
  // Clear phase timeout on normal completion
932
- if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
933
-
934
- // Build a structured summary of what happened in this phase
935
- // Instead of raw LLM text, extract concrete actions: files created/modified,
936
- // errors encountered, test results. This gives the next phase actionable context.
937
- const messages = event.messages || [];
938
- const filesWritten: string[] = [];
939
- const filesEdited: string[] = [];
940
- const errorsHit: string[] = [];
941
- const testResults: string[] = [];
942
- let toolCallCount = 0;
943
-
944
- for (const rawMsg of messages) {
945
- // Pi message variants are scanned dynamically; cast to a loose shape once.
946
- const msg = rawMsg as { role?: string; content?: unknown; name?: string; toolName?: string; isError?: boolean };
947
- // Pi uses role: "toolResult" instead of "tool"
948
- if (msg.role === 'tool' || msg.role === 'function' || msg.role === 'toolResult') {
949
- toolCallCount++;
950
- const content = Array.isArray(msg.content)
951
- ? msg.content.map((c: any) => c.text || '').join('')
952
- : String(msg.content || '');
953
- const name = msg.name || msg.toolName || '';
954
- // Track writes
955
- if (name === 'write' && content.includes('Successfully wrote')) {
956
- const match = content.match(/wrote \d+ bytes to (.+)/);
957
- if (match) filesWritten.push(match[1]);
958
- }
959
- // Track edits — the edit tool returns "Successfully replaced N block(s) in <path>."
960
- // Anchor the path capture so it does not over-capture unrelated text.
961
- if (name === 'edit' && !content.includes('ERR') && !msg.isError) {
962
- const match = content.match(/replaced \d+ block\(s\) in (.+?)\.?$/m);
963
- if (match) filesEdited.push(match[1]);
964
- }
965
- // Track errors — but filter out edit retries (old_text mismatch = normal retry, not error)
966
- if ((content.includes('ERR:') || content.includes('Error:') || content.includes('FAIL'))
967
- && !content.includes('old text must match')
968
- && !content.includes('The old text')
969
- && !content.includes('oldText not found')
970
- && !content.includes('old_text not found')) {
971
- const preview = content.slice(0, 150).replace(/\n/g, ' ');
972
- errorsHit.push(`${name}: ${preview}`);
973
- }
974
- // Track test results
975
- if (content.includes('PASS') || content.includes('✅') || content.includes('✗') || content.includes('❌')) {
976
- const lines = content.split('\n').filter((l: string) => /PASS|FAIL|✅|❌|✗/.test(l));
977
- testResults.push(...lines.slice(0, 10));
978
- }
979
- }
1061
+ if (phaseTimeoutId) {
1062
+ clearTimeout(phaseTimeoutId);
1063
+ phaseTimeoutId = null;
980
1064
  }
981
1065
 
982
- // Detect API errors (401, auth failures) abort workflow if found
983
- const hasAuthError = messages.some((msg: any) => {
984
- const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content || '');
985
- 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,
986
1075
  });
987
- // Only a genuine auth failure (401) is fatal — abort the whole workflow.
988
- if (hasAuthError) {
989
- 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
+ );
990
1083
  stopOrchestration();
991
1084
  return;
992
1085
  }
993
1086
 
994
- // Transient provider/proxy failure (timeout text / 5xx / 429 / broken JSON
995
- // tool call) that is NOT a 401: retry the SAME phase once on the fallback
996
- // model (a different family often gets through) before chaining onward.
997
- if (currentPhase && !currentPhase.retried && isTransientError(messages)) {
998
- 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).
999
1091
  currentPhase.retried = true;
1000
1092
  currentPhase.useFallback = true;
1001
1093
  phaseQueue.unshift(currentPhase);
1002
1094
  phasePending = false;
1003
- 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
+ );
1004
1099
  sendNextPhase(ctx);
1005
1100
  return;
1006
1101
  }
1007
1102
 
1008
- // A phase that made 0 tool calls is NOT fatal: a model may legitimately
1009
- // answer with text only (e.g. an inline plan). Warn and continue rather
1010
- // than killing phases 2-5, mirroring the graceful phase-timeout path.
1011
- if (toolCallCount === 0 && messages.length > 0) {
1012
- 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
+ );
1013
1120
  if (phaseQueue.length > 0) {
1014
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.`;
1015
1122
  }
1016
1123
  }
1017
1124
 
1018
- // Build the summary
1019
- const summaryParts: string[] = [];
1020
- summaryParts.push(`Tool calls: ${toolCallCount}`);
1021
- // Anti-loop guard: warn if tool calls are excessive
1022
- if (toolCallCount > MAX_TOOL_CALLS_PER_PHASE) {
1023
- 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
+ );
1024
1133
  }
1025
- if (filesWritten.length > 0) summaryParts.push(`Files created/written: ${filesWritten.join(', ')}`);
1026
- if (filesEdited.length > 0) summaryParts.push(`Files edited: ${filesEdited.join(', ')}`);
1027
- if (testResults.length > 0) summaryParts.push(`Test results:\n${testResults.join('\n')}`);
1028
- if (errorsHit.length > 0) summaryParts.push(`Errors encountered: ${errorsHit.length}\n${errorsHit.slice(0, 5).join('\n')}`);
1029
-
1030
- // Verify mandatory tool usage
1031
- const toolNames = messages
1032
- .filter((m: any) => m.role === 'tool' || m.role === 'function' || m.role === 'toolResult')
1033
- .map((m: any) => (m as any).name || (m as any).toolName || '');
1034
- const hasMemorySearch = toolNames.includes('memory_search');
1035
- const hasMemoryWrite = toolNames.includes('memory_write');
1036
- if (!hasMemorySearch) summaryParts.push(`⚠️ Phase did NOT call memory_search (mandatory)`);
1037
- if (!hasMemoryWrite) summaryParts.push(`⚠️ Phase did NOT call memory_write (mandatory)`);
1038
-
1039
- const phaseSummary = summaryParts.join('\n');
1040
1134
 
1041
1135
  // Prefer the phase's canonical "## HANDOFF" block (a deterministic text
1042
1136
  // contract written to its report file) over the heuristic summary; fall back
1043
1137
  // to the heuristic when the model did not write one.
1044
- const reportContent = currentPhase ? readPhaseReport(currentPhase.key, currentRunTs) : null;
1045
1138
  const handoff = reportContent ? extractHandoff(reportContent) : "";
1046
- const nextBrief = handoff
1047
- ? `Tool calls: ${toolCallCount}\n## HANDOFF (from ${currentPhase?.label || "previous phase"})\n${handoff}`
1048
- : phaseSummary;
1139
+ const nextBrief = buildNextBrief(
1140
+ analysis,
1141
+ handoff,
1142
+ currentPhase?.label || "previous phase",
1143
+ MAX_TOOL_CALLS_PER_PHASE,
1144
+ );
1049
1145
  if (nextBrief && phaseQueue.length > 0) {
1050
1146
  phaseQueue[0].instruction += `\n\n**Previous phase summary:**\n${nextBrief}`;
1051
1147
  }
1052
1148
 
1053
- // Verdict-driven control (best-effort, never breaks the chain):
1054
- // - BLOCKED: the phase could not run; pause the run for the user.
1055
- // - REVIEW FAIL: open ONE bounded fix -> re-review cycle.
1056
- try {
1057
- const verdict = reportContent ? parsePhaseVerdict(reportContent) : null;
1058
- const looped = toolCallCount > MAX_TOOL_CALLS_PER_PHASE;
1059
-
1060
- if (verdict === "BLOCKED" && currentPhase) {
1061
- 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");
1062
- stopOrchestration();
1063
- return;
1064
- }
1065
-
1066
- if (currentPhase?.key === "review" && verdict === "FAIL" && reviewFixRounds < 1 && !looped) {
1067
- reviewFixRounds++;
1068
- const blocking = (reportContent ? extractBlockingFindings(reportContent) : "").slice(0, 4000);
1069
- const fixPhase: OrchestratorPhase = {
1070
- key: "code",
1071
- label: "🔧 Fix — CODE (review remediation)",
1072
- model: currentPhase.model,
1073
- fallback: currentPhase.fallback,
1074
- agent: loadAgentDef("code"),
1075
- 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,
1076
- };
1077
- const reReviewPhase: OrchestratorPhase = {
1078
- ...currentPhase,
1079
- retried: false,
1080
- useFallback: false,
1081
- label: "🔍 Re-REVIEW (after fix)",
1082
- };
1083
- // Run next: fix, then re-review.
1084
- phaseQueue.unshift(reReviewPhase);
1085
- phaseQueue.unshift(fixPhase);
1086
- ctx.ui.notify(`\n🔁 **REVIEW verdict: FAIL** — running one targeted fix + re-review cycle.`, "warning");
1087
- }
1088
- } 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
+ }
1089
1174
 
1090
1175
  // Checkpoint resume position after each base phase (synthetic fix/re-review
1091
1176
  // phases have no baseIndex and do not advance it).
@@ -1136,7 +1221,10 @@ Tag the note with relevant keywords for vector search.
1136
1221
  const firstResume = remaining[0];
1137
1222
  currentPhase = firstResume;
1138
1223
  phaseQueue = remaining.slice(1);
1139
- 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
+ );
1140
1228
  const { modelId, warning } = await switchModelForPhase(firstResume, ctx);
1141
1229
  activateAgent(firstResume, ctx);
1142
1230
  ctx.ui.notify(`${firstResume.label} → \`${modelId}\``, "info");
@@ -1151,7 +1239,8 @@ Tag the note with relevant keywords for vector search.
1151
1239
  const description = rawArgs.replace(/(^|\s)--fanout\b/g, " ").trim();
1152
1240
 
1153
1241
  if (!description) {
1154
- 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)
1155
1244
 
1156
1245
  **Examples:**
1157
1246
  /plan Build a REST API for user authentication with JWT
@@ -1159,7 +1248,9 @@ Tag the note with relevant keywords for vector search.
1159
1248
  /plan Add test coverage to the payment module
1160
1249
 
1161
1250
  **Other commands:**
1162
- /plans — List all plans`, "info");
1251
+ /plans — List all plans`,
1252
+ "info",
1253
+ );
1163
1254
  return;
1164
1255
  }
1165
1256
 
@@ -1167,7 +1258,11 @@ Tag the note with relevant keywords for vector search.
1167
1258
  await ensurePlansDir();
1168
1259
  const ts = timestamp();
1169
1260
  const specFile = `spec-${ts}.md`;
1170
- 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
+ );
1171
1266
 
1172
1267
  // Build phases with model assignments + agent definitions
1173
1268
  currentDescription = description;
@@ -1207,7 +1302,10 @@ Tag the note with relevant keywords for vector search.
1207
1302
  try {
1208
1303
  const available = ctx.modelRegistry?.getAvailable?.() || [];
1209
1304
  const exploreModelId = resolveModelRef(available, firstPhase.model)?.id || firstPhase.model;
1210
- 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
+ );
1211
1309
  const fan = await runExploreFanout(defaultExplorerSpecs(description), {
1212
1310
  model: exploreModelId,
1213
1311
  tools: READONLY_EXPLORER_TOOLS,
@@ -1217,14 +1315,22 @@ Tag the note with relevant keywords for vector search.
1217
1315
  });
1218
1316
  const okCount = fan.results.filter((r) => r.ok).length;
1219
1317
  if (fan.merged.trim()) {
1220
- firstPhase.instruction =
1221
- `## 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}`;
1222
- 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
+ );
1223
1323
  } else {
1224
- 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
+ );
1225
1328
  }
1226
1329
  } catch {
1227
- 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
+ );
1228
1334
  }
1229
1335
  }
1230
1336
 
@@ -1261,7 +1367,10 @@ Tag the note with relevant keywords for vector search.
1261
1367
  }
1262
1368
 
1263
1369
  const files = await readdir(plansDir);
1264
- 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();
1265
1374
 
1266
1375
  if (specs.length === 0) {
1267
1376
  ctx.ui.notify("No plans found.", "info");