@ilya-lesikov/pi-pi 0.6.0 → 0.7.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/extensions/orchestrator/config.ts +3 -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.ts +47 -6
- package/extensions/orchestrator/flant-infra.test.ts +287 -0
- package/extensions/orchestrator/flant-infra.ts +316 -44
- package/extensions/orchestrator/integration.test.ts +205 -6
- package/extensions/orchestrator/model-registry.test.ts +76 -12
- package/extensions/orchestrator/model-registry.ts +48 -32
- package/extensions/orchestrator/orchestrator.ts +13 -2
- package/extensions/orchestrator/phases/review.test.ts +54 -0
- package/extensions/orchestrator/phases/review.ts +54 -12
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +199 -41
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +14 -2
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +74 -11
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { reviewSystemPrompt } from "./review.js";
|
|
3
|
+
|
|
4
|
+
describe("reviewSystemPrompt apply_feedback wording", () => {
|
|
5
|
+
it("autonomous plan/implement mandates re-calling pp_phase_complete and does not tell the agent to wait for the user", () => {
|
|
6
|
+
const prompt = reviewSystemPrompt("/tmp/task", 1, "plan", "autonomous");
|
|
7
|
+
expect(prompt).toContain("pp_phase_complete");
|
|
8
|
+
expect(prompt).not.toContain("Present the synthesis to the user");
|
|
9
|
+
expect(prompt).not.toContain("A new review pass will begin");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("guided plan/implement keeps the user-facing synthesis behavior", () => {
|
|
13
|
+
const prompt = reviewSystemPrompt("/tmp/task", 1, "plan", "guided");
|
|
14
|
+
expect(prompt).toContain("Present the synthesis to the user");
|
|
15
|
+
expect(prompt).not.toContain("Call pp_phase_complete again to finalize");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("brainstorm prompt is unaffected by mode (never instructs pp_phase_complete)", () => {
|
|
19
|
+
const auto = reviewSystemPrompt("/tmp/task", 1, "brainstorm", "autonomous");
|
|
20
|
+
const guided = reviewSystemPrompt("/tmp/task", 1, "brainstorm", "guided");
|
|
21
|
+
expect(auto).toBe(guided);
|
|
22
|
+
expect(auto).toContain("BRAINSTORM REVIEW CYCLE");
|
|
23
|
+
expect(auto).not.toContain("pp_phase_complete");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("autonomous plan feedback folds into a new synthesized plan, not a separate fix-plan file", () => {
|
|
27
|
+
// Re-review and the transition read only the latest `*synthesized*` plan, so
|
|
28
|
+
// plan-phase feedback must land there.
|
|
29
|
+
const plan = reviewSystemPrompt("/tmp/task", 1, "plan", "autonomous");
|
|
30
|
+
expect(plan).toContain("synthesized.md");
|
|
31
|
+
expect(plan).not.toContain("do NOT modify the original synthesized plan");
|
|
32
|
+
|
|
33
|
+
// The implement phase keeps the fix-plan/implement pattern (synthesized plan
|
|
34
|
+
// is code guidance there, not the reviewed artifact).
|
|
35
|
+
const impl = reviewSystemPrompt("/tmp/task", 1, "implement", "autonomous");
|
|
36
|
+
expect(impl).toContain("Create a fix plan");
|
|
37
|
+
expect(impl).toContain("Implement the fixes");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("points each phase at the directory its reviewers actually write to", () => {
|
|
41
|
+
// plan reviewers write to plan-reviews (planning.ts) and outputs load from
|
|
42
|
+
// plan-reviews (context.ts); the prompt must match, not code-reviews.
|
|
43
|
+
const plan = reviewSystemPrompt("/tmp/task", 1, "plan", "autonomous");
|
|
44
|
+
expect(plan).toContain("plan-reviews/");
|
|
45
|
+
expect(plan).not.toContain("code-reviews/");
|
|
46
|
+
|
|
47
|
+
const impl = reviewSystemPrompt("/tmp/task", 1, "implement", "autonomous");
|
|
48
|
+
expect(impl).toContain("code-reviews/");
|
|
49
|
+
expect(impl).not.toContain("plan-reviews/");
|
|
50
|
+
|
|
51
|
+
const brainstorm = reviewSystemPrompt("/tmp/task", 1, "brainstorm", "guided");
|
|
52
|
+
expect(brainstorm).toContain("brainstorm-reviews/");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -12,8 +12,14 @@ function isEnabled(value: { enabled?: boolean } | undefined): boolean {
|
|
|
12
12
|
return value?.enabled !== false;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function reviewSystemPrompt(taskDir: string, pass: number, phase?: string): string {
|
|
16
|
-
|
|
15
|
+
export function reviewSystemPrompt(taskDir: string, pass: number, phase?: string, mode?: "guided" | "autonomous"): string {
|
|
16
|
+
// Each phase writes/loads its review outputs in a distinct directory:
|
|
17
|
+
// brainstorm -> brainstorm-reviews, plan -> plan-reviews, everything else
|
|
18
|
+
// (implement/review) -> code-reviews. The apply_feedback prompt must point the
|
|
19
|
+
// agent at the SAME directory the reviewers wrote to (see planning.ts /
|
|
20
|
+
// context.ts), otherwise it synthesizes against the wrong (empty) directory.
|
|
21
|
+
const reviewsDirName = phase === "brainstorm" ? "brainstorm-reviews" : phase === "plan" ? "plan-reviews" : "code-reviews";
|
|
22
|
+
const reviewsDir = join(taskDir, reviewsDirName);
|
|
17
23
|
const plansDir = join(taskDir, "plans");
|
|
18
24
|
|
|
19
25
|
if (phase === "brainstorm") {
|
|
@@ -42,26 +48,62 @@ export function reviewSystemPrompt(taskDir: string, pass: number, phase?: string
|
|
|
42
48
|
].join("\n");
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
// In autonomous plan/implement the phase does NOT complete until the agent
|
|
52
|
+
// re-calls pp_phase_complete: that is what finalizes the pass and advances the
|
|
53
|
+
// phase. Telling it to "present to the user" and wait causes the plan phase to
|
|
54
|
+
// stall after applying feedback. Guided mode keeps the user-facing behavior.
|
|
55
|
+
const finalStep =
|
|
56
|
+
mode === "autonomous"
|
|
57
|
+
? "3. Call pp_phase_complete again to finalize this review pass. The phase is NOT complete until you do — do NOT stop or wait for the user"
|
|
58
|
+
: "3. Present the synthesis to the user";
|
|
59
|
+
|
|
60
|
+
// In the plan phase the synthesized plan IS the reviewed artifact: re-review
|
|
61
|
+
// and the phase transition both read only the LATEST `*synthesized*` file (see
|
|
62
|
+
// getLatestSynthesizedPlan). So autonomous plan feedback must be folded into a
|
|
63
|
+
// new synthesized plan, NOT a separate fix-plan file (which those readers
|
|
64
|
+
// ignore). In the implement phase the synthesized plan is code guidance, so
|
|
65
|
+
// the fix-plan/implement/afterImplement pattern is correct there.
|
|
66
|
+
const tail =
|
|
67
|
+
mode === "autonomous"
|
|
68
|
+
? phase === "plan"
|
|
69
|
+
? [
|
|
70
|
+
"",
|
|
71
|
+
"If the reviewers require changes:",
|
|
72
|
+
`1. Fold the required changes into a NEW synthesized plan at ${plansDir}/<timestamp>_synthesized.md (re-review and the phase transition read only the latest \`*synthesized*\` plan, so the fixes MUST land there — do not write them to a separate fix-plan file)`,
|
|
73
|
+
"2. Then call pp_phase_complete again — the extension will start a new review pass or advance the phase as appropriate. Do NOT wait for the user.",
|
|
74
|
+
]
|
|
75
|
+
: [
|
|
76
|
+
"",
|
|
77
|
+
"If changes are needed:",
|
|
78
|
+
`1. Create a fix plan at ${plansDir}/<timestamp>_<description>.md (do NOT modify the original synthesized plan)`,
|
|
79
|
+
"2. Implement the fixes",
|
|
80
|
+
"3. Run afterImplement commands",
|
|
81
|
+
"4. Then call pp_phase_complete again — the extension will start a new review pass or advance the phase as appropriate. Do NOT wait for the user.",
|
|
82
|
+
]
|
|
83
|
+
: [
|
|
84
|
+
"",
|
|
85
|
+
"If changes are needed:",
|
|
86
|
+
`1. Create a fix plan at ${plansDir}/<timestamp>_<description>.md (do NOT modify the original synthesized plan)`,
|
|
87
|
+
"2. Implement the fixes",
|
|
88
|
+
"3. Run afterImplement commands",
|
|
89
|
+
"4. A new review pass will begin",
|
|
90
|
+
];
|
|
91
|
+
|
|
45
92
|
return [
|
|
46
93
|
`[PI-PI — REVIEW CYCLE (pass ${pass})]`,
|
|
47
94
|
"",
|
|
48
|
-
"
|
|
95
|
+
"Reviewer outputs are ready.",
|
|
49
96
|
`Read them from ${reviewsDir}/, synthesize feedback, and implement fixes if needed.`,
|
|
50
97
|
"",
|
|
51
|
-
"You are a SYNTHESIZER: merge the reviewer outputs. Do NOT write your own
|
|
52
|
-
|
|
98
|
+
"You are a SYNTHESIZER: merge the reviewer outputs. Do NOT write your own review from scratch.",
|
|
99
|
+
`- Do NOT create the ${reviewsDirName}/ directory yourself — the extension manages it.`,
|
|
53
100
|
"- Do NOT call plannotator_submit_plan.",
|
|
54
101
|
"",
|
|
55
102
|
"# Your job (in this order):",
|
|
56
103
|
`1. Read ALL reviewer outputs from ${reviewsDir}/`,
|
|
57
104
|
`2. Synthesize into ${reviewsDir}/<timestamp>_final_pass-${pass}.md`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"If changes are needed:",
|
|
61
|
-
`1. Create a fix plan at ${plansDir}/<timestamp>_<description>.md (do NOT modify the original synthesized plan)`,
|
|
62
|
-
"2. Implement the fixes",
|
|
63
|
-
"3. Run afterImplement commands",
|
|
64
|
-
"4. A new review pass will begin",
|
|
105
|
+
finalStep,
|
|
106
|
+
...tail,
|
|
65
107
|
].join("\n");
|
|
66
108
|
}
|
|
67
109
|
|
|
@@ -3,10 +3,83 @@ import { join } from "path";
|
|
|
3
3
|
import { getDefaultConfig, GLOBAL_CONFIG_PATH, parseDuration } from "./config.js";
|
|
4
4
|
import * as configModule from "./config.js";
|
|
5
5
|
import * as flantInfra from "./flant-infra.js";
|
|
6
|
-
import { formatDuration, formatSourceTags, getConfigSourceInfo, pickMaxReviewPasses } from "./pp-menu.js";
|
|
6
|
+
import { formatDuration, formatSourceTags, getConfigSourceInfo, pickMaxReviewPasses, showUsage } from "./pp-menu.js";
|
|
7
|
+
import { createUsageTracker } from "./usage-tracker.js";
|
|
8
|
+
|
|
9
|
+
const USAGE_TRACKER_SYMBOL = Symbol.for("pi-pi:usage-tracker");
|
|
7
10
|
|
|
8
11
|
afterEach(() => {
|
|
9
12
|
vi.restoreAllMocks();
|
|
13
|
+
delete (globalThis as any)[USAGE_TRACKER_SYMBOL];
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function renderUsage(tracker: ReturnType<typeof createUsageTracker>): string {
|
|
17
|
+
(globalThis as any)[USAGE_TRACKER_SYMBOL] = tracker;
|
|
18
|
+
let captured = "";
|
|
19
|
+
showUsage({ ui: { notify: (text: string) => { captured = text; } } });
|
|
20
|
+
return captured;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("showUsage subscription rendering", () => {
|
|
24
|
+
it("labels subscription model and agent rows and excludes their dollars", () => {
|
|
25
|
+
const tracker = createUsageTracker();
|
|
26
|
+
tracker.recordTurn("openai/gpt-5", "openai", 100, 50, 0, 0, 0.4, false);
|
|
27
|
+
tracker.recordTurn("sub/claude-opus-4-6", "pp-flant-anthropic-sub", 200, 100, 0, 0, 9.9, false);
|
|
28
|
+
tracker.recordSubagentCompletion({ input: 30, output: 10 } as any, 5.0, {
|
|
29
|
+
description: "Explore", agentType: "explore", modelId: "sub/claude-haiku-4-5",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const out = renderUsage(tracker);
|
|
33
|
+
|
|
34
|
+
expect(out).toContain("sub/claude-opus-4-6:");
|
|
35
|
+
expect(out).toContain("subscription");
|
|
36
|
+
expect(out).toContain("explore");
|
|
37
|
+
expect(out).toContain("$0.40");
|
|
38
|
+
expect(out).not.toContain("$9.90");
|
|
39
|
+
expect(out).not.toContain("$5.00");
|
|
40
|
+
expect(out).toContain("Cost: $0.40");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("shows the four-bucket input breakdown and processed-input totals", () => {
|
|
44
|
+
const tracker = createUsageTracker();
|
|
45
|
+
// uncached 84, output 27k, cacheRead 1000, cacheWrite 200 on a sub model.
|
|
46
|
+
tracker.recordTurn("sub/claude-opus-4-8", "pp-flant-anthropic-sub", 84, 27000, 1000, 200, 0, true);
|
|
47
|
+
// A cache-using subagent, to exercise the inline By-agent breakdown.
|
|
48
|
+
tracker.recordSubagentCompletion({ input: 10, output: 500, cacheRead: 300, cacheWrite: 90 } as any, 0, {
|
|
49
|
+
description: "Explore", agentType: "explore", modelId: "sub/claude-haiku-4-5",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const out = renderUsage(tracker);
|
|
53
|
+
|
|
54
|
+
// Total "Input" is the processed input across main + subagent:
|
|
55
|
+
// main 84+1000+200=1284, subagent 10+300+90=400 → 1684 → "1.7k".
|
|
56
|
+
expect(out).toContain("Input: 1.7k tokens");
|
|
57
|
+
expect(out).toContain("uncached: 94"); // 84 + 10
|
|
58
|
+
expect(out).toContain("cache read: 1.3k"); // 1000 + 300
|
|
59
|
+
expect(out).toContain("cache write: 290"); // 200 + 90
|
|
60
|
+
expect(out).toContain("Output: 28k tokens"); // 27000 + 500
|
|
61
|
+
// Hit rate = 1300 / 1684 = 77% (rounded).
|
|
62
|
+
expect(out).toContain("⚡77% hit rate");
|
|
63
|
+
// Cost is always shown, even at $0.00 for subscription sessions.
|
|
64
|
+
expect(out).toContain("Cost: $0.00");
|
|
65
|
+
// Per-model row is a one-liner: processed input (↑1.3k) with an inline
|
|
66
|
+
// uncached / cache read / cache write breakdown.
|
|
67
|
+
expect(out).toContain("sub/claude-opus-4-8: ↑1.3k (u84 r1.0k w200)");
|
|
68
|
+
// By-agent row carries the same inline breakdown (400 processed = 10+300+90).
|
|
69
|
+
expect(out).toContain("explore: ↑400 (u10 r300 w90)");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not inflate paid model share when a subscription model is present", () => {
|
|
73
|
+
const tracker = createUsageTracker();
|
|
74
|
+
tracker.recordTurn("openai/gpt-5", "openai", 100, 0, 0, 0, 0.5, false);
|
|
75
|
+
tracker.recordTurn("sub/claude-opus-4-6", "pp-flant-anthropic-sub", 100, 0, 0, 0, 2.0, false);
|
|
76
|
+
|
|
77
|
+
const out = renderUsage(tracker);
|
|
78
|
+
|
|
79
|
+
expect(out).toContain("openai/gpt-5:");
|
|
80
|
+
expect(out).toContain("$0.50");
|
|
81
|
+
expect(out).not.toContain("$0.25");
|
|
82
|
+
});
|
|
10
83
|
});
|
|
11
84
|
|
|
12
85
|
describe("settings helpers", () => {
|