@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
@@ -24,7 +24,10 @@ import { registerAgentDefinitions, unregisterAgentDefinitions } from "./agents/r
24
24
  import { createExploreAgent } from "./agents/explore.js";
25
25
  import { createLibrarianAgent } from "./agents/librarian.js";
26
26
  import { createTaskAgent } from "./agents/task.js";
27
- import { resolveModel, getModelInfo } from "./model-registry.js";
27
+ import { createAdvisorAgent } from "./agents/advisor.js";
28
+ import { createDeepDebuggerAgent } from "./agents/deep-debugger.js";
29
+ import { createReviewerAgent } from "./agents/reviewer.js";
30
+ import { resolveModel, getModelInfo, findLatestFamilyMatch, setSubscriptionFallbackActive } from "./model-registry.js";
28
31
  import { buildRepoContext } from "./agents/repo-context.js";
29
32
  import { getLogger, addTaskDestination, removeTaskDestination, setLogLevel } from "./log.js";
30
33
  import { handleSpawnResult } from "./spawn-cleanup.js";
@@ -77,10 +80,36 @@ export class Orchestrator {
77
80
  nudgeHalted = false;
78
81
  pendingSubagentSpawns = 0;
79
82
  errorRetryCount = 0;
83
+ // Halts the API-error auto-retry once errorRetryCount exceeds its cap, mirroring
84
+ // nudgeHalted. Without this, a benign intervening turn (e.g. the retried turn
85
+ // ends as a text-only "I'll wait") reset errorRetryCount to 0, so the 5-retry
86
+ // cap never accumulated and the "Previous request failed" nudge could fire
87
+ // unbounded (hundreds of times) against transient errors. Cleared only on
88
+ // genuine (non-[PI-PI]) user re-engagement, like nudgeHalted.
89
+ errorNudgeHalted = false;
80
90
  commitReminderSent = false;
81
91
  phaseStartTime = 0;
82
92
  pendingRetryTimer: ReturnType<typeof setTimeout> | null = null;
93
+ // Unsubscribe for the direct ESC interrupt armed while pendingRetryTimer is
94
+ // live. pi-pi's own post-error retry is NOT covered by any SDK/interactive ESC
95
+ // binding (the turn already ended in error, the session is not streaming), so
96
+ // without this ESC would not cancel it.
97
+ pendingRetryEscUnsub: (() => void) | null = null;
83
98
  activeTaskToken = 0;
99
+ // Subscription rate-limit fallback (Issue 5). subFallbackActive mirrors the
100
+ // model-registry override flag; subFallbackDialogPending guards against
101
+ // opening more than one switch dialogue at a time (across main + subagents);
102
+ // subFallbackModelId records the sub model that hit the limit (used by the
103
+ // switch-back probe); subSwitchBackTimer is the fixed-interval probe timer.
104
+ subFallbackActive = false;
105
+ subFallbackDialogPending = false;
106
+ // Set SYNCHRONOUSLY the moment a sub-429 is detected (before any async dialog),
107
+ // and cleared once the decision resolves. The autonomous planner/reviewer
108
+ // auto-retry consults this to avoid re-spawning a failed variant on the still-
109
+ // sub-routed model while the fallback decision is in flight.
110
+ subFallbackPendingDecision = false;
111
+ subFallbackModelId: string | null = null;
112
+ subSwitchBackTimer: ReturnType<typeof setTimeout> | null = null;
84
113
  userGatePending = false;
85
114
  lastCtx: any = null;
86
115
  failedPlannerVariants: string[] = [];
@@ -123,6 +152,87 @@ export class Orchestrator {
123
152
  };
124
153
  }
125
154
 
155
+ // Arm a direct ESC interrupt for the post-error retry window. Idempotent: a
156
+ // single onTerminalInput handler stays registered until the retry is delivered,
157
+ // cancelled, or the task is reset. While pendingRetryTimer is live, ESC cancels
158
+ // the pending retry (no other binding covers this window).
159
+ armRetryEscInterrupt(ctx: any): void {
160
+ if (this.pendingRetryEscUnsub) return;
161
+ const onTerminalInput = ctx?.ui?.onTerminalInput;
162
+ if (typeof onTerminalInput !== "function") return;
163
+ const unsub = onTerminalInput.call(ctx.ui, (data: string) => {
164
+ if (!this.pendingRetryTimer) return undefined;
165
+ // Match a STANDALONE ESC only. Arrow/function/mouse sequences also start
166
+ // with 0x1b (e.g. "\x1b[A"), so `includes` would misfire on navigation
167
+ // keys and swallow them; a bare ESC is exactly the one-byte string.
168
+ if (data === "\x1b") {
169
+ this.cancelPendingRetry();
170
+ ctx?.ui?.notify?.("Retry cancelled.", "info");
171
+ return { consume: true };
172
+ }
173
+ return undefined;
174
+ });
175
+ this.pendingRetryEscUnsub = typeof unsub === "function" ? unsub : null;
176
+ }
177
+
178
+ disarmRetryEscInterrupt(): void {
179
+ if (this.pendingRetryEscUnsub) {
180
+ try {
181
+ this.pendingRetryEscUnsub();
182
+ } catch {
183
+ // ignore unsubscribe failures
184
+ }
185
+ this.pendingRetryEscUnsub = null;
186
+ }
187
+ }
188
+
189
+ // Cancel a pending post-error retry (timer + ESC interrupt) and reset the retry
190
+ // counter. Used by the ESC interrupt handler and by abort paths.
191
+ cancelPendingRetry(): void {
192
+ if (this.pendingRetryTimer) {
193
+ clearTimeout(this.pendingRetryTimer);
194
+ this.pendingRetryTimer = null;
195
+ }
196
+ this.disarmRetryEscInterrupt();
197
+ this.errorRetryCount = 0;
198
+ this.errorNudgeHalted = false;
199
+ }
200
+
201
+ // Deliver a queued message only once the main session is idle. Firing a
202
+ // followUp while the SDK still has an active run triggers an async, runtime-
203
+ // swallowed "Agent is already processing" rejection (surfaces as
204
+ // Extension "<runtime>" error), so we PRE-CHECK idle and DEFER (bounded poll)
205
+ // rather than dropping the nudge. Guarded by activeTaskToken; the poll reuses
206
+ // pendingRetryTimer so ESC/abort cancels it.
207
+ sendUserMessageWhenIdle(text: string, taskToken: number, attempt = 0): void {
208
+ const log = getLogger();
209
+ if (this.activeTaskToken !== taskToken || !this.active) {
210
+ this.disarmRetryEscInterrupt();
211
+ return;
212
+ }
213
+ const idleFn = this.lastCtx?.isIdle;
214
+ const idle = typeof idleFn === "function" ? !!idleFn.call(this.lastCtx) : true;
215
+ if (idle) {
216
+ this.disarmRetryEscInterrupt();
217
+ this.safeSendUserMessage(text);
218
+ return;
219
+ }
220
+ const MAX_ATTEMPTS = 120; // ~2min at 1s poll
221
+ if (attempt >= MAX_ATTEMPTS) {
222
+ log.warn({ s: "orchestrator", attempt }, "sendUserMessageWhenIdle gave up waiting for idle");
223
+ this.disarmRetryEscInterrupt();
224
+ this.lastCtx?.ui?.notify?.(
225
+ "pi-pi stopped waiting for the agent to go idle; auto-continuation was dropped. Send any message to resume.",
226
+ "warning",
227
+ );
228
+ return;
229
+ }
230
+ this.pendingRetryTimer = setTimeout(() => {
231
+ this.pendingRetryTimer = null;
232
+ this.sendUserMessageWhenIdle(text, taskToken, attempt + 1);
233
+ }, 1000);
234
+ }
235
+
126
236
  safeSendUserMessage(text: string): void {
127
237
  const log = getLogger();
128
238
  const attempt = (retries: number) => {
@@ -175,6 +285,16 @@ export class Orchestrator {
175
285
  (m) => m.provider.toLowerCase() === provider && m.id.toLowerCase() === modelId,
176
286
  );
177
287
  }
288
+ if (!resolved) {
289
+ const allSpecs = allModels.map((m) => `${m.provider}/${m.id}`);
290
+ const familyMatch = findLatestFamilyMatch(spec, allSpecs);
291
+ if (familyMatch) {
292
+ const fmLower = familyMatch.toLowerCase();
293
+ resolved = allModels.find(
294
+ (m) => `${m.provider.toLowerCase()}/${m.id.toLowerCase()}` === fmLower,
295
+ );
296
+ }
297
+ }
178
298
  if (!resolved) {
179
299
  const pattern = spec.toLowerCase();
180
300
  const matches = allModels.filter(
@@ -301,7 +421,7 @@ export class Orchestrator {
301
421
 
302
422
  if (this.active.state.reviewCycle?.step === "apply_feedback") {
303
423
  const pass = this.active.state.reviewCycle.pass;
304
- return reviewCycleSystemPrompt(this.active.dir, pass, this.active.state.phase);
424
+ return reviewCycleSystemPrompt(this.active.dir, pass, this.active.state.phase, mode);
305
425
  }
306
426
 
307
427
  switch (this.active.state.phase) {
@@ -445,6 +565,7 @@ export class Orchestrator {
445
565
  type === "debug" ? "debug"
446
566
  : type === "brainstorm" ? "brainstorm"
447
567
  : type === "review" ? "review"
568
+ : type === "quick" ? "quick"
448
569
  : "implement"
449
570
  ];
450
571
  const modelOk = await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
@@ -528,6 +649,7 @@ export class Orchestrator {
528
649
  this.agentLifecycle.clear();
529
650
  this.pendingSubagentSpawns = 0;
530
651
  this.errorRetryCount = 0;
652
+ this.errorNudgeHalted = false;
531
653
  this.commitReminderSent = false;
532
654
  this.consecutiveNudges = 0;
533
655
  this.nudgeHalted = false;
@@ -541,10 +663,27 @@ export class Orchestrator {
541
663
  clearTimeout(this.pendingRetryTimer);
542
664
  this.pendingRetryTimer = null;
543
665
  }
666
+ this.disarmRetryEscInterrupt();
544
667
  if (this.staleAgentTimer) {
545
668
  clearInterval(this.staleAgentTimer);
546
669
  this.staleAgentTimer = null;
547
670
  }
671
+ this.clearSubscriptionFallback();
672
+ }
673
+
674
+ // Reset the subscription rate-limit fallback: cancel the switch-back probe
675
+ // timer, clear the model-registry override, and reset guards. Called on task
676
+ // reset/cleanup so the sticky override never leaks across tasks.
677
+ clearSubscriptionFallback(): void {
678
+ if (this.subSwitchBackTimer) {
679
+ clearTimeout(this.subSwitchBackTimer);
680
+ this.subSwitchBackTimer = null;
681
+ }
682
+ this.subFallbackActive = false;
683
+ this.subFallbackDialogPending = false;
684
+ this.subFallbackPendingDecision = false;
685
+ this.subFallbackModelId = null;
686
+ setSubscriptionFallbackActive(false);
548
687
  }
549
688
 
550
689
  async cleanupActive(): Promise<void> {
@@ -567,7 +706,10 @@ export class Orchestrator {
567
706
  const log = getLogger();
568
707
  const explore = createExploreAgent(this.config);
569
708
  const librarian = createLibrarianAgent(this.config);
570
- const taskAgent = createTaskAgent(this.config, "{{subtask}}", { userRequest: "", synthesizedPlan: "" });
709
+ const taskAgent = createTaskAgent(this.config);
710
+ const advisor = createAdvisorAgent(this.config);
711
+ const deepDebugger = createDeepDebuggerAgent(this.config);
712
+ const reviewer = createReviewerAgent(this.config);
571
713
  const phase = this.active?.state.phase;
572
714
  const repos = this.active?.state.repos ?? [];
573
715
  log.debug({ s: "agents", phase, repoCount: repos.length }, "registering agent definitions");
@@ -604,6 +746,24 @@ export class Orchestrator {
604
746
  ...taskAgent,
605
747
  prompt: appendContext("task", taskAgent.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.task.model))),
606
748
  },
749
+ {
750
+ type: "advisor",
751
+ variant: null,
752
+ ...advisor,
753
+ prompt: appendContext("advisor", advisor.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.advisor.model))),
754
+ },
755
+ {
756
+ type: "deep-debugger",
757
+ variant: null,
758
+ ...deepDebugger,
759
+ prompt: appendContext("deep-debugger", deepDebugger.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple["deep-debugger"].model))),
760
+ },
761
+ {
762
+ type: "reviewer",
763
+ variant: null,
764
+ ...reviewer,
765
+ prompt: appendContext("reviewer", reviewer.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.reviewer.model))),
766
+ },
607
767
  ]);
608
768
  }
609
769
 
@@ -4,7 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { resolvePreset, type PiPiConfig, type VariantConfig } from "../config.js";
5
5
  import { registerAgentDefinitions, spawnViaRpc, waitForCompletion } from "../agents/registry.js";
6
6
  import { createBrainstormReviewerAgent } from "../agents/brainstorm-reviewer.js";
7
- import { getContextDirs } from "../context.js";
7
+ import { getContextDirs, getArtifactManifest } from "../context.js";
8
8
  import type { RepoInfo } from "../repo-utils.js";
9
9
  import type { TaskType } from "../state.js";
10
10
  import type { PhaseSend } from "../transition-controller.js";
@@ -26,13 +26,8 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
26
26
  "",
27
27
  "# Your job:",
28
28
  "1. Clarify the problem with the user if needed",
29
- "2. Spawn subagents for research (subagent_type is REQUIRED calls without it are rejected):",
30
- ' - Agent(subagent_type="Explore", ...) codebase research. Prefer this for most lookups.',
31
- ' - Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
32
- ' - Agent(subagent_type="Task", ...) — only when you need a subtask that writes files or runs complex multi-step commands.',
33
- " Explore is fast and cheap. Use it liberally for codebase questions. Spawn multiple in parallel.",
34
- "3. Use bash to run commands, check logs, reproduce issues",
35
- "4. Use tools to trace the bug:",
29
+ "2. Delegate research to subagents where useful (see the delegation guidance in your system prompt), and use bash to run commands, check logs, reproduce issues",
30
+ "3. Use tools to trace the bug:",
36
31
  " - cbm_search/cbm_search_code: find relevant code by concept or text",
37
32
  " - lsp goToDefinition, findReferences, hover: precise navigation and type info",
38
33
  " - lsp goToImplementation: check interface implementors",
@@ -60,6 +55,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
60
55
  " <Unresolved items needing user input. Omit section if none.>",
61
56
  "",
62
57
  "These files are validated programmatically. Missing sections or unexpected sections will be rejected.",
58
+ "Use pp_write_state_file / pp_edit_state_file (NOT the generic write/edit) for .pp state files — they keep the output compact and validate structure.",
63
59
  "",
64
60
  "# Optional: focused analysis artifacts",
65
61
  `You may also write additional analysis files to ${taskDir}/artifacts/<name>.md`,
@@ -84,11 +80,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
84
80
  "",
85
81
  "# How to work:",
86
82
  "- Discuss the topic with the user. Ask clarifying questions. Propose approaches. Analyze tradeoffs.",
87
- "- Spawn subagents for research (subagent_type is REQUIRED calls without it are rejected):",
88
- ' Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
89
- ' Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
90
- ' Agent(subagent_type="Task", ...) — only when you need a subtask that writes files or runs complex multi-step commands.',
91
- " Spawn multiple Explore agents in parallel for broad searches.",
83
+ "- Delegate research to subagents where useful (see the delegation guidance in your system prompt).",
92
84
  "- Use tools directly for quick lookups (cbm_search, lsp, ast_search, grep, etc.)",
93
85
  "- Present findings to the user and discuss them. Don't just dump raw results.",
94
86
  "",
@@ -120,11 +112,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
120
112
  "",
121
113
  "# Steps:",
122
114
  "1. Clarify requirements with the user if anything is ambiguous",
123
- "2. Spawn subagents for research (subagent_type is REQUIREDcalls without it are rejected):",
124
- ' - Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
125
- ' - Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
126
- ' - Agent(subagent_type="Task", ...) — only when you need a subtask that writes files or runs complex multi-step commands.',
127
- " Spawn multiple Explore agents in parallel for broad searches.",
115
+ "2. Delegate research to subagents where useful (see the delegation guidance in your system prompt) spawn multiple explores in parallel for broad searches",
128
116
  "3. Use tools to understand code structure:",
129
117
  " - cbm_search: natural-language search across all symbols",
130
118
  " - cbm_search_code: graph-augmented grep (deduplicates into functions)",
@@ -155,6 +143,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
155
143
  " <Unresolved items needing user input. Omit section if none.>",
156
144
  "",
157
145
  "These files are validated programmatically. Missing sections or unexpected sections will be rejected.",
146
+ "Use pp_write_state_file / pp_edit_state_file (NOT the generic write/edit) for .pp state files — they keep the output compact and validate structure.",
158
147
  "",
159
148
  "# Optional: focused analysis artifacts",
160
149
  `You may also write additional analysis files to ${taskDir}/artifacts/<name>.md`,
@@ -210,7 +199,7 @@ export async function spawnBrainstormReviewers(
210
199
  const agent = createBrainstormReviewerAgent(
211
200
  variant,
212
201
  reviewerVariants,
213
- { userRequest, research, artifacts: artifacts.length > 0 ? artifacts : undefined },
202
+ { userRequest, research, artifacts: artifacts.length > 0 ? artifacts : undefined, manifest: getArtifactManifest(taskDir) },
214
203
  outputPath,
215
204
  contextDirs,
216
205
  "brainstorm",
@@ -261,7 +250,7 @@ export async function spawnBrainstormReviewers(
261
250
  `${reviewOutputFiles.length} brainstorm reviewer(s) completed (round ${round}). Reviews in ${reviewsDir}:`,
262
251
  ...reviewOutputFiles.map((f) => ` - ${f}`),
263
252
  "",
264
- "Read all reviews and update USER_REQUEST.md and RESEARCH.md if needed.",
253
+ "Read all reviews and update USER_REQUEST.md, RESEARCH.md, and any artifacts/ files if needed.",
265
254
  ].join("\n"),
266
255
  display: true,
267
256
  },
@@ -0,0 +1,11 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { implementationSystemPrompt } from "./implementation.js";
3
+
4
+ describe("implementationSystemPrompt self-complete directive", () => {
5
+ it("instructs the agent to call pp_phase_complete when the implement phase is complete", () => {
6
+ const prompt = implementationSystemPrompt("/tmp/task", "/tmp");
7
+ expect(prompt).toContain("pp_phase_complete");
8
+ expect(prompt).toContain("Do NOT instead ask the user to run /pp manually");
9
+ expect(prompt).not.toContain("do NOT wait for the user");
10
+ });
11
+ });
@@ -11,7 +11,7 @@ export function implementationSystemPrompt(taskDir: string, cwd: string): string
11
11
  "# Instructions:",
12
12
  "1. Review the synthesized plan in your context",
13
13
  "2. Implement each item in order",
14
- "3. Check off items in the plan (change - [ ] to - [x]) as you complete them",
14
+ "3. Check off items in the plan (change - [ ] to - [x]) as you complete them — use pp_edit_state_file (NOT the generic edit) to update the plan compactly",
15
15
  "4. After editing files, run lsp diagnostics to check for errors; use lsp codeActions for auto-fixes",
16
16
  "5. Before modifying code, understand it first:",
17
17
  " - cbm_search/cbm_search_code: find relevant functions and their context",
@@ -21,11 +21,7 @@ export function implementationSystemPrompt(taskDir: string, cwd: string): string
21
21
  " - cbm_trace: trace dependencies across the codebase",
22
22
  " - ast_search: find structural patterns (e.g. error handling conventions)",
23
23
  "6. After completing changes, run cbm_changes to verify blast radius",
24
- "7. Spawn subagents as needed (subagent_type is REQUIREDcalls without it are rejected):",
25
- ' - Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
26
- ' - Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
27
- ' - Agent(subagent_type="Task", ...) — parallelizable, self-contained implementation subtasks.',
28
- " Spawn multiple Explore agents in parallel for broad searches.",
24
+ "7. Delegate to subagents where useful (see the delegation guidance in your system prompt) spawn multiple explores in parallel for broad searches, and use task for parallelizable self-contained implementation subtasks",
29
25
  "",
30
26
  "# Commits:",
31
27
  "After completing a logical unit of work (a plan item, a bug fix, a test), call pp_commit",
@@ -34,5 +30,7 @@ export function implementationSystemPrompt(taskDir: string, cwd: string): string
34
30
  "Don't batch all changes into one commit.",
35
31
  "",
36
32
  "Fix issues found by lsp diagnostics before moving on.",
33
+ "",
34
+ "When you judge the implement phase complete, call pp_phase_complete — the extension opens the advance gate for the user to review and confirm. Do NOT instead ask the user to run /pp manually.",
37
35
  ].join("\n");
38
36
  }
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { planningSystemPrompt } from "./planning.js";
3
+
4
+ describe("planningSystemPrompt self-complete directive", () => {
5
+ it("guided synthesis instructs the agent to call pp_phase_complete when synthesis is complete", () => {
6
+ const prompt = planningSystemPrompt("/tmp/task", "guided");
7
+ expect(prompt).toContain("pp_phase_complete");
8
+ expect(prompt).toContain("Do NOT instead ask the user to run /pp manually");
9
+ });
10
+
11
+ it("autonomous synthesis does not add the guided self-complete directive", () => {
12
+ const prompt = planningSystemPrompt("/tmp/task", "autonomous");
13
+ expect(prompt).not.toContain("pp_phase_complete");
14
+ expect(prompt).not.toContain("Do NOT instead ask the user to run /pp manually");
15
+ });
16
+ });
@@ -5,7 +5,7 @@ import { resolvePreset, type PiPiConfig, type VariantConfig } from "../config.js
5
5
  import { registerAgentDefinitions, spawnViaRpc, waitForCompletion } from "../agents/registry.js";
6
6
  import { createPlannerAgent } from "../agents/planner.js";
7
7
  import { createPlanReviewerAgent } from "../agents/plan-reviewer.js";
8
- import { getContextDirs, getLatestSynthesizedPlan } from "../context.js";
8
+ import { getContextDirs, getLatestSynthesizedPlan, getArtifactManifest } from "../context.js";
9
9
  import type { RepoInfo } from "../repo-utils.js";
10
10
  import { validatePlan } from "../validate-artifacts.js";
11
11
  import type { TaskMode } from "../state.js";
@@ -21,6 +21,10 @@ export function planningSystemPrompt(taskDir: string, mode: TaskMode): string {
21
21
  mode === "autonomous"
22
22
  ? " If planner outputs CONTRADICT each other on a locked decision, resolve it by favoring USER_REQUEST.md, RECORD the contradiction and your chosen resolution in the plan, and proceed — do NOT stall waiting for the user (there is none)."
23
23
  : " If planner outputs CONTRADICT each other on a locked decision, surface the contradiction to the user and let them decide — do NOT silently invent a compromise.";
24
+ const synthesizeCompletionRule =
25
+ mode === "autonomous"
26
+ ? ""
27
+ : "When you judge the plan synthesis complete, call pp_phase_complete — the extension opens the advance gate for the user to review and confirm. Do NOT instead ask the user to run /pp manually.";
24
28
  return [
25
29
  "[PI-PI — PLAN PHASE]",
26
30
  "",
@@ -46,9 +50,12 @@ export function planningSystemPrompt(taskDir: string, mode: TaskMode): string {
46
50
  "- ## Scope: 2-4 lines — what changes, what doesn't, critical constraints",
47
51
  "- ## Checklist: each item is - [ ] <outcome> — Done when: <observable condition>",
48
52
  " Each item = one independently verifiable outcome. No code snippets or file-by-file instructions.",
53
+ "- ## Pattern constraints: include this section whenever the task adds a type, function, parser, annotation, config key, enum, or any user-facing value. For each, name the CLOSEST EXISTING analog in the codebase (found by behavior, not filename) and the exact conventions the implementer MUST mirror: data shape (prefer one existing shape over inventing parallel/duplicated state), spelling/casing of user-facing values (match existing values — never invent a new casing), and parser/validation/error-handling shape. These are acceptance criteria, not suggestions. Omit the section only if the task adds none of the above.",
49
54
  "- ## Blockers: unresolved issues blocking implementation (omit if none)",
55
+ "Write/update the synthesized plan with pp_write_state_file / pp_edit_state_file (NOT the generic write/edit) — they keep the output compact and validate structure.",
50
56
  "- No other top-level sections allowed",
51
- "- Describe outcomes, not code-level mechanics",
57
+ "- Describe outcomes, not code-level mechanics, EXCEPT in ## Pattern constraints where naming the concrete analog and conventions is required",
58
+ ...(synthesizeCompletionRule ? ["", synthesizeCompletionRule] : []),
52
59
  ].join("\n");
53
60
  }
54
61
 
@@ -84,7 +91,7 @@ export async function spawnPlanners(
84
91
 
85
92
  for (const [variant] of enabledVariants) {
86
93
  const outputPath = join(plansDir, `${timestamp}_${variant}.md`);
87
- const agent = createPlannerAgent(variant, plannerVariants, { userRequest, research }, outputPath, contextDirs, "plan", repos);
94
+ const agent = createPlannerAgent(variant, plannerVariants, { userRequest, research, manifest: getArtifactManifest(taskDir) }, outputPath, contextDirs, "plan", repos);
88
95
 
89
96
  registerAgentDefinitions(pi, [{ type: "planner", variant, ...agent }]);
90
97
 
@@ -211,7 +218,7 @@ export async function spawnPlanReviewers(
211
218
  const agent = createPlanReviewerAgent(
212
219
  variant,
213
220
  reviewerVariants,
214
- { userRequest, research, synthesizedPlan },
221
+ { userRequest, research, synthesizedPlan, manifest: getArtifactManifest(taskDir) },
215
222
  outputPath,
216
223
  contextDirs,
217
224
  "plan",
@@ -12,10 +12,7 @@ export function reviewSystemPrompt(taskDir: string, cwd: string): string {
12
12
  "- read, lsp, grep, find for understanding the code",
13
13
  "- gh pr view for GitHub PR context if a PR URL is mentioned",
14
14
  "",
15
- "Spawn subagents as needed (subagent_type is REQUIREDcalls without it are rejected):",
16
- '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
17
- '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
18
- "Spawn multiple Explore agents in parallel for broad searches.",
15
+ "Delegate to subagents where useful (see the delegation guidance in your system prompt) spawn multiple explores in parallel for broad searches.",
19
16
  "",
20
17
  "Write your findings:",
21
18
  `- ${taskDir}/USER_REQUEST.md — update with a clear problem statement of issues found`,
@@ -0,0 +1,62 @@
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("brainstorm apply-feedback prompt permits artifact updates and steers to the compact tools", () => {
27
+ const prompt = reviewSystemPrompt("/tmp/task", 1, "brainstorm", "autonomous");
28
+ expect(prompt).toContain("artifacts/");
29
+ expect(prompt).toContain("pp_write_state_file");
30
+ // The no-new-sections rule for the two structured files is retained.
31
+ expect(prompt).toContain("Do NOT add, rename, or remove sections in USER_REQUEST.md or RESEARCH.md");
32
+ });
33
+
34
+ it("autonomous plan feedback folds into a new synthesized plan, not a separate fix-plan file", () => {
35
+ // Re-review and the transition read only the latest `*synthesized*` plan, so
36
+ // plan-phase feedback must land there.
37
+ const plan = reviewSystemPrompt("/tmp/task", 1, "plan", "autonomous");
38
+ expect(plan).toContain("synthesized.md");
39
+ expect(plan).not.toContain("do NOT modify the original synthesized plan");
40
+
41
+ // The implement phase keeps the fix-plan/implement pattern (synthesized plan
42
+ // is code guidance there, not the reviewed artifact).
43
+ const impl = reviewSystemPrompt("/tmp/task", 1, "implement", "autonomous");
44
+ expect(impl).toContain("Create a fix plan");
45
+ expect(impl).toContain("Implement the fixes");
46
+ });
47
+
48
+ it("points each phase at the directory its reviewers actually write to", () => {
49
+ // plan reviewers write to plan-reviews (planning.ts) and outputs load from
50
+ // plan-reviews (context.ts); the prompt must match, not code-reviews.
51
+ const plan = reviewSystemPrompt("/tmp/task", 1, "plan", "autonomous");
52
+ expect(plan).toContain("plan-reviews/");
53
+ expect(plan).not.toContain("code-reviews/");
54
+
55
+ const impl = reviewSystemPrompt("/tmp/task", 1, "implement", "autonomous");
56
+ expect(impl).toContain("code-reviews/");
57
+ expect(impl).not.toContain("plan-reviews/");
58
+
59
+ const brainstorm = reviewSystemPrompt("/tmp/task", 1, "brainstorm", "guided");
60
+ expect(brainstorm).toContain("brainstorm-reviews/");
61
+ });
62
+ });
@@ -4,7 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { resolvePreset, type PiPiConfig, type VariantConfig } from "../config.js";
5
5
  import { registerAgentDefinitions, spawnViaRpc, waitForCompletion } from "../agents/registry.js";
6
6
  import { createCodeReviewerAgent } from "../agents/code-reviewer.js";
7
- import { getContextDirs, getLatestSynthesizedPlan } from "../context.js";
7
+ import { getContextDirs, getLatestSynthesizedPlan, getArtifactManifest } from "../context.js";
8
8
  import type { RepoInfo } from "../repo-utils.js";
9
9
  import type { PhaseSend } from "../transition-controller.js";
10
10
 
@@ -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") {
@@ -32,36 +38,73 @@ export function reviewSystemPrompt(taskDir: string, pass: number, phase?: string
32
38
  "# Your job:",
33
39
  `1. Read ALL reviewer outputs from ${reviewsDir}/`,
34
40
  "2. Identify valid gaps and inaccuracies that would block planning",
35
- "3. If changes are needed: update ONLY the content within existing sections of USER_REQUEST.md / RESEARCH.md",
41
+ "3. If changes are needed: update the content within existing sections of USER_REQUEST.md / RESEARCH.md, and add/update artifacts/ files as needed",
36
42
  "4. If reviewers found no actionable gaps (e.g. task already done, minor suggestions only): do NOT modify the files",
37
43
  "5. Ignore suggestions that don't affect downstream planning quality",
38
44
  "",
39
45
  "USER_REQUEST.md MUST keep exactly: # User Request, ## Problem, ## Constraints",
40
46
  "RESEARCH.md MUST keep exactly: ## Affected Code, ## Architecture Context, ## Constraints & Edge Cases, ## Open Questions (optional)",
41
47
  "Any other sections will fail validation.",
48
+ "Use pp_write_state_file / pp_edit_state_file (NOT the generic write/edit) for these .pp state files — they keep the output compact and validate structure.",
42
49
  ].join("\n");
43
50
  }
44
51
 
52
+ // In autonomous plan/implement the phase does NOT complete until the agent
53
+ // re-calls pp_phase_complete: that is what finalizes the pass and advances the
54
+ // phase. Telling it to "present to the user" and wait causes the plan phase to
55
+ // stall after applying feedback. Guided mode keeps the user-facing behavior.
56
+ const finalStep =
57
+ mode === "autonomous"
58
+ ? "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"
59
+ : "3. Present the synthesis to the user";
60
+
61
+ // In the plan phase the synthesized plan IS the reviewed artifact: re-review
62
+ // and the phase transition both read only the LATEST `*synthesized*` file (see
63
+ // getLatestSynthesizedPlan). So autonomous plan feedback must be folded into a
64
+ // new synthesized plan, NOT a separate fix-plan file (which those readers
65
+ // ignore). In the implement phase the synthesized plan is code guidance, so
66
+ // the fix-plan/implement/afterImplement pattern is correct there.
67
+ const tail =
68
+ mode === "autonomous"
69
+ ? phase === "plan"
70
+ ? [
71
+ "",
72
+ "If the reviewers require changes:",
73
+ `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)`,
74
+ "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.",
75
+ ]
76
+ : [
77
+ "",
78
+ "If changes are needed:",
79
+ `1. Create a fix plan at ${plansDir}/<timestamp>_<description>.md (do NOT modify the original synthesized plan)`,
80
+ "2. Implement the fixes",
81
+ "3. Run afterImplement commands",
82
+ "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.",
83
+ ]
84
+ : [
85
+ "",
86
+ "If changes are needed:",
87
+ `1. Create a fix plan at ${plansDir}/<timestamp>_<description>.md (do NOT modify the original synthesized plan)`,
88
+ "2. Implement the fixes",
89
+ "3. Run afterImplement commands",
90
+ "4. A new review pass will begin",
91
+ ];
92
+
45
93
  return [
46
94
  `[PI-PI — REVIEW CYCLE (pass ${pass})]`,
47
95
  "",
48
- "Code reviewer outputs are ready.",
96
+ "Reviewer outputs are ready.",
49
97
  `Read them from ${reviewsDir}/, synthesize feedback, and implement fixes if needed.`,
50
98
  "",
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.",
99
+ "You are a SYNTHESIZER: merge the reviewer outputs. Do NOT write your own review from scratch.",
100
+ `- Do NOT create the ${reviewsDirName}/ directory yourself — the extension manages it.`,
53
101
  "- Do NOT call plannotator_submit_plan.",
54
102
  "",
55
103
  "# Your job (in this order):",
56
104
  `1. Read ALL reviewer outputs from ${reviewsDir}/`,
57
105
  `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",
106
+ finalStep,
107
+ ...tail,
65
108
  ].join("\n");
66
109
  }
67
110
 
@@ -117,7 +160,7 @@ export async function spawnCodeReviewers(
117
160
  const agent = createCodeReviewerAgent(
118
161
  variant,
119
162
  reviewerVariants,
120
- { userRequest, research, synthesizedPlan },
163
+ { userRequest, research, synthesizedPlan, manifest: getArtifactManifest(taskDir) },
121
164
  outputPath,
122
165
  contextDirs,
123
166
  reviewerPhase,