@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.
- package/3p/pi-ask-user/index.ts +1 -1
- package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
- package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
- package/3p/pi-subagents/src/agent-manager.ts +34 -1
- package/3p/pi-subagents/src/agent-runner.ts +66 -33
- package/3p/pi-subagents/src/index.ts +3 -38
- package/3p/pi-subagents/src/types.ts +4 -0
- package/extensions/orchestrator/agents/advisor.ts +35 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/constraints.test.ts +44 -0
- package/extensions/orchestrator/agents/constraints.ts +3 -0
- package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/planner.ts +9 -8
- package/extensions/orchestrator/agents/prompts.test.ts +120 -0
- package/extensions/orchestrator/agents/reviewer.ts +48 -0
- package/extensions/orchestrator/agents/task.ts +6 -20
- package/extensions/orchestrator/agents/tool-routing.ts +23 -1
- package/extensions/orchestrator/command-handlers.ts +1 -1
- package/extensions/orchestrator/config.ts +8 -4
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +65 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.test.ts +97 -1
- package/extensions/orchestrator/event-handlers.ts +222 -48
- package/extensions/orchestrator/flant-infra.test.ts +312 -0
- package/extensions/orchestrator/flant-infra.ts +407 -44
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +312 -18
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +124 -13
- package/extensions/orchestrator/model-registry.ts +91 -33
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +163 -3
- package/extensions/orchestrator/phases/brainstorm.ts +9 -20
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.ts +1 -4
- package/extensions/orchestrator/phases/review.test.ts +62 -0
- package/extensions/orchestrator/phases/review.ts +58 -15
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +366 -94
- package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
- package/extensions/orchestrator/pp-state-tools.ts +249 -0
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
- package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +18 -3
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +78 -11
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- 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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
+
});
|