shariq-pi-extensions 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { SubagentCoordinator } from "../subagents/src/coordinator.ts";
|
|
8
|
+
import type { SubagentSnapshot } from "../subagents/src/domain.ts";
|
|
9
|
+
import {
|
|
10
|
+
finalReviewPrompt,
|
|
11
|
+
initialOrchestratorPrompt,
|
|
12
|
+
orchestratorContinuationPrompt,
|
|
13
|
+
taskReviewPrompt,
|
|
14
|
+
workerFixPrompt,
|
|
15
|
+
workerPrompt,
|
|
16
|
+
} from "./prompts.ts";
|
|
17
|
+
import {
|
|
18
|
+
materializeTask,
|
|
19
|
+
parseOrchestratorDecision,
|
|
20
|
+
parseReviewDecision,
|
|
21
|
+
validateTaskGraph,
|
|
22
|
+
} from "./protocol.ts";
|
|
23
|
+
import { loadOrchestrationSettings } from "./settings.ts";
|
|
24
|
+
import { loadRuns, saveRun } from "./storage.ts";
|
|
25
|
+
import type {
|
|
26
|
+
OrchestrationRole,
|
|
27
|
+
OrchestrationRun,
|
|
28
|
+
OrchestrationTask,
|
|
29
|
+
ReviewDecision,
|
|
30
|
+
OrchestrationSettings,
|
|
31
|
+
} from "./types.ts";
|
|
32
|
+
|
|
33
|
+
const MAX_FIX_ROUNDS = 2;
|
|
34
|
+
const WRITER_ROLES = new Set<OrchestrationRole>(["frontend", "backend", "general"]);
|
|
35
|
+
|
|
36
|
+
export interface EngineStorage {
|
|
37
|
+
load(): OrchestrationRun[];
|
|
38
|
+
save(run: OrchestrationRun): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EngineHooks {
|
|
42
|
+
changed(): void;
|
|
43
|
+
notify(message: string, level?: "info" | "warning" | "error"): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class OrchestrationEngine {
|
|
47
|
+
private readonly runs = new Map<string, OrchestrationRun>();
|
|
48
|
+
private readonly pi: ExtensionAPI;
|
|
49
|
+
private readonly coordinator: SubagentCoordinator;
|
|
50
|
+
private readonly context: () => ExtensionContext;
|
|
51
|
+
private readonly hooks: EngineHooks;
|
|
52
|
+
private readonly settings: () => OrchestrationSettings;
|
|
53
|
+
private readonly storage: EngineStorage;
|
|
54
|
+
private reconciling = false;
|
|
55
|
+
private reconcileAgain = false;
|
|
56
|
+
private unsubscribe?: () => void;
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
pi: ExtensionAPI,
|
|
60
|
+
coordinator: SubagentCoordinator,
|
|
61
|
+
context: () => ExtensionContext,
|
|
62
|
+
hooks: EngineHooks,
|
|
63
|
+
settings: () => OrchestrationSettings = loadOrchestrationSettings,
|
|
64
|
+
storage: EngineStorage = { load: loadRuns, save: saveRun },
|
|
65
|
+
) {
|
|
66
|
+
this.pi = pi;
|
|
67
|
+
this.coordinator = coordinator;
|
|
68
|
+
this.context = context;
|
|
69
|
+
this.hooks = hooks;
|
|
70
|
+
this.settings = settings;
|
|
71
|
+
this.storage = storage;
|
|
72
|
+
for (const run of storage.load()) this.runs.set(run.id, run);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async start() {
|
|
76
|
+
this.unsubscribe = await this.coordinator.subscribe(() => void this.reconcile());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop() {
|
|
80
|
+
this.unsubscribe?.();
|
|
81
|
+
this.unsubscribe = undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
list() {
|
|
85
|
+
return [...this.runs.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get(id: string) {
|
|
89
|
+
return this.runs.get(id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private persist(run: OrchestrationRun) {
|
|
93
|
+
run.updatedAt = Date.now();
|
|
94
|
+
this.storage.save(run);
|
|
95
|
+
this.hooks.changed();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private role(role: OrchestrationRole) {
|
|
99
|
+
const value = this.settings().roles[role];
|
|
100
|
+
if (!value.model) throw new Error(`Configure the ${role} model in /orchestration settings first.`);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async projectInfo(cwd: string) {
|
|
105
|
+
const resolved = path.resolve(cwd);
|
|
106
|
+
const root = await this.pi.exec("git", ["-C", resolved, "rev-parse", "--show-toplevel"]);
|
|
107
|
+
if (root.code !== 0) {
|
|
108
|
+
return {
|
|
109
|
+
cwd: resolved,
|
|
110
|
+
projectKey: createHash("sha256").update(resolved).digest("hex").slice(0, 16),
|
|
111
|
+
git: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const gitRoot = root.stdout.trim();
|
|
115
|
+
const status = await this.pi.exec("git", ["-C", gitRoot, "status", "--porcelain", "--untracked-files=all"]);
|
|
116
|
+
if (status.code !== 0) throw new Error("Could not inspect the source repository.");
|
|
117
|
+
if (status.stdout.trim()) {
|
|
118
|
+
throw new Error("Writing orchestration requires a clean source checkout. Commit or stash existing changes first.");
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
cwd: gitRoot,
|
|
122
|
+
projectKey: createHash("sha256").update(gitRoot).digest("hex").slice(0, 16),
|
|
123
|
+
git: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async create(objective: string, cwd = this.context().cwd) {
|
|
128
|
+
const text = objective.trim();
|
|
129
|
+
if (!text) throw new Error("Orchestration objective cannot be empty.");
|
|
130
|
+
if ([...text].length > 20_000) throw new Error("Orchestration objective exceeds 20,000 characters.");
|
|
131
|
+
const project = await this.projectInfo(cwd);
|
|
132
|
+
const id = `orc_${randomUUID().slice(0, 12)}`;
|
|
133
|
+
const run: OrchestrationRun = {
|
|
134
|
+
id,
|
|
135
|
+
objective: text,
|
|
136
|
+
cwd: project.cwd,
|
|
137
|
+
projectKey: project.projectKey,
|
|
138
|
+
gitBacked: project.git,
|
|
139
|
+
status: "planning",
|
|
140
|
+
summary: "The dedicated orchestrator is preparing the plan.",
|
|
141
|
+
tasks: [],
|
|
142
|
+
createdAt: Date.now(),
|
|
143
|
+
updatedAt: Date.now(),
|
|
144
|
+
};
|
|
145
|
+
this.runs.set(id, run);
|
|
146
|
+
this.persist(run);
|
|
147
|
+
const orchestrator = this.role("orchestrator");
|
|
148
|
+
try {
|
|
149
|
+
const snapshot = await this.coordinator.spawn(this.context(), {
|
|
150
|
+
message: initialOrchestratorPrompt(text),
|
|
151
|
+
taskName: `orchestrator ${id}`,
|
|
152
|
+
cwd: project.cwd,
|
|
153
|
+
model: orchestrator.model,
|
|
154
|
+
thinking: orchestrator.thinking,
|
|
155
|
+
capability: "execute",
|
|
156
|
+
isolation: project.git ? "worktree" : "none",
|
|
157
|
+
agentType: "plan",
|
|
158
|
+
concurrencyGroup: `orchestrator:${id}`,
|
|
159
|
+
maxConcurrent: 1,
|
|
160
|
+
});
|
|
161
|
+
run.orchestratorAgentId = snapshot.id;
|
|
162
|
+
this.persist(run);
|
|
163
|
+
return run;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
run.status = "blocked";
|
|
166
|
+
run.error = error instanceof Error ? error.message : String(error);
|
|
167
|
+
this.persist(run);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async feedback(id: string, feedback: string) {
|
|
173
|
+
const run = this.required(id);
|
|
174
|
+
if (run.status !== "awaiting-approval" || !run.orchestratorAgentId) {
|
|
175
|
+
throw new Error("Plan feedback is available only while awaiting approval.");
|
|
176
|
+
}
|
|
177
|
+
run.status = "planning";
|
|
178
|
+
this.persist(run);
|
|
179
|
+
await this.coordinator.send(
|
|
180
|
+
run.orchestratorAgentId,
|
|
181
|
+
`Revise the plan using this user feedback, then return the same JSON decision contract.\n\nFeedback:\n${feedback}\n\nCurrent plan:\n${JSON.stringify(run.tasks, null, 2)}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async approve(id: string) {
|
|
186
|
+
const run = this.required(id);
|
|
187
|
+
if (run.status !== "awaiting-approval") throw new Error("This run is not awaiting plan approval.");
|
|
188
|
+
run.status = "running";
|
|
189
|
+
run.error = undefined;
|
|
190
|
+
this.persist(run);
|
|
191
|
+
await this.schedule(run);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async pause(id: string) {
|
|
195
|
+
const run = this.required(id);
|
|
196
|
+
run.status = "paused";
|
|
197
|
+
const active = this.agentIds(run);
|
|
198
|
+
if (active.length) {
|
|
199
|
+
await this.coordinator.cancel(active);
|
|
200
|
+
await Promise.all(active.map((id) => this.coordinator.release(id)));
|
|
201
|
+
}
|
|
202
|
+
this.persist(run);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async resume(id: string) {
|
|
206
|
+
const run = this.required(id);
|
|
207
|
+
if (!["paused", "interrupted", "blocked"].includes(run.status)) {
|
|
208
|
+
throw new Error("Only paused, interrupted, or blocked runs can resume.");
|
|
209
|
+
}
|
|
210
|
+
for (const task of run.tasks) {
|
|
211
|
+
if (["implementing", "reviewing", "integrating"].includes(task.status)) {
|
|
212
|
+
task.status = "pending";
|
|
213
|
+
task.reviewerAgentId = undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const orchestrator = run.orchestratorAgentId
|
|
217
|
+
? await this.coordinator.get(run.orchestratorAgentId)
|
|
218
|
+
: undefined;
|
|
219
|
+
if (run.orchestratorAgentId && !orchestrator) {
|
|
220
|
+
const role = this.role("orchestrator");
|
|
221
|
+
const resumed = await this.coordinator.spawn(this.context(), {
|
|
222
|
+
message: orchestratorContinuationPrompt(run, "Recover this orchestration after Pi restarted."),
|
|
223
|
+
taskName: `orchestrator ${run.id}`,
|
|
224
|
+
cwd: run.cwd,
|
|
225
|
+
model: role.model,
|
|
226
|
+
thinking: role.thinking,
|
|
227
|
+
capability: "execute",
|
|
228
|
+
isolation: run.gitBacked ? "worktree" : "none",
|
|
229
|
+
agentType: "plan",
|
|
230
|
+
resumeFrom: run.orchestratorAgentId,
|
|
231
|
+
concurrencyGroup: `orchestrator:${run.id}`,
|
|
232
|
+
maxConcurrent: 1,
|
|
233
|
+
});
|
|
234
|
+
run.orchestratorAgentId = resumed.id;
|
|
235
|
+
}
|
|
236
|
+
run.status = run.tasks.length ? "running" : "planning";
|
|
237
|
+
run.error = undefined;
|
|
238
|
+
this.persist(run);
|
|
239
|
+
if (run.status === "planning" && run.orchestratorAgentId) {
|
|
240
|
+
await this.coordinator.send(
|
|
241
|
+
run.orchestratorAgentId,
|
|
242
|
+
orchestratorContinuationPrompt(run, "Resume after interruption."),
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
await this.schedule(run);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async cancel(id: string) {
|
|
250
|
+
const run = this.required(id);
|
|
251
|
+
const active = this.agentIds(run);
|
|
252
|
+
if (active.length) {
|
|
253
|
+
await this.coordinator.cancel(active);
|
|
254
|
+
await Promise.all(active.map((agentId) => this.coordinator.release(agentId)));
|
|
255
|
+
}
|
|
256
|
+
run.status = "cancelled";
|
|
257
|
+
for (const task of run.tasks) {
|
|
258
|
+
if (!new Set(["completed", "cancelled"]).has(task.status)) task.status = "cancelled";
|
|
259
|
+
}
|
|
260
|
+
this.persist(run);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private required(id: string) {
|
|
264
|
+
const run = this.runs.get(id);
|
|
265
|
+
if (!run) throw new Error(`Unknown orchestration ${id}.`);
|
|
266
|
+
return run;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private agentIds(run: OrchestrationRun) {
|
|
270
|
+
return [
|
|
271
|
+
run.orchestratorAgentId,
|
|
272
|
+
run.finalReviewerAgentId,
|
|
273
|
+
...run.tasks.flatMap((task) => [task.workerAgentId, task.reviewerAgentId]),
|
|
274
|
+
].filter((id): id is string => !!id);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private async reconcile() {
|
|
278
|
+
if (this.reconciling) {
|
|
279
|
+
this.reconcileAgain = true;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.reconciling = true;
|
|
283
|
+
try {
|
|
284
|
+
do {
|
|
285
|
+
this.reconcileAgain = false;
|
|
286
|
+
for (const run of this.list()) await this.reconcileRun(run);
|
|
287
|
+
} while (this.reconcileAgain);
|
|
288
|
+
} finally {
|
|
289
|
+
this.reconciling = false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async snapshot(id?: string) {
|
|
294
|
+
return id ? this.coordinator.get(id) : undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private async reconcileRun(run: OrchestrationRun) {
|
|
298
|
+
if (["completed", "cancelled", "paused", "interrupted"].includes(run.status)) return;
|
|
299
|
+
const orchestrator = await this.snapshot(run.orchestratorAgentId);
|
|
300
|
+
if (
|
|
301
|
+
orchestrator &&
|
|
302
|
+
orchestrator.status !== "running" &&
|
|
303
|
+
(orchestrator.settledAt ?? 0) > (run.orchestratorHandledAt ?? 0)
|
|
304
|
+
) {
|
|
305
|
+
run.orchestratorHandledAt = orchestrator.settledAt;
|
|
306
|
+
await this.handleOrchestrator(run, orchestrator);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (run.status !== "running") return;
|
|
310
|
+
|
|
311
|
+
for (const task of run.tasks) {
|
|
312
|
+
if (task.status === "implementing") {
|
|
313
|
+
const worker = await this.snapshot(task.workerAgentId);
|
|
314
|
+
if (worker && worker.status !== "running" && (worker.settledAt ?? 0) > (task.lastWorkerSettledAt ?? 0)) {
|
|
315
|
+
task.lastWorkerSettledAt = worker.settledAt;
|
|
316
|
+
await this.handleWorker(run, task, worker);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
} else if (task.status === "reviewing") {
|
|
320
|
+
const reviewer = await this.snapshot(task.reviewerAgentId);
|
|
321
|
+
if (reviewer && reviewer.status !== "running" && (reviewer.settledAt ?? 0) > (task.lastReviewerSettledAt ?? 0)) {
|
|
322
|
+
task.lastReviewerSettledAt = reviewer.settledAt;
|
|
323
|
+
await this.handleReviewer(run, task, reviewer);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const finalReviewer = await this.snapshot(run.finalReviewerAgentId);
|
|
330
|
+
if (
|
|
331
|
+
finalReviewer &&
|
|
332
|
+
finalReviewer.status !== "running" &&
|
|
333
|
+
(finalReviewer.settledAt ?? 0) > (run.finalReviewerHandledAt ?? 0)
|
|
334
|
+
) {
|
|
335
|
+
run.finalReviewerHandledAt = finalReviewer.settledAt;
|
|
336
|
+
await this.handleFinalReview(run, finalReviewer);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
await this.schedule(run);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async handleOrchestrator(run: OrchestrationRun, snapshot: SubagentSnapshot) {
|
|
343
|
+
if (snapshot.status === "error") return this.block(run, snapshot.errorText ?? "Orchestrator failed.");
|
|
344
|
+
try {
|
|
345
|
+
const decision = parseOrchestratorDecision(snapshot.finalText);
|
|
346
|
+
run.summary = decision.summary;
|
|
347
|
+
if (decision.decision === "blocked") return this.block(run, decision.blocker ?? decision.summary);
|
|
348
|
+
const taskCountBefore = run.tasks.length;
|
|
349
|
+
if (run.status === "planning") {
|
|
350
|
+
run.tasks = (decision.tasks ?? []).map(materializeTask);
|
|
351
|
+
} else {
|
|
352
|
+
const existing = new Set(run.tasks.map((task) => task.id));
|
|
353
|
+
for (const task of decision.tasks ?? []) {
|
|
354
|
+
if (!existing.has(task.id)) {
|
|
355
|
+
run.tasks.push(materializeTask(task));
|
|
356
|
+
existing.add(task.id);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
validateTaskGraph(run.tasks);
|
|
361
|
+
if (decision.decision === "complete" && run.finalReviewPassed) {
|
|
362
|
+
await this.complete(run);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (run.status === "planning") {
|
|
366
|
+
if (!run.tasks.length) return this.block(run, "The orchestrator returned no executable tasks.");
|
|
367
|
+
run.status = "awaiting-approval";
|
|
368
|
+
this.persist(run);
|
|
369
|
+
this.hooks.notify(`Orchestration ${run.id} plan is ready. Open /orchestration to review it.`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
decision.decision === "continue" &&
|
|
374
|
+
run.finalReviewPassed === false &&
|
|
375
|
+
run.tasks.length === taskCountBefore
|
|
376
|
+
) {
|
|
377
|
+
return this.block(run, "Final review failed, but the orchestrator did not create corrective work.");
|
|
378
|
+
}
|
|
379
|
+
run.status = "running";
|
|
380
|
+
run.finalReviewerAgentId = undefined;
|
|
381
|
+
run.finalReviewerHandledAt = undefined;
|
|
382
|
+
this.persist(run);
|
|
383
|
+
await this.schedule(run);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
this.block(run, `Invalid orchestrator result: ${error instanceof Error ? error.message : String(error)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private async handleWorker(run: OrchestrationRun, task: OrchestrationTask, worker: SubagentSnapshot) {
|
|
390
|
+
if (worker.status === "error") {
|
|
391
|
+
task.error = worker.errorText ?? "Worker failed.";
|
|
392
|
+
if (task.fixRounds >= MAX_FIX_ROUNDS || !task.workerAgentId) {
|
|
393
|
+
task.status = "blocked";
|
|
394
|
+
return this.block(run, `Worker ${task.id} failed after ${MAX_FIX_ROUNDS} recovery rounds: ${task.error}`);
|
|
395
|
+
}
|
|
396
|
+
task.fixRounds++;
|
|
397
|
+
task.status = "implementing";
|
|
398
|
+
this.persist(run);
|
|
399
|
+
await this.coordinator.send(
|
|
400
|
+
task.workerAgentId,
|
|
401
|
+
`Your previous run failed: ${task.error}\n\nRecover in the same task worktree, inspect the current state, complete the original assignment, and rerun targeted validation. Do not commit or push.`,
|
|
402
|
+
);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (task.role === "explorer") {
|
|
406
|
+
task.status = "completed";
|
|
407
|
+
task.reviewSummary = worker.finalText.slice(0, 12_000);
|
|
408
|
+
await this.coordinator.release(worker.id);
|
|
409
|
+
this.persist(run);
|
|
410
|
+
await this.schedule(run);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
task.status = "reviewing";
|
|
414
|
+
task.reviewerAgentId = undefined;
|
|
415
|
+
this.persist(run);
|
|
416
|
+
await this.spawnTaskReviewer(run, task, worker);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async handleReviewer(run: OrchestrationRun, task: OrchestrationTask, reviewer: SubagentSnapshot) {
|
|
420
|
+
if (reviewer.status === "error") return this.block(run, reviewer.errorText ?? `Reviewer failed for ${task.id}.`);
|
|
421
|
+
let review: ReviewDecision;
|
|
422
|
+
try {
|
|
423
|
+
review = parseReviewDecision(reviewer.finalText);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return this.block(run, `Invalid review for ${task.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
426
|
+
}
|
|
427
|
+
task.reviewSummary = review.summary;
|
|
428
|
+
await this.coordinator.release(reviewer.id);
|
|
429
|
+
if (!review.passed) {
|
|
430
|
+
if (task.fixRounds >= MAX_FIX_ROUNDS) {
|
|
431
|
+
task.status = "blocked";
|
|
432
|
+
task.error = `Review still failed after ${MAX_FIX_ROUNDS} fix rounds: ${review.summary}`;
|
|
433
|
+
return this.block(run, task.error);
|
|
434
|
+
}
|
|
435
|
+
task.fixRounds++;
|
|
436
|
+
task.status = "implementing";
|
|
437
|
+
task.reviewerAgentId = undefined;
|
|
438
|
+
this.persist(run);
|
|
439
|
+
if (!task.workerAgentId) return this.block(run, `Missing original worker for ${task.id}.`);
|
|
440
|
+
await this.coordinator.send(task.workerAgentId, workerFixPrompt(task, review));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
await this.integrateTask(run, task);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private async integrationPath(run: OrchestrationRun) {
|
|
447
|
+
const orchestrator = await this.snapshot(run.orchestratorAgentId);
|
|
448
|
+
return orchestrator?.meta.worktree?.path;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private async integrateTask(run: OrchestrationRun, task: OrchestrationTask) {
|
|
452
|
+
task.status = "integrating";
|
|
453
|
+
this.persist(run);
|
|
454
|
+
try {
|
|
455
|
+
if (!task.workerAgentId) throw new Error("Missing worker agent id.");
|
|
456
|
+
const target = await this.integrationPath(run);
|
|
457
|
+
if (target) {
|
|
458
|
+
const result = await this.coordinator.apply(task.workerAgentId, target);
|
|
459
|
+
task.filesChanged = result.files;
|
|
460
|
+
if (result.changed) {
|
|
461
|
+
const add = await this.pi.exec("git", ["-C", target, "add", "-A", "--", "."]);
|
|
462
|
+
if (add.code !== 0) throw new Error(add.stderr || "Could not stage internal integration changes.");
|
|
463
|
+
const commit = await this.pi.exec("git", [
|
|
464
|
+
"-C", target,
|
|
465
|
+
"-c", "user.name=Pi Orchestration",
|
|
466
|
+
"-c", "user.email=pi-orchestration@localhost",
|
|
467
|
+
"commit", "-m", `Integrate orchestration task ${task.id}`,
|
|
468
|
+
]);
|
|
469
|
+
if (commit.code !== 0) throw new Error(commit.stderr || "Could not create internal integration commit.");
|
|
470
|
+
}
|
|
471
|
+
await this.coordinator.discard(task.workerAgentId);
|
|
472
|
+
}
|
|
473
|
+
task.status = "completed";
|
|
474
|
+
task.error = undefined;
|
|
475
|
+
this.persist(run);
|
|
476
|
+
await this.schedule(run);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
479
|
+
if (task.fixRounds < MAX_FIX_ROUNDS && task.workerAgentId) {
|
|
480
|
+
task.fixRounds++;
|
|
481
|
+
task.status = "implementing";
|
|
482
|
+
const orchestrator = await this.snapshot(run.orchestratorAgentId);
|
|
483
|
+
const branch = orchestrator?.meta.worktree?.branch;
|
|
484
|
+
this.persist(run);
|
|
485
|
+
await this.coordinator.send(
|
|
486
|
+
task.workerAgentId,
|
|
487
|
+
`Integration found a conflict: ${task.error}\n\nUpdate this same task worktree against the current orchestration integration${branch ? ` branch ${branch}` : " workspace"}, resolve only the task-related conflicts, preserve reviewed changes, rerun validation, and return an updated handoff. Do not commit or push.`,
|
|
488
|
+
);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
task.status = "blocked";
|
|
492
|
+
this.block(run, `Integration failed for ${task.id}: ${task.error}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private async schedule(run: OrchestrationRun) {
|
|
497
|
+
if (run.status !== "running") return;
|
|
498
|
+
const blockedDependency = run.tasks.find(
|
|
499
|
+
(task) => task.status === "pending" && task.dependencies.some((id) => run.tasks.find((item) => item.id === id)?.status === "blocked"),
|
|
500
|
+
);
|
|
501
|
+
if (blockedDependency) return this.block(run, `Task ${blockedDependency.id} depends on blocked work.`);
|
|
502
|
+
|
|
503
|
+
const active = run.tasks.filter(
|
|
504
|
+
(task) =>
|
|
505
|
+
task.status === "implementing" ||
|
|
506
|
+
task.status === "integrating" ||
|
|
507
|
+
(task.status === "reviewing" && !!task.reviewerAgentId),
|
|
508
|
+
).length;
|
|
509
|
+
const completedState = run.tasks
|
|
510
|
+
.filter((task) => task.status === "completed")
|
|
511
|
+
.map((task) => `${task.id}:${task.reviewSummary ?? "done"}`)
|
|
512
|
+
.sort()
|
|
513
|
+
.join("|");
|
|
514
|
+
if (
|
|
515
|
+
active === 0 &&
|
|
516
|
+
completedState &&
|
|
517
|
+
completedState !== run.lastCoordinatedTaskState
|
|
518
|
+
) {
|
|
519
|
+
run.lastCoordinatedTaskState = completedState;
|
|
520
|
+
this.persist(run);
|
|
521
|
+
await this.wakeOrchestrator(run, "A worker wave settled. Review the completed task evidence and coordinate the next step.");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
let slots = Math.max(0, 10 - active);
|
|
525
|
+
const gitBacked = !!(await this.integrationPath(run));
|
|
526
|
+
for (const task of run.tasks) {
|
|
527
|
+
if (slots <= 0) break;
|
|
528
|
+
if (task.status !== "reviewing" || task.reviewerAgentId || !task.workerAgentId) continue;
|
|
529
|
+
const worker = await this.snapshot(task.workerAgentId);
|
|
530
|
+
if (!worker) continue;
|
|
531
|
+
const started = await this.spawnTaskReviewer(run, task, worker);
|
|
532
|
+
if (started) slots--;
|
|
533
|
+
}
|
|
534
|
+
const activeWriter = this.list().some(
|
|
535
|
+
(candidate) =>
|
|
536
|
+
candidate.projectKey === run.projectKey &&
|
|
537
|
+
candidate.tasks.some(
|
|
538
|
+
(task) =>
|
|
539
|
+
WRITER_ROLES.has(task.role) &&
|
|
540
|
+
["implementing", "reviewing", "integrating"].includes(task.status),
|
|
541
|
+
),
|
|
542
|
+
);
|
|
543
|
+
for (const task of run.tasks) {
|
|
544
|
+
if (slots <= 0) break;
|
|
545
|
+
if (task.status !== "pending") continue;
|
|
546
|
+
if (!task.dependencies.every((id) => run.tasks.find((item) => item.id === id)?.status === "completed")) continue;
|
|
547
|
+
if (!gitBacked && WRITER_ROLES.has(task.role) && activeWriter) continue;
|
|
548
|
+
const started = await this.spawnTask(run, task, gitBacked);
|
|
549
|
+
if (!started) break;
|
|
550
|
+
slots--;
|
|
551
|
+
if (!gitBacked && WRITER_ROLES.has(task.role)) break;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const unfinished = run.tasks.some((task) => task.status !== "completed" && task.status !== "cancelled");
|
|
555
|
+
const nowActive = run.tasks.some((task) => ["implementing", "reviewing", "integrating"].includes(task.status));
|
|
556
|
+
if (!unfinished && !nowActive && !run.finalReviewerAgentId) {
|
|
557
|
+
await this.spawnFinalReview(run, gitBacked);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private async spawnTaskReviewer(
|
|
562
|
+
run: OrchestrationRun,
|
|
563
|
+
task: OrchestrationTask,
|
|
564
|
+
worker: SubagentSnapshot,
|
|
565
|
+
) {
|
|
566
|
+
const role = this.role("reviewer");
|
|
567
|
+
const worktree = worker.meta.worktree?.path;
|
|
568
|
+
const integration = await this.integrationPath(run);
|
|
569
|
+
if (integration && !worktree) {
|
|
570
|
+
this.block(run, `Writer ${task.id} has no isolated worktree.`);
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
const reviewer = await this.coordinator.spawn(this.context(), {
|
|
575
|
+
message: taskReviewPrompt(run, task),
|
|
576
|
+
taskName: `review ${task.id}`,
|
|
577
|
+
cwd: worktree ?? run.cwd,
|
|
578
|
+
model: role.model,
|
|
579
|
+
thinking: role.thinking,
|
|
580
|
+
capability: "all",
|
|
581
|
+
isolation: "none",
|
|
582
|
+
agentType: "general-purpose",
|
|
583
|
+
concurrencyGroup: `orchestration:${run.projectKey}`,
|
|
584
|
+
maxConcurrent: 10,
|
|
585
|
+
});
|
|
586
|
+
task.reviewerAgentId = reviewer.id;
|
|
587
|
+
this.persist(run);
|
|
588
|
+
return true;
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (/concurrent|capacity/i.test(error instanceof Error ? error.message : String(error))) return false;
|
|
591
|
+
this.block(run, error instanceof Error ? error.message : String(error));
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private async spawnTask(run: OrchestrationRun, task: OrchestrationTask, gitBacked: boolean) {
|
|
597
|
+
const role = this.role(task.role);
|
|
598
|
+
const base = (await this.integrationPath(run)) ?? run.cwd;
|
|
599
|
+
const writer = WRITER_ROLES.has(task.role);
|
|
600
|
+
try {
|
|
601
|
+
const snapshot = await this.coordinator.spawn(this.context(), {
|
|
602
|
+
message: workerPrompt(run, task),
|
|
603
|
+
taskName: `${task.role} ${task.id}`,
|
|
604
|
+
cwd: base,
|
|
605
|
+
model: role.model,
|
|
606
|
+
thinking: role.thinking,
|
|
607
|
+
capability: task.role === "explorer" ? "execute" : "all",
|
|
608
|
+
isolation: writer && gitBacked ? "worktree" : "none",
|
|
609
|
+
agentType: task.role === "explorer" ? "explore" : "general-purpose",
|
|
610
|
+
resumeFrom: task.workerAgentId,
|
|
611
|
+
concurrencyGroup: `orchestration:${run.projectKey}`,
|
|
612
|
+
maxConcurrent: 10,
|
|
613
|
+
});
|
|
614
|
+
task.workerAgentId = snapshot.id;
|
|
615
|
+
task.status = "implementing";
|
|
616
|
+
this.persist(run);
|
|
617
|
+
return true;
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (/concurrent|capacity/i.test(error instanceof Error ? error.message : String(error))) return false;
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private async spawnFinalReview(run: OrchestrationRun, gitBacked: boolean) {
|
|
625
|
+
const role = this.role("reviewer");
|
|
626
|
+
const cwd = (await this.integrationPath(run)) ?? run.cwd;
|
|
627
|
+
try {
|
|
628
|
+
const snapshot = await this.coordinator.spawn(this.context(), {
|
|
629
|
+
message: finalReviewPrompt(run),
|
|
630
|
+
taskName: `final review ${run.id}`,
|
|
631
|
+
cwd,
|
|
632
|
+
model: role.model,
|
|
633
|
+
thinking: role.thinking,
|
|
634
|
+
capability: "execute",
|
|
635
|
+
isolation: "none",
|
|
636
|
+
agentType: "explore",
|
|
637
|
+
concurrencyGroup: `orchestration:${run.projectKey}`,
|
|
638
|
+
maxConcurrent: 10,
|
|
639
|
+
});
|
|
640
|
+
run.finalReviewerAgentId = snapshot.id;
|
|
641
|
+
run.finalReviewPassed = undefined;
|
|
642
|
+
this.persist(run);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (!/concurrent|capacity/i.test(error instanceof Error ? error.message : String(error))) throw error;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private async handleFinalReview(run: OrchestrationRun, snapshot: SubagentSnapshot) {
|
|
649
|
+
if (snapshot.status === "error") return this.block(run, snapshot.errorText ?? "Final review failed.");
|
|
650
|
+
try {
|
|
651
|
+
const review = parseReviewDecision(snapshot.finalText);
|
|
652
|
+
run.finalReviewPassed = review.passed;
|
|
653
|
+
run.finalReviewSummary = review.summary;
|
|
654
|
+
await this.coordinator.release(snapshot.id);
|
|
655
|
+
this.persist(run);
|
|
656
|
+
await this.wakeOrchestrator(
|
|
657
|
+
run,
|
|
658
|
+
review.passed
|
|
659
|
+
? `Final holistic review passed: ${review.summary}`
|
|
660
|
+
: `Final holistic review failed. Findings: ${(review.findings ?? []).join("; ")}`,
|
|
661
|
+
);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
this.block(run, `Invalid final review: ${error instanceof Error ? error.message : String(error)}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private async wakeOrchestrator(run: OrchestrationRun, event: string) {
|
|
668
|
+
if (!run.orchestratorAgentId) return this.block(run, "Dedicated orchestrator session is unavailable.");
|
|
669
|
+
run.status = "running";
|
|
670
|
+
this.persist(run);
|
|
671
|
+
await this.coordinator.send(
|
|
672
|
+
run.orchestratorAgentId,
|
|
673
|
+
orchestratorContinuationPrompt(run, event),
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private async complete(run: OrchestrationRun) {
|
|
678
|
+
try {
|
|
679
|
+
const integration = await this.integrationPath(run);
|
|
680
|
+
if (integration && run.orchestratorAgentId) {
|
|
681
|
+
const status = await this.pi.exec("git", ["-C", run.cwd, "status", "--porcelain", "--untracked-files=all"]);
|
|
682
|
+
if (status.code !== 0 || status.stdout.trim()) {
|
|
683
|
+
throw new Error("The source checkout changed during orchestration; final integration requires it to remain clean.");
|
|
684
|
+
}
|
|
685
|
+
await this.coordinator.apply(run.orchestratorAgentId);
|
|
686
|
+
await this.coordinator.discard(run.orchestratorAgentId);
|
|
687
|
+
} else if (run.orchestratorAgentId) {
|
|
688
|
+
await this.coordinator.release(run.orchestratorAgentId);
|
|
689
|
+
}
|
|
690
|
+
run.status = "completed";
|
|
691
|
+
run.completedAt = Date.now();
|
|
692
|
+
run.error = undefined;
|
|
693
|
+
this.persist(run);
|
|
694
|
+
this.hooks.notify(`Orchestration ${run.id} completed and integrated.`, "info");
|
|
695
|
+
} catch (error) {
|
|
696
|
+
this.block(run, `Final integration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private block(run: OrchestrationRun, message: string) {
|
|
701
|
+
run.status = "blocked";
|
|
702
|
+
run.error = message;
|
|
703
|
+
this.persist(run);
|
|
704
|
+
this.hooks.notify(`Orchestration ${run.id} needs attention: ${message}`, "warning");
|
|
705
|
+
}
|
|
706
|
+
}
|