pi-subagents 0.62.0 → 0.63.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/docs/agents.md +5 -4
  3. package/docs/configuration.md +28 -2
  4. package/docs/models.md +5 -5
  5. package/docs/observability.md +4 -1
  6. package/docs/tool-reference.md +2 -2
  7. package/package.json +1 -1
  8. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  9. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  10. package/src/agents/agent-management.ts +22 -4
  11. package/src/agents/agents.ts +107 -125
  12. package/src/api/shared-types.ts +3 -0
  13. package/src/extension/config.ts +20 -0
  14. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  15. package/src/runs/background/active-async-capacity.ts +0 -1
  16. package/src/runs/background/async-execution.ts +53 -7
  17. package/src/runs/background/async-resume.ts +3 -0
  18. package/src/runs/background/async-status.ts +18 -2
  19. package/src/runs/background/notify.ts +13 -1
  20. package/src/runs/background/run-status.ts +22 -2
  21. package/src/runs/background/subagent-runner.ts +30 -2
  22. package/src/runs/background/wait-completions.ts +13 -0
  23. package/src/runs/foreground/subagent-executor.ts +35 -3
  24. package/src/runs/shared/acceptance.ts +15 -9
  25. package/src/runs/shared/lane-metadata.ts +24 -3
  26. package/src/runs/shared/parallel-handoff.ts +4 -0
  27. package/src/runs/shared/pi-args.ts +8 -2
  28. package/src/runs/shared/task-intent.ts +5 -2
  29. package/src/runs/shared/worktree.ts +467 -63
  30. package/src/shared/types.ts +29 -0
  31. package/src/shared/utils.ts +18 -7
  32. package/src/slash/subagents-admin.ts +24 -12
  33. package/src/tui/fleet-status.ts +61 -2
  34. package/src/tui/fleet.ts +12 -7
  35. package/src/tui/render.ts +222 -14
  36. package/src/workflows/workflow-checklist.ts +441 -0
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
11
11
  import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig } from "../shared/types.ts";
12
12
  import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
13
13
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
14
+ import { expandHomePath } from "../shared/settings.ts";
14
15
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
15
16
  import { parseChain, parseJsonChain } from "./chain-serializer.ts";
16
17
  import { mergeAgentsForScope } from "./agent-selection.ts";
@@ -113,6 +114,8 @@ interface BuiltinAgentOverrideInfo {
113
114
  scope: "user" | "project";
114
115
  path: string;
115
116
  base: BuiltinAgentOverrideBase;
117
+ fields?: string[];
118
+ fieldScopes?: Record<string, Array<"user" | "project">>;
116
119
  }
117
120
 
118
121
  export interface AgentModelSourceInfo {
@@ -184,6 +187,7 @@ type ProjectRootResolution = "nearest" | "git-root";
184
187
  interface SubagentSettings {
185
188
  overrides: Record<string, BuiltinAgentOverrideConfig>;
186
189
  providerOverrides: Record<string, Record<string, BuiltinAgentOverrideConfig>>;
190
+ agentScanDirs?: string[];
187
191
  defaultModel?: string;
188
192
  defaultProvider?: string;
189
193
  defaultThinking?: string;
@@ -1172,6 +1176,14 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
1172
1176
  }
1173
1177
  defaultExtensions = subagentsObject.defaultExtensions.map((item) => item.trim());
1174
1178
  }
1179
+ let agentScanDirs: string[] | undefined;
1180
+ if ("agentScanDirs" in subagentsObject) {
1181
+ if (!Array.isArray(subagentsObject.agentScanDirs)
1182
+ || subagentsObject.agentScanDirs.some((item) => typeof item !== "string" || !item.trim())) {
1183
+ throw new Error(`Subagent settings in '${filePath}' have invalid 'agentScanDirs'; expected an array of non-empty strings.`);
1184
+ }
1185
+ agentScanDirs = subagentsObject.agentScanDirs.map((item) => item.trim());
1186
+ }
1175
1187
  const modelScope = parseModelScopeConfig(subagentsObject.modelScope, { filePath });
1176
1188
 
1177
1189
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
@@ -1186,6 +1198,7 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
1186
1198
  ...(defaultThinking !== undefined ? { defaultThinking } : {}),
1187
1199
  ...(maxThinking !== undefined ? { maxThinking } : {}),
1188
1200
  ...(defaultExtensions !== undefined ? { defaultExtensions } : {}),
1201
+ ...(agentScanDirs !== undefined ? { agentScanDirs } : {}),
1189
1202
  ...(disableBuiltins !== undefined ? { disableBuiltins } : {}),
1190
1203
  ...(disableThinking !== undefined ? { disableThinking } : {}),
1191
1204
  ...(modelScope !== undefined ? { modelScope } : {}),
@@ -1348,9 +1361,19 @@ function applyBuiltinOverride(
1348
1361
  override: BuiltinAgentOverrideConfig,
1349
1362
  meta: { scope: "user" | "project"; path: string },
1350
1363
  ): AgentConfig {
1364
+ const overrideInfo: BuiltinAgentOverrideInfo = {
1365
+ ...meta,
1366
+ base: agent.override?.base ?? cloneOverrideBase(agent),
1367
+ fields: [...new Set([...(agent.override?.fields ?? []), ...Object.keys(override)])].sort(),
1368
+ fieldScopes: Object.fromEntries(Object.entries({ ...(agent.override?.fieldScopes ?? {}) }).map(([field, scopes]) => [field, [...scopes]])),
1369
+ };
1370
+ for (const field of Object.keys(override)) {
1371
+ const scopes = overrideInfo.fieldScopes![field] ?? [];
1372
+ overrideInfo.fieldScopes![field] = [...new Set([...scopes, meta.scope])].sort();
1373
+ }
1351
1374
  const next: AgentConfig = {
1352
1375
  ...agent,
1353
- override: { ...meta, base: cloneOverrideBase(agent) },
1376
+ override: overrideInfo,
1354
1377
  };
1355
1378
 
1356
1379
  if (override.description !== undefined) next.description = override.description;
@@ -1450,128 +1473,12 @@ function applyBuiltinOverrides(
1450
1473
  });
1451
1474
  }
1452
1475
 
1453
- export function agentHasFrontmatterField(agent: AgentConfig, ...fields: string[]): boolean {
1454
- const frontmatterFields = agentFrontmatterFields.get(agent);
1455
- return frontmatterFields ? fields.some((field) => frontmatterFields.has(field)) : false;
1456
- }
1457
-
1458
1476
  function applyCustomAgentOverride(
1459
1477
  agent: AgentConfig,
1460
1478
  override: BuiltinAgentOverrideConfig,
1461
1479
  meta: { scope: "user" | "project"; path: string },
1462
1480
  ): AgentConfig {
1463
- let next: AgentConfig | undefined;
1464
- let anyFilled = false;
1465
-
1466
- const mutable = (): AgentConfig => {
1467
- next ??= { ...agent };
1468
- return next;
1469
- };
1470
-
1471
- const fill = <K extends keyof AgentConfig>(
1472
- field: K,
1473
- frontmatterFields: string[],
1474
- value: AgentConfig[K],
1475
- ): void => {
1476
- if (agentHasFrontmatterField(agent, ...frontmatterFields)) return;
1477
- const target = mutable();
1478
- if (value === undefined) delete target[field]; else target[field] = value;
1479
- anyFilled = true;
1480
- };
1481
-
1482
- if (override.description !== undefined) {
1483
- mutable().description = override.description;
1484
- anyFilled = true;
1485
- }
1486
- if (override.output !== undefined) {
1487
- fill("output", ["output"], override.output === false ? undefined : override.output);
1488
- }
1489
- if (override.outputMode !== undefined) {
1490
- fill("outputMode", ["outputMode"], override.outputMode);
1491
- }
1492
- if (override.defaultReads !== undefined) {
1493
- fill("defaultReads", ["defaultReads"], override.defaultReads === false ? undefined : [...override.defaultReads]);
1494
- }
1495
- if (override.model !== undefined && !agentHasFrontmatterField(agent, "model")) {
1496
- const target = mutable();
1497
- if (override.model === false) delete target.model; else target.model = override.model;
1498
- delete target.modelSource;
1499
- anyFilled = true;
1500
- }
1501
- if (override.defaultProvider !== undefined) {
1502
- fill("modelProvider", ["modelProvider", "defaultProvider"], override.defaultProvider === false ? undefined : override.defaultProvider);
1503
- }
1504
- if (override.fallbackModels !== undefined) {
1505
- fill(
1506
- "fallbackModels",
1507
- ["fallbackModels"],
1508
- override.fallbackModels === false ? undefined : [...override.fallbackModels],
1509
- );
1510
- }
1511
- if (override.fast !== undefined) {
1512
- fill("fast", ["fast"], override.fast);
1513
- }
1514
- if (override.thinking !== undefined) {
1515
- fill("thinking", ["thinking"], override.thinking === false ? undefined : override.thinking);
1516
- }
1517
- if (override.systemPromptMode !== undefined) {
1518
- fill("systemPromptMode", ["systemPromptMode"], override.systemPromptMode);
1519
- }
1520
- if (override.inheritProjectContext !== undefined) {
1521
- fill("inheritProjectContext", ["inheritProjectContext"], override.inheritProjectContext);
1522
- }
1523
- if (override.inheritGlobalContext !== undefined) {
1524
- fill("inheritGlobalContext", ["inheritGlobalContext"], override.inheritGlobalContext);
1525
- }
1526
- if (override.inheritSkills !== undefined) {
1527
- fill("inheritSkills", ["inheritSkills"], override.inheritSkills);
1528
- }
1529
- if (override.defaultContext !== undefined) {
1530
- fill("defaultContext", ["defaultContext"], override.defaultContext === false ? undefined : override.defaultContext);
1531
- }
1532
- if (override.acceptanceRole !== undefined) {
1533
- fill("acceptanceRole", ["acceptanceRole"], override.acceptanceRole === false ? undefined : override.acceptanceRole);
1534
- }
1535
- if (override.disabled !== undefined) {
1536
- // Custom agent files cannot set `disabled`, so project overrides replace user overrides.
1537
- mutable().disabled = override.disabled;
1538
- anyFilled = true;
1539
- }
1540
- if (override.skills !== undefined) {
1541
- fill("skills", ["skill", "skills"], override.skills === false ? undefined : [...override.skills]);
1542
- }
1543
- if (override.tools !== undefined && !agentHasFrontmatterField(agent, "tools")) {
1544
- applyToolsOverride(mutable(), override.tools);
1545
- anyFilled = true;
1546
- }
1547
- if (override.excludeTools !== undefined) {
1548
- fill("excludeTools", ["excludeTools"], override.excludeTools === false ? undefined : [...override.excludeTools]);
1549
- }
1550
- if (override.allowNestedSubagents !== undefined) {
1551
- fill("allowNestedSubagents", ["allowNestedSubagents"], override.allowNestedSubagents);
1552
- }
1553
- if (override.extensions !== undefined) {
1554
- fill("extensions", ["extensions"], override.extensions === false ? undefined : [...override.extensions]);
1555
- }
1556
- if (override.subagentOnlyExtensions !== undefined) {
1557
- fill(
1558
- "subagentOnlyExtensions",
1559
- ["subagentOnlyExtensions"],
1560
- override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions],
1561
- );
1562
- }
1563
- if (override.mutationTools !== undefined) {
1564
- fill("mutationTools", ["mutationTools"], override.mutationTools === false ? undefined : [...override.mutationTools]);
1565
- }
1566
- if (override.completionGuard !== undefined) {
1567
- fill("completionGuard", ["completionGuard"], override.completionGuard);
1568
- }
1569
- if (override.toolBudget !== undefined) {
1570
- fill("toolBudget", ["toolBudget"], override.toolBudget === false ? undefined : override.toolBudget);
1571
- }
1572
-
1573
- if (!anyFilled || !next) return agent;
1574
- next.override = { ...meta, base: agent.override?.base ?? cloneOverrideBase(agent) };
1481
+ const next = applyBuiltinOverride(agent, override, meta);
1575
1482
  const frontmatterFields = agentFrontmatterFields.get(agent);
1576
1483
  if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields);
1577
1484
  return next;
@@ -2342,6 +2249,61 @@ function extraUserAgentDirs(): string[] {
2342
2249
  .filter((dir) => dir.length > 0);
2343
2250
  }
2344
2251
 
2252
+ interface AgentScanDirs {
2253
+ dirs: string[];
2254
+ watchPaths: string[];
2255
+ }
2256
+
2257
+ function readConfiguredAgentScanDirs(filePath: string | null): string[] {
2258
+ if (!filePath) return [];
2259
+ try {
2260
+ const settings = readSettingsFileStrict(filePath);
2261
+ const subagents = settings.subagents;
2262
+ if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return [];
2263
+ const dirs = (subagents as { agentScanDirs?: unknown }).agentScanDirs;
2264
+ return Array.isArray(dirs) ? dirs.filter((dir): dir is string => typeof dir === "string" && dir.trim().length > 0) : [];
2265
+ } catch {
2266
+ return [];
2267
+ }
2268
+ }
2269
+
2270
+ function expandAgentScanDirPattern(pattern: string): AgentScanDirs {
2271
+ const expanded = expandHomePath(pattern.trim()).replace(/[\\/]+/g, path.sep);
2272
+ if (!expanded) return { dirs: [], watchPaths: [] };
2273
+ const wildcardMatches = [...expanded.matchAll(/\*/g)];
2274
+ if (wildcardMatches.length === 0) {
2275
+ const dir = path.resolve(expanded);
2276
+ return { dirs: fs.existsSync(dir) ? [dir] : [], watchPaths: [dir] };
2277
+ }
2278
+ const parts = expanded.split(path.sep);
2279
+ const wildcardIndex = parts.findIndex((part) => part.includes("*"));
2280
+ if (wildcardMatches.length !== 1 || wildcardIndex === -1 || parts[wildcardIndex] !== "*") return { dirs: [], watchPaths: [] };
2281
+ const base = path.resolve(parts.slice(0, wildcardIndex).join(path.sep) || path.sep);
2282
+ const rest = parts.slice(wildcardIndex + 1);
2283
+ let entries: fs.Dirent[];
2284
+ try {
2285
+ entries = fs.readdirSync(base, { withFileTypes: true });
2286
+ } catch {
2287
+ return { dirs: [], watchPaths: [base] };
2288
+ }
2289
+ const candidateDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(base, entry.name, ...rest));
2290
+ return {
2291
+ dirs: candidateDirs.filter((dir) => fs.existsSync(dir)),
2292
+ watchPaths: [base, ...candidateDirs],
2293
+ };
2294
+ }
2295
+
2296
+ function settingsAgentScanDirs(entries: string[]): AgentScanDirs {
2297
+ const dirs = new Set<string>();
2298
+ const watchPaths = new Set<string>();
2299
+ for (const entry of entries) {
2300
+ const expanded = expandAgentScanDirPattern(entry);
2301
+ for (const dir of expanded.dirs) dirs.add(dir);
2302
+ for (const watchPath of expanded.watchPaths) watchPaths.add(watchPath);
2303
+ }
2304
+ return { dirs: [...dirs], watchPaths: [...watchPaths] };
2305
+ }
2306
+
2345
2307
  export interface AgentDiscoveryAllResult {
2346
2308
  builtin: AgentConfig[];
2347
2309
  package: AgentConfig[];
@@ -2502,16 +2464,18 @@ function buildAgentDiscoverySources(cwd: string, preferredModelProvider?: string
2502
2464
  const userSettingsPath = getUserAgentSettingsPath();
2503
2465
  const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
2504
2466
  const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd);
2467
+ const userScanDirs = settingsAgentScanDirs(readConfiguredAgentScanDirs(userSettingsPath));
2468
+ const projectScanDirs = settingsAgentScanDirs(readConfiguredAgentScanDirs(projectSettingsPath));
2505
2469
 
2506
2470
  const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2507
- const userLoaded = [...extraUserAgentDirs(), userDirOld, userDirNew].map((dir, discoveryPriority): LoadedAgentDirectory => {
2471
+ const userLoaded = [...extraUserAgentDirs(), ...userScanDirs.dirs, userDirOld, userDirNew].map((dir, discoveryPriority): LoadedAgentDirectory => {
2508
2472
  const inspection = inspectAgentDefinitionDirectory(dir);
2509
2473
  return { dir, inspection, loaded: loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection) };
2510
2474
  });
2511
2475
  const projectInspections = new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2512
- const projectLoaded = projectAgentDirs.map((dir): LoadedAgentDirectory => {
2513
- const inspection = projectInspections.get(dir)!;
2514
- return { dir, inspection, loaded: loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0, undefined, inspection) };
2476
+ const projectLoaded = [...projectScanDirs.dirs, ...projectAgentDirs].map((dir, discoveryPriority): LoadedAgentDirectory => {
2477
+ const inspection = projectInspections.get(dir) ?? inspectAgentDefinitionDirectory(dir);
2478
+ return { dir, inspection, loaded: loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : discoveryPriority, undefined, inspection) };
2515
2479
  });
2516
2480
  const packageLoaded = packageSubagentPaths.agents.map((entry, index): LoadedAgentDirectory => {
2517
2481
  const inspection = inspectAgentDefinitionDirectory(entry.dir);
@@ -2523,6 +2487,8 @@ function buildAgentDiscoverySources(cwd: string, preferredModelProvider?: string
2523
2487
  ...(projectSettingsPath ? [projectSettingsPath] : []),
2524
2488
  ...projectDiscoveryWatchPaths(effectiveCwd),
2525
2489
  ...findProjectRootCandidates(effectiveCwd).map((root) => path.join(getProjectConfigDir(root), "settings.json")),
2490
+ ...userScanDirs.watchPaths,
2491
+ ...projectScanDirs.watchPaths,
2526
2492
  ...packageSubagentPaths.watchPaths,
2527
2493
  ]);
2528
2494
  for (const directory of userLoaded) addDirectoryWatchPaths(watchPaths, directory.dir, directory.inspection.files, inspectedAgentDefinitionDirectories(directory.inspection, directory.dir));
@@ -2530,6 +2496,7 @@ function buildAgentDiscoverySources(cwd: string, preferredModelProvider?: string
2530
2496
  const inspection = projectInspections.get(dir);
2531
2497
  addDirectoryWatchPaths(watchPaths, dir, inspection?.files ?? [], inspection ? inspectedAgentDefinitionDirectories(inspection, dir) : []);
2532
2498
  }
2499
+ for (const directory of projectLoaded) addDirectoryWatchPaths(watchPaths, directory.dir, directory.inspection.files, inspectedAgentDefinitionDirectories(directory.inspection, directory.dir));
2533
2500
  for (const directory of packageLoaded) addDirectoryWatchPaths(watchPaths, directory.dir, directory.inspection.files, inspectedAgentDefinitionDirectories(directory.inspection, directory.dir));
2534
2501
 
2535
2502
  const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
@@ -2665,7 +2632,16 @@ function configuredAgentsForScope(sources: AgentDiscoverySources, scope: AgentSc
2665
2632
  function discoveryDirectories(sources: AgentDiscoverySources, scope: AgentScope): AgentDefinitionDirectoryReport[] {
2666
2633
  const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
2667
2634
  if (scope !== "project") for (const directory of sources.userLoaded) directories.push(reportAgentDefinitionDirectory("user", directory.dir, directory.inspection));
2668
- if (scope !== "user") for (const dir of sources.projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, sources.projectInspections.get(dir)!));
2635
+ if (scope !== "user") {
2636
+ const reported = new Set<string>();
2637
+ for (const dir of sources.projectCandidateDirs) {
2638
+ reported.add(path.resolve(dir));
2639
+ directories.push(reportAgentDefinitionDirectory("project", dir, sources.projectInspections.get(dir)!));
2640
+ }
2641
+ for (const directory of sources.projectLoaded) {
2642
+ if (!reported.has(path.resolve(directory.dir))) directories.push(reportAgentDefinitionDirectory("project", directory.dir, directory.inspection));
2643
+ }
2644
+ }
2669
2645
  for (const directory of sources.packageLoaded) {
2670
2646
  if (directory.packageEntry && packageEntryIncluded(scope, directory.packageEntry.scope)) directories.push(reportAgentDefinitionDirectory("package", directory.dir, directory.inspection));
2671
2647
  }
@@ -2774,7 +2750,9 @@ function discoverAgentsUncached(cwd: string, scope: AgentScope, preferredModelPr
2774
2750
  const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
2775
2751
  const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2776
2752
  const builtinAgents = applyBuiltinOverrides(applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2777
- const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew].map((dir, discoveryPriority) => {
2753
+ const userScanDirs = settingsAgentScanDirs(userSettings.agentScanDirs ?? []);
2754
+ const projectScanDirs = settingsAgentScanDirs(projectSettings.agentScanDirs ?? []);
2755
+ const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), ...userScanDirs.dirs, userDirOld, userDirNew].map((dir, discoveryPriority) => {
2778
2756
  const inspection = inspectAgentDefinitionDirectory(dir);
2779
2757
  directories.push(reportAgentDefinitionDirectory("user", dir, inspection));
2780
2758
  return loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection);
@@ -2782,7 +2760,11 @@ function discoverAgentsUncached(cwd: string, scope: AgentScope, preferredModelPr
2782
2760
  const userAgents = applyCustomAgentOverrides(applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2783
2761
  const projectInspections = scope === "user" ? new Map<string, AgentDefinitionInspection>() : new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2784
2762
  if (scope !== "user") for (const dir of projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, projectInspections.get(dir)!));
2785
- const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0, undefined, projectInspections.get(dir)!));
2763
+ const projectLoaded = scope === "user" ? [] : [...projectScanDirs.dirs, ...projectAgentDirs].map((dir, discoveryPriority) => {
2764
+ const inspection = projectInspections.get(dir) ?? inspectAgentDefinitionDirectory(dir);
2765
+ if (!projectInspections.has(dir)) directories.push(reportAgentDefinitionDirectory("project", dir, inspection));
2766
+ return loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : discoveryPriority, undefined, inspection);
2767
+ });
2786
2768
  const projectAgents = applyCustomAgentOverrides(applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2787
2769
  const packageLoaded = packageSubagentPaths.agents.map((entry, index) => {
2788
2770
  const inspection = inspectAgentDefinitionDirectory(entry.dir);
@@ -20,5 +20,8 @@ export {
20
20
  type SubagentResultStatus,
21
21
  type SubagentRunMode,
22
22
  type Usage,
23
+ type ManagedWorktreeProvider,
24
+ type WorktreeNaming,
25
+ type WorktreeProvider,
23
26
  type WorkflowResourceProvenanceV1,
24
27
  } from "../shared/types.ts";
@@ -9,6 +9,7 @@ import { getAgentDir } from "../shared/utils.ts";
9
9
  import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
10
10
  import { validatePermissionConfig } from "../runs/shared/permissions.ts";
11
11
  import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
12
+ import { normalizeWorktreeBranchPrefix } from "../runs/shared/worktree.ts";
12
13
 
13
14
  const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
14
15
  const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
@@ -138,6 +139,16 @@ function validateMainWindowRendererConfig(value: unknown): void {
138
139
  }
139
140
 
140
141
  function validateConfig(config: Record<string, unknown>): void {
142
+ if (config.worktree !== undefined && typeof config.worktree !== "boolean") {
143
+ throw new Error("config.worktree must be a boolean");
144
+ }
145
+ if (config.worktreeProvider !== undefined && config.worktreeProvider !== "auto" && config.worktreeProvider !== "native" && config.worktreeProvider !== "worktrunk") {
146
+ throw new Error('config.worktreeProvider must be "auto", "native", or "worktrunk"');
147
+ }
148
+ if (config.worktreeBranchPrefix !== undefined) {
149
+ if (typeof config.worktreeBranchPrefix !== "string") throw new Error("config.worktreeBranchPrefix must be a string");
150
+ normalizeWorktreeBranchPrefix(config.worktreeBranchPrefix);
151
+ }
141
152
  if (config.defaultSubagentContext !== undefined && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
142
153
  throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
143
154
  }
@@ -229,6 +240,15 @@ export function loadConfig(): ExtensionConfig {
229
240
  return readConfigForUpdate(configPath);
230
241
  } catch (error) {
231
242
  if (error instanceof PrunedForkConfigError) throw error;
243
+ // An explicitly requested worktree provider/prefix must not be silently
244
+ // discarded and replaced by the built-in defaults after validation fails.
245
+ try {
246
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
247
+ if (raw && typeof raw === "object" && !Array.isArray(raw)
248
+ && (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix"))) throw error;
249
+ } catch (readError) {
250
+ if (readError === error) throw error;
251
+ }
232
252
  console.error(`Failed to load subagent config from '${configPath}':`, error);
233
253
  }
234
254
  return {};
@@ -41,7 +41,8 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
41
41
  lines.push("");
42
42
  }
43
43
  lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
44
- const controls = [input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
44
+ const acceptsPlainGuidance = input.index !== undefined || status.mode === "single";
45
+ const controls = [input.allowSteer === false || !acceptsPlainGuidance ? undefined : "type guidance", input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
45
46
  lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
46
47
  return lines.join("\n");
47
48
  }
@@ -81,6 +82,20 @@ function isTerminal(status: AsyncStatus): boolean {
81
82
  return status.state !== "queued" && status.state !== "running";
82
83
  }
83
84
 
85
+ function queueInspectorSteer(options: RunnerOptions, status: AsyncStatus, message: string): string {
86
+ if (options.allowSteer === false) throw new Error("Authority policy does not allow steer from this inspector.");
87
+ if (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be steered.`);
88
+ const runningIndexes = (status.steps ?? []).map((step, index) => step.status === "running" ? index : undefined).filter((index): index is number => index !== undefined);
89
+ const targetIndex = options.index ?? (status.mode === "single" ? 0 : undefined);
90
+ if (targetIndex === undefined && runningIndexes.length === 0) throw new Error("No running child is available to steer. Open a child-specific inspector for a pending child.");
91
+ requestAsyncSteer(options.asyncDir, {
92
+ message,
93
+ ...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
94
+ source: "herdr-inspector",
95
+ });
96
+ return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
97
+ }
98
+
84
99
  export function submitInspectorControl(options: RunnerOptions, line: string): string {
85
100
  const command = line.trim();
86
101
  if (!command || command === "status") return "Status refreshed.";
@@ -93,22 +108,13 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
93
108
  return `Stop requested for run ${options.runId}.`;
94
109
  }
95
110
  if (command.startsWith("steer ")) {
96
- if (options.allowSteer === false) throw new Error("Authority policy does not allow steer from this inspector.");
97
111
  const message = command.slice("steer ".length).trim();
98
112
  if (!message) throw new Error("steer requires a message.");
99
- if (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be steered.`);
100
- const runningIndexes = (status.steps ?? []).map((step, index) => step.status === "running" ? index : undefined).filter((index): index is number => index !== undefined);
101
- const targetIndex = options.index ?? (status.mode === "single" ? 0 : undefined);
102
- if (targetIndex === undefined && runningIndexes.length === 0) throw new Error("No running child is available to steer. Open a child-specific inspector for a pending child.");
103
- requestAsyncSteer(options.asyncDir, {
104
- message,
105
- ...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
106
- source: "herdr-inspector",
107
- });
108
- return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
113
+ return queueInspectorSteer(options, status, message);
109
114
  }
110
115
  if (command.startsWith("reply ")) throw new Error("Supervisor replies are owned by the parent Pi session; use subagent_supervisor/intercom there.");
111
- throw new Error("Unknown control. Use steer <message>, stop, or status.");
116
+ if (options.index === undefined && status.mode !== "single") throw new Error("Plain guidance requires a child-specific inspector. Use steer <message> to target all running children from the aggregate inspector.");
117
+ return queueInspectorSteer(options, status, command);
112
118
  }
113
119
 
114
120
  export function runInspector(argv = process.argv.slice(2)): void {
@@ -271,7 +271,6 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
271
271
  if (liveWorkflowRunIds.has(owner.runId)) return { state: "retained", reason: "workflow controller is still live" };
272
272
  for (const step of status.steps ?? []) {
273
273
  const label = step.workflowKey ?? step.agent;
274
- if (step.status === "pending" || step.status === "running" || step.status === "paused") return { state: "retained", reason: `workflow child ${label} is still ${step.status}` };
275
274
  if (typeof step.async !== "boolean") return { state: "retained", reason: `workflow child ${label} is missing async classification` };
276
275
  if (!step.async) continue;
277
276
  if (!step.runId) return { state: "retained", reason: `async workflow child ${label} is missing run id` };
@@ -16,7 +16,7 @@ import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
16
  import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
18
18
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
- import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
19
+ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
20
20
  import type { RunnerStep } from "../shared/parallel-utils.ts";
21
21
  import type { ContextMode } from "../shared/context-mode.ts";
22
22
  import { resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
@@ -31,7 +31,7 @@ import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout
31
31
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
32
32
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
33
33
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
34
- import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
34
+ import { resolveExpectedWorktreeAgentCwd, resolveWorktreeProvider, shouldDeferWorktreeCwd, WORKTREE_AGENT_CWD_PLACEHOLDER } from "../shared/worktree.ts";
35
35
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
36
36
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
37
37
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
@@ -178,6 +178,8 @@ interface AsyncChainParams {
178
178
  worktreeSetupHook?: string;
179
179
  worktreeSetupHookTimeoutMs?: number;
180
180
  worktreeBaseDir?: string;
181
+ worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
182
+ worktreeBranchPrefix?: string;
181
183
  controlConfig?: ResolvedControlConfig;
182
184
  controlIntercomTarget?: string;
183
185
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
@@ -245,6 +247,8 @@ interface AsyncSingleParams {
245
247
  worktreeSetupHook?: string;
246
248
  worktreeSetupHookTimeoutMs?: number;
247
249
  worktreeBaseDir?: string;
250
+ worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
251
+ worktreeBranchPrefix?: string;
248
252
  worktree?: boolean;
249
253
  controlConfig?: ResolvedControlConfig;
250
254
  intercomBridge?: IntercomBridgeConfig;
@@ -310,6 +314,8 @@ export interface AsyncRunnerStepBuildParams {
310
314
  waitToolEnabled?: boolean;
311
315
  waitToolDefaultTimeoutMs?: number;
312
316
  worktreeBaseDir?: string;
317
+ worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
318
+ worktreeBranchPrefix?: string;
313
319
  asyncDir: string;
314
320
  outputBaseDir?: string;
315
321
  validateOutputBindings?: boolean;
@@ -740,6 +746,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
740
746
  thinkingOverridesByFlatIndex,
741
747
  maxSubagentDepth,
742
748
  worktreeBaseDir,
749
+ worktreeProvider,
750
+ worktreeBranchPrefix,
743
751
  asyncDir,
744
752
  } = params;
745
753
  const outputBaseDir = params.outputBaseDir;
@@ -747,6 +755,15 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
747
755
  const chainSkills = params.chainSkills ?? [];
748
756
  const availableModels = params.availableModels;
749
757
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
758
+ let managedWorktreeProvider: "native" | "worktrunk" | undefined;
759
+ try {
760
+ if (chain.some((step) => "worktree" in step && step.worktree === true)) {
761
+ const resolved = resolveWorktreeProvider(worktreeProvider, worktreeBaseDir);
762
+ managedWorktreeProvider = shouldDeferWorktreeCwd(worktreeProvider, worktreeBaseDir) ? "worktrunk" : resolved;
763
+ }
764
+ } catch (error) {
765
+ return { error: error instanceof Error ? error.message : String(error) };
766
+ }
750
767
  const progressDir = params.progressDir ?? runnerCwd;
751
768
  const graphChain: ChainStep[] = params.attachRoot
752
769
  ? [{
@@ -873,7 +890,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
873
890
  if (validationError) throw new AsyncStartValidationError(validationError);
874
891
  let taskTemplate = s.task ?? "{previous}";
875
892
  taskTemplate = taskTemplate.replace(/\{task\}/g, originalTask ?? "");
876
- taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
893
+ taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, behaviorCwd ?? runnerCwd);
877
894
  const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
878
895
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
879
896
 
@@ -1058,7 +1075,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1058
1075
  return {
1059
1076
  parallel: s.parallel.map((t, taskIndex) => {
1060
1077
  let behaviorCwd: string | undefined;
1061
- if (s.worktree) {
1078
+ if (s.worktree && managedWorktreeProvider === "worktrunk") {
1079
+ behaviorCwd = WORKTREE_AGENT_CWD_PLACEHOLDER;
1080
+ } else if (s.worktree && managedWorktreeProvider === "native") {
1062
1081
  try {
1063
1082
  behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex, worktreeBaseDir);
1064
1083
  } catch {
@@ -1113,7 +1132,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1113
1132
  }
1114
1133
  const sequential = s as SequentialStep;
1115
1134
  let behaviorCwd: string | undefined;
1116
- if (sequential.worktree) {
1135
+ if (sequential.worktree && managedWorktreeProvider === "worktrunk") {
1136
+ behaviorCwd = WORKTREE_AGENT_CWD_PLACEHOLDER;
1137
+ } else if (sequential.worktree && managedWorktreeProvider === "native") {
1117
1138
  try {
1118
1139
  behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, 0, worktreeBaseDir);
1119
1140
  } catch {
@@ -1183,6 +1204,8 @@ export function executeAsyncChain(
1183
1204
  worktreeSetupHook,
1184
1205
  worktreeSetupHookTimeoutMs,
1185
1206
  worktreeBaseDir,
1207
+ worktreeProvider,
1208
+ worktreeBranchPrefix,
1186
1209
  controlConfig,
1187
1210
  controlIntercomTarget,
1188
1211
  childIntercomTarget,
@@ -1239,6 +1262,8 @@ export function executeAsyncChain(
1239
1262
  waitToolEnabled: params.waitToolEnabled,
1240
1263
  waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
1241
1264
  worktreeBaseDir,
1265
+ worktreeProvider,
1266
+ worktreeBranchPrefix,
1242
1267
  asyncDir,
1243
1268
  fast: params.fast,
1244
1269
  toolBudget: params.toolBudget,
@@ -1314,6 +1339,8 @@ export function executeAsyncChain(
1314
1339
  worktreeSetupHook,
1315
1340
  worktreeSetupHookTimeoutMs,
1316
1341
  worktreeBaseDir,
1342
+ worktreeProvider,
1343
+ worktreeBranchPrefix,
1317
1344
  controlConfig,
1318
1345
  toolBudget: params.toolBudget,
1319
1346
  usageBudget: params.usageBudget,
@@ -1511,6 +1538,8 @@ export function executeAsyncSingle(
1511
1538
  worktreeSetupHook,
1512
1539
  worktreeSetupHookTimeoutMs,
1513
1540
  worktreeBaseDir,
1541
+ worktreeProvider,
1542
+ worktreeBranchPrefix,
1514
1543
  controlConfig,
1515
1544
  controlIntercomTarget,
1516
1545
  childIntercomTarget,
@@ -1556,7 +1585,18 @@ export function executeAsyncSingle(
1556
1585
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1557
1586
  }
1558
1587
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1559
- const instructionCwd = params.worktree === true
1588
+ let managedWorktreeProvider: "native" | "worktrunk" | undefined;
1589
+ if (params.worktree === true) {
1590
+ try {
1591
+ const resolved = resolveWorktreeProvider(params.worktreeProvider, worktreeBaseDir);
1592
+ managedWorktreeProvider = shouldDeferWorktreeCwd(params.worktreeProvider, worktreeBaseDir) ? "worktrunk" : resolved;
1593
+ } catch (error) {
1594
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1595
+ }
1596
+ }
1597
+ const instructionCwd = params.worktree === true && managedWorktreeProvider === "worktrunk"
1598
+ ? WORKTREE_AGENT_CWD_PLACEHOLDER
1599
+ : params.worktree === true && managedWorktreeProvider === "native"
1560
1600
  ? resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s0`, 0, worktreeBaseDir)
1561
1601
  : runnerCwd;
1562
1602
  const readExistenceCwd = params.worktree === true ? runnerCwd : instructionCwd;
@@ -1610,7 +1650,11 @@ export function executeAsyncSingle(
1610
1650
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
1611
1651
  // absolute paths pass through; relative paths resolve against the child cwd.
1612
1652
  const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
1613
- const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, readExistenceCwd) : [];
1653
+ const readPaths = Array.isArray(reads)
1654
+ ? managedWorktreeProvider === "worktrunk"
1655
+ ? resolveExistingReadInstructionPaths(reads, instructionCwd, readExistenceCwd)
1656
+ : resolveExistingReadPaths(reads, readExistenceCwd)
1657
+ : [];
1614
1658
  const readsInstruction = readPaths.length > 0
1615
1659
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
1616
1660
  : "";
@@ -1901,6 +1945,8 @@ export function executeAsyncSingle(
1901
1945
  worktreeSetupHook,
1902
1946
  worktreeSetupHookTimeoutMs,
1903
1947
  worktreeBaseDir,
1948
+ worktreeProvider,
1949
+ worktreeBranchPrefix,
1904
1950
  controlConfig,
1905
1951
  timeoutMs,
1906
1952
  deadlineAt,
@@ -45,6 +45,8 @@ export type AsyncResumeTarget = {
45
45
  sessionName?: string;
46
46
  index: number;
47
47
  cwd?: string;
48
+ /** True when cwd is the retained managed worktree recorded by the handoff. */
49
+ managedWorktree?: boolean;
48
50
  sessionFile?: string;
49
51
  model?: string;
50
52
  thinking?: string;
@@ -569,6 +571,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
569
571
  ...(statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName ? { sessionName: statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName } : {}),
570
572
  index,
571
573
  ...(resumeCwd ? { cwd: resumeCwd } : {}),
574
+ ...(managedWorktreeCwd ? { managedWorktree: true } : {}),
572
575
  ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
573
576
  ...(stepModel ? { model: stepModel } : {}),
574
577
  ...(stepThinking ? { thinking: stepThinking } : {}),