@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.
@@ -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
- const reviewsDir = phase === "brainstorm" ? join(taskDir, "brainstorm-reviews") : join(taskDir, "code-reviews");
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
- "Code reviewer outputs are ready.",
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 code review from scratch.",
52
- "- Do NOT create the code-reviews/ directory yourself — the extension manages it.",
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
- "3. Present the synthesis to the user",
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", () => {