@phi-code-admin/phi-code 0.86.0 → 0.88.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 +104 -0
- package/docs/adr/0001-phase-contract.md +57 -0
- package/docs/adr/0002-independent-review.md +47 -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 +451 -237
- 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 +31 -5
- package/extensions/phi/providers/phase-machine.ts +283 -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,21 @@
|
|
|
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 {
|
|
28
|
+
analyzePhaseMessages,
|
|
29
|
+
buildNextBrief,
|
|
30
|
+
decidePhaseTransition,
|
|
31
|
+
resolvePhaseOutcome,
|
|
32
|
+
type StructuredPhaseResult,
|
|
33
|
+
} from "./providers/phase-machine.js";
|
|
33
34
|
|
|
34
35
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
35
36
|
|
|
@@ -58,35 +59,51 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
58
59
|
// ─── Plan File Generators ────────────────────────────────────────
|
|
59
60
|
|
|
60
61
|
function generateSpec(p: {
|
|
61
|
-
title: string;
|
|
62
|
-
|
|
62
|
+
title: string;
|
|
63
|
+
description: string;
|
|
64
|
+
goals: string[];
|
|
65
|
+
requirements: string[];
|
|
66
|
+
architecture?: string[];
|
|
67
|
+
constraints?: string[];
|
|
68
|
+
successCriteria?: string[];
|
|
69
|
+
tasks: TaskDef[];
|
|
63
70
|
}): string {
|
|
64
71
|
let spec = `# ${p.title}\n\n`;
|
|
65
72
|
spec += `**Created:** ${new Date().toLocaleString()}\n\n`;
|
|
66
73
|
spec += `## Description\n\n${p.description}\n\n`;
|
|
67
74
|
spec += `## Goals\n\n`;
|
|
68
|
-
p.goals.forEach((g, i) => {
|
|
75
|
+
p.goals.forEach((g, i) => {
|
|
76
|
+
spec += `${i + 1}. ${g}\n`;
|
|
77
|
+
});
|
|
69
78
|
spec += "\n## Requirements\n\n";
|
|
70
|
-
p.requirements.forEach(r => {
|
|
79
|
+
p.requirements.forEach((r) => {
|
|
80
|
+
spec += `- ${r}\n`;
|
|
81
|
+
});
|
|
71
82
|
spec += "\n";
|
|
72
83
|
if (p.architecture?.length) {
|
|
73
84
|
spec += `## Architecture\n\n`;
|
|
74
|
-
p.architecture.forEach(a => {
|
|
85
|
+
p.architecture.forEach((a) => {
|
|
86
|
+
spec += `- ${a}\n`;
|
|
87
|
+
});
|
|
75
88
|
spec += "\n";
|
|
76
89
|
}
|
|
77
90
|
if (p.constraints?.length) {
|
|
78
91
|
spec += `## Constraints\n\n`;
|
|
79
|
-
p.constraints.forEach(c => {
|
|
92
|
+
p.constraints.forEach((c) => {
|
|
93
|
+
spec += `- ${c}\n`;
|
|
94
|
+
});
|
|
80
95
|
spec += "\n";
|
|
81
96
|
}
|
|
82
97
|
if (p.successCriteria?.length) {
|
|
83
98
|
spec += `## Success Criteria\n\n`;
|
|
84
|
-
p.successCriteria.forEach(s => {
|
|
99
|
+
p.successCriteria.forEach((s) => {
|
|
100
|
+
spec += `- [ ] ${s}\n`;
|
|
101
|
+
});
|
|
85
102
|
spec += "\n";
|
|
86
103
|
}
|
|
87
104
|
spec += `## Task Overview\n\n| # | Task | Agent | Priority | Dependencies |\n|---|------|-------|----------|-------------|\n`;
|
|
88
105
|
p.tasks.forEach((t, i) => {
|
|
89
|
-
const deps = t.dependencies?.map(d => `#${d}`).join(", ") || "—";
|
|
106
|
+
const deps = t.dependencies?.map((d) => `#${d}`).join(", ") || "—";
|
|
90
107
|
spec += `| ${i + 1} | ${t.title} | ${t.agent || "code"} | ${t.priority || "medium"} | ${deps} |\n`;
|
|
91
108
|
});
|
|
92
109
|
spec += `\n---\n*Generated by Phi Code Orchestrator*\n`;
|
|
@@ -102,12 +119,15 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
102
119
|
const prioTag = t.priority === "high" ? " 🔴" : t.priority === "low" ? " 🟢" : " 🟡";
|
|
103
120
|
const depsTag = t.dependencies?.length ? ` (after #${t.dependencies.join(", #")})` : "";
|
|
104
121
|
todo += `## Task ${i + 1}: ${t.title}${prioTag}${agentTag}${depsTag}\n\n- [ ] ${t.description}\n`;
|
|
105
|
-
if (t.subtasks)
|
|
122
|
+
if (t.subtasks)
|
|
123
|
+
t.subtasks.forEach((st) => {
|
|
124
|
+
todo += ` - [ ] ${st}\n`;
|
|
125
|
+
});
|
|
106
126
|
todo += "\n";
|
|
107
127
|
});
|
|
108
128
|
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`;
|
|
129
|
+
todo += `- High priority: ${tasks.filter((t) => t.priority === "high").length}\n`;
|
|
130
|
+
todo += `- Agents: ${[...new Set(tasks.map((t) => t.agent || "code"))].join(", ")}\n`;
|
|
111
131
|
return todo;
|
|
112
132
|
}
|
|
113
133
|
|
|
@@ -116,8 +136,10 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
116
136
|
pi.registerTool({
|
|
117
137
|
name: "orchestrate",
|
|
118
138
|
label: "Project Orchestrator",
|
|
119
|
-
description:
|
|
120
|
-
|
|
139
|
+
description:
|
|
140
|
+
"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.",
|
|
141
|
+
promptSnippet:
|
|
142
|
+
"Plan + structure projects using the prompt-architect pattern. Each task gets [CONTEXT] → [TASK] → [FORMAT] → [CONSTRAINTS].",
|
|
121
143
|
promptGuidelines: [
|
|
122
144
|
// NOTE: The old guideline "When asked to plan or build a project, call the orchestrate tool"
|
|
123
145
|
// was REMOVED because it hijacked the /plan command flow. The orchestrate tool is now
|
|
@@ -130,30 +152,59 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
130
152
|
],
|
|
131
153
|
parameters: Type.Object({
|
|
132
154
|
title: Type.String({ description: "Concise project title" }),
|
|
133
|
-
description: Type.String({
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
description: Type.String({
|
|
156
|
+
description: "Full project description: what to build, why, and any relevant context",
|
|
157
|
+
}),
|
|
158
|
+
goals: Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
159
|
+
description: "Measurable project goals (what success looks like)",
|
|
160
|
+
}),
|
|
161
|
+
requirements: Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
162
|
+
description: "Technical and functional requirements",
|
|
163
|
+
}),
|
|
164
|
+
architecture: Type.Optional(
|
|
165
|
+
Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
166
|
+
description: "Architecture decisions, tech stack choices, trade-offs",
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
137
169
|
tasks: Type.Array(
|
|
138
170
|
Type.Object({
|
|
139
171
|
title: Type.String({ description: "Clear, action-oriented task title" }),
|
|
140
|
-
description: Type.String({
|
|
172
|
+
description: Type.String({
|
|
173
|
+
description: "SELF-CONTAINED task description. Include ALL context the sub-agent needs.",
|
|
174
|
+
}),
|
|
141
175
|
agent: Type.Optional(Type.String({ description: "Agent type: explore, plan, code, test, review" })),
|
|
142
176
|
priority: Type.Optional(Type.String({ description: "high, medium, low" })),
|
|
143
|
-
dependencies: Type.Optional(
|
|
144
|
-
|
|
177
|
+
dependencies: Type.Optional(
|
|
178
|
+
Type.Array(Type.Number(), { description: "Task numbers this depends on (1-indexed)" }),
|
|
179
|
+
),
|
|
180
|
+
subtasks: Type.Optional(
|
|
181
|
+
Type.Array(Type.String(), { description: "Specific sub-steps within this task" }),
|
|
182
|
+
),
|
|
183
|
+
}),
|
|
184
|
+
{ description: "Ordered list of tasks" },
|
|
185
|
+
),
|
|
186
|
+
constraints: Type.Optional(
|
|
187
|
+
Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
188
|
+
description: "Hard constraints: things to avoid, rules",
|
|
189
|
+
}),
|
|
190
|
+
),
|
|
191
|
+
successCriteria: Type.Optional(
|
|
192
|
+
Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
193
|
+
description: "How to verify the project is complete",
|
|
145
194
|
}),
|
|
146
|
-
{ description: "Ordered list of tasks" }
|
|
147
195
|
),
|
|
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
196
|
}),
|
|
151
197
|
|
|
152
198
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
153
199
|
// Block if /plan orchestration is active — prevent hijacking
|
|
154
200
|
if (orchestrationActive) {
|
|
155
201
|
return {
|
|
156
|
-
content: [
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: "⚠️ Orchestration is already active via /plan. Do NOT call orchestrate during /plan phases. Follow the phase instructions instead.",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
157
208
|
details: undefined,
|
|
158
209
|
};
|
|
159
210
|
}
|
|
@@ -164,7 +215,11 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
164
215
|
const toArray = (v: any): string[] => {
|
|
165
216
|
if (!v) return [];
|
|
166
217
|
if (Array.isArray(v)) return v;
|
|
167
|
-
if (typeof v === "string")
|
|
218
|
+
if (typeof v === "string")
|
|
219
|
+
return v
|
|
220
|
+
.split("\n")
|
|
221
|
+
.map((s: string) => s.replace(/^[-•*]\s*/, "").trim())
|
|
222
|
+
.filter(Boolean);
|
|
168
223
|
return [];
|
|
169
224
|
};
|
|
170
225
|
|
|
@@ -191,7 +246,8 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
191
246
|
await writeFile(join(plansDir, specFile), spec, "utf-8");
|
|
192
247
|
await writeFile(join(plansDir, todoFile), todo, "utf-8");
|
|
193
248
|
|
|
194
|
-
const summary =
|
|
249
|
+
const summary =
|
|
250
|
+
`📋 **Project "${p.title}" — Structured plan created!**\n\n` +
|
|
195
251
|
`**Spec:** \`${specFile}\` | **Todo:** \`${todoFile}\`\n\n` +
|
|
196
252
|
`**Goals:** ${p.goals.length} | **Requirements:** ${p.requirements.length} | **Tasks:** ${p.tasks.length}\n\n` +
|
|
197
253
|
`The spec and todo files are ready in \`.phi/plans/\`. ` +
|
|
@@ -234,7 +290,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
234
290
|
let phaseQueue: OrchestratorPhase[] = [];
|
|
235
291
|
let orchestrationActive = false;
|
|
236
292
|
let activeAgentPrompt: string | null = null;
|
|
237
|
-
let
|
|
293
|
+
let _activeAgentTools: string[] | null = null;
|
|
238
294
|
let savedTools: string[] | null = null;
|
|
239
295
|
let phasePending = false; // true while waiting for a phase to complete
|
|
240
296
|
let phaseTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -258,13 +314,89 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
258
314
|
// Set true right before an INTERNAL ctx.abort() (phase timeout) so the
|
|
259
315
|
// resulting agent_end is not mistaken for a user Ctrl+C cancellation.
|
|
260
316
|
let internalAbort = false;
|
|
317
|
+
// Structured result emitted by the current phase via the `phase_result` tool
|
|
318
|
+
// (robust primary path). Reset at each phase start; read in agent_end and
|
|
319
|
+
// merged with the parsed markdown report (fallback) by resolvePhaseOutcome.
|
|
320
|
+
// currentPhaseResultKey stamps which phase produced it, so a stale result
|
|
321
|
+
// from a previous run/phase (or a tool call that lands after the phase
|
|
322
|
+
// transition) is never mistaken for the current phase's output.
|
|
323
|
+
let currentPhaseResult: StructuredPhaseResult | null = null;
|
|
324
|
+
let currentPhaseResultKey: string | null = null;
|
|
325
|
+
|
|
326
|
+
// ─── phase_result Tool — structured phase outcome (robust primary path) ───
|
|
327
|
+
// A phase agent CALLS this to emit its verdict/handoff/blocking as data
|
|
328
|
+
// instead of relying on the orchestrator regex-scraping its markdown report.
|
|
329
|
+
// It is only meaningful during /plan; outside orchestration it is a no-op so
|
|
330
|
+
// a stray call never errors. The markdown report is still written (it is the
|
|
331
|
+
// human-readable artifact); this just makes the machine-read path exact.
|
|
332
|
+
pi.registerTool({
|
|
333
|
+
name: "phase_result",
|
|
334
|
+
label: "Phase Result",
|
|
335
|
+
description:
|
|
336
|
+
"Report THIS /plan phase's structured outcome so the orchestrator reads it exactly instead of parsing your markdown. Call it once, at the end, IN ADDITION to writing your report file. TEST and REVIEW phases MUST call it with a verdict.",
|
|
337
|
+
promptGuidelines: [
|
|
338
|
+
"During a /plan phase, after writing your report file, call phase_result once with your verdict (TEST/REVIEW) and a concise handoff for the next phase.",
|
|
339
|
+
"verdict: PASS only if you OBSERVED success; FAIL if any blocking issue; BLOCKED if you could not run; SKIP if not applicable.",
|
|
340
|
+
"blocking: the must-fix findings (REVIEW), one per line. handoff: what is done, open risks, and the single most important next action.",
|
|
341
|
+
],
|
|
342
|
+
parameters: Type.Object({
|
|
343
|
+
verdict: Type.Optional(
|
|
344
|
+
Type.Union([Type.Literal("PASS"), Type.Literal("FAIL"), Type.Literal("BLOCKED"), Type.Literal("SKIP")], {
|
|
345
|
+
description: "Phase verdict (required for TEST and REVIEW phases)",
|
|
346
|
+
}),
|
|
347
|
+
),
|
|
348
|
+
blocking: Type.Optional(
|
|
349
|
+
Type.String({ description: "Must-fix findings, one per line (REVIEW). Empty when none." }),
|
|
350
|
+
),
|
|
351
|
+
handoff: Type.Optional(
|
|
352
|
+
Type.String({ description: "Concise handoff for the next phase: state, open risks, next action." }),
|
|
353
|
+
),
|
|
354
|
+
}),
|
|
355
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
356
|
+
if (!orchestrationActive) {
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text", text: "phase_result is only used during /plan orchestration; ignored here." }],
|
|
359
|
+
details: undefined,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const raw = params as { verdict?: StructuredPhaseResult["verdict"]; blocking?: string; handoff?: string };
|
|
363
|
+
// Merge only the fields this call actually set, so a phase that emits
|
|
364
|
+
// its result across several calls (verdict first, handoff later — a
|
|
365
|
+
// common LLM pattern) does not erase earlier fields. Start fresh when
|
|
366
|
+
// the previous result belonged to a different phase.
|
|
367
|
+
const defined: StructuredPhaseResult = {};
|
|
368
|
+
if (raw.verdict !== undefined) defined.verdict = raw.verdict;
|
|
369
|
+
if (typeof raw.blocking === "string") defined.blocking = raw.blocking;
|
|
370
|
+
if (typeof raw.handoff === "string") defined.handoff = raw.handoff;
|
|
371
|
+
const samePhase = currentPhaseResultKey === (currentPhase?.key ?? null);
|
|
372
|
+
currentPhaseResult = { ...(samePhase && currentPhaseResult ? currentPhaseResult : {}), ...defined };
|
|
373
|
+
currentPhaseResultKey = currentPhase?.key ?? null;
|
|
374
|
+
const parts = [
|
|
375
|
+
raw.verdict ? `verdict ${raw.verdict}` : null,
|
|
376
|
+
raw.blocking?.trim() ? "blocking findings" : null,
|
|
377
|
+
raw.handoff?.trim() ? "handoff" : null,
|
|
378
|
+
].filter(Boolean);
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: "text", text: `Phase result recorded${parts.length ? ` (${parts.join(", ")})` : ""}.` }],
|
|
381
|
+
details: undefined,
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// A phase's report file is not always named <key>-<ts>.md: PLAN writes its
|
|
387
|
+
// handoff into todo-<ts>.md and CODE into progress-<ts>.md. Map key -> file
|
|
388
|
+
// stem so the text fallback for HANDOFF/BLOCKING actually finds them.
|
|
389
|
+
const REPORT_FILE_STEM: Record<string, string> = { plan: "todo", code: "progress" };
|
|
261
390
|
|
|
262
|
-
/** Read a phase's report file (.phi/plans/<
|
|
391
|
+
/** Read a phase's report file (.phi/plans/<stem>-<ts>.md). Null-safe. */
|
|
263
392
|
function readPhaseReport(key: string, ts: string): string | null {
|
|
264
393
|
try {
|
|
265
|
-
const
|
|
394
|
+
const stem = REPORT_FILE_STEM[key] ?? key;
|
|
395
|
+
const f = join(process.cwd(), ".phi", "plans", `${stem}-${ts}.md`);
|
|
266
396
|
if (existsSync(f)) return readFileSync(f, "utf-8");
|
|
267
|
-
} catch {
|
|
397
|
+
} catch {
|
|
398
|
+
/* ignore */
|
|
399
|
+
}
|
|
268
400
|
return null;
|
|
269
401
|
}
|
|
270
402
|
|
|
@@ -277,13 +409,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
277
409
|
}
|
|
278
410
|
function writeCheckpoint(nextBaseIndex: number): void {
|
|
279
411
|
try {
|
|
280
|
-
if (nextBaseIndex >= 5) {
|
|
412
|
+
if (nextBaseIndex >= 5) {
|
|
413
|
+
clearCheckpoint();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
281
416
|
writeFileSync(
|
|
282
417
|
checkpointPath(),
|
|
283
418
|
JSON.stringify({ description: currentDescription, ts: currentRunTs, nextBaseIndex }, null, 2),
|
|
284
419
|
"utf-8",
|
|
285
420
|
);
|
|
286
|
-
} catch {
|
|
421
|
+
} catch {
|
|
422
|
+
/* best effort */
|
|
423
|
+
}
|
|
287
424
|
}
|
|
288
425
|
function readCheckpoint(): { description: string; ts: string; nextBaseIndex: number } | null {
|
|
289
426
|
try {
|
|
@@ -293,14 +430,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
293
430
|
if (typeof c?.description === "string" && typeof c?.ts === "string" && typeof c?.nextBaseIndex === "number") {
|
|
294
431
|
return c;
|
|
295
432
|
}
|
|
296
|
-
} catch {
|
|
433
|
+
} catch {
|
|
434
|
+
/* ignore */
|
|
435
|
+
}
|
|
297
436
|
return null;
|
|
298
437
|
}
|
|
299
438
|
function clearCheckpoint(): void {
|
|
300
439
|
try {
|
|
301
440
|
const p = checkpointPath();
|
|
302
441
|
if (existsSync(p)) unlinkSync(p);
|
|
303
|
-
} catch {
|
|
442
|
+
} catch {
|
|
443
|
+
/* best effort */
|
|
444
|
+
}
|
|
304
445
|
}
|
|
305
446
|
|
|
306
447
|
/**
|
|
@@ -344,7 +485,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
344
485
|
const fmMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
|
|
345
486
|
const body = (fmMatch ? fmMatch[1] : content).trim();
|
|
346
487
|
if (body) return body;
|
|
347
|
-
} catch {
|
|
488
|
+
} catch {
|
|
489
|
+
/* try next dir */
|
|
490
|
+
}
|
|
348
491
|
}
|
|
349
492
|
return null;
|
|
350
493
|
}
|
|
@@ -358,7 +501,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
358
501
|
let routing: any = { routes: {}, default: { model: "default" } };
|
|
359
502
|
try {
|
|
360
503
|
routing = JSON.parse(readFileSync(routingPath, "utf-8"));
|
|
361
|
-
} catch {
|
|
504
|
+
} catch {
|
|
505
|
+
/* no routing config */
|
|
506
|
+
}
|
|
362
507
|
|
|
363
508
|
function getModel(routeKey: string): { preferred: string; fallback: string } {
|
|
364
509
|
const route = routing.routes?.[routeKey];
|
|
@@ -377,9 +522,10 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
377
522
|
const ts = tsOverride || timestamp();
|
|
378
523
|
currentRunTs = ts; // shared by all phase report file names this run (or resumed)
|
|
379
524
|
// Inject runtime info so agents can adapt to the host OS
|
|
380
|
-
const shellNote =
|
|
381
|
-
|
|
382
|
-
|
|
525
|
+
const shellNote =
|
|
526
|
+
process.platform === "win32"
|
|
527
|
+
? `\nShell: bash (Git Bash), NOT cmd.exe. Always use Unix syntax: rm not del, test -f not if exist, / not \\\\`
|
|
528
|
+
: "";
|
|
383
529
|
const runtimeInfo = `\n\nRuntime: ${process.platform} (${process.arch})${shellNote}`;
|
|
384
530
|
|
|
385
531
|
// Embed the prompt-architect skill so the PLAN phase applies it to every
|
|
@@ -393,9 +539,13 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
393
539
|
|
|
394
540
|
const phases: OrchestratorPhase[] = [
|
|
395
541
|
{
|
|
396
|
-
key: "explore",
|
|
542
|
+
key: "explore",
|
|
543
|
+
label: "🔍 Phase 1 — EXPLORE",
|
|
544
|
+
model: explore.preferred,
|
|
545
|
+
fallback: explore.fallback,
|
|
397
546
|
agent: loadAgentDef("explore"),
|
|
398
|
-
instruction:
|
|
547
|
+
instruction:
|
|
548
|
+
`You are the EXPLORE agent. Analyze the project requirements and existing codebase.
|
|
399
549
|
|
|
400
550
|
**FIRST ACTION (MANDATORY):** Call \`memory_search\` with project-relevant keywords to check for prior context.
|
|
401
551
|
|
|
@@ -448,12 +598,18 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
|
|
|
448
598
|
- Every function fully implemented
|
|
449
599
|
- Follow existing patterns if codebase exists
|
|
450
600
|
- [Any other specific constraints]
|
|
451
|
-
\`\`\`` +
|
|
601
|
+
\`\`\`` +
|
|
602
|
+
runtimeInfo +
|
|
603
|
+
COMMON_PHASE_RULES,
|
|
452
604
|
},
|
|
453
605
|
{
|
|
454
|
-
key: "plan",
|
|
606
|
+
key: "plan",
|
|
607
|
+
label: "📐 Phase 2 — PLAN",
|
|
608
|
+
model: plan.preferred,
|
|
609
|
+
fallback: plan.fallback,
|
|
455
610
|
agent: loadAgentDef("plan"),
|
|
456
|
-
instruction:
|
|
611
|
+
instruction:
|
|
612
|
+
`You are the PLAN agent. Design the architecture and create a detailed task list.
|
|
457
613
|
|
|
458
614
|
**Context Retrieval:**
|
|
459
615
|
1. Use \`ontology_query\` to retrieve all entities and relations from Phase 1
|
|
@@ -496,12 +652,18 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
|
|
|
496
652
|
- Dependencies: Task 1
|
|
497
653
|
\`\`\`
|
|
498
654
|
${promptArchitectSection}
|
|
499
|
-
Before finishing, use \`memory_write\` to save your plan summary with relevant tags for future reference.` +
|
|
655
|
+
Before finishing, use \`memory_write\` to save your plan summary with relevant tags for future reference.` +
|
|
656
|
+
runtimeInfo +
|
|
657
|
+
COMMON_PHASE_RULES,
|
|
500
658
|
},
|
|
501
659
|
{
|
|
502
|
-
key: "code",
|
|
660
|
+
key: "code",
|
|
661
|
+
label: "💻 Phase 3 — CODE",
|
|
662
|
+
model: code.preferred,
|
|
663
|
+
fallback: code.fallback,
|
|
503
664
|
agent: loadAgentDef("code"),
|
|
504
|
-
instruction:
|
|
665
|
+
instruction:
|
|
666
|
+
`You are the CODE agent. Implement the complete project.
|
|
505
667
|
|
|
506
668
|
**Context Retrieval:**
|
|
507
669
|
1. Use \`memory_search\` with project keywords to find notes from previous phases
|
|
@@ -545,12 +707,18 @@ After implementation, use \`memory_write\` to save a summary of what was built,
|
|
|
545
707
|
**CRITICAL RULES:**
|
|
546
708
|
- Write ONE file per tool call — NEVER combine multiple files in a single response
|
|
547
709
|
- 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\`` +
|
|
710
|
+
- After writing ALL files, verify they exist with a single \`find . -name '*.ts' -o -name '*.js' -o -name '*.html' | sort\`` +
|
|
711
|
+
runtimeInfo +
|
|
712
|
+
COMMON_PHASE_RULES,
|
|
549
713
|
},
|
|
550
714
|
{
|
|
551
|
-
key: "test",
|
|
715
|
+
key: "test",
|
|
716
|
+
label: "🧪 Phase 4 — TEST",
|
|
717
|
+
model: test.preferred,
|
|
718
|
+
fallback: test.fallback,
|
|
552
719
|
agent: loadAgentDef("test"),
|
|
553
|
-
instruction:
|
|
720
|
+
instruction:
|
|
721
|
+
`You are the TEST agent. Verify the implementation.
|
|
554
722
|
|
|
555
723
|
**Context Retrieval:**
|
|
556
724
|
1. Use \`memory_search\` to find implementation notes from the CODE phase
|
|
@@ -569,6 +737,7 @@ After implementation, use \`memory_write\` to save a summary of what was built,
|
|
|
569
737
|
- Running \`npm test\` alone is NOT sufficient proof for a feature.
|
|
570
738
|
**Step 4:** Fix any real errors you find (root cause, not a workaround).
|
|
571
739
|
**Step 5:** Write test results to \`.phi/plans/test-${ts}.md\`, starting the file with a VERDICT line.
|
|
740
|
+
**Step 6 (MANDATORY):** Call the \`phase_result\` tool with your \`verdict\` (PASS/FAIL/BLOCKED) and a concise \`handoff\`. This is how the orchestrator reads your outcome exactly; the report file above is the human-readable copy.
|
|
572
741
|
|
|
573
742
|
**Test report format (the FIRST line MUST be the verdict):**
|
|
574
743
|
\`\`\`markdown
|
|
@@ -609,12 +778,18 @@ $ <command actually executed>
|
|
|
609
778
|
|
|
610
779
|
After testing, use \`memory_write\` to save test results, bugs found, and lessons learned.
|
|
611
780
|
|
|
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.` +
|
|
781
|
+
**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.` +
|
|
782
|
+
runtimeInfo +
|
|
783
|
+
COMMON_PHASE_RULES,
|
|
613
784
|
},
|
|
614
785
|
{
|
|
615
|
-
key: "review",
|
|
786
|
+
key: "review",
|
|
787
|
+
label: "🔍 Phase 5 — REVIEW",
|
|
788
|
+
model: review.preferred,
|
|
789
|
+
fallback: review.fallback,
|
|
616
790
|
agent: loadAgentDef("review"),
|
|
617
|
-
instruction:
|
|
791
|
+
instruction:
|
|
792
|
+
`You are the REVIEW agent. Final quality review.
|
|
618
793
|
|
|
619
794
|
**FIRST ACTIONS (MANDATORY — do these before anything else):**
|
|
620
795
|
1. Call \`memory_search\` to find all notes from previous phases (explore, plan, code, test)
|
|
@@ -636,6 +811,7 @@ After testing, use \`memory_write\` to save test results, bugs found, and lesson
|
|
|
636
811
|
- **REFUTED** - drop it. Only refute if you can quote the line that proves it cannot happen (a type, guard, or constant). When unsure, keep it PLAUSIBLE.
|
|
637
812
|
Keep only CONFIRMED + PLAUSIBLE. A finding with no concrete failure scenario is noise: drop it.
|
|
638
813
|
**Step 4:** Write \`.phi/plans/review-${ts}.md\`, starting with the VERDICT line.
|
|
814
|
+
**Step 5 (MANDATORY):** Call the \`phase_result\` tool with your \`verdict\` (PASS/FAIL), the \`blocking\` findings (one per line, empty when none), and a short \`handoff\`. A FAIL here drives the one bounded fix→re-review cycle, so the orchestrator must read it exactly — do not rely on markdown parsing alone.
|
|
639
815
|
|
|
640
816
|
**Final report format (the FIRST line MUST be the verdict):**
|
|
641
817
|
\`\`\`markdown
|
|
@@ -666,10 +842,14 @@ Tag the note with relevant keywords for vector search.
|
|
|
666
842
|
**Ontology enrichment:** After your review, use \`ontology_add\` to save your key findings:
|
|
667
843
|
- Add a "review-report" entity with type "Document"
|
|
668
844
|
- Add relations to the project: "reviews" → project, quality score as entity property
|
|
669
|
-
- Save any new architectural decisions or patterns discovered` +
|
|
845
|
+
- Save any new architectural decisions or patterns discovered` +
|
|
846
|
+
runtimeInfo +
|
|
847
|
+
COMMON_PHASE_RULES,
|
|
670
848
|
},
|
|
671
849
|
];
|
|
672
|
-
phases.forEach((p, i) => {
|
|
850
|
+
phases.forEach((p, i) => {
|
|
851
|
+
p.baseIndex = i;
|
|
852
|
+
});
|
|
673
853
|
return phases;
|
|
674
854
|
}
|
|
675
855
|
|
|
@@ -733,7 +913,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
733
913
|
/**
|
|
734
914
|
* Activate agent for a phase: set system prompt + restrict tools.
|
|
735
915
|
*/
|
|
736
|
-
function activateAgent(phase: OrchestratorPhase,
|
|
916
|
+
function activateAgent(phase: OrchestratorPhase, _ctx: any) {
|
|
737
917
|
// Fallback capture: savedTools is normally taken in the /plan handler at
|
|
738
918
|
// orchestration start, but capture it here too so restoration is always defined.
|
|
739
919
|
if (!savedTools) {
|
|
@@ -745,22 +925,29 @@ Tag the note with relevant keywords for vector search.
|
|
|
745
925
|
activeAgentPrompt = agentDef.systemPrompt;
|
|
746
926
|
// Restrict tools to agent's allowed tools
|
|
747
927
|
if (agentDef.tools.length > 0) {
|
|
748
|
-
// Always include memory tools in orchestration phases
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
928
|
+
// Always include the memory tools and phase_result in orchestration phases.
|
|
929
|
+
const forcedTools = [
|
|
930
|
+
"memory_search",
|
|
931
|
+
"memory_write",
|
|
932
|
+
"memory_read",
|
|
933
|
+
"ontology_add",
|
|
934
|
+
"ontology_query",
|
|
935
|
+
"phase_result",
|
|
936
|
+
];
|
|
937
|
+
const agentTools = [...agentDef.tools, ...forcedTools.filter((t) => !agentDef.tools.includes(t))];
|
|
938
|
+
_activeAgentTools = agentTools;
|
|
752
939
|
pi.setActiveTools(agentTools);
|
|
753
940
|
} else if (savedTools) {
|
|
754
941
|
// Agent with no tool restriction: reset to the full toolset so the
|
|
755
942
|
// previous phase's narrower restriction does not leak into this one.
|
|
756
|
-
|
|
943
|
+
_activeAgentTools = null;
|
|
757
944
|
pi.setActiveTools(savedTools);
|
|
758
945
|
}
|
|
759
946
|
} else {
|
|
760
947
|
// No agent for this phase: restore the full toolset, otherwise the prior
|
|
761
948
|
// phase's restriction would persist.
|
|
762
949
|
activeAgentPrompt = null;
|
|
763
|
-
|
|
950
|
+
_activeAgentTools = null;
|
|
764
951
|
if (savedTools) {
|
|
765
952
|
pi.setActiveTools(savedTools);
|
|
766
953
|
}
|
|
@@ -772,7 +959,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
772
959
|
*/
|
|
773
960
|
function deactivateAgent() {
|
|
774
961
|
activeAgentPrompt = null;
|
|
775
|
-
|
|
962
|
+
_activeAgentTools = null;
|
|
776
963
|
if (savedTools) {
|
|
777
964
|
pi.setActiveTools(savedTools);
|
|
778
965
|
savedTools = null;
|
|
@@ -796,11 +983,16 @@ Tag the note with relevant keywords for vector search.
|
|
|
796
983
|
setOrchestrationActive(false);
|
|
797
984
|
phasePending = false;
|
|
798
985
|
deactivateAgent();
|
|
799
|
-
if (phaseTimeoutId) {
|
|
986
|
+
if (phaseTimeoutId) {
|
|
987
|
+
clearTimeout(phaseTimeoutId);
|
|
988
|
+
phaseTimeoutId = null;
|
|
989
|
+
}
|
|
800
990
|
if (originalModel) {
|
|
801
991
|
const toRestore = originalModel;
|
|
802
992
|
originalModel = null;
|
|
803
|
-
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
993
|
+
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
994
|
+
/* best effort */
|
|
995
|
+
});
|
|
804
996
|
}
|
|
805
997
|
}
|
|
806
998
|
|
|
@@ -815,18 +1007,26 @@ Tag the note with relevant keywords for vector search.
|
|
|
815
1007
|
if (originalModel) {
|
|
816
1008
|
const toRestore = originalModel;
|
|
817
1009
|
originalModel = null;
|
|
818
|
-
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
1010
|
+
Promise.resolve(pi.setModel(toRestore)).catch(() => {
|
|
1011
|
+
/* best effort */
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
if (phaseTimeoutId) {
|
|
1015
|
+
clearTimeout(phaseTimeoutId);
|
|
1016
|
+
phaseTimeoutId = null;
|
|
819
1017
|
}
|
|
820
|
-
if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
|
|
821
1018
|
// Generate global final summary using the real phase counters
|
|
822
1019
|
const elapsed = phaseStartTime ? Math.round((Date.now() - phaseStartTime) / 1000) : 0;
|
|
823
1020
|
const minutes = Math.floor(elapsed / 60);
|
|
824
1021
|
const seconds = elapsed % 60;
|
|
825
1022
|
const skippedNote = skippedPhases ? ` (${skippedPhases} skipped on timeout)` : "";
|
|
826
|
-
ctx.ui.notify(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1023
|
+
ctx.ui.notify(
|
|
1024
|
+
`\n📊 **Orchestration Summary**\n` +
|
|
1025
|
+
` Phases: ${completedPhases}/5 completed${skippedNote}\n` +
|
|
1026
|
+
` Duration: ${minutes}m ${seconds}s\n` +
|
|
1027
|
+
` Check \`.phi/plans/\` for all reports`,
|
|
1028
|
+
"info",
|
|
1029
|
+
);
|
|
830
1030
|
const allDone = completedPhases >= 5 && skippedPhases === 0;
|
|
831
1031
|
const doneMsg = allDone
|
|
832
1032
|
? `\n✅ **All 5 phases complete!**`
|
|
@@ -835,7 +1035,11 @@ Tag the note with relevant keywords for vector search.
|
|
|
835
1035
|
ctx.ui.notify(doneMsg, "info");
|
|
836
1036
|
} catch {
|
|
837
1037
|
// Fallback: inject completion message into conversation stream
|
|
838
|
-
try {
|
|
1038
|
+
try {
|
|
1039
|
+
pi.sendUserMessage(`Orchestration finished: ${completedPhases}/5 phases completed${skippedNote}.`);
|
|
1040
|
+
} catch {
|
|
1041
|
+
/* best effort */
|
|
1042
|
+
}
|
|
839
1043
|
}
|
|
840
1044
|
return;
|
|
841
1045
|
}
|
|
@@ -843,6 +1047,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
843
1047
|
const phase = phaseQueue.shift()!;
|
|
844
1048
|
phasePending = true;
|
|
845
1049
|
currentPhase = phase;
|
|
1050
|
+
// New phase starts with no structured result; it is set only if this
|
|
1051
|
+
// phase's agent calls phase_result.
|
|
1052
|
+
currentPhaseResult = null;
|
|
1053
|
+
currentPhaseResultKey = null;
|
|
846
1054
|
|
|
847
1055
|
switchModelForPhase(phase, ctx).then(({ modelId, warning }) => {
|
|
848
1056
|
activateAgent(phase, ctx);
|
|
@@ -862,7 +1070,11 @@ Tag the note with relevant keywords for vector search.
|
|
|
862
1070
|
// race a still-streaming phase. Mark it internal so the resulting
|
|
863
1071
|
// agent_end is not treated as a user cancellation.
|
|
864
1072
|
internalAbort = true;
|
|
865
|
-
try {
|
|
1073
|
+
try {
|
|
1074
|
+
ctx.abort();
|
|
1075
|
+
} catch {
|
|
1076
|
+
/* best effort */
|
|
1077
|
+
}
|
|
866
1078
|
// Retry the SAME phase once on the fallback model before skipping:
|
|
867
1079
|
// a timed-out phase usually means the model/route stalled, and a
|
|
868
1080
|
// different family often gets through (cap = one retry per phase).
|
|
@@ -870,7 +1082,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
870
1082
|
phase.retried = true;
|
|
871
1083
|
phase.useFallback = true;
|
|
872
1084
|
phaseQueue.unshift(phase);
|
|
873
|
-
ctx.ui.notify(
|
|
1085
|
+
ctx.ui.notify(
|
|
1086
|
+
`\n⏰ **Phase timed out** (${MAX_PHASE_DURATION_MS / 60000} min) — retrying once on the fallback model.`,
|
|
1087
|
+
"warning",
|
|
1088
|
+
);
|
|
874
1089
|
} else {
|
|
875
1090
|
skippedPhases++;
|
|
876
1091
|
ctx.ui.notify(`\n⏰ **Phase timed out again** — skipping to next phase.`, "warning");
|
|
@@ -883,9 +1098,9 @@ Tag the note with relevant keywords for vector search.
|
|
|
883
1098
|
|
|
884
1099
|
// ─── System Prompt Injection — Agent personas ────────────────────
|
|
885
1100
|
|
|
886
|
-
pi.on("before_agent_start", async (
|
|
1101
|
+
pi.on("before_agent_start", async (_event, _ctx) => {
|
|
887
1102
|
if (!orchestrationActive || !activeAgentPrompt) {
|
|
888
|
-
return {
|
|
1103
|
+
return {};
|
|
889
1104
|
}
|
|
890
1105
|
// Replace system prompt with the active agent's prompt
|
|
891
1106
|
return { systemPrompt: activeAgentPrompt };
|
|
@@ -905,15 +1120,21 @@ Tag the note with relevant keywords for vector search.
|
|
|
905
1120
|
// An internal abort (phase timeout) re-emits agent_end; that transition was
|
|
906
1121
|
// already handled by the timeout. Consume the flag and ignore this event so
|
|
907
1122
|
// it is not mistaken for a user cancellation below.
|
|
908
|
-
if (internalAbort) {
|
|
1123
|
+
if (internalAbort) {
|
|
1124
|
+
internalAbort = false;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const messages = event.messages || [];
|
|
1129
|
+
// Analyze what happened, read the phase's report, and let the pure state
|
|
1130
|
+
// machine (providers/phase-machine.ts, unit tested) decide the transition.
|
|
1131
|
+
// This hook only interprets the decision: notifications, queue edits,
|
|
1132
|
+
// checkpoints, model switches.
|
|
1133
|
+
const analysis = analyzePhaseMessages(messages);
|
|
909
1134
|
|
|
910
1135
|
// 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) {
|
|
1136
|
+
// must NOT be treated as phase completion.
|
|
1137
|
+
if (analysis.userAborted) {
|
|
917
1138
|
ctx.ui.notify(`\n🛑 **Orchestration cancelled** by user. Remaining phases skipped.`, "warning");
|
|
918
1139
|
stopOrchestration();
|
|
919
1140
|
return;
|
|
@@ -929,163 +1150,123 @@ Tag the note with relevant keywords for vector search.
|
|
|
929
1150
|
}
|
|
930
1151
|
|
|
931
1152
|
// 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
|
-
}
|
|
1153
|
+
if (phaseTimeoutId) {
|
|
1154
|
+
clearTimeout(phaseTimeoutId);
|
|
1155
|
+
phaseTimeoutId = null;
|
|
980
1156
|
}
|
|
981
1157
|
|
|
982
|
-
//
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1158
|
+
// Resolve the phase outcome: prefer the structured phase_result emission
|
|
1159
|
+
// (exact) and fall back per-field to the regex-scraped markdown report.
|
|
1160
|
+
// Only trust a structured result stamped with THIS phase's key — a stale
|
|
1161
|
+
// result from a previous run or a late tool call is ignored.
|
|
1162
|
+
const reportContent = currentPhase ? readPhaseReport(currentPhase.key, currentRunTs) : null;
|
|
1163
|
+
const structuredForThisPhase = currentPhaseResultKey === (currentPhase?.key ?? null) ? currentPhaseResult : null;
|
|
1164
|
+
const outcome = resolvePhaseOutcome(structuredForThisPhase, reportContent);
|
|
1165
|
+
const verdict = outcome.verdict;
|
|
1166
|
+
const decision = decidePhaseTransition({
|
|
1167
|
+
analysis,
|
|
1168
|
+
phase: currentPhase ? { key: currentPhase.key, retried: currentPhase.retried } : null,
|
|
1169
|
+
verdict,
|
|
1170
|
+
reviewFixRounds,
|
|
1171
|
+
maxToolCallsPerPhase: MAX_TOOL_CALLS_PER_PHASE,
|
|
986
1172
|
});
|
|
987
|
-
|
|
988
|
-
if (
|
|
989
|
-
|
|
1173
|
+
|
|
1174
|
+
if (decision.action === "stop") {
|
|
1175
|
+
// Only a genuine auth failure (401) is fatal — abort the whole workflow.
|
|
1176
|
+
ctx.ui.notify(
|
|
1177
|
+
`\n❌ **Orchestrator aborted:** API authentication error (401)\nCheck your API key and model configuration.`,
|
|
1178
|
+
"error",
|
|
1179
|
+
);
|
|
990
1180
|
stopOrchestration();
|
|
991
1181
|
return;
|
|
992
1182
|
}
|
|
993
1183
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
|
|
1184
|
+
if (decision.action === "retry-fallback" && currentPhase) {
|
|
1185
|
+
// Transient provider/proxy failure (timeout text / 5xx / 429 / broken
|
|
1186
|
+
// JSON tool call) that is NOT a 401: retry the SAME phase once on the
|
|
1187
|
+
// fallback model (a different family often gets through).
|
|
999
1188
|
currentPhase.retried = true;
|
|
1000
1189
|
currentPhase.useFallback = true;
|
|
1001
1190
|
phaseQueue.unshift(currentPhase);
|
|
1002
1191
|
phasePending = false;
|
|
1003
|
-
ctx.ui.notify(
|
|
1192
|
+
ctx.ui.notify(
|
|
1193
|
+
`\n🔁 **Transient provider error** in ${currentPhase.label} — retrying once on the fallback model.`,
|
|
1194
|
+
"warning",
|
|
1195
|
+
);
|
|
1004
1196
|
sendNextPhase(ctx);
|
|
1005
1197
|
return;
|
|
1006
1198
|
}
|
|
1007
1199
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1200
|
+
if (decision.action === "pause-blocked" && currentPhase) {
|
|
1201
|
+
ctx.ui.notify(
|
|
1202
|
+
`\n⏸️ **${currentPhase.label} reported BLOCKED.** Pausing /plan — see \`.phi/plans/${currentPhase.key}-${currentRunTs}.md\`. Fix the blocker and re-run \`/plan\` to continue.`,
|
|
1203
|
+
"warning",
|
|
1204
|
+
);
|
|
1205
|
+
stopOrchestration();
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (decision.action === "continue" && decision.zeroToolCalls && messages.length > 0) {
|
|
1210
|
+
// A phase that made 0 tool calls is NOT fatal: a model may legitimately
|
|
1211
|
+
// answer with text only (e.g. an inline plan). Warn and continue rather
|
|
1212
|
+
// than killing phases 2-5, mirroring the graceful phase-timeout path.
|
|
1213
|
+
ctx.ui.notify(
|
|
1214
|
+
`\n⚠️ **Phase made 0 tool calls** — it may have responded with text only. Continuing to the next phase.`,
|
|
1215
|
+
"warning",
|
|
1216
|
+
);
|
|
1013
1217
|
if (phaseQueue.length > 0) {
|
|
1014
1218
|
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
1219
|
}
|
|
1016
1220
|
}
|
|
1017
1221
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1222
|
+
if (decision.action === "continue" && decision.missingVerdict && currentPhase) {
|
|
1223
|
+
// The model deviated from BOTH contract paths: it neither called
|
|
1224
|
+
// phase_result with a verdict nor wrote a parseable "VERDICT:" line.
|
|
1225
|
+
// Surface it instead of silently treating the phase as passed.
|
|
1226
|
+
ctx.ui.notify(
|
|
1227
|
+
`\n⚠️ **${currentPhase.label} reported no verdict** (no phase_result call and no VERDICT line in \`.phi/plans/${currentPhase.key}-${currentRunTs}.md\`) — treating it as passed. Check the report manually.`,
|
|
1228
|
+
"warning",
|
|
1229
|
+
);
|
|
1024
1230
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1041
|
-
// Prefer the phase's canonical "## HANDOFF" block (a deterministic text
|
|
1042
|
-
// contract written to its report file) over the heuristic summary; fall back
|
|
1043
|
-
// to the heuristic when the model did not write one.
|
|
1044
|
-
const reportContent = currentPhase ? readPhaseReport(currentPhase.key, currentRunTs) : null;
|
|
1045
|
-
const handoff = reportContent ? extractHandoff(reportContent) : "";
|
|
1046
|
-
const nextBrief = handoff
|
|
1047
|
-
? `Tool calls: ${toolCallCount}\n## HANDOFF (from ${currentPhase?.label || "previous phase"})\n${handoff}`
|
|
1048
|
-
: phaseSummary;
|
|
1231
|
+
|
|
1232
|
+
// Prefer the resolved HANDOFF (structured phase_result, else the report's
|
|
1233
|
+
// "## HANDOFF" block) over the heuristic summary; fall back to the
|
|
1234
|
+
// heuristic when neither was provided.
|
|
1235
|
+
const nextBrief = buildNextBrief(
|
|
1236
|
+
analysis,
|
|
1237
|
+
outcome.handoff,
|
|
1238
|
+
currentPhase?.label || "previous phase",
|
|
1239
|
+
MAX_TOOL_CALLS_PER_PHASE,
|
|
1240
|
+
);
|
|
1049
1241
|
if (nextBrief && phaseQueue.length > 0) {
|
|
1050
1242
|
phaseQueue[0].instruction += `\n\n**Previous phase summary:**\n${nextBrief}`;
|
|
1051
1243
|
}
|
|
1052
1244
|
|
|
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 */ }
|
|
1245
|
+
if (decision.action === "review-fix-cycle" && currentPhase) {
|
|
1246
|
+
// REVIEW FAIL: open ONE bounded fix -> re-review cycle.
|
|
1247
|
+
reviewFixRounds++;
|
|
1248
|
+
const blocking = outcome.blocking.slice(0, 4000);
|
|
1249
|
+
const fixPhase: OrchestratorPhase = {
|
|
1250
|
+
key: "code",
|
|
1251
|
+
label: "🔧 Fix — CODE (review remediation)",
|
|
1252
|
+
model: currentPhase.model,
|
|
1253
|
+
fallback: currentPhase.fallback,
|
|
1254
|
+
agent: loadAgentDef("code"),
|
|
1255
|
+
instruction:
|
|
1256
|
+
`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` +
|
|
1257
|
+
COMMON_PHASE_RULES,
|
|
1258
|
+
};
|
|
1259
|
+
const reReviewPhase: OrchestratorPhase = {
|
|
1260
|
+
...currentPhase,
|
|
1261
|
+
retried: false,
|
|
1262
|
+
useFallback: false,
|
|
1263
|
+
label: "🔍 Re-REVIEW (after fix)",
|
|
1264
|
+
};
|
|
1265
|
+
// Run next: fix, then re-review.
|
|
1266
|
+
phaseQueue.unshift(reReviewPhase);
|
|
1267
|
+
phaseQueue.unshift(fixPhase);
|
|
1268
|
+
ctx.ui.notify(`\n🔁 **REVIEW verdict: FAIL** — running one targeted fix + re-review cycle.`, "warning");
|
|
1269
|
+
}
|
|
1089
1270
|
|
|
1090
1271
|
// Checkpoint resume position after each base phase (synthetic fix/re-review
|
|
1091
1272
|
// phases have no baseIndex and do not advance it).
|
|
@@ -1132,11 +1313,18 @@ Tag the note with relevant keywords for vector search.
|
|
|
1132
1313
|
skippedPhases = 0;
|
|
1133
1314
|
reviewFixRounds = 0;
|
|
1134
1315
|
internalAbort = false;
|
|
1316
|
+
// Clear any structured result left over from a prior run so this
|
|
1317
|
+
// resumed phase's outcome is read fresh (see phase-result leak guard).
|
|
1318
|
+
currentPhaseResult = null;
|
|
1319
|
+
currentPhaseResultKey = null;
|
|
1135
1320
|
phaseStartTime = Date.now();
|
|
1136
1321
|
const firstResume = remaining[0];
|
|
1137
1322
|
currentPhase = firstResume;
|
|
1138
1323
|
phaseQueue = remaining.slice(1);
|
|
1139
|
-
ctx.ui.notify(
|
|
1324
|
+
ctx.ui.notify(
|
|
1325
|
+
`📋 **Resuming orchestration** at ${firstResume.label} (${remaining.length} phase(s) left)\n`,
|
|
1326
|
+
"info",
|
|
1327
|
+
);
|
|
1140
1328
|
const { modelId, warning } = await switchModelForPhase(firstResume, ctx);
|
|
1141
1329
|
activateAgent(firstResume, ctx);
|
|
1142
1330
|
ctx.ui.notify(`${firstResume.label} → \`${modelId}\``, "info");
|
|
@@ -1151,7 +1339,8 @@ Tag the note with relevant keywords for vector search.
|
|
|
1151
1339
|
const description = rawArgs.replace(/(^|\s)--fanout\b/g, " ").trim();
|
|
1152
1340
|
|
|
1153
1341
|
if (!description) {
|
|
1154
|
-
ctx.ui.notify(
|
|
1342
|
+
ctx.ui.notify(
|
|
1343
|
+
`**Usage:** \`/plan <project description>\` (or \`/plan --resume\` to continue a checkpointed run, \`/plan --fanout <desc>\` for parallel exploration)
|
|
1155
1344
|
|
|
1156
1345
|
**Examples:**
|
|
1157
1346
|
/plan Build a REST API for user authentication with JWT
|
|
@@ -1159,7 +1348,9 @@ Tag the note with relevant keywords for vector search.
|
|
|
1159
1348
|
/plan Add test coverage to the payment module
|
|
1160
1349
|
|
|
1161
1350
|
**Other commands:**
|
|
1162
|
-
/plans — List all plans`,
|
|
1351
|
+
/plans — List all plans`,
|
|
1352
|
+
"info",
|
|
1353
|
+
);
|
|
1163
1354
|
return;
|
|
1164
1355
|
}
|
|
1165
1356
|
|
|
@@ -1167,7 +1358,11 @@ Tag the note with relevant keywords for vector search.
|
|
|
1167
1358
|
await ensurePlansDir();
|
|
1168
1359
|
const ts = timestamp();
|
|
1169
1360
|
const specFile = `spec-${ts}.md`;
|
|
1170
|
-
await writeFile(
|
|
1361
|
+
await writeFile(
|
|
1362
|
+
join(plansDir, specFile),
|
|
1363
|
+
`# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`,
|
|
1364
|
+
"utf-8",
|
|
1365
|
+
);
|
|
1171
1366
|
|
|
1172
1367
|
// Build phases with model assignments + agent definitions
|
|
1173
1368
|
currentDescription = description;
|
|
@@ -1184,6 +1379,11 @@ Tag the note with relevant keywords for vector search.
|
|
|
1184
1379
|
skippedPhases = 0;
|
|
1185
1380
|
reviewFixRounds = 0;
|
|
1186
1381
|
internalAbort = false;
|
|
1382
|
+
// Clear any structured result left over from a previous /plan run in
|
|
1383
|
+
// this session; the first phase is launched directly (not via
|
|
1384
|
+
// sendNextPhase), so without this a stale verdict/handoff would leak.
|
|
1385
|
+
currentPhaseResult = null;
|
|
1386
|
+
currentPhaseResultKey = null;
|
|
1187
1387
|
const firstPhase = phases[0];
|
|
1188
1388
|
currentPhase = firstPhase;
|
|
1189
1389
|
|
|
@@ -1207,7 +1407,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
1207
1407
|
try {
|
|
1208
1408
|
const available = ctx.modelRegistry?.getAvailable?.() || [];
|
|
1209
1409
|
const exploreModelId = resolveModelRef(available, firstPhase.model)?.id || firstPhase.model;
|
|
1210
|
-
ctx.ui.notify(
|
|
1410
|
+
ctx.ui.notify(
|
|
1411
|
+
`\n🔭 **Parallel exploration** (read-only, up to 2 concurrent) — building a fuller map before phase 1...`,
|
|
1412
|
+
"info",
|
|
1413
|
+
);
|
|
1211
1414
|
const fan = await runExploreFanout(defaultExplorerSpecs(description), {
|
|
1212
1415
|
model: exploreModelId,
|
|
1213
1416
|
tools: READONLY_EXPLORER_TOOLS,
|
|
@@ -1217,14 +1420,22 @@ Tag the note with relevant keywords for vector search.
|
|
|
1217
1420
|
});
|
|
1218
1421
|
const okCount = fan.results.filter((r) => r.ok).length;
|
|
1219
1422
|
if (fan.merged.trim()) {
|
|
1220
|
-
firstPhase.instruction =
|
|
1221
|
-
|
|
1222
|
-
|
|
1423
|
+
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}`;
|
|
1424
|
+
ctx.ui.notify(
|
|
1425
|
+
`\n🔭 ${okCount} sub-exploration(s) merged into EXPLORE${fan.rateLimited ? " (rate limit hit — ran the rest sequentially)" : ""}.`,
|
|
1426
|
+
"info",
|
|
1427
|
+
);
|
|
1223
1428
|
} else {
|
|
1224
|
-
ctx.ui.notify(
|
|
1429
|
+
ctx.ui.notify(
|
|
1430
|
+
`\n🔭 Parallel exploration returned nothing usable${fan.rateLimited ? " (rate-limited)" : ""} — running the normal single-agent EXPLORE.`,
|
|
1431
|
+
"warning",
|
|
1432
|
+
);
|
|
1225
1433
|
}
|
|
1226
1434
|
} catch {
|
|
1227
|
-
ctx.ui.notify(
|
|
1435
|
+
ctx.ui.notify(
|
|
1436
|
+
`\n🔭 Parallel exploration skipped (error) — running the normal single-agent EXPLORE.`,
|
|
1437
|
+
"warning",
|
|
1438
|
+
);
|
|
1228
1439
|
}
|
|
1229
1440
|
}
|
|
1230
1441
|
|
|
@@ -1261,7 +1472,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
1261
1472
|
}
|
|
1262
1473
|
|
|
1263
1474
|
const files = await readdir(plansDir);
|
|
1264
|
-
const specs = files
|
|
1475
|
+
const specs = files
|
|
1476
|
+
.filter((f) => f.startsWith("spec-") && f.endsWith(".md"))
|
|
1477
|
+
.sort()
|
|
1478
|
+
.reverse();
|
|
1265
1479
|
|
|
1266
1480
|
if (specs.length === 0) {
|
|
1267
1481
|
ctx.ui.notify("No plans found.", "info");
|