@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.
- 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 +68 -28
- 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 +1177 -341
- 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 +287 -140
- package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
- package/extensions/orchestrator/phases/brainstorm.ts +41 -36
- 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 -31
- package/extensions/orchestrator/phases/review-task.ts +3 -7
- 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 +2557 -347
- 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
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { existsSync, copyFileSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
2
2
|
import { join, basename, relative } from "path";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { loadConfig,
|
|
4
|
+
import { loadConfig, resolvePreset, type NormalizedPiPiConfig } from "./config.js";
|
|
5
5
|
import {
|
|
6
6
|
createTask,
|
|
7
7
|
loadTask,
|
|
8
8
|
saveTask,
|
|
9
9
|
lockTask,
|
|
10
|
-
|
|
10
|
+
getEffectivePhaseMode,
|
|
11
11
|
type TaskType,
|
|
12
|
+
type TaskMode,
|
|
12
13
|
type TaskState,
|
|
13
14
|
type Phase,
|
|
14
15
|
} from "./state.js";
|
|
15
16
|
import { phasePipeline } from "./phases/machine.js";
|
|
16
|
-
import {
|
|
17
|
+
import { getContextDirs, loadAllContextFiles, getPhaseArtifacts, getLatestSynthesizedPlan } from "./context.js";
|
|
17
18
|
import { brainstormSystemPrompt } from "./phases/brainstorm.js";
|
|
18
19
|
import { planningSystemPrompt, spawnPlanners } from "./phases/planning.js";
|
|
19
20
|
import { implementationSystemPrompt } from "./phases/implementation.js";
|
|
@@ -23,6 +24,15 @@ import { registerAgentDefinitions, unregisterAgentDefinitions } from "./agents/r
|
|
|
23
24
|
import { createExploreAgent } from "./agents/explore.js";
|
|
24
25
|
import { createLibrarianAgent } from "./agents/librarian.js";
|
|
25
26
|
import { createTaskAgent } from "./agents/task.js";
|
|
27
|
+
import { resolveModel, getModelInfo } from "./model-registry.js";
|
|
28
|
+
import { buildRepoContext } from "./agents/repo-context.js";
|
|
29
|
+
import { getLogger, addTaskDestination, removeTaskDestination, setLogLevel } from "./log.js";
|
|
30
|
+
import { handleSpawnResult } from "./spawn-cleanup.js";
|
|
31
|
+
import { TransitionController, type TransitionHost } from "./transition-controller.js";
|
|
32
|
+
|
|
33
|
+
function isEnabled(value: { enabled?: boolean } | undefined): boolean {
|
|
34
|
+
return value?.enabled !== false;
|
|
35
|
+
}
|
|
26
36
|
|
|
27
37
|
const BUNDLED_TOOLS = new Set([
|
|
28
38
|
"Agent", "get_subagent_result", "steer_subagent",
|
|
@@ -43,7 +53,7 @@ export interface ActiveTask {
|
|
|
43
53
|
|
|
44
54
|
export class Orchestrator {
|
|
45
55
|
active: ActiveTask | null = null;
|
|
46
|
-
config!:
|
|
56
|
+
config!: NormalizedPiPiConfig;
|
|
47
57
|
cwd = "";
|
|
48
58
|
spawnedAgentIds = new Set<string>();
|
|
49
59
|
agentDescriptions = new Map<string, string>();
|
|
@@ -60,23 +70,18 @@ export class Orchestrator {
|
|
|
60
70
|
step?: string;
|
|
61
71
|
}>();
|
|
62
72
|
staleAgentTimer: ReturnType<typeof setInterval> | null = null;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
nudgeTimestamps: number[] = [];
|
|
68
|
-
cooldownHits: number[] = [];
|
|
73
|
+
// Single consecutive-nudge guard (replaces the old multi-tier throttle). Reset
|
|
74
|
+
// to 0 on any productive turn; once it reaches the cap the nudges halt with one
|
|
75
|
+
// user notification.
|
|
76
|
+
consecutiveNudges = 0;
|
|
69
77
|
nudgeHalted = false;
|
|
70
78
|
pendingSubagentSpawns = 0;
|
|
71
79
|
errorRetryCount = 0;
|
|
72
80
|
commitReminderSent = false;
|
|
73
|
-
textStopReminderSent = false;
|
|
74
81
|
phaseStartTime = 0;
|
|
75
|
-
awaitPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
76
82
|
pendingRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
77
83
|
activeTaskToken = 0;
|
|
78
84
|
userGatePending = false;
|
|
79
|
-
reviewTransitionToken = -1;
|
|
80
85
|
lastCtx: any = null;
|
|
81
86
|
failedPlannerVariants: string[] = [];
|
|
82
87
|
failedReviewerVariants: string[] = [];
|
|
@@ -84,23 +89,60 @@ export class Orchestrator {
|
|
|
84
89
|
reviewerFailureDialogPending = false;
|
|
85
90
|
plannotatorReject: ((reason: Error) => void) | null = null;
|
|
86
91
|
plannotatorUnsub: (() => void) | null = null;
|
|
87
|
-
|
|
92
|
+
plannotatorTimer: ReturnType<typeof setTimeout> | null = null;
|
|
93
|
+
transitionToNextPhase: (ctx: any, plannerPreset?: string) => Promise<{ ok: boolean; error?: string }> = async () => ({ ok: false, error: "not initialized" });
|
|
94
|
+
// Assigned by registerEventHandlers. Wired into planner spawn onSettled as the
|
|
95
|
+
// safety net the deleted 5s poller used to provide: when a spawn settles having
|
|
96
|
+
// produced ZERO agents (zero enabled planners, or all spawns failed before any
|
|
97
|
+
// subagents:completed/failed event), nothing else would advance await_planners.
|
|
98
|
+
// For spawned>0 the lifecycle events drive completion, so onSettled passes the
|
|
99
|
+
// spawned count and this only force-checks the zero case.
|
|
100
|
+
checkPlannerCompletion: () => void = () => {};
|
|
101
|
+
readonly transitionController: TransitionController;
|
|
102
|
+
|
|
103
|
+
constructor(readonly pi: ExtensionAPI) {
|
|
104
|
+
// The controller calls pi (the main session) directly for sends, and uses the
|
|
105
|
+
// host only for live-ctx-dependent bits (compact/isIdle/currentStep).
|
|
106
|
+
this.transitionController = new TransitionController(this.makeTransitionHost(), this.pi);
|
|
107
|
+
}
|
|
88
108
|
|
|
89
|
-
|
|
109
|
+
// Live-session host the TransitionController uses for compaction/idle/step.
|
|
110
|
+
private makeTransitionHost(): TransitionHost {
|
|
111
|
+
return {
|
|
112
|
+
compact: (options) => {
|
|
113
|
+
const compact = this.lastCtx?.compact;
|
|
114
|
+
if (!compact) return false;
|
|
115
|
+
compact(options);
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
isIdle: () => {
|
|
119
|
+
const idle = this.lastCtx?.isIdle;
|
|
120
|
+
return typeof idle === "function" ? !!idle.call(this.lastCtx) : false;
|
|
121
|
+
},
|
|
122
|
+
currentStep: () => this.active?.state.step ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
90
125
|
|
|
91
126
|
safeSendUserMessage(text: string): void {
|
|
92
|
-
const log = (
|
|
93
|
-
if (this.active) appendTaskLog(this.active.dir, "debug.jsonl", { timestamp: new Date().toISOString(), event, text: text.slice(0, 200), ...extra });
|
|
94
|
-
};
|
|
127
|
+
const log = getLogger();
|
|
95
128
|
const attempt = (retries: number) => {
|
|
96
129
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
// Route through the controller's single send path. "instruction" maps to
|
|
131
|
+
// followUp: queues the message and triggers a turn once the current one
|
|
132
|
+
// settles (or runs immediately when idle), so it never throws "Agent is
|
|
133
|
+
// already processing" when called mid-tool (e.g. during a transition).
|
|
134
|
+
this.transitionController.send(text, "instruction");
|
|
135
|
+
log.debug({ s: "orchestrator", retries, text: text.slice(0, 200) }, "safeSend sent");
|
|
99
136
|
} catch (err: any) {
|
|
100
137
|
if (retries < 30) {
|
|
138
|
+
log.debug({ s: "orchestrator", retries, err: err?.message }, "safeSend retry");
|
|
101
139
|
setTimeout(() => attempt(retries + 1), 1000);
|
|
102
140
|
} else {
|
|
103
|
-
log("
|
|
141
|
+
log.error({ s: "orchestrator", retries, err: err?.message ?? String(err), text: text.slice(0, 200) }, "safeSend failed after max retries");
|
|
142
|
+
this.lastCtx?.ui?.notify?.(
|
|
143
|
+
"pi-pi could not deliver a message to the agent; the task may be stalled. See logs.",
|
|
144
|
+
"error",
|
|
145
|
+
);
|
|
104
146
|
}
|
|
105
147
|
}
|
|
106
148
|
};
|
|
@@ -117,34 +159,53 @@ export class Orchestrator {
|
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
async switchModel(ctx: ExtensionContext, modelSpec: string, thinking: string): Promise<boolean> {
|
|
162
|
+
const log = getLogger();
|
|
120
163
|
const registry = ctx.modelRegistry;
|
|
121
164
|
const allModels = registry.getAvailable();
|
|
122
165
|
|
|
123
|
-
const
|
|
166
|
+
const requestedSpecs = [resolveModel(modelSpec), modelSpec].filter((value, index, arr) => arr.indexOf(value) === index);
|
|
167
|
+
log.debug({ s: "model", requestedSpecs, thinking, availableCount: allModels.length }, "switchModel");
|
|
124
168
|
let resolved;
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
169
|
+
for (const spec of requestedSpecs) {
|
|
170
|
+
const slashIdx = spec.indexOf("/");
|
|
171
|
+
if (slashIdx !== -1) {
|
|
172
|
+
const provider = spec.substring(0, slashIdx).trim().toLowerCase();
|
|
173
|
+
const modelId = spec.substring(slashIdx + 1).trim().toLowerCase();
|
|
174
|
+
resolved = allModels.find(
|
|
175
|
+
(m) => m.provider.toLowerCase() === provider && m.id.toLowerCase() === modelId,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!resolved) {
|
|
179
|
+
const pattern = spec.toLowerCase();
|
|
180
|
+
const matches = allModels.filter(
|
|
181
|
+
(m) => m.id.toLowerCase() === pattern || m.id.toLowerCase().includes(pattern),
|
|
182
|
+
);
|
|
183
|
+
if (matches.length === 1) resolved = matches[0];
|
|
184
|
+
}
|
|
185
|
+
if (resolved) break;
|
|
131
186
|
}
|
|
187
|
+
|
|
132
188
|
if (!resolved) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
(m) => m.id.toLowerCase() === pattern || m.id.toLowerCase().includes(pattern),
|
|
136
|
-
);
|
|
137
|
-
if (matches.length === 1) resolved = matches[0];
|
|
189
|
+
log.warn({ s: "model", requestedSpecs }, "model not found");
|
|
190
|
+
return false;
|
|
138
191
|
}
|
|
139
192
|
|
|
140
|
-
if (!resolved) return false;
|
|
141
|
-
|
|
142
193
|
const ok = await this.pi.setModel(resolved);
|
|
143
|
-
if (!ok)
|
|
194
|
+
if (!ok) {
|
|
195
|
+
log.warn({ s: "model", resolved: `${resolved.provider}/${resolved.id}` }, "setModel returned false");
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
144
198
|
|
|
145
|
-
const VALID_THINKING = new Set(["off", "low", "medium", "high"]);
|
|
146
|
-
const thinkingLevel = (VALID_THINKING.has(thinking) ? thinking : "high") as
|
|
199
|
+
const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
|
|
200
|
+
const thinkingLevel = (VALID_THINKING.has(thinking) ? thinking : "high") as
|
|
201
|
+
| "off"
|
|
202
|
+
| "minimal"
|
|
203
|
+
| "low"
|
|
204
|
+
| "medium"
|
|
205
|
+
| "high"
|
|
206
|
+
| "xhigh";
|
|
147
207
|
this.pi.setThinkingLevel(thinkingLevel);
|
|
208
|
+
log.debug({ s: "model", model: `${resolved.provider}/${resolved.id}`, thinking: thinkingLevel }, "model switched");
|
|
148
209
|
return true;
|
|
149
210
|
}
|
|
150
211
|
|
|
@@ -158,11 +219,13 @@ export class Orchestrator {
|
|
|
158
219
|
const phase = this.active.state.phase;
|
|
159
220
|
const step = this.active.state.step;
|
|
160
221
|
const reviewCycle = this.active.state.reviewCycle;
|
|
222
|
+
const effectiveMode = this.active.state.effectiveMode ?? this.active.state.mode;
|
|
223
|
+
const modeLabel = effectiveMode === "autonomous" ? " [autonomous]" : "";
|
|
161
224
|
|
|
162
|
-
if (type === "debug" || type === "brainstorm") {
|
|
225
|
+
if (type === "debug" || type === "brainstorm" || type === "quick") {
|
|
163
226
|
const elapsed = this.phaseStartTime > 0 ? this.formatElapsed(this.phaseStartTime) : "";
|
|
164
227
|
const suffix = elapsed ? ` (${elapsed})` : "";
|
|
165
|
-
ctx.ui.setStatus("pp-phase", `pp: ${type}${suffix}`);
|
|
228
|
+
ctx.ui.setStatus("pp-phase", `pp: ${type}${modeLabel}${suffix}`);
|
|
166
229
|
return;
|
|
167
230
|
}
|
|
168
231
|
|
|
@@ -183,7 +246,7 @@ export class Orchestrator {
|
|
|
183
246
|
else if (step === "user_gate") detail = "review";
|
|
184
247
|
|
|
185
248
|
if (reviewCycle) {
|
|
186
|
-
const kind = reviewCycle.kind === "plannotator" ? "plannotator" :
|
|
249
|
+
const kind = reviewCycle.kind === "plannotator" ? "plannotator" : "review";
|
|
187
250
|
detail = `${kind} #${reviewCycle.pass}`;
|
|
188
251
|
}
|
|
189
252
|
|
|
@@ -195,7 +258,7 @@ export class Orchestrator {
|
|
|
195
258
|
}
|
|
196
259
|
}
|
|
197
260
|
|
|
198
|
-
ctx.ui.setStatus("pp-phase", `pp: ${parts.join(" → ")}`);
|
|
261
|
+
ctx.ui.setStatus("pp-phase", `pp: ${parts.join(" → ")}${modeLabel}`);
|
|
199
262
|
}
|
|
200
263
|
|
|
201
264
|
private formatElapsed(startTime: number): string {
|
|
@@ -209,14 +272,22 @@ export class Orchestrator {
|
|
|
209
272
|
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
210
273
|
}
|
|
211
274
|
|
|
212
|
-
getPlanStartState(taskDir: string): { step: string; shouldSpawnPlanners: boolean } {
|
|
275
|
+
getPlanStartState(taskDir: string, plannerPresetName?: string): { step: string; shouldSpawnPlanners: boolean } {
|
|
213
276
|
const plansDir = join(taskDir, "plans");
|
|
214
|
-
const
|
|
277
|
+
const presetName = plannerPresetName ?? this.config.agents.subagents.presetGroups.planners.default;
|
|
278
|
+
const plannerVariants = resolvePreset(this.config, "planners", presetName);
|
|
279
|
+
const enabledPlannerVariants = Object.entries(plannerVariants)
|
|
280
|
+
.filter(([, v]) => isEnabled(v))
|
|
281
|
+
.map(([name]) => name);
|
|
215
282
|
const plannerOutputs = existsSync(plansDir)
|
|
216
283
|
? readdirSync(plansDir).filter((f) => f.endsWith(".md") && !f.includes("synthesized") && !f.includes("review_"))
|
|
217
284
|
: [];
|
|
285
|
+
const completedVariants = new Set(
|
|
286
|
+
plannerOutputs.map((f) => f.replace(/^\d+_/, "").replace(/\.md$/, "")),
|
|
287
|
+
);
|
|
288
|
+
const hasAllEnabledVariants = enabledPlannerVariants.every((name) => completedVariants.has(name));
|
|
218
289
|
|
|
219
|
-
if (
|
|
290
|
+
if (enabledPlannerVariants.length === 0 || hasAllEnabledVariants || getLatestSynthesizedPlan(taskDir)) {
|
|
220
291
|
return { step: "synthesize", shouldSpawnPlanners: false };
|
|
221
292
|
}
|
|
222
293
|
|
|
@@ -226,23 +297,26 @@ export class Orchestrator {
|
|
|
226
297
|
getPhasePrompt(_ctx: ExtensionContext): string {
|
|
227
298
|
if (!this.active) return "";
|
|
228
299
|
|
|
300
|
+
const mode: TaskMode = getEffectivePhaseMode(this.active.state);
|
|
301
|
+
|
|
229
302
|
if (this.active.state.reviewCycle?.step === "apply_feedback") {
|
|
230
303
|
const pass = this.active.state.reviewCycle.pass;
|
|
231
|
-
|
|
232
|
-
return reviewCycleSystemPrompt(this.active.dir, pass, manualReview, this.active.state.phase);
|
|
304
|
+
return reviewCycleSystemPrompt(this.active.dir, pass, this.active.state.phase);
|
|
233
305
|
}
|
|
234
306
|
|
|
235
307
|
switch (this.active.state.phase) {
|
|
236
308
|
case "brainstorm":
|
|
237
|
-
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir);
|
|
309
|
+
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir, this.cwd);
|
|
238
310
|
case "debug":
|
|
239
|
-
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir);
|
|
311
|
+
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir, this.cwd);
|
|
240
312
|
case "plan":
|
|
241
|
-
return planningSystemPrompt(this.active.dir);
|
|
313
|
+
return planningSystemPrompt(this.active.dir, mode);
|
|
242
314
|
case "implement":
|
|
243
|
-
return implementationSystemPrompt(this.active.dir);
|
|
315
|
+
return implementationSystemPrompt(this.active.dir, this.cwd);
|
|
244
316
|
case "review":
|
|
245
|
-
return reviewTaskSystemPrompt(this.active.dir);
|
|
317
|
+
return reviewTaskSystemPrompt(this.active.dir, this.cwd);
|
|
318
|
+
case "quick":
|
|
319
|
+
return "Work on the user's request directly. There are no phases, planning, or reviews.";
|
|
246
320
|
default:
|
|
247
321
|
return "";
|
|
248
322
|
}
|
|
@@ -265,7 +339,11 @@ export class Orchestrator {
|
|
|
265
339
|
description: string,
|
|
266
340
|
fromTaskDir?: string,
|
|
267
341
|
skipBrainstorm?: boolean,
|
|
342
|
+
mode?: TaskMode,
|
|
268
343
|
): Promise<void> {
|
|
344
|
+
const log = getLogger();
|
|
345
|
+
log.info({ s: "task", type, description, fromTaskDir: fromTaskDir ?? null, skipBrainstorm: skipBrainstorm ?? false, mode: mode ?? null }, "startTask");
|
|
346
|
+
const hadActive = !!this.active;
|
|
269
347
|
if (this.active) {
|
|
270
348
|
ctx.ui.notify(
|
|
271
349
|
`Pausing previous task "${this.active.description}" (phase: ${this.active.state.phase})…`,
|
|
@@ -277,6 +355,15 @@ export class Orchestrator {
|
|
|
277
355
|
await this.cleanupActive();
|
|
278
356
|
}
|
|
279
357
|
|
|
358
|
+
if (hadActive) {
|
|
359
|
+
// Route new-task compaction through the controller as a "done" target.
|
|
360
|
+
this.lastCtx = ctx;
|
|
361
|
+
await this.transitionController.requestTransition({
|
|
362
|
+
kind: "done",
|
|
363
|
+
summary: `Starting new ${type} task. Previous conversation discarded.`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
280
367
|
try {
|
|
281
368
|
this.config = loadConfig(this.cwd);
|
|
282
369
|
} catch (err: any) {
|
|
@@ -284,16 +371,28 @@ export class Orchestrator {
|
|
|
284
371
|
return;
|
|
285
372
|
}
|
|
286
373
|
|
|
374
|
+
setLogLevel(this.config.general.logLevel);
|
|
287
375
|
ensureGitignore(this.cwd);
|
|
288
376
|
|
|
289
|
-
const dir = createTask(this.cwd, type, description);
|
|
377
|
+
const dir = createTask(this.cwd, type, description, mode);
|
|
290
378
|
const state = loadTask(dir);
|
|
291
379
|
|
|
292
380
|
if (fromTaskDir) {
|
|
293
381
|
const srcUr = join(fromTaskDir, "USER_REQUEST.md");
|
|
294
382
|
const srcRes = join(fromTaskDir, "RESEARCH.md");
|
|
295
383
|
const srcArtifacts = join(fromTaskDir, "artifacts");
|
|
296
|
-
if (existsSync(srcUr))
|
|
384
|
+
if (existsSync(srcUr)) {
|
|
385
|
+
const originalUr = readFileSync(srcUr, "utf-8");
|
|
386
|
+
const implNote =
|
|
387
|
+
"# IMPLEMENTATION TASK\n\n" +
|
|
388
|
+
"This is now an **implement** task — the previous brainstorm/debug/review task is over.\n" +
|
|
389
|
+
"The user request, research, and artifacts below are carried over as context for implementation.\n" +
|
|
390
|
+
"Your job is to plan and implement actual code changes based on this research.\n" +
|
|
391
|
+
"Any prior instructions in the text below saying \"brainstorm only\", \"review only\",\n" +
|
|
392
|
+
"\"do not implement\", \"no code changes\", or similar DO NOT APPLY — they were for the previous task.\n\n" +
|
|
393
|
+
"---\n\n";
|
|
394
|
+
writeFileSync(join(dir, "USER_REQUEST.md"), implNote + originalUr, "utf-8");
|
|
395
|
+
}
|
|
297
396
|
if (existsSync(srcRes)) copyFileSync(srcRes, join(dir, "RESEARCH.md"));
|
|
298
397
|
if (existsSync(srcArtifacts)) {
|
|
299
398
|
const destArtifacts = join(dir, "artifacts");
|
|
@@ -305,19 +404,21 @@ export class Orchestrator {
|
|
|
305
404
|
state.from = relative(join(this.cwd, ".pp", "state"), fromTaskDir);
|
|
306
405
|
if (skipBrainstorm && type === "implement") {
|
|
307
406
|
state.phase = "plan";
|
|
308
|
-
state.
|
|
407
|
+
state.initialPhase = "plan";
|
|
408
|
+
state.activePlannerPreset = this.config.agents.subagents.presetGroups.planners.default;
|
|
409
|
+
state.step = this.getPlanStartState(dir, state.activePlannerPreset).step;
|
|
309
410
|
}
|
|
310
411
|
saveTask(dir, state);
|
|
311
412
|
}
|
|
312
413
|
|
|
313
414
|
let release: (() => Promise<void>) | null = null;
|
|
314
415
|
try {
|
|
315
|
-
release = await lockTask(dir, this.config.
|
|
416
|
+
release = await lockTask(dir, this.config.performance.internals);
|
|
316
417
|
} catch (err: any) {
|
|
317
418
|
try {
|
|
318
419
|
rmSync(dir, { recursive: true, force: true });
|
|
319
420
|
} catch {
|
|
320
|
-
|
|
421
|
+
log.warn({ s: "task", dir }, "failed to clean up orphaned task dir");
|
|
321
422
|
}
|
|
322
423
|
ctx.ui.notify(`Failed to lock task: ${err.message}`, "error");
|
|
323
424
|
return;
|
|
@@ -337,7 +438,10 @@ export class Orchestrator {
|
|
|
337
438
|
description: state.description,
|
|
338
439
|
};
|
|
339
440
|
|
|
340
|
-
|
|
441
|
+
addTaskDestination(dir);
|
|
442
|
+
log.info({ s: "task", dir, taskId: this.active.taskId, phase: state.phase, step: state.step }, "task activated");
|
|
443
|
+
|
|
444
|
+
const modelConfig = this.config.agents.orchestrators[
|
|
341
445
|
type === "debug" ? "debug"
|
|
342
446
|
: type === "brainstorm" ? "brainstorm"
|
|
343
447
|
: type === "review" ? "review"
|
|
@@ -357,30 +461,52 @@ export class Orchestrator {
|
|
|
357
461
|
|
|
358
462
|
this.phaseStartTime = Date.now();
|
|
359
463
|
const isGenericDescription = ["implement", "debug", "brainstorm", "review"].includes(this.active.description);
|
|
464
|
+
const isGenericQuickDescription = this.active.description === "quick";
|
|
360
465
|
const hasInheritedTaskContext = Boolean(fromTaskDir && type === "implement");
|
|
361
466
|
const isWaitingForPlanners = this.active.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
362
|
-
if (isGenericDescription && !hasInheritedTaskContext) {
|
|
467
|
+
if ((isGenericDescription || isGenericQuickDescription) && !hasInheritedTaskContext) {
|
|
363
468
|
ctx.ui.notify("Task created. Describe what you'd like to do.", "info");
|
|
364
469
|
} else if (isWaitingForPlanners) {
|
|
365
470
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
366
471
|
} else {
|
|
367
|
-
|
|
472
|
+
const desc = this.active.description;
|
|
473
|
+
const descSuffix = !isGenericDescription ? `\n\nTask: ${desc}` : "";
|
|
474
|
+
this.safeSendUserMessage(`[PI-PI] Entered ${this.active.state.phase} phase. Begin working.${descSuffix}`);
|
|
368
475
|
}
|
|
369
476
|
|
|
370
477
|
if (this.active.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
371
|
-
|
|
478
|
+
const requestedPlannerPresetName = this.active.state.activePlannerPreset ?? this.config.agents.subagents.presetGroups.planners.default;
|
|
479
|
+
const plannerPresetExists = Object.prototype.hasOwnProperty.call(this.config.agents.subagents.presetGroups.planners.presets ?? {}, requestedPlannerPresetName);
|
|
480
|
+
const plannerPresetName = plannerPresetExists
|
|
481
|
+
? requestedPlannerPresetName
|
|
482
|
+
: (Object.keys(this.config.agents.subagents.presetGroups.planners.presets ?? {})[0] ?? requestedPlannerPresetName);
|
|
483
|
+
if (this.active.state.activePlannerPreset !== plannerPresetName) {
|
|
484
|
+
this.active.state.activePlannerPreset = plannerPresetName;
|
|
485
|
+
saveTask(this.active.dir, this.active.state);
|
|
486
|
+
}
|
|
487
|
+
if (!plannerPresetExists && plannerPresetName !== requestedPlannerPresetName) {
|
|
488
|
+
ctx.ui.notify(
|
|
489
|
+
`Planner preset "${requestedPlannerPresetName}" not found. Falling back to "${plannerPresetName}".`,
|
|
490
|
+
"warning",
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const plannerVariants = resolvePreset(this.config, "planners", plannerPresetName);
|
|
494
|
+
this.pendingSubagentSpawns = Object.values(plannerVariants).filter((v) => isEnabled(v)).length;
|
|
372
495
|
this.failedPlannerVariants = [];
|
|
373
|
-
|
|
374
|
-
this
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
496
|
+
handleSpawnResult(
|
|
497
|
+
this,
|
|
498
|
+
spawnPlanners(
|
|
499
|
+
this.pi,
|
|
500
|
+
this.cwd,
|
|
501
|
+
this.active.dir,
|
|
502
|
+
this.active.taskId,
|
|
503
|
+
this.config,
|
|
504
|
+
this.transitionController.phaseSend,
|
|
505
|
+
plannerVariants,
|
|
506
|
+
this.active?.state.repos ?? [],
|
|
507
|
+
),
|
|
508
|
+
{ kind: "planner", logScope: "planner", logMessage: "spawnPlanners failed", onSettled: (result) => { if (!result?.spawned) this.checkPlannerCompletion(); } },
|
|
509
|
+
);
|
|
384
510
|
}
|
|
385
511
|
}
|
|
386
512
|
|
|
@@ -403,23 +529,14 @@ export class Orchestrator {
|
|
|
403
529
|
this.pendingSubagentSpawns = 0;
|
|
404
530
|
this.errorRetryCount = 0;
|
|
405
531
|
this.commitReminderSent = false;
|
|
406
|
-
this.
|
|
407
|
-
this.nudgeTimestamps = [];
|
|
408
|
-
this.cooldownHits = [];
|
|
532
|
+
this.consecutiveNudges = 0;
|
|
409
533
|
this.nudgeHalted = false;
|
|
410
|
-
this.phaseCompactionPending = false;
|
|
411
|
-
this.phaseCompactionSummary = "";
|
|
412
534
|
this.phaseStartTime = 0;
|
|
413
535
|
this.userGatePending = false;
|
|
414
|
-
this.reviewTransitionToken = -1;
|
|
415
536
|
this.failedPlannerVariants = [];
|
|
416
537
|
this.failedReviewerVariants = [];
|
|
417
538
|
this.plannerFailureDialogPending = false;
|
|
418
539
|
this.reviewerFailureDialogPending = false;
|
|
419
|
-
if (this.awaitPollTimer) {
|
|
420
|
-
clearInterval(this.awaitPollTimer);
|
|
421
|
-
this.awaitPollTimer = null;
|
|
422
|
-
}
|
|
423
540
|
if (this.pendingRetryTimer) {
|
|
424
541
|
clearTimeout(this.pendingRetryTimer);
|
|
425
542
|
this.pendingRetryTimer = null;
|
|
@@ -432,80 +549,127 @@ export class Orchestrator {
|
|
|
432
549
|
|
|
433
550
|
async cleanupActive(): Promise<void> {
|
|
434
551
|
if (!this.active) return;
|
|
552
|
+
const dir = this.active.dir;
|
|
553
|
+
getLogger().info({ s: "task", dir }, "cleaning up active task");
|
|
554
|
+
removeTaskDestination();
|
|
435
555
|
this.resetTaskScopedState();
|
|
436
556
|
if (this.active.release) {
|
|
437
557
|
try {
|
|
438
558
|
await this.active.release();
|
|
439
559
|
} catch (err: any) {
|
|
440
|
-
|
|
560
|
+
getLogger().error({ s: "task", dir, err: err.message }, "failed to release lock");
|
|
441
561
|
}
|
|
442
562
|
}
|
|
443
563
|
this.active = null;
|
|
444
564
|
}
|
|
445
565
|
|
|
446
566
|
registerAgents(): void {
|
|
567
|
+
const log = getLogger();
|
|
447
568
|
const explore = createExploreAgent(this.config);
|
|
448
569
|
const librarian = createLibrarianAgent(this.config);
|
|
449
570
|
const taskAgent = createTaskAgent(this.config, "{{subtask}}", { userRequest: "", synthesizedPlan: "" });
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
571
|
+
const phase = this.active?.state.phase;
|
|
572
|
+
const repos = this.active?.state.repos ?? [];
|
|
573
|
+
log.debug({ s: "agents", phase, repoCount: repos.length }, "registering agent definitions");
|
|
574
|
+
const contextDirs = getContextDirs(this.cwd, repos, this.config.general.loadExtraRepoConfigs);
|
|
575
|
+
const repoContext = buildRepoContext(repos);
|
|
576
|
+
|
|
577
|
+
const appendContext = (agentType: string, prompt: string, modelInfo: { vendor: string; family: string; tier: string }): string => {
|
|
578
|
+
const contextFiles = loadAllContextFiles(contextDirs, agentType as any, "system", phase, modelInfo);
|
|
579
|
+
if (contextFiles.length === 0 && !repoContext) return prompt;
|
|
580
|
+
const parts = [prompt];
|
|
581
|
+
if (repoContext) parts.push(repoContext.trimEnd());
|
|
582
|
+
if (contextFiles.length === 0) return parts.join("\n\n");
|
|
454
583
|
const contextBlock = contextFiles.map((f) => f.content).join("\n\n");
|
|
455
|
-
|
|
584
|
+
parts.push("# Project Context\n\n" + contextBlock);
|
|
585
|
+
return parts.join("\n\n");
|
|
456
586
|
};
|
|
457
587
|
|
|
458
588
|
registerAgentDefinitions(this.pi, [
|
|
459
|
-
{
|
|
460
|
-
|
|
461
|
-
|
|
589
|
+
{
|
|
590
|
+
type: "explore",
|
|
591
|
+
variant: null,
|
|
592
|
+
...explore,
|
|
593
|
+
prompt: appendContext("explore", explore.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.explore.model))),
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
type: "librarian",
|
|
597
|
+
variant: null,
|
|
598
|
+
...librarian,
|
|
599
|
+
prompt: appendContext("librarian", librarian.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.librarian.model))),
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
type: "task",
|
|
603
|
+
variant: null,
|
|
604
|
+
...taskAgent,
|
|
605
|
+
prompt: appendContext("task", taskAgent.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.task.model))),
|
|
606
|
+
},
|
|
462
607
|
]);
|
|
463
608
|
}
|
|
464
609
|
|
|
465
610
|
injectContextAndArtifacts(taskDir: string, phase: Phase): void {
|
|
466
|
-
const
|
|
611
|
+
const log = getLogger();
|
|
612
|
+
log.debug({ s: "context", taskDir, phase }, "injecting context and artifacts");
|
|
613
|
+
const modelSpec =
|
|
614
|
+
phase === "debug" && this.active?.type === "debug"
|
|
615
|
+
? this.config.agents.orchestrators.debug.model
|
|
616
|
+
: phase === "brainstorm" && this.active?.type === "brainstorm"
|
|
617
|
+
? this.config.agents.orchestrators.brainstorm.model
|
|
618
|
+
: phase === "review" && this.active?.type === "review"
|
|
619
|
+
? this.config.agents.orchestrators.review.model
|
|
620
|
+
: phase === "plan"
|
|
621
|
+
? this.config.agents.orchestrators.plan.model
|
|
622
|
+
: this.config.agents.orchestrators.implement.model;
|
|
623
|
+
const activeModelSpec = this.lastCtx?.model
|
|
624
|
+
? `${this.lastCtx.model.provider}/${this.lastCtx.model.id}`
|
|
625
|
+
: modelSpec;
|
|
626
|
+
const repos = this.active?.state.repos ?? [];
|
|
627
|
+
const contextDirs = getContextDirs(this.cwd, repos, this.config.general.loadExtraRepoConfigs);
|
|
628
|
+
const contextFiles = loadAllContextFiles(
|
|
629
|
+
contextDirs,
|
|
630
|
+
"main",
|
|
631
|
+
"context",
|
|
632
|
+
phase,
|
|
633
|
+
getModelInfo(activeModelSpec),
|
|
634
|
+
);
|
|
467
635
|
for (const cf of contextFiles) {
|
|
468
|
-
this.
|
|
636
|
+
this.transitionController.sendCustom(
|
|
469
637
|
{ customType: "pp-context", content: cf.content, display: false },
|
|
470
|
-
|
|
638
|
+
"context",
|
|
471
639
|
);
|
|
472
640
|
}
|
|
473
641
|
const artifacts = getPhaseArtifacts(taskDir, phase);
|
|
474
642
|
for (const artifact of artifacts) {
|
|
475
|
-
this.
|
|
643
|
+
this.transitionController.sendCustom(
|
|
476
644
|
{ customType: "pp-artifact", content: `=== ${artifact.name} ===\n${artifact.content}`, display: false },
|
|
477
|
-
|
|
645
|
+
"context",
|
|
478
646
|
);
|
|
479
647
|
}
|
|
480
648
|
}
|
|
481
649
|
|
|
482
|
-
compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase, onReady?: () => void): void {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
},
|
|
504
|
-
onError: (err) => {
|
|
505
|
-
console.error(`[pi-pi] Phase compaction failed: ${err.message}`);
|
|
506
|
-
this.phaseCompactionPending = false;
|
|
507
|
-
finalize();
|
|
650
|
+
compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase, onReady?: () => void, summary?: string): void {
|
|
651
|
+
getLogger().info({ s: "phase", taskDir, phase }, "compact and transition");
|
|
652
|
+
// Ensure the controller's host can reach this live ctx for compact/isIdle.
|
|
653
|
+
this.lastCtx = ctx;
|
|
654
|
+
// Notify-only case: entering plan and immediately awaiting planners. No
|
|
655
|
+
// "Begin working" instruction is sent — the agent waits for onSubagentsDone.
|
|
656
|
+
const notifyOnly = this.active?.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
657
|
+
void this.transitionController.requestTransition({
|
|
658
|
+
kind: "phase",
|
|
659
|
+
summary: summary || "Phase transition — previous phase completed.",
|
|
660
|
+
onResume: async () => {
|
|
661
|
+
this.phaseStartTime = Date.now();
|
|
662
|
+
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
663
|
+
const modelConfig = phase === "plan" ? this.config.agents.orchestrators.plan : this.config.agents.orchestrators.implement;
|
|
664
|
+
await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|
|
665
|
+
}
|
|
666
|
+
this.injectContextAndArtifacts(taskDir, phase);
|
|
667
|
+
onReady?.();
|
|
668
|
+
if (notifyOnly) {
|
|
669
|
+
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
670
|
+
}
|
|
508
671
|
},
|
|
672
|
+
instruction: notifyOnly ? undefined : `[PI-PI] Entered ${phase} phase. Begin working.`,
|
|
509
673
|
});
|
|
510
674
|
}
|
|
511
675
|
|
|
@@ -521,23 +685,6 @@ export class Orchestrator {
|
|
|
521
685
|
}
|
|
522
686
|
}
|
|
523
687
|
|
|
524
|
-
export function deepReviewConfig(config: PiPiConfig): PiPiConfig {
|
|
525
|
-
const THINKING_UPGRADE: Record<string, string> = { low: "medium", medium: "high", high: "xhigh" };
|
|
526
|
-
const upgrade = (reviewers: Record<string, VariantConfig>) => {
|
|
527
|
-
const upgraded: Record<string, VariantConfig> = {};
|
|
528
|
-
for (const [name, variant] of Object.entries(reviewers)) {
|
|
529
|
-
upgraded[name] = { ...variant, thinking: THINKING_UPGRADE[variant.thinking] ?? "high" };
|
|
530
|
-
}
|
|
531
|
-
return upgraded;
|
|
532
|
-
};
|
|
533
|
-
return {
|
|
534
|
-
...config,
|
|
535
|
-
codeReviewers: upgrade(config.codeReviewers),
|
|
536
|
-
planReviewers: upgrade(config.planReviewers),
|
|
537
|
-
brainstormReviewers: upgrade(config.brainstormReviewers),
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
|
|
541
688
|
export function ensureGitignore(cwd: string): void {
|
|
542
689
|
const ppDir = join(cwd, ".pp");
|
|
543
690
|
if (!existsSync(ppDir)) {
|
|
@@ -545,7 +692,7 @@ export function ensureGitignore(cwd: string): void {
|
|
|
545
692
|
}
|
|
546
693
|
|
|
547
694
|
const gitignorePath = join(ppDir, ".gitignore");
|
|
548
|
-
const requiredEntries = ["state/", "config.json"];
|
|
695
|
+
const requiredEntries = ["state/", "config.json", "logs/"];
|
|
549
696
|
|
|
550
697
|
if (!existsSync(gitignorePath)) {
|
|
551
698
|
writeFileSync(gitignorePath, requiredEntries.join("\n") + "\n", "utf-8");
|