pi-cohort 7.5.0 → 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 CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## [7.5.0] - 2026-10-26
4
9
 
5
10
  ### Added
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. Full env/JSON reference: [doc/configuration.md](doc/configuration.md).
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.5.0",
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",
@@ -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
- let executionBackend: string | undefined;
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
- if (levelSettings.executionBackend !== undefined) {
523
- executionBackend = levelSettings.executionBackend;
524
- }
551
+ Object.assign(extension, projectExtensionSettings(levelSettings));
525
552
  }
526
- return { overrides, disableBuiltins, executionBackend };
553
+ return { ...extension, overrides, disableBuiltins };
527
554
  }
528
555
 
529
- export function readProjectExecutionBackend(cwd: string): string | undefined {
530
- const merged = readMergedProjectSubagentSettings(cwd);
531
- return merged.executionBackend;
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 projectPreference = readProjectExecutionBackend(input.cwd);
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
- config,
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
 
@@ -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 config.json)
9
+ * Toggle: async parameter (default: false, configurable via settings.json#subagents)
10
10
  *
11
- * Config file: ~/.pi/agent/extensions/pi-cohort/config.json
12
- * { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "showRosterOnStart": true, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
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 { loadConfig } from "./config.ts";
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
- config,
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
@@ -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";
@@ -608,6 +609,7 @@ function writeRunLog(
608
609
 
609
610
  /** Internal context for one detached-runner leaf. */
610
611
  export interface SingleStepContext {
612
+ jobCwd?: string;
611
613
  previousOutput: string;
612
614
  outputs?: ChainOutputMap;
613
615
  trees?: TreeRegistry;
@@ -666,9 +668,13 @@ export async function runSingleStep(
666
668
  }
667
669
  let backendSelection: import("../../execution-backend/selection.ts").ExecutionBackendSelectionResult;
668
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;
669
675
  backendSelection = await selectConfiguredExecutionBackend({
670
676
  cwd: childCwd,
671
- userConfig: { executionBackend: ctx.executionBackendConfig?.userPreference },
677
+ userConfig: { executionBackend: unitConfig.executionBackend ?? jobPreference },
672
678
  preparation: ctx.executionBackendCoordinator,
673
679
  });
674
680
  } catch (error) {
@@ -1961,6 +1967,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1961
1967
  writeStatusPayload();
1962
1968
  appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
1963
1969
  const singleResult = await runSingleStep(task, {
1970
+ jobCwd: config.cwd,
1964
1971
  previousOutput, placeholder, cwd, sessionEnabled,
1965
1972
  outputs,
1966
1973
  trees,
@@ -2102,8 +2109,37 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2102
2109
  }
2103
2110
 
2104
2111
  if (isParallelGroup(step)) {
2105
- const group = step;
2106
- const groupCwd = group.cwd?.includes("{outputs.") ? group.cwd : resolveChildCwd(cwd, group.cwd);
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
+ }
2107
2143
  const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
2108
2144
  const failFast = group.failFast ?? false;
2109
2145
  const groupStartFlatIndex = flatIndex;
@@ -2132,11 +2168,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2132
2168
  try {
2133
2169
  worktreeSetup = createWorktrees(groupCwd, `${id}-s${stepIndex}`, group.parallel.length, {
2134
2170
  agents: group.parallel.map((task) => task.agent),
2135
- setupHook: config.worktreeSetupHook
2136
- ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
2171
+ setupHook: groupConfig.worktreeSetupHook
2172
+ ? { hookPath: groupConfig.worktreeSetupHook, timeoutMs: groupConfig.worktreeSetupHookTimeoutMs }
2137
2173
  : undefined,
2138
- teardownHook: config.worktreeTeardownHook
2139
- ? { hookPath: config.worktreeTeardownHook, timeoutMs: config.worktreeTeardownHookTimeoutMs }
2174
+ teardownHook: groupConfig.worktreeTeardownHook
2175
+ ? { hookPath: groupConfig.worktreeTeardownHook, timeoutMs: groupConfig.worktreeTeardownHookTimeoutMs }
2140
2176
  : undefined,
2141
2177
  });
2142
2178
  } catch (error) {
@@ -2218,9 +2254,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2218
2254
  const taskSessionDir = config.sessionDir
2219
2255
  ? path.join(config.sessionDir, `parallel-${fi}`)
2220
2256
  : undefined;
2221
- const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx, group.cwd);
2257
+ const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx, groupCwd);
2222
2258
 
2223
2259
  const singleResult = await runSingleStep(taskForRun, {
2260
+ jobCwd: config.cwd,
2224
2261
  previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
2225
2262
  outputs,
2226
2263
  trees,
@@ -2400,6 +2437,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2400
2437
  }));
2401
2438
 
2402
2439
  const singleResult = await runSingleStep(seqStep, {
2440
+ jobCwd: config.cwd,
2403
2441
  previousOutput, placeholder, cwd, sessionEnabled,
2404
2442
  outputs,
2405
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,
@@ -423,6 +426,7 @@ interface ChainExecutionParams {
423
426
  worktreeTeardownHook?: string;
424
427
  worktreeTeardownHookTimeoutMs?: number;
425
428
  forwardedFlags?: string[];
429
+ resolveConfig?: (cwd: string) => ExtensionConfig;
426
430
  }
427
431
 
428
432
  interface ChainExecutionResult {
@@ -645,10 +649,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
645
649
 
646
650
  if (isParallelStep(step)) {
647
651
  const parallelTemplates = stepTemplates as string[];
648
- const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
652
+ const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd === undefined ? undefined : resolveOutputReferences(step.cwd, outputs, trees));
649
653
  let worktreeSetup: WorktreeSetup | undefined;
650
654
  if (step.worktree) {
651
- const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(step.parallel, parallelCwd);
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);
652
658
  if (worktreeTaskCwdConflict) {
653
659
  return buildChainExecutionErrorResult(
654
660
  `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
@@ -659,11 +665,11 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
659
665
  try {
660
666
  worktreeSetup = createWorktrees(parallelCwd, `${runId}-s${stepIndex}`, step.parallel.length, {
661
667
  agents: step.parallel.map((task) => task.agent),
662
- setupHook: params.worktreeSetupHook
663
- ? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
668
+ setupHook: groupConfig.worktreeSetupHook
669
+ ? { hookPath: groupConfig.worktreeSetupHook, timeoutMs: groupConfig.worktreeSetupHookTimeoutMs }
664
670
  : undefined,
665
- teardownHook: params.worktreeTeardownHook
666
- ? { hookPath: params.worktreeTeardownHook, timeoutMs: params.worktreeTeardownHookTimeoutMs }
671
+ teardownHook: groupConfig.worktreeTeardownHook
672
+ ? { hookPath: groupConfig.worktreeTeardownHook, timeoutMs: groupConfig.worktreeTeardownHookTimeoutMs }
667
673
  : undefined,
668
674
  });
669
675
  } catch (error) {
@@ -703,6 +709,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
703
709
  runId,
704
710
  globalTaskIndex,
705
711
  forwardedFlags: params.forwardedFlags,
712
+ resolveConfig: params.resolveConfig,
706
713
  sessionDirForIndex,
707
714
  sessionFileForIndex,
708
715
  shareEnabled,
@@ -714,7 +721,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
714
721
  allProgress,
715
722
  outputs,
716
723
  trees,
717
- stepCwd: step.cwd,
724
+ stepCwd: parallelCwd,
718
725
  chainAgents,
719
726
  chainSteps,
720
727
  totalSteps,
@@ -912,6 +919,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
912
919
  runId,
913
920
  globalTaskIndex,
914
921
  forwardedFlags: params.forwardedFlags,
922
+ resolveConfig: params.resolveConfig,
915
923
  sessionDirForIndex,
916
924
  sessionFileForIndex,
917
925
  shareEnabled,
@@ -1131,6 +1139,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1131
1139
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1132
1140
  runtime: params.runtime,
1133
1141
  cwd: stepCwd,
1142
+ executionBackend: params.resolveConfig?.(stepCwd).executionBackend,
1134
1143
  signal,
1135
1144
  interruptSignal: interruptController.signal,
1136
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
- config: ExtensionConfig;
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: ExecutorDeps): NestedRunResolutionScope | undefined {
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: ExecutorDeps, cwd: string, runtime: PiRuntimeIdentity) {
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: ExecutorDeps;
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,10 +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: input.deps.config.worktreeSetupHook,
667
- worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
668
- worktreeTeardownHook: input.deps.config.worktreeTeardownHook,
669
- worktreeTeardownHookTimeoutMs: input.deps.config.worktreeTeardownHookTimeoutMs,
668
+ worktreeSetupHook: unitConfig.worktreeSetupHook,
669
+ worktreeSetupHookTimeoutMs: unitConfig.worktreeSetupHookTimeoutMs,
670
+ worktreeTeardownHook: unitConfig.worktreeTeardownHook,
671
+ worktreeTeardownHookTimeoutMs: unitConfig.worktreeTeardownHookTimeoutMs,
670
672
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
671
673
  availableModels,
672
674
  forwardedFlags: input.deps.forwardedFlags,
@@ -980,7 +982,7 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
980
982
  });
981
983
  }
982
984
 
983
- function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult<Details> | null {
985
+ function runAsyncPath(data: ExecutionContextData, deps: CallExecutorDeps): AgentToolResult<Details> | null {
984
986
  const {
985
987
  params,
986
988
  effectiveCwd,
@@ -1162,7 +1164,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1162
1164
  return null;
1163
1165
  }
1164
1166
 
1165
- async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
1167
+ async function runChainPath(data: ExecutionContextData, deps: CallExecutorDeps): Promise<AgentToolResult<Details>> {
1166
1168
  const {
1167
1169
  params,
1168
1170
  effectiveCwd,
@@ -1208,6 +1210,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1208
1210
  nestedRoute: foregroundControl?.nestedRoute,
1209
1211
  chainSkills,
1210
1212
  chainDir: params.chainDir,
1213
+ resolveConfig: deps.resolveConfig,
1211
1214
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1212
1215
  maxSubagentDepth: currentMaxSubagentDepth,
1213
1216
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1288,7 +1291,7 @@ interface ForegroundParallelRunInput {
1288
1291
  onControlEvent?: (event: ControlEvent) => void;
1289
1292
  foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
1290
1293
  concurrencyLimit: number;
1291
- executionBackend?: string;
1294
+ resolveConfig: (cwd: string) => ExtensionConfig;
1292
1295
  liveResults: (SingleResult | undefined)[];
1293
1296
  liveProgress: (AgentProgress | undefined)[];
1294
1297
  onUpdate?: (r: AgentToolResult<Details>) => void;
@@ -1481,7 +1484,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1481
1484
  signal: input.signal,
1482
1485
  interruptSignal: interruptController.signal,
1483
1486
  runId: input.runId,
1484
- executionBackend: input.executionBackend,
1487
+ executionBackend: input.resolveConfig(taskCwd).executionBackend,
1485
1488
  index,
1486
1489
  sessionDir: input.sessionDirForIndex(index),
1487
1490
  sessionFile: input.sessionFileForIndex(index),
@@ -1547,7 +1550,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1547
1550
  });
1548
1551
  }
1549
1552
 
1550
- async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
1553
+ async function runParallelPath(data: ExecutionContextData, deps: CallExecutorDeps): Promise<AgentToolResult<Details>> {
1551
1554
  const {
1552
1555
  params,
1553
1556
  effectiveCwd,
@@ -1807,7 +1810,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1807
1810
  onUpdate: onUpdateWithCost,
1808
1811
  worktreeSetup,
1809
1812
  forwardedFlags: deps.forwardedFlags,
1810
- executionBackend: deps.config.executionBackend,
1813
+ resolveConfig: deps.resolveConfig,
1811
1814
  });
1812
1815
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1813
1816
  recordSyncCost(deps.state.grandTotal, runId, results.reduce((sum, r) => sum + r.usage.cost, 0) + sumNestedCost(foregroundControl?.nestedChildren));
@@ -1867,7 +1870,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1867
1870
  }
1868
1871
  }
1869
1872
 
1870
- async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
1873
+ async function runSinglePath(data: ExecutionContextData, deps: CallExecutorDeps): Promise<AgentToolResult<Details>> {
1871
1874
  const {
1872
1875
  params,
1873
1876
  effectiveCwd,
@@ -2132,7 +2135,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2132
2135
  };
2133
2136
  }
2134
2137
 
2135
- export function createSubagentExecutor(deps: ExecutorDeps): {
2138
+ export function createSubagentExecutor(services: ExecutorDeps): {
2136
2139
  execute: (
2137
2140
  id: string,
2138
2141
  params: SubagentParamsLike,
@@ -2145,7 +2148,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2145
2148
  let runtimeError: unknown;
2146
2149
  try {
2147
2150
  // SDK hosts are not launched through Pi's CLI; the installation this extension was loaded from is the fallback.
2148
- runtime = (deps.captureRuntime ?? (() => capturePiRuntime({ installRoot: getPackageDir() })))();
2151
+ runtime = (services.captureRuntime ?? (() => capturePiRuntime({ installRoot: getPackageDir() })))();
2149
2152
  } catch (error) {
2150
2153
  runtimeError = error;
2151
2154
  }
@@ -2156,11 +2159,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2156
2159
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
2157
2160
  ctx: ExtensionContext,
2158
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 };
2159
2165
  deps.state.baseCwd = ctx.cwd;
2160
2166
  deps.state.foregroundRuns ??= new Map();
2161
2167
  deps.state.foregroundControls ??= new Map();
2162
2168
  deps.state.lastForegroundControlId ??= null;
2163
- const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
2164
2169
  const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd };
2165
2170
  if (params.action) {
2166
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) {
@@ -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;
@@ -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
- }