@ilya-lesikov/pi-pi 0.5.0 → 0.7.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 +631 -121
- package/extensions/orchestrator/context.test.ts +177 -10
- package/extensions/orchestrator/context.ts +115 -14
- package/extensions/orchestrator/custom-footer.ts +7 -3
- package/extensions/orchestrator/doctor.test.ts +561 -0
- package/extensions/orchestrator/doctor.ts +702 -0
- package/extensions/orchestrator/event-handlers.test.ts +84 -22
- package/extensions/orchestrator/event-handlers.ts +1220 -343
- package/extensions/orchestrator/exa.test.ts +46 -0
- package/extensions/orchestrator/exa.ts +16 -10
- package/extensions/orchestrator/flant-infra.test.ts +511 -0
- package/extensions/orchestrator/flant-infra.ts +390 -49
- package/extensions/orchestrator/index.ts +13 -2
- package/extensions/orchestrator/integration.test.ts +3065 -118
- package/extensions/orchestrator/log.test.ts +219 -0
- package/extensions/orchestrator/log.ts +153 -0
- package/extensions/orchestrator/model-registry.test.ts +302 -0
- package/extensions/orchestrator/model-registry.ts +298 -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 +298 -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.test.ts +54 -0
- package/extensions/orchestrator/phases/review.ts +89 -48
- 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 +207 -0
- package/extensions/orchestrator/pp-menu.ts +2741 -373
- 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 +128 -44
- package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
- package/extensions/orchestrator/test-helpers.ts +229 -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 +563 -0
- package/extensions/orchestrator/usage-tracker.ts +96 -12
- 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, findLatestFamilyMatch } 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,63 @@ 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 allSpecs = allModels.map((m) => `${m.provider}/${m.id}`);
|
|
180
|
+
const familyMatch = findLatestFamilyMatch(spec, allSpecs);
|
|
181
|
+
if (familyMatch) {
|
|
182
|
+
const fmLower = familyMatch.toLowerCase();
|
|
183
|
+
resolved = allModels.find(
|
|
184
|
+
(m) => `${m.provider.toLowerCase()}/${m.id.toLowerCase()}` === fmLower,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!resolved) {
|
|
189
|
+
const pattern = spec.toLowerCase();
|
|
190
|
+
const matches = allModels.filter(
|
|
191
|
+
(m) => m.id.toLowerCase() === pattern || m.id.toLowerCase().includes(pattern),
|
|
192
|
+
);
|
|
193
|
+
if (matches.length === 1) resolved = matches[0];
|
|
194
|
+
}
|
|
195
|
+
if (resolved) break;
|
|
131
196
|
}
|
|
197
|
+
|
|
132
198
|
if (!resolved) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
(m) => m.id.toLowerCase() === pattern || m.id.toLowerCase().includes(pattern),
|
|
136
|
-
);
|
|
137
|
-
if (matches.length === 1) resolved = matches[0];
|
|
199
|
+
log.warn({ s: "model", requestedSpecs }, "model not found");
|
|
200
|
+
return false;
|
|
138
201
|
}
|
|
139
202
|
|
|
140
|
-
if (!resolved) return false;
|
|
141
|
-
|
|
142
203
|
const ok = await this.pi.setModel(resolved);
|
|
143
|
-
if (!ok)
|
|
204
|
+
if (!ok) {
|
|
205
|
+
log.warn({ s: "model", resolved: `${resolved.provider}/${resolved.id}` }, "setModel returned false");
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
144
208
|
|
|
145
|
-
const VALID_THINKING = new Set(["off", "low", "medium", "high"]);
|
|
146
|
-
const thinkingLevel = (VALID_THINKING.has(thinking) ? thinking : "high") as
|
|
209
|
+
const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
|
|
210
|
+
const thinkingLevel = (VALID_THINKING.has(thinking) ? thinking : "high") as
|
|
211
|
+
| "off"
|
|
212
|
+
| "minimal"
|
|
213
|
+
| "low"
|
|
214
|
+
| "medium"
|
|
215
|
+
| "high"
|
|
216
|
+
| "xhigh";
|
|
147
217
|
this.pi.setThinkingLevel(thinkingLevel);
|
|
218
|
+
log.debug({ s: "model", model: `${resolved.provider}/${resolved.id}`, thinking: thinkingLevel }, "model switched");
|
|
148
219
|
return true;
|
|
149
220
|
}
|
|
150
221
|
|
|
@@ -158,11 +229,13 @@ export class Orchestrator {
|
|
|
158
229
|
const phase = this.active.state.phase;
|
|
159
230
|
const step = this.active.state.step;
|
|
160
231
|
const reviewCycle = this.active.state.reviewCycle;
|
|
232
|
+
const effectiveMode = this.active.state.effectiveMode ?? this.active.state.mode;
|
|
233
|
+
const modeLabel = effectiveMode === "autonomous" ? " [autonomous]" : "";
|
|
161
234
|
|
|
162
|
-
if (type === "debug" || type === "brainstorm") {
|
|
235
|
+
if (type === "debug" || type === "brainstorm" || type === "quick") {
|
|
163
236
|
const elapsed = this.phaseStartTime > 0 ? this.formatElapsed(this.phaseStartTime) : "";
|
|
164
237
|
const suffix = elapsed ? ` (${elapsed})` : "";
|
|
165
|
-
ctx.ui.setStatus("pp-phase", `pp: ${type}${suffix}`);
|
|
238
|
+
ctx.ui.setStatus("pp-phase", `pp: ${type}${modeLabel}${suffix}`);
|
|
166
239
|
return;
|
|
167
240
|
}
|
|
168
241
|
|
|
@@ -183,7 +256,7 @@ export class Orchestrator {
|
|
|
183
256
|
else if (step === "user_gate") detail = "review";
|
|
184
257
|
|
|
185
258
|
if (reviewCycle) {
|
|
186
|
-
const kind = reviewCycle.kind === "plannotator" ? "plannotator" :
|
|
259
|
+
const kind = reviewCycle.kind === "plannotator" ? "plannotator" : "review";
|
|
187
260
|
detail = `${kind} #${reviewCycle.pass}`;
|
|
188
261
|
}
|
|
189
262
|
|
|
@@ -195,7 +268,7 @@ export class Orchestrator {
|
|
|
195
268
|
}
|
|
196
269
|
}
|
|
197
270
|
|
|
198
|
-
ctx.ui.setStatus("pp-phase", `pp: ${parts.join(" → ")}`);
|
|
271
|
+
ctx.ui.setStatus("pp-phase", `pp: ${parts.join(" → ")}${modeLabel}`);
|
|
199
272
|
}
|
|
200
273
|
|
|
201
274
|
private formatElapsed(startTime: number): string {
|
|
@@ -209,14 +282,22 @@ export class Orchestrator {
|
|
|
209
282
|
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
210
283
|
}
|
|
211
284
|
|
|
212
|
-
getPlanStartState(taskDir: string): { step: string; shouldSpawnPlanners: boolean } {
|
|
285
|
+
getPlanStartState(taskDir: string, plannerPresetName?: string): { step: string; shouldSpawnPlanners: boolean } {
|
|
213
286
|
const plansDir = join(taskDir, "plans");
|
|
214
|
-
const
|
|
287
|
+
const presetName = plannerPresetName ?? this.config.agents.subagents.presetGroups.planners.default;
|
|
288
|
+
const plannerVariants = resolvePreset(this.config, "planners", presetName);
|
|
289
|
+
const enabledPlannerVariants = Object.entries(plannerVariants)
|
|
290
|
+
.filter(([, v]) => isEnabled(v))
|
|
291
|
+
.map(([name]) => name);
|
|
215
292
|
const plannerOutputs = existsSync(plansDir)
|
|
216
293
|
? readdirSync(plansDir).filter((f) => f.endsWith(".md") && !f.includes("synthesized") && !f.includes("review_"))
|
|
217
294
|
: [];
|
|
295
|
+
const completedVariants = new Set(
|
|
296
|
+
plannerOutputs.map((f) => f.replace(/^\d+_/, "").replace(/\.md$/, "")),
|
|
297
|
+
);
|
|
298
|
+
const hasAllEnabledVariants = enabledPlannerVariants.every((name) => completedVariants.has(name));
|
|
218
299
|
|
|
219
|
-
if (
|
|
300
|
+
if (enabledPlannerVariants.length === 0 || hasAllEnabledVariants || getLatestSynthesizedPlan(taskDir)) {
|
|
220
301
|
return { step: "synthesize", shouldSpawnPlanners: false };
|
|
221
302
|
}
|
|
222
303
|
|
|
@@ -226,23 +307,26 @@ export class Orchestrator {
|
|
|
226
307
|
getPhasePrompt(_ctx: ExtensionContext): string {
|
|
227
308
|
if (!this.active) return "";
|
|
228
309
|
|
|
310
|
+
const mode: TaskMode = getEffectivePhaseMode(this.active.state);
|
|
311
|
+
|
|
229
312
|
if (this.active.state.reviewCycle?.step === "apply_feedback") {
|
|
230
313
|
const pass = this.active.state.reviewCycle.pass;
|
|
231
|
-
|
|
232
|
-
return reviewCycleSystemPrompt(this.active.dir, pass, manualReview, this.active.state.phase);
|
|
314
|
+
return reviewCycleSystemPrompt(this.active.dir, pass, this.active.state.phase, mode);
|
|
233
315
|
}
|
|
234
316
|
|
|
235
317
|
switch (this.active.state.phase) {
|
|
236
318
|
case "brainstorm":
|
|
237
|
-
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir);
|
|
319
|
+
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir, this.cwd);
|
|
238
320
|
case "debug":
|
|
239
|
-
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir);
|
|
321
|
+
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir, this.cwd);
|
|
240
322
|
case "plan":
|
|
241
|
-
return planningSystemPrompt(this.active.dir);
|
|
323
|
+
return planningSystemPrompt(this.active.dir, mode);
|
|
242
324
|
case "implement":
|
|
243
|
-
return implementationSystemPrompt(this.active.dir);
|
|
325
|
+
return implementationSystemPrompt(this.active.dir, this.cwd);
|
|
244
326
|
case "review":
|
|
245
|
-
return reviewTaskSystemPrompt(this.active.dir);
|
|
327
|
+
return reviewTaskSystemPrompt(this.active.dir, this.cwd);
|
|
328
|
+
case "quick":
|
|
329
|
+
return "Work on the user's request directly. There are no phases, planning, or reviews.";
|
|
246
330
|
default:
|
|
247
331
|
return "";
|
|
248
332
|
}
|
|
@@ -265,7 +349,11 @@ export class Orchestrator {
|
|
|
265
349
|
description: string,
|
|
266
350
|
fromTaskDir?: string,
|
|
267
351
|
skipBrainstorm?: boolean,
|
|
352
|
+
mode?: TaskMode,
|
|
268
353
|
): Promise<void> {
|
|
354
|
+
const log = getLogger();
|
|
355
|
+
log.info({ s: "task", type, description, fromTaskDir: fromTaskDir ?? null, skipBrainstorm: skipBrainstorm ?? false, mode: mode ?? null }, "startTask");
|
|
356
|
+
const hadActive = !!this.active;
|
|
269
357
|
if (this.active) {
|
|
270
358
|
ctx.ui.notify(
|
|
271
359
|
`Pausing previous task "${this.active.description}" (phase: ${this.active.state.phase})…`,
|
|
@@ -277,6 +365,15 @@ export class Orchestrator {
|
|
|
277
365
|
await this.cleanupActive();
|
|
278
366
|
}
|
|
279
367
|
|
|
368
|
+
if (hadActive) {
|
|
369
|
+
// Route new-task compaction through the controller as a "done" target.
|
|
370
|
+
this.lastCtx = ctx;
|
|
371
|
+
await this.transitionController.requestTransition({
|
|
372
|
+
kind: "done",
|
|
373
|
+
summary: `Starting new ${type} task. Previous conversation discarded.`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
280
377
|
try {
|
|
281
378
|
this.config = loadConfig(this.cwd);
|
|
282
379
|
} catch (err: any) {
|
|
@@ -284,16 +381,28 @@ export class Orchestrator {
|
|
|
284
381
|
return;
|
|
285
382
|
}
|
|
286
383
|
|
|
384
|
+
setLogLevel(this.config.general.logLevel);
|
|
287
385
|
ensureGitignore(this.cwd);
|
|
288
386
|
|
|
289
|
-
const dir = createTask(this.cwd, type, description);
|
|
387
|
+
const dir = createTask(this.cwd, type, description, mode);
|
|
290
388
|
const state = loadTask(dir);
|
|
291
389
|
|
|
292
390
|
if (fromTaskDir) {
|
|
293
391
|
const srcUr = join(fromTaskDir, "USER_REQUEST.md");
|
|
294
392
|
const srcRes = join(fromTaskDir, "RESEARCH.md");
|
|
295
393
|
const srcArtifacts = join(fromTaskDir, "artifacts");
|
|
296
|
-
if (existsSync(srcUr))
|
|
394
|
+
if (existsSync(srcUr)) {
|
|
395
|
+
const originalUr = readFileSync(srcUr, "utf-8");
|
|
396
|
+
const implNote =
|
|
397
|
+
"# IMPLEMENTATION TASK\n\n" +
|
|
398
|
+
"This is now an **implement** task — the previous brainstorm/debug/review task is over.\n" +
|
|
399
|
+
"The user request, research, and artifacts below are carried over as context for implementation.\n" +
|
|
400
|
+
"Your job is to plan and implement actual code changes based on this research.\n" +
|
|
401
|
+
"Any prior instructions in the text below saying \"brainstorm only\", \"review only\",\n" +
|
|
402
|
+
"\"do not implement\", \"no code changes\", or similar DO NOT APPLY — they were for the previous task.\n\n" +
|
|
403
|
+
"---\n\n";
|
|
404
|
+
writeFileSync(join(dir, "USER_REQUEST.md"), implNote + originalUr, "utf-8");
|
|
405
|
+
}
|
|
297
406
|
if (existsSync(srcRes)) copyFileSync(srcRes, join(dir, "RESEARCH.md"));
|
|
298
407
|
if (existsSync(srcArtifacts)) {
|
|
299
408
|
const destArtifacts = join(dir, "artifacts");
|
|
@@ -305,19 +414,21 @@ export class Orchestrator {
|
|
|
305
414
|
state.from = relative(join(this.cwd, ".pp", "state"), fromTaskDir);
|
|
306
415
|
if (skipBrainstorm && type === "implement") {
|
|
307
416
|
state.phase = "plan";
|
|
308
|
-
state.
|
|
417
|
+
state.initialPhase = "plan";
|
|
418
|
+
state.activePlannerPreset = this.config.agents.subagents.presetGroups.planners.default;
|
|
419
|
+
state.step = this.getPlanStartState(dir, state.activePlannerPreset).step;
|
|
309
420
|
}
|
|
310
421
|
saveTask(dir, state);
|
|
311
422
|
}
|
|
312
423
|
|
|
313
424
|
let release: (() => Promise<void>) | null = null;
|
|
314
425
|
try {
|
|
315
|
-
release = await lockTask(dir, this.config.
|
|
426
|
+
release = await lockTask(dir, this.config.performance.internals);
|
|
316
427
|
} catch (err: any) {
|
|
317
428
|
try {
|
|
318
429
|
rmSync(dir, { recursive: true, force: true });
|
|
319
430
|
} catch {
|
|
320
|
-
|
|
431
|
+
log.warn({ s: "task", dir }, "failed to clean up orphaned task dir");
|
|
321
432
|
}
|
|
322
433
|
ctx.ui.notify(`Failed to lock task: ${err.message}`, "error");
|
|
323
434
|
return;
|
|
@@ -337,10 +448,14 @@ export class Orchestrator {
|
|
|
337
448
|
description: state.description,
|
|
338
449
|
};
|
|
339
450
|
|
|
340
|
-
|
|
451
|
+
addTaskDestination(dir);
|
|
452
|
+
log.info({ s: "task", dir, taskId: this.active.taskId, phase: state.phase, step: state.step }, "task activated");
|
|
453
|
+
|
|
454
|
+
const modelConfig = this.config.agents.orchestrators[
|
|
341
455
|
type === "debug" ? "debug"
|
|
342
456
|
: type === "brainstorm" ? "brainstorm"
|
|
343
457
|
: type === "review" ? "review"
|
|
458
|
+
: type === "quick" ? "quick"
|
|
344
459
|
: "implement"
|
|
345
460
|
];
|
|
346
461
|
const modelOk = await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|
|
@@ -357,30 +472,52 @@ export class Orchestrator {
|
|
|
357
472
|
|
|
358
473
|
this.phaseStartTime = Date.now();
|
|
359
474
|
const isGenericDescription = ["implement", "debug", "brainstorm", "review"].includes(this.active.description);
|
|
475
|
+
const isGenericQuickDescription = this.active.description === "quick";
|
|
360
476
|
const hasInheritedTaskContext = Boolean(fromTaskDir && type === "implement");
|
|
361
477
|
const isWaitingForPlanners = this.active.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
362
|
-
if (isGenericDescription && !hasInheritedTaskContext) {
|
|
478
|
+
if ((isGenericDescription || isGenericQuickDescription) && !hasInheritedTaskContext) {
|
|
363
479
|
ctx.ui.notify("Task created. Describe what you'd like to do.", "info");
|
|
364
480
|
} else if (isWaitingForPlanners) {
|
|
365
481
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
366
482
|
} else {
|
|
367
|
-
|
|
483
|
+
const desc = this.active.description;
|
|
484
|
+
const descSuffix = !isGenericDescription ? `\n\nTask: ${desc}` : "";
|
|
485
|
+
this.safeSendUserMessage(`[PI-PI] Entered ${this.active.state.phase} phase. Begin working.${descSuffix}`);
|
|
368
486
|
}
|
|
369
487
|
|
|
370
488
|
if (this.active.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
371
|
-
|
|
489
|
+
const requestedPlannerPresetName = this.active.state.activePlannerPreset ?? this.config.agents.subagents.presetGroups.planners.default;
|
|
490
|
+
const plannerPresetExists = Object.prototype.hasOwnProperty.call(this.config.agents.subagents.presetGroups.planners.presets ?? {}, requestedPlannerPresetName);
|
|
491
|
+
const plannerPresetName = plannerPresetExists
|
|
492
|
+
? requestedPlannerPresetName
|
|
493
|
+
: (Object.keys(this.config.agents.subagents.presetGroups.planners.presets ?? {})[0] ?? requestedPlannerPresetName);
|
|
494
|
+
if (this.active.state.activePlannerPreset !== plannerPresetName) {
|
|
495
|
+
this.active.state.activePlannerPreset = plannerPresetName;
|
|
496
|
+
saveTask(this.active.dir, this.active.state);
|
|
497
|
+
}
|
|
498
|
+
if (!plannerPresetExists && plannerPresetName !== requestedPlannerPresetName) {
|
|
499
|
+
ctx.ui.notify(
|
|
500
|
+
`Planner preset "${requestedPlannerPresetName}" not found. Falling back to "${plannerPresetName}".`,
|
|
501
|
+
"warning",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const plannerVariants = resolvePreset(this.config, "planners", plannerPresetName);
|
|
505
|
+
this.pendingSubagentSpawns = Object.values(plannerVariants).filter((v) => isEnabled(v)).length;
|
|
372
506
|
this.failedPlannerVariants = [];
|
|
373
|
-
|
|
374
|
-
this
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
507
|
+
handleSpawnResult(
|
|
508
|
+
this,
|
|
509
|
+
spawnPlanners(
|
|
510
|
+
this.pi,
|
|
511
|
+
this.cwd,
|
|
512
|
+
this.active.dir,
|
|
513
|
+
this.active.taskId,
|
|
514
|
+
this.config,
|
|
515
|
+
this.transitionController.phaseSend,
|
|
516
|
+
plannerVariants,
|
|
517
|
+
this.active?.state.repos ?? [],
|
|
518
|
+
),
|
|
519
|
+
{ kind: "planner", logScope: "planner", logMessage: "spawnPlanners failed", onSettled: (result) => { if (!result?.spawned) this.checkPlannerCompletion(); } },
|
|
520
|
+
);
|
|
384
521
|
}
|
|
385
522
|
}
|
|
386
523
|
|
|
@@ -403,23 +540,14 @@ export class Orchestrator {
|
|
|
403
540
|
this.pendingSubagentSpawns = 0;
|
|
404
541
|
this.errorRetryCount = 0;
|
|
405
542
|
this.commitReminderSent = false;
|
|
406
|
-
this.
|
|
407
|
-
this.nudgeTimestamps = [];
|
|
408
|
-
this.cooldownHits = [];
|
|
543
|
+
this.consecutiveNudges = 0;
|
|
409
544
|
this.nudgeHalted = false;
|
|
410
|
-
this.phaseCompactionPending = false;
|
|
411
|
-
this.phaseCompactionSummary = "";
|
|
412
545
|
this.phaseStartTime = 0;
|
|
413
546
|
this.userGatePending = false;
|
|
414
|
-
this.reviewTransitionToken = -1;
|
|
415
547
|
this.failedPlannerVariants = [];
|
|
416
548
|
this.failedReviewerVariants = [];
|
|
417
549
|
this.plannerFailureDialogPending = false;
|
|
418
550
|
this.reviewerFailureDialogPending = false;
|
|
419
|
-
if (this.awaitPollTimer) {
|
|
420
|
-
clearInterval(this.awaitPollTimer);
|
|
421
|
-
this.awaitPollTimer = null;
|
|
422
|
-
}
|
|
423
551
|
if (this.pendingRetryTimer) {
|
|
424
552
|
clearTimeout(this.pendingRetryTimer);
|
|
425
553
|
this.pendingRetryTimer = null;
|
|
@@ -432,80 +560,127 @@ export class Orchestrator {
|
|
|
432
560
|
|
|
433
561
|
async cleanupActive(): Promise<void> {
|
|
434
562
|
if (!this.active) return;
|
|
563
|
+
const dir = this.active.dir;
|
|
564
|
+
getLogger().info({ s: "task", dir }, "cleaning up active task");
|
|
565
|
+
removeTaskDestination();
|
|
435
566
|
this.resetTaskScopedState();
|
|
436
567
|
if (this.active.release) {
|
|
437
568
|
try {
|
|
438
569
|
await this.active.release();
|
|
439
570
|
} catch (err: any) {
|
|
440
|
-
|
|
571
|
+
getLogger().error({ s: "task", dir, err: err.message }, "failed to release lock");
|
|
441
572
|
}
|
|
442
573
|
}
|
|
443
574
|
this.active = null;
|
|
444
575
|
}
|
|
445
576
|
|
|
446
577
|
registerAgents(): void {
|
|
578
|
+
const log = getLogger();
|
|
447
579
|
const explore = createExploreAgent(this.config);
|
|
448
580
|
const librarian = createLibrarianAgent(this.config);
|
|
449
581
|
const taskAgent = createTaskAgent(this.config, "{{subtask}}", { userRequest: "", synthesizedPlan: "" });
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
582
|
+
const phase = this.active?.state.phase;
|
|
583
|
+
const repos = this.active?.state.repos ?? [];
|
|
584
|
+
log.debug({ s: "agents", phase, repoCount: repos.length }, "registering agent definitions");
|
|
585
|
+
const contextDirs = getContextDirs(this.cwd, repos, this.config.general.loadExtraRepoConfigs);
|
|
586
|
+
const repoContext = buildRepoContext(repos);
|
|
587
|
+
|
|
588
|
+
const appendContext = (agentType: string, prompt: string, modelInfo: { vendor: string; family: string; tier: string }): string => {
|
|
589
|
+
const contextFiles = loadAllContextFiles(contextDirs, agentType as any, "system", phase, modelInfo);
|
|
590
|
+
if (contextFiles.length === 0 && !repoContext) return prompt;
|
|
591
|
+
const parts = [prompt];
|
|
592
|
+
if (repoContext) parts.push(repoContext.trimEnd());
|
|
593
|
+
if (contextFiles.length === 0) return parts.join("\n\n");
|
|
454
594
|
const contextBlock = contextFiles.map((f) => f.content).join("\n\n");
|
|
455
|
-
|
|
595
|
+
parts.push("# Project Context\n\n" + contextBlock);
|
|
596
|
+
return parts.join("\n\n");
|
|
456
597
|
};
|
|
457
598
|
|
|
458
599
|
registerAgentDefinitions(this.pi, [
|
|
459
|
-
{
|
|
460
|
-
|
|
461
|
-
|
|
600
|
+
{
|
|
601
|
+
type: "explore",
|
|
602
|
+
variant: null,
|
|
603
|
+
...explore,
|
|
604
|
+
prompt: appendContext("explore", explore.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.explore.model))),
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
type: "librarian",
|
|
608
|
+
variant: null,
|
|
609
|
+
...librarian,
|
|
610
|
+
prompt: appendContext("librarian", librarian.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.librarian.model))),
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
type: "task",
|
|
614
|
+
variant: null,
|
|
615
|
+
...taskAgent,
|
|
616
|
+
prompt: appendContext("task", taskAgent.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.task.model))),
|
|
617
|
+
},
|
|
462
618
|
]);
|
|
463
619
|
}
|
|
464
620
|
|
|
465
621
|
injectContextAndArtifacts(taskDir: string, phase: Phase): void {
|
|
466
|
-
const
|
|
622
|
+
const log = getLogger();
|
|
623
|
+
log.debug({ s: "context", taskDir, phase }, "injecting context and artifacts");
|
|
624
|
+
const modelSpec =
|
|
625
|
+
phase === "debug" && this.active?.type === "debug"
|
|
626
|
+
? this.config.agents.orchestrators.debug.model
|
|
627
|
+
: phase === "brainstorm" && this.active?.type === "brainstorm"
|
|
628
|
+
? this.config.agents.orchestrators.brainstorm.model
|
|
629
|
+
: phase === "review" && this.active?.type === "review"
|
|
630
|
+
? this.config.agents.orchestrators.review.model
|
|
631
|
+
: phase === "plan"
|
|
632
|
+
? this.config.agents.orchestrators.plan.model
|
|
633
|
+
: this.config.agents.orchestrators.implement.model;
|
|
634
|
+
const activeModelSpec = this.lastCtx?.model
|
|
635
|
+
? `${this.lastCtx.model.provider}/${this.lastCtx.model.id}`
|
|
636
|
+
: modelSpec;
|
|
637
|
+
const repos = this.active?.state.repos ?? [];
|
|
638
|
+
const contextDirs = getContextDirs(this.cwd, repos, this.config.general.loadExtraRepoConfigs);
|
|
639
|
+
const contextFiles = loadAllContextFiles(
|
|
640
|
+
contextDirs,
|
|
641
|
+
"main",
|
|
642
|
+
"context",
|
|
643
|
+
phase,
|
|
644
|
+
getModelInfo(activeModelSpec),
|
|
645
|
+
);
|
|
467
646
|
for (const cf of contextFiles) {
|
|
468
|
-
this.
|
|
647
|
+
this.transitionController.sendCustom(
|
|
469
648
|
{ customType: "pp-context", content: cf.content, display: false },
|
|
470
|
-
|
|
649
|
+
"context",
|
|
471
650
|
);
|
|
472
651
|
}
|
|
473
652
|
const artifacts = getPhaseArtifacts(taskDir, phase);
|
|
474
653
|
for (const artifact of artifacts) {
|
|
475
|
-
this.
|
|
654
|
+
this.transitionController.sendCustom(
|
|
476
655
|
{ customType: "pp-artifact", content: `=== ${artifact.name} ===\n${artifact.content}`, display: false },
|
|
477
|
-
|
|
656
|
+
"context",
|
|
478
657
|
);
|
|
479
658
|
}
|
|
480
659
|
}
|
|
481
660
|
|
|
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();
|
|
661
|
+
compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase, onReady?: () => void, summary?: string): void {
|
|
662
|
+
getLogger().info({ s: "phase", taskDir, phase }, "compact and transition");
|
|
663
|
+
// Ensure the controller's host can reach this live ctx for compact/isIdle.
|
|
664
|
+
this.lastCtx = ctx;
|
|
665
|
+
// Notify-only case: entering plan and immediately awaiting planners. No
|
|
666
|
+
// "Begin working" instruction is sent — the agent waits for onSubagentsDone.
|
|
667
|
+
const notifyOnly = this.active?.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
668
|
+
void this.transitionController.requestTransition({
|
|
669
|
+
kind: "phase",
|
|
670
|
+
summary: summary || "Phase transition — previous phase completed.",
|
|
671
|
+
onResume: async () => {
|
|
672
|
+
this.phaseStartTime = Date.now();
|
|
673
|
+
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
674
|
+
const modelConfig = phase === "plan" ? this.config.agents.orchestrators.plan : this.config.agents.orchestrators.implement;
|
|
675
|
+
await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|
|
676
|
+
}
|
|
677
|
+
this.injectContextAndArtifacts(taskDir, phase);
|
|
678
|
+
onReady?.();
|
|
679
|
+
if (notifyOnly) {
|
|
680
|
+
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
681
|
+
}
|
|
508
682
|
},
|
|
683
|
+
instruction: notifyOnly ? undefined : `[PI-PI] Entered ${phase} phase. Begin working.`,
|
|
509
684
|
});
|
|
510
685
|
}
|
|
511
686
|
|
|
@@ -521,23 +696,6 @@ export class Orchestrator {
|
|
|
521
696
|
}
|
|
522
697
|
}
|
|
523
698
|
|
|
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
699
|
export function ensureGitignore(cwd: string): void {
|
|
542
700
|
const ppDir = join(cwd, ".pp");
|
|
543
701
|
if (!existsSync(ppDir)) {
|
|
@@ -545,7 +703,7 @@ export function ensureGitignore(cwd: string): void {
|
|
|
545
703
|
}
|
|
546
704
|
|
|
547
705
|
const gitignorePath = join(ppDir, ".gitignore");
|
|
548
|
-
const requiredEntries = ["state/", "config.json"];
|
|
706
|
+
const requiredEntries = ["state/", "config.json", "logs/"];
|
|
549
707
|
|
|
550
708
|
if (!existsSync(gitignorePath)) {
|
|
551
709
|
writeFileSync(gitignorePath, requiredEntries.join("\n") + "\n", "utf-8");
|