@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.
- package/3p/pi-ask-user/index.ts +65 -49
- package/3p/pi-subagents/src/agent-manager.ts +8 -0
- package/3p/pi-subagents/src/agent-runner.ts +112 -19
- package/3p/pi-subagents/src/index.ts +3 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
- package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
- package/extensions/orchestrator/agents/constraints.ts +55 -0
- package/extensions/orchestrator/agents/explore.ts +13 -13
- package/extensions/orchestrator/agents/librarian.ts +12 -9
- package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
- package/extensions/orchestrator/agents/planner.ts +28 -23
- package/extensions/orchestrator/agents/registry.ts +2 -1
- package/extensions/orchestrator/agents/repo-context.ts +11 -0
- package/extensions/orchestrator/agents/task.ts +14 -11
- package/extensions/orchestrator/agents/tool-routing.ts +17 -32
- package/extensions/orchestrator/ast-search.ts +2 -1
- package/extensions/orchestrator/cbm.test.ts +35 -0
- package/extensions/orchestrator/cbm.ts +43 -13
- package/extensions/orchestrator/command-handlers.test.ts +390 -19
- package/extensions/orchestrator/command-handlers.ts +73 -31
- package/extensions/orchestrator/commands.test.ts +255 -2
- package/extensions/orchestrator/commands.ts +108 -10
- package/extensions/orchestrator/config.test.ts +289 -68
- package/extensions/orchestrator/config.ts +630 -121
- package/extensions/orchestrator/context.test.ts +177 -10
- package/extensions/orchestrator/context.ts +115 -14
- package/extensions/orchestrator/custom-footer.ts +2 -1
- package/extensions/orchestrator/doctor.test.ts +559 -0
- package/extensions/orchestrator/doctor.ts +664 -0
- package/extensions/orchestrator/event-handlers.test.ts +84 -22
- package/extensions/orchestrator/event-handlers.ts +1191 -360
- package/extensions/orchestrator/exa.test.ts +46 -0
- package/extensions/orchestrator/exa.ts +16 -10
- package/extensions/orchestrator/flant-infra.test.ts +224 -0
- package/extensions/orchestrator/flant-infra.ts +110 -41
- package/extensions/orchestrator/index.ts +13 -2
- package/extensions/orchestrator/integration.test.ts +2866 -118
- package/extensions/orchestrator/log.test.ts +219 -0
- package/extensions/orchestrator/log.ts +153 -0
- package/extensions/orchestrator/model-registry.test.ts +238 -0
- package/extensions/orchestrator/model-registry.ts +282 -0
- package/extensions/orchestrator/model-version.test.ts +27 -0
- package/extensions/orchestrator/model-version.ts +19 -0
- package/extensions/orchestrator/orchestrator.test.ts +206 -56
- package/extensions/orchestrator/orchestrator.ts +295 -148
- package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
- package/extensions/orchestrator/phases/brainstorm.ts +41 -35
- package/extensions/orchestrator/phases/implementation.ts +7 -11
- package/extensions/orchestrator/phases/machine.test.ts +27 -8
- package/extensions/orchestrator/phases/machine.ts +13 -0
- package/extensions/orchestrator/phases/planning.ts +57 -29
- package/extensions/orchestrator/phases/review-task.ts +3 -3
- package/extensions/orchestrator/phases/review.ts +38 -39
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
- package/extensions/orchestrator/phases/verdict.test.ts +139 -0
- package/extensions/orchestrator/phases/verdict.ts +82 -0
- package/extensions/orchestrator/plannotator.test.ts +85 -0
- package/extensions/orchestrator/plannotator.ts +24 -6
- package/extensions/orchestrator/pp-menu.test.ts +134 -0
- package/extensions/orchestrator/pp-menu.ts +2631 -392
- package/extensions/orchestrator/repo-utils.test.ts +151 -0
- package/extensions/orchestrator/repo-utils.ts +67 -0
- package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
- package/extensions/orchestrator/spawn-cleanup.ts +35 -0
- package/extensions/orchestrator/state.test.ts +76 -6
- package/extensions/orchestrator/state.ts +89 -26
- package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
- package/extensions/orchestrator/test-helpers.ts +217 -0
- package/extensions/orchestrator/tracer.test.ts +132 -0
- package/extensions/orchestrator/tracer.ts +127 -0
- package/extensions/orchestrator/transition-controller.test.ts +207 -0
- package/extensions/orchestrator/transition-controller.ts +259 -0
- package/extensions/orchestrator/usage-tracker.test.ts +435 -0
- package/extensions/orchestrator/usage-tracker.ts +23 -2
- package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
- 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
|
|
6
|
-
const prompt = brainstormSystemPrompt("debug", "fix a bug", "/tmp/task");
|
|
7
|
-
expect(prompt).toContain("
|
|
8
|
-
expect(prompt).not.toContain("
|
|
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
|
|
12
|
-
const prompt = brainstormSystemPrompt("brainstorm", "explore ideas", "/tmp/task");
|
|
13
|
-
expect(prompt).toContain("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
+
"context",
|
|
263
269
|
);
|
|
264
270
|
} else if (enabledVariants.length > 0) {
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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).
|
|
36
|
-
"",
|
|
37
|
-
"
|
|
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
|
-
"
|
|
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", "
|
|
67
|
-
expect(canTransition("debug", "debug", "
|
|
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", "
|
|
73
|
-
expect(canTransition("brainstorm", "brainstorm", "
|
|
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("
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
"context",
|
|
107
121
|
);
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
} catch (err: any) {
|
|
111
125
|
failedVariants.push(variant);
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
"context",
|
|
141
155
|
);
|
|
142
156
|
} else {
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
+
"context",
|
|
242
270
|
);
|
|
243
271
|
} else if (enabledVariants.length > 0) {
|
|
244
|
-
|
|
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
|
-
|
|
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
|
}
|