@ilya-lesikov/pi-pi 0.1.0 → 0.3.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/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +1 -3
- package/3p/pi-plannotator/packages/shared/review-core.ts +0 -2
- package/3p/pi-subagents/src/index.ts +9 -1
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +4 -1
- package/extensions/orchestrator/agents/planner.ts +13 -2
- package/extensions/orchestrator/agents/tool-routing.ts +9 -0
- package/extensions/orchestrator/command-handlers.test.ts +22 -25
- package/extensions/orchestrator/config.test.ts +1 -0
- package/extensions/orchestrator/config.ts +3 -1
- package/extensions/orchestrator/custom-footer.ts +146 -0
- package/extensions/orchestrator/event-handlers.test.ts +1 -0
- package/extensions/orchestrator/event-handlers.ts +100 -155
- package/extensions/orchestrator/flant-infra.ts +7 -2
- package/extensions/orchestrator/integration.test.ts +3 -1
- package/extensions/orchestrator/orchestrator.test.ts +1 -0
- package/extensions/orchestrator/orchestrator.ts +27 -4
- package/extensions/orchestrator/phases/machine.ts +38 -0
- package/extensions/orchestrator/phases/review-task.ts +32 -0
- package/extensions/orchestrator/pp-menu.ts +510 -72
- package/extensions/orchestrator/state.ts +8 -9
- package/extensions/orchestrator/usage-tracker.ts +309 -0
- package/package.json +1 -1
|
@@ -24,9 +24,13 @@ import { spawnCodeReviewers } from "./phases/review.js";
|
|
|
24
24
|
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
25
25
|
import { openPlannotator, waitForPlannotatorResult, cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
26
26
|
import { Orchestrator, deepReviewConfig, type ActiveTask } from "./orchestrator.js";
|
|
27
|
+
import { createCustomFooter, setFooterContext, setFooterTracker } from "./custom-footer.js";
|
|
28
|
+
import { createUsageTracker, dumpUsageSummary, loadUsageSummary, type UsageTracker } from "./usage-tracker.js";
|
|
27
29
|
import { askUser } from "../../3p/pi-ask-user/index.js";
|
|
28
30
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
31
|
|
|
32
|
+
const USAGE_TRACKER_KEY = Symbol.for("pi-pi:usage-tracker");
|
|
33
|
+
|
|
30
34
|
export async function detectDefaultBranch(pi: ExtensionAPI, cwd: string, config?: { diffBaseBranch?: string }): Promise<string> {
|
|
31
35
|
if (config?.diffBaseBranch) return config.diffBaseBranch;
|
|
32
36
|
try {
|
|
@@ -46,17 +50,6 @@ export async function detectDefaultBranch(pi: ExtensionAPI, cwd: string, config?
|
|
|
46
50
|
return "main";
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
export async function detectRepoCwd(pi: ExtensionAPI, modifiedFiles: Set<string>, fallbackCwd: string): Promise<string> {
|
|
50
|
-
if (modifiedFiles.size === 0) return fallbackCwd;
|
|
51
|
-
const firstFile = [...modifiedFiles][0];
|
|
52
|
-
const dir = resolve(firstFile, "..");
|
|
53
|
-
try {
|
|
54
|
-
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: dir, timeout: 5000 });
|
|
55
|
-
if (result.code === 0 && result.stdout.trim()) return result.stdout.trim();
|
|
56
|
-
} catch {}
|
|
57
|
-
return fallbackCwd;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
53
|
export async function selectOption(ctx: any, question: string, options: string[]): Promise<string | undefined> {
|
|
61
54
|
const result = await askUser(ctx, {
|
|
62
55
|
question,
|
|
@@ -69,12 +62,6 @@ export async function selectOption(ctx: any, question: string, options: string[]
|
|
|
69
62
|
return result.selections[0];
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
function setStep(orchestrator: Orchestrator, step: string): void {
|
|
73
|
-
if (!orchestrator.active) return;
|
|
74
|
-
orchestrator.active.state.step = step;
|
|
75
|
-
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
65
|
function tryCompleteReviewCycle(orchestrator: Orchestrator): void {
|
|
79
66
|
if (
|
|
80
67
|
!orchestrator.active?.state.reviewCycle ||
|
|
@@ -163,7 +150,7 @@ export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kin
|
|
|
163
150
|
orchestrator.active.state.step = "synthesize";
|
|
164
151
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
165
152
|
const feedback = result.feedback ? `\n\nFeedback:\n${result.feedback}` : "";
|
|
166
|
-
return `Plannotator requested changes.${feedback}\n\
|
|
153
|
+
return `Plannotator requested changes.${feedback}\n\nAddress the user's feedback. If the feedback contains questions, answer them. If it requests changes, make the changes. Then call pp_phase_complete when done.`;
|
|
167
154
|
}
|
|
168
155
|
|
|
169
156
|
orchestrator.active.state.reviewCycle = null;
|
|
@@ -214,13 +201,13 @@ export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kin
|
|
|
214
201
|
return `Started review cycle pass ${pass} (${kind}). Awaiting reviewers.`;
|
|
215
202
|
}
|
|
216
203
|
|
|
217
|
-
export function stopTask(orchestrator: Orchestrator): string {
|
|
204
|
+
export async function stopTask(orchestrator: Orchestrator): Promise<string> {
|
|
218
205
|
if (!orchestrator.active) return "No active task.";
|
|
219
206
|
orchestrator.abortAllSubagents();
|
|
220
207
|
orchestrator.active.state.phase = "done";
|
|
221
208
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
222
209
|
const desc = orchestrator.active.description;
|
|
223
|
-
orchestrator.cleanupActive();
|
|
210
|
+
await orchestrator.cleanupActive();
|
|
224
211
|
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
225
212
|
taskStore?.clearAll?.();
|
|
226
213
|
return `Task "${desc}" stopped.`;
|
|
@@ -238,128 +225,6 @@ export function finalizeReviewCycle(task: ActiveTask): void {
|
|
|
238
225
|
saveTask(task.dir, task.state);
|
|
239
226
|
}
|
|
240
227
|
|
|
241
|
-
export async function runUserGateDialog(orchestrator: Orchestrator, ctx: any, summary: string): Promise<string> {
|
|
242
|
-
if (!orchestrator.active) return "No active task.";
|
|
243
|
-
if (orchestrator.userGatePending) return "A user-gate dialog is already in progress.";
|
|
244
|
-
orchestrator.userGatePending = true;
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
return await runUserGateDialogInner(orchestrator, ctx, summary);
|
|
248
|
-
} finally {
|
|
249
|
-
orchestrator.userGatePending = false;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function runUserGateDialogInner(orchestrator: Orchestrator, ctx: any, summary: string): Promise<string> {
|
|
254
|
-
if (!orchestrator.active) return "No active task.";
|
|
255
|
-
const pi = orchestrator.pi;
|
|
256
|
-
const phase = orchestrator.active.state.phase;
|
|
257
|
-
const task = orchestrator.active;
|
|
258
|
-
|
|
259
|
-
if (orchestrator.spawnedAgentIds.size > 0 || orchestrator.pendingSubagentSpawns > 0) {
|
|
260
|
-
const count = orchestrator.spawnedAgentIds.size + orchestrator.pendingSubagentSpawns;
|
|
261
|
-
return `${count} subagent(s) still running or spawning.`;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const byKind = task.state.reviewPassByKind ?? {};
|
|
265
|
-
const autoCount = byKind["auto"] ?? 0;
|
|
266
|
-
const deepCount = byKind["auto-deep"] ?? 0;
|
|
267
|
-
const autoLabel = autoCount > 0 ? `Review (pass ${autoCount + 1})` : "Review";
|
|
268
|
-
const deepLabel = deepCount > 0 ? `Deep review (pass ${deepCount + 1})` : "Deep review";
|
|
269
|
-
|
|
270
|
-
const continueMessage = "User wants to continue. Run /pp when ready to advance.";
|
|
271
|
-
|
|
272
|
-
if (phase === "brainstorm" && task.type === "implement") {
|
|
273
|
-
const choice = await selectOption(ctx, summary, ["Approve brainstorm", autoLabel, deepLabel, "Continue brainstorming", "Stop task"]);
|
|
274
|
-
finalizeReviewCycle(task);
|
|
275
|
-
if (choice === "Approve brainstorm") {
|
|
276
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
277
|
-
return result.ok ? "Brainstorm approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
278
|
-
}
|
|
279
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
280
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
281
|
-
if (choice === "Stop task") return stopTask(orchestrator);
|
|
282
|
-
setStep(orchestrator, "llm_work");
|
|
283
|
-
return continueMessage;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (phase === "brainstorm" && task.type === "brainstorm") {
|
|
287
|
-
const choice = await selectOption(ctx, summary, ["Approve brainstorm", autoLabel, deepLabel, "Continue brainstorming", "Finish brainstorming", "Stop task"]);
|
|
288
|
-
finalizeReviewCycle(task);
|
|
289
|
-
if (choice === "Approve brainstorm") {
|
|
290
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
291
|
-
return result.ok ? "Brainstorm approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
292
|
-
}
|
|
293
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
294
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
295
|
-
if (choice === "Finish brainstorming" || choice === "Stop task") return stopTask(orchestrator);
|
|
296
|
-
setStep(orchestrator, "llm_work");
|
|
297
|
-
return continueMessage;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (phase === "debug") {
|
|
301
|
-
const choice = await selectOption(ctx, summary, ["Approve debug", autoLabel, deepLabel, "Continue debugging", "Finish debugging", "Stop task"]);
|
|
302
|
-
finalizeReviewCycle(task);
|
|
303
|
-
if (choice === "Approve debug") {
|
|
304
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
305
|
-
return result.ok ? "Debug approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
306
|
-
}
|
|
307
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
308
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
309
|
-
if (choice === "Finish debugging" || choice === "Stop task") return stopTask(orchestrator);
|
|
310
|
-
setStep(orchestrator, "llm_work");
|
|
311
|
-
return continueMessage;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (phase === "plan") {
|
|
315
|
-
const choice = await selectOption(ctx, summary, [
|
|
316
|
-
"Approve plan",
|
|
317
|
-
autoLabel,
|
|
318
|
-
deepLabel,
|
|
319
|
-
"Review in Plannotator",
|
|
320
|
-
"Review on my own",
|
|
321
|
-
"Continue planning",
|
|
322
|
-
"Stop task",
|
|
323
|
-
]);
|
|
324
|
-
finalizeReviewCycle(task);
|
|
325
|
-
if (choice === "Approve plan") {
|
|
326
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
327
|
-
return result.ok ? "Plan approved. Transitioned to implement." : `Transition blocked: ${result.error}`;
|
|
328
|
-
}
|
|
329
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
330
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
331
|
-
if (choice === "Review in Plannotator") return enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
332
|
-
if (choice === "Stop task") return stopTask(orchestrator);
|
|
333
|
-
setStep(orchestrator, "synthesize");
|
|
334
|
-
return continueMessage;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (phase === "implement") {
|
|
338
|
-
const choice = await selectOption(ctx, summary, [
|
|
339
|
-
"Approve implementation",
|
|
340
|
-
autoLabel,
|
|
341
|
-
deepLabel,
|
|
342
|
-
"Review in Plannotator",
|
|
343
|
-
"Review on my own",
|
|
344
|
-
"Continue implementation",
|
|
345
|
-
"Stop task",
|
|
346
|
-
]);
|
|
347
|
-
finalizeReviewCycle(task);
|
|
348
|
-
if (choice === "Approve implementation") {
|
|
349
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
350
|
-
return result.ok ? "Implementation approved. Task completed." : `Transition blocked: ${result.error}`;
|
|
351
|
-
}
|
|
352
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
353
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
354
|
-
if (choice === "Review in Plannotator") return enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
355
|
-
if (choice === "Stop task") return stopTask(orchestrator);
|
|
356
|
-
setStep(orchestrator, "llm_work");
|
|
357
|
-
return continueMessage;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return "No dialog available for this phase.";
|
|
361
|
-
}
|
|
362
|
-
|
|
363
228
|
function registerOrchestratorTools(orchestrator: Orchestrator): void {
|
|
364
229
|
registerPhaseCompleteTool(orchestrator);
|
|
365
230
|
registerCommitTool(orchestrator);
|
|
@@ -411,9 +276,11 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
411
276
|
let hasNeedsChanges = false;
|
|
412
277
|
for (const review of params.reviews) {
|
|
413
278
|
ctx.ui?.setWorkingMessage?.(`Waiting for Plannotator review: ${review.range}…`);
|
|
279
|
+
const rangeBase = review.range.includes("..") ? review.range.split("..")[0] : review.range;
|
|
414
280
|
const result = await openCodeReviewDirect(pi, {
|
|
415
281
|
cwd: review.cwd,
|
|
416
|
-
diffType:
|
|
282
|
+
diffType: "branch",
|
|
283
|
+
defaultBranch: rangeBase,
|
|
417
284
|
});
|
|
418
285
|
if ("error" in result) {
|
|
419
286
|
results.push(`${review.cwd} (${review.range}): ${result.error}`);
|
|
@@ -434,15 +301,23 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
434
301
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
435
302
|
}
|
|
436
303
|
return {
|
|
437
|
-
content: [{ type: "text" as const, text: `Plannotator review complete.\n\n${summary}\n\
|
|
304
|
+
content: [{ type: "text" as const, text: `Plannotator review complete.\n\n${summary}\n\nAddress the user's feedback. If the feedback contains questions, answer them. If it requests changes, make the changes. Then call pp_phase_complete when done.` }],
|
|
438
305
|
details: {},
|
|
439
306
|
};
|
|
440
307
|
}
|
|
441
308
|
|
|
442
309
|
ctx.ui?.setWorkingMessage?.("Waiting for user input…");
|
|
443
310
|
try {
|
|
444
|
-
const
|
|
445
|
-
|
|
311
|
+
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
312
|
+
const text = await showActiveTaskMenu(orchestrator, ctx, `Plannotator review complete.\n\n${summary}`, "tool");
|
|
313
|
+
if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
|
|
314
|
+
await orchestrator.waitForCompaction();
|
|
315
|
+
return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
|
|
316
|
+
}
|
|
317
|
+
if (!text) {
|
|
318
|
+
return { content: [{ type: "text" as const, text: "User dismissed the menu. Wait for the user's next message. When you resume work, update USER_REQUEST.md and RESEARCH.md with any new findings before calling pp_phase_complete." }], details: {} };
|
|
319
|
+
}
|
|
320
|
+
return { content: [{ type: "text" as const, text }], details: {} };
|
|
446
321
|
} finally {
|
|
447
322
|
ctx.ui?.setWorkingMessage?.();
|
|
448
323
|
}
|
|
@@ -476,11 +351,22 @@ function registerCommitTool(orchestrator: Orchestrator): void {
|
|
|
476
351
|
if (!orchestrator.config.autoCommit) {
|
|
477
352
|
return { content: [{ type: "text" as const, text: "autoCommit is disabled in config." }], details: {} };
|
|
478
353
|
}
|
|
479
|
-
|
|
354
|
+
const files: string[] = [];
|
|
355
|
+
try {
|
|
356
|
+
const gitResult = await pi.exec("git", ["diff", "--name-only"], { cwd: orchestrator.cwd, timeout: 5000 });
|
|
357
|
+
if (gitResult.code === 0 && gitResult.stdout.trim()) {
|
|
358
|
+
files.push(...gitResult.stdout.trim().split("\n").filter(Boolean));
|
|
359
|
+
}
|
|
360
|
+
const stagedResult = await pi.exec("git", ["diff", "--name-only", "--cached"], { cwd: orchestrator.cwd, timeout: 5000 });
|
|
361
|
+
if (stagedResult.code === 0 && stagedResult.stdout.trim()) {
|
|
362
|
+
for (const f of stagedResult.stdout.trim().split("\n").filter(Boolean)) {
|
|
363
|
+
if (!files.includes(f)) files.push(f);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch {}
|
|
367
|
+
if (files.length === 0) {
|
|
480
368
|
return { content: [{ type: "text" as const, text: "No modified files to commit." }], details: {} };
|
|
481
369
|
}
|
|
482
|
-
|
|
483
|
-
const files = [...orchestrator.active.modifiedFiles];
|
|
484
370
|
const result = autoCommit(files, params.message, orchestrator.cwd);
|
|
485
371
|
if (result.ok) {
|
|
486
372
|
orchestrator.active.modifiedFiles.clear();
|
|
@@ -523,10 +409,11 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
|
523
409
|
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
524
410
|
const text = await showActiveTaskMenu(orchestrator, ctx, params.summary, "tool");
|
|
525
411
|
if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
|
|
526
|
-
|
|
412
|
+
await orchestrator.waitForCompaction();
|
|
413
|
+
return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
|
|
527
414
|
}
|
|
528
415
|
if (!text) {
|
|
529
|
-
return { content: [{ type: "text" as const, text: "
|
|
416
|
+
return { content: [{ type: "text" as const, text: "User dismissed the menu. Wait for the user's next message. When you resume work, update USER_REQUEST.md and RESEARCH.md with any new findings before calling pp_phase_complete." }], details: {} };
|
|
530
417
|
}
|
|
531
418
|
return { content: [{ type: "text" as const, text }], details: {} };
|
|
532
419
|
} finally {
|
|
@@ -539,6 +426,10 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
|
539
426
|
export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
540
427
|
const pi = orchestrator.pi;
|
|
541
428
|
|
|
429
|
+
function getUsageTracker(): UsageTracker | undefined {
|
|
430
|
+
return (globalThis as any)[USAGE_TRACKER_KEY] as UsageTracker | undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
542
433
|
function trackSubagentEvent(data: any, event: "created" | "started" | "first_tool" | "first_turn" | "completed" | "failed"): void {
|
|
543
434
|
if (!orchestrator.active || !data?.id) return;
|
|
544
435
|
const now = Date.now();
|
|
@@ -709,7 +600,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
709
600
|
}
|
|
710
601
|
|
|
711
602
|
if (choice === "Stop task") {
|
|
712
|
-
pi.sendUserMessage(`[PI-PI] ${stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
603
|
+
pi.sendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
713
604
|
return;
|
|
714
605
|
}
|
|
715
606
|
|
|
@@ -821,7 +712,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
821
712
|
}
|
|
822
713
|
|
|
823
714
|
if (choice === "Stop task") {
|
|
824
|
-
pi.sendUserMessage(`[PI-PI] ${stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
715
|
+
pi.sendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
825
716
|
return;
|
|
826
717
|
}
|
|
827
718
|
|
|
@@ -848,6 +739,18 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
848
739
|
}
|
|
849
740
|
|
|
850
741
|
pi.events.on("subagents:completed", (data: any) => {
|
|
742
|
+
const usageTracker = getUsageTracker();
|
|
743
|
+
if (usageTracker && data?.tokens) {
|
|
744
|
+
usageTracker.recordSubagentCompletion(data.tokens, undefined, {
|
|
745
|
+
description: data.description || data.type || data.id || "unknown",
|
|
746
|
+
agentType: data.type || "unknown",
|
|
747
|
+
modelId: data.modelId || "unknown",
|
|
748
|
+
durationMs: data.durationMs,
|
|
749
|
+
toolUses: data.toolUses,
|
|
750
|
+
});
|
|
751
|
+
(orchestrator.lastCtx?.ui as any)?.requestRender?.();
|
|
752
|
+
}
|
|
753
|
+
|
|
851
754
|
if (!orchestrator.active || !data?.id) return;
|
|
852
755
|
trackSubagentEvent(data, "completed");
|
|
853
756
|
orchestrator.spawnedAgentIds.delete(data.id);
|
|
@@ -916,10 +819,36 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
916
819
|
await orchestrator.cleanupActive();
|
|
917
820
|
});
|
|
918
821
|
|
|
822
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
823
|
+
if ((globalThis as any)[SUBAGENT_SESSION_KEY]) return;
|
|
824
|
+
const tracker = getUsageTracker();
|
|
825
|
+
if (!tracker) return;
|
|
826
|
+
const sessionId = ctx.sessionManager?.getSessionId?.() || `session-${Date.now()}`;
|
|
827
|
+
try {
|
|
828
|
+
dumpUsageSummary(tracker, sessionId);
|
|
829
|
+
} catch (err: any) {
|
|
830
|
+
console.error(`[pi-pi] Failed to dump usage summary: ${err.message}`);
|
|
831
|
+
}
|
|
832
|
+
delete (globalThis as any)[USAGE_TRACKER_KEY];
|
|
833
|
+
});
|
|
834
|
+
|
|
919
835
|
pi.on("session_start", async (_event, ctx) => {
|
|
920
836
|
orchestrator.lastCtx = ctx;
|
|
921
837
|
orchestrator.cwd = ctx.cwd;
|
|
922
838
|
|
|
839
|
+
if (!(globalThis as any)[SUBAGENT_SESSION_KEY]) {
|
|
840
|
+
const tracker = createUsageTracker();
|
|
841
|
+
const sessionId = ctx.sessionManager?.getSessionId?.() || "";
|
|
842
|
+
if (sessionId) {
|
|
843
|
+
const previous = loadUsageSummary(sessionId);
|
|
844
|
+
if (previous) tracker.loadFromSummary(previous);
|
|
845
|
+
}
|
|
846
|
+
(globalThis as any)[USAGE_TRACKER_KEY] = tracker;
|
|
847
|
+
setFooterContext(ctx);
|
|
848
|
+
setFooterTracker(tracker);
|
|
849
|
+
ctx.ui.setFooter(createCustomFooter);
|
|
850
|
+
}
|
|
851
|
+
|
|
923
852
|
const subagentsMgr = (globalThis as any)[Symbol.for("pi-subagents:manager")];
|
|
924
853
|
subagentsMgr?.refreshWidget?.(ctx.ui);
|
|
925
854
|
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
@@ -1196,6 +1125,20 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1196
1125
|
});
|
|
1197
1126
|
|
|
1198
1127
|
pi.on("turn_end", async (event, ctx) => {
|
|
1128
|
+
const msg = event.message as any;
|
|
1129
|
+
const usageTracker = getUsageTracker();
|
|
1130
|
+
if (usageTracker && msg?.usage) {
|
|
1131
|
+
const input = typeof msg.usage.input === "number" ? msg.usage.input : 0;
|
|
1132
|
+
const output = typeof msg.usage.output === "number" ? msg.usage.output : 0;
|
|
1133
|
+
const cacheRead = typeof msg.usage.cacheRead === "number" ? msg.usage.cacheRead : 0;
|
|
1134
|
+
const cacheWrite = typeof msg.usage.cacheWrite === "number" ? msg.usage.cacheWrite : 0;
|
|
1135
|
+
const cost = typeof msg.usage.cost?.total === "number" ? msg.usage.cost.total : 0;
|
|
1136
|
+
const modelId = (typeof msg.model === "string" && msg.model) || ctx.model?.id || "unknown-model";
|
|
1137
|
+
const provider = (typeof msg.provider === "string" && msg.provider) || ctx.model?.provider || "unknown";
|
|
1138
|
+
usageTracker.recordTurn(modelId, provider, input, output, cacheRead, cacheWrite, cost);
|
|
1139
|
+
(ctx.ui as any)?.requestRender?.();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1199
1142
|
if (!orchestrator.active || orchestrator.active.state.phase === "done") return;
|
|
1200
1143
|
orchestrator.updateStatus(ctx);
|
|
1201
1144
|
|
|
@@ -1218,7 +1161,6 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1218
1161
|
|
|
1219
1162
|
const phase = orchestrator.active.state.phase;
|
|
1220
1163
|
|
|
1221
|
-
const msg = event.message as any;
|
|
1222
1164
|
if (msg?.stopReason === "aborted") return;
|
|
1223
1165
|
if (msg?.stopReason === "error") {
|
|
1224
1166
|
const errorMsg = msg.errorMessage || "unknown error";
|
|
@@ -1323,6 +1265,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1323
1265
|
}
|
|
1324
1266
|
|
|
1325
1267
|
if (orchestrator.active.type === "brainstorm" && phase === "brainstorm") return;
|
|
1268
|
+
if (orchestrator.active.type === "review" && phase === "review") return;
|
|
1326
1269
|
|
|
1327
1270
|
const contentParts = Array.isArray(msg?.content) ? msg.content : [];
|
|
1328
1271
|
const hasText = contentParts.some((c: any) => c.type === "text" && c.text?.trim());
|
|
@@ -1331,7 +1274,9 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1331
1274
|
const turnWasEmpty = !hasText && !hasToolCalls && !hasToolResults;
|
|
1332
1275
|
|
|
1333
1276
|
if (!turnWasEmpty) {
|
|
1334
|
-
|
|
1277
|
+
const lastPart = contentParts.length > 0 ? contentParts[contentParts.length - 1] : null;
|
|
1278
|
+
const endsWithText = lastPart?.type === "text" && lastPart?.text?.trim();
|
|
1279
|
+
if (hasText && (!hasToolCalls || endsWithText)) {
|
|
1335
1280
|
const step = orchestrator.active.state.step;
|
|
1336
1281
|
if (step !== "await_planners" && step !== "await_reviewers") {
|
|
1337
1282
|
pi.sendMessage(
|
|
@@ -11,6 +11,8 @@ export interface OpenRouterModelData {
|
|
|
11
11
|
pricing: {
|
|
12
12
|
prompt: number;
|
|
13
13
|
completion: number;
|
|
14
|
+
cacheRead: number;
|
|
15
|
+
cacheWrite: number;
|
|
14
16
|
};
|
|
15
17
|
modality: string;
|
|
16
18
|
}
|
|
@@ -241,6 +243,8 @@ export async function fetchOpenRouterMetadata(modelIds: string[]): Promise<Recor
|
|
|
241
243
|
pricing: {
|
|
242
244
|
prompt: toNumber(pricing.prompt, 0),
|
|
243
245
|
completion: toNumber(pricing.completion, 0),
|
|
246
|
+
cacheRead: toNumber(pricing.input_cache_read, 0),
|
|
247
|
+
cacheWrite: toNumber(pricing.input_cache_write, 0),
|
|
244
248
|
},
|
|
245
249
|
modality: typeof architecture.modality === "string" ? architecture.modality : "text",
|
|
246
250
|
};
|
|
@@ -267,8 +271,8 @@ function buildProviderModelConfig(
|
|
|
267
271
|
cost: {
|
|
268
272
|
input: toNumber(modelMeta?.pricing.prompt, 0) * 1_000_000,
|
|
269
273
|
output: toNumber(modelMeta?.pricing.completion, 0) * 1_000_000,
|
|
270
|
-
cacheRead: 0,
|
|
271
|
-
cacheWrite: 0,
|
|
274
|
+
cacheRead: toNumber(modelMeta?.pricing.cacheRead, 0) * 1_000_000,
|
|
275
|
+
cacheWrite: toNumber(modelMeta?.pricing.cacheWrite, 0) * 1_000_000,
|
|
272
276
|
},
|
|
273
277
|
contextWindow: toNumber(modelMeta?.context_length, 200000),
|
|
274
278
|
maxTokens: toNumber(modelMeta?.max_completion_tokens, 32000),
|
|
@@ -358,6 +362,7 @@ export function generateFlantConfig(models: string[]): Partial<PiPiConfig> {
|
|
|
358
362
|
implement: { model: modelSpec(implementModel), thinking: "high" },
|
|
359
363
|
debug: { model: modelSpec(debugModel), thinking: "high" },
|
|
360
364
|
brainstorm: { model: modelSpec(brainstormModel), thinking: "high" },
|
|
365
|
+
review: { model: modelSpec(implementModel), thinking: "high" },
|
|
361
366
|
},
|
|
362
367
|
planners: {
|
|
363
368
|
opus: makeVariant(latestOpus, fallback),
|
|
@@ -4,7 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { Orchestrator } from "./orchestrator.js";
|
|
6
6
|
import { registerCommandHandlers } from "./command-handlers.js";
|
|
7
|
-
import { registerEventHandlers
|
|
7
|
+
import { registerEventHandlers } from "./event-handlers.js";
|
|
8
8
|
import { createTask, loadTask, saveTask } from "./state.js";
|
|
9
9
|
|
|
10
10
|
vi.mock("./cbm.js", () => ({ registerCbmTools: vi.fn() }));
|
|
@@ -25,6 +25,7 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
25
25
|
implement: { model: "test/model", thinking: "high" },
|
|
26
26
|
debug: { model: "test/model", thinking: "high" },
|
|
27
27
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
28
|
+
review: { model: "test/model", thinking: "high" },
|
|
28
29
|
},
|
|
29
30
|
planners: { test: { enabled: true, model: "test/planner", thinking: "low" } },
|
|
30
31
|
planReviewers: {},
|
|
@@ -142,6 +143,7 @@ function makeConfig() {
|
|
|
142
143
|
implement: { model: "test/model", thinking: "high" },
|
|
143
144
|
debug: { model: "test/model", thinking: "high" },
|
|
144
145
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
146
|
+
review: { model: "test/model", thinking: "high" },
|
|
145
147
|
},
|
|
146
148
|
planners: {
|
|
147
149
|
test: { enabled: true, model: "test/planner", thinking: "low" },
|
|
@@ -98,6 +98,7 @@ describe("deepReviewConfig", () => {
|
|
|
98
98
|
implement: { model: "a/impl", thinking: "high" },
|
|
99
99
|
debug: { model: "a/debug", thinking: "high" },
|
|
100
100
|
brainstorm: { model: "a/brain", thinking: "high" },
|
|
101
|
+
review: { model: "a/review", thinking: "high" },
|
|
101
102
|
},
|
|
102
103
|
planners: {},
|
|
103
104
|
planReviewers: {
|
|
@@ -16,7 +16,8 @@ import { loadContextFiles, getPhaseArtifacts, getLatestSynthesizedPlan } from ".
|
|
|
16
16
|
import { brainstormSystemPrompt } from "./phases/brainstorm.js";
|
|
17
17
|
import { planningSystemPrompt, spawnPlanners } from "./phases/planning.js";
|
|
18
18
|
import { implementationSystemPrompt } from "./phases/implementation.js";
|
|
19
|
-
import { reviewSystemPrompt } from "./phases/review.js";
|
|
19
|
+
import { reviewSystemPrompt as reviewCycleSystemPrompt } from "./phases/review.js";
|
|
20
|
+
import { reviewSystemPrompt as reviewTaskSystemPrompt } from "./phases/review-task.js";
|
|
20
21
|
import { registerAgentDefinitions, unregisterAgentDefinitions } from "./agents/registry.js";
|
|
21
22
|
import { createExploreAgent } from "./agents/explore.js";
|
|
22
23
|
import { createLibrarianAgent } from "./agents/librarian.js";
|
|
@@ -207,7 +208,7 @@ export class Orchestrator {
|
|
|
207
208
|
if (this.active.state.reviewCycle?.step === "apply_feedback") {
|
|
208
209
|
const pass = this.active.state.reviewCycle.pass;
|
|
209
210
|
const manualReview = this.active.state.reviewCycle.kind === "manual";
|
|
210
|
-
return
|
|
211
|
+
return reviewCycleSystemPrompt(this.active.dir, pass, manualReview, this.active.state.phase);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
switch (this.active.state.phase) {
|
|
@@ -219,6 +220,8 @@ export class Orchestrator {
|
|
|
219
220
|
return planningSystemPrompt(this.active.dir);
|
|
220
221
|
case "implement":
|
|
221
222
|
return implementationSystemPrompt(this.active.dir);
|
|
223
|
+
case "review":
|
|
224
|
+
return reviewTaskSystemPrompt(this.active.dir);
|
|
222
225
|
default:
|
|
223
226
|
return "";
|
|
224
227
|
}
|
|
@@ -314,7 +317,12 @@ export class Orchestrator {
|
|
|
314
317
|
description: state.description,
|
|
315
318
|
};
|
|
316
319
|
|
|
317
|
-
const modelConfig = this.config.mainModel[
|
|
320
|
+
const modelConfig = this.config.mainModel[
|
|
321
|
+
type === "debug" ? "debug"
|
|
322
|
+
: type === "brainstorm" ? "brainstorm"
|
|
323
|
+
: type === "review" ? "review"
|
|
324
|
+
: "implement"
|
|
325
|
+
];
|
|
318
326
|
const modelOk = await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|
|
319
327
|
if (!modelOk) {
|
|
320
328
|
ctx.ui.notify(`Model "${modelConfig.model}" not found — using current model`, "warning");
|
|
@@ -328,7 +336,7 @@ export class Orchestrator {
|
|
|
328
336
|
this.injectContextAndArtifacts(this.active.dir, this.active.state.phase);
|
|
329
337
|
|
|
330
338
|
this.phaseStartTime = Date.now();
|
|
331
|
-
const isGenericDescription = ["implement", "debug", "brainstorm"].includes(this.active.description);
|
|
339
|
+
const isGenericDescription = ["implement", "debug", "brainstorm", "review"].includes(this.active.description);
|
|
332
340
|
const hasInheritedTaskContext = Boolean(fromTaskDir && type === "implement");
|
|
333
341
|
const isWaitingForPlanners = this.active.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
334
342
|
if (isGenericDescription && !hasInheritedTaskContext) {
|
|
@@ -450,6 +458,13 @@ export class Orchestrator {
|
|
|
450
458
|
}
|
|
451
459
|
}
|
|
452
460
|
|
|
461
|
+
waitForCompaction(): Promise<void> {
|
|
462
|
+
if (!this.phaseCompactionPending && !this.taskDoneCompactionPending) return Promise.resolve();
|
|
463
|
+
return new Promise((resolve) => {
|
|
464
|
+
this.phaseCompactionResolve = resolve;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
453
468
|
compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase): void {
|
|
454
469
|
this.phaseCompactionPending = true;
|
|
455
470
|
ctx.compact({
|
|
@@ -461,6 +476,10 @@ export class Orchestrator {
|
|
|
461
476
|
this.phaseCompactionResolve = null;
|
|
462
477
|
}
|
|
463
478
|
this.phaseStartTime = Date.now();
|
|
479
|
+
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
480
|
+
const modelConfig = this.config.mainModel.implement;
|
|
481
|
+
this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
|
|
482
|
+
}
|
|
464
483
|
this.injectContextAndArtifacts(taskDir, phase);
|
|
465
484
|
if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
466
485
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
@@ -476,6 +495,10 @@ export class Orchestrator {
|
|
|
476
495
|
this.phaseCompactionResolve = null;
|
|
477
496
|
}
|
|
478
497
|
this.phaseStartTime = Date.now();
|
|
498
|
+
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
499
|
+
const modelConfig = this.config.mainModel.implement;
|
|
500
|
+
this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
|
|
501
|
+
}
|
|
479
502
|
this.injectContextAndArtifacts(taskDir, phase);
|
|
480
503
|
if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
481
504
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
@@ -67,6 +67,11 @@ const TRANSITIONS: Record<TaskType, Record<string, string[]>> = {
|
|
|
67
67
|
plan: ["implement"],
|
|
68
68
|
implement: ["done"],
|
|
69
69
|
},
|
|
70
|
+
review: {
|
|
71
|
+
review: ["plan"],
|
|
72
|
+
plan: ["implement"],
|
|
73
|
+
implement: ["done"],
|
|
74
|
+
},
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
export function canTransition(taskType: TaskType, from: Phase, to: Phase): boolean {
|
|
@@ -116,6 +121,37 @@ export function validateExitCriteria(
|
|
|
116
121
|
return { ok: true };
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
case "review": {
|
|
125
|
+
const ur = join(taskDir, "USER_REQUEST.md");
|
|
126
|
+
const res = join(taskDir, "RESEARCH.md");
|
|
127
|
+
if (isMissingOrEmpty(ur)) {
|
|
128
|
+
return { ok: false, reason: "USER_REQUEST.md does not exist or is empty" };
|
|
129
|
+
}
|
|
130
|
+
if (isMissingOrEmpty(res)) {
|
|
131
|
+
return { ok: false, reason: "RESEARCH.md does not exist or is empty" };
|
|
132
|
+
}
|
|
133
|
+
const urContent = readFileSync(ur, "utf-8");
|
|
134
|
+
const resContent = readFileSync(res, "utf-8");
|
|
135
|
+
|
|
136
|
+
const userRequestValidation = validateUserRequest(urContent);
|
|
137
|
+
if (!userRequestValidation.ok) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
reason: formatValidationErrors("USER_REQUEST.md", userRequestValidation.errors, USER_REQUEST_TEMPLATE),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const researchValidation = validateResearch(resContent);
|
|
145
|
+
if (!researchValidation.ok) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: formatValidationErrors("RESEARCH.md", researchValidation.errors, RESEARCH_TEMPLATE),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ok: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
case "plan": {
|
|
120
156
|
const plan = getLatestSynthesizedPlan(taskDir);
|
|
121
157
|
if (!plan) {
|
|
@@ -205,5 +241,7 @@ export function phasePipeline(taskType: TaskType): Phase[] {
|
|
|
205
241
|
return ["debug", "plan", "implement", "done"];
|
|
206
242
|
case "brainstorm":
|
|
207
243
|
return ["brainstorm", "plan", "implement", "done"];
|
|
244
|
+
case "review":
|
|
245
|
+
return ["review", "plan", "implement", "done"];
|
|
208
246
|
}
|
|
209
247
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function reviewSystemPrompt(taskDir: string): string {
|
|
2
|
+
return [
|
|
3
|
+
"[PI-PI — REVIEW PHASE]",
|
|
4
|
+
"",
|
|
5
|
+
"You are reviewing code changes. USER_REQUEST.md describes what to review.",
|
|
6
|
+
"Read it first to understand the scope.",
|
|
7
|
+
"",
|
|
8
|
+
"Use available tools to analyze the changes:",
|
|
9
|
+
"- git diff, git log, git show for examining commits and diffs",
|
|
10
|
+
"- read, lsp, grep, find for understanding the code",
|
|
11
|
+
"- gh pr view for GitHub PR context if a PR URL is mentioned",
|
|
12
|
+
"",
|
|
13
|
+
"Write your findings:",
|
|
14
|
+
`- ${taskDir}/USER_REQUEST.md — update with a clear problem statement of issues found`,
|
|
15
|
+
`- ${taskDir}/RESEARCH.md — detailed technical analysis`,
|
|
16
|
+
"",
|
|
17
|
+
"USER_REQUEST.md format:",
|
|
18
|
+
"- # User Request",
|
|
19
|
+
"- ## Problem",
|
|
20
|
+
"- ## Constraints",
|
|
21
|
+
"",
|
|
22
|
+
"RESEARCH.md format:",
|
|
23
|
+
"- ## Affected Code",
|
|
24
|
+
"- ## Architecture Context",
|
|
25
|
+
"- ## Constraints & Edge Cases",
|
|
26
|
+
"- ## Open Questions (optional)",
|
|
27
|
+
"",
|
|
28
|
+
"Focus on: correctness, edge cases, style consistency, missing tests, potential bugs.",
|
|
29
|
+
"",
|
|
30
|
+
"When complete, call pp_phase_complete with a brief summary of findings.",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|