@ilya-lesikov/pi-pi 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/3p/pi-ask-user/index.ts +1 -1
  2. package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
  3. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
  4. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
  5. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
  6. package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
  7. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
  8. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
  9. package/3p/pi-subagents/src/agent-manager.ts +34 -1
  10. package/3p/pi-subagents/src/agent-runner.ts +66 -33
  11. package/3p/pi-subagents/src/index.ts +3 -38
  12. package/3p/pi-subagents/src/types.ts +4 -0
  13. package/extensions/orchestrator/agents/advisor.ts +35 -0
  14. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
  15. package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
  16. package/extensions/orchestrator/agents/constraints.test.ts +44 -0
  17. package/extensions/orchestrator/agents/constraints.ts +3 -0
  18. package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
  19. package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
  20. package/extensions/orchestrator/agents/planner.ts +9 -8
  21. package/extensions/orchestrator/agents/prompts.test.ts +120 -0
  22. package/extensions/orchestrator/agents/reviewer.ts +48 -0
  23. package/extensions/orchestrator/agents/task.ts +6 -20
  24. package/extensions/orchestrator/agents/tool-routing.ts +23 -1
  25. package/extensions/orchestrator/command-handlers.ts +1 -1
  26. package/extensions/orchestrator/config.ts +8 -4
  27. package/extensions/orchestrator/context.test.ts +54 -0
  28. package/extensions/orchestrator/context.ts +65 -2
  29. package/extensions/orchestrator/custom-footer.ts +5 -2
  30. package/extensions/orchestrator/doctor.test.ts +3 -1
  31. package/extensions/orchestrator/doctor.ts +40 -2
  32. package/extensions/orchestrator/event-handlers.test.ts +97 -1
  33. package/extensions/orchestrator/event-handlers.ts +222 -48
  34. package/extensions/orchestrator/flant-infra.test.ts +312 -0
  35. package/extensions/orchestrator/flant-infra.ts +407 -44
  36. package/extensions/orchestrator/index.ts +1 -1
  37. package/extensions/orchestrator/integration.test.ts +312 -18
  38. package/extensions/orchestrator/messages.test.ts +30 -0
  39. package/extensions/orchestrator/messages.ts +6 -0
  40. package/extensions/orchestrator/model-registry.test.ts +124 -13
  41. package/extensions/orchestrator/model-registry.ts +91 -33
  42. package/extensions/orchestrator/orchestrator.test.ts +113 -0
  43. package/extensions/orchestrator/orchestrator.ts +163 -3
  44. package/extensions/orchestrator/phases/brainstorm.ts +9 -20
  45. package/extensions/orchestrator/phases/implementation.test.ts +11 -0
  46. package/extensions/orchestrator/phases/implementation.ts +4 -6
  47. package/extensions/orchestrator/phases/planning.test.ts +16 -0
  48. package/extensions/orchestrator/phases/planning.ts +11 -4
  49. package/extensions/orchestrator/phases/review-task.ts +1 -4
  50. package/extensions/orchestrator/phases/review.test.ts +62 -0
  51. package/extensions/orchestrator/phases/review.ts +58 -15
  52. package/extensions/orchestrator/plannotator.ts +9 -6
  53. package/extensions/orchestrator/pp-menu.test.ts +74 -1
  54. package/extensions/orchestrator/pp-menu.ts +366 -94
  55. package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
  56. package/extensions/orchestrator/pp-state-tools.ts +249 -0
  57. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
  58. package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
  59. package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
  60. package/extensions/orchestrator/state.ts +41 -20
  61. package/extensions/orchestrator/test-helpers.ts +18 -3
  62. package/extensions/orchestrator/usage-tracker.test.ts +131 -3
  63. package/extensions/orchestrator/usage-tracker.ts +78 -11
  64. package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
  65. package/extensions/orchestrator/validate-artifacts.ts +2 -2
  66. package/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ import type { PiPiConfig } from "../config.js";
2
+ import { resolveModel } from "../model-registry.js";
3
+ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-routing.js";
4
+
5
+ export function createReviewerAgent(config: PiPiConfig) {
6
+ return {
7
+ frontmatter: {
8
+ description: "Code reviewer for changes/diffs with severity-rated findings — spawn only when the user asks for a review (pi-pi)",
9
+ tools: `read, bash, grep, find, ls, lsp, ast_search, ${ALL_CBM_TOOLS}, ${EXA_TOOLS}`,
10
+ model: resolveModel(config.agents.subagents.simple.reviewer.model),
11
+ thinking: config.agents.subagents.simple.reviewer.thinking,
12
+ max_turns: 120,
13
+ prompt_mode: "replace",
14
+ },
15
+ prompt: [
16
+ "<constraints>",
17
+ "You are a code REVIEWER. You review implementation changes for bugs, correctness, and quality.",
18
+ "You are READ-ONLY: you MUST NOT implement, fix, or modify any source code.",
19
+ "Begin your review with the verdict on the VERY FIRST LINE: `VERDICT: APPROVE` or `VERDICT: NEEDS_CHANGES`.",
20
+ "</constraints>",
21
+ "",
22
+ PRINCIPLES_BLOCK,
23
+ "",
24
+ TOOLS_BLOCK,
25
+ "",
26
+ "<task>",
27
+ "Steps:",
28
+ "1. Run `git diff` to see all changes (try HEAD~1, main, or the appropriate base).",
29
+ "2. Run cbm_changes for symbol-level impact and blast radius.",
30
+ "3. Read changed files for full context; run lsp diagnostics on them.",
31
+ "4. Use lsp findReferences to check callers of modified functions.",
32
+ "",
33
+ "Review criteria: logic errors, off-by-ones, null/edge handling, race conditions; correctness vs intent; error handling and type safety; missing or untested paths.",
34
+ "",
35
+ "Evidence: every CRITICAL or MAJOR finding MUST cite file:line or quoted code. Never assert a problem without reading the code. Low-confidence concerns go under OPEN QUESTIONS.",
36
+ "",
37
+ "Format — verdict on the FIRST LINE, then:",
38
+ "VERDICT: APPROVE | NEEDS_CHANGES",
39
+ "- CRITICAL: (must fix — file:line evidence)",
40
+ "- MAJOR: (should fix — evidence)",
41
+ "- MINOR: (nice to have)",
42
+ "- OPEN QUESTIONS: (low-confidence / speculative)",
43
+ "",
44
+ "Return the full review as your result.",
45
+ "</task>",
46
+ ].join("\n"),
47
+ };
48
+ }
@@ -2,11 +2,7 @@ import type { PiPiConfig } from "../config.js";
2
2
  import { resolveModel } from "../model-registry.js";
3
3
  import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK, FAILURE_RECOVERY } from "./tool-routing.js";
4
4
 
5
- export function createTaskAgent(
6
- config: PiPiConfig,
7
- subtaskDescription: string,
8
- taskArtifacts: { userRequest: string; synthesizedPlan: string },
9
- ) {
5
+ export function createTaskAgent(config: PiPiConfig) {
10
6
  return {
11
7
  frontmatter: {
12
8
  description: "Implementation subtask (pi-pi)",
@@ -32,24 +28,14 @@ export function createTaskAgent(
32
28
  FAILURE_RECOVERY,
33
29
  "",
34
30
  "<task>",
35
- "- subagent_type is REQUIRED when spawning subagents — calls without it are rejected:",
36
- ' Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
37
- ' Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
31
+ "- You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
32
+ ' Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
33
+ ' Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
34
+ " Do NOT spawn task, advisor, deep-debugger, or reviewer subagents.",
38
35
  "- Before modifying a function, use lsp findReferences to understand all callers",
39
36
  "- After editing files, run lsp diagnostics and fix errors before moving on",
37
+ "- Your subtask and task context (USER_REQUEST, RESEARCH, and a manifest of additional documents) are provided in the spawn message. Read the manifested files from disk if relevant.",
40
38
  "</task>",
41
- "",
42
- // --- dynamic suffix ---
43
- "=== YOUR SUBTASK ===",
44
- subtaskDescription,
45
- "",
46
- "=== USER REQUEST (for context) ===",
47
- taskArtifacts.userRequest,
48
- "",
49
- "=== SYNTHESIZED PLAN (for broader context) ===",
50
- taskArtifacts.synthesizedPlan,
51
- "",
52
- "The artifacts above are already in your context. Do NOT re-read them from disk.",
53
39
  ].join("\n"),
54
40
  };
55
41
  }
@@ -7,8 +7,9 @@ export const PRINCIPLES_BLOCK = [
7
7
  "- Understand before modifying. Read the code, trace callers, check types BEFORE editing. Compiling ≠ correct.",
8
8
  "- Smallest viable change. Do what was asked, nothing more. Don't broaden scope, don't refactor adjacent code.",
9
9
  "- No temporary artifacts. No console.log, TODO, HACK, debugger, or commented-out code left behind.",
10
+ "- DO NOT WRITE COMMENTS. This is a hard rule, not a preference. Almost every comment an LLM writes is noise: it restates the code, repeats the function/variable name, narrates the obvious, or labels sections. NEVER write any of those. The ONLY allowed comments are (1) a genuine WHY that the code cannot express — a non-obvious constraint, workaround, or gotcha a reader would otherwise get wrong, or (2) required public-API/doc-comment syntax. If a comment restates WHAT the code does, delete it. When unsure, do not comment. Match the existing comment density of the surrounding code — if neighbors have none, add none.",
10
11
  "- Evidence over claims. 'It should work' is not proof. Show fresh tool output (lsp diagnostics, test results, build output).",
11
- "- Match existing patterns. Mirror the codebase's naming, error handling, imports, and structure exactly.",
12
+ "- Match existing patterns. Before adding a type, function, or user-facing value, find how the codebase already solves the most similar problem — search by behavior, not by filename — and mirror its shape, naming, error handling, and conventions. Reading one neighboring file is not enough.",
12
13
  "- Be concise and dense: minimum words, no preamble/filler/restatement. Don't narrate what you're about to do or just did.",
13
14
  "- Think critically. Push back when something seems wrong, and state concerns before implementing.",
14
15
  "</principles>",
@@ -76,3 +77,24 @@ const TOOL_ROUTING_BODY = [
76
77
  ];
77
78
 
78
79
  export const TOOLS_BLOCK = ["<tools>", ...TOOL_ROUTING_BODY, "</tools>"].join("\n");
80
+
81
+ // Single source of truth for main-agent delegation guidance. Injected into the MAIN
82
+ // agent prompt in every phase. Registry-consistent lowercase agent names.
83
+ export const DELEGATION_BLOCK = [
84
+ "<delegation>",
85
+ "Prefer delegating over doing wide or deep work yourself — subagents run in parallel, dig",
86
+ "deeper, and preserve your context. subagent_type is REQUIRED (calls without it are rejected).",
87
+ "",
88
+ '- Find or locate code ("where is X", "how does Y connect", map a flow) → explore',
89
+ "- External library / API / documentation knowledge → librarian",
90
+ '- Judgment call (design tradeoff, "is this correct", "why is this broken") → advisor',
91
+ "- Root-cause a test or build that keeps failing after a quick attempt → deep-debugger",
92
+ "- A parallel, self-contained implementation subtask → task",
93
+ "- A code review of your changes — ONLY when the user explicitly asks for one → reviewer",
94
+ "",
95
+ "explore FINDS; advisor JUDGES. Spawn several explores in parallel for broad searches.",
96
+ "Don't use advisor for lookups, or deep-debugger for trivial errors. deep-debugger diagnoses",
97
+ "only — it must NOT write the actual fix. Do NOT spawn reviewer unless the user explicitly",
98
+ "asks for a review (the automatic review panel already covers implement/review phases).",
99
+ "</delegation>",
100
+ ].join("\n");
@@ -151,7 +151,7 @@ export function registerCommandHandlers(orchestrator: Orchestrator): void {
151
151
  const { showPpMenu } = await import("./pp-menu.js");
152
152
  const text = await showPpMenu(orchestrator, ctx, "command");
153
153
  if (text) {
154
- orchestrator.safeSendUserMessage(`[PI-PI] ${text}`);
154
+ orchestrator.safeSendUserMessage(text.startsWith("[PI-PI]") ? text : `[PI-PI] ${text}`);
155
155
  }
156
156
  },
157
157
  });
@@ -6,8 +6,8 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
6
  import { isValidLogLevel, getLogger, type LogLevel } from "./log.js";
7
7
 
8
8
  export type DurationValue = string | number;
9
- export type OrchestratorRole = "implement" | "plan" | "debug" | "brainstorm" | "review";
10
- export type SimpleSubagentRole = "explore" | "librarian" | "task";
9
+ export type OrchestratorRole = "implement" | "plan" | "debug" | "brainstorm" | "review" | "quick";
10
+ export type SimpleSubagentRole = "explore" | "librarian" | "task" | "advisor" | "deep-debugger" | "reviewer";
11
11
  export type PresetGroupKey = "planners" | "codeReviewers" | "planReviewers" | "brainstormReviewers";
12
12
 
13
13
  export interface AgentConfig {
@@ -91,8 +91,8 @@ export type TimeoutConfig = NormalizedPiPiConfig["performance"]["internals"];
91
91
 
92
92
  export const PRESET_GROUPS = ["planners", "codeReviewers", "planReviewers", "brainstormReviewers"] as const;
93
93
 
94
- const ORCHESTRATOR_ROLES: OrchestratorRole[] = ["implement", "plan", "debug", "brainstorm", "review"];
95
- const SIMPLE_SUBAGENT_ROLES: SimpleSubagentRole[] = ["explore", "librarian", "task"];
94
+ const ORCHESTRATOR_ROLES: OrchestratorRole[] = ["implement", "plan", "debug", "brainstorm", "review", "quick"];
95
+ const SIMPLE_SUBAGENT_ROLES: SimpleSubagentRole[] = ["explore", "librarian", "task", "advisor", "deep-debugger", "reviewer"];
96
96
 
97
97
  const DEFAULT_CONFIG: PiPiConfig = {
98
98
  general: {
@@ -108,12 +108,16 @@ const DEFAULT_CONFIG: PiPiConfig = {
108
108
  debug: { model: "openai/gpt-latest", thinking: "high" },
109
109
  brainstorm: { model: "anthropic/claude-opus-latest", thinking: "high" },
110
110
  review: { model: "anthropic/claude-opus-latest", thinking: "high" },
111
+ quick: { model: "anthropic/claude-opus-latest", thinking: "high" },
111
112
  },
112
113
  subagents: {
113
114
  simple: {
114
115
  explore: { model: "google/gemini-flash-latest", thinking: "low" },
115
116
  librarian: { model: "google/gemini-flash-latest", thinking: "medium" },
116
117
  task: { model: "anthropic/claude-opus-latest", thinking: "medium" },
118
+ advisor: { model: "openai/gpt-latest", thinking: "high" },
119
+ "deep-debugger": { model: "openai/gpt-latest", thinking: "high" },
120
+ reviewer: { model: "openai/gpt-latest", thinking: "high" },
117
121
  },
118
122
  presetGroups: {
119
123
  planners: {
@@ -14,6 +14,8 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
14
14
  import {
15
15
  getContextDirs,
16
16
  getLatestSynthesizedPlan,
17
+ getArtifactManifest,
18
+ formatManifestBlock,
17
19
  getPhaseArtifacts,
18
20
  loadAllContextFiles,
19
21
  loadContextFiles,
@@ -377,6 +379,58 @@ describe("getLatestSynthesizedPlan", () => {
377
379
  });
378
380
  });
379
381
 
382
+ describe("getArtifactManifest", () => {
383
+ it("returns empty when no artifacts or plan exist", () => {
384
+ const taskDir = makeTempDir();
385
+ expect(getArtifactManifest(taskDir)).toEqual([]);
386
+ });
387
+
388
+ it("extracts titles from artifact headings with a filename fallback", () => {
389
+ const taskDir = makeTempDir();
390
+ const artifactsDir = join(taskDir, "artifacts");
391
+ mkdirSync(artifactsDir, { recursive: true });
392
+ writeFileSync(join(artifactsDir, "design.md"), "# My Design\n\nbody", "utf-8");
393
+ writeFileSync(join(artifactsDir, "notitle.md"), "no heading here", "utf-8");
394
+
395
+ const manifest = getArtifactManifest(taskDir);
396
+ expect(manifest).toEqual([
397
+ { title: "My Design", path: join(artifactsDir, "design.md") },
398
+ { title: "artifacts/notitle.md", path: join(artifactsDir, "notitle.md") },
399
+ ]);
400
+ });
401
+
402
+ it("includes the latest synthesized plan with its REAL path, regardless of phase", () => {
403
+ const taskDir = makeTempDir();
404
+ const plansDir = join(taskDir, "plans");
405
+ mkdirSync(plansDir, { recursive: true });
406
+ writeFileSync(join(plansDir, "001_synthesized.md"), "old", "utf-8");
407
+ writeFileSync(join(plansDir, "010_synthesized.md"), "new", "utf-8");
408
+
409
+ const manifest = getArtifactManifest(taskDir);
410
+ expect(manifest).toEqual([
411
+ { title: "Synthesized implementation plan", path: join(plansDir, "010_synthesized.md") },
412
+ ]);
413
+ });
414
+ });
415
+
416
+ describe("formatManifestBlock", () => {
417
+ it("emits the do-not-re-read line only when the manifest is empty", () => {
418
+ const block = formatManifestBlock([]);
419
+ expect(block).toContain("Do NOT re-read them from disk");
420
+ expect(block).not.toContain("read them from disk with the read tool");
421
+ });
422
+
423
+ it("lists each manifest entry's path and title for on-demand reading", () => {
424
+ const block = formatManifestBlock([
425
+ { title: "Design", path: "/t/artifacts/design.md" },
426
+ { title: "Synthesized implementation plan", path: "/t/plans/1_synthesized.md" },
427
+ ]);
428
+ expect(block).toContain("do NOT re-read them from disk");
429
+ expect(block).toContain("- /t/artifacts/design.md — Design");
430
+ expect(block).toContain("- /t/plans/1_synthesized.md — Synthesized implementation plan");
431
+ });
432
+ });
433
+
380
434
  describe("context regressions", () => {
381
435
  it("parses bracket array values without truncation when closing bracket is missing", () => {
382
436
  const cwd = makeTempDir();
@@ -5,7 +5,7 @@ import type { RepoInfo } from "./repo-utils.js";
5
5
  import type { Phase } from "./state.js";
6
6
  import { getLogger } from "./log.js";
7
7
 
8
- type AgentType = "main" | "explore" | "librarian" | "planner" | "planReviewer" | "task" | "codeReviewer" | "brainstormReviewer";
8
+ type AgentType = "main" | "explore" | "librarian" | "planner" | "planReviewer" | "task" | "codeReviewer" | "brainstormReviewer" | "advisor" | "deep-debugger" | "reviewer";
9
9
  type AgentGroup = "all" | "subagents";
10
10
  type InjectMode = "system" | "context";
11
11
  type PhaseFilter = "brainstorm" | "debug" | "plan" | "implement" | "review";
@@ -30,7 +30,7 @@ interface Frontmatter {
30
30
  }
31
31
 
32
32
  const VALID_INJECT_MODES: readonly string[] = ["system", "context"];
33
- const VALID_AGENTS: readonly string[] = ["main", "explore", "librarian", "planner", "planReviewer", "task", "codeReviewer", "brainstormReviewer"];
33
+ const VALID_AGENTS: readonly string[] = ["main", "explore", "librarian", "planner", "planReviewer", "task", "codeReviewer", "brainstormReviewer", "advisor", "deep-debugger", "reviewer"];
34
34
  const VALID_AGENT_GROUPS: readonly string[] = ["all", "subagents"];
35
35
  const VALID_PHASES: readonly string[] = ["brainstorm", "debug", "plan", "implement", "review"];
36
36
  const VALID_VENDORS: readonly string[] = ["anthropic", "openai", "google", "unknown"];
@@ -279,6 +279,69 @@ export function getLatestSynthesizedPlan(taskDir: string): string | null {
279
279
  return readFileSync(join(plansDir, synthFiles[synthFiles.length - 1]), "utf-8");
280
280
  }
281
281
 
282
+ function getLatestSynthesizedPlanPath(taskDir: string): string | null {
283
+ const plansDir = join(taskDir, "plans");
284
+ if (!existsSync(plansDir)) return null;
285
+
286
+ const synthFiles = readdirSync(plansDir)
287
+ .filter((f) => f.includes("synthesized"))
288
+ .sort(sortByTimestampPrefix);
289
+ if (synthFiles.length === 0) return null;
290
+
291
+ return join(plansDir, synthFiles[synthFiles.length - 1]);
292
+ }
293
+
294
+ function extractTitle(path: string, fallback: string): string {
295
+ try {
296
+ const content = readFileSync(path, "utf-8");
297
+ for (const line of content.split("\n")) {
298
+ const m = line.match(/^#\s+(.+?)\s*$/);
299
+ if (m) return m[1];
300
+ }
301
+ } catch {
302
+ // fall through to filename fallback
303
+ }
304
+ return fallback;
305
+ }
306
+
307
+ // Path-aware manifest of on-demand task documents (artifacts/*.md + the synthesized
308
+ // plan when present) — each entry carries a REAL filesystem path an agent can read.
309
+ // Unlike getPhaseArtifacts, the plan is included whenever it exists (not phase-gated),
310
+ // so a reviewer spawned in the review phase still gets the plan path.
311
+ export function getArtifactManifest(taskDir: string): { title: string; path: string }[] {
312
+ const manifest: { title: string; path: string }[] = [];
313
+
314
+ const artifactsDir = join(taskDir, "artifacts");
315
+ if (existsSync(artifactsDir)) {
316
+ for (const file of readdirSync(artifactsDir).filter((f) => f.endsWith(".md")).sort()) {
317
+ const path = join(artifactsDir, file);
318
+ manifest.push({ title: extractTitle(path, `artifacts/${file}`), path });
319
+ }
320
+ }
321
+
322
+ const planPath = getLatestSynthesizedPlanPath(taskDir);
323
+ if (planPath) {
324
+ manifest.push({ title: "Synthesized implementation plan", path: planPath });
325
+ }
326
+
327
+ return manifest;
328
+ }
329
+
330
+ // Renders a manifest into the trailing prompt block shared by phased panels.
331
+ // Replaces the old "Do NOT re-read them from disk" line: USER_REQUEST/RESEARCH
332
+ // (and the inlined plan) stay in context, while additional artifacts are offered
333
+ // for on-demand reading from disk.
334
+ export function formatManifestBlock(manifest: { title: string; path: string }[]): string {
335
+ if (manifest.length === 0) {
336
+ return "The USER REQUEST and RESEARCH above are already in your context. Do NOT re-read them from disk.";
337
+ }
338
+ return [
339
+ "The USER REQUEST and RESEARCH above are already in your context — do NOT re-read them from disk.",
340
+ "Additional analysis artifacts and the plan are listed below; read them from disk with the read tool if relevant:",
341
+ ...manifest.map((m) => `- ${m.path} — ${m.title}`),
342
+ ].join("\n");
343
+ }
344
+
282
345
  export function loadBrainstormReviewOutputs(taskDir: string, pass: number): { name: string; content: string }[] {
283
346
  const dir = join(taskDir, "brainstorm-reviews");
284
347
  if (!existsSync(dir)) return [];
@@ -58,7 +58,9 @@ function renderStatsLine(width: number, theme: Theme): string {
58
58
  const ctx = footerCtx;
59
59
  const tracker = footerTracker;
60
60
 
61
- const inputTokens = tracker?.getTotalInputTokens() ?? 0;
61
+ // is the total input the model actually processed (uncached + cache read +
62
+ // cache write) across main + subagents — not just the tiny uncached sliver.
63
+ const inputTokens = tracker?.getTotalProcessedInputTokens() ?? 0;
62
64
  const outputTokens = tracker?.getTotalOutputTokens() ?? 0;
63
65
  const cacheRate = tracker?.getCacheHitRate() ?? 0;
64
66
  const totalCost = tracker?.getTotalCost() ?? 0;
@@ -66,7 +68,8 @@ function renderStatsLine(width: number, theme: Theme): string {
66
68
  const cacheSupported = tracker?.isCacheSupported() ?? false;
67
69
  const leftParts: string[] = [`↑${formatTokens(inputTokens)}`, `↓${formatTokens(outputTokens)}`];
68
70
  if (cacheSupported) leftParts.push(`⚡${Math.round(cacheRate * 100)}%`);
69
- if (totalCost > 0) leftParts.push(`$${totalCost.toFixed(2)}`);
71
+ // Always show cost, even $0.00 (subscription/flat-rate sessions).
72
+ leftParts.push(`$${totalCost.toFixed(2)}`);
70
73
  leftParts.push(toContextUsagePart(ctx, theme));
71
74
  let left = leftParts.join(" ");
72
75
 
@@ -58,6 +58,7 @@ function createConfig() {
58
58
  debug: { model: "openai/gpt-latest", thinking: "high" },
59
59
  brainstorm: { model: "anthropic/claude-opus-latest", thinking: "high" },
60
60
  review: { model: "anthropic/claude-opus-latest", thinking: "high" },
61
+ quick: { model: "anthropic/claude-opus-latest", thinking: "high" },
61
62
  },
62
63
  subagents: {
63
64
  simple: {
@@ -143,6 +144,7 @@ function createCtx() {
143
144
  },
144
145
  modelRegistry: {
145
146
  getAvailable: vi.fn(() => [
147
+ { provider: "anthropic", id: "claude-opus-latest" },
146
148
  { provider: "anthropic", id: "claude-opus-4-6" },
147
149
  { provider: "openai", id: "gpt-5.4" },
148
150
  { provider: "google", id: "gemini-3.1-flash" },
@@ -154,7 +156,7 @@ function createCtx() {
154
156
 
155
157
  beforeEach(() => {
156
158
  const aliasMap: Record<string, string> = {
157
- "anthropic/claude-opus-latest": "anthropic/claude-opus-4-6",
159
+ "anthropic/claude-opus-latest": "anthropic/claude-opus-latest",
158
160
  "openai/gpt-latest": "openai/gpt-5.4",
159
161
  "google/gemini-flash-latest": "google/gemini-3.1-flash",
160
162
  "google/gemini-pro-latest": "google/gemini-3.1-pro",
@@ -12,7 +12,7 @@ import {
12
12
  PRESET_GROUPS,
13
13
  } from "./config.js";
14
14
  import { resolveModel, getAllAliases } from "./model-registry.js";
15
- import { loadFlantSettings } from "./flant-infra.js";
15
+ import { loadFlantSettings, readClaudeOAuthToken, readGatewayApiKey, refreshClaudeOAuthToken } from "./flant-infra.js";
16
16
  import type { Orchestrator } from "./orchestrator.js";
17
17
 
18
18
  type Severity = "pass" | "warning" | "failure";
@@ -470,7 +470,7 @@ export async function runDoctor(orchestrator: Orchestrator, ctx: any): Promise<v
470
470
 
471
471
  await safeCheck(async () => {
472
472
  const settings = loadFlantSettings();
473
- const shouldCheck = Boolean(process.env.FLANT_API_KEY) || settings.enabled || !!settings.cachedFlantModels || !!settings.cachedOpenRouterData;
473
+ const shouldCheck = Boolean(process.env.FLANT_API_KEY) || settings.enabled || settings.subscription || !!settings.cachedFlantModels || !!settings.cachedOpenRouterData;
474
474
  if (!shouldCheck) {
475
475
  addLine({ severity: "pass", text: "Skipped: FLANT_API_KEY not set and no Flant configuration detected" });
476
476
  return;
@@ -526,6 +526,44 @@ export async function runDoctor(orchestrator: Orchestrator, ctx: any): Promise<v
526
526
  } catch (error) {
527
527
  addLine({ severity: "failure", text: `OpenRouter probe failed: ${toErrorMessage(error)}` });
528
528
  }
529
+
530
+ if (settings.subscription) {
531
+ const oauthToken = (await refreshClaudeOAuthToken()) ?? readClaudeOAuthToken();
532
+ const gatewayKey = readGatewayApiKey();
533
+ if (!oauthToken) {
534
+ addLine({ severity: "warning", text: "Personal subscription enabled, but no valid Claude OAuth token found (log in to your subscription in pi)" });
535
+ } else if (!gatewayKey) {
536
+ addLine({ severity: "warning", text: "Personal subscription enabled, but no gateway key (LLM_API_KEY / FLANT_API_KEY)" });
537
+ } else {
538
+ const started = Date.now();
539
+ try {
540
+ const response = await timedFetch("https://llm-api.flant.ru/v1/messages", {
541
+ method: "POST",
542
+ headers: {
543
+ "content-type": "application/json",
544
+ "anthropic-version": "2023-06-01",
545
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
546
+ "user-agent": "claude-cli/1.0.0",
547
+ "x-app": "cli",
548
+ Authorization: `Bearer ${oauthToken}`,
549
+ "x-litellm-api-key": `Bearer ${gatewayKey}`,
550
+ },
551
+ body: JSON.stringify({
552
+ model: "sub/claude-haiku-4-5",
553
+ max_tokens: 4,
554
+ messages: [{ role: "user", content: "ping" }],
555
+ }),
556
+ }, 15000);
557
+ if (!response.ok) {
558
+ addLine({ severity: "failure", text: `Personal subscription probe failed with HTTP ${response.status}` });
559
+ } else {
560
+ addLine({ severity: "pass", text: `Personal subscription reachable (sub/claude-*, ${Date.now() - started}ms)` });
561
+ }
562
+ } catch (error) {
563
+ addLine({ severity: "failure", text: `Personal subscription probe failed: ${toErrorMessage(error)}` });
564
+ }
565
+ }
566
+ }
529
567
  }, "Flant checks failed");
530
568
 
531
569
  addCategory("LSP");
@@ -1,4 +1,7 @@
1
- import { describe, expect, it, vi, beforeEach } from "vitest";
1
+ import { afterEach, describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
2
5
  import { registerEventHandlers } from "./event-handlers.js";
3
6
  import { Orchestrator, type ActiveTask } from "./orchestrator.js";
4
7
  import { getDefaultConfig } from "./config.js";
@@ -251,3 +254,96 @@ describe("ask_user ESC aborts the turn", () => {
251
254
  expect(ctx.abort).not.toHaveBeenCalled();
252
255
  });
253
256
  });
257
+
258
+ describe("tool_call Agent routing and spawn-time context injection", () => {
259
+ const tempDirs: string[] = [];
260
+
261
+ function makeTaskDir(): string {
262
+ const dir = mkdtempSync(join(tmpdir(), "pi-pi-agent-hook-test-"));
263
+ tempDirs.push(dir);
264
+ writeFileSync(join(dir, "USER_REQUEST.md"), "the user request body", "utf-8");
265
+ writeFileSync(join(dir, "RESEARCH.md"), "the research body", "utf-8");
266
+ const artifactsDir = join(dir, "artifacts");
267
+ mkdirSync(artifactsDir, { recursive: true });
268
+ writeFileSync(join(artifactsDir, "design.md"), "# Design Doc\n\nstuff", "utf-8");
269
+ const plansDir = join(dir, "plans");
270
+ mkdirSync(plansDir, { recursive: true });
271
+ writeFileSync(join(plansDir, "1_synthesized.md"), "the plan", "utf-8");
272
+ return dir;
273
+ }
274
+
275
+ function activeWith(dir: string): ActiveTask {
276
+ const task = makeActiveTask();
277
+ task.dir = dir;
278
+ return task;
279
+ }
280
+
281
+ afterEach(() => {
282
+ for (const dir of tempDirs.splice(0)) {
283
+ rmSync(dir, { recursive: true, force: true });
284
+ }
285
+ });
286
+
287
+ it("blocks a missing subagent_type with a valid-types message", async () => {
288
+ orchestrator.active = makeActiveTask();
289
+ const handler = getHandler("tool_call");
290
+ const result = await handler({ toolName: "Agent", input: { prompt: "do a thing" } }, {});
291
+ expect(result?.block).toBe(true);
292
+ expect(result?.reason).toContain("subagent_type is required");
293
+ });
294
+
295
+ it("blocks an unknown subagent_type instead of collapsing to task", async () => {
296
+ orchestrator.active = makeActiveTask();
297
+ const handler = getHandler("tool_call");
298
+ const input: Record<string, unknown> = { subagent_type: "wizard", prompt: "x" };
299
+ const result = await handler({ toolName: "Agent", input }, {});
300
+ expect(result?.block).toBe(true);
301
+ expect(result?.reason).toContain('Unknown subagent_type "wizard"');
302
+ expect(result?.reason).toContain("deep-debugger");
303
+ // must NOT have been rewritten to task
304
+ expect(input.subagent_type).toBe("wizard");
305
+ });
306
+
307
+ it("routes advisor to its own model/thinking and injects context into the prompt", async () => {
308
+ const dir = makeTaskDir();
309
+ orchestrator.active = activeWith(dir);
310
+ const handler = getHandler("tool_call");
311
+ const input: Record<string, unknown> = { subagent_type: "advisor", prompt: "why is this broken" };
312
+ const result = await handler({ toolName: "Agent", input }, {});
313
+ expect(result).toBeUndefined();
314
+ expect(input.subagent_type).toBe("advisor");
315
+ expect(input.thinking).toBe("high");
316
+ const prompt = input.prompt as string;
317
+ expect(prompt).toContain("why is this broken");
318
+ expect(prompt).toContain("=== USER REQUEST ===");
319
+ expect(prompt).toContain("the user request body");
320
+ expect(prompt).toContain("=== RESEARCH ===");
321
+ expect(prompt).toContain("the research body");
322
+ // manifest lists real paths, not inlined content
323
+ expect(prompt).toContain(join(dir, "artifacts", "design.md"));
324
+ expect(prompt).toContain(join(dir, "plans", "1_synthesized.md"));
325
+ expect(prompt).not.toContain("the plan");
326
+ });
327
+
328
+ it("maps deep-debugger via bracket-notation config", async () => {
329
+ const dir = makeTaskDir();
330
+ orchestrator.active = activeWith(dir);
331
+ const handler = getHandler("tool_call");
332
+ const input: Record<string, unknown> = { subagent_type: "deep-debugger", prompt: "trace it" };
333
+ const result = await handler({ toolName: "Agent", input }, {});
334
+ expect(result).toBeUndefined();
335
+ expect(input.subagent_type).toBe("deep-debugger");
336
+ expect(input.thinking).toBe("high");
337
+ });
338
+
339
+ it("does NOT inject task context into explore spawns", async () => {
340
+ const dir = makeTaskDir();
341
+ orchestrator.active = activeWith(dir);
342
+ const handler = getHandler("tool_call");
343
+ const input: Record<string, unknown> = { subagent_type: "explore", prompt: "find X" };
344
+ const result = await handler({ toolName: "Agent", input }, {});
345
+ expect(result).toBeUndefined();
346
+ expect(input.subagent_type).toBe("explore");
347
+ expect(input.prompt).toBe("find X");
348
+ });
349
+ });