@diegopetrucci/pi-extensions 0.1.81 → 0.1.82
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/README.md +6 -1
- package/extensions/agent-workflow-audit/index.ts +47 -25
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/brrr/index.ts +21 -15
- package/extensions/brrr/package.json +1 -1
- package/extensions/code-reviewer/index.ts +52 -19
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/contrarian/index.ts +17 -17
- package/extensions/contrarian/package.json +1 -1
- package/extensions/developer/.pi-fleet-tested-version +1 -0
- package/extensions/developer/README.md +70 -0
- package/extensions/developer/index.ts +906 -0
- package/extensions/developer/package.json +40 -0
- package/extensions/diff-summarizer/.pi-fleet-tested-version +1 -0
- package/extensions/diff-summarizer/README.md +75 -0
- package/extensions/diff-summarizer/index.ts +1585 -0
- package/extensions/diff-summarizer/package.json +40 -0
- package/extensions/librarian/index.ts +10 -0
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +5 -1
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/index.ts +7 -2
- package/extensions/notify/package.json +1 -1
- package/extensions/oracle/index.ts +17 -17
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +6 -11
- package/extensions/permission-gate/index.ts +11 -54
- package/extensions/permission-gate/package.json +1 -3
- package/extensions/project-mcp-json/README.md +4 -3
- package/extensions/project-mcp-json/package.json +1 -1
- package/extensions/repo-scout/.pi-fleet-tested-version +1 -0
- package/extensions/repo-scout/README.md +74 -0
- package/extensions/repo-scout/index.ts +1575 -0
- package/extensions/repo-scout/package.json +40 -0
- package/extensions/test-runner/.pi-fleet-tested-version +1 -0
- package/extensions/test-runner/README.md +70 -0
- package/extensions/test-runner/index.ts +798 -0
- package/extensions/test-runner/package.json +40 -0
- package/extensions/triage-comments/index.ts +66 -16
- package/extensions/triage-comments/package.json +1 -1
- package/extensions/web-scout/.pi-fleet-tested-version +1 -0
- package/extensions/web-scout/README.md +67 -0
- package/extensions/web-scout/index.ts +1204 -0
- package/extensions/web-scout/package.json +41 -0
- package/package.json +7 -2
- package/extensions/permission-gate/powershell-safety.ts +0 -1053
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import type { AgentSession, ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
DefaultResourceLoader,
|
|
10
|
+
SessionManager,
|
|
11
|
+
SettingsManager,
|
|
12
|
+
createAgentSession,
|
|
13
|
+
getAgentDir,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Type } from "typebox";
|
|
17
|
+
|
|
18
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
19
|
+
|
|
20
|
+
const MAX_TOOL_CALLS_TO_KEEP = 80;
|
|
21
|
+
const MAX_TURNS = 40;
|
|
22
|
+
const MAX_RUN_MS = 30 * 60 * 1000;
|
|
23
|
+
const DEFAULT_BASH_TIMEOUT_SECONDS = 120;
|
|
24
|
+
const DEFAULT_THINKING_LEVEL = "high";
|
|
25
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
26
|
+
const BUILTIN_TOOL_NAMES = ["read", "write", "edit", "grep", "find", "ls", "bash"] as const;
|
|
27
|
+
|
|
28
|
+
type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
29
|
+
type ThinkingLevelMap = Partial<Record<ThinkingLevel, unknown | null>>;
|
|
30
|
+
|
|
31
|
+
const SUBAGENT_MCP_TOOLS = [
|
|
32
|
+
"mcp__*",
|
|
33
|
+
"tool_search",
|
|
34
|
+
"list_mcp_resources",
|
|
35
|
+
"list_mcp_resource_templates",
|
|
36
|
+
"read_mcp_resource",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Derive project-trust for the isolated subagent session from the host tool context.
|
|
40
|
+
// Fails closed: returns false if the host has not trusted the project, or if the subagent
|
|
41
|
+
// runs in a different directory (e.g. a temp workspace), preventing an untrusted project's
|
|
42
|
+
// .pi/mcp.json from being loaded by the MCP extension.
|
|
43
|
+
function resolveSubagentProjectTrusted(
|
|
44
|
+
ctx: { cwd: string; isProjectTrusted(): boolean },
|
|
45
|
+
subagentCwd: string,
|
|
46
|
+
): boolean {
|
|
47
|
+
// Feature-detected for older Pi hosts that may lack isProjectTrusted.
|
|
48
|
+
const hostTrusted =
|
|
49
|
+
typeof (ctx as { isProjectTrusted?: unknown }).isProjectTrusted === "function"
|
|
50
|
+
? ctx.isProjectTrusted()
|
|
51
|
+
: false;
|
|
52
|
+
if (!hostTrusted) return false;
|
|
53
|
+
try {
|
|
54
|
+
return realpathSync(subagentCwd) === realpathSync(ctx.cwd);
|
|
55
|
+
} catch {
|
|
56
|
+
return false; // fail closed if realpath resolution fails
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isSubagentMcpTool(toolName: string): boolean {
|
|
61
|
+
return (
|
|
62
|
+
toolName.startsWith("mcp__") ||
|
|
63
|
+
toolName === "tool_search" ||
|
|
64
|
+
toolName === "list_mcp_resources" ||
|
|
65
|
+
toolName === "list_mcp_resource_templates" ||
|
|
66
|
+
toolName === "read_mcp_resource"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Returns true when v is a semver string >=1.0.4 (ignores prerelease; missing/garbage -> false). */
|
|
71
|
+
function isMcpCompatibleVersion(v: string | undefined): boolean {
|
|
72
|
+
if (!v) return false;
|
|
73
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
74
|
+
if (!m) return false;
|
|
75
|
+
const major = Number(m[1]),
|
|
76
|
+
minor = Number(m[2]),
|
|
77
|
+
patch = Number(m[3]);
|
|
78
|
+
if (major !== 1) return major > 1;
|
|
79
|
+
if (minor !== 0) return minor > 0;
|
|
80
|
+
return patch >= 4;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Gated on VERSION >=1.0.4: on older hosts mcp__* allowlist entries are exact-name
|
|
84
|
+
// matches that resolve nothing, so spawning MCP servers provides no benefit.
|
|
85
|
+
function createMcpSubagentFactories(): ExtensionFactory[] {
|
|
86
|
+
const ns = piCodingAgent as Record<string, unknown>;
|
|
87
|
+
if (!isMcpCompatibleVersion(ns["VERSION"] as string | undefined)) return [];
|
|
88
|
+
const factories: ExtensionFactory[] = [];
|
|
89
|
+
const mcp = ns["createMcpExtension"];
|
|
90
|
+
const toolSearch = ns["createToolSearchExtension"];
|
|
91
|
+
if (typeof mcp === "function") factories.push((mcp as () => ExtensionFactory)());
|
|
92
|
+
if (typeof toolSearch === "function") factories.push((toolSearch as () => ExtensionFactory)());
|
|
93
|
+
return factories;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Emit session_shutdown before dispose so MCP closes server connections (no leaked child processes).
|
|
97
|
+
async function disposeSubagentSession(session: AgentSession | undefined): Promise<void> {
|
|
98
|
+
if (!session) return;
|
|
99
|
+
try {
|
|
100
|
+
const runner = (
|
|
101
|
+
session as unknown as {
|
|
102
|
+
extensionRunner?: {
|
|
103
|
+
hasHandlers(n: string): boolean;
|
|
104
|
+
emit(e: { type: string; reason: string }): Promise<unknown>;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
).extensionRunner;
|
|
108
|
+
if (runner?.hasHandlers("session_shutdown")) {
|
|
109
|
+
await runner.emit({ type: "session_shutdown", reason: "quit" });
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Shutdown errors must not prevent dispose.
|
|
113
|
+
}
|
|
114
|
+
session.dispose();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const DeveloperParams = Type.Object({
|
|
118
|
+
task: Type.String({
|
|
119
|
+
description: "The single task to implement, including acceptance criteria and validation.",
|
|
120
|
+
}),
|
|
121
|
+
ticket: Type.Optional(
|
|
122
|
+
Type.String({
|
|
123
|
+
description: "Optional ticket ID or ticket text that is the source of truth for the task.",
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
context: Type.Optional(
|
|
127
|
+
Type.String({
|
|
128
|
+
description: "Optional caller context such as constraints, conventions, or known risks.",
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
cwd: Type.Optional(
|
|
132
|
+
Type.String({
|
|
133
|
+
description:
|
|
134
|
+
"Optional working directory (checkout) for the developer; defaults to the current session directory.",
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
type ToolCall = {
|
|
140
|
+
name: string;
|
|
141
|
+
args: unknown;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
type RunStatus = "running" | "done" | "error" | "aborted";
|
|
145
|
+
|
|
146
|
+
type RunDetails = {
|
|
147
|
+
status: RunStatus;
|
|
148
|
+
cwd: string;
|
|
149
|
+
task: string;
|
|
150
|
+
turns: number;
|
|
151
|
+
toolCalls: ToolCall[];
|
|
152
|
+
startedAt: number;
|
|
153
|
+
endedAt?: number;
|
|
154
|
+
error?: string;
|
|
155
|
+
modelRef?: string;
|
|
156
|
+
modelName?: string;
|
|
157
|
+
requestedThinkingLevel?: ThinkingLevel;
|
|
158
|
+
effectiveThinkingLevel?: ThinkingLevel;
|
|
159
|
+
thinkingLevelClamped?: boolean;
|
|
160
|
+
thinkingLevelNote?: string;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
type PiModel = {
|
|
164
|
+
provider: string;
|
|
165
|
+
id: string;
|
|
166
|
+
name?: string;
|
|
167
|
+
reasoning?: boolean;
|
|
168
|
+
contextWindow?: number;
|
|
169
|
+
maxTokens?: number;
|
|
170
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
|
|
174
|
+
type CreateAgentSessionModel = CreateAgentSessionOptions["model"];
|
|
175
|
+
|
|
176
|
+
function getModelRuntimeOption(ctx: {
|
|
177
|
+
modelRegistry?: unknown;
|
|
178
|
+
}): Pick<CreateAgentSessionOptions, "modelRuntime"> {
|
|
179
|
+
const modelRuntime = (
|
|
180
|
+
ctx.modelRegistry as { runtime?: CreateAgentSessionOptions["modelRuntime"] } | undefined
|
|
181
|
+
)?.runtime;
|
|
182
|
+
return modelRuntime ? { modelRuntime } : {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeThinkingLevel(value: unknown): ThinkingLevel | undefined {
|
|
186
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value as ThinkingLevel)
|
|
187
|
+
? (value as ThinkingLevel)
|
|
188
|
+
: undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Keep these local so the extension stays compatible with older pi peer installs that do not export clamp helpers.
|
|
192
|
+
function isThinkingLevelSupported(model: PiModel, level: ThinkingLevel): boolean {
|
|
193
|
+
if (!model.reasoning) return level === "off";
|
|
194
|
+
|
|
195
|
+
const map = model.thinkingLevelMap;
|
|
196
|
+
if (level === "xhigh" || level === "max") {
|
|
197
|
+
return !!map && Object.prototype.hasOwnProperty.call(map, level) && map[level] != null;
|
|
198
|
+
}
|
|
199
|
+
return map?.[level] !== null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingLevel {
|
|
203
|
+
if (isThinkingLevelSupported(model, requested)) return requested;
|
|
204
|
+
|
|
205
|
+
const requestedIndex = THINKING_LEVELS.indexOf(requested);
|
|
206
|
+
for (let index = requestedIndex + 1; index < THINKING_LEVELS.length; index += 1) {
|
|
207
|
+
const level = THINKING_LEVELS[index];
|
|
208
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
209
|
+
}
|
|
210
|
+
for (let index = requestedIndex - 1; index >= 0; index -= 1) {
|
|
211
|
+
const level = THINKING_LEVELS[index];
|
|
212
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return "off";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function resolveThinkingLevel(
|
|
219
|
+
model: PiModel | undefined,
|
|
220
|
+
requestedThinkingLevel: ThinkingLevel | undefined,
|
|
221
|
+
): {
|
|
222
|
+
requested: ThinkingLevel;
|
|
223
|
+
effective: ThinkingLevel;
|
|
224
|
+
clamped: boolean;
|
|
225
|
+
note: string;
|
|
226
|
+
} {
|
|
227
|
+
const requested = requestedThinkingLevel ?? (model?.reasoning ? DEFAULT_THINKING_LEVEL : "off");
|
|
228
|
+
const effective = model ? clampThinkingLevel(model, requested) : requested;
|
|
229
|
+
const clamped = effective !== requested;
|
|
230
|
+
if (clamped) {
|
|
231
|
+
return {
|
|
232
|
+
requested,
|
|
233
|
+
effective,
|
|
234
|
+
clamped,
|
|
235
|
+
note: `requested ${requested}; clamped to ${effective}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (requestedThinkingLevel) {
|
|
239
|
+
return { requested, effective, clamped, note: `requested ${requested}` };
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
requested,
|
|
243
|
+
effective,
|
|
244
|
+
clamped,
|
|
245
|
+
note: model?.reasoning
|
|
246
|
+
? `defaulted to ${effective}`
|
|
247
|
+
: `defaulted to off for non-reasoning model`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function applyModelAttemptDetails(
|
|
252
|
+
details: RunDetails,
|
|
253
|
+
model: PiModel,
|
|
254
|
+
thinking: ReturnType<typeof resolveThinkingLevel>,
|
|
255
|
+
): void {
|
|
256
|
+
details.modelRef = `${model.provider}/${model.id}`;
|
|
257
|
+
details.modelName = model.name;
|
|
258
|
+
details.requestedThinkingLevel = thinking.requested;
|
|
259
|
+
details.effectiveThinkingLevel = thinking.effective;
|
|
260
|
+
details.thinkingLevelClamped = thinking.clamped || undefined;
|
|
261
|
+
details.thinkingLevelNote = thinking.note;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createEmptyUsage(): Usage {
|
|
265
|
+
return {
|
|
266
|
+
input: 0,
|
|
267
|
+
output: 0,
|
|
268
|
+
cacheRead: 0,
|
|
269
|
+
cacheWrite: 0,
|
|
270
|
+
totalTokens: 0,
|
|
271
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function addUsage(total: Usage, usage: Usage): void {
|
|
276
|
+
total.input += usage.input || 0;
|
|
277
|
+
total.output += usage.output || 0;
|
|
278
|
+
total.cacheRead += usage.cacheRead || 0;
|
|
279
|
+
total.cacheWrite += usage.cacheWrite || 0;
|
|
280
|
+
if (usage.cacheWrite1h !== undefined)
|
|
281
|
+
total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
|
|
282
|
+
if (usage.reasoning !== undefined) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
|
|
283
|
+
total.totalTokens += usage.totalTokens || 0;
|
|
284
|
+
total.cost.input += usage.cost?.input || 0;
|
|
285
|
+
total.cost.output += usage.cost?.output || 0;
|
|
286
|
+
total.cost.cacheRead += usage.cost?.cacheRead || 0;
|
|
287
|
+
total.cost.cacheWrite += usage.cost?.cacheWrite || 0;
|
|
288
|
+
total.cost.total += usage.cost?.total || 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type AssistantLikeMessage = {
|
|
292
|
+
role?: string;
|
|
293
|
+
content?: unknown;
|
|
294
|
+
stopReason?: unknown;
|
|
295
|
+
errorMessage?: unknown;
|
|
296
|
+
usage?: unknown;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
function addAssistantMessageUsage(total: Usage, message: unknown): void {
|
|
300
|
+
const assistant = message as AssistantLikeMessage;
|
|
301
|
+
if (assistant?.role !== "assistant" || !assistant.usage || typeof assistant.usage !== "object")
|
|
302
|
+
return;
|
|
303
|
+
addUsage(total, assistant.usage as Usage);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function addSessionEventUsage(total: Usage, event: unknown): void {
|
|
307
|
+
const sessionEvent = event as {
|
|
308
|
+
type?: string;
|
|
309
|
+
message?: unknown;
|
|
310
|
+
result?: { usage?: Usage };
|
|
311
|
+
};
|
|
312
|
+
if (sessionEvent?.type === "message_end")
|
|
313
|
+
return void addAssistantMessageUsage(total, sessionEvent.message);
|
|
314
|
+
if (sessionEvent?.type === "compaction_end" && sessionEvent.result?.usage)
|
|
315
|
+
addUsage(total, sessionEvent.result.usage);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function inspectFinalAssistant(
|
|
319
|
+
messages: unknown[],
|
|
320
|
+
):
|
|
321
|
+
| { ok: true; answer: string; stopReason?: string }
|
|
322
|
+
| { ok: false; reason: string; stopReason?: string; errorMessage?: string } {
|
|
323
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
324
|
+
const message = messages[i] as AssistantLikeMessage;
|
|
325
|
+
if (message?.role !== "assistant") continue;
|
|
326
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason.trim() : "";
|
|
327
|
+
const errorMessage =
|
|
328
|
+
typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
|
|
329
|
+
const answer = Array.isArray(message.content)
|
|
330
|
+
? message.content
|
|
331
|
+
.map((part) =>
|
|
332
|
+
part &&
|
|
333
|
+
typeof part === "object" &&
|
|
334
|
+
(part as { type?: string }).type === "text" &&
|
|
335
|
+
typeof (part as { text?: unknown }).text === "string"
|
|
336
|
+
? (part as { text: string }).text
|
|
337
|
+
: "",
|
|
338
|
+
)
|
|
339
|
+
.join("")
|
|
340
|
+
.trim()
|
|
341
|
+
: "";
|
|
342
|
+
if (stopReason === "error")
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
reason: `developer subagent error: ${errorMessage || "provider/model error"}`,
|
|
346
|
+
stopReason,
|
|
347
|
+
errorMessage,
|
|
348
|
+
};
|
|
349
|
+
if (stopReason === "aborted")
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
reason: "developer subagent aborted before producing a usable answer",
|
|
353
|
+
stopReason,
|
|
354
|
+
};
|
|
355
|
+
if (answer) return { ok: true, answer, stopReason: stopReason || undefined };
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
reason: stopReason
|
|
359
|
+
? `developer subagent produced no final assistant text (stopReason: ${stopReason})`
|
|
360
|
+
: "developer subagent produced no final assistant text",
|
|
361
|
+
stopReason: stopReason || undefined,
|
|
362
|
+
errorMessage: errorMessage || undefined,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
reason: "developer subagent produced no assistant message",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function classifyRunFailure(
|
|
372
|
+
error: unknown,
|
|
373
|
+
aborted: boolean,
|
|
374
|
+
): { status: "aborted" | "error"; message: string; error?: string } {
|
|
375
|
+
const message = aborted ? "Aborted" : error instanceof Error ? error.message : String(error);
|
|
376
|
+
return {
|
|
377
|
+
status: aborted ? "aborted" : "error",
|
|
378
|
+
message,
|
|
379
|
+
error: aborted ? undefined : message,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isInside(parent: string, child: string): boolean {
|
|
384
|
+
const parentResolved = path.resolve(parent);
|
|
385
|
+
const childResolved = path.resolve(child);
|
|
386
|
+
return (
|
|
387
|
+
childResolved === parentResolved || childResolved.startsWith(`${parentResolved}${path.sep}`)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const UNICODE_SPACES_PATTERN = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Mirrors normalizeWindowsShellPath from dist/utils/paths.js. No-op on
|
|
395
|
+
* non-win32 paths.
|
|
396
|
+
*/
|
|
397
|
+
function normalizeWindowsShellPathLocal(filePath: string): string {
|
|
398
|
+
if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\"))
|
|
399
|
+
return filePath;
|
|
400
|
+
const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
|
|
401
|
+
if (!match) return filePath;
|
|
402
|
+
const suffix = match[2]?.replaceAll("/", "\\");
|
|
403
|
+
return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
|
|
408
|
+
* drive-path normalisation always runs; only tilde expansion is gated by
|
|
409
|
+
* expandTilde.
|
|
410
|
+
*/
|
|
411
|
+
function normalizePath(
|
|
412
|
+
input: string,
|
|
413
|
+
opts: {
|
|
414
|
+
normalizeUnicodeSpaces?: boolean;
|
|
415
|
+
stripAtPrefix?: boolean;
|
|
416
|
+
expandTilde?: boolean;
|
|
417
|
+
} = {},
|
|
418
|
+
): string {
|
|
419
|
+
const { normalizeUnicodeSpaces = false, stripAtPrefix = false, expandTilde = true } = opts;
|
|
420
|
+
let p = input;
|
|
421
|
+
if (normalizeUnicodeSpaces) p = p.replace(UNICODE_SPACES_PATTERN, " ");
|
|
422
|
+
if (stripAtPrefix && p.startsWith("@")) p = p.slice(1);
|
|
423
|
+
if (process.platform === "win32") p = normalizeWindowsShellPathLocal(p);
|
|
424
|
+
if (expandTilde) {
|
|
425
|
+
const home = os.homedir();
|
|
426
|
+
if (p === "~") p = home;
|
|
427
|
+
else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~\\")))
|
|
428
|
+
p = path.join(home, p.slice(2));
|
|
429
|
+
}
|
|
430
|
+
if (/^file:\/\//.test(p)) p = fileURLToPath(p);
|
|
431
|
+
return p;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Mirrors Pi's resolveToCwd (dist/core/tools/path-utils.js). Exposed via
|
|
436
|
+
* __test__ for parity tests; not imported by other extensions.
|
|
437
|
+
*/
|
|
438
|
+
function resolveToCwd(rawPath: string, cwd: string): string {
|
|
439
|
+
const normalized = normalizePath(rawPath, {
|
|
440
|
+
normalizeUnicodeSpaces: true,
|
|
441
|
+
stripAtPrefix: true,
|
|
442
|
+
});
|
|
443
|
+
const base = normalizePath(cwd);
|
|
444
|
+
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(base, normalized);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Write/edit guard. Resolves the nearest existing ancestor through symlinks so that
|
|
449
|
+
* new files created beneath a symlinked directory cannot escape the checkout.
|
|
450
|
+
*/
|
|
451
|
+
async function assertMutationPathInsideCwd(
|
|
452
|
+
cwd: string,
|
|
453
|
+
rawPath: unknown,
|
|
454
|
+
toolName: string,
|
|
455
|
+
): Promise<string | undefined> {
|
|
456
|
+
if (typeof rawPath !== "string" || !rawPath.trim())
|
|
457
|
+
return `${toolName} path must be a non-empty string.`;
|
|
458
|
+
try {
|
|
459
|
+
const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
460
|
+
let existing = resolveToCwd(rawPath, cwd);
|
|
461
|
+
const remainder: string[] = [];
|
|
462
|
+
let hops = 0;
|
|
463
|
+
for (;;) {
|
|
464
|
+
try {
|
|
465
|
+
existing = await fs.realpath(existing);
|
|
466
|
+
break;
|
|
467
|
+
} catch {
|
|
468
|
+
// A dangling symlink is followed by write/edit, so resolve its target instead.
|
|
469
|
+
const linkTarget = await fs.readlink(existing).catch(() => undefined);
|
|
470
|
+
if (linkTarget !== undefined && ++hops < 40) {
|
|
471
|
+
existing = path.resolve(path.dirname(existing), linkTarget);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const parent = path.dirname(existing);
|
|
475
|
+
if (parent === existing) break;
|
|
476
|
+
remainder.unshift(path.basename(existing));
|
|
477
|
+
existing = parent;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const target = path.join(existing, ...remainder);
|
|
481
|
+
if (!isInside(root, target)) return `${toolName} is limited to the local checkout: ${target}`;
|
|
482
|
+
} catch {
|
|
483
|
+
return `${toolName} path could not be resolved safely.`;
|
|
484
|
+
}
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function createDeveloperRuntimeGuardExtension(options: {
|
|
489
|
+
cwd: string;
|
|
490
|
+
maxTurns: number;
|
|
491
|
+
}): ExtensionFactory {
|
|
492
|
+
return (pi) => {
|
|
493
|
+
let currentTurn = 0;
|
|
494
|
+
|
|
495
|
+
pi.on("turn_start", async (event) => {
|
|
496
|
+
currentTurn = event.turnIndex;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
pi.on("tool_call", async (event) => {
|
|
500
|
+
const allowed =
|
|
501
|
+
(BUILTIN_TOOL_NAMES as readonly string[]).includes(event.toolName) ||
|
|
502
|
+
isSubagentMcpTool(event.toolName);
|
|
503
|
+
if (!allowed) {
|
|
504
|
+
return {
|
|
505
|
+
block: true,
|
|
506
|
+
reason: `developer exposes read, write, edit, grep, find, ls, bash, and MCP tools; ${event.toolName} is not allowed.`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (currentTurn >= options.maxTurns - 1) {
|
|
511
|
+
return {
|
|
512
|
+
block: true,
|
|
513
|
+
reason: `Tool use is disabled on final developer turn ${options.maxTurns}/${options.maxTurns}. Report what is done and any blockers now.`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
518
|
+
const reason = await assertMutationPathInsideCwd(
|
|
519
|
+
options.cwd,
|
|
520
|
+
(event.input as { path?: unknown }).path,
|
|
521
|
+
event.toolName,
|
|
522
|
+
);
|
|
523
|
+
if (reason) return { block: true, reason };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (event.toolName === "bash") {
|
|
527
|
+
const input = event.input as { timeout?: unknown };
|
|
528
|
+
if (typeof input.timeout !== "number") input.timeout = DEFAULT_BASH_TIMEOUT_SECONDS;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return undefined;
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
pi.on("tool_result", async (event) => ({
|
|
535
|
+
content: [
|
|
536
|
+
...(event.content ?? []),
|
|
537
|
+
{
|
|
538
|
+
type: "text",
|
|
539
|
+
text: `\n\n[developer turn budget] turn ${Math.min(currentTurn + 1, options.maxTurns)}/${options.maxTurns}`,
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
}));
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function buildSystemPrompt(options: {
|
|
547
|
+
cwd: string;
|
|
548
|
+
maxTurns: number;
|
|
549
|
+
maxRunSeconds: number;
|
|
550
|
+
}): string {
|
|
551
|
+
return `You are the Developer, an isolated senior-engineer agent running inside the developer package. Implement exactly one task in the local checkout, validate it, and return a concise completion report.
|
|
552
|
+
|
|
553
|
+
Working directory: ${options.cwd}
|
|
554
|
+
Turn budget: ${options.maxTurns} turns total, including your final answer.
|
|
555
|
+
Wall-clock budget: ${options.maxRunSeconds} seconds.
|
|
556
|
+
|
|
557
|
+
Available tools: read, write, edit, grep, find, ls, bash, and any configured MCP tools. A runtime guard limits write/edit to paths inside the working directory. bash is unrestricted and is not sandboxed: use it only for the task (inspection, validation, repository tooling) and never to modify files outside the checkout or to bypass the rules below. Prefer the built-in read, grep, find, ls, write, and edit tools for local files. MCP tools run without confirmation; use them only within the task scope and avoid state-changing actions the task does not require. MCP access requires host Pi >=1.0.4; on older hosts MCP tools are absent with no error.
|
|
558
|
+
|
|
559
|
+
## Source of truth
|
|
560
|
+
|
|
561
|
+
The task (and ticket text, if supplied) in the user message is the source of truth and your authorization to proceed; do not ask for confirmation before starting. If it is missing, invalid, or ambiguous in a way that blocks safe implementation, do not guess on important decisions: stop without editing files and report the blocker in your final result. There is no way to ask questions mid-run.
|
|
562
|
+
|
|
563
|
+
## Operating model
|
|
564
|
+
|
|
565
|
+
- Plan approval or task approval is not authorization to mutate, revert, overwrite, or clean up pre-existing worktree or index changes you did not create for the current task.
|
|
566
|
+
- Treat pre-existing worktree and index changes as human-owned. Touch them only with scoped authorization given in the task or context.
|
|
567
|
+
- Do not use git stash, git restore, git reset, non-dry-run git clean, or checkout/switch discard or force options against pre-existing state without that authorization.
|
|
568
|
+
- Never create a git commit, push, or publish unless the task explicitly authorizes it.
|
|
569
|
+
- Preserve unrelated state while implementing the task.
|
|
570
|
+
- Implement only what the task asks for. Do not implement future tasks, nice-to-haves, speculative refactors, or unrelated cleanup.
|
|
571
|
+
- Keep changes small, cohesive, and easy to review.
|
|
572
|
+
- Follow existing repository conventions for structure, naming, formatting, tests, and error handling.
|
|
573
|
+
|
|
574
|
+
## Blockers
|
|
575
|
+
|
|
576
|
+
Report blockers in your final result instead of guessing when: the task is ambiguous or missing a decision needed for safe implementation; requirements conflict with existing behavior or project conventions; a product/API/scope decision appears; a discovery invalidates the intended approach; pre-existing changes overlap the task and block a safe, scoped implementation; or validation cannot be completed for an environmental reason.
|
|
577
|
+
|
|
578
|
+
## Implementation expectations
|
|
579
|
+
|
|
580
|
+
- Prefer the simplest correct implementation.
|
|
581
|
+
- Add or update high-ROI tests for meaningful behavior, regressions, edge cases, error handling, or security-sensitive logic. Avoid low-value tests that merely restate implementation details.
|
|
582
|
+
- Update docs or comments only when they materially help users or maintainers.
|
|
583
|
+
- Handle errors deliberately; avoid fragile behavior and silent failure.
|
|
584
|
+
- Keep secrets and PII out of code, logs, tests, and reports.
|
|
585
|
+
|
|
586
|
+
## Validation
|
|
587
|
+
|
|
588
|
+
- Discover the repository's checks and run the narrowest meaningful validation before reporting completion, unless the task explicitly says otherwise.
|
|
589
|
+
- If the task defines a specific validation scope, follow it exactly.
|
|
590
|
+
- If validation you were expected to run fails, fix the issue and rerun it until it passes. Do not claim validation you did not perform.
|
|
591
|
+
|
|
592
|
+
## Completion report
|
|
593
|
+
|
|
594
|
+
Finish with a report containing, in this order:
|
|
595
|
+
|
|
596
|
+
- Summary: 2–4 bullets describing what changed and why.
|
|
597
|
+
- Files changed: list of file paths.
|
|
598
|
+
- Validation: exact commands run and outcomes, or note when validation was explicitly deferred.
|
|
599
|
+
- Problems encountered: unclear, surprising, or worked-around issues, including any blockers.
|
|
600
|
+
- Tradeoffs or risks: only meaningful ones.
|
|
601
|
+
|
|
602
|
+
Do not request code review yourself; the caller owns review.`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function buildUserPrompt(
|
|
606
|
+
input: { task: string; ticket?: string; context?: string },
|
|
607
|
+
cwd: string,
|
|
608
|
+
): string {
|
|
609
|
+
return `Task to implement:\n${input.task}\n\nLocal checkout: ${cwd}\n\nOptional ticket (source of truth when provided):\n${input.ticket?.trim() ? input.ticket : "(not provided)"}\n\nOptional caller context:\n${input.context?.trim() ? input.context : "(not provided)"}\n\nImplement only this task, validate it, and respond using the required completion report format.`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function formatToolCall(call: ToolCall): string {
|
|
613
|
+
const args =
|
|
614
|
+
call.args && typeof call.args === "object" ? (call.args as Record<string, unknown>) : {};
|
|
615
|
+
if (call.name === "read") {
|
|
616
|
+
const readPath = typeof args.path === "string" ? args.path : "";
|
|
617
|
+
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
618
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
619
|
+
const range =
|
|
620
|
+
offset || limit ? `:${offset ?? 1}${limit ? `-${(offset ?? 1) + limit - 1}` : ""}` : "";
|
|
621
|
+
return `read ${readPath}${range}`.trim();
|
|
622
|
+
}
|
|
623
|
+
if (call.name === "write" || call.name === "edit") {
|
|
624
|
+
return `${call.name} ${typeof args.path === "string" ? args.path : ""}`.trim();
|
|
625
|
+
}
|
|
626
|
+
if (call.name === "grep") {
|
|
627
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
628
|
+
const grepPath = typeof args.path === "string" ? args.path : ".";
|
|
629
|
+
return `grep ${pattern.slice(0, 50)} ${grepPath}`.trim();
|
|
630
|
+
}
|
|
631
|
+
if (call.name === "find") {
|
|
632
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
633
|
+
const findPath = typeof args.path === "string" ? args.path : ".";
|
|
634
|
+
return `find ${pattern.slice(0, 50)} ${findPath}`.trim();
|
|
635
|
+
}
|
|
636
|
+
if (call.name === "ls") return `ls ${typeof args.path === "string" ? args.path : "."}`.trim();
|
|
637
|
+
if (call.name === "bash") {
|
|
638
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
639
|
+
return `bash ${command.slice(0, 120)}`.trim();
|
|
640
|
+
}
|
|
641
|
+
return call.name;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function formatDuration(ms: number): string {
|
|
645
|
+
if (ms < 1000) return `${ms}ms`;
|
|
646
|
+
const seconds = Math.round(ms / 100) / 10;
|
|
647
|
+
if (seconds < 60) return `${seconds}s`;
|
|
648
|
+
const minutes = Math.floor(seconds / 60);
|
|
649
|
+
const remainingSeconds = Math.round((seconds % 60) * 10) / 10;
|
|
650
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function appendRunDetails(report: string, details: RunDetails): string {
|
|
654
|
+
const trimmed = report.trim();
|
|
655
|
+
const toolSummary =
|
|
656
|
+
details.toolCalls.length > 0 ? details.toolCalls.map(formatToolCall).join("; ") : "none";
|
|
657
|
+
const duration = formatDuration((details.endedAt ?? Date.now()) - details.startedAt);
|
|
658
|
+
const modelSummary = details.modelRef ? `model ${details.modelRef}` : "model unknown";
|
|
659
|
+
const thinkingSummary = details.effectiveThinkingLevel
|
|
660
|
+
? `thinking ${details.effectiveThinkingLevel}${details.thinkingLevelNote ? ` (${details.thinkingLevelNote})` : ""}`
|
|
661
|
+
: "thinking unknown";
|
|
662
|
+
const suffix = `\n\n---\nRun details: ${modelSummary}; ${thinkingSummary}; ${details.turns} turn(s); ${details.toolCalls.length} tool call(s); duration ${duration}; cwd ${details.cwd}; tools ${toolSummary}.`;
|
|
663
|
+
if (!trimmed)
|
|
664
|
+
return `## Summary\n- No completion report was produced.\n\n## Files changed\n- unknown\n\n## Validation\n- none\n\n## Problems encountered\n- The isolated developer session returned no assistant text.${suffix}`;
|
|
665
|
+
return `${trimmed}${suffix}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export const __test__ = {
|
|
669
|
+
SUBAGENT_MCP_TOOLS,
|
|
670
|
+
BUILTIN_TOOL_NAMES,
|
|
671
|
+
isMcpCompatibleVersion,
|
|
672
|
+
resolveSubagentProjectTrusted,
|
|
673
|
+
createMcpSubagentFactories,
|
|
674
|
+
disposeSubagentSession,
|
|
675
|
+
createDeveloperRuntimeGuardExtension,
|
|
676
|
+
assertMutationPathInsideCwd,
|
|
677
|
+
buildSystemPrompt,
|
|
678
|
+
buildUserPrompt,
|
|
679
|
+
normalizeThinkingLevel,
|
|
680
|
+
resolveThinkingLevel,
|
|
681
|
+
createEmptyUsage,
|
|
682
|
+
addAssistantMessageUsage,
|
|
683
|
+
addSessionEventUsage,
|
|
684
|
+
inspectFinalAssistant,
|
|
685
|
+
classifyRunFailure,
|
|
686
|
+
appendRunDetails,
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
async function resolveRunCwd(baseCwd: string, requested: unknown): Promise<string> {
|
|
690
|
+
const base = path.resolve(baseCwd);
|
|
691
|
+
if (requested === undefined) return base;
|
|
692
|
+
if (typeof requested !== "string" || !requested.trim())
|
|
693
|
+
throw new Error("Invalid parameters: expected cwd to be a non-empty string when provided.");
|
|
694
|
+
const resolved = resolveToCwd(requested, base);
|
|
695
|
+
const stat = await fs.stat(resolved).catch(() => undefined);
|
|
696
|
+
if (!stat?.isDirectory())
|
|
697
|
+
throw new Error(`Invalid parameters: cwd is not an existing directory: ${resolved}`);
|
|
698
|
+
return resolved;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export default function developerExtension(pi: ExtensionAPI) {
|
|
702
|
+
pi.on("tool_result", async (event) => {
|
|
703
|
+
if (event.toolName !== "developer") return undefined;
|
|
704
|
+
const details = event.details as RunDetails | undefined;
|
|
705
|
+
if (details?.status !== "error") return undefined;
|
|
706
|
+
return { isError: true };
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
pi.registerTool({
|
|
710
|
+
name: "developer",
|
|
711
|
+
label: "Developer",
|
|
712
|
+
description:
|
|
713
|
+
"Isolated developer that implements exactly one task in the local checkout using read, write, edit, grep, find, ls, unrestricted bash, and configured MCP tools, then returns a completion report.",
|
|
714
|
+
promptSnippet:
|
|
715
|
+
"Delegate one well-scoped implementation task to an isolated developer child agent that edits the checkout, validates its work, and returns a completion report with run details.",
|
|
716
|
+
promptGuidelines: [
|
|
717
|
+
"Use developer to implement one clearly scoped task in an isolated child agent; provide the task, acceptance criteria, validation expectations, and any ticket text.",
|
|
718
|
+
"developer can edit files and run arbitrary bash commands in the checkout (write/edit are limited to the checkout; bash is not sandboxed). Do not hand it destructive or out-of-scope work.",
|
|
719
|
+
],
|
|
720
|
+
parameters: DeveloperParams,
|
|
721
|
+
|
|
722
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
723
|
+
const task = typeof params.task === "string" ? params.task.trim() : "";
|
|
724
|
+
if (!task) throw new Error("Invalid parameters: expected task to be a non-empty string.");
|
|
725
|
+
const cwd = await resolveRunCwd(ctx.cwd, params.cwd);
|
|
726
|
+
|
|
727
|
+
const input = {
|
|
728
|
+
task,
|
|
729
|
+
ticket: typeof params.ticket === "string" ? params.ticket : undefined,
|
|
730
|
+
context: typeof params.context === "string" ? params.context : undefined,
|
|
731
|
+
};
|
|
732
|
+
const details: RunDetails = {
|
|
733
|
+
status: "running",
|
|
734
|
+
cwd,
|
|
735
|
+
task,
|
|
736
|
+
turns: 0,
|
|
737
|
+
toolCalls: [],
|
|
738
|
+
startedAt: Date.now(),
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
let lastContent = "(implementing task...)";
|
|
742
|
+
let session: AgentSession | undefined;
|
|
743
|
+
let unsubscribe: (() => void) | undefined;
|
|
744
|
+
let runTimeout: NodeJS.Timeout | undefined;
|
|
745
|
+
let abortListenerAdded = false;
|
|
746
|
+
let callerAborted = Boolean(signal?.aborted);
|
|
747
|
+
const usage = createEmptyUsage();
|
|
748
|
+
|
|
749
|
+
const emit = () => {
|
|
750
|
+
onUpdate?.({ content: [{ type: "text", text: lastContent }], details });
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const abortForCleanup = () => {
|
|
754
|
+
void session?.abort();
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const abortFromCaller = () => {
|
|
758
|
+
callerAborted = true;
|
|
759
|
+
details.status = "aborted";
|
|
760
|
+
details.endedAt = Date.now();
|
|
761
|
+
lastContent = "Aborted";
|
|
762
|
+
emit();
|
|
763
|
+
abortForCleanup();
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
if (signal?.aborted) abortFromCaller();
|
|
767
|
+
if (signal && !signal.aborted) {
|
|
768
|
+
signal.addEventListener("abort", abortFromCaller);
|
|
769
|
+
abortListenerAdded = true;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
try {
|
|
773
|
+
emit();
|
|
774
|
+
|
|
775
|
+
const model = ctx.model as PiModel | undefined;
|
|
776
|
+
if (!model)
|
|
777
|
+
throw new Error(
|
|
778
|
+
"No active session model is available. Select a model before using developer.",
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
const projectTrusted = resolveSubagentProjectTrusted(ctx, cwd);
|
|
782
|
+
const isolatedSettingsManager = SettingsManager.inMemory({}, { projectTrusted });
|
|
783
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
784
|
+
cwd,
|
|
785
|
+
agentDir: getAgentDir(),
|
|
786
|
+
settingsManager: isolatedSettingsManager,
|
|
787
|
+
noExtensions: true,
|
|
788
|
+
noSkills: true,
|
|
789
|
+
noPromptTemplates: true,
|
|
790
|
+
noThemes: true,
|
|
791
|
+
noContextFiles: true,
|
|
792
|
+
extensionFactories: [
|
|
793
|
+
createDeveloperRuntimeGuardExtension({ cwd, maxTurns: MAX_TURNS }),
|
|
794
|
+
...createMcpSubagentFactories(),
|
|
795
|
+
],
|
|
796
|
+
systemPromptOverride: () =>
|
|
797
|
+
buildSystemPrompt({
|
|
798
|
+
cwd,
|
|
799
|
+
maxTurns: MAX_TURNS,
|
|
800
|
+
maxRunSeconds: Math.round(MAX_RUN_MS / 1000),
|
|
801
|
+
}),
|
|
802
|
+
appendSystemPromptOverride: () => [],
|
|
803
|
+
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
804
|
+
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
|
|
805
|
+
themesOverride: () => ({ themes: [], diagnostics: [] }),
|
|
806
|
+
agentsFilesOverride: () => ({ agentsFiles: [] }),
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
await resourceLoader.reload();
|
|
810
|
+
|
|
811
|
+
const thinking = resolveThinkingLevel(model, normalizeThinkingLevel(pi.getThinkingLevel()));
|
|
812
|
+
applyModelAttemptDetails(details, model, thinking);
|
|
813
|
+
|
|
814
|
+
const created = await createAgentSession({
|
|
815
|
+
cwd,
|
|
816
|
+
...getModelRuntimeOption(ctx),
|
|
817
|
+
resourceLoader,
|
|
818
|
+
settingsManager: isolatedSettingsManager,
|
|
819
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
820
|
+
model: model as CreateAgentSessionModel,
|
|
821
|
+
thinkingLevel: thinking.effective,
|
|
822
|
+
tools: [...BUILTIN_TOOL_NAMES, ...SUBAGENT_MCP_TOOLS],
|
|
823
|
+
});
|
|
824
|
+
session = created.session;
|
|
825
|
+
// Connect extensions (MCP servers) before prompting. Feature-detected: Pi >=0.99.0.
|
|
826
|
+
await (created.session as any).bindExtensions?.({ mode: "json" });
|
|
827
|
+
unsubscribe = session.subscribe((event) => {
|
|
828
|
+
addSessionEventUsage(usage, event);
|
|
829
|
+
switch (event.type) {
|
|
830
|
+
case "turn_end":
|
|
831
|
+
details.turns += 1;
|
|
832
|
+
emit();
|
|
833
|
+
break;
|
|
834
|
+
case "tool_execution_start":
|
|
835
|
+
details.toolCalls.push({
|
|
836
|
+
name: event.toolName,
|
|
837
|
+
args: event.args,
|
|
838
|
+
});
|
|
839
|
+
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
840
|
+
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
841
|
+
}
|
|
842
|
+
emit();
|
|
843
|
+
break;
|
|
844
|
+
case "message_update":
|
|
845
|
+
if (event.assistantMessageEvent?.type === "text_delta") {
|
|
846
|
+
lastContent += event.assistantMessageEvent.delta ?? "";
|
|
847
|
+
emit();
|
|
848
|
+
}
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
if (!callerAborted) {
|
|
854
|
+
const promptPromise = session.prompt(buildUserPrompt(input, cwd), {
|
|
855
|
+
expandPromptTemplates: false,
|
|
856
|
+
});
|
|
857
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
858
|
+
runTimeout = setTimeout(() => {
|
|
859
|
+
abortForCleanup();
|
|
860
|
+
reject(
|
|
861
|
+
new Error(`developer timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`),
|
|
862
|
+
);
|
|
863
|
+
}, MAX_RUN_MS);
|
|
864
|
+
});
|
|
865
|
+
await Promise.race([promptPromise, timeoutPromise]);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const outcome = inspectFinalAssistant(session.state.messages);
|
|
869
|
+
if (outcome.ok) {
|
|
870
|
+
lastContent = outcome.answer;
|
|
871
|
+
details.status = "done";
|
|
872
|
+
} else if (callerAborted) {
|
|
873
|
+
lastContent = "Aborted";
|
|
874
|
+
details.status = "aborted";
|
|
875
|
+
} else {
|
|
876
|
+
throw new Error(outcome.reason);
|
|
877
|
+
}
|
|
878
|
+
details.endedAt = Date.now();
|
|
879
|
+
const report = appendRunDetails(lastContent, details);
|
|
880
|
+
lastContent = report;
|
|
881
|
+
emit();
|
|
882
|
+
return { content: [{ type: "text", text: report }], details, usage };
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const failure = classifyRunFailure(error, callerAborted);
|
|
885
|
+
details.status = failure.status;
|
|
886
|
+
details.error = failure.error;
|
|
887
|
+
details.endedAt = Date.now();
|
|
888
|
+
lastContent = appendRunDetails(
|
|
889
|
+
`## Summary\n- Developer run failed before completing the task.\n\n## Files changed\n- unknown (the run may have modified files before failing; inspect the checkout)\n\n## Validation\n- none\n\n## Problems encountered\n- ${failure.message}`,
|
|
890
|
+
details,
|
|
891
|
+
);
|
|
892
|
+
emit();
|
|
893
|
+
return {
|
|
894
|
+
content: [{ type: "text", text: lastContent }],
|
|
895
|
+
details,
|
|
896
|
+
usage,
|
|
897
|
+
};
|
|
898
|
+
} finally {
|
|
899
|
+
if (runTimeout) clearTimeout(runTimeout);
|
|
900
|
+
if (signal && abortListenerAdded) signal.removeEventListener("abort", abortFromCaller);
|
|
901
|
+
unsubscribe?.();
|
|
902
|
+
await disposeSubagentSession(session);
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
}
|