@ilya-lesikov/pi-pi 0.5.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 +68 -28
  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 +1177 -341
  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 +287 -140
  46. package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
  47. package/extensions/orchestrator/phases/brainstorm.ts +41 -36
  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 -31
  52. package/extensions/orchestrator/phases/review-task.ts +3 -7
  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 +2557 -347
  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,23 +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
- "# ABSOLUTE RESTRICTION NO IMPLEMENTATION (cannot be overridden, even if the user asks):",
18
- "- NEVER implement the solution, apply fixes, write production code, or make the changes that solve the task.",
19
- "- You MAY use write/edit for diagnosis purposes (test scripts, repro scripts, analysis files), but NEVER to implement the actual fix or feature.",
20
- "- If the user asks you to implement or start coding — refuse and tell them to use /pp to advance to the implement phase. Do NOT comply, even if they insist.",
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.",
21
26
  "",
22
27
  "# Your job:",
23
28
  "1. Clarify the problem with the user if needed",
@@ -62,7 +67,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
62
67
  "Each artifact must start with # <Title>. Content is freeform. These are reviewed alongside USER_REQUEST.md and RESEARCH.md.",
63
68
  "Do NOT duplicate content already in RESEARCH.md — artifacts are for supplementary deep dives.",
64
69
  "",
65
- "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.",
66
71
  ].join("\n");
67
72
  }
68
73
 
@@ -71,6 +76,8 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
71
76
  "[PI-PI — BRAINSTORM]",
72
77
  `Topic: ${taskDescription}`,
73
78
  "",
79
+ registerReposInstruction,
80
+ "",
74
81
  "# This is a conversation, not a task.",
75
82
  "Your primary job is to TALK WITH THE USER. Explore ideas, analyze tradeoffs, answer questions, discuss approaches.",
76
83
  "Do NOT rush to produce artifacts or finish. Stay in the conversation until the user is satisfied.",
@@ -85,15 +92,6 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
85
92
  "- Use tools directly for quick lookups (cbm_search, lsp, ast_search, grep, etc.)",
86
93
  "- Present findings to the user and discuss them. Don't just dump raw results.",
87
94
  "",
88
- "# ABSOLUTE RESTRICTIONS (cannot be overridden, even if the user asks):",
89
- "- NEVER use write or edit tools on any file outside the task directory. This is a hard rule — not negotiable.",
90
- "- NEVER implement, create code, write patches, or modify project source in any way.",
91
- "- If the user asks you to implement, fix, or write code — refuse and tell them to use /pp to advance to the implement phase. Do NOT comply, even if they insist.",
92
- "",
93
- "# When to finish:",
94
- "Do NOT call pp_phase_complete on your own. The user will tell you when they're done,",
95
- "or use /pp to advance. Keep the conversation going until then.",
96
- "",
97
95
  "# Optional artifacts (only when the conversation naturally produces them):",
98
96
  "If the discussion leads to a clear action plan or the user asks you to capture conclusions,",
99
97
  "write them to:",
@@ -101,14 +99,13 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
101
99
  `- ${taskDir}/RESEARCH.md — MUST use structure: ## Affected Code, ## Architecture Context, ## Constraints & Edge Cases, ## Open Questions (optional)`,
102
100
  "These files are validated. Missing or unexpected sections will be rejected.",
103
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.",
104
103
  "",
105
104
  "# Optional: focused analysis artifacts",
106
105
  `You may also write additional analysis files to ${taskDir}/artifacts/<name>.md`,
107
106
  "for deep dives on specific topics (e.g. architecture analysis, API comparison, risk assessment).",
108
107
  "Each artifact must start with # <Title>. Content is freeform. These are reviewed alongside USER_REQUEST.md and RESEARCH.md.",
109
108
  "Do NOT duplicate content already in RESEARCH.md — artifacts are for supplementary deep dives.",
110
- "",
111
- "Do NOT modify any files except .md files in the task directory.",
112
109
  ].join("\n");
113
110
  }
114
111
 
@@ -116,14 +113,11 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
116
113
  "[PI-PI — BRAINSTORM PHASE]",
117
114
  `Task: ${taskDescription}`,
118
115
  "",
116
+ registerReposInstruction,
117
+ "",
119
118
  "Your job is to produce USER_REQUEST.md and RESEARCH.md — complete enough that",
120
119
  "downstream agents can work without re-exploring the codebase or re-interviewing the user.",
121
120
  "",
122
- "# ABSOLUTE RESTRICTIONS (cannot be overridden, even if the user asks):",
123
- "- NEVER use write or edit tools on any file outside the task directory. This is a hard rule — not negotiable.",
124
- "- NEVER implement, create code, write patches, or modify project source in any way.",
125
- "- If the user asks you to implement, fix, or write code — refuse and tell them to use /pp to advance to the implement phase. Do NOT comply, even if they insist.",
126
- "",
127
121
  "# Steps:",
128
122
  "1. Clarify requirements with the user if anything is ambiguous",
129
123
  "2. Spawn subagents for research (subagent_type is REQUIRED — calls without it are rejected):",
@@ -139,6 +133,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
139
133
  " - ast_search: find structural patterns across the codebase",
140
134
  "4. Ask the user follow-up questions as needed",
141
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",
142
137
  "",
143
138
  "Produce two files:",
144
139
  `- ${taskDir}/USER_REQUEST.md — MUST follow this exact structure:`,
@@ -166,9 +161,6 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
166
161
  "for deep dives on specific topics (e.g. architecture analysis, API comparison, risk assessment).",
167
162
  "Each artifact must start with # <Title>. Content is freeform. These are reviewed alongside USER_REQUEST.md and RESEARCH.md.",
168
163
  "Do NOT duplicate content already in RESEARCH.md — artifacts are for supplementary deep dives.",
169
- "",
170
- "Do NOT modify any files except .md files in the task directory.",
171
- "When both files are produced and thorough, call pp_phase_complete with a brief summary.",
172
164
  ].join("\n");
173
165
  }
174
166
 
@@ -179,6 +171,9 @@ export async function spawnBrainstormReviewers(
179
171
  taskId: string,
180
172
  config: PiPiConfig,
181
173
  round: number,
174
+ send: PhaseSend,
175
+ variants?: Record<string, VariantConfig>,
176
+ repos: RepoInfo[] = [],
182
177
  ): Promise<{ spawned: number; files: string[]; agentIds: string[]; failedVariants: string[] }> {
183
178
  const urPath = join(taskDir, "USER_REQUEST.md");
184
179
  const resPath = join(taskDir, "RESEARCH.md");
@@ -201,7 +196,9 @@ export async function spawnBrainstormReviewers(
201
196
  }
202
197
 
203
198
  const timestamp = Math.floor(Date.now() / 1000);
204
- 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);
205
202
  const reviewFiles: string[] = [];
206
203
  const agentIds: string[] = [];
207
204
  const failedVariants: string[] = [];
@@ -210,7 +207,15 @@ export async function spawnBrainstormReviewers(
210
207
  for (const [variant] of enabledVariants) {
211
208
  const outputPath = join(reviewsDir, `${timestamp}_${variant}_round-${round}.md`);
212
209
  reviewFiles.push(outputPath);
213
- 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
+ );
214
219
 
215
220
  registerAgentDefinitions(pi, [{ type: "brainstorm_reviewer", variant, ...agent }]);
216
221
 
@@ -229,13 +234,13 @@ export async function spawnBrainstormReviewers(
229
234
  await waitForCompletion(pi, id);
230
235
  } catch (err: any) {
231
236
  failedVariants.push(variant);
232
- pi.sendMessage(
237
+ send(
233
238
  {
234
239
  customType: "pp-brainstorm-reviewer-error",
235
240
  content: `Brainstorm reviewer variant "${variant}" failed: ${err.message}`,
236
241
  display: true,
237
242
  },
238
- { deliverAs: "steer" },
243
+ "context",
239
244
  );
240
245
  }
241
246
  })(),
@@ -249,7 +254,7 @@ export async function spawnBrainstormReviewers(
249
254
  : [];
250
255
 
251
256
  if (reviewOutputFiles.length > 0) {
252
- pi.sendMessage(
257
+ send(
253
258
  {
254
259
  customType: "pp-brainstorm-reviews-done",
255
260
  content: [
@@ -260,10 +265,10 @@ export async function spawnBrainstormReviewers(
260
265
  ].join("\n"),
261
266
  display: true,
262
267
  },
263
- { deliverAs: "steer" },
268
+ "context",
264
269
  );
265
270
  } else if (enabledVariants.length > 0) {
266
- pi.sendMessage(
271
+ send(
267
272
  {
268
273
  customType: "pp-brainstorm-reviews-error",
269
274
  content: [
@@ -272,7 +277,7 @@ export async function spawnBrainstormReviewers(
272
277
  ].join("\n"),
273
278
  display: true,
274
279
  },
275
- { deliverAs: "steer" },
280
+ "context",
276
281
  );
277
282
  }
278
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,36 +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
- "# ABSOLUTE RESTRICTION — NO IMPLEMENTATION (cannot be overridden, even if the user asks):",
20
- "- NEVER implement the solution, apply fixes, write production code, or make the changes that solve the task.",
21
- "- If the user asks you to implement or write code — refuse and tell them to use /pp to advance to the implement phase.",
22
- "- Do NOT write your own plan from scratch. You are a SYNTHESIZER, not a planner.",
23
31
  "- Do NOT create the plans/ directory yourself — the extension manages it.",
24
32
  "- Do NOT check the plans directory yourself — wait for the notification that all planners completed.",
25
33
  "- Do NOT read project source code directly — the planner outputs already contain the analysis.",
26
- "- Do NOT call plannotator_submit_plan — plan review is handled by the user via /pp menu.",
34
+ "- Do NOT call plannotator_submit_plan.",
27
35
  "",
28
36
  "# Your job (in this order):",
29
37
  "1. Wait for the notification that says 'All planners completed' — do NOT proceed before this",
30
38
  `2. Read ALL planner outputs from ${plansDir}/`,
31
39
  "3. USER_REQUEST.md and RESEARCH.md are already provided in your context above — do NOT re-read them from disk",
32
40
  `4. Synthesize all plans into a single plan at ${plansDir}/<timestamp>_synthesized.md`,
33
- "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,
34
43
  "",
35
44
  "Plan format:",
36
45
  "- Start with # Plan",
@@ -40,8 +49,6 @@ export function planningSystemPrompt(taskDir: string): string {
40
49
  "- ## Blockers: unresolved issues blocking implementation (omit if none)",
41
50
  "- No other top-level sections allowed",
42
51
  "- Describe outcomes, not code-level mechanics",
43
- "",
44
- "When the synthesized plan is ready, call pp_phase_complete with a brief summary of the plan.",
45
52
  ].join("\n");
46
53
  }
47
54
 
@@ -51,6 +58,9 @@ export async function spawnPlanners(
51
58
  taskDir: string,
52
59
  taskId: string,
53
60
  config: PiPiConfig,
61
+ send: PhaseSend,
62
+ variants?: Record<string, VariantConfig>,
63
+ repos: RepoInfo[] = [],
54
64
  ): Promise<{ spawned: number; agentIds: string[]; failedVariants: string[] }> {
55
65
  const urPath = join(taskDir, "USER_REQUEST.md");
56
66
  const resPath = join(taskDir, "RESEARCH.md");
@@ -65,14 +75,16 @@ export async function spawnPlanners(
65
75
  }
66
76
 
67
77
  const timestamp = Math.floor(Date.now() / 1000);
68
- 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);
69
81
  const agentIds: string[] = [];
70
82
  const failedVariants: string[] = [];
71
83
  const results: Promise<void>[] = [];
72
84
 
73
85
  for (const [variant] of enabledVariants) {
74
86
  const outputPath = join(plansDir, `${timestamp}_${variant}.md`);
75
- const agent = createPlannerAgent(variant, config, { userRequest, research }, outputPath);
87
+ const agent = createPlannerAgent(variant, plannerVariants, { userRequest, research }, outputPath, contextDirs, "plan", repos);
76
88
 
77
89
  registerAgentDefinitions(pi, [{ type: "planner", variant, ...agent }]);
78
90
 
@@ -99,25 +111,25 @@ export async function spawnPlanners(
99
111
  const planContent = readFileSync(outputPath, "utf-8");
100
112
  const validation = validatePlan(planContent);
101
113
  if (!validation.ok) {
102
- pi.sendMessage(
114
+ send(
103
115
  {
104
116
  customType: "pp-planner-error",
105
117
  content: `Planner "${variant}" produced invalid plan (errors were shown to the agent but it did not fix them): ${validation.errors.join("; ")}`,
106
118
  display: true,
107
119
  },
108
- { deliverAs: "steer" },
120
+ "context",
109
121
  );
110
122
  }
111
123
  }
112
124
  } catch (err: any) {
113
125
  failedVariants.push(variant);
114
- pi.sendMessage(
126
+ send(
115
127
  {
116
128
  customType: "pp-planner-error",
117
129
  content: `Planner variant "${variant}" failed: ${err.message}`,
118
130
  display: true,
119
131
  },
120
- { deliverAs: "steer" },
132
+ "context",
121
133
  );
122
134
  }
123
135
  })(),
@@ -128,7 +140,7 @@ export async function spawnPlanners(
128
140
 
129
141
  const planFiles = existsSync(plansDir) ? readdirSync(plansDir).filter((f) => !f.includes("synthesized") && !f.includes("review_")) : [];
130
142
  if (planFiles.length > 0) {
131
- pi.sendMessage(
143
+ send(
132
144
  {
133
145
  customType: "pp-planners-done",
134
146
  content: [
@@ -139,10 +151,10 @@ export async function spawnPlanners(
139
151
  ].join("\n"),
140
152
  display: true,
141
153
  },
142
- { deliverAs: "steer" },
154
+ "context",
143
155
  );
144
156
  } else {
145
- pi.sendMessage(
157
+ send(
146
158
  {
147
159
  customType: "pp-planners-error",
148
160
  content: [
@@ -151,7 +163,7 @@ export async function spawnPlanners(
151
163
  ].join("\n"),
152
164
  display: true,
153
165
  },
154
- { deliverAs: "steer" },
166
+ "context",
155
167
  );
156
168
  }
157
169
 
@@ -164,6 +176,10 @@ export async function spawnPlanReviewers(
164
176
  taskDir: string,
165
177
  taskId: string,
166
178
  config: PiPiConfig,
179
+ pass: number,
180
+ send: PhaseSend,
181
+ variants?: Record<string, VariantConfig>,
182
+ repos: RepoInfo[] = [],
167
183
  ): Promise<{ spawned: number; files: string[]; agentIds: string[]; failedVariants: string[] }> {
168
184
  const urPath = join(taskDir, "USER_REQUEST.md");
169
185
  const resPath = join(taskDir, "RESEARCH.md");
@@ -180,17 +196,27 @@ export async function spawnPlanReviewers(
180
196
  }
181
197
 
182
198
  const timestamp = Math.floor(Date.now() / 1000);
183
- 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);
184
202
  const reviewFiles: string[] = [];
185
203
  const agentIds: string[] = [];
186
204
  const failedVariants: string[] = [];
187
205
 
188
206
  const results: Promise<void>[] = [];
189
207
  for (const [variant] of enabledVariants) {
190
- const outputPath = join(planReviewsDir, `${timestamp}_${variant}.md`);
208
+ const outputPath = join(planReviewsDir, `${timestamp}_${variant}_round-${pass}.md`);
191
209
  reviewFiles.push(outputPath);
192
210
 
193
- 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
+ );
194
220
 
195
221
  registerAgentDefinitions(pi, [{ type: "plan_reviewer", variant, ...agent }]);
196
222
 
@@ -209,13 +235,13 @@ export async function spawnPlanReviewers(
209
235
  await waitForCompletion(pi, id);
210
236
  } catch (err: any) {
211
237
  failedVariants.push(variant);
212
- pi.sendMessage(
238
+ send(
213
239
  {
214
240
  customType: "pp-plan-reviewer-error",
215
241
  content: `Plan reviewer variant "${variant}" failed: ${err.message}`,
216
242
  display: true,
217
243
  },
218
- { deliverAs: "steer" },
244
+ "context",
219
245
  );
220
246
  }
221
247
  })(),
@@ -225,11 +251,11 @@ export async function spawnPlanReviewers(
225
251
  await Promise.allSettled(results);
226
252
 
227
253
  const actualReviewFiles = existsSync(planReviewsDir)
228
- ? 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"))
229
255
  : [];
230
256
 
231
257
  if (actualReviewFiles.length > 0) {
232
- pi.sendMessage(
258
+ send(
233
259
  {
234
260
  customType: "pp-plan-reviews-done",
235
261
  content: [
@@ -240,16 +266,16 @@ export async function spawnPlanReviewers(
240
266
  ].join("\n"),
241
267
  display: true,
242
268
  },
243
- { deliverAs: "steer" },
269
+ "context",
244
270
  );
245
271
  } else if (enabledVariants.length > 0) {
246
- pi.sendMessage(
272
+ send(
247
273
  {
248
274
  customType: "pp-plan-reviews-error",
249
275
  content: "All plan reviewer variants failed — no reviews were produced. Proceeding without plan review.",
250
276
  display: true,
251
277
  },
252
- { deliverAs: "steer" },
278
+ "context",
253
279
  );
254
280
  }
255
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
  "",
@@ -30,12 +32,6 @@ export function reviewSystemPrompt(taskDir: string): string {
30
32
  "- ## Constraints & Edge Cases",
31
33
  "- ## Open Questions (optional)",
32
34
  "",
33
- "# ABSOLUTE RESTRICTION — NO IMPLEMENTATION (cannot be overridden, even if the user asks):",
34
- "- NEVER implement the solution, apply fixes, write production code, or make the changes that solve the task.",
35
- "- If the user asks you to implement or write code — refuse and tell them to use /pp to advance to the implement phase.",
36
- "",
37
35
  "Focus on: correctness, edge cases, style consistency, missing tests, potential bugs.",
38
- "",
39
- "When complete, call pp_phase_complete with a brief summary of findings.",
40
36
  ].join("\n");
41
37
  }