@ilya-lesikov/pi-pi 0.4.0 → 0.6.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 (76) hide show
  1. package/3p/pi-ask-user/index.ts +65 -49
  2. package/3p/pi-subagents/src/agent-manager.ts +8 -0
  3. package/3p/pi-subagents/src/agent-runner.ts +112 -19
  4. package/3p/pi-subagents/src/index.ts +3 -0
  5. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
  6. package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
  7. package/extensions/orchestrator/agents/constraints.ts +55 -0
  8. package/extensions/orchestrator/agents/explore.ts +13 -13
  9. package/extensions/orchestrator/agents/librarian.ts +12 -9
  10. package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
  11. package/extensions/orchestrator/agents/planner.ts +28 -23
  12. package/extensions/orchestrator/agents/registry.ts +2 -1
  13. package/extensions/orchestrator/agents/repo-context.ts +11 -0
  14. package/extensions/orchestrator/agents/task.ts +14 -11
  15. package/extensions/orchestrator/agents/tool-routing.ts +17 -32
  16. package/extensions/orchestrator/ast-search.ts +2 -1
  17. package/extensions/orchestrator/cbm.test.ts +35 -0
  18. package/extensions/orchestrator/cbm.ts +43 -13
  19. package/extensions/orchestrator/command-handlers.test.ts +390 -19
  20. package/extensions/orchestrator/command-handlers.ts +73 -31
  21. package/extensions/orchestrator/commands.test.ts +255 -2
  22. package/extensions/orchestrator/commands.ts +108 -10
  23. package/extensions/orchestrator/config.test.ts +289 -68
  24. package/extensions/orchestrator/config.ts +630 -121
  25. package/extensions/orchestrator/context.test.ts +177 -10
  26. package/extensions/orchestrator/context.ts +115 -14
  27. package/extensions/orchestrator/custom-footer.ts +2 -1
  28. package/extensions/orchestrator/doctor.test.ts +559 -0
  29. package/extensions/orchestrator/doctor.ts +664 -0
  30. package/extensions/orchestrator/event-handlers.test.ts +84 -22
  31. package/extensions/orchestrator/event-handlers.ts +1191 -360
  32. package/extensions/orchestrator/exa.test.ts +46 -0
  33. package/extensions/orchestrator/exa.ts +16 -10
  34. package/extensions/orchestrator/flant-infra.test.ts +224 -0
  35. package/extensions/orchestrator/flant-infra.ts +110 -41
  36. package/extensions/orchestrator/index.ts +13 -2
  37. package/extensions/orchestrator/integration.test.ts +2866 -118
  38. package/extensions/orchestrator/log.test.ts +219 -0
  39. package/extensions/orchestrator/log.ts +153 -0
  40. package/extensions/orchestrator/model-registry.test.ts +238 -0
  41. package/extensions/orchestrator/model-registry.ts +282 -0
  42. package/extensions/orchestrator/model-version.test.ts +27 -0
  43. package/extensions/orchestrator/model-version.ts +19 -0
  44. package/extensions/orchestrator/orchestrator.test.ts +206 -56
  45. package/extensions/orchestrator/orchestrator.ts +295 -148
  46. package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
  47. package/extensions/orchestrator/phases/brainstorm.ts +41 -35
  48. package/extensions/orchestrator/phases/implementation.ts +7 -11
  49. package/extensions/orchestrator/phases/machine.test.ts +27 -8
  50. package/extensions/orchestrator/phases/machine.ts +13 -0
  51. package/extensions/orchestrator/phases/planning.ts +57 -29
  52. package/extensions/orchestrator/phases/review-task.ts +3 -3
  53. package/extensions/orchestrator/phases/review.ts +38 -39
  54. package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
  55. package/extensions/orchestrator/phases/verdict.test.ts +139 -0
  56. package/extensions/orchestrator/phases/verdict.ts +82 -0
  57. package/extensions/orchestrator/plannotator.test.ts +85 -0
  58. package/extensions/orchestrator/plannotator.ts +24 -6
  59. package/extensions/orchestrator/pp-menu.test.ts +134 -0
  60. package/extensions/orchestrator/pp-menu.ts +2631 -392
  61. package/extensions/orchestrator/repo-utils.test.ts +151 -0
  62. package/extensions/orchestrator/repo-utils.ts +67 -0
  63. package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
  64. package/extensions/orchestrator/spawn-cleanup.ts +35 -0
  65. package/extensions/orchestrator/state.test.ts +76 -6
  66. package/extensions/orchestrator/state.ts +89 -26
  67. package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
  68. package/extensions/orchestrator/test-helpers.ts +217 -0
  69. package/extensions/orchestrator/tracer.test.ts +132 -0
  70. package/extensions/orchestrator/tracer.ts +127 -0
  71. package/extensions/orchestrator/transition-controller.test.ts +207 -0
  72. package/extensions/orchestrator/transition-controller.ts +259 -0
  73. package/extensions/orchestrator/usage-tracker.test.ts +435 -0
  74. package/extensions/orchestrator/usage-tracker.ts +23 -2
  75. package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
  76. package/package.json +2 -1
@@ -2,14 +2,17 @@ import { describe, expect, it } from "vitest";
2
2
  import { brainstormSystemPrompt } from "./brainstorm.js";
3
3
 
4
4
  describe("brainstormSystemPrompt", () => {
5
- it("debug prompt uses pp_phase_complete", () => {
6
- const prompt = brainstormSystemPrompt("debug", "fix a bug", "/tmp/task");
7
- expect(prompt).toContain("pp_phase_complete");
8
- expect(prompt).not.toContain("/pp:next");
5
+ it("debug prompt body is pure procedure (no completion/menu restatements)", () => {
6
+ const prompt = brainstormSystemPrompt("debug", "fix a bug", "/tmp/task", "/tmp");
7
+ expect(prompt).toContain("DEBUG PHASE");
8
+ expect(prompt).not.toContain("pp_phase_complete");
9
+ expect(prompt).not.toContain("/pp");
9
10
  });
10
11
 
11
- it("brainstorm prompt uses pp_phase_complete", () => {
12
- const prompt = brainstormSystemPrompt("brainstorm", "explore ideas", "/tmp/task");
13
- expect(prompt).toContain("pp_phase_complete");
12
+ it("brainstorm prompt body is pure procedure (no completion/menu restatements)", () => {
13
+ const prompt = brainstormSystemPrompt("brainstorm", "explore ideas", "/tmp/task", "/tmp");
14
+ expect(prompt).toContain("conversation");
15
+ expect(prompt).not.toContain("pp_phase_complete");
16
+ expect(prompt).not.toContain("/pp");
14
17
  });
15
18
  });
@@ -1,24 +1,28 @@
1
1
  import { readFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
2
2
  import { join } from "path";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import type { PiPiConfig } from "../config.js";
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";
8
+ import type { RepoInfo } from "../repo-utils.js";
7
9
  import type { TaskType } from "../state.js";
10
+ import type { PhaseSend } from "../transition-controller.js";
8
11
 
9
- export function brainstormSystemPrompt(taskType: TaskType, taskDescription: string, taskDir: string): string {
12
+ function isEnabled(value: { enabled?: boolean } | undefined): boolean {
13
+ return value?.enabled !== false;
14
+ }
15
+
16
+ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: string, taskDir: string, cwd: string): string {
17
+ const registerReposInstruction = `First, register all git repositories you'll work in using pp_register_repo (including the root: ${cwd}). For each, determine the base branch by examining the current branch and remote tracking.`;
10
18
  if (taskType === "debug") {
11
19
  return [
12
20
  "[PI-PI — DEBUG PHASE]",
13
21
  `Problem: ${taskDescription}`,
14
22
  "",
15
- "Read-only diagnosis mode.",
16
- "",
17
- "# FORBIDDENdo NOT do any of these:",
18
- "- Do NOT modify project source code (no write or edit tools on project files)",
19
- "- Do NOT create or modify any files outside the task directory",
20
- "- Do NOT implement fixes — only diagnose and recommend",
21
- "- If the user asks you to implement a fix or start coding — call pp_phase_complete instead. It will offer \"Implement a fix\" as an option. Do NOT implement directly in this session.",
23
+ registerReposInstruction,
24
+ "",
25
+ "Read-only diagnosis mode. You MAY use write/edit for diagnosis only (repro/test/analysis files) never to implement the actual fix or feature.",
22
26
  "",
23
27
  "# Your job:",
24
28
  "1. Clarify the problem with the user if needed",
@@ -63,7 +67,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
63
67
  "Each artifact must start with # <Title>. Content is freeform. These are reviewed alongside USER_REQUEST.md and RESEARCH.md.",
64
68
  "Do NOT duplicate content already in RESEARCH.md — artifacts are for supplementary deep dives.",
65
69
  "",
66
- "When both required files are complete, call pp_phase_complete with a brief summary.",
70
+ "Keep USER_REQUEST.md current: update it whenever the user's request changes or clarifies, so it never goes stale.",
67
71
  ].join("\n");
68
72
  }
69
73
 
@@ -72,6 +76,8 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
72
76
  "[PI-PI — BRAINSTORM]",
73
77
  `Topic: ${taskDescription}`,
74
78
  "",
79
+ registerReposInstruction,
80
+ "",
75
81
  "# This is a conversation, not a task.",
76
82
  "Your primary job is to TALK WITH THE USER. Explore ideas, analyze tradeoffs, answer questions, discuss approaches.",
77
83
  "Do NOT rush to produce artifacts or finish. Stay in the conversation until the user is satisfied.",
@@ -85,12 +91,6 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
85
91
  " Spawn multiple Explore agents in parallel for broad searches.",
86
92
  "- Use tools directly for quick lookups (cbm_search, lsp, ast_search, grep, etc.)",
87
93
  "- Present findings to the user and discuss them. Don't just dump raw results.",
88
- "- Do NOT modify project source code.",
89
- "",
90
- "# When to finish:",
91
- "Do NOT call pp_phase_complete on your own. The user will tell you when they're done,",
92
- "or use /pp to advance. Keep the conversation going until then.",
93
- "If the user asks you to implement, write code, or start building — tell them to use /pp which will offer \"Start implementation\" as an option. Do NOT implement directly in this session.",
94
94
  "",
95
95
  "# Optional artifacts (only when the conversation naturally produces them):",
96
96
  "If the discussion leads to a clear action plan or the user asks you to capture conclusions,",
@@ -99,14 +99,13 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
99
99
  `- ${taskDir}/RESEARCH.md — MUST use structure: ## Affected Code, ## Architecture Context, ## Constraints & Edge Cases, ## Open Questions (optional)`,
100
100
  "These files are validated. Missing or unexpected sections will be rejected.",
101
101
  "Do NOT create these files preemptively. Only write them when there's substance to capture.",
102
+ "Once USER_REQUEST.md exists, keep it current: update it whenever the user's request changes or clarifies, so it never goes stale.",
102
103
  "",
103
104
  "# Optional: focused analysis artifacts",
104
105
  `You may also write additional analysis files to ${taskDir}/artifacts/<name>.md`,
105
106
  "for deep dives on specific topics (e.g. architecture analysis, API comparison, risk assessment).",
106
107
  "Each artifact must start with # <Title>. Content is freeform. These are reviewed alongside USER_REQUEST.md and RESEARCH.md.",
107
108
  "Do NOT duplicate content already in RESEARCH.md — artifacts are for supplementary deep dives.",
108
- "",
109
- "Do NOT modify any files except .md files in the task directory.",
110
109
  ].join("\n");
111
110
  }
112
111
 
@@ -114,15 +113,11 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
114
113
  "[PI-PI — BRAINSTORM PHASE]",
115
114
  `Task: ${taskDescription}`,
116
115
  "",
116
+ registerReposInstruction,
117
+ "",
117
118
  "Your job is to produce USER_REQUEST.md and RESEARCH.md — complete enough that",
118
119
  "downstream agents can work without re-exploring the codebase or re-interviewing the user.",
119
120
  "",
120
- "# FORBIDDEN — do NOT do any of these:",
121
- "- Do NOT modify project source code (no write or edit tools on project files)",
122
- "- Do NOT create or modify any files outside the task directory",
123
- "- Do NOT start implementing — only research and document",
124
- "- If the user asks you to implement or start coding — tell them to use /pp which will offer phase advancement. Do NOT implement directly in this session.",
125
- "",
126
121
  "# Steps:",
127
122
  "1. Clarify requirements with the user if anything is ambiguous",
128
123
  "2. Spawn subagents for research (subagent_type is REQUIRED — calls without it are rejected):",
@@ -138,6 +133,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
138
133
  " - ast_search: find structural patterns across the codebase",
139
134
  "4. Ask the user follow-up questions as needed",
140
135
  "5. Write findings into RESEARCH.md as results come back — don't wait for all subagents",
136
+ "6. Keep USER_REQUEST.md current: update it whenever the user's request changes or clarifies, so it reflects what the user actually wants — don't write it once and leave it stale",
141
137
  "",
142
138
  "Produce two files:",
143
139
  `- ${taskDir}/USER_REQUEST.md — MUST follow this exact structure:`,
@@ -165,9 +161,6 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
165
161
  "for deep dives on specific topics (e.g. architecture analysis, API comparison, risk assessment).",
166
162
  "Each artifact must start with # <Title>. Content is freeform. These are reviewed alongside USER_REQUEST.md and RESEARCH.md.",
167
163
  "Do NOT duplicate content already in RESEARCH.md — artifacts are for supplementary deep dives.",
168
- "",
169
- "Do NOT modify any files except .md files in the task directory.",
170
- "When both files are produced and thorough, call pp_phase_complete with a brief summary.",
171
164
  ].join("\n");
172
165
  }
173
166
 
@@ -178,6 +171,9 @@ export async function spawnBrainstormReviewers(
178
171
  taskId: string,
179
172
  config: PiPiConfig,
180
173
  round: number,
174
+ send: PhaseSend,
175
+ variants?: Record<string, VariantConfig>,
176
+ repos: RepoInfo[] = [],
181
177
  ): Promise<{ spawned: number; files: string[]; agentIds: string[]; failedVariants: string[] }> {
182
178
  const urPath = join(taskDir, "USER_REQUEST.md");
183
179
  const resPath = join(taskDir, "RESEARCH.md");
@@ -200,7 +196,9 @@ export async function spawnBrainstormReviewers(
200
196
  }
201
197
 
202
198
  const timestamp = Math.floor(Date.now() / 1000);
203
- const enabledVariants = Object.entries(config.brainstormReviewers).filter(([, v]) => v.enabled);
199
+ const reviewerVariants = variants ?? resolvePreset(config, "brainstormReviewers");
200
+ const enabledVariants = Object.entries(reviewerVariants).filter(([, v]) => isEnabled(v));
201
+ const contextDirs = getContextDirs(cwd, repos, config.general.loadExtraRepoConfigs);
204
202
  const reviewFiles: string[] = [];
205
203
  const agentIds: string[] = [];
206
204
  const failedVariants: string[] = [];
@@ -209,7 +207,15 @@ export async function spawnBrainstormReviewers(
209
207
  for (const [variant] of enabledVariants) {
210
208
  const outputPath = join(reviewsDir, `${timestamp}_${variant}_round-${round}.md`);
211
209
  reviewFiles.push(outputPath);
212
- const agent = createBrainstormReviewerAgent(variant, config, { userRequest, research, artifacts: artifacts.length > 0 ? artifacts : undefined }, outputPath);
210
+ const agent = createBrainstormReviewerAgent(
211
+ variant,
212
+ reviewerVariants,
213
+ { userRequest, research, artifacts: artifacts.length > 0 ? artifacts : undefined },
214
+ outputPath,
215
+ contextDirs,
216
+ "brainstorm",
217
+ repos,
218
+ );
213
219
 
214
220
  registerAgentDefinitions(pi, [{ type: "brainstorm_reviewer", variant, ...agent }]);
215
221
 
@@ -228,13 +234,13 @@ export async function spawnBrainstormReviewers(
228
234
  await waitForCompletion(pi, id);
229
235
  } catch (err: any) {
230
236
  failedVariants.push(variant);
231
- pi.sendMessage(
237
+ send(
232
238
  {
233
239
  customType: "pp-brainstorm-reviewer-error",
234
240
  content: `Brainstorm reviewer variant "${variant}" failed: ${err.message}`,
235
241
  display: true,
236
242
  },
237
- { deliverAs: "steer" },
243
+ "context",
238
244
  );
239
245
  }
240
246
  })(),
@@ -248,7 +254,7 @@ export async function spawnBrainstormReviewers(
248
254
  : [];
249
255
 
250
256
  if (reviewOutputFiles.length > 0) {
251
- pi.sendMessage(
257
+ send(
252
258
  {
253
259
  customType: "pp-brainstorm-reviews-done",
254
260
  content: [
@@ -259,10 +265,10 @@ export async function spawnBrainstormReviewers(
259
265
  ].join("\n"),
260
266
  display: true,
261
267
  },
262
- { deliverAs: "steer" },
268
+ "context",
263
269
  );
264
270
  } else if (enabledVariants.length > 0) {
265
- pi.sendMessage(
271
+ send(
266
272
  {
267
273
  customType: "pp-brainstorm-reviews-error",
268
274
  content: [
@@ -271,7 +277,7 @@ export async function spawnBrainstormReviewers(
271
277
  ].join("\n"),
272
278
  display: true,
273
279
  },
274
- { deliverAs: "steer" },
280
+ "context",
275
281
  );
276
282
  }
277
283
 
@@ -1,11 +1,10 @@
1
- export function implementationSystemPrompt(taskDir: string): string {
1
+ export function implementationSystemPrompt(taskDir: string, cwd: string): string {
2
2
  return [
3
3
  "[PI-PI — IMPLEMENT PHASE]",
4
4
  "",
5
- "The plan has been approved. Implement it.",
5
+ `First, register all git repositories you'll work in using pp_register_repo (including the root: ${cwd}). For each, determine the base branch by examining the current branch and remote tracking.`,
6
6
  "",
7
- "# FORBIDDEN do NOT do any of these:",
8
- "- Do NOT skip plan items without the user's approval",
7
+ "The plan has been approved. Implement it.",
9
8
  "",
10
9
  "USER_REQUEST.md, RESEARCH.md, and the synthesized plan are already provided in your context above — do NOT re-read them from disk.",
11
10
  "",
@@ -28,15 +27,12 @@ export function implementationSystemPrompt(taskDir: string): string {
28
27
  ' - Agent(subagent_type="Task", ...) — parallelizable, self-contained implementation subtasks.',
29
28
  " Spawn multiple Explore agents in parallel for broad searches.",
30
29
  "",
31
- "If a fix fails 3 times, STOP and re-plan the approach — do not keep pushing.",
32
- "",
33
30
  "# Commits:",
34
31
  "After completing a logical unit of work (a plan item, a bug fix, a test), call pp_commit",
35
- "with a descriptive message (what changed and why). Don't batch all changes into one commit.",
36
- "",
37
- "Constraints:",
38
- "- Fix issues found by lsp diagnostics before moving on",
32
+ "with a descriptive message (what changed and why). Prefix it with a conventional-commit",
33
+ "type (fix:, feat:, or chore:) unless the user asked for a different commit style.",
34
+ "Don't batch all changes into one commit.",
39
35
  "",
40
- "When all plan items are checked off, call pp_phase_complete with a brief summary.",
36
+ "Fix issues found by lsp diagnostics before moving on.",
41
37
  ].join("\n");
42
38
  }
@@ -63,16 +63,21 @@ describe("canTransition", () => {
63
63
  });
64
64
 
65
65
  it("handles debug transitions", () => {
66
- expect(canTransition("debug", "debug", "done")).toBe(true);
67
- expect(canTransition("debug", "debug", "plan")).toBe(false);
66
+ expect(canTransition("debug", "debug", "plan")).toBe(true);
67
+ expect(canTransition("debug", "debug", "done")).toBe(false);
68
68
  expect(canTransition("debug", "done", "debug")).toBe(false);
69
69
  });
70
70
 
71
71
  it("handles brainstorm transitions", () => {
72
- expect(canTransition("brainstorm", "brainstorm", "done")).toBe(true);
73
- expect(canTransition("brainstorm", "brainstorm", "plan")).toBe(false);
72
+ expect(canTransition("brainstorm", "brainstorm", "plan")).toBe(true);
73
+ expect(canTransition("brainstorm", "brainstorm", "done")).toBe(false);
74
74
  expect(canTransition("brainstorm", "done", "brainstorm")).toBe(false);
75
75
  });
76
+
77
+ it("handles quick transitions", () => {
78
+ expect(canTransition("quick", "quick", "done")).toBe(true);
79
+ expect(canTransition("quick", "quick", "implement")).toBe(false);
80
+ });
76
81
  });
77
82
 
78
83
  describe("nextPhase", () => {
@@ -84,14 +89,19 @@ describe("nextPhase", () => {
84
89
  });
85
90
 
86
91
  it("returns debug next phase and terminal null", () => {
87
- expect(nextPhase("debug", "debug")).toBe("done");
92
+ expect(nextPhase("debug", "debug")).toBe("plan");
88
93
  expect(nextPhase("debug", "done")).toBeNull();
89
94
  });
90
95
 
91
96
  it("returns brainstorm next phase and terminal null", () => {
92
- expect(nextPhase("brainstorm", "brainstorm")).toBe("done");
97
+ expect(nextPhase("brainstorm", "brainstorm")).toBe("plan");
93
98
  expect(nextPhase("brainstorm", "done")).toBeNull();
94
99
  });
100
+
101
+ it("returns quick next phase and terminal null", () => {
102
+ expect(nextPhase("quick", "quick")).toBe("done");
103
+ expect(nextPhase("quick", "done")).toBeNull();
104
+ });
95
105
  });
96
106
 
97
107
  describe("phasePipeline", () => {
@@ -100,11 +110,15 @@ describe("phasePipeline", () => {
100
110
  });
101
111
 
102
112
  it("returns debug pipeline", () => {
103
- expect(phasePipeline("debug")).toEqual(["debug", "done"]);
113
+ expect(phasePipeline("debug")).toEqual(["debug", "plan", "implement", "done"]);
104
114
  });
105
115
 
106
116
  it("returns brainstorm pipeline", () => {
107
- expect(phasePipeline("brainstorm")).toEqual(["brainstorm", "done"]);
117
+ expect(phasePipeline("brainstorm")).toEqual(["brainstorm", "plan", "implement", "done"]);
118
+ });
119
+
120
+ it("returns quick pipeline", () => {
121
+ expect(phasePipeline("quick")).toEqual(["quick", "done"]);
108
122
  });
109
123
  });
110
124
 
@@ -290,4 +304,9 @@ Fix bug.
290
304
  reason: "Unknown phase: done",
291
305
  });
292
306
  });
307
+
308
+ it("always passes quick phase validation", () => {
309
+ const dir = makeTempDir();
310
+ expect(validateExitCriteria(dir, "quick", "quick")).toEqual({ ok: true });
311
+ });
293
312
  });
@@ -72,6 +72,9 @@ const TRANSITIONS: Record<TaskType, Record<string, string[]>> = {
72
72
  plan: ["implement"],
73
73
  implement: ["done"],
74
74
  },
75
+ quick: {
76
+ quick: ["done"],
77
+ },
75
78
  };
76
79
 
77
80
  export function canTransition(taskType: TaskType, from: Phase, to: Phase): boolean {
@@ -91,6 +94,10 @@ export function validateExitCriteria(
91
94
  ): { ok: true } | { ok: false; reason: string } {
92
95
  switch (phase) {
93
96
  case "brainstorm": {
97
+ if (taskType === "brainstorm") {
98
+ return { ok: true };
99
+ }
100
+
94
101
  const ur = join(taskDir, "USER_REQUEST.md");
95
102
  const res = join(taskDir, "RESEARCH.md");
96
103
  if (isMissingOrEmpty(ur)) {
@@ -228,6 +235,10 @@ export function validateExitCriteria(
228
235
  return { ok: true };
229
236
  }
230
237
 
238
+ case "quick": {
239
+ return { ok: true };
240
+ }
241
+
231
242
  default:
232
243
  return { ok: false, reason: `Unknown phase: ${phase}` };
233
244
  }
@@ -243,5 +254,7 @@ export function phasePipeline(taskType: TaskType): Phase[] {
243
254
  return ["brainstorm", "plan", "implement", "done"];
244
255
  case "review":
245
256
  return ["review", "plan", "implement", "done"];
257
+ case "quick":
258
+ return ["quick", "done"];
246
259
  }
247
260
  }
@@ -1,34 +1,45 @@
1
1
  import { readFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
2
2
  import { join } from "path";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import type { PiPiConfig } from "../config.js";
4
+ 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 { getLatestSynthesizedPlan } from "../context.js";
8
+ import { getContextDirs, getLatestSynthesizedPlan } from "../context.js";
9
+ import type { RepoInfo } from "../repo-utils.js";
9
10
  import { validatePlan } from "../validate-artifacts.js";
11
+ import type { TaskMode } from "../state.js";
12
+ import type { PhaseSend } from "../transition-controller.js";
10
13
 
11
- export function planningSystemPrompt(taskDir: string): string {
14
+ function isEnabled(value: { enabled?: boolean } | undefined): boolean {
15
+ return value?.enabled !== false;
16
+ }
17
+
18
+ export function planningSystemPrompt(taskDir: string, mode: TaskMode): string {
12
19
  const plansDir = join(taskDir, "plans");
20
+ const contradictionRule =
21
+ mode === "autonomous"
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
+ : " 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.";
13
24
  return [
14
25
  "[PI-PI — PLAN PHASE]",
15
26
  "",
27
+ "You are a SYNTHESIZER: you MERGE the planner outputs into one plan. Do NOT write your own plan from scratch.",
16
28
  "Planning subagents are working in parallel to create plans.",
17
29
  `They will write their outputs to ${plansDir}/. Wait for the notification that all planners completed.`,
18
30
  "",
19
- "# FORBIDDEN — do NOT do any of these:",
20
- "- Do NOT write your own plan from scratch. You are a SYNTHESIZER, not a planner.",
21
31
  "- Do NOT create the plans/ directory yourself — the extension manages it.",
22
32
  "- Do NOT check the plans directory yourself — wait for the notification that all planners completed.",
23
33
  "- Do NOT read project source code directly — the planner outputs already contain the analysis.",
24
- "- Do NOT call plannotator_submit_plan — plan review is handled by the user via /pp menu.",
34
+ "- Do NOT call plannotator_submit_plan.",
25
35
  "",
26
36
  "# Your job (in this order):",
27
37
  "1. Wait for the notification that says 'All planners completed' — do NOT proceed before this",
28
38
  `2. Read ALL planner outputs from ${plansDir}/`,
29
39
  "3. USER_REQUEST.md and RESEARCH.md are already provided in your context above — do NOT re-read them from disk",
30
40
  `4. Synthesize all plans into a single plan at ${plansDir}/<timestamp>_synthesized.md`,
31
- "5. Ask the user for clarifications if unsure about anything",
41
+ "5. Treat as LOCKED PREDICATES (do not re-litigate): the user's explicit constraints, chosen language/framework, and scope from USER_REQUEST.md. Discard any planner suggestion that violates them.",
42
+ contradictionRule,
32
43
  "",
33
44
  "Plan format:",
34
45
  "- Start with # Plan",
@@ -38,8 +49,6 @@ export function planningSystemPrompt(taskDir: string): string {
38
49
  "- ## Blockers: unresolved issues blocking implementation (omit if none)",
39
50
  "- No other top-level sections allowed",
40
51
  "- Describe outcomes, not code-level mechanics",
41
- "",
42
- "When the synthesized plan is ready, call pp_phase_complete with a brief summary of the plan.",
43
52
  ].join("\n");
44
53
  }
45
54
 
@@ -49,6 +58,9 @@ export async function spawnPlanners(
49
58
  taskDir: string,
50
59
  taskId: string,
51
60
  config: PiPiConfig,
61
+ send: PhaseSend,
62
+ variants?: Record<string, VariantConfig>,
63
+ repos: RepoInfo[] = [],
52
64
  ): Promise<{ spawned: number; agentIds: string[]; failedVariants: string[] }> {
53
65
  const urPath = join(taskDir, "USER_REQUEST.md");
54
66
  const resPath = join(taskDir, "RESEARCH.md");
@@ -63,14 +75,16 @@ export async function spawnPlanners(
63
75
  }
64
76
 
65
77
  const timestamp = Math.floor(Date.now() / 1000);
66
- const enabledVariants = Object.entries(config.planners).filter(([, v]) => v.enabled);
78
+ const plannerVariants = variants ?? resolvePreset(config, "planners");
79
+ const enabledVariants = Object.entries(plannerVariants).filter(([, v]) => isEnabled(v));
80
+ const contextDirs = getContextDirs(cwd, repos, config.general.loadExtraRepoConfigs);
67
81
  const agentIds: string[] = [];
68
82
  const failedVariants: string[] = [];
69
83
  const results: Promise<void>[] = [];
70
84
 
71
85
  for (const [variant] of enabledVariants) {
72
86
  const outputPath = join(plansDir, `${timestamp}_${variant}.md`);
73
- const agent = createPlannerAgent(variant, config, { userRequest, research }, outputPath);
87
+ const agent = createPlannerAgent(variant, plannerVariants, { userRequest, research }, outputPath, contextDirs, "plan", repos);
74
88
 
75
89
  registerAgentDefinitions(pi, [{ type: "planner", variant, ...agent }]);
76
90
 
@@ -97,25 +111,25 @@ export async function spawnPlanners(
97
111
  const planContent = readFileSync(outputPath, "utf-8");
98
112
  const validation = validatePlan(planContent);
99
113
  if (!validation.ok) {
100
- pi.sendMessage(
114
+ send(
101
115
  {
102
116
  customType: "pp-planner-error",
103
117
  content: `Planner "${variant}" produced invalid plan (errors were shown to the agent but it did not fix them): ${validation.errors.join("; ")}`,
104
118
  display: true,
105
119
  },
106
- { deliverAs: "steer" },
120
+ "context",
107
121
  );
108
122
  }
109
123
  }
110
124
  } catch (err: any) {
111
125
  failedVariants.push(variant);
112
- pi.sendMessage(
126
+ send(
113
127
  {
114
128
  customType: "pp-planner-error",
115
129
  content: `Planner variant "${variant}" failed: ${err.message}`,
116
130
  display: true,
117
131
  },
118
- { deliverAs: "steer" },
132
+ "context",
119
133
  );
120
134
  }
121
135
  })(),
@@ -126,7 +140,7 @@ export async function spawnPlanners(
126
140
 
127
141
  const planFiles = existsSync(plansDir) ? readdirSync(plansDir).filter((f) => !f.includes("synthesized") && !f.includes("review_")) : [];
128
142
  if (planFiles.length > 0) {
129
- pi.sendMessage(
143
+ send(
130
144
  {
131
145
  customType: "pp-planners-done",
132
146
  content: [
@@ -137,10 +151,10 @@ export async function spawnPlanners(
137
151
  ].join("\n"),
138
152
  display: true,
139
153
  },
140
- { deliverAs: "steer" },
154
+ "context",
141
155
  );
142
156
  } else {
143
- pi.sendMessage(
157
+ send(
144
158
  {
145
159
  customType: "pp-planners-error",
146
160
  content: [
@@ -149,7 +163,7 @@ export async function spawnPlanners(
149
163
  ].join("\n"),
150
164
  display: true,
151
165
  },
152
- { deliverAs: "steer" },
166
+ "context",
153
167
  );
154
168
  }
155
169
 
@@ -162,6 +176,10 @@ export async function spawnPlanReviewers(
162
176
  taskDir: string,
163
177
  taskId: string,
164
178
  config: PiPiConfig,
179
+ pass: number,
180
+ send: PhaseSend,
181
+ variants?: Record<string, VariantConfig>,
182
+ repos: RepoInfo[] = [],
165
183
  ): Promise<{ spawned: number; files: string[]; agentIds: string[]; failedVariants: string[] }> {
166
184
  const urPath = join(taskDir, "USER_REQUEST.md");
167
185
  const resPath = join(taskDir, "RESEARCH.md");
@@ -178,17 +196,27 @@ export async function spawnPlanReviewers(
178
196
  }
179
197
 
180
198
  const timestamp = Math.floor(Date.now() / 1000);
181
- const enabledVariants = Object.entries(config.planReviewers).filter(([, v]) => v.enabled);
199
+ const reviewerVariants = variants ?? resolvePreset(config, "planReviewers");
200
+ const enabledVariants = Object.entries(reviewerVariants).filter(([, v]) => isEnabled(v));
201
+ const contextDirs = getContextDirs(cwd, repos, config.general.loadExtraRepoConfigs);
182
202
  const reviewFiles: string[] = [];
183
203
  const agentIds: string[] = [];
184
204
  const failedVariants: string[] = [];
185
205
 
186
206
  const results: Promise<void>[] = [];
187
207
  for (const [variant] of enabledVariants) {
188
- const outputPath = join(planReviewsDir, `${timestamp}_${variant}.md`);
208
+ const outputPath = join(planReviewsDir, `${timestamp}_${variant}_round-${pass}.md`);
189
209
  reviewFiles.push(outputPath);
190
210
 
191
- const agent = createPlanReviewerAgent(variant, config, { userRequest, research, synthesizedPlan }, outputPath);
211
+ const agent = createPlanReviewerAgent(
212
+ variant,
213
+ reviewerVariants,
214
+ { userRequest, research, synthesizedPlan },
215
+ outputPath,
216
+ contextDirs,
217
+ "plan",
218
+ repos,
219
+ );
192
220
 
193
221
  registerAgentDefinitions(pi, [{ type: "plan_reviewer", variant, ...agent }]);
194
222
 
@@ -207,13 +235,13 @@ export async function spawnPlanReviewers(
207
235
  await waitForCompletion(pi, id);
208
236
  } catch (err: any) {
209
237
  failedVariants.push(variant);
210
- pi.sendMessage(
238
+ send(
211
239
  {
212
240
  customType: "pp-plan-reviewer-error",
213
241
  content: `Plan reviewer variant "${variant}" failed: ${err.message}`,
214
242
  display: true,
215
243
  },
216
- { deliverAs: "steer" },
244
+ "context",
217
245
  );
218
246
  }
219
247
  })(),
@@ -223,11 +251,11 @@ export async function spawnPlanReviewers(
223
251
  await Promise.allSettled(results);
224
252
 
225
253
  const actualReviewFiles = existsSync(planReviewsDir)
226
- ? readdirSync(planReviewsDir).filter((f) => f.startsWith(`${timestamp}`) && f.endsWith(".md"))
254
+ ? readdirSync(planReviewsDir).filter((f) => f.startsWith(`${timestamp}`) && f.includes(`round-${pass}`) && f.endsWith(".md"))
227
255
  : [];
228
256
 
229
257
  if (actualReviewFiles.length > 0) {
230
- pi.sendMessage(
258
+ send(
231
259
  {
232
260
  customType: "pp-plan-reviews-done",
233
261
  content: [
@@ -238,16 +266,16 @@ export async function spawnPlanReviewers(
238
266
  ].join("\n"),
239
267
  display: true,
240
268
  },
241
- { deliverAs: "steer" },
269
+ "context",
242
270
  );
243
271
  } else if (enabledVariants.length > 0) {
244
- pi.sendMessage(
272
+ send(
245
273
  {
246
274
  customType: "pp-plan-reviews-error",
247
275
  content: "All plan reviewer variants failed — no reviews were produced. Proceeding without plan review.",
248
276
  display: true,
249
277
  },
250
- { deliverAs: "steer" },
278
+ "context",
251
279
  );
252
280
  }
253
281
 
@@ -1,7 +1,9 @@
1
- export function reviewSystemPrompt(taskDir: string): string {
1
+ export function reviewSystemPrompt(taskDir: string, cwd: string): string {
2
2
  return [
3
3
  "[PI-PI — REVIEW PHASE]",
4
4
  "",
5
+ `First, register all git repositories you'll work in using pp_register_repo (including the root: ${cwd}). For each, determine the base branch by examining the current branch and remote tracking.`,
6
+ "",
5
7
  "You are reviewing code changes. USER_REQUEST.md describes what to review.",
6
8
  "Read it first to understand the scope.",
7
9
  "",
@@ -31,7 +33,5 @@ export function reviewSystemPrompt(taskDir: string): string {
31
33
  "- ## Open Questions (optional)",
32
34
  "",
33
35
  "Focus on: correctness, edge cases, style consistency, missing tests, potential bugs.",
34
- "",
35
- "When complete, call pp_phase_complete with a brief summary of findings.",
36
36
  ].join("\n");
37
37
  }