@ilya-lesikov/pi-pi 0.2.0 → 0.4.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.
@@ -442,12 +442,7 @@ export async function startCodeReviewBrowserSession(
442
442
  gitRef = result.gitRef;
443
443
  diffError = result.error;
444
444
  initialBase = result.base;
445
- if (typeof diffType === "string" && diffType.startsWith("range:") && gitCtx) {
446
- const range = diffType.slice("range:".length);
447
- if (!gitCtx.diffOptions.some((o: { id: string }) => o.id === diffType)) {
448
- gitCtx.diffOptions.push({ id: diffType, label: `Commits ${range}` });
449
- }
450
- }
445
+
451
446
  } else {
452
447
  workspace = await buildLocalWorkspaceReview(cwd, {
453
448
  requestedDiffType: options.diffType,
@@ -22,7 +22,6 @@ export type DiffType =
22
22
  | "branch"
23
23
  | "merge-base"
24
24
  | "all"
25
- | `range:${string}`
26
25
  | `worktree:${string}`
27
26
  | "p4-default"
28
27
  | `p4-changelist:${string}`;
@@ -737,28 +736,8 @@ export async function runGitDiff(
737
736
  break;
738
737
  }
739
738
 
740
- default: {
741
- if (effectiveDiffType.startsWith("range:")) {
742
- const range = effectiveDiffType.slice("range:".length);
743
- const rangeArgs = [
744
- "diff",
745
- "--no-ext-diff",
746
- ...wFlag,
747
- "--src-prefix=a/",
748
- "--dst-prefix=b/",
749
- "--end-of-options",
750
- range,
751
- ];
752
- const rangeDiff = assertGitSuccess(
753
- await runtime.runGit(rangeArgs, { cwd }),
754
- rangeArgs,
755
- );
756
- patch = rangeDiff.stdout;
757
- label = `Commits ${range}`;
758
- break;
759
- }
739
+ default:
760
740
  return { patch: "", label: "Unknown diff type" };
761
- }
762
741
  }
763
742
  } catch (error) {
764
743
  const raw = error instanceof Error ? error.message : String(error);
@@ -158,7 +158,7 @@ export function createGitProvider(runtime: ReviewGitRuntime): VcsProvider {
158
158
  },
159
159
 
160
160
  ownsDiffType(diffType: string): boolean {
161
- return GIT_DIFF_TYPES.has(diffType) || diffType.startsWith("worktree:") || diffType.startsWith("range:");
161
+ return GIT_DIFF_TYPES.has(diffType) || diffType.startsWith("worktree:");
162
162
  },
163
163
 
164
164
  canStageFiles(diffType: string): boolean {
@@ -51,7 +51,10 @@ export function createBrainstormReviewerAgent(
51
51
  "- SUGGESTIONS: (improvements, not required)",
52
52
  "- VERDICT: APPROVE or NEEDS_WORK (with reason)",
53
53
  "",
54
- 'You can spawn subagents: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs.',
54
+ "subagent_type is REQUIRED when spawning subagents calls without it are rejected:",
55
+ '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
56
+ '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
57
+ "Spawn multiple Explore agents in parallel for broad searches.",
55
58
  "",
56
59
  "# MANDATORY: Write your review to this exact file using the write tool:",
57
60
  ` ${outputPath}`,
@@ -65,7 +65,10 @@ export function createCodeReviewerAgent(
65
65
  "- OPEN QUESTIONS: (low-confidence concerns, speculative follow-ups)",
66
66
  "- VERDICT: APPROVE or NEEDS_CHANGES",
67
67
  "",
68
- 'You can spawn subagents: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs.',
68
+ "subagent_type is REQUIRED when spawning subagents calls without it are rejected:",
69
+ '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
70
+ '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
71
+ "Spawn multiple Explore agents in parallel for broad searches.",
69
72
  "",
70
73
  // --- dynamic suffix ---
71
74
  "# MANDATORY: Write your review to this exact file using the write tool:",
@@ -51,7 +51,10 @@ export function createPlanReviewerAgent(
51
51
  "- SUGGESTIONS: (improvements, not required)",
52
52
  "- VERDICT: APPROVE or REJECT (with reason)",
53
53
  "",
54
- 'You can spawn subagents: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs.',
54
+ "subagent_type is REQUIRED when spawning subagents calls without it are rejected:",
55
+ '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
56
+ '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
57
+ "Spawn multiple Explore agents in parallel for broad searches.",
55
58
  "",
56
59
  // --- dynamic suffix ---
57
60
  "# MANDATORY OUTPUT",
@@ -40,7 +40,10 @@ export function createPlannerAgent(
40
40
  "- No other top-level sections allowed",
41
41
  "- Describe outcomes, not code-level mechanics",
42
42
  "",
43
- 'You can spawn subagents: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs.',
43
+ "subagent_type is REQUIRED when spawning subagents calls without it are rejected:",
44
+ '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
45
+ '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
46
+ "Spawn multiple Explore agents in parallel for broad searches.",
44
47
  "",
45
48
  // --- dynamic suffix ---
46
49
  "# MANDATORY OUTPUT",
@@ -28,8 +28,10 @@ export function createTaskAgent(
28
28
  FAILURE_RECOVERY,
29
29
  "",
30
30
  "# Constraints",
31
- "- Do NOT spawn task subagents (no recursion)",
32
- '- You CAN spawn subagents: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs',
31
+ "- Do NOT spawn task subagents (no recursion)",
32
+ "- subagent_type is REQUIRED when spawning subagents calls without it are rejected:",
33
+ ' Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
34
+ ' Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
33
35
  "- Focus only on your subtask — do not modify unrelated code",
34
36
  "- Before modifying a function, use lsp findReferences to understand all callers",
35
37
  "- After editing files, run lsp diagnostics and fix errors before moving on",
@@ -109,7 +109,7 @@ export function registerCommandHandlers(orchestrator: Orchestrator): void {
109
109
  const { showPpMenu } = await import("./pp-menu.js");
110
110
  const text = await showPpMenu(orchestrator, ctx, "command");
111
111
  if (text) {
112
- pi.sendUserMessage(`[PI-PI] ${text}`);
112
+ pi.sendUserMessage(`[PI-PI] ${text}`, { deliverAs: "followUp" });
113
113
  }
114
114
  },
115
115
  });
@@ -150,7 +150,7 @@ export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kin
150
150
  orchestrator.active.state.step = "synthesize";
151
151
  saveTask(orchestrator.active.dir, orchestrator.active.state);
152
152
  const feedback = result.feedback ? `\n\nFeedback:\n${result.feedback}` : "";
153
- return `Plannotator requested changes.${feedback}\n\nUser wants to continue. Run /pp when ready to advance.`;
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.`;
154
154
  }
155
155
 
156
156
  orchestrator.active.state.reviewCycle = null;
@@ -276,9 +276,11 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
276
276
  let hasNeedsChanges = false;
277
277
  for (const review of params.reviews) {
278
278
  ctx.ui?.setWorkingMessage?.(`Waiting for Plannotator review: ${review.range}…`);
279
+ const rangeBase = review.range.includes("..") ? review.range.split("..")[0] : review.range;
279
280
  const result = await openCodeReviewDirect(pi, {
280
281
  cwd: review.cwd,
281
- diffType: `range:${review.range}`,
282
+ diffType: "branch",
283
+ defaultBranch: rangeBase,
282
284
  });
283
285
  if ("error" in result) {
284
286
  results.push(`${review.cwd} (${review.range}): ${result.error}`);
@@ -299,7 +301,7 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
299
301
  saveTask(orchestrator.active.dir, orchestrator.active.state);
300
302
  }
301
303
  return {
302
- content: [{ type: "text" as const, text: `Plannotator review complete.\n\n${summary}\n\nFix the issues noted above, then call pp_phase_complete when done.` }],
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.` }],
303
305
  details: {},
304
306
  };
305
307
  }
@@ -309,10 +311,11 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
309
311
  const { showActiveTaskMenu } = await import("./pp-menu.js");
310
312
  const text = await showActiveTaskMenu(orchestrator, ctx, `Plannotator review complete.\n\n${summary}`, "tool");
311
313
  if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
312
- return { content: [{ type: "text" as const, text: "Phase transition in progress." }], details: {} };
314
+ await orchestrator.waitForCompaction();
315
+ return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
313
316
  }
314
317
  if (!text) {
315
- return { content: [{ type: "text" as const, text: "No action selected." }], details: {} };
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: {} };
316
319
  }
317
320
  return { content: [{ type: "text" as const, text }], details: {} };
318
321
  } finally {
@@ -348,11 +351,22 @@ function registerCommitTool(orchestrator: Orchestrator): void {
348
351
  if (!orchestrator.config.autoCommit) {
349
352
  return { content: [{ type: "text" as const, text: "autoCommit is disabled in config." }], details: {} };
350
353
  }
351
- if (orchestrator.active.modifiedFiles.size === 0) {
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) {
352
368
  return { content: [{ type: "text" as const, text: "No modified files to commit." }], details: {} };
353
369
  }
354
-
355
- const files = [...orchestrator.active.modifiedFiles];
356
370
  const result = autoCommit(files, params.message, orchestrator.cwd);
357
371
  if (result.ok) {
358
372
  orchestrator.active.modifiedFiles.clear();
@@ -395,10 +409,11 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
395
409
  const { showActiveTaskMenu } = await import("./pp-menu.js");
396
410
  const text = await showActiveTaskMenu(orchestrator, ctx, params.summary, "tool");
397
411
  if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
398
- return { content: [{ type: "text" as const, text: "Phase transition in progress." }], details: {} };
412
+ await orchestrator.waitForCompaction();
413
+ return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
399
414
  }
400
415
  if (!text) {
401
- return { content: [{ type: "text" as const, text: "No action selected." }], details: {} };
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: {} };
402
417
  }
403
418
  return { content: [{ type: "text" as const, text }], details: {} };
404
419
  } finally {
@@ -728,6 +743,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
728
743
  if (usageTracker && data?.tokens) {
729
744
  usageTracker.recordSubagentCompletion(data.tokens, undefined, {
730
745
  description: data.description || data.type || data.id || "unknown",
746
+ agentType: data.type || "unknown",
731
747
  modelId: data.modelId || "unknown",
732
748
  durationMs: data.durationMs,
733
749
  toolUses: data.toolUses,
@@ -927,7 +943,10 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
927
943
  if (event.toolName === "Agent" && orchestrator.active) {
928
944
  const input = event.input as Record<string, unknown>;
929
945
  const requestedType = ((input.subagent_type as string) || "").toLowerCase();
930
- const isExplore = !requestedType || requestedType === "explore";
946
+ if (!requestedType) {
947
+ return { block: true, reason: "subagent_type is required. Use Explore for codebase research, Librarian for external docs, or Task for implementation subtasks." };
948
+ }
949
+ const isExplore = requestedType === "explore";
931
950
  const isLibrarian = requestedType === "librarian";
932
951
 
933
952
  if (isExplore) {
@@ -1167,7 +1186,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1167
1186
  orchestrator.pendingRetryTimer = setTimeout(() => {
1168
1187
  orchestrator.pendingRetryTimer = null;
1169
1188
  if (orchestrator.activeTaskToken !== taskToken || !orchestrator.active) return;
1170
- pi.sendUserMessage(`[PI-PI] Previous request failed due to an API error. Continue working on the current phase (${phase}).`);
1189
+ pi.sendUserMessage(`[PI-PI] Previous request failed due to an API error. Continue working on the current phase (${phase}).`, { deliverAs: "followUp" });
1171
1190
  }, delay);
1172
1191
  } else {
1173
1192
  ctx.ui.notify(`API error persisted after 3 retries: ${errorMsg}. Stopping auto-retry.`, "error");
@@ -1202,7 +1221,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1202
1221
  orchestrator.pendingSubagentSpawns = 0;
1203
1222
  orchestrator.active.state.step = "synthesize";
1204
1223
  saveTask(orchestrator.active.dir, orchestrator.active.state);
1205
- pi.sendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.");
1224
+ pi.sendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.", { deliverAs: "followUp" });
1206
1225
  }
1207
1226
  }
1208
1227
  } else if (orchestrator.active.state.step === "await_reviewers" && orchestrator.active.state.reviewCycle) {
@@ -1233,7 +1252,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1233
1252
  { customType: "pp-review-ready", content: `[PI-PI] Reviewer outputs are ready.\n\n${rendered}`, display: false },
1234
1253
  { deliverAs: "followUp" },
1235
1254
  );
1236
- pi.sendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.");
1255
+ pi.sendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.", { deliverAs: "followUp" });
1237
1256
  }
1238
1257
  } else {
1239
1258
  clearInterval(orchestrator.awaitPollTimer!);
@@ -1258,7 +1277,9 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1258
1277
  const turnWasEmpty = !hasText && !hasToolCalls && !hasToolResults;
1259
1278
 
1260
1279
  if (!turnWasEmpty) {
1261
- if (hasText && !hasToolCalls) {
1280
+ const lastPart = contentParts.length > 0 ? contentParts[contentParts.length - 1] : null;
1281
+ const endsWithText = lastPart?.type === "text" && lastPart?.text?.trim();
1282
+ if (hasText && (!hasToolCalls || endsWithText)) {
1262
1283
  const step = orchestrator.active.state.step;
1263
1284
  if (step !== "await_planners" && step !== "await_reviewers") {
1264
1285
  pi.sendMessage(
@@ -344,7 +344,7 @@ export class Orchestrator {
344
344
  } else if (isWaitingForPlanners) {
345
345
  ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
346
346
  } else {
347
- this.pi.sendUserMessage(`[PI-PI] Entered ${this.active.state.phase} phase. Begin working.`);
347
+ this.pi.sendUserMessage(`[PI-PI] Entered ${this.active.state.phase} phase. Begin working.`, { deliverAs: "followUp" });
348
348
  }
349
349
 
350
350
  if (this.active.state.phase === "plan" && this.active.state.step === "await_planners") {
@@ -458,6 +458,13 @@ export class Orchestrator {
458
458
  }
459
459
  }
460
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
+
461
468
  compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase): void {
462
469
  this.phaseCompactionPending = true;
463
470
  ctx.compact({
@@ -469,11 +476,15 @@ export class Orchestrator {
469
476
  this.phaseCompactionResolve = null;
470
477
  }
471
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
+ }
472
483
  this.injectContextAndArtifacts(taskDir, phase);
473
484
  if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
474
485
  ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
475
486
  } else {
476
- this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`);
487
+ this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`, { deliverAs: "followUp" });
477
488
  }
478
489
  },
479
490
  onError: (err) => {
@@ -484,11 +495,15 @@ export class Orchestrator {
484
495
  this.phaseCompactionResolve = null;
485
496
  }
486
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
+ }
487
502
  this.injectContextAndArtifacts(taskDir, phase);
488
503
  if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
489
504
  ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
490
505
  } else {
491
- this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`);
506
+ this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`, { deliverAs: "followUp" });
492
507
  }
493
508
  },
494
509
  });
@@ -22,7 +22,11 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
22
22
  "",
23
23
  "# Your job:",
24
24
  "1. Clarify the problem with the user if needed",
25
- '2. Spawn explore subagents via Agent(subagent_type="Explore", ...) for codebase research',
25
+ "2. Spawn subagents for research (subagent_type is REQUIRED calls without it are rejected):",
26
+ ' - Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups.',
27
+ ' - Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
28
+ ' - Agent(subagent_type="Task", ...) — only when you need a subtask that writes files or runs complex multi-step commands.',
29
+ " Explore is fast and cheap. Use it liberally for codebase questions. Spawn multiple in parallel.",
26
30
  "3. Use bash to run commands, check logs, reproduce issues",
27
31
  "4. Use tools to trace the bug:",
28
32
  " - cbm_search/cbm_search_code: find relevant code by concept or text",
@@ -31,8 +35,6 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
31
35
  " - lsp diagnostics: find type errors",
32
36
  " - cbm_trace: trace call chains to/from suspect functions",
33
37
  " - ast_search: find structural patterns (e.g. error handling, goroutines)",
34
- "",
35
- "IMPORTANT: Always specify subagent_type. Use Explore for codebase, Librarian for external docs.",
36
38
  "",
37
39
  "Produce two files:",
38
40
  `- ${taskDir}/USER_REQUEST.md — the fix request. MUST follow this exact structure:`,
@@ -76,8 +78,11 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
76
78
  "",
77
79
  "# How to work:",
78
80
  "- Discuss the topic with the user. Ask clarifying questions. Propose approaches. Analyze tradeoffs.",
79
- '- Spawn subagents for research: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs.',
80
- " IMPORTANT: Always specify subagent_type. Do NOT omit it.",
81
+ "- Spawn subagents for research (subagent_type is REQUIRED calls without it are rejected):",
82
+ ' Agent(subagent_type="Explore", ...) codebase research. Prefer this for most lookups. Fast and cheap.',
83
+ ' Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
84
+ ' Agent(subagent_type="Task", ...) — only when you need a subtask that writes files or runs complex multi-step commands.',
85
+ " Spawn multiple Explore agents in parallel for broad searches.",
81
86
  "- Use tools directly for quick lookups (cbm_search, lsp, ast_search, grep, etc.)",
82
87
  "- Present findings to the user and discuss them. Don't just dump raw results.",
83
88
  "- Do NOT modify project source code.",
@@ -120,18 +125,19 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
120
125
  "",
121
126
  "# Steps:",
122
127
  "1. Clarify requirements with the user if anything is ambiguous",
123
- '2. Spawn explore subagents via Agent(subagent_type="Explore", ...) for codebase research',
124
- '3. Spawn librarian subagents via Agent(subagent_type="Librarian", ...) for external docs/library research',
125
- "4. Use tools to understand code structure:",
128
+ "2. Spawn subagents for research (subagent_type is REQUIRED calls without it are rejected):",
129
+ ' - Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
130
+ ' - Agent(subagent_type="Librarian", ...) external docs, library APIs, web research.',
131
+ ' - Agent(subagent_type="Task", ...) — only when you need a subtask that writes files or runs complex multi-step commands.',
132
+ " Spawn multiple Explore agents in parallel for broad searches.",
133
+ "3. Use tools to understand code structure:",
126
134
  " - cbm_search: natural-language search across all symbols",
127
135
  " - cbm_search_code: graph-augmented grep (deduplicates into functions)",
128
136
  " - lsp documentSymbol, goToDefinition, findReferences, goToImplementation, hover",
129
137
  " - cbm_trace: trace call chains for dependency understanding",
130
138
  " - ast_search: find structural patterns across the codebase",
131
- "5. Ask the user follow-up questions as needed",
132
- "6. Write findings into RESEARCH.md as results come back — don't wait for all subagents",
133
- "",
134
- "IMPORTANT: Always specify subagent_type. Use Explore for codebase, Librarian for external docs.",
139
+ "4. Ask the user follow-up questions as needed",
140
+ "5. Write findings into RESEARCH.md as results come back — don't wait for all subagents",
135
141
  "",
136
142
  "Produce two files:",
137
143
  `- ${taskDir}/USER_REQUEST.md — MUST follow this exact structure:`,
@@ -22,9 +22,11 @@ export function implementationSystemPrompt(taskDir: string): string {
22
22
  " - cbm_trace: trace dependencies across the codebase",
23
23
  " - ast_search: find structural patterns (e.g. error handling conventions)",
24
24
  "6. After completing changes, run cbm_changes to verify blast radius",
25
- '7. For parallelizable, self-contained subtasks, delegate via Agent(subagent_type="Task", ...)',
26
- '8. For codebase research, use Agent(subagent_type="Explore", ...)',
27
- '9. For external docs research, use Agent(subagent_type="Librarian", ...)',
25
+ "7. Spawn subagents as needed (subagent_type is REQUIRED — calls without it are rejected):",
26
+ ' - Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
27
+ ' - Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
28
+ ' - Agent(subagent_type="Task", ...) — parallelizable, self-contained implementation subtasks.',
29
+ " Spawn multiple Explore agents in parallel for broad searches.",
28
30
  "",
29
31
  "If a fix fails 3 times, STOP and re-plan the approach — do not keep pushing.",
30
32
  "",
@@ -10,6 +10,11 @@ export function reviewSystemPrompt(taskDir: string): string {
10
10
  "- read, lsp, grep, find for understanding the code",
11
11
  "- gh pr view for GitHub PR context if a PR URL is mentioned",
12
12
  "",
13
+ "Spawn subagents as needed (subagent_type is REQUIRED — calls without it are rejected):",
14
+ '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
15
+ '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
16
+ "Spawn multiple Explore agents in parallel for broad searches.",
17
+ "",
13
18
  "Write your findings:",
14
19
  `- ${taskDir}/USER_REQUEST.md — update with a clear problem statement of issues found`,
15
20
  `- ${taskDir}/RESEARCH.md — detailed technical analysis`,
@@ -344,9 +344,9 @@ export async function resumeTask(
344
344
  if (step === "await_planners" || step === "await_reviewers") {
345
345
  ctx.ui.notify(`Resumed task. Awaiting subagents (${step}).`, "info");
346
346
  } else if (step === "apply_feedback") {
347
- pi.sendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Read reviewer outputs and apply feedback.`);
347
+ pi.sendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Read reviewer outputs and apply feedback.`, { deliverAs: "followUp" });
348
348
  } else {
349
- pi.sendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Continue working.`);
349
+ pi.sendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Continue working.`, { deliverAs: "followUp" });
350
350
  }
351
351
 
352
352
  return { ok: true };
@@ -591,8 +591,11 @@ function showUsage(ctx: any): void {
591
591
  getTotalInputTokens(): number; getTotalOutputTokens(): number;
592
592
  getTotalCacheReadTokens(): number; getTotalCacheWriteTokens(): number;
593
593
  getTotalCost(): number; getCacheHitRate(): number;
594
+ getMainInputTokens(): number; getMainOutputTokens(): number;
595
+ getMainCacheReadTokens(): number; getMainCacheWriteTokens(): number;
596
+ getMainCost(): number;
594
597
  getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; turns: number }>;
595
- getSubagentList(): Array<{ description: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cost: number; durationMs: number; toolUses: number }>;
598
+ getSubagentList(): Array<{ description: string; agentType: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cost: number; durationMs: number; toolUses: number }>;
596
599
  }
597
600
  | undefined;
598
601
 
@@ -601,44 +604,34 @@ function showUsage(ctx: any): void {
601
604
  return;
602
605
  }
603
606
 
604
- const mainInput = tracker.getTotalInputTokens();
605
- const mainOutput = tracker.getTotalOutputTokens();
606
- const mainCacheRead = tracker.getTotalCacheReadTokens();
607
- const mainCacheWrite = tracker.getTotalCacheWriteTokens();
608
- const mainCost = tracker.getTotalCost();
609
- const models = tracker.getPerModelUsage();
610
- const subagents = tracker.getSubagentList();
611
-
612
- let saInput = 0, saOutput = 0, saCacheRead = 0, saCacheWrite = 0, saCost = 0;
613
- for (const sa of subagents) {
614
- saInput += sa.inputTokens;
615
- saOutput += sa.outputTokens;
616
- saCacheRead += sa.cacheReadTokens;
617
- saCacheWrite += sa.cacheWriteTokens;
618
- saCost += sa.cost;
619
- }
620
-
621
- const totalInput = mainInput + saInput;
622
- const totalOutput = mainOutput + saOutput;
623
- const totalCacheRead = mainCacheRead + saCacheRead;
624
- const totalCost = mainCost + saCost;
607
+ const totalInput = tracker.getTotalInputTokens();
608
+ const totalOutput = tracker.getTotalOutputTokens();
609
+ const totalCacheRead = tracker.getTotalCacheReadTokens();
610
+ const totalCost = tracker.getTotalCost();
625
611
  const totalCacheRate = (totalCacheRead + totalInput) > 0
626
612
  ? totalCacheRead / (totalCacheRead + totalInput)
627
613
  : 0;
628
614
 
615
+ const mainInput = tracker.getMainInputTokens();
616
+ const mainOutput = tracker.getMainOutputTokens();
617
+ const mainCacheRead = tracker.getMainCacheReadTokens();
618
+ const mainCost = tracker.getMainCost();
619
+ const models = tracker.getPerModelUsage();
620
+ const subagents = tracker.getSubagentList();
621
+
629
622
  const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }>();
630
- for (const [modelId, usage] of Object.entries(models)) {
623
+ const mainModelEntries = Object.entries(models);
624
+ const mainTotalTokens = mainModelEntries.reduce((s, [, u]) => s + u.inputTokens + u.outputTokens, 0);
625
+ for (const [modelId, usage] of mainModelEntries) {
626
+ const modelTokens = usage.inputTokens + usage.outputTokens;
627
+ const modelCostShare = mainTotalTokens > 0 ? mainCost * (modelTokens / mainTotalTokens) : 0;
631
628
  byModel.set(modelId, {
632
629
  input: usage.inputTokens, output: usage.outputTokens,
633
- cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cost: 0,
630
+ cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cost: modelCostShare,
634
631
  });
635
632
  }
636
- if (mainCost > 0 && Object.keys(models).length === 1) {
637
- const entry = byModel.values().next().value;
638
- if (entry) entry.cost = mainCost;
639
- }
640
633
  for (const sa of subagents) {
641
- const key = sa.modelId !== "unknown" ? sa.modelId : sa.description;
634
+ const key = sa.modelId !== "unknown" ? sa.modelId : `subagent:${sa.description}`;
642
635
  const existing = byModel.get(key);
643
636
  if (existing) {
644
637
  existing.input += sa.inputTokens;
@@ -658,7 +651,7 @@ function showUsage(ctx: any): void {
658
651
  lines.push(` Input: ${formatTokenCount(totalInput)} tokens`);
659
652
  lines.push(` Output: ${formatTokenCount(totalOutput)} tokens`);
660
653
  if (totalCacheRead > 0) lines.push(` Cache: ⚡${Math.round(totalCacheRate * 100)}% hit rate`);
661
- if (totalCost > 0) lines.push(` Cost: $${totalCost.toFixed(3)}`);
654
+ if (totalCost > 0) lines.push(` Cost: $${totalCost.toFixed(2)}`);
662
655
 
663
656
  if (byModel.size > 0) {
664
657
  lines.push("");
@@ -667,31 +660,50 @@ function showUsage(ctx: any): void {
667
660
  const cr = (m.cacheRead + m.input) > 0 ? Math.round(m.cacheRead / (m.cacheRead + m.input) * 100) : 0;
668
661
  const parts = [`↑${formatTokenCount(m.input)}`, `↓${formatTokenCount(m.output)}`];
669
662
  if (cr > 0) parts.push(`⚡${cr}%`);
670
- if (m.cost > 0) parts.push(`$${m.cost.toFixed(3)}`);
663
+ if (m.cost > 0) parts.push(`$${m.cost.toFixed(2)}`);
671
664
  lines.push(` ${modelId}: ${parts.join(" ")}`);
672
665
  }
673
666
  }
674
667
 
675
668
  lines.push("");
676
669
  lines.push("By agent:");
677
- const mainModelEntries = Object.entries(models);
678
- if (mainModelEntries.length > 0) {
670
+ const agentModelNames = Object.keys(models);
671
+ if (agentModelNames.length > 0) {
679
672
  const mainParts = [`↑${formatTokenCount(mainInput)}`, `↓${formatTokenCount(mainOutput)}`];
680
673
  const mainCR = (mainCacheRead + mainInput) > 0 ? Math.round(mainCacheRead / (mainCacheRead + mainInput) * 100) : 0;
681
674
  if (mainCR > 0) mainParts.push(`⚡${mainCR}%`);
682
- if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(3)}`);
683
- const mainModel = mainModelEntries.map(([id]) => id).join(", ");
684
- lines.push(` Main (${mainModel}): ${mainParts.join(" ")}`);
675
+ if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(2)}`);
676
+ lines.push(` Main (${agentModelNames.join(", ")}): ${mainParts.join(" ")}`);
685
677
  }
678
+ const byAgentType = new Map<string, { input: number; output: number; cacheRead: number; cost: number; durationMs: number; toolUses: number; count: number }>();
686
679
  for (const sa of subagents) {
687
- const saCR = (sa.cacheReadTokens + sa.inputTokens) > 0
688
- ? Math.round(sa.cacheReadTokens / (sa.cacheReadTokens + sa.inputTokens) * 100) : 0;
689
- const parts = [`↑${formatTokenCount(sa.inputTokens)}`, `↓${formatTokenCount(sa.outputTokens)}`];
680
+ const key = sa.agentType || sa.description;
681
+ const existing = byAgentType.get(key);
682
+ if (existing) {
683
+ existing.input += sa.inputTokens;
684
+ existing.output += sa.outputTokens;
685
+ existing.cacheRead += sa.cacheReadTokens;
686
+ existing.cost += sa.cost;
687
+ existing.durationMs += sa.durationMs;
688
+ existing.toolUses += sa.toolUses;
689
+ existing.count += 1;
690
+ } else {
691
+ byAgentType.set(key, {
692
+ input: sa.inputTokens, output: sa.outputTokens, cacheRead: sa.cacheReadTokens,
693
+ cost: sa.cost, durationMs: sa.durationMs, toolUses: sa.toolUses, count: 1,
694
+ });
695
+ }
696
+ }
697
+ for (const [agentType, agg] of byAgentType) {
698
+ const saCR = (agg.cacheRead + agg.input) > 0
699
+ ? Math.round(agg.cacheRead / (agg.cacheRead + agg.input) * 100) : 0;
700
+ const parts = [`↑${formatTokenCount(agg.input)}`, `↓${formatTokenCount(agg.output)}`];
690
701
  if (saCR > 0) parts.push(`⚡${saCR}%`);
691
- if (sa.cost > 0) parts.push(`$${sa.cost.toFixed(3)}`);
692
- if (sa.durationMs > 0) parts.push(formatDuration(sa.durationMs));
693
- if (sa.toolUses > 0) parts.push(`${sa.toolUses} tools`);
694
- lines.push(` ${sa.description}: ${parts.join(" ")}`);
702
+ if (agg.cost > 0) parts.push(`$${agg.cost.toFixed(2)}`);
703
+ if (agg.durationMs > 0) parts.push(formatDuration(agg.durationMs));
704
+ if (agg.toolUses > 0) parts.push(`${agg.toolUses} tools`);
705
+ const countSuffix = agg.count > 1 ? ` (×${agg.count})` : "";
706
+ lines.push(` ${agentType}${countSuffix}: ${parts.join(" ")}`);
695
707
  }
696
708
 
697
709
  ctx.ui.notify(lines.join("\n"), "info");
@@ -699,10 +711,12 @@ function showUsage(ctx: any): void {
699
711
 
700
712
  async function showSettingsMenu(orchestrator: Orchestrator, ctx: any, showFlant = true): Promise<typeof BACK> {
701
713
  while (true) {
702
- const options: OptionInput[] = [
703
- { title: "Usage", description: "Show session token usage and cost breakdown" },
704
- { title: "LSP", description: "Language server status and controls" },
705
- ];
714
+ const options: OptionInput[] = [];
715
+ if (orchestrator.active) {
716
+ options.push({ title: "Task status", description: "Show current task phase, step, and timing" });
717
+ }
718
+ options.push({ title: "Usage", description: "Show session token usage and cost breakdown" });
719
+ options.push({ title: "LSP", description: "Language server status and controls" });
706
720
  if (showFlant) {
707
721
  options.push({ title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" });
708
722
  }
@@ -710,6 +724,10 @@ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any, showFlant
710
724
 
711
725
  const choice = await selectOption(ctx, "Settings", options);
712
726
  if (!choice || choice === "Back") return BACK;
727
+ if (choice === "Task status") {
728
+ showStatus(orchestrator, ctx);
729
+ continue;
730
+ }
713
731
  if (choice === "Usage") {
714
732
  showUsage(ctx);
715
733
  continue;
@@ -878,9 +896,10 @@ async function detectCurrentPrContext(orchestrator: Orchestrator): Promise<{ prU
878
896
  }
879
897
  }
880
898
 
881
- async function openReviewTaskInPlannotator(orchestrator: Orchestrator): Promise<string> {
882
- if (!orchestrator.active) return "No active task.";
899
+ async function openCodeReviewInPlannotator(orchestrator: Orchestrator, diffType?: string, defaultBranch?: string): Promise<string> {
883
900
  const payload: Record<string, unknown> = { cwd: orchestrator.cwd };
901
+ if (diffType) payload.diffType = diffType;
902
+ if (defaultBranch) payload.defaultBranch = defaultBranch;
884
903
 
885
904
  return await new Promise((resolve) => {
886
905
  let handled = false;
@@ -1109,33 +1128,20 @@ export async function showActiveTaskMenu(
1109
1128
  const options: OptionInput[] = [];
1110
1129
  options.push(opt("Finish", "Complete, pause, or continue to next phase"));
1111
1130
  if (!waiting) {
1112
- options.push(opt(autoLabel, "Run automated review with configured reviewers"));
1113
- options.push(opt(deepLabel, "Run automated review with higher thinking level"));
1114
- if (hasPlannotator) {
1115
- if (isReviewPhase) {
1116
- options.push(opt("Review in Plannotator", "Open visual diff review in browser"));
1117
- } else {
1118
- options.push(opt("Review in Plannotator", "Open visual review in browser"));
1119
- }
1120
- if (!isReviewPhase) {
1121
- options.push(opt("Review on my own", "Review manually, then continue"));
1122
- }
1123
- }
1131
+ options.push(opt("Review", "Auto review, Plannotator, or manual review"));
1124
1132
  }
1125
1133
  options.push(opt("Subagents", "Manage running agents"));
1126
- options.push(opt("Status", "Show current task phase, step, and timing"));
1127
- options.push(opt("Settings", "LSP and other configuration"));
1134
+ options.push(opt("Settings", "Task status, usage, and LSP"));
1128
1135
  options.push(opt("Back", "Return to the prompt and keep working"));
1129
1136
 
1130
- const choice = await selectOption(ctx, summary, options);
1137
+ const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
1138
+ if (summary !== "/pp") headerLines.push(`\n${summary}`);
1139
+ const menuTitle = headerLines.join("");
1140
+ const choice = await selectOption(ctx, menuTitle, options);
1131
1141
  if (!choice || choice === "Back") {
1132
1142
  return "";
1133
1143
  }
1134
1144
 
1135
- if (choice === "Status") {
1136
- showStatus(orchestrator, ctx);
1137
- continue;
1138
- }
1139
1145
  if (choice === "Subagents") {
1140
1146
  await showSubagentsMenu(ctx);
1141
1147
  continue;
@@ -1179,17 +1185,95 @@ export async function showActiveTaskMenu(
1179
1185
  return "";
1180
1186
  }
1181
1187
 
1182
- if (choice === "Review in Plannotator" && isReviewPhase) {
1183
- const text = await openReviewTaskInPlannotator(orchestrator);
1184
- ctx.ui.notify(text, "info");
1185
- continue;
1186
- }
1188
+ if (choice === "Review") {
1189
+ const reviewOptions: OptionInput[] = [
1190
+ opt(autoLabel, "Run automated review with configured reviewers"),
1191
+ opt(deepLabel, "Run automated review with higher thinking level"),
1192
+ ];
1193
+ if (hasPlannotator) {
1194
+ reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
1195
+ }
1196
+ reviewOptions.push(opt("Review on my own", "Review manually, then continue"));
1197
+ reviewOptions.push(opt("Back", "Return to the previous menu"));
1198
+
1199
+ const reviewChoice = await selectOption(ctx, "Review", reviewOptions);
1200
+ if (!reviewChoice || reviewChoice === "Back") continue;
1201
+
1202
+ if (reviewChoice === "Review in Plannotator") {
1203
+ if (phase === "plan") {
1204
+ finalizeReviewCycle(task);
1205
+ const text = await enterReviewCycle(orchestrator, ctx, "plannotator");
1206
+ const curStep = orchestrator.active?.state.step;
1207
+ if (curStep === "await_reviewers") return "";
1208
+ const handled = handleReviewResult(ctx, text);
1209
+ if (handled.continueLoop) continue;
1210
+ return handled.text ?? text;
1211
+ }
1212
+ const diffChoice = await selectOption(ctx, "Review in Plannotator", [
1213
+ opt("All branch changes", "Committed changes vs base branch"),
1214
+ opt("Last commit", "Changes in the most recent commit"),
1215
+ opt("Recent commits", "Choose how many recent commits to review"),
1216
+ opt("Uncommitted changes", "Working directory changes"),
1217
+ opt("Back", "Return to the previous menu"),
1218
+ ]);
1219
+ if (!diffChoice || diffChoice === "Back") continue;
1220
+
1221
+ let diffType: string | undefined;
1222
+ let defaultBranch: string | undefined;
1223
+ if (diffChoice === "All branch changes") {
1224
+ diffType = "branch";
1225
+ } else if (diffChoice === "Last commit") {
1226
+ diffType = "last-commit";
1227
+ } else if (diffChoice === "Recent commits") {
1228
+ let commits: Array<{ hash: string; message: string; age: string }> = [];
1229
+ try {
1230
+ const logResult = await orchestrator.pi.exec(
1231
+ "git", ["log", "--oneline", "--format=%h\t%s\t%cr", "-15"],
1232
+ { cwd: orchestrator.cwd, timeout: 5000 },
1233
+ );
1234
+ if (logResult.code === 0 && logResult.stdout.trim()) {
1235
+ commits = logResult.stdout.trim().split("\n").map((line) => {
1236
+ const [hash, message, age] = line.split("\t");
1237
+ return { hash: hash || "", message: message || "", age: age || "" };
1238
+ }).filter((c) => c.hash);
1239
+ }
1240
+ } catch {}
1241
+ if (commits.length === 0) {
1242
+ ctx.ui.notify("No commits found.", "info");
1243
+ continue;
1244
+ }
1245
+ const commitOptions: OptionInput[] = commits.map((c) => ({
1246
+ title: `${c.hash} ${c.message}`,
1247
+ description: c.age,
1248
+ }));
1249
+ commitOptions.push(opt("Back", "Return to the previous menu"));
1250
+ const picked = await selectOption(ctx, "Review changes since:", commitOptions);
1251
+ if (!picked || picked === "Back") continue;
1252
+ const pickedHash = picked.split(" ")[0];
1253
+ if (!pickedHash) continue;
1254
+ diffType = "branch";
1255
+ defaultBranch = pickedHash;
1256
+ } else {
1257
+ diffType = "uncommitted";
1258
+ }
1187
1259
 
1188
- finalizeReviewCycle(task);
1260
+ const text = await openCodeReviewInPlannotator(orchestrator, diffType, defaultBranch);
1261
+ ctx.ui.notify(text, "info");
1262
+ continue;
1263
+ }
1264
+
1265
+ if (reviewChoice === "Review on my own") {
1266
+ if (phase === "plan") {
1267
+ setStep(orchestrator, "synthesize");
1268
+ } else {
1269
+ setStep(orchestrator, "llm_work");
1270
+ }
1271
+ return continueMessage;
1272
+ }
1189
1273
 
1190
- if (choice === autoLabel || choice === deepLabel || choice === "Review in Plannotator") {
1191
- const kind = choice === autoLabel ? "auto" as const : choice === deepLabel ? "auto-deep" as const : "plannotator" as const;
1192
- if (kind !== "plannotator" && !hasEnabledReviewers(orchestrator, kind)) {
1274
+ finalizeReviewCycle(task);
1275
+ const kind = reviewChoice === autoLabel ? "auto" as const : "auto-deep" as const;
1276
+ if (!hasEnabledReviewers(orchestrator, kind)) {
1193
1277
  const label = phase === "brainstorm" ? "brainstorm" : phase === "plan" ? "plan" : "code";
1194
1278
  ctx.ui.notify(`No ${label} reviewers enabled.`, "info");
1195
1279
  continue;
@@ -1201,15 +1285,6 @@ export async function showActiveTaskMenu(
1201
1285
  if (handled.continueLoop) continue;
1202
1286
  return handled.text ?? text;
1203
1287
  }
1204
-
1205
- if (choice === "Review on my own") {
1206
- if (phase === "plan") {
1207
- setStep(orchestrator, "synthesize");
1208
- } else {
1209
- setStep(orchestrator, "llm_work");
1210
- }
1211
- return continueMessage;
1212
- }
1213
1288
  }
1214
1289
  }
1215
1290
 
@@ -1217,6 +1292,6 @@ export async function showPpMenu(orchestrator: Orchestrator, ctx: any, mode: Men
1217
1292
  if (!orchestrator.active) {
1218
1293
  return showNoActiveMenu(orchestrator, ctx);
1219
1294
  }
1220
- const text = await showActiveTaskMenu(orchestrator, ctx, "Choose next action", mode);
1295
+ const text = await showActiveTaskMenu(orchestrator, ctx, "/pp", mode);
1221
1296
  return text || undefined;
1222
1297
  }
@@ -12,13 +12,18 @@ export interface ModelUsage {
12
12
 
13
13
  export interface UsageTracker {
14
14
  recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void;
15
- recordSubagentCompletion(tokens: { input?: number; output?: number; total?: number }, cost?: number, meta?: { description?: string; modelId?: string; durationMs?: number; toolUses?: number }): void;
15
+ recordSubagentCompletion(tokens: { input?: number; output?: number; total?: number }, cost?: number, meta?: { description?: string; agentType?: string; modelId?: string; durationMs?: number; toolUses?: number }): void;
16
16
  loadFromSummary(summary: Record<string, unknown>): void;
17
17
  getTotalInputTokens(): number;
18
18
  getTotalOutputTokens(): number;
19
19
  getTotalCacheReadTokens(): number;
20
20
  getTotalCacheWriteTokens(): number;
21
21
  getTotalCost(): number;
22
+ getMainInputTokens(): number;
23
+ getMainOutputTokens(): number;
24
+ getMainCacheReadTokens(): number;
25
+ getMainCacheWriteTokens(): number;
26
+ getMainCost(): number;
22
27
  getCacheHitRate(): number;
23
28
  getPerModelUsage(): Record<string, ModelUsage>;
24
29
  getSubagentTotals(): { inputTokens: number; outputTokens: number; cost: number };
@@ -29,6 +34,7 @@ export interface UsageTracker {
29
34
 
30
35
  export interface SubagentUsage {
31
36
  description: string;
37
+ agentType: string;
32
38
  modelId: string;
33
39
  inputTokens: number;
34
40
  outputTokens: number;
@@ -124,7 +130,7 @@ export function createUsageTracker(): UsageTracker {
124
130
  recordSubagentCompletion(
125
131
  tokens: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; total?: number; cost?: number },
126
132
  cost?: number,
127
- meta?: { description?: string; modelId?: string; durationMs?: number; toolUses?: number },
133
+ meta?: { description?: string; agentType?: string; modelId?: string; durationMs?: number; toolUses?: number },
128
134
  ): void {
129
135
  const safeInput = toFiniteNumber(tokens.input);
130
136
  const safeOutput = toFiniteNumber(tokens.output);
@@ -141,6 +147,7 @@ export function createUsageTracker(): UsageTracker {
141
147
 
142
148
  state.subagents.push({
143
149
  description: meta?.description ?? "unknown",
150
+ agentType: meta?.agentType ?? "unknown",
144
151
  modelId: meta?.modelId ?? "unknown",
145
152
  inputTokens: effectiveInput,
146
153
  outputTokens: safeOutput,
@@ -168,6 +175,7 @@ export function createUsageTracker(): UsageTracker {
168
175
  for (const sa of summary.subagents as Record<string, unknown>[]) {
169
176
  const entry: SubagentUsage = {
170
177
  description: typeof sa.description === "string" ? sa.description : "unknown",
178
+ agentType: typeof sa.agentType === "string" ? sa.agentType : "unknown",
171
179
  modelId: typeof sa.modelId === "string" ? sa.modelId : "unknown",
172
180
  inputTokens: toFiniteNumber(sa.inputTokens),
173
181
  outputTokens: toFiniteNumber(sa.outputTokens),
@@ -200,11 +208,11 @@ export function createUsageTracker(): UsageTracker {
200
208
  },
201
209
 
202
210
  getTotalInputTokens(): number {
203
- return state.totalInputTokens;
211
+ return state.totalInputTokens + state.subagentInputTokens;
204
212
  },
205
213
 
206
214
  getTotalOutputTokens(): number {
207
- return state.totalOutputTokens;
215
+ return state.totalOutputTokens + state.subagentOutputTokens;
208
216
  },
209
217
 
210
218
  getTotalCacheReadTokens(): number {
@@ -216,13 +224,35 @@ export function createUsageTracker(): UsageTracker {
216
224
  },
217
225
 
218
226
  getTotalCost(): number {
227
+ return state.totalCost + state.subagentCost;
228
+ },
229
+
230
+ getMainInputTokens(): number {
231
+ return state.totalInputTokens;
232
+ },
233
+
234
+ getMainOutputTokens(): number {
235
+ return state.totalOutputTokens;
236
+ },
237
+
238
+ getMainCacheReadTokens(): number {
239
+ return state.totalCacheReadTokens;
240
+ },
241
+
242
+ getMainCacheWriteTokens(): number {
243
+ return state.totalCacheWriteTokens;
244
+ },
245
+
246
+ getMainCost(): number {
219
247
  return state.totalCost;
220
248
  },
221
249
 
222
250
  getCacheHitRate(): number {
223
- const denominator = state.totalCacheReadTokens + state.totalInputTokens;
251
+ const totalInput = state.totalInputTokens + state.subagentInputTokens;
252
+ const totalCacheRead = state.totalCacheReadTokens;
253
+ const denominator = totalCacheRead + totalInput;
224
254
  if (denominator <= 0) return 0;
225
- return state.totalCacheReadTokens / denominator;
255
+ return totalCacheRead / denominator;
226
256
  },
227
257
 
228
258
  getPerModelUsage(): Record<string, ModelUsage> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",