pi-cohort 7.4.5 → 7.6.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 +13 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +50 -12
- package/src/execution-backend/configured-selection.ts +1 -3
- package/src/extension/fanout-child.ts +2 -7
- package/src/extension/index.ts +21 -13
- package/src/runs/background/async-execution.ts +12 -0
- package/src/runs/background/subagent-runner.ts +49 -6
- package/src/runs/foreground/chain-execution.ts +19 -5
- package/src/runs/foreground/subagent-executor.ts +47 -19
- package/src/runs/shared/chain-outputs.ts +0 -3
- package/src/runs/shared/worktree.ts +104 -20
- package/src/shared/types.ts +3 -0
- package/src/extension/config.ts +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.6.0] - 2026-10-26
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
- All extension settings now come only from `settings.json#subagents`: the profile is below project levels, and the nearest defined value wins. Move the legacy configuration object's keys under `subagents` in the profile's `settings.json` or project `.pi/settings.json`; the old file is ignored and triggers one top-level session-start migration warning. Hooks and backend selection follow each execution unit's effective cwd, including detached runs. (#29)
|
|
7
|
+
|
|
8
|
+
## [7.5.0] - 2026-10-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `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)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
3
16
|
## [7.4.5] - 2026-10-23
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ Full reference: agent/chain authoring in [doc/agents-and-chains.md](doc/agents-a
|
|
|
186
186
|
|
|
187
187
|
## Configuration
|
|
188
188
|
|
|
189
|
-
Most installs need zero configuration.
|
|
189
|
+
Most installs need zero configuration. Set options under `subagents` in the profile's `settings.json` or project `.pi/settings.json`; project levels override the profile, nearest wins. See the [settings reference and migration guide](doc/configuration.md) for defaults, whole-object replacement, and moving legacy configuration.
|
|
190
190
|
|
|
191
191
|
Run history lives under the pinned profile's `pi-cohort/` root and has no automatic age expiry. Status listings can grow without bound as history accumulates. To remove history manually, delete only a terminal top-level run directory after confirming its entire subtree has stopped; never delete queued or running state, individual nested records, or shared route files. This removes that directory's status and resume lookup, not separately stored session files or shared routes. Keep those sessions if you need their context; deleting a run directory is not a complete session purge. Storage details: [doc/observability.md](doc/observability.md#files-logs-and-observability).
|
|
192
192
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cohort",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.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",
|
package/src/agents/agents.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import type { AcceptanceInput, OutputMode, StopCondition } from "../shared/types.ts";
|
|
9
|
+
import type { AcceptanceInput, ExtensionConfig, OutputMode, StopCondition } from "../shared/types.ts";
|
|
10
10
|
import { getAgentDir } from "../shared/utils.ts";
|
|
11
11
|
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
12
12
|
import { parseChain, parseJsonChain } from "./chain-serializer.ts";
|
|
@@ -114,12 +114,29 @@ export interface AgentConfig {
|
|
|
114
114
|
override?: BuiltinAgentOverrideInfo;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
interface SubagentSettings {
|
|
117
|
+
interface SubagentSettings extends ExtensionConfig {
|
|
118
118
|
overrides: Record<string, BuiltinAgentOverrideConfig>;
|
|
119
119
|
disableBuiltins?: boolean;
|
|
120
120
|
executionBackend?: string;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
const EXTENSION_SETTING_TYPES = {
|
|
124
|
+
asyncByDefault: "boolean",
|
|
125
|
+
forceTopLevelAsync: "boolean",
|
|
126
|
+
defaultSessionDir: "string",
|
|
127
|
+
maxSubagentDepth: "depth",
|
|
128
|
+
control: "object",
|
|
129
|
+
parallel: "object",
|
|
130
|
+
chain: "object",
|
|
131
|
+
worktreeSetupHook: "string",
|
|
132
|
+
worktreeSetupHookTimeoutMs: "timeout",
|
|
133
|
+
worktreeTeardownHook: "string",
|
|
134
|
+
worktreeTeardownHookTimeoutMs: "timeout",
|
|
135
|
+
showRosterOnStart: "boolean",
|
|
136
|
+
forwardParentFlags: "boolean",
|
|
137
|
+
executionBackend: "backend",
|
|
138
|
+
} as const;
|
|
139
|
+
|
|
123
140
|
const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
|
|
124
141
|
|
|
125
142
|
export interface ChainStepConfig {
|
|
@@ -475,16 +492,28 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
|
|
|
475
492
|
executionBackend = trimmed;
|
|
476
493
|
}
|
|
477
494
|
|
|
495
|
+
const extension: Record<string, unknown> = {};
|
|
496
|
+
for (const [key, type] of Object.entries(EXTENSION_SETTING_TYPES)) {
|
|
497
|
+
if (type === "backend" || !(key in subagentsObject)) continue;
|
|
498
|
+
const value = subagentsObject[key];
|
|
499
|
+
const valid = type === "timeout"
|
|
500
|
+
|| (type === "depth" ? typeof value === "number" && Number.isInteger(value) && value >= 0
|
|
501
|
+
: type === "object" ? value !== null && typeof value === "object" && !Array.isArray(value)
|
|
502
|
+
: typeof value === type);
|
|
503
|
+
if (valid) extension[key] = value;
|
|
504
|
+
else console.warn(`Subagent settings in '${filePath}' have invalid '${key}'; expected ${type}.`);
|
|
505
|
+
}
|
|
506
|
+
|
|
478
507
|
const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
|
|
479
508
|
const agentOverrides = subagentsObject.agentOverrides;
|
|
480
509
|
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
|
|
481
|
-
return { overrides: parsed, disableBuiltins, executionBackend };
|
|
510
|
+
return { ...extension, overrides: parsed, disableBuiltins, executionBackend };
|
|
482
511
|
}
|
|
483
512
|
for (const [name, value] of Object.entries(agentOverrides)) {
|
|
484
513
|
const override = parseBuiltinOverrideEntry(name, value, filePath);
|
|
485
514
|
if (override) parsed[name] = override;
|
|
486
515
|
}
|
|
487
|
-
return { overrides: parsed, disableBuiltins, executionBackend };
|
|
516
|
+
return { ...extension, overrides: parsed, disableBuiltins, executionBackend };
|
|
488
517
|
}
|
|
489
518
|
|
|
490
519
|
// Merge project .pi/settings.json across every enumerated level, farthest-first
|
|
@@ -498,7 +527,7 @@ function readMergedProjectSubagentSettings(cwd: string): SubagentSettings {
|
|
|
498
527
|
|
|
499
528
|
const overrides: Record<string, BuiltinAgentOverrideConfig> = {};
|
|
500
529
|
let disableBuiltins: boolean | undefined;
|
|
501
|
-
|
|
530
|
+
const extension: ExtensionConfig = {};
|
|
502
531
|
for (const level of levels) {
|
|
503
532
|
const settingsPath = path.join(level, ".pi", "settings.json");
|
|
504
533
|
let levelSettings: SubagentSettings;
|
|
@@ -519,16 +548,25 @@ function readMergedProjectSubagentSettings(cwd: string): SubagentSettings {
|
|
|
519
548
|
if (levelSettings.disableBuiltins !== undefined) {
|
|
520
549
|
disableBuiltins = levelSettings.disableBuiltins;
|
|
521
550
|
}
|
|
522
|
-
|
|
523
|
-
executionBackend = levelSettings.executionBackend;
|
|
524
|
-
}
|
|
551
|
+
Object.assign(extension, projectExtensionSettings(levelSettings));
|
|
525
552
|
}
|
|
526
|
-
return { overrides, disableBuiltins
|
|
553
|
+
return { ...extension, overrides, disableBuiltins };
|
|
527
554
|
}
|
|
528
555
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
556
|
+
function projectExtensionSettings(settings: SubagentSettings): ExtensionConfig {
|
|
557
|
+
return Object.fromEntries(
|
|
558
|
+
Object.keys(EXTENSION_SETTING_TYPES)
|
|
559
|
+
.filter((key) => settings[key as keyof ExtensionConfig] !== undefined)
|
|
560
|
+
.map((key) => [key, settings[key as keyof ExtensionConfig]]),
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function resolveExtensionConfig(cwd: string): ExtensionConfig {
|
|
565
|
+
const profile = readSubagentSettings(getUserAgentSettingsPath());
|
|
566
|
+
return {
|
|
567
|
+
...projectExtensionSettings(profile),
|
|
568
|
+
...projectExtensionSettings(readMergedProjectSubagentSettings(cwd)),
|
|
569
|
+
};
|
|
532
570
|
}
|
|
533
571
|
|
|
534
572
|
function composeOverrideTools(
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { selectExecutionBackend } from "./selection.ts";
|
|
2
|
-
import { readProjectExecutionBackend } from "../agents/agents.ts";
|
|
3
2
|
import type { ExtensionConfig } from "../shared/types.ts";
|
|
4
3
|
import type { ExecutionBackendPreparation, ExecutionBackendSelectionResult } from "./selection.ts";
|
|
5
4
|
|
|
@@ -13,7 +12,6 @@ export interface ConfiguredSelectionInput {
|
|
|
13
12
|
export async function selectConfiguredExecutionBackend(
|
|
14
13
|
input: ConfiguredSelectionInput,
|
|
15
14
|
): Promise<ExecutionBackendSelectionResult> {
|
|
16
|
-
const
|
|
17
|
-
const preference = projectPreference ?? input.userConfig.executionBackend ?? "auto";
|
|
15
|
+
const preference = input.userConfig.executionBackend ?? "auto";
|
|
18
16
|
return selectExecutionBackend(preference, input.preparation);
|
|
19
17
|
}
|
|
@@ -2,14 +2,12 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { discoverAgents } from "../agents/agents.ts";
|
|
5
|
+
import { resolveExtensionConfig, discoverAgents } from "../agents/agents.ts";
|
|
6
6
|
import { getArtifactsDir } from "../shared/artifacts.ts";
|
|
7
7
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
8
8
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
9
9
|
import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedControlResult } from "../runs/shared/nested-events.ts";
|
|
10
10
|
import { SubagentParams } from "./schemas.ts";
|
|
11
|
-
import { loadConfig } from "./config.ts";
|
|
12
|
-
import { deriveForwardedFlags } from "../runs/shared/forward-flags.ts";
|
|
13
11
|
import { RUN_STATE_ROOT_DIR, type Details, type SubagentState } from "../shared/types.ts";
|
|
14
12
|
import { emptyGrandTotal } from "./grand-total.ts";
|
|
15
13
|
|
|
@@ -126,18 +124,15 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
126
124
|
if (registeredApis.has(pi)) return;
|
|
127
125
|
registeredApis.add(pi);
|
|
128
126
|
|
|
129
|
-
const config = loadConfig();
|
|
130
127
|
const state = createChildSafeState();
|
|
131
128
|
const executor = createSubagentExecutor({
|
|
132
129
|
pi,
|
|
133
130
|
state,
|
|
134
|
-
|
|
135
|
-
asyncByDefault: config.asyncByDefault === true,
|
|
131
|
+
resolveConfig: resolveExtensionConfig,
|
|
136
132
|
tempArtifactsDir: getArtifactsDir(null),
|
|
137
133
|
getSubagentSessionRoot,
|
|
138
134
|
expandTilde,
|
|
139
135
|
discoverAgents,
|
|
140
|
-
forwardedFlags: deriveForwardedFlags(process.argv, config),
|
|
141
136
|
allowMutatingManagementActions: false,
|
|
142
137
|
});
|
|
143
138
|
|
package/src/extension/index.ts
CHANGED
|
@@ -6,10 +6,18 @@
|
|
|
6
6
|
* - Async: Background execution, emits events when done
|
|
7
7
|
*
|
|
8
8
|
* Modes: single (agent + task), parallel (tasks[]), chain (chain[] with {previous})
|
|
9
|
-
* Toggle: async parameter (default: false, configurable via
|
|
9
|
+
* Toggle: async parameter (default: false, configurable via settings.json#subagents)
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* { "
|
|
11
|
+
* Settings: settings.json#subagents (profile or project .pi/settings.json)
|
|
12
|
+
* { "subagents": {
|
|
13
|
+
* "asyncByDefault": true, "forceTopLevelAsync": true,
|
|
14
|
+
* "defaultSessionDir": "./sessions", "maxSubagentDepth": 1,
|
|
15
|
+
* "control": {}, "parallel": { "maxTasks": 8 },
|
|
16
|
+
* "chain": { "dynamicFanout": { "maxItems": 8 } },
|
|
17
|
+
* "worktreeSetupHook": "./scripts/setup-worktree.mjs", "worktreeSetupHookTimeoutMs": 30000,
|
|
18
|
+
* "worktreeTeardownHook": "./scripts/teardown-worktree.mjs", "worktreeTeardownHookTimeoutMs": 30000,
|
|
19
|
+
* "showRosterOnStart": true, "forwardParentFlags": true, "executionBackend": "auto"
|
|
20
|
+
* } }
|
|
13
21
|
*/
|
|
14
22
|
|
|
15
23
|
import * as fs from "node:fs";
|
|
@@ -18,7 +26,7 @@ import * as path from "node:path";
|
|
|
18
26
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
19
27
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
20
28
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
21
|
-
import { discoverAgents, discoverAgentsAll } from "../agents/agents.ts";
|
|
29
|
+
import { resolveExtensionConfig, discoverAgents, discoverAgentsAll } from "../agents/agents.ts";
|
|
22
30
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
23
31
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
24
32
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
@@ -36,8 +44,7 @@ import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
|
36
44
|
import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
37
45
|
import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
38
46
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
39
|
-
import {
|
|
40
|
-
import { deriveForwardedFlags } from "../runs/shared/forward-flags.ts";
|
|
47
|
+
import { getAgentDir } from "../shared/utils.ts";
|
|
41
48
|
import {
|
|
42
49
|
type Details,
|
|
43
50
|
type SubagentState,
|
|
@@ -61,8 +68,6 @@ import {
|
|
|
61
68
|
type SubagentControlMessageDetails,
|
|
62
69
|
} from "./control-notices.ts";
|
|
63
70
|
|
|
64
|
-
export { loadConfig } from "./config.ts";
|
|
65
|
-
|
|
66
71
|
/**
|
|
67
72
|
* Derive subagent session base directory from parent session file.
|
|
68
73
|
* If parent session is ~/.pi/agent/sessions/abc123.jsonl,
|
|
@@ -250,8 +255,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
250
255
|
ensureAccessibleDir(ASYNC_DIR, true);
|
|
251
256
|
cleanupOldChainDirs();
|
|
252
257
|
|
|
253
|
-
const config = loadConfig();
|
|
254
|
-
const asyncByDefault = config.asyncByDefault === true;
|
|
255
258
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
256
259
|
cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
|
|
257
260
|
|
|
@@ -290,13 +293,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
290
293
|
const executor = createSubagentExecutor({
|
|
291
294
|
pi,
|
|
292
295
|
state,
|
|
293
|
-
|
|
294
|
-
asyncByDefault,
|
|
296
|
+
resolveConfig: resolveExtensionConfig,
|
|
295
297
|
tempArtifactsDir,
|
|
296
298
|
getSubagentSessionRoot,
|
|
297
299
|
expandTilde,
|
|
298
300
|
discoverAgents,
|
|
299
|
-
forwardedFlags: deriveForwardedFlags(process.argv, config),
|
|
300
301
|
});
|
|
301
302
|
|
|
302
303
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
@@ -542,6 +543,13 @@ DIAGNOSTICS:
|
|
|
542
543
|
|
|
543
544
|
pi.on("session_start", (_event, ctx) => {
|
|
544
545
|
resetSessionState(ctx);
|
|
546
|
+
const config = resolveExtensionConfig(ctx.cwd);
|
|
547
|
+
const legacyConfigPath = path.join(getAgentDir(), "extensions", "pi-cohort", "config.json");
|
|
548
|
+
if (fs.existsSync(legacyConfigPath)) {
|
|
549
|
+
const message = `${legacyConfigPath}: pi-cohort no longer reads this file; move its keys under \`subagents\` in \`settings.json\`.`;
|
|
550
|
+
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
551
|
+
else console.warn(message);
|
|
552
|
+
}
|
|
545
553
|
if (ctx.hasUI && config.showRosterOnStart !== false) {
|
|
546
554
|
const roster = formatSubagentRoster(ctx.cwd);
|
|
547
555
|
// Posted to scrollback at start (alongside [Skills]/[Extensions]). Safe to
|
|
@@ -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),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
|
|
3
|
+
import { resolveExtensionConfig } from "../../agents/agents.ts";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -120,6 +121,8 @@ interface SubagentRunConfig {
|
|
|
120
121
|
forwardedFlags?: string[];
|
|
121
122
|
worktreeSetupHook?: string;
|
|
122
123
|
worktreeSetupHookTimeoutMs?: number;
|
|
124
|
+
worktreeTeardownHook?: string;
|
|
125
|
+
worktreeTeardownHookTimeoutMs?: number;
|
|
123
126
|
controlConfig?: ResolvedControlConfig;
|
|
124
127
|
resultMode?: SubagentRunMode;
|
|
125
128
|
dynamicFanoutMaxItems?: number;
|
|
@@ -606,6 +609,7 @@ function writeRunLog(
|
|
|
606
609
|
|
|
607
610
|
/** Internal context for one detached-runner leaf. */
|
|
608
611
|
export interface SingleStepContext {
|
|
612
|
+
jobCwd?: string;
|
|
609
613
|
previousOutput: string;
|
|
610
614
|
outputs?: ChainOutputMap;
|
|
611
615
|
trees?: TreeRegistry;
|
|
@@ -664,9 +668,13 @@ export async function runSingleStep(
|
|
|
664
668
|
}
|
|
665
669
|
let backendSelection: import("../../execution-backend/selection.ts").ExecutionBackendSelectionResult;
|
|
666
670
|
try {
|
|
671
|
+
const unitConfig = resolveExtensionConfig(childCwd);
|
|
672
|
+
const jobPreference = path.resolve(childCwd) === path.resolve(ctx.jobCwd ?? ctx.cwd)
|
|
673
|
+
? ctx.executionBackendConfig?.userPreference
|
|
674
|
+
: undefined;
|
|
667
675
|
backendSelection = await selectConfiguredExecutionBackend({
|
|
668
676
|
cwd: childCwd,
|
|
669
|
-
userConfig: { executionBackend:
|
|
677
|
+
userConfig: { executionBackend: unitConfig.executionBackend ?? jobPreference },
|
|
670
678
|
preparation: ctx.executionBackendCoordinator,
|
|
671
679
|
});
|
|
672
680
|
} catch (error) {
|
|
@@ -1959,6 +1967,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1959
1967
|
writeStatusPayload();
|
|
1960
1968
|
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
|
|
1961
1969
|
const singleResult = await runSingleStep(task, {
|
|
1970
|
+
jobCwd: config.cwd,
|
|
1962
1971
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
1963
1972
|
outputs,
|
|
1964
1973
|
trees,
|
|
@@ -2100,8 +2109,37 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2100
2109
|
}
|
|
2101
2110
|
|
|
2102
2111
|
if (isParallelGroup(step)) {
|
|
2103
|
-
|
|
2104
|
-
|
|
2112
|
+
let group = step;
|
|
2113
|
+
let groupCwd = group.cwd?.includes("{outputs.") ? group.cwd : resolveChildCwd(cwd, group.cwd);
|
|
2114
|
+
let groupConfig: ReturnType<typeof resolveExtensionConfig> = {};
|
|
2115
|
+
if (group.worktree) {
|
|
2116
|
+
try {
|
|
2117
|
+
groupCwd = resolveChildCwd(cwd, resolveOutputReferences(group.cwd ?? cwd, outputs, trees));
|
|
2118
|
+
group = {
|
|
2119
|
+
...group,
|
|
2120
|
+
parallel: group.parallel.map((task) => ({
|
|
2121
|
+
...task,
|
|
2122
|
+
cwd: task.cwd === undefined ? undefined : resolveChildCwd(cwd, resolveOutputReferences(task.cwd, outputs, trees)),
|
|
2123
|
+
})),
|
|
2124
|
+
};
|
|
2125
|
+
const resolved = resolveExtensionConfig(groupCwd);
|
|
2126
|
+
const fallback = path.resolve(groupCwd) === path.resolve(config.cwd) ? config : {};
|
|
2127
|
+
groupConfig = {
|
|
2128
|
+
worktreeSetupHook: resolved.worktreeSetupHook ?? fallback.worktreeSetupHook,
|
|
2129
|
+
worktreeSetupHookTimeoutMs: resolved.worktreeSetupHookTimeoutMs === undefined ? fallback.worktreeSetupHookTimeoutMs : resolved.worktreeSetupHookTimeoutMs,
|
|
2130
|
+
worktreeTeardownHook: resolved.worktreeTeardownHook ?? fallback.worktreeTeardownHook,
|
|
2131
|
+
worktreeTeardownHookTimeoutMs: resolved.worktreeTeardownHookTimeoutMs === undefined ? fallback.worktreeTeardownHookTimeoutMs : resolved.worktreeTeardownHookTimeoutMs,
|
|
2132
|
+
};
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
markParallelGroupSetupFailure({
|
|
2135
|
+
statusPayload, results, group, groupStartFlatIndex: flatIndex,
|
|
2136
|
+
setupError: error instanceof Error ? error.message : String(error), failedAt: Date.now(),
|
|
2137
|
+
statusPath, eventsPath, asyncDir, runId: id, stepIndex,
|
|
2138
|
+
});
|
|
2139
|
+
flatIndex += group.parallel.length;
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2105
2143
|
const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
|
|
2106
2144
|
const failFast = group.failFast ?? false;
|
|
2107
2145
|
const groupStartFlatIndex = flatIndex;
|
|
@@ -2130,8 +2168,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2130
2168
|
try {
|
|
2131
2169
|
worktreeSetup = createWorktrees(groupCwd, `${id}-s${stepIndex}`, group.parallel.length, {
|
|
2132
2170
|
agents: group.parallel.map((task) => task.agent),
|
|
2133
|
-
setupHook:
|
|
2134
|
-
? { hookPath:
|
|
2171
|
+
setupHook: groupConfig.worktreeSetupHook
|
|
2172
|
+
? { hookPath: groupConfig.worktreeSetupHook, timeoutMs: groupConfig.worktreeSetupHookTimeoutMs }
|
|
2173
|
+
: undefined,
|
|
2174
|
+
teardownHook: groupConfig.worktreeTeardownHook
|
|
2175
|
+
? { hookPath: groupConfig.worktreeTeardownHook, timeoutMs: groupConfig.worktreeTeardownHookTimeoutMs }
|
|
2135
2176
|
: undefined,
|
|
2136
2177
|
});
|
|
2137
2178
|
} catch (error) {
|
|
@@ -2213,9 +2254,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2213
2254
|
const taskSessionDir = config.sessionDir
|
|
2214
2255
|
? path.join(config.sessionDir, `parallel-${fi}`)
|
|
2215
2256
|
: undefined;
|
|
2216
|
-
const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx,
|
|
2257
|
+
const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx, groupCwd);
|
|
2217
2258
|
|
|
2218
2259
|
const singleResult = await runSingleStep(taskForRun, {
|
|
2260
|
+
jobCwd: config.cwd,
|
|
2219
2261
|
previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
|
|
2220
2262
|
outputs,
|
|
2221
2263
|
trees,
|
|
@@ -2395,6 +2437,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2395
2437
|
}));
|
|
2396
2438
|
|
|
2397
2439
|
const singleResult = await runSingleStep(seqStep, {
|
|
2440
|
+
jobCwd: config.cwd,
|
|
2398
2441
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
2399
2442
|
outputs,
|
|
2400
2443
|
trees,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type ResolvedTemplates,
|
|
32
32
|
} from "../../shared/settings.ts";
|
|
33
33
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
34
|
+
import type { ExtensionConfig } from "../../shared/types.ts";
|
|
34
35
|
import { runSync } from "./execution.ts";
|
|
35
36
|
import type { PiRuntimeIdentity } from "../shared/pi-spawn.ts";
|
|
36
37
|
import { buildChainSummary, buildChainOutputsSection, buildChainStopSection, type ChainSummaryOptions } from "../../shared/formatters.ts";
|
|
@@ -142,6 +143,7 @@ interface ParallelChainRunInput {
|
|
|
142
143
|
maxSubagentDepth: number;
|
|
143
144
|
nestedRoute?: NestedRouteInfo;
|
|
144
145
|
forwardedFlags?: string[];
|
|
146
|
+
resolveConfig?: (cwd: string) => ExtensionConfig;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
|
|
@@ -295,6 +297,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
295
297
|
const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
|
|
296
298
|
runtime: input.runtime,
|
|
297
299
|
cwd: taskCwd,
|
|
300
|
+
executionBackend: input.resolveConfig?.(taskCwd).executionBackend,
|
|
298
301
|
signal: input.signal,
|
|
299
302
|
interruptSignal: interruptController.signal,
|
|
300
303
|
runId: input.runId,
|
|
@@ -420,7 +423,10 @@ interface ChainExecutionParams {
|
|
|
420
423
|
nestedRoute?: NestedRouteInfo;
|
|
421
424
|
worktreeSetupHook?: string;
|
|
422
425
|
worktreeSetupHookTimeoutMs?: number;
|
|
426
|
+
worktreeTeardownHook?: string;
|
|
427
|
+
worktreeTeardownHookTimeoutMs?: number;
|
|
423
428
|
forwardedFlags?: string[];
|
|
429
|
+
resolveConfig?: (cwd: string) => ExtensionConfig;
|
|
424
430
|
}
|
|
425
431
|
|
|
426
432
|
interface ChainExecutionResult {
|
|
@@ -643,10 +649,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
643
649
|
|
|
644
650
|
if (isParallelStep(step)) {
|
|
645
651
|
const parallelTemplates = stepTemplates as string[];
|
|
646
|
-
const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
|
|
652
|
+
const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd === undefined ? undefined : resolveOutputReferences(step.cwd, outputs, trees));
|
|
647
653
|
let worktreeSetup: WorktreeSetup | undefined;
|
|
648
654
|
if (step.worktree) {
|
|
649
|
-
const
|
|
655
|
+
const groupConfig = params.resolveConfig?.(parallelCwd) ?? params;
|
|
656
|
+
const resolvedTasks = step.parallel.map((task) => ({ ...task, cwd: task.cwd === undefined ? undefined : resolveOutputReferences(task.cwd, outputs, trees) }));
|
|
657
|
+
const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(resolvedTasks, parallelCwd);
|
|
650
658
|
if (worktreeTaskCwdConflict) {
|
|
651
659
|
return buildChainExecutionErrorResult(
|
|
652
660
|
`parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
|
|
@@ -657,8 +665,11 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
657
665
|
try {
|
|
658
666
|
worktreeSetup = createWorktrees(parallelCwd, `${runId}-s${stepIndex}`, step.parallel.length, {
|
|
659
667
|
agents: step.parallel.map((task) => task.agent),
|
|
660
|
-
setupHook:
|
|
661
|
-
? { hookPath:
|
|
668
|
+
setupHook: groupConfig.worktreeSetupHook
|
|
669
|
+
? { hookPath: groupConfig.worktreeSetupHook, timeoutMs: groupConfig.worktreeSetupHookTimeoutMs }
|
|
670
|
+
: undefined,
|
|
671
|
+
teardownHook: groupConfig.worktreeTeardownHook
|
|
672
|
+
? { hookPath: groupConfig.worktreeTeardownHook, timeoutMs: groupConfig.worktreeTeardownHookTimeoutMs }
|
|
662
673
|
: undefined,
|
|
663
674
|
});
|
|
664
675
|
} catch (error) {
|
|
@@ -698,6 +709,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
698
709
|
runId,
|
|
699
710
|
globalTaskIndex,
|
|
700
711
|
forwardedFlags: params.forwardedFlags,
|
|
712
|
+
resolveConfig: params.resolveConfig,
|
|
701
713
|
sessionDirForIndex,
|
|
702
714
|
sessionFileForIndex,
|
|
703
715
|
shareEnabled,
|
|
@@ -709,7 +721,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
709
721
|
allProgress,
|
|
710
722
|
outputs,
|
|
711
723
|
trees,
|
|
712
|
-
stepCwd:
|
|
724
|
+
stepCwd: parallelCwd,
|
|
713
725
|
chainAgents,
|
|
714
726
|
chainSteps,
|
|
715
727
|
totalSteps,
|
|
@@ -907,6 +919,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
907
919
|
runId,
|
|
908
920
|
globalTaskIndex,
|
|
909
921
|
forwardedFlags: params.forwardedFlags,
|
|
922
|
+
resolveConfig: params.resolveConfig,
|
|
910
923
|
sessionDirForIndex,
|
|
911
924
|
sessionFileForIndex,
|
|
912
925
|
shareEnabled,
|
|
@@ -1126,6 +1139,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1126
1139
|
const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
1127
1140
|
runtime: params.runtime,
|
|
1128
1141
|
cwd: stepCwd,
|
|
1142
|
+
executionBackend: params.resolveConfig?.(stepCwd).executionBackend,
|
|
1129
1143
|
signal,
|
|
1130
1144
|
interruptSignal: interruptController.signal,
|
|
1131
1145
|
runId,
|
|
@@ -8,6 +8,7 @@ import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
|
|
|
8
8
|
import { getArtifactsDir } from "../../shared/artifacts.ts";
|
|
9
9
|
import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
|
|
10
10
|
import { parentModelFullId, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
11
|
+
import { deriveForwardedFlags } from "../shared/forward-flags.ts";
|
|
11
12
|
import { executeChain } from "./chain-execution.ts";
|
|
12
13
|
import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
|
|
13
14
|
import { handleManagementAction } from "../../agents/agent-management.ts";
|
|
@@ -137,9 +138,7 @@ interface ExecutorDeps {
|
|
|
137
138
|
captureRuntime?: () => PiRuntimeIdentity;
|
|
138
139
|
pi: ExtensionAPI;
|
|
139
140
|
state: SubagentState;
|
|
140
|
-
|
|
141
|
-
forwardedFlags: string[];
|
|
142
|
-
asyncByDefault: boolean;
|
|
141
|
+
resolveConfig: (cwd: string) => ExtensionConfig;
|
|
143
142
|
tempArtifactsDir: string;
|
|
144
143
|
getSubagentSessionRoot: (parentSessionFile: string | null) => string;
|
|
145
144
|
expandTilde: (p: string) => string;
|
|
@@ -147,6 +146,8 @@ interface ExecutorDeps {
|
|
|
147
146
|
allowMutatingManagementActions?: boolean;
|
|
148
147
|
}
|
|
149
148
|
|
|
149
|
+
type CallExecutorDeps = ExecutorDeps & { config: ExtensionConfig; forwardedFlags: string[]; asyncByDefault: boolean };
|
|
150
|
+
|
|
150
151
|
interface ExecutionContextData {
|
|
151
152
|
runtime: PiRuntimeIdentity;
|
|
152
153
|
params: SubagentParamsLike;
|
|
@@ -205,7 +206,7 @@ function formatForegroundActivity(control: SubagentState["foregroundControls"] e
|
|
|
205
206
|
return [`active ${seconds}s ago`, ...facts].join(" | ");
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
function nestedResolutionScopeForExecutor(deps:
|
|
209
|
+
function nestedResolutionScopeForExecutor(deps: CallExecutorDeps): NestedRunResolutionScope | undefined {
|
|
209
210
|
if (deps.allowMutatingManagementActions !== false) return undefined;
|
|
210
211
|
const route = resolveInheritedNestedRouteFromEnv();
|
|
211
212
|
const address = route ? resolveNestedParentAddressFromEnv() : undefined;
|
|
@@ -215,7 +216,7 @@ function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResoluti
|
|
|
215
216
|
};
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
function buildAsyncContext(ctx: ExtensionContext, deps:
|
|
219
|
+
function buildAsyncContext(ctx: ExtensionContext, deps: CallExecutorDeps, cwd: string, runtime: PiRuntimeIdentity) {
|
|
219
220
|
return {
|
|
220
221
|
runtime,
|
|
221
222
|
pi: deps.pi,
|
|
@@ -581,7 +582,7 @@ async function resumeAsyncRun(input: {
|
|
|
581
582
|
params: SubagentParamsLike;
|
|
582
583
|
requestCwd: string;
|
|
583
584
|
ctx: ExtensionContext;
|
|
584
|
-
deps:
|
|
585
|
+
deps: CallExecutorDeps;
|
|
585
586
|
}): Promise<AgentToolResult<Details>> {
|
|
586
587
|
const followUp = (input.params.message ?? input.params.task ?? "").trim();
|
|
587
588
|
if (!followUp) {
|
|
@@ -627,6 +628,7 @@ async function resumeAsyncRun(input: {
|
|
|
627
628
|
|
|
628
629
|
input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
|
|
629
630
|
const effectiveCwd = target.cwd ?? input.requestCwd;
|
|
631
|
+
const unitConfig = effectiveCwd === input.requestCwd ? input.deps.config : input.deps.resolveConfig(effectiveCwd);
|
|
630
632
|
const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
|
|
631
633
|
const discoveredAgents = input.deps.discoverAgents(effectiveCwd, scope).agents;
|
|
632
634
|
const agentConfig = discoveredAgents.find((agent) => agent.name === target.agent);
|
|
@@ -649,7 +651,7 @@ async function resumeAsyncRun(input: {
|
|
|
649
651
|
agent: target.agent,
|
|
650
652
|
task: buildRevivedAsyncTask(target, followUp),
|
|
651
653
|
agentConfig,
|
|
652
|
-
ctx: buildAsyncContext(input.ctx, input.deps, input.requestCwd, target.runtime),
|
|
654
|
+
ctx: { ...buildAsyncContext(input.ctx, input.deps, input.requestCwd, target.runtime), executionBackend: unitConfig.executionBackend },
|
|
653
655
|
cwd: effectiveCwd,
|
|
654
656
|
maxOutput: input.params.maxOutput,
|
|
655
657
|
artifactsDir: input.deps.tempArtifactsDir,
|
|
@@ -663,8 +665,10 @@ async function resumeAsyncRun(input: {
|
|
|
663
665
|
originSessionRoot: originalRoot,
|
|
664
666
|
sessionFile: target.sessionFile,
|
|
665
667
|
maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
|
|
666
|
-
worktreeSetupHook:
|
|
667
|
-
worktreeSetupHookTimeoutMs:
|
|
668
|
+
worktreeSetupHook: unitConfig.worktreeSetupHook,
|
|
669
|
+
worktreeSetupHookTimeoutMs: unitConfig.worktreeSetupHookTimeoutMs,
|
|
670
|
+
worktreeTeardownHook: unitConfig.worktreeTeardownHook,
|
|
671
|
+
worktreeTeardownHookTimeoutMs: unitConfig.worktreeTeardownHookTimeoutMs,
|
|
668
672
|
controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
|
|
669
673
|
availableModels,
|
|
670
674
|
forwardedFlags: input.deps.forwardedFlags,
|
|
@@ -978,7 +982,7 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
|
|
|
978
982
|
});
|
|
979
983
|
}
|
|
980
984
|
|
|
981
|
-
function runAsyncPath(data: ExecutionContextData, deps:
|
|
985
|
+
function runAsyncPath(data: ExecutionContextData, deps: CallExecutorDeps): AgentToolResult<Details> | null {
|
|
982
986
|
const {
|
|
983
987
|
params,
|
|
984
988
|
effectiveCwd,
|
|
@@ -1073,6 +1077,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1073
1077
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1074
1078
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1075
1079
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1080
|
+
worktreeTeardownHook: deps.config.worktreeTeardownHook,
|
|
1081
|
+
worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
|
|
1076
1082
|
controlConfig,
|
|
1077
1083
|
nestedRoute,
|
|
1078
1084
|
forwardedFlags: deps.forwardedFlags,
|
|
@@ -1102,6 +1108,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1102
1108
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1103
1109
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1104
1110
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1111
|
+
worktreeTeardownHook: deps.config.worktreeTeardownHook,
|
|
1112
|
+
worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
|
|
1105
1113
|
controlConfig,
|
|
1106
1114
|
nestedRoute,
|
|
1107
1115
|
forwardedFlags: deps.forwardedFlags,
|
|
@@ -1144,6 +1152,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1144
1152
|
maxSubagentDepth,
|
|
1145
1153
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1146
1154
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1155
|
+
worktreeTeardownHook: deps.config.worktreeTeardownHook,
|
|
1156
|
+
worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
|
|
1147
1157
|
controlConfig,
|
|
1148
1158
|
nestedRoute,
|
|
1149
1159
|
acceptance: params.acceptance,
|
|
@@ -1154,7 +1164,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1154
1164
|
return null;
|
|
1155
1165
|
}
|
|
1156
1166
|
|
|
1157
|
-
async function runChainPath(data: ExecutionContextData, deps:
|
|
1167
|
+
async function runChainPath(data: ExecutionContextData, deps: CallExecutorDeps): Promise<AgentToolResult<Details>> {
|
|
1158
1168
|
const {
|
|
1159
1169
|
params,
|
|
1160
1170
|
effectiveCwd,
|
|
@@ -1200,10 +1210,13 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1200
1210
|
nestedRoute: foregroundControl?.nestedRoute,
|
|
1201
1211
|
chainSkills,
|
|
1202
1212
|
chainDir: params.chainDir,
|
|
1213
|
+
resolveConfig: deps.resolveConfig,
|
|
1203
1214
|
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1204
1215
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1205
1216
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1206
1217
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1218
|
+
worktreeTeardownHook: deps.config.worktreeTeardownHook,
|
|
1219
|
+
worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
|
|
1207
1220
|
forwardedFlags: deps.forwardedFlags,
|
|
1208
1221
|
});
|
|
1209
1222
|
|
|
@@ -1237,6 +1250,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1237
1250
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1238
1251
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1239
1252
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1253
|
+
worktreeTeardownHook: deps.config.worktreeTeardownHook,
|
|
1254
|
+
worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
|
|
1240
1255
|
controlConfig,
|
|
1241
1256
|
nestedRoute: data.nestedRoute,
|
|
1242
1257
|
forwardedFlags: deps.forwardedFlags,
|
|
@@ -1276,7 +1291,7 @@ interface ForegroundParallelRunInput {
|
|
|
1276
1291
|
onControlEvent?: (event: ControlEvent) => void;
|
|
1277
1292
|
foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
|
|
1278
1293
|
concurrencyLimit: number;
|
|
1279
|
-
|
|
1294
|
+
resolveConfig: (cwd: string) => ExtensionConfig;
|
|
1280
1295
|
liveResults: (SingleResult | undefined)[];
|
|
1281
1296
|
liveProgress: (AgentProgress | undefined)[];
|
|
1282
1297
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
@@ -1299,6 +1314,8 @@ function createParallelWorktreeSetup(
|
|
|
1299
1314
|
tasks: TaskParam[],
|
|
1300
1315
|
setupHook: ExtensionConfig["worktreeSetupHook"],
|
|
1301
1316
|
setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
|
|
1317
|
+
teardownHook: ExtensionConfig["worktreeTeardownHook"],
|
|
1318
|
+
teardownHookTimeoutMs: ExtensionConfig["worktreeTeardownHookTimeoutMs"],
|
|
1302
1319
|
): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
|
|
1303
1320
|
if (!enabled) return {};
|
|
1304
1321
|
try {
|
|
@@ -1308,6 +1325,9 @@ function createParallelWorktreeSetup(
|
|
|
1308
1325
|
setupHook: setupHook
|
|
1309
1326
|
? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs }
|
|
1310
1327
|
: undefined,
|
|
1328
|
+
teardownHook: teardownHook
|
|
1329
|
+
? { hookPath: teardownHook, timeoutMs: teardownHookTimeoutMs }
|
|
1330
|
+
: undefined,
|
|
1311
1331
|
}),
|
|
1312
1332
|
};
|
|
1313
1333
|
} catch (error) {
|
|
@@ -1464,7 +1484,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1464
1484
|
signal: input.signal,
|
|
1465
1485
|
interruptSignal: interruptController.signal,
|
|
1466
1486
|
runId: input.runId,
|
|
1467
|
-
executionBackend: input.executionBackend,
|
|
1487
|
+
executionBackend: input.resolveConfig(taskCwd).executionBackend,
|
|
1468
1488
|
index,
|
|
1469
1489
|
sessionDir: input.sessionDirForIndex(index),
|
|
1470
1490
|
sessionFile: input.sessionFileForIndex(index),
|
|
@@ -1530,7 +1550,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1530
1550
|
});
|
|
1531
1551
|
}
|
|
1532
1552
|
|
|
1533
|
-
async function runParallelPath(data: ExecutionContextData, deps:
|
|
1553
|
+
async function runParallelPath(data: ExecutionContextData, deps: CallExecutorDeps): Promise<AgentToolResult<Details>> {
|
|
1534
1554
|
const {
|
|
1535
1555
|
params,
|
|
1536
1556
|
effectiveCwd,
|
|
@@ -1691,6 +1711,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1691
1711
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1692
1712
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1693
1713
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1714
|
+
worktreeTeardownHook: deps.config.worktreeTeardownHook,
|
|
1715
|
+
worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
|
|
1694
1716
|
controlConfig,
|
|
1695
1717
|
forwardedFlags: deps.forwardedFlags,
|
|
1696
1718
|
});
|
|
@@ -1709,6 +1731,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1709
1731
|
tasks,
|
|
1710
1732
|
deps.config.worktreeSetupHook,
|
|
1711
1733
|
deps.config.worktreeSetupHookTimeoutMs,
|
|
1734
|
+
deps.config.worktreeTeardownHook,
|
|
1735
|
+
deps.config.worktreeTeardownHookTimeoutMs,
|
|
1712
1736
|
);
|
|
1713
1737
|
if (errorResult) return errorResult;
|
|
1714
1738
|
|
|
@@ -1786,7 +1810,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1786
1810
|
onUpdate: onUpdateWithCost,
|
|
1787
1811
|
worktreeSetup,
|
|
1788
1812
|
forwardedFlags: deps.forwardedFlags,
|
|
1789
|
-
|
|
1813
|
+
resolveConfig: deps.resolveConfig,
|
|
1790
1814
|
});
|
|
1791
1815
|
if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
|
|
1792
1816
|
recordSyncCost(deps.state.grandTotal, runId, results.reduce((sum, r) => sum + r.usage.cost, 0) + sumNestedCost(foregroundControl?.nestedChildren));
|
|
@@ -1846,7 +1870,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1846
1870
|
}
|
|
1847
1871
|
}
|
|
1848
1872
|
|
|
1849
|
-
async function runSinglePath(data: ExecutionContextData, deps:
|
|
1873
|
+
async function runSinglePath(data: ExecutionContextData, deps: CallExecutorDeps): Promise<AgentToolResult<Details>> {
|
|
1850
1874
|
const {
|
|
1851
1875
|
params,
|
|
1852
1876
|
effectiveCwd,
|
|
@@ -1952,6 +1976,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1952
1976
|
maxSubagentDepth,
|
|
1953
1977
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1954
1978
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1979
|
+
worktreeTeardownHook: deps.config.worktreeTeardownHook,
|
|
1980
|
+
worktreeTeardownHookTimeoutMs: deps.config.worktreeTeardownHookTimeoutMs,
|
|
1955
1981
|
controlConfig,
|
|
1956
1982
|
forwardedFlags: deps.forwardedFlags,
|
|
1957
1983
|
});
|
|
@@ -2109,7 +2135,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2109
2135
|
};
|
|
2110
2136
|
}
|
|
2111
2137
|
|
|
2112
|
-
export function createSubagentExecutor(
|
|
2138
|
+
export function createSubagentExecutor(services: ExecutorDeps): {
|
|
2113
2139
|
execute: (
|
|
2114
2140
|
id: string,
|
|
2115
2141
|
params: SubagentParamsLike,
|
|
@@ -2122,7 +2148,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2122
2148
|
let runtimeError: unknown;
|
|
2123
2149
|
try {
|
|
2124
2150
|
// SDK hosts are not launched through Pi's CLI; the installation this extension was loaded from is the fallback.
|
|
2125
|
-
runtime = (
|
|
2151
|
+
runtime = (services.captureRuntime ?? (() => capturePiRuntime({ installRoot: getPackageDir() })))();
|
|
2126
2152
|
} catch (error) {
|
|
2127
2153
|
runtimeError = error;
|
|
2128
2154
|
}
|
|
@@ -2133,11 +2159,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2133
2159
|
onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
|
|
2134
2160
|
ctx: ExtensionContext,
|
|
2135
2161
|
): Promise<AgentToolResult<Details>> => {
|
|
2162
|
+
const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
|
|
2163
|
+
const config = services.resolveConfig(requestCwd);
|
|
2164
|
+
const deps: CallExecutorDeps = { ...services, config, forwardedFlags: deriveForwardedFlags(process.argv, config), asyncByDefault: config.asyncByDefault === true };
|
|
2136
2165
|
deps.state.baseCwd = ctx.cwd;
|
|
2137
2166
|
deps.state.foregroundRuns ??= new Map();
|
|
2138
2167
|
deps.state.foregroundControls ??= new Map();
|
|
2139
2168
|
deps.state.lastForegroundControlId ??= null;
|
|
2140
|
-
const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
|
|
2141
2169
|
const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd };
|
|
2142
2170
|
if (params.action) {
|
|
2143
2171
|
if (params.action === "doctor") {
|
|
@@ -81,9 +81,6 @@ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutCon
|
|
|
81
81
|
}
|
|
82
82
|
for (const { template, cwd } of taskTemplatesForStep(step)) {
|
|
83
83
|
for (const match of template.matchAll(OUTPUT_REF_PATTERN)) {
|
|
84
|
-
if (cwd && isParallelStep(step) && step.worktree) {
|
|
85
|
-
throw new ChainOutputValidationError(`Chain step ${stepIndex + 1}: cwd output references cannot be combined with worktree: true.`);
|
|
86
|
-
}
|
|
87
84
|
const rawReference = match[0];
|
|
88
85
|
const parsed = parseOutputReference(match[1]!);
|
|
89
86
|
if (!parsed) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/shared/types.ts
CHANGED
|
@@ -859,6 +859,7 @@ interface ExtensionChainConfig {
|
|
|
859
859
|
};
|
|
860
860
|
}
|
|
861
861
|
|
|
862
|
+
/** Sparse extension settings resolved from settings.json#subagents. */
|
|
862
863
|
export interface ExtensionConfig {
|
|
863
864
|
asyncByDefault?: boolean;
|
|
864
865
|
forceTopLevelAsync?: boolean;
|
|
@@ -869,6 +870,8 @@ export interface ExtensionConfig {
|
|
|
869
870
|
chain?: ExtensionChainConfig;
|
|
870
871
|
worktreeSetupHook?: string;
|
|
871
872
|
worktreeSetupHookTimeoutMs?: number;
|
|
873
|
+
worktreeTeardownHook?: string;
|
|
874
|
+
worktreeTeardownHookTimeoutMs?: number;
|
|
872
875
|
/** Print a roster of discovered subagents/chains into chat at session start. Default: true. */
|
|
873
876
|
showRosterOnStart?: boolean;
|
|
874
877
|
/** Forward the parent pi's extension CLI flags (e.g. --no-autofix) into spawned children. Default: true. */
|
package/src/extension/config.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { ExtensionConfig } from "../shared/types.ts";
|
|
4
|
-
import { getAgentDir } from "../shared/utils.ts";
|
|
5
|
-
|
|
6
|
-
export function loadConfig(): ExtensionConfig {
|
|
7
|
-
const configPath = path.join(getAgentDir(), "extensions", "pi-cohort", "config.json");
|
|
8
|
-
try {
|
|
9
|
-
if (fs.existsSync(configPath)) {
|
|
10
|
-
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
11
|
-
}
|
|
12
|
-
} catch (error) {
|
|
13
|
-
console.error(`Failed to load pi-cohort config from '${configPath}':`, error);
|
|
14
|
-
}
|
|
15
|
-
return {};
|
|
16
|
-
}
|