pi-cohort 7.4.5 → 7.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.5.0] - 2026-10-26
4
+
5
+ ### Added
6
+ - `worktreeTeardownHook` and `worktreeTeardownHookTimeoutMs` let consumers clean up external resources before task checkout removal, including setup rollback; failures produce best-effort warnings without changing task results. (#27)
7
+
8
+ ### Fixed
9
+ - The Tests workflow passes on Windows again: the fanout extension-inheritance integration test hands its preload to `--import` as a `file://` URL instead of a bare drive-letter path.
10
+
3
11
  ## [7.4.5] - 2026-10-23
4
12
 
5
13
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "7.4.5",
3
+ "version": "7.5.0",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -80,6 +80,8 @@ interface AsyncChainParams {
80
80
  maxSubagentDepth: number;
81
81
  worktreeSetupHook?: string;
82
82
  worktreeSetupHookTimeoutMs?: number;
83
+ worktreeTeardownHook?: string;
84
+ worktreeTeardownHookTimeoutMs?: number;
83
85
  controlConfig?: ResolvedControlConfig;
84
86
  nestedRoute?: NestedRouteInfo;
85
87
  forwardedFlags?: string[];
@@ -115,6 +117,8 @@ interface AsyncSingleParams {
115
117
  maxSubagentDepth: number;
116
118
  worktreeSetupHook?: string;
117
119
  worktreeSetupHookTimeoutMs?: number;
120
+ worktreeTeardownHook?: string;
121
+ worktreeTeardownHookTimeoutMs?: number;
118
122
  controlConfig?: ResolvedControlConfig;
119
123
  nestedRoute?: NestedRouteInfo;
120
124
  forwardedFlags?: string[];
@@ -376,6 +380,8 @@ export function executeAsyncChain(
376
380
  maxSubagentDepth,
377
381
  worktreeSetupHook,
378
382
  worktreeSetupHookTimeoutMs,
383
+ worktreeTeardownHook,
384
+ worktreeTeardownHookTimeoutMs,
379
385
  controlConfig,
380
386
  nestedRoute,
381
387
  forwardedFlags,
@@ -630,6 +636,8 @@ export function executeAsyncChain(
630
636
  forwardedFlags,
631
637
  worktreeSetupHook,
632
638
  worktreeSetupHookTimeoutMs,
639
+ worktreeTeardownHook,
640
+ worktreeTeardownHookTimeoutMs,
633
641
  controlConfig,
634
642
  resultMode,
635
643
  executionBackends: detachedExecutionBackends(ctx.executionBackend),
@@ -748,6 +756,8 @@ export function executeAsyncSingle(
748
756
  maxSubagentDepth,
749
757
  worktreeSetupHook,
750
758
  worktreeSetupHookTimeoutMs,
759
+ worktreeTeardownHook,
760
+ worktreeTeardownHookTimeoutMs,
751
761
  controlConfig,
752
762
  nestedRoute,
753
763
  forwardedFlags,
@@ -840,6 +850,8 @@ export function executeAsyncSingle(
840
850
  forwardedFlags,
841
851
  worktreeSetupHook,
842
852
  worktreeSetupHookTimeoutMs,
853
+ worktreeTeardownHook,
854
+ worktreeTeardownHookTimeoutMs,
843
855
  controlConfig,
844
856
  resultMode: "single",
845
857
  executionBackends: detachedExecutionBackends(ctx.executionBackend),
@@ -120,6 +120,8 @@ interface SubagentRunConfig {
120
120
  forwardedFlags?: string[];
121
121
  worktreeSetupHook?: string;
122
122
  worktreeSetupHookTimeoutMs?: number;
123
+ worktreeTeardownHook?: string;
124
+ worktreeTeardownHookTimeoutMs?: number;
123
125
  controlConfig?: ResolvedControlConfig;
124
126
  resultMode?: SubagentRunMode;
125
127
  dynamicFanoutMaxItems?: number;
@@ -2133,6 +2135,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2133
2135
  setupHook: config.worktreeSetupHook
2134
2136
  ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
2135
2137
  : undefined,
2138
+ teardownHook: config.worktreeTeardownHook
2139
+ ? { hookPath: config.worktreeTeardownHook, timeoutMs: config.worktreeTeardownHookTimeoutMs }
2140
+ : undefined,
2136
2141
  });
2137
2142
  } catch (error) {
2138
2143
  const setupError = error instanceof Error ? error.message : String(error);
@@ -420,6 +420,8 @@ interface ChainExecutionParams {
420
420
  nestedRoute?: NestedRouteInfo;
421
421
  worktreeSetupHook?: string;
422
422
  worktreeSetupHookTimeoutMs?: number;
423
+ worktreeTeardownHook?: string;
424
+ worktreeTeardownHookTimeoutMs?: number;
423
425
  forwardedFlags?: string[];
424
426
  }
425
427
 
@@ -660,6 +662,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
660
662
  setupHook: params.worktreeSetupHook
661
663
  ? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
662
664
  : undefined,
665
+ teardownHook: params.worktreeTeardownHook
666
+ ? { hookPath: params.worktreeTeardownHook, timeoutMs: params.worktreeTeardownHookTimeoutMs }
667
+ : undefined,
663
668
  });
664
669
  } catch (error) {
665
670
  const message = error instanceof Error ? error.message : String(error);
@@ -665,6 +665,8 @@ async function resumeAsyncRun(input: {
665
665
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
666
666
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
667
667
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
668
+ worktreeTeardownHook: input.deps.config.worktreeTeardownHook,
669
+ worktreeTeardownHookTimeoutMs: input.deps.config.worktreeTeardownHookTimeoutMs,
668
670
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
669
671
  availableModels,
670
672
  forwardedFlags: input.deps.forwardedFlags,
@@ -1073,6 +1075,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1073
1075
  maxSubagentDepth: currentMaxSubagentDepth,
1074
1076
  worktreeSetupHook: deps.config.worktreeSetupHook,
1075
1077
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1078
+ worktreeTeardownHook: deps.config.worktreeTeardownHook,
1079
+ worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
1076
1080
  controlConfig,
1077
1081
  nestedRoute,
1078
1082
  forwardedFlags: deps.forwardedFlags,
@@ -1102,6 +1106,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1102
1106
  maxSubagentDepth: currentMaxSubagentDepth,
1103
1107
  worktreeSetupHook: deps.config.worktreeSetupHook,
1104
1108
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1109
+ worktreeTeardownHook: deps.config.worktreeTeardownHook,
1110
+ worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
1105
1111
  controlConfig,
1106
1112
  nestedRoute,
1107
1113
  forwardedFlags: deps.forwardedFlags,
@@ -1144,6 +1150,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1144
1150
  maxSubagentDepth,
1145
1151
  worktreeSetupHook: deps.config.worktreeSetupHook,
1146
1152
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1153
+ worktreeTeardownHook: deps.config.worktreeTeardownHook,
1154
+ worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
1147
1155
  controlConfig,
1148
1156
  nestedRoute,
1149
1157
  acceptance: params.acceptance,
@@ -1204,6 +1212,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1204
1212
  maxSubagentDepth: currentMaxSubagentDepth,
1205
1213
  worktreeSetupHook: deps.config.worktreeSetupHook,
1206
1214
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1215
+ worktreeTeardownHook: deps.config.worktreeTeardownHook,
1216
+ worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
1207
1217
  forwardedFlags: deps.forwardedFlags,
1208
1218
  });
1209
1219
 
@@ -1237,6 +1247,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1237
1247
  maxSubagentDepth: currentMaxSubagentDepth,
1238
1248
  worktreeSetupHook: deps.config.worktreeSetupHook,
1239
1249
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1250
+ worktreeTeardownHook: deps.config.worktreeTeardownHook,
1251
+ worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
1240
1252
  controlConfig,
1241
1253
  nestedRoute: data.nestedRoute,
1242
1254
  forwardedFlags: deps.forwardedFlags,
@@ -1299,6 +1311,8 @@ function createParallelWorktreeSetup(
1299
1311
  tasks: TaskParam[],
1300
1312
  setupHook: ExtensionConfig["worktreeSetupHook"],
1301
1313
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
1314
+ teardownHook: ExtensionConfig["worktreeTeardownHook"],
1315
+ teardownHookTimeoutMs: ExtensionConfig["worktreeTeardownHookTimeoutMs"],
1302
1316
  ): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
1303
1317
  if (!enabled) return {};
1304
1318
  try {
@@ -1308,6 +1322,9 @@ function createParallelWorktreeSetup(
1308
1322
  setupHook: setupHook
1309
1323
  ? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs }
1310
1324
  : undefined,
1325
+ teardownHook: teardownHook
1326
+ ? { hookPath: teardownHook, timeoutMs: teardownHookTimeoutMs }
1327
+ : undefined,
1311
1328
  }),
1312
1329
  };
1313
1330
  } catch (error) {
@@ -1691,6 +1708,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1691
1708
  maxSubagentDepth: currentMaxSubagentDepth,
1692
1709
  worktreeSetupHook: deps.config.worktreeSetupHook,
1693
1710
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1711
+ worktreeTeardownHook: deps.config.worktreeTeardownHook,
1712
+ worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
1694
1713
  controlConfig,
1695
1714
  forwardedFlags: deps.forwardedFlags,
1696
1715
  });
@@ -1709,6 +1728,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1709
1728
  tasks,
1710
1729
  deps.config.worktreeSetupHook,
1711
1730
  deps.config.worktreeSetupHookTimeoutMs,
1731
+ deps.config.worktreeTeardownHook,
1732
+ deps.config.worktreeTeardownHookTimeoutMs,
1712
1733
  );
1713
1734
  if (errorResult) return errorResult;
1714
1735
 
@@ -1952,6 +1973,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1952
1973
  maxSubagentDepth,
1953
1974
  worktreeSetupHook: deps.config.worktreeSetupHook,
1954
1975
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1976
+ worktreeTeardownHook: deps.config.worktreeTeardownHook,
1977
+ worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
1955
1978
  controlConfig,
1956
1979
  forwardedFlags: deps.forwardedFlags,
1957
1980
  });
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import * as fs from "node:fs";
2
+ import fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
 
@@ -10,6 +10,8 @@ export interface WorktreeSetup {
10
10
  }
11
11
 
12
12
  interface WorktreeInfo {
13
+ hookInput: WorktreeSetupHookInput;
14
+ teardownHook?: ResolvedWorktreeTeardownHook;
13
15
  path: string;
14
16
  agentCwd: string;
15
17
  branch: string;
@@ -41,7 +43,18 @@ interface WorktreeSetupHookConfig {
41
43
  timeoutMs?: number;
42
44
  }
43
45
 
46
+ interface WorktreeTeardownHookConfig {
47
+ hookPath: string;
48
+ timeoutMs?: number;
49
+ }
50
+
51
+ interface ResolvedWorktreeTeardownHook {
52
+ hookPath: string;
53
+ timeoutMs: number;
54
+ }
55
+
44
56
  interface CreateWorktreesOptions {
57
+ teardownHook?: WorktreeTeardownHookConfig;
45
58
  agents?: string[];
46
59
  setupHook?: WorktreeSetupHookConfig;
47
60
  }
@@ -229,6 +242,43 @@ function resolveWorktreeSetupHook(
229
242
  };
230
243
  }
231
244
 
245
+ function resolveWorktreeTeardownHook(
246
+ repoRoot: string,
247
+ config: WorktreeTeardownHookConfig | undefined,
248
+ ): ResolvedWorktreeTeardownHook | undefined {
249
+ if (!config) return undefined;
250
+ const hookPath = config.hookPath.trim();
251
+ if (!hookPath) {
252
+ throw new Error("worktree teardown hook path cannot be empty");
253
+ }
254
+
255
+ const expandedHookPath = hookPath.startsWith("~/") ? path.join(os.homedir(), hookPath.slice(2)) : hookPath;
256
+ let resolvedPath: string;
257
+ if (path.isAbsolute(expandedHookPath)) {
258
+ resolvedPath = expandedHookPath;
259
+ } else if (expandedHookPath.includes("/") || expandedHookPath.includes("\\")) {
260
+ resolvedPath = path.resolve(repoRoot, expandedHookPath);
261
+ } else {
262
+ throw new Error("worktree teardown hook must be an absolute path or a repo-relative path");
263
+ }
264
+
265
+ if (!fs.existsSync(resolvedPath)) {
266
+ throw new Error(`worktree teardown hook not found: ${resolvedPath}`);
267
+ }
268
+ if (fs.statSync(resolvedPath).isDirectory()) {
269
+ throw new Error(`worktree teardown hook must be a file, got directory: ${resolvedPath}`);
270
+ }
271
+
272
+ const timeoutMs = config.timeoutMs === undefined ? 30000 : config.timeoutMs;
273
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
274
+ throw new Error("worktree teardown hook timeout must be an integer greater than 0");
275
+ }
276
+ return {
277
+ hookPath: resolvedPath,
278
+ timeoutMs: timeoutMs,
279
+ };
280
+ }
281
+
232
282
  function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
233
283
  const trimmed = rawPath.trim();
234
284
  if (!trimmed) throw new Error("synthetic path cannot be empty");
@@ -313,6 +363,40 @@ function runWorktreeSetupHook(
313
363
  return [...uniquePaths];
314
364
  }
315
365
 
366
+ function warnWorktreeCleanup(message: string): void {
367
+ try { fs.write(2, message + "\n", () => {}); } catch {}
368
+ }
369
+
370
+ function runWorktreeTeardownHook(
371
+ hook: ResolvedWorktreeTeardownHook | undefined,
372
+ input: WorktreeSetupHookInput,
373
+ ): void {
374
+ if (!hook) return;
375
+ try {
376
+ const result = spawnSync(hook.hookPath, [], {
377
+ cwd: input.worktreePath,
378
+ encoding: "utf-8",
379
+ input: JSON.stringify(input),
380
+ timeout: hook.timeoutMs,
381
+ killSignal: "SIGKILL",
382
+ shell: false,
383
+ stdio: ["pipe", "ignore", "pipe"],
384
+ });
385
+ let cause: string;
386
+ if (result.error) {
387
+ cause = "code" in result.error && result.error.code === "ETIMEDOUT"
388
+ ? `timed out after ${hook.timeoutMs}ms`
389
+ : result.error.message;
390
+ } else if (result.status !== 0) {
391
+ cause = result.signal ? `signal ${result.signal}` : `exit code ${result.status}`;
392
+ } else return;
393
+ const stderr = result.stderr?.trim();
394
+ warnWorktreeCleanup(`worktree teardown hook failed for '${input.worktreePath}': ${cause}${stderr ? `: ${stderr}` : ""}`);
395
+ } catch (error) {
396
+ warnWorktreeCleanup(`worktree teardown hook failed for '${input.worktreePath}': ${error instanceof Error ? error.message : String(error)}`);
397
+ }
398
+ }
399
+
316
400
  function createSingleWorktree(
317
401
  toplevel: string,
318
402
  cwdRelative: string,
@@ -320,6 +404,7 @@ function createSingleWorktree(
320
404
  index: number,
321
405
  baseCommit: string,
322
406
  setupHook: ResolvedWorktreeSetupHook | undefined,
407
+ teardownHook: ResolvedWorktreeTeardownHook | undefined,
323
408
  agent: string | undefined,
324
409
  ): WorktreeInfo {
325
410
  const branch = buildWorktreeBranch(runId, index);
@@ -331,27 +416,22 @@ function createSingleWorktree(
331
416
  }
332
417
 
333
418
  const agentCwd = cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath;
419
+ const hookInput: WorktreeSetupHookInput = {
420
+ version: 1, repoRoot: toplevel, worktreePath, agentCwd, branch, index, runId, baseCommit, agent,
421
+ };
334
422
  try {
335
423
  const nodeModulesLinked = linkNodeModulesIfPresent(toplevel, worktreePath);
336
424
  const syntheticPaths = nodeModulesLinked ? ["node_modules"] : [];
337
425
 
338
426
  if (setupHook) {
339
- const hookSyntheticPaths = runWorktreeSetupHook(setupHook, {
340
- version: 1,
341
- repoRoot: toplevel,
342
- worktreePath,
343
- agentCwd,
344
- branch,
345
- index,
346
- runId,
347
- baseCommit,
348
- agent,
349
- });
427
+ const hookSyntheticPaths = runWorktreeSetupHook(setupHook, hookInput);
350
428
  syntheticPaths.push(...hookSyntheticPaths);
351
429
  }
352
430
 
353
431
  return {
354
432
  path: worktreePath,
433
+ hookInput,
434
+ teardownHook,
355
435
  agentCwd,
356
436
  branch,
357
437
  index,
@@ -359,11 +439,12 @@ function createSingleWorktree(
359
439
  syntheticPaths,
360
440
  };
361
441
  } catch (error) {
362
- try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
363
- // Best-effort rollback; preserve the original setup failure.
442
+ runWorktreeTeardownHook(teardownHook, hookInput);
443
+ try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch (cleanupError) {
444
+ warnWorktreeCleanup(`worktree remove failed for '${worktreePath}': ${cleanupError}`);
364
445
  }
365
- try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {
366
- // Best-effort rollback; preserve the original setup failure.
446
+ try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch (cleanupError) {
447
+ warnWorktreeCleanup(`branch -D '${branch}' failed for '${worktreePath}': ${cleanupError}`);
367
448
  }
368
449
  throw error;
369
450
  }
@@ -477,11 +558,12 @@ function writeEmptyPatch(patchPath: string): void {
477
558
  }
478
559
 
479
560
  function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
480
- try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
481
- // Cleanup is best-effort to avoid masking caller errors.
561
+ runWorktreeTeardownHook(worktree.teardownHook, worktree.hookInput);
562
+ try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch (error) {
563
+ warnWorktreeCleanup(`worktree remove failed for '${worktree.path}': ${error}`);
482
564
  }
483
- try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {
484
- // Cleanup is best-effort to avoid masking caller errors.
565
+ try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch (error) {
566
+ warnWorktreeCleanup(`branch -D '${worktree.branch}' failed for '${worktree.path}': ${error}`);
485
567
  }
486
568
  }
487
569
 
@@ -498,6 +580,7 @@ export function patchPathForDiff(diff: WorktreeDiff): string | undefined {
498
580
  export function createWorktrees(cwd: string, runId: string, count: number, options?: CreateWorktreesOptions): WorktreeSetup {
499
581
  const repo = resolveRepoState(cwd);
500
582
  const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
583
+ const teardownHook = resolveWorktreeTeardownHook(repo.toplevel, options?.teardownHook);
501
584
  const worktrees: WorktreeInfo[] = [];
502
585
 
503
586
  try {
@@ -509,6 +592,7 @@ export function createWorktrees(cwd: string, runId: string, count: number, optio
509
592
  index,
510
593
  repo.baseCommit,
511
594
  setupHook,
595
+ teardownHook,
512
596
  options?.agents?.[index],
513
597
  ));
514
598
  }
@@ -869,6 +869,8 @@ export interface ExtensionConfig {
869
869
  chain?: ExtensionChainConfig;
870
870
  worktreeSetupHook?: string;
871
871
  worktreeSetupHookTimeoutMs?: number;
872
+ worktreeTeardownHook?: string;
873
+ worktreeTeardownHookTimeoutMs?: number;
872
874
  /** Print a roster of discovered subagents/chains into chat at session start. Default: true. */
873
875
  showRosterOnStart?: boolean;
874
876
  /** Forward the parent pi's extension CLI flags (e.g. --no-autofix) into spawned children. Default: true. */