pi-subagents 0.62.0 → 0.64.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 (57) hide show
  1. package/CHANGELOG.md +52 -1
  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/docs/watchdog.md +92 -114
  8. package/package.json +1 -1
  9. package/skills/pi-subagents/references/execution-controls.md +4 -3
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  11. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  12. package/src/agents/agent-management.ts +22 -4
  13. package/src/agents/agents.ts +107 -125
  14. package/src/api/shared-types.ts +3 -0
  15. package/src/extension/config.ts +20 -0
  16. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  17. package/src/runs/background/active-async-capacity.ts +0 -1
  18. package/src/runs/background/async-execution.ts +56 -7
  19. package/src/runs/background/async-resume.ts +3 -0
  20. package/src/runs/background/async-status.ts +18 -2
  21. package/src/runs/background/notify.ts +54 -3
  22. package/src/runs/background/run-status.ts +22 -2
  23. package/src/runs/background/subagent-runner.ts +53 -3
  24. package/src/runs/background/wait-completions.ts +13 -0
  25. package/src/runs/foreground/execution.ts +7 -1
  26. package/src/runs/foreground/subagent-executor.ts +56 -4
  27. package/src/runs/shared/acceptance.ts +25 -9
  28. package/src/runs/shared/async-status-projection.ts +11 -43
  29. package/src/runs/shared/lane-metadata.ts +24 -3
  30. package/src/runs/shared/parallel-handoff.ts +4 -0
  31. package/src/runs/shared/pi-args.ts +9 -6
  32. package/src/runs/shared/subagent-control.ts +4 -2
  33. package/src/runs/shared/task-intent.ts +5 -2
  34. package/src/runs/shared/worktree.ts +467 -63
  35. package/src/shared/types.ts +38 -2
  36. package/src/shared/utils.ts +18 -7
  37. package/src/slash/subagents-admin.ts +24 -12
  38. package/src/tui/fleet-status.ts +61 -2
  39. package/src/tui/fleet.ts +12 -7
  40. package/src/tui/render.ts +227 -14
  41. package/src/watchdog/child-status.ts +54 -33
  42. package/src/watchdog/diff-tool.ts +77 -0
  43. package/src/watchdog/emission-guard.ts +5 -3
  44. package/src/watchdog/guidance.ts +20 -0
  45. package/src/watchdog/register-child.ts +16 -13
  46. package/src/watchdog/register-main.ts +10 -9
  47. package/src/watchdog/render.ts +4 -5
  48. package/src/watchdog/review.ts +15 -4
  49. package/src/watchdog/rules.ts +70 -0
  50. package/src/watchdog/runtime.ts +75 -92
  51. package/src/watchdog/scope.ts +0 -11
  52. package/src/watchdog/settings.ts +48 -104
  53. package/src/watchdog/types.ts +18 -32
  54. package/src/watchdog/warning-format.ts +0 -1
  55. package/src/workflows/chat-progress.ts +3 -2
  56. package/src/workflows/workflow-checklist.ts +439 -0
  57. package/src/workflows/workflow-preflight.ts +28 -1
@@ -250,13 +250,25 @@ function withDeclaredExtensionPaths(config: AgentConfig, filePath: string): Agen
250
250
  export function editableAgentConfig(agent: AgentConfig): AgentConfig {
251
251
  const { extensions: _extensions, ...withoutExtensions } = agent;
252
252
  const base = agent.override?.base;
253
+ const description = base?.description ?? agent.description;
254
+ const frontmatterFields = agent.source === "builtin" || agent.source === "runtime" ? undefined : readAgentFrontmatterFields(agent.filePath);
255
+ const hasDeclaredField = (...fields: string[]) => frontmatterFields === undefined || fields.some((field) => frontmatterFields.has(field));
256
+ const withoutSettingsDefaults = (config: AgentConfig): AgentConfig => {
257
+ if (!frontmatterFields) return config;
258
+ const next = { ...config };
259
+ if (!hasDeclaredField("model")) delete next.model;
260
+ if (!hasDeclaredField("thinking")) delete next.thinking;
261
+ return next;
262
+ };
253
263
  const {
254
264
  override: _override,
265
+ description: _description,
255
266
  output: _output,
256
267
  outputMode: _outputMode,
257
268
  defaultReads: _defaultReads,
258
269
  model: _model,
259
270
  fallbackModels: _fallbackModels,
271
+ fast: _fast,
260
272
  thinking: _thinking,
261
273
  systemPromptMode: _systemPromptMode,
262
274
  inheritProjectContext: _inheritProjectContext,
@@ -271,26 +283,30 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
271
283
  tools: _tools,
272
284
  excludeTools: _excludeTools,
273
285
  mcpDirectTools: _mcpDirectTools,
286
+ allowNestedSubagents: _allowNestedSubagents,
274
287
  subagentOnlyExtensions: _subagentOnlyExtensions,
275
288
  mutationTools: _mutationTools,
276
289
  completionGuard: _completionGuard,
290
+ toolBudget: _toolBudget,
277
291
  ...editable
278
292
  } = withoutExtensions;
279
293
  if (!base) {
280
- return withDeclaredExtensionPaths({
294
+ return withDeclaredExtensionPaths(withoutSettingsDefaults({
281
295
  ...withoutExtensions,
282
296
  ...(agent.extensionsFromDefault ? {} : agent.extensions !== undefined ? { extensions: [...agent.extensions] } : {}),
283
- }, agent.filePath);
297
+ }), agent.filePath);
284
298
  }
285
299
 
286
300
  return withDeclaredExtensionPaths({
287
301
  ...editable,
302
+ description,
288
303
  ...(base.output !== undefined ? { output: base.output } : {}),
289
304
  ...(base.outputMode !== undefined ? { outputMode: base.outputMode } : {}),
290
305
  ...(base.defaultReads !== undefined ? { defaultReads: [...base.defaultReads] } : {}),
291
- ...(base.model !== undefined ? { model: base.model } : {}),
306
+ ...(base.model !== undefined && hasDeclaredField("model") ? { model: base.model } : {}),
292
307
  ...(base.fallbackModels !== undefined ? { fallbackModels: [...base.fallbackModels] } : {}),
293
- ...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
308
+ ...(base.fast !== undefined ? { fast: base.fast } : {}),
309
+ ...(base.thinking !== undefined && hasDeclaredField("thinking") ? { thinking: base.thinking } : {}),
294
310
  systemPromptMode: base.systemPromptMode,
295
311
  inheritProjectContext: base.inheritProjectContext,
296
312
  inheritGlobalContext: base.inheritGlobalContext,
@@ -304,10 +320,12 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
304
320
  ...(base.tools !== undefined ? { tools: [...base.tools] } : {}),
305
321
  ...(base.excludeTools !== undefined ? { excludeTools: [...base.excludeTools] } : {}),
306
322
  ...(base.mcpDirectTools !== undefined ? { mcpDirectTools: [...base.mcpDirectTools] } : {}),
323
+ ...(base.allowNestedSubagents !== undefined ? { allowNestedSubagents: base.allowNestedSubagents } : {}),
307
324
  ...(base.extensions !== undefined ? { extensions: [...base.extensions] } : {}),
308
325
  ...(base.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...base.subagentOnlyExtensions] } : {}),
309
326
  ...(base.mutationTools !== undefined ? { mutationTools: [...base.mutationTools] } : {}),
310
327
  ...(base.completionGuard !== undefined ? { completionGuard: base.completionGuard } : {}),
328
+ ...(base.toolBudget !== undefined ? { toolBudget: base.toolBudget } : {}),
311
329
  }, agent.filePath);
312
330
  }
313
331
 
@@ -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` };