pi-subagents 0.12.2 → 0.12.4
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/CHANGELOG.md +16 -0
- package/README.md +66 -6
- package/agent-management.ts +12 -5
- package/agent-manager-chain-detail.ts +2 -2
- package/agent-manager-detail.ts +9 -7
- package/agent-manager-edit.ts +4 -4
- package/agent-manager-list.ts +2 -2
- package/agent-manager-parallel.ts +3 -3
- package/agent-manager.ts +28 -14
- package/agent-scope.ts +1 -1
- package/agent-selection.ts +1 -1
- package/agent-serializer.ts +5 -1
- package/agent-templates.ts +1 -1
- package/agents.ts +31 -9
- package/artifacts.ts +1 -1
- package/async-execution.ts +28 -9
- package/chain-clarify.ts +39 -17
- package/chain-execution.ts +37 -12
- package/chain-serializer.ts +2 -2
- package/execution.ts +20 -11
- package/formatters.ts +3 -3
- package/index.ts +16 -16
- package/notify.ts +1 -1
- package/package.json +8 -1
- package/parallel-utils.ts +1 -0
- package/pi-spawn.ts +6 -9
- package/render.ts +7 -7
- package/schemas.ts +1 -1
- package/settings.ts +2 -2
- package/single-output.ts +50 -10
- package/skills.ts +5 -2
- package/subagent-executor.ts +55 -7
- package/subagent-runner.ts +32 -22
- package/types.ts +31 -5
- package/utils.ts +5 -1
- package/worktree.ts +240 -17
package/worktree.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface WorktreeInfo {
|
|
|
15
15
|
branch: string;
|
|
16
16
|
index: number;
|
|
17
17
|
nodeModulesLinked: boolean;
|
|
18
|
+
syntheticPaths: string[];
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface WorktreeDiff {
|
|
@@ -34,6 +35,37 @@ export interface WorktreeTaskCwdConflict {
|
|
|
34
35
|
cwd: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export interface WorktreeSetupHookConfig {
|
|
39
|
+
hookPath: string;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CreateWorktreesOptions {
|
|
44
|
+
agents?: string[];
|
|
45
|
+
setupHook?: WorktreeSetupHookConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ResolvedWorktreeSetupHook {
|
|
49
|
+
hookPath: string;
|
|
50
|
+
timeoutMs: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface WorktreeSetupHookInput {
|
|
54
|
+
version: 1;
|
|
55
|
+
repoRoot: string;
|
|
56
|
+
worktreePath: string;
|
|
57
|
+
agentCwd: string;
|
|
58
|
+
branch: string;
|
|
59
|
+
index: number;
|
|
60
|
+
runId: string;
|
|
61
|
+
baseCommit: string;
|
|
62
|
+
agent?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface WorktreeSetupHookOutput {
|
|
66
|
+
syntheticPaths?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
interface GitResult {
|
|
38
70
|
stdout: string;
|
|
39
71
|
stderr: string;
|
|
@@ -46,6 +78,8 @@ interface RepoState {
|
|
|
46
78
|
baseCommit: string;
|
|
47
79
|
}
|
|
48
80
|
|
|
81
|
+
const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
|
|
82
|
+
|
|
49
83
|
function runGit(cwd: string, args: string[]): GitResult {
|
|
50
84
|
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
51
85
|
return {
|
|
@@ -139,7 +173,140 @@ function linkNodeModulesIfPresent(toplevel: string, worktreePath: string): boole
|
|
|
139
173
|
}
|
|
140
174
|
}
|
|
141
175
|
|
|
142
|
-
function
|
|
176
|
+
function parseHookTimeout(timeoutMs: number | undefined): number {
|
|
177
|
+
if (timeoutMs === undefined) return DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS;
|
|
178
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
179
|
+
throw new Error("worktree setup hook timeout must be an integer greater than 0");
|
|
180
|
+
}
|
|
181
|
+
return timeoutMs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveWorktreeSetupHook(
|
|
185
|
+
repoRoot: string,
|
|
186
|
+
config: WorktreeSetupHookConfig | undefined,
|
|
187
|
+
): ResolvedWorktreeSetupHook | undefined {
|
|
188
|
+
if (!config) return undefined;
|
|
189
|
+
const hookPath = config.hookPath.trim();
|
|
190
|
+
if (!hookPath) {
|
|
191
|
+
throw new Error("worktree setup hook path cannot be empty");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const expandedHookPath = hookPath.startsWith("~/") ? path.join(os.homedir(), hookPath.slice(2)) : hookPath;
|
|
195
|
+
let resolvedPath: string;
|
|
196
|
+
if (path.isAbsolute(expandedHookPath)) {
|
|
197
|
+
resolvedPath = expandedHookPath;
|
|
198
|
+
} else if (expandedHookPath.includes("/") || expandedHookPath.includes("\\")) {
|
|
199
|
+
resolvedPath = path.resolve(repoRoot, expandedHookPath);
|
|
200
|
+
} else {
|
|
201
|
+
throw new Error("worktree setup hook must be an absolute path or a repo-relative path");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
205
|
+
throw new Error(`worktree setup hook not found: ${resolvedPath}`);
|
|
206
|
+
}
|
|
207
|
+
if (fs.statSync(resolvedPath).isDirectory()) {
|
|
208
|
+
throw new Error(`worktree setup hook must be a file, got directory: ${resolvedPath}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
hookPath: resolvedPath,
|
|
213
|
+
timeoutMs: parseHookTimeout(config.timeoutMs),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
|
|
218
|
+
const trimmed = rawPath.trim();
|
|
219
|
+
if (!trimmed) throw new Error("synthetic path cannot be empty");
|
|
220
|
+
if (path.isAbsolute(trimmed)) throw new Error(`synthetic path must be relative: ${rawPath}`);
|
|
221
|
+
|
|
222
|
+
const resolved = path.resolve(worktreePath, trimmed);
|
|
223
|
+
const relative = path.relative(worktreePath, resolved);
|
|
224
|
+
if (!relative || relative === ".") {
|
|
225
|
+
throw new Error(`synthetic path cannot target the worktree root: ${rawPath}`);
|
|
226
|
+
}
|
|
227
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
228
|
+
throw new Error(`synthetic path escapes the worktree root: ${rawPath}`);
|
|
229
|
+
}
|
|
230
|
+
return path.normalize(relative);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasTrackedEntries(worktreePath: string, relativePath: string): boolean {
|
|
234
|
+
const result = runGit(worktreePath, ["ls-files", "--", relativePath]);
|
|
235
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseWorktreeSetupHookOutput(rawStdout: string): WorktreeSetupHookOutput {
|
|
239
|
+
const trimmed = rawStdout.trim();
|
|
240
|
+
if (!trimmed) {
|
|
241
|
+
throw new Error("worktree setup hook returned empty stdout; expected JSON object");
|
|
242
|
+
}
|
|
243
|
+
let parsed: unknown;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(trimmed);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
248
|
+
throw new Error(`worktree setup hook returned invalid JSON: ${message}`);
|
|
249
|
+
}
|
|
250
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
251
|
+
throw new Error("worktree setup hook stdout must be a JSON object");
|
|
252
|
+
}
|
|
253
|
+
return parsed as WorktreeSetupHookOutput;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function runWorktreeSetupHook(
|
|
257
|
+
hook: ResolvedWorktreeSetupHook,
|
|
258
|
+
input: WorktreeSetupHookInput,
|
|
259
|
+
): string[] {
|
|
260
|
+
const result = spawnSync(hook.hookPath, [], {
|
|
261
|
+
cwd: input.worktreePath,
|
|
262
|
+
encoding: "utf-8",
|
|
263
|
+
input: JSON.stringify(input),
|
|
264
|
+
timeout: hook.timeoutMs,
|
|
265
|
+
shell: false,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (result.error) {
|
|
269
|
+
const code = "code" in result.error ? result.error.code : undefined;
|
|
270
|
+
if (code === "ETIMEDOUT") {
|
|
271
|
+
throw new Error(`worktree setup hook timed out after ${hook.timeoutMs}ms`);
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`worktree setup hook failed: ${result.error.message}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (result.status !== 0) {
|
|
277
|
+
const details = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
278
|
+
throw new Error(`worktree setup hook failed with exit code ${result.status}: ${details}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const output = parseWorktreeSetupHookOutput(result.stdout);
|
|
282
|
+
if (output.syntheticPaths === undefined) return [];
|
|
283
|
+
if (!Array.isArray(output.syntheticPaths)) {
|
|
284
|
+
throw new Error("worktree setup hook output field 'syntheticPaths' must be an array of relative paths");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const uniquePaths = new Set<string>();
|
|
288
|
+
for (const candidate of output.syntheticPaths) {
|
|
289
|
+
if (typeof candidate !== "string") {
|
|
290
|
+
throw new Error("worktree setup hook output field 'syntheticPaths' must contain only strings");
|
|
291
|
+
}
|
|
292
|
+
const normalizedPath = normalizeSyntheticPath(input.worktreePath, candidate);
|
|
293
|
+
if (hasTrackedEntries(input.worktreePath, normalizedPath)) {
|
|
294
|
+
throw new Error(`worktree setup hook cannot mark tracked paths as synthetic: ${normalizedPath}`);
|
|
295
|
+
}
|
|
296
|
+
uniquePaths.add(normalizedPath);
|
|
297
|
+
}
|
|
298
|
+
return [...uniquePaths];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createSingleWorktree(
|
|
302
|
+
toplevel: string,
|
|
303
|
+
cwdRelative: string,
|
|
304
|
+
runId: string,
|
|
305
|
+
index: number,
|
|
306
|
+
baseCommit: string,
|
|
307
|
+
setupHook: ResolvedWorktreeSetupHook | undefined,
|
|
308
|
+
agent: string | undefined,
|
|
309
|
+
): WorktreeInfo {
|
|
143
310
|
const branch = buildWorktreeBranch(runId, index);
|
|
144
311
|
const worktreePath = buildWorktreePath(runId, index);
|
|
145
312
|
const add = runGit(toplevel, ["worktree", "add", worktreePath, "-b", branch, "HEAD"]);
|
|
@@ -148,29 +315,76 @@ function createSingleWorktree(toplevel: string, cwdRelative: string, runId: stri
|
|
|
148
315
|
throw new Error(message);
|
|
149
316
|
}
|
|
150
317
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
318
|
+
const agentCwd = cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath;
|
|
319
|
+
try {
|
|
320
|
+
const nodeModulesLinked = linkNodeModulesIfPresent(toplevel, worktreePath);
|
|
321
|
+
const syntheticPaths = nodeModulesLinked ? ["node_modules"] : [];
|
|
322
|
+
|
|
323
|
+
if (setupHook) {
|
|
324
|
+
const hookSyntheticPaths = runWorktreeSetupHook(setupHook, {
|
|
325
|
+
version: 1,
|
|
326
|
+
repoRoot: toplevel,
|
|
327
|
+
worktreePath,
|
|
328
|
+
agentCwd,
|
|
329
|
+
branch,
|
|
330
|
+
index,
|
|
331
|
+
runId,
|
|
332
|
+
baseCommit,
|
|
333
|
+
agent,
|
|
334
|
+
});
|
|
335
|
+
syntheticPaths.push(...hookSyntheticPaths);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
path: worktreePath,
|
|
340
|
+
agentCwd,
|
|
341
|
+
branch,
|
|
342
|
+
index,
|
|
343
|
+
nodeModulesLinked,
|
|
344
|
+
syntheticPaths,
|
|
345
|
+
};
|
|
346
|
+
} catch (error) {
|
|
347
|
+
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {}
|
|
348
|
+
try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {}
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
158
351
|
}
|
|
159
352
|
|
|
160
|
-
function
|
|
161
|
-
|
|
353
|
+
function removeSyntheticPath(worktree: WorktreeInfo, syntheticPath: string): void {
|
|
354
|
+
const resolved = path.resolve(worktree.path, syntheticPath);
|
|
355
|
+
const relative = path.relative(worktree.path, resolved);
|
|
356
|
+
if (!relative || relative === "." || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
162
359
|
|
|
163
|
-
const nodeModulesPath = path.join(worktree.path, "node_modules");
|
|
164
360
|
let stat: fs.Stats;
|
|
165
361
|
try {
|
|
166
|
-
stat = fs.lstatSync(
|
|
362
|
+
stat = fs.lstatSync(resolved);
|
|
167
363
|
} catch (error) {
|
|
168
364
|
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
169
365
|
if (code === "ENOENT") return;
|
|
170
366
|
throw error;
|
|
171
367
|
}
|
|
172
|
-
|
|
173
|
-
|
|
368
|
+
|
|
369
|
+
if (stat.isSymbolicLink()) {
|
|
370
|
+
fs.unlinkSync(resolved);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (stat.isDirectory()) {
|
|
374
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
fs.rmSync(resolved, { force: true });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function removeSyntheticPathsBeforeDiff(worktree: WorktreeInfo): void {
|
|
381
|
+
if (worktree.syntheticPaths.length === 0) return;
|
|
382
|
+
const seen = new Set<string>();
|
|
383
|
+
for (const syntheticPath of worktree.syntheticPaths) {
|
|
384
|
+
if (seen.has(syntheticPath)) continue;
|
|
385
|
+
seen.add(syntheticPath);
|
|
386
|
+
removeSyntheticPath(worktree, syntheticPath);
|
|
387
|
+
}
|
|
174
388
|
}
|
|
175
389
|
|
|
176
390
|
function emptyDiff(index: number, agent: string, branch: string, patchPath: string): WorktreeDiff {
|
|
@@ -212,7 +426,7 @@ function captureWorktreeDiff(
|
|
|
212
426
|
agent: string,
|
|
213
427
|
patchPath: string,
|
|
214
428
|
): WorktreeDiff {
|
|
215
|
-
|
|
429
|
+
removeSyntheticPathsBeforeDiff(worktree);
|
|
216
430
|
runGitChecked(worktree.path, ["add", "-A"]);
|
|
217
431
|
const diffStat = runGitChecked(worktree.path, ["diff", "--cached", "--stat", setup.baseCommit]).trim();
|
|
218
432
|
const patch = runGitChecked(worktree.path, ["diff", "--cached", setup.baseCommit]);
|
|
@@ -251,13 +465,22 @@ function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
|
251
465
|
return diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0;
|
|
252
466
|
}
|
|
253
467
|
|
|
254
|
-
export function createWorktrees(cwd: string, runId: string, count: number): WorktreeSetup {
|
|
468
|
+
export function createWorktrees(cwd: string, runId: string, count: number, options?: CreateWorktreesOptions): WorktreeSetup {
|
|
255
469
|
const repo = resolveRepoState(cwd);
|
|
470
|
+
const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
|
|
256
471
|
const worktrees: WorktreeInfo[] = [];
|
|
257
472
|
|
|
258
473
|
try {
|
|
259
474
|
for (let index = 0; index < count; index++) {
|
|
260
|
-
worktrees.push(createSingleWorktree(
|
|
475
|
+
worktrees.push(createSingleWorktree(
|
|
476
|
+
repo.toplevel,
|
|
477
|
+
repo.cwdRelative,
|
|
478
|
+
runId,
|
|
479
|
+
index,
|
|
480
|
+
repo.baseCommit,
|
|
481
|
+
setupHook,
|
|
482
|
+
options?.agents?.[index],
|
|
483
|
+
));
|
|
261
484
|
}
|
|
262
485
|
} catch (error) {
|
|
263
486
|
cleanupWorktrees({
|