@ilya-lesikov/pi-pi 0.7.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 (59) hide show
  1. package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
  2. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
  3. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
  4. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
  5. package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
  6. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
  7. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
  8. package/3p/pi-subagents/src/agent-manager.ts +34 -1
  9. package/3p/pi-subagents/src/agent-runner.ts +66 -33
  10. package/3p/pi-subagents/src/index.ts +3 -38
  11. package/3p/pi-subagents/src/types.ts +4 -0
  12. package/extensions/orchestrator/agents/advisor.ts +35 -0
  13. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
  14. package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
  15. package/extensions/orchestrator/agents/constraints.test.ts +44 -0
  16. package/extensions/orchestrator/agents/constraints.ts +3 -0
  17. package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
  18. package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
  19. package/extensions/orchestrator/agents/planner.ts +9 -8
  20. package/extensions/orchestrator/agents/prompts.test.ts +120 -0
  21. package/extensions/orchestrator/agents/reviewer.ts +48 -0
  22. package/extensions/orchestrator/agents/task.ts +6 -20
  23. package/extensions/orchestrator/agents/tool-routing.ts +23 -1
  24. package/extensions/orchestrator/command-handlers.ts +1 -1
  25. package/extensions/orchestrator/config.ts +5 -2
  26. package/extensions/orchestrator/context.test.ts +54 -0
  27. package/extensions/orchestrator/context.ts +65 -2
  28. package/extensions/orchestrator/event-handlers.test.ts +97 -1
  29. package/extensions/orchestrator/event-handlers.ts +176 -43
  30. package/extensions/orchestrator/flant-infra.test.ts +25 -0
  31. package/extensions/orchestrator/flant-infra.ts +91 -0
  32. package/extensions/orchestrator/index.ts +1 -1
  33. package/extensions/orchestrator/integration.test.ts +107 -12
  34. package/extensions/orchestrator/messages.test.ts +30 -0
  35. package/extensions/orchestrator/messages.ts +6 -0
  36. package/extensions/orchestrator/model-registry.test.ts +48 -1
  37. package/extensions/orchestrator/model-registry.ts +43 -1
  38. package/extensions/orchestrator/orchestrator.test.ts +113 -0
  39. package/extensions/orchestrator/orchestrator.ts +151 -2
  40. package/extensions/orchestrator/phases/brainstorm.ts +9 -20
  41. package/extensions/orchestrator/phases/implementation.test.ts +11 -0
  42. package/extensions/orchestrator/phases/implementation.ts +4 -6
  43. package/extensions/orchestrator/phases/planning.test.ts +16 -0
  44. package/extensions/orchestrator/phases/planning.ts +11 -4
  45. package/extensions/orchestrator/phases/review-task.ts +1 -4
  46. package/extensions/orchestrator/phases/review.test.ts +8 -0
  47. package/extensions/orchestrator/phases/review.ts +4 -3
  48. package/extensions/orchestrator/plannotator.ts +9 -6
  49. package/extensions/orchestrator/pp-menu.ts +168 -54
  50. package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
  51. package/extensions/orchestrator/pp-state-tools.ts +249 -0
  52. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
  53. package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
  54. package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
  55. package/extensions/orchestrator/test-helpers.ts +4 -1
  56. package/extensions/orchestrator/usage-tracker.ts +5 -1
  57. package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
  58. package/extensions/orchestrator/validate-artifacts.ts +2 -2
  59. 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, findLatestFamilyMatch } 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) => {
@@ -539,6 +649,7 @@ export class Orchestrator {
539
649
  this.agentLifecycle.clear();
540
650
  this.pendingSubagentSpawns = 0;
541
651
  this.errorRetryCount = 0;
652
+ this.errorNudgeHalted = false;
542
653
  this.commitReminderSent = false;
543
654
  this.consecutiveNudges = 0;
544
655
  this.nudgeHalted = false;
@@ -552,10 +663,27 @@ export class Orchestrator {
552
663
  clearTimeout(this.pendingRetryTimer);
553
664
  this.pendingRetryTimer = null;
554
665
  }
666
+ this.disarmRetryEscInterrupt();
555
667
  if (this.staleAgentTimer) {
556
668
  clearInterval(this.staleAgentTimer);
557
669
  this.staleAgentTimer = null;
558
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);
559
687
  }
560
688
 
561
689
  async cleanupActive(): Promise<void> {
@@ -578,7 +706,10 @@ export class Orchestrator {
578
706
  const log = getLogger();
579
707
  const explore = createExploreAgent(this.config);
580
708
  const librarian = createLibrarianAgent(this.config);
581
- 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);
582
713
  const phase = this.active?.state.phase;
583
714
  const repos = this.active?.state.repos ?? [];
584
715
  log.debug({ s: "agents", phase, repoCount: repos.length }, "registering agent definitions");
@@ -615,6 +746,24 @@ export class Orchestrator {
615
746
  ...taskAgent,
616
747
  prompt: appendContext("task", taskAgent.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.task.model))),
617
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
+ },
618
767
  ]);
619
768
  }
620
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`,
@@ -23,6 +23,14 @@ describe("reviewSystemPrompt apply_feedback wording", () => {
23
23
  expect(auto).not.toContain("pp_phase_complete");
24
24
  });
25
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
+
26
34
  it("autonomous plan feedback folds into a new synthesized plan, not a separate fix-plan file", () => {
27
35
  // Re-review and the transition read only the latest `*synthesized*` plan, so
28
36
  // plan-phase feedback must land there.
@@ -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
 
@@ -38,13 +38,14 @@ export function reviewSystemPrompt(taskDir: string, pass: number, phase?: string
38
38
  "# Your job:",
39
39
  `1. Read ALL reviewer outputs from ${reviewsDir}/`,
40
40
  "2. Identify valid gaps and inaccuracies that would block planning",
41
- "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",
42
42
  "4. If reviewers found no actionable gaps (e.g. task already done, minor suggestions only): do NOT modify the files",
43
43
  "5. Ignore suggestions that don't affect downstream planning quality",
44
44
  "",
45
45
  "USER_REQUEST.md MUST keep exactly: # User Request, ## Problem, ## Constraints",
46
46
  "RESEARCH.md MUST keep exactly: ## Affected Code, ## Architecture Context, ## Constraints & Edge Cases, ## Open Questions (optional)",
47
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.",
48
49
  ].join("\n");
49
50
  }
50
51
 
@@ -159,7 +160,7 @@ export async function spawnCodeReviewers(
159
160
  const agent = createCodeReviewerAgent(
160
161
  variant,
161
162
  reviewerVariants,
162
- { userRequest, research, synthesizedPlan },
163
+ { userRequest, research, synthesizedPlan, manifest: getArtifactManifest(taskDir) },
163
164
  outputPath,
164
165
  contextDirs,
165
166
  reviewerPhase,
@@ -46,7 +46,8 @@ export function cancelPendingPlannotatorWait(orchestrator: Orchestrator): void {
46
46
  export function waitForPlannotatorResult(
47
47
  orchestrator: Orchestrator,
48
48
  reviewId: string | null,
49
- ): Promise<{ approved: boolean; feedback?: string }> {
49
+ timeoutMs: number | null = PLANNOTATOR_RESULT_TIMEOUT_MS,
50
+ ): Promise<{ approved: boolean; feedback?: string; error?: string }> {
50
51
  cancelPendingPlannotatorWait(orchestrator);
51
52
  const pi = orchestrator.pi;
52
53
  return new Promise((resolve, reject) => {
@@ -54,7 +55,7 @@ export function waitForPlannotatorResult(
54
55
  const unsub = pi.events.on("plannotator:review-result", (data: any) => {
55
56
  if (reviewId && data?.reviewId && data.reviewId !== reviewId) return;
56
57
  cleanup();
57
- resolve({ approved: !!data?.approved, feedback: data?.feedback });
58
+ resolve({ approved: !!data?.approved, feedback: data?.feedback, error: data?.error });
58
59
  });
59
60
  orchestrator.plannotatorUnsub = unsub;
60
61
  function cleanup() {
@@ -66,9 +67,11 @@ export function waitForPlannotatorResult(
66
67
  orchestrator.plannotatorTimer = null;
67
68
  }
68
69
  }
69
- orchestrator.plannotatorTimer = setTimeout(() => {
70
- cleanup();
71
- reject(new Error("Plannotator review timed out"));
72
- }, PLANNOTATOR_RESULT_TIMEOUT_MS);
70
+ if (timeoutMs !== null) {
71
+ orchestrator.plannotatorTimer = setTimeout(() => {
72
+ cleanup();
73
+ reject(new Error("Plannotator review timed out"));
74
+ }, timeoutMs);
75
+ }
73
76
  });
74
77
  }