@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.
- package/CHANGELOG.md +43 -0
- package/docs/fork-policy.md +18 -0
- package/extensions/phi/agents.ts +7 -4
- package/extensions/phi/benchmark.ts +129 -49
- package/extensions/phi/browser.ts +10 -37
- package/extensions/phi/btw/btw.ts +1 -7
- package/extensions/phi/chrome/index.ts +1283 -741
- package/extensions/phi/commit.ts +9 -14
- package/extensions/phi/goal/index.ts +10 -33
- package/extensions/phi/init.ts +37 -47
- package/extensions/phi/keys.ts +2 -7
- package/extensions/phi/mcp/callback-server.ts +162 -165
- package/extensions/phi/mcp/config.ts +122 -136
- package/extensions/phi/mcp/errors.ts +18 -23
- package/extensions/phi/mcp/index.ts +322 -355
- package/extensions/phi/mcp/oauth-provider.ts +289 -289
- package/extensions/phi/mcp/server-manager.ts +390 -413
- package/extensions/phi/mcp/tool-bridge.ts +381 -415
- package/extensions/phi/memory.ts +2 -2
- package/extensions/phi/models.ts +27 -26
- package/extensions/phi/orchestrator.ts +338 -229
- package/extensions/phi/productivity.ts +4 -2
- package/extensions/phi/providers/agent-def.ts +1 -1
- package/extensions/phi/providers/alibaba.ts +56 -7
- package/extensions/phi/providers/context-window.ts +4 -1
- package/extensions/phi/providers/live-models.ts +5 -20
- package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
- package/extensions/phi/providers/phase-machine.ts +220 -0
- package/extensions/phi/setup.ts +196 -169
- package/extensions/phi/skill-loader.ts +14 -17
- package/extensions/phi/smart-router.ts +6 -6
- package/extensions/phi/web-search.ts +90 -50
- 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
|
-
|
|
27
|
-
|
|
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;
|
|
62
|
-
|
|
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) => {
|
|
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 => {
|
|
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 => {
|
|
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 => {
|
|
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 => {
|
|
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)
|
|
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:
|
|
120
|
-
|
|
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({
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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({
|
|
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(
|
|
144
|
-
|
|
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: [
|
|
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")
|
|
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 =
|
|
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
|
|
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 {
|
|
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) {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 =
|
|
381
|
-
|
|
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",
|
|
463
|
+
key: "explore",
|
|
464
|
+
label: "🔍 Phase 1 — EXPLORE",
|
|
465
|
+
model: explore.preferred,
|
|
466
|
+
fallback: explore.fallback,
|
|
397
467
|
agent: loadAgentDef("explore"),
|
|
398
|
-
instruction:
|
|
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
|
-
\`\`\`` +
|
|
522
|
+
\`\`\`` +
|
|
523
|
+
runtimeInfo +
|
|
524
|
+
COMMON_PHASE_RULES,
|
|
452
525
|
},
|
|
453
526
|
{
|
|
454
|
-
key: "plan",
|
|
527
|
+
key: "plan",
|
|
528
|
+
label: "📐 Phase 2 — PLAN",
|
|
529
|
+
model: plan.preferred,
|
|
530
|
+
fallback: plan.fallback,
|
|
455
531
|
agent: loadAgentDef("plan"),
|
|
456
|
-
instruction:
|
|
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.` +
|
|
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",
|
|
581
|
+
key: "code",
|
|
582
|
+
label: "💻 Phase 3 — CODE",
|
|
583
|
+
model: code.preferred,
|
|
584
|
+
fallback: code.fallback,
|
|
503
585
|
agent: loadAgentDef("code"),
|
|
504
|
-
instruction:
|
|
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\`` +
|
|
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",
|
|
636
|
+
key: "test",
|
|
637
|
+
label: "🧪 Phase 4 — TEST",
|
|
638
|
+
model: test.preferred,
|
|
639
|
+
fallback: test.fallback,
|
|
552
640
|
agent: loadAgentDef("test"),
|
|
553
|
-
instruction:
|
|
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.` +
|
|
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",
|
|
706
|
+
key: "review",
|
|
707
|
+
label: "🔍 Phase 5 — REVIEW",
|
|
708
|
+
model: review.preferred,
|
|
709
|
+
fallback: review.fallback,
|
|
616
710
|
agent: loadAgentDef("review"),
|
|
617
|
-
instruction:
|
|
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` +
|
|
764
|
+
- Save any new architectural decisions or patterns discovered` +
|
|
765
|
+
runtimeInfo +
|
|
766
|
+
COMMON_PHASE_RULES,
|
|
670
767
|
},
|
|
671
768
|
];
|
|
672
|
-
phases.forEach((p, 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,
|
|
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 = [
|
|
750
|
-
const agentTools = [...agentDef.tools, ...memoryTools.filter(t => !agentDef.tools.includes(t))];
|
|
751
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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(() => {
|
|
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(() => {
|
|
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(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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 {
|
|
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 {
|
|
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(
|
|
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 (
|
|
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) {
|
|
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.
|
|
912
|
-
|
|
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) {
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
988
|
-
if (
|
|
989
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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(
|
|
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
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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 =
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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(
|
|
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(
|
|
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`,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1222
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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");
|