pi-subagents 0.52.1 → 0.54.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 (67) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +4 -0
  3. package/docs/configuration.md +12 -2
  4. package/docs/extension-api.md +3 -1
  5. package/docs/models.md +17 -3
  6. package/docs/workflows.md +2 -0
  7. package/index.ts +10 -1
  8. package/package.json +2 -1
  9. package/prompts/council.md +51 -0
  10. package/skills/council-mode/SKILL.md +231 -0
  11. package/skills/pi-subagents/SKILL.md +5 -1
  12. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
  13. package/skills/pi-subagents/references/execution-controls.md +13 -0
  14. package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
  15. package/src/agents/agent-management.ts +107 -8
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +96 -37
  18. package/src/agents/builtin-names.ts +9 -0
  19. package/src/agents/runtime-agent-registry.ts +418 -0
  20. package/src/api/agents.ts +7 -0
  21. package/src/api/preflight.ts +8 -3
  22. package/src/extension/config.ts +3 -0
  23. package/src/extension/doctor.ts +11 -0
  24. package/src/extension/fanout-child.ts +3 -2
  25. package/src/extension/index.ts +20 -4
  26. package/src/extension/public-execution.ts +1 -1
  27. package/src/extension/rpc.ts +41 -1
  28. package/src/extension/schemas.ts +6 -3
  29. package/src/extension/tool-description.ts +2 -2
  30. package/src/extension/tool-result.ts +19 -0
  31. package/src/inspectors/herdr/client.ts +3 -3
  32. package/src/runs/background/async-execution.ts +12 -6
  33. package/src/runs/background/async-job-tracker.ts +4 -3
  34. package/src/runs/background/async-resume.ts +2 -1
  35. package/src/runs/background/async-retention.ts +1 -1
  36. package/src/runs/background/async-status-snapshot.ts +14 -5
  37. package/src/runs/background/auto-drain.ts +1 -0
  38. package/src/runs/background/chain-root-attachment.ts +5 -0
  39. package/src/runs/background/result-watcher.ts +9 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -0
  41. package/src/runs/background/subagent-runner.ts +32 -5
  42. package/src/runs/background/subagent-wait.ts +9 -5
  43. package/src/runs/background/terminal-run-index.ts +15 -6
  44. package/src/runs/background/wait-completions.ts +2 -0
  45. package/src/runs/background/wait-tool.ts +5 -3
  46. package/src/runs/foreground/execution.ts +34 -2
  47. package/src/runs/foreground/subagent-executor.ts +196 -52
  48. package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
  49. package/src/runs/shared/acceptance.ts +44 -1
  50. package/src/runs/shared/model-exclusions.ts +242 -0
  51. package/src/runs/shared/model-fallback.ts +72 -16
  52. package/src/runs/shared/model-scope.ts +106 -39
  53. package/src/runs/shared/pi-args.ts +34 -1
  54. package/src/runs/shared/subagent-control.ts +25 -3
  55. package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
  56. package/src/shared/fork-context.ts +17 -1
  57. package/src/shared/model-info.ts +20 -0
  58. package/src/shared/settings.ts +2 -2
  59. package/src/shared/types.ts +47 -2
  60. package/src/slash/slash-commands.ts +20 -6
  61. package/src/slash/slash-live-state.ts +3 -3
  62. package/src/tui/fleet-status.ts +86 -1
  63. package/src/tui/fleet.ts +55 -2
  64. package/src/tui/render.ts +73 -3
  65. package/src/watchdog/permission-arbiter.ts +59 -51
  66. package/src/workflows/scripted-workflow.ts +100 -12
  67. package/src/workflows/workflow-receipt.ts +140 -0
@@ -36,10 +36,12 @@ import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
36
36
  import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
37
37
  import { getProjectConfigDir } from "../shared/utils.ts";
38
38
  import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
39
+ import { listRuntimeAgentConfigs, mergeRuntimeAgents, type RuntimeAgentOwner } from "./runtime-agent-registry.ts";
40
+ import { listExternalJobProviders } from "../api/external-job-provider.ts";
39
41
 
40
42
  type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
41
43
  type ManagementScope = "user" | "project";
42
- type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string };
44
+ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner };
43
45
 
44
46
  interface ManagementParams {
45
47
  action?: string;
@@ -142,7 +144,7 @@ function diagnosticsForScope(diagnostics: AgentDiscoveryDiagnostic[] | undefined
142
144
  return diagnostics?.filter((diagnostic) => diagnostic.source !== excludedSource);
143
145
  }
144
146
 
145
- const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
147
+ const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3, runtime: 4 };
146
148
 
147
149
  // Returns the highest-precedence definition for a resolved canonical name (project > user > package > builtin),
148
150
  // matching mergeAgentsForScope for "both", including disabled agents so disable/enable can locate hidden targets.
@@ -213,6 +215,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
213
215
  const base = agent.override?.base;
214
216
  const {
215
217
  override: _override,
218
+ outputMode: _outputMode,
216
219
  model: _model,
217
220
  fallbackModels: _fallbackModels,
218
221
  thinking: _thinking,
@@ -240,6 +243,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
240
243
 
241
244
  return withDeclaredExtensionPaths({
242
245
  ...editable,
246
+ ...(base.outputMode !== undefined ? { outputMode: base.outputMode } : {}),
243
247
  ...(base.model !== undefined ? { model: base.model } : {}),
244
248
  ...(base.fallbackModels !== undefined ? { fallbackModels: [...base.fallbackModels] } : {}),
245
249
  ...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
@@ -311,6 +315,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
311
315
  if (hasKey(cfg, "acceptance")) changed("acceptance");
312
316
  if (hasKey(cfg, "acceptanceRole")) changed("acceptanceRole");
313
317
  if (hasKey(cfg, "output")) changed("output");
318
+ if (hasKey(cfg, "outputMode")) changed("outputMode");
314
319
  if (hasKey(cfg, "reads")) changed("defaultReads");
315
320
  if (hasKey(cfg, "progress")) changed("defaultProgress");
316
321
  if (hasKey(cfg, "maxSubagentDepth")) changed("maxSubagentDepth");
@@ -501,6 +506,10 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
501
506
  else if (typeof cfg.output === "string") target.output = cfg.output;
502
507
  else return "config.output must be a string or false when provided.";
503
508
  }
509
+ if (hasKey(cfg, "outputMode")) {
510
+ if (cfg.outputMode === "inline" || cfg.outputMode === "file-only") target.outputMode = cfg.outputMode;
511
+ else return "config.outputMode must be 'inline' or 'file-only' when provided.";
512
+ }
504
513
  if (hasKey(cfg, "reads")) {
505
514
  if (cfg.reads === false || cfg.reads === "") delete target.defaultReads;
506
515
  else if (typeof cfg.reads === "string") {
@@ -582,9 +591,83 @@ function renamePath(currentPath: string, newName: string, scope: ManagementScope
582
591
  return { filePath };
583
592
  }
584
593
 
594
+ function packageSourceLabel(agent: AgentConfig): string {
595
+ if (!agent.packageSourceName) return agent.source;
596
+ return agent.packageSourceVersion ? `${agent.packageSourceName}@${agent.packageSourceVersion}` : agent.packageSourceName;
597
+ }
598
+
599
+ type ExternalJobProviderStatus =
600
+ | { ok: true; names: Set<string> }
601
+ | { ok: false; error: string };
602
+
603
+ function errorMessage(error: unknown): string {
604
+ return error instanceof Error ? error.message : String(error);
605
+ }
606
+
607
+ function registeredExternalJobProviderStatus(): ExternalJobProviderStatus {
608
+ try {
609
+ return { ok: true, names: new Set(listExternalJobProviders().map((provider) => provider.name)) };
610
+ } catch (error) {
611
+ return { ok: false, error: errorMessage(error) };
612
+ }
613
+ }
614
+
615
+ function externalJobProviderSuffix(provider: string, names: Set<string> | undefined): string {
616
+ if (!names) return "?";
617
+ return names.has(provider) ? "✓" : "missing";
618
+ }
619
+
620
+ function runnerListBadge(agent: AgentConfig, providerNames: Set<string> | undefined): string | undefined {
621
+ if (agent.runner?.type === "external-job") return `external-job:${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
622
+ if (agent.runner?.type === "external-cli") return "external-cli";
623
+ return undefined;
624
+ }
625
+
626
+ function formatAgentListLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
627
+ const source = agent.source === "package" ? packageSourceLabel(agent) : agent.source;
628
+ const parts = [
629
+ source,
630
+ runnerListBadge(agent, providerNames),
631
+ agent.defaultContext ? `context: ${agent.defaultContext}` : undefined,
632
+ agent.aliases?.length ? `aliases: ${agent.aliases.join(", ")}` : undefined,
633
+ ].filter((part): part is string => Boolean(part));
634
+ return `- ${agent.name} (${parts.join(", ")}): ${agent.description}`;
635
+ }
636
+
637
+ function formatAgentListSections(agents: AgentConfig[], providerNames: Set<string> | undefined): string[] {
638
+ if (agents.length === 0) return ["- (none)"];
639
+ const sections: Array<[AgentSource, string]> = [
640
+ ["package", "Package agents"],
641
+ ["user", "User agents"],
642
+ ["project", "Project agents"],
643
+ ["runtime", "Runtime agents"],
644
+ ["builtin", "Builtin agents"],
645
+ ];
646
+ const lines: string[] = [];
647
+ for (const [source, label] of sections) {
648
+ const matches = agents.filter((agent) => agent.source === source);
649
+ if (matches.length === 0) continue;
650
+ if (lines.length > 0) lines.push("");
651
+ lines.push(label, ...matches.map((agent) => formatAgentListLine(agent, providerNames)));
652
+ }
653
+ return lines;
654
+ }
655
+
656
+ function formatRunnerDetail(agent: AgentConfig, providerNames: Set<string> | undefined): string | undefined {
657
+ if (!agent.runner) return undefined;
658
+ if (agent.runner.type === "external-job") return `Runner: external-job via ${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
659
+ if (agent.runner.type === "external-cli") return `Runner: external-cli ${agent.runner.command}`;
660
+ return `Runner: ${JSON.stringify(agent.runner)}`;
661
+ }
662
+
585
663
  function formatAgentDetail(agent: AgentConfig): string {
586
664
  const tools = [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
665
+ const providerStatus = registeredExternalJobProviderStatus();
587
666
  const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
667
+ if (agent.source === "package" && agent.packageSourceName) {
668
+ lines.push(`Source package: ${packageSourceLabel(agent)}`);
669
+ if (agent.packageSourceRoot) lines.push(`Package root: ${agent.packageSourceRoot}`);
670
+ }
588
671
  if (agent.packageName) {
589
672
  lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
590
673
  lines.push(`Package: ${agent.packageName}`);
@@ -596,7 +679,12 @@ function formatAgentDetail(agent: AgentConfig): string {
596
679
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
597
680
  if (agent.skillPath?.length) lines.push(`Skill paths: ${agent.skillPath.join(", ")}`);
598
681
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
599
- if (agent.runner) lines.push(`Runner: ${JSON.stringify(agent.runner)}`);
682
+ const runnerDetail = formatRunnerDetail(agent, providerStatus.ok ? providerStatus.names : undefined);
683
+ if (runnerDetail) {
684
+ lines.push(runnerDetail);
685
+ if (agent.runner?.type === "external-job" && !providerStatus.ok) lines.push(`External-job provider registry unavailable: ${providerStatus.error}`);
686
+ if (agent.runner?.type === "external-job" && agent.runner.options) lines.push(`Runner options: ${JSON.stringify(agent.runner.options)}`);
687
+ }
600
688
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
601
689
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
602
690
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
@@ -610,6 +698,7 @@ function formatAgentDetail(agent: AgentConfig): string {
610
698
  if (agent.subagentOnlyExtensions !== undefined) lines.push(`Subagent-only extensions: ${agent.subagentOnlyExtensions.length ? agent.subagentOnlyExtensions.join(", ") : "(none)"}`);
611
699
  if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
612
700
  if (agent.output) lines.push(`Output: ${agent.output}`);
701
+ if (agent.outputMode) lines.push(`Output mode: ${agent.outputMode}`);
613
702
  if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
614
703
  if (agent.defaultProgress) lines.push("Progress: true");
615
704
  if (agent.maxSubagentDepth !== undefined) lines.push(`Max subagent depth: ${agent.maxSubagentDepth}`);
@@ -623,7 +712,17 @@ function formatAgentDetail(agent: AgentConfig): string {
623
712
  export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
624
713
  const scope = normalizeListScope(params.agentScope) ?? "both";
625
714
  const d = discoverAgentsAll(ctx.cwd);
626
- const scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package)
715
+ let scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package);
716
+ if (ctx.runtimeAgentOwner && listRuntimeAgentConfigs(ctx.runtimeAgentOwner).length > 0) {
717
+ const configuredAgents: AgentConfig[] = [
718
+ ...d.builtin,
719
+ ...d.package,
720
+ ...d.user,
721
+ ...d.project,
722
+ ];
723
+ scopedAgents = mergeRuntimeAgents(ctx.runtimeAgentOwner, { agents: scopedAgents }, configuredAgents).agents;
724
+ }
725
+ scopedAgents = scopedAgents
627
726
  .sort((a, b) => a.name.localeCompare(b.name));
628
727
  const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
629
728
  const visibleAgents = scopedAgents.filter((a) => !a.disabled);
@@ -635,16 +734,16 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
635
734
  ...(ctx.config?.proactiveSkillSubagents !== undefined ? { config: ctx.config.proactiveSkillSubagents } : {}),
636
735
  discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
637
736
  });
737
+ const providerStatus = registeredExternalJobProviderStatus();
638
738
  const lines = [
639
739
  "Executable agents:",
640
- ...(agents.length
641
- ? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`)
642
- : ["- (none)"]),
740
+ ...formatAgentListSections(agents, providerStatus.ok ? providerStatus.names : undefined),
643
741
  ...(restrictedAgents.length ? [
644
742
  "",
645
743
  `Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
646
- ...restrictedAgents.map((a) => `- ${a.name} (${a.source}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`),
744
+ ...restrictedAgents.map((a) => formatAgentListLine(a, providerStatus.ok ? providerStatus.names : undefined)),
647
745
  ] : []),
746
+ ...(!providerStatus.ok && [...agents, ...restrictedAgents].some((agent) => agent.runner?.type === "external-job") ? ["", `External-job provider registry unavailable: ${providerStatus.error}`] : []),
648
747
  ...(d.agentDiagnostics?.length ? [
649
748
  "",
650
749
  "Invalid agent definitions:",
@@ -28,6 +28,7 @@ export const KNOWN_FIELDS = new Set([
28
28
  "extensions",
29
29
  "subagentOnlyExtensions",
30
30
  "output",
31
+ "outputMode",
31
32
  "defaultReads",
32
33
  "defaultProgress",
33
34
  "interactive",
@@ -113,6 +114,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
113
114
  }
114
115
 
115
116
  if (config.output) lines.push(`output: ${config.output}`);
117
+ if (config.outputMode || preserve("outputMode")) lines.push(`outputMode: ${config.outputMode ?? ""}`);
116
118
 
117
119
  const readsValue = joinComma(config.defaultReads);
118
120
  if (readsValue) lines.push(`defaultReads: ${readsValue}`);
@@ -16,6 +16,7 @@ import { mergeAgentsForScope } from "./agent-selection.ts";
16
16
  import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.ts";
17
17
  import { buildRuntimeName, parsePackageName } from "./identity.ts";
18
18
  import { parseModelScopeConfig, type ModelScopeConfig } from "../runs/shared/model-scope.ts";
19
+ export { BUILTIN_AGENT_NAMES } from "./builtin-names.ts";
19
20
  export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
20
21
  import { parseMemoryFrontmatter } from "./agent-memory.ts";
21
22
  import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
@@ -24,7 +25,7 @@ import { validatePermissionRules, type PermissionRules } from "../runs/shared/pe
24
25
 
25
26
  export type AgentScope = "user" | "project" | "both";
26
27
 
27
- export type AgentSource = "builtin" | "package" | "user" | "project";
28
+ export type AgentSource = "builtin" | "package" | "user" | "project" | "runtime";
28
29
  type SystemPromptMode = "append" | "replace";
29
30
  export type AgentDefaultContext = "fresh" | "fork";
30
31
 
@@ -35,16 +36,6 @@ export interface AgentMemoryConfig {
35
36
  path: string;
36
37
  }
37
38
 
38
- export const BUILTIN_AGENT_NAMES = [
39
- "advisor",
40
- "delegate",
41
- "oracle",
42
- "researcher",
43
- "reviewer",
44
- "scout",
45
- "worker",
46
- ] as const;
47
-
48
39
  export function defaultSystemPromptMode(name: string): SystemPromptMode {
49
40
  return name === "delegate" ? "append" : "replace";
50
41
  }
@@ -59,6 +50,7 @@ export function defaultInheritSkills(): boolean {
59
50
 
60
51
  export interface BuiltinAgentOverrideBase {
61
52
  description?: string;
53
+ outputMode?: OutputMode;
62
54
  model?: string;
63
55
  fallbackModels?: string[];
64
56
  thinking?: string | false;
@@ -81,6 +73,7 @@ export interface BuiltinAgentOverrideBase {
81
73
 
82
74
  interface BuiltinAgentOverrideConfig {
83
75
  description?: string;
76
+ outputMode?: OutputMode;
84
77
  model?: string | false;
85
78
  fallbackModels?: string[] | false;
86
79
  thinking?: string | false;
@@ -117,6 +110,9 @@ export interface AgentConfig {
117
110
  runner?: AgentRunnerConfig;
118
111
  localName?: string;
119
112
  packageName?: string;
113
+ packageSourceName?: string;
114
+ packageSourceVersion?: string;
115
+ packageSourceRoot?: string;
120
116
  description: string;
121
117
  aliases?: string[];
122
118
  tools?: string[];
@@ -144,6 +140,7 @@ export interface AgentConfig {
144
140
  extensionsFromDefault?: boolean;
145
141
  subagentOnlyExtensions?: string[];
146
142
  output?: string;
143
+ outputMode?: OutputMode;
147
144
  defaultReads?: string[];
148
145
  defaultProgress?: boolean;
149
146
  interactive?: boolean;
@@ -225,6 +222,7 @@ const AGENT_SOURCE_PRIORITY: Record<AgentSource, number> = {
225
222
  package: 1,
226
223
  user: 2,
227
224
  project: 3,
225
+ runtime: 4,
228
226
  };
229
227
 
230
228
  function agentDefinitionPriority(definition: Pick<AgentConfig | AgentDiscoveryDiagnostic, "source" | "discoveryPriority">): number {
@@ -260,8 +258,15 @@ function getUserChainDir(): string {
260
258
  return path.join(getAgentDir(), "chains");
261
259
  }
262
260
 
261
+ interface PackageSubagentPath {
262
+ dir: string;
263
+ packageName?: string;
264
+ packageVersion?: string;
265
+ packageRoot: string;
266
+ }
267
+
263
268
  interface PackageSubagentPaths {
264
- agents: string[];
269
+ agents: PackageSubagentPath[];
265
270
  chains: string[];
266
271
  }
267
272
 
@@ -396,18 +401,30 @@ function stringArray(value: unknown): string[] {
396
401
  return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
397
402
  }
398
403
 
404
+ function packageMetadata(pkg: Record<string, unknown>, packageRoot: string): Omit<PackageSubagentPath, "dir"> {
405
+ const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : undefined;
406
+ const version = typeof pkg.version === "string" && pkg.version.trim() ? pkg.version.trim() : undefined;
407
+ return {
408
+ packageRoot,
409
+ ...(name ? { packageName: name } : {}),
410
+ ...(version ? { packageVersion: version } : {}),
411
+ };
412
+ }
413
+
399
414
  function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths {
400
415
  const packageJsonPath = path.join(packageRoot, "package.json");
401
416
  const pkg = readJsonFileBestEffort(packageJsonPath);
402
417
  if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] };
418
+ const pkgRecord = pkg as Record<string, unknown>;
419
+ const metadata = packageMetadata(pkgRecord, packageRoot);
403
420
 
404
421
  const roots: Record<string, unknown>[] = [];
405
- const piSubagents = (pkg as { "pi-subagents"?: unknown })["pi-subagents"];
422
+ const piSubagents = pkgRecord["pi-subagents"];
406
423
  if (piSubagents && typeof piSubagents === "object" && !Array.isArray(piSubagents)) {
407
424
  roots.push(piSubagents as Record<string, unknown>);
408
425
  }
409
426
 
410
- const pi = (pkg as { pi?: unknown }).pi;
427
+ const pi = pkgRecord.pi;
411
428
  if (pi && typeof pi === "object" && !Array.isArray(pi)) {
412
429
  const subagents = (pi as { subagents?: unknown }).subagents;
413
430
  if (subagents && typeof subagents === "object" && !Array.isArray(subagents)) {
@@ -415,10 +432,10 @@ function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubage
415
432
  }
416
433
  }
417
434
 
418
- const agents: string[] = [];
435
+ const agents: PackageSubagentPath[] = [];
419
436
  const chains: string[] = [];
420
437
  for (const root of roots) {
421
- for (const entry of stringArray(root.agents)) agents.push(path.resolve(packageRoot, entry));
438
+ for (const entry of stringArray(root.agents)) agents.push({ dir: path.resolve(packageRoot, entry), ...metadata });
422
439
  for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry));
423
440
  }
424
441
  return { agents, chains };
@@ -510,17 +527,17 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
510
527
  const seenRoots = new Set<string>();
511
528
  const seenAgents = new Set<string>();
512
529
  const seenChains = new Set<string>();
513
- const agents: string[] = [];
530
+ const agents: PackageSubagentPath[] = [];
514
531
  const chains: string[] = [];
515
532
  for (const packageRoot of packageRoots) {
516
533
  const resolvedRoot = path.resolve(packageRoot);
517
534
  if (seenRoots.has(resolvedRoot)) continue;
518
535
  seenRoots.add(resolvedRoot);
519
536
  const paths = extractSubagentPathsFromPackageRoot(resolvedRoot);
520
- for (const agentDir of paths.agents) {
521
- if (seenAgents.has(agentDir)) continue;
522
- seenAgents.add(agentDir);
523
- agents.push(agentDir);
537
+ for (const agentPath of paths.agents) {
538
+ if (seenAgents.has(agentPath.dir)) continue;
539
+ seenAgents.add(agentPath.dir);
540
+ agents.push(agentPath);
524
541
  }
525
542
  for (const chainDir of paths.chains) {
526
543
  if (seenChains.has(chainDir)) continue;
@@ -540,7 +557,7 @@ function normalizeAgentAliases(rawAliases: string[] | undefined, agentName: stri
540
557
  function effectiveAgentMatch(matches: AgentConfig[]): { agent?: AgentConfig; error?: string } {
541
558
  const distinctNames = [...new Set(matches.map((agent) => agent.name))];
542
559
  if (distinctNames.length === 1) {
543
- const sourceRank = new Map<AgentConfig["source"], number>([["builtin", 0], ["package", 1], ["user", 2], ["project", 3]]);
560
+ const sourceRank = new Map<AgentConfig["source"], number>([["builtin", 0], ["package", 1], ["user", 2], ["project", 3], ["runtime", 4]]);
544
561
  const agent = [...matches].sort((a, b) => (sourceRank.get(b.source) ?? 0) - (sourceRank.get(a.source) ?? 0))[0];
545
562
  return agent ? { agent } : {};
546
563
  }
@@ -604,6 +621,7 @@ function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean
604
621
  function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
605
622
  return {
606
623
  description: agent.description,
624
+ ...(agent.outputMode !== undefined ? { outputMode: agent.outputMode } : {}),
607
625
  ...(agent.model !== undefined ? { model: agent.model } : {}),
608
626
  ...(agent.fallbackModels ? { fallbackModels: [...agent.fallbackModels] } : {}),
609
627
  ...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
@@ -628,6 +646,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
628
646
  function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
629
647
  return {
630
648
  ...(override.description !== undefined ? { description: override.description } : {}),
649
+ ...(override.outputMode !== undefined ? { outputMode: override.outputMode } : {}),
631
650
  ...(override.model !== undefined ? { model: override.model } : {}),
632
651
  ...(override.fallbackModels !== undefined
633
652
  ? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
@@ -808,6 +827,14 @@ function parseBuiltinOverrideEntry(
808
827
  }
809
828
  }
810
829
 
830
+ if ("outputMode" in input) {
831
+ if (input.outputMode === "inline" || input.outputMode === "file-only") {
832
+ override.outputMode = input.outputMode;
833
+ } else {
834
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'outputMode'; expected 'inline' or 'file-only'.`);
835
+ }
836
+ }
837
+
811
838
  if ("model" in input) {
812
839
  if (typeof input.model === "string" || input.model === false) override.model = input.model;
813
840
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
@@ -1076,6 +1103,7 @@ function applyBuiltinOverride(
1076
1103
  };
1077
1104
 
1078
1105
  if (override.description !== undefined) next.description = override.description;
1106
+ if (override.outputMode !== undefined) next.outputMode = override.outputMode;
1079
1107
  if (override.model !== undefined) {
1080
1108
  if (override.model === false) delete next.model; else next.model = override.model;
1081
1109
  delete next.modelSource;
@@ -1193,6 +1221,9 @@ function applyCustomAgentOverride(
1193
1221
  mutable().description = override.description;
1194
1222
  anyFilled = true;
1195
1223
  }
1224
+ if (override.outputMode !== undefined) {
1225
+ fill("outputMode", ["outputMode"], override.outputMode);
1226
+ }
1196
1227
  if (override.model !== undefined && !agentHasFrontmatterField(agent, "model")) {
1197
1228
  const target = mutable();
1198
1229
  if (override.model === false) delete target.model; else target.model = override.model;
@@ -1224,7 +1255,18 @@ function applyCustomAgentOverride(
1224
1255
  if (override.acceptanceRole !== undefined) {
1225
1256
  fill("acceptanceRole", ["acceptanceRole"], override.acceptanceRole === false ? undefined : override.acceptanceRole);
1226
1257
  }
1227
- if (override.disabled !== undefined && agent.disabled === undefined) {
1258
+ if (override.disabled !== undefined) {
1259
+ // Unconditional, matching applyBuiltinOverride: custom agents have no
1260
+ // frontmatter concept of `disabled` to protect (unlike model/thinking/
1261
+ // etc., which use `fill()` + agentHasFrontmatterField to defer to the
1262
+ // agent's own file). A guard here previously read `agent.disabled ===
1263
+ // undefined`, which was a no-op before user+project layering existed
1264
+ // (this function only ever ran once, against the pristine base agent,
1265
+ // whose `.disabled` is always undefined for custom agents) but became a
1266
+ // real bug once a user-scope override could run first: the guard then
1267
+ // silently blocked a later project-scope override from ever changing
1268
+ // `disabled`, breaking this PR's own "project wins" precedence for this
1269
+ // one field.
1228
1270
  mutable().disabled = override.disabled;
1229
1271
  anyFilled = true;
1230
1272
  }
@@ -1266,24 +1308,31 @@ function applyCustomAgentOverrides(
1266
1308
  userSettingsPath: string,
1267
1309
  projectSettingsPath: string | null,
1268
1310
  ): AgentConfig[] {
1311
+ // Both scopes are applied, user first then project, so a project override that
1312
+ // only sets a subset of fields (e.g. just `extensions`) layers on top of the
1313
+ // user's override instead of silently discarding it (#1341 / issue writeup:
1314
+ // "agentOverrides project override drops user-only fields for custom agents").
1315
+ // Per-field precedence still favors project over user: applyCustomAgentOverride
1316
+ // only fills fields present in the override it's given, and project is applied
1317
+ // last, so any field set at both scopes ends up with the project's value.
1269
1318
  return agents.map((agent) => {
1319
+ const userOverride = userSettings.overrides[agent.name];
1320
+ const withUserOverride = userOverride
1321
+ ? applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath })
1322
+ : agent;
1323
+
1270
1324
  const projectOverride = projectSettings.overrides[agent.name];
1271
1325
  if (projectOverride && projectSettingsPath) {
1272
- return applyCustomAgentOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
1326
+ return applyCustomAgentOverride(withUserOverride, projectOverride, { scope: "project", path: projectSettingsPath });
1273
1327
  }
1274
1328
 
1275
- const userOverride = userSettings.overrides[agent.name];
1276
- if (userOverride) {
1277
- return applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
1278
- }
1279
-
1280
- return agent;
1329
+ return withUserOverride;
1281
1330
  });
1282
1331
  }
1283
1332
 
1284
1333
  export function buildBuiltinOverrideConfig(
1285
1334
  base: BuiltinAgentOverrideBase,
1286
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description">>,
1335
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "outputMode">>,
1287
1336
  ): BuiltinAgentOverrideConfig | undefined {
1288
1337
  const override: BuiltinAgentOverrideConfig = {};
1289
1338
 
@@ -1291,6 +1340,7 @@ export function buildBuiltinOverrideConfig(
1291
1340
  const description = draft.description.trim();
1292
1341
  if (description && description !== base.description) override.description = description;
1293
1342
  }
1343
+ if (draft.outputMode !== undefined && draft.outputMode !== base.outputMode) override.outputMode = draft.outputMode;
1294
1344
  if (draft.model !== base.model) override.model = draft.model ?? false;
1295
1345
  if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
1296
1346
  if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
@@ -1605,7 +1655,7 @@ function resolveAgentRelativeExtensionPaths(paths: string[] | undefined, agentFi
1605
1655
  });
1606
1656
  }
1607
1657
 
1608
- function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1658
+ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1609
1659
  const agents: AgentConfig[] = [];
1610
1660
  const diagnostics: AgentDiscoveryDiagnostic[] = [];
1611
1661
 
@@ -1690,6 +1740,11 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1690
1740
  defaultTurnBudget = resolved.turnBudget;
1691
1741
  }
1692
1742
  const defaultAcceptance = parseAgentAcceptanceFrontmatter(frontmatter.acceptance, localName);
1743
+ let outputMode: OutputMode | undefined;
1744
+ if (frontmatter.outputMode !== undefined) {
1745
+ if (frontmatter.outputMode === "inline" || frontmatter.outputMode === "file-only") outputMode = frontmatter.outputMode;
1746
+ else throw new Error(`Agent '${localName}' has invalid outputMode frontmatter; expected 'inline' or 'file-only'.`);
1747
+ }
1693
1748
  let acceptanceRole: AcceptanceRole | undefined;
1694
1749
  if (frontmatter.acceptanceRole !== undefined && frontmatter.acceptanceRole.trim()) {
1695
1750
  if (frontmatter.acceptanceRole === "read-only" || frontmatter.acceptanceRole === "writer") acceptanceRole = frontmatter.acceptanceRole;
@@ -1735,6 +1790,9 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1735
1790
  ...(runner !== undefined ? { runner } : {}),
1736
1791
  localName,
1737
1792
  ...(packageName !== undefined ? { packageName } : {}),
1793
+ ...(packageSource?.packageName ? { packageSourceName: packageSource.packageName } : {}),
1794
+ ...(packageSource?.packageVersion ? { packageSourceVersion: packageSource.packageVersion } : {}),
1795
+ ...(packageSource?.packageRoot ? { packageSourceRoot: packageSource.packageRoot } : {}),
1738
1796
  description: frontmatter.description,
1739
1797
  ...(aliases !== undefined ? { aliases } : {}),
1740
1798
  ...(rawTools !== undefined ? { tools } : {}),
@@ -1761,6 +1819,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1761
1819
  ...(extensions !== undefined ? { extensions } : {}),
1762
1820
  ...(subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions } : {}),
1763
1821
  ...(frontmatter.output !== undefined ? { output: frontmatter.output } : {}),
1822
+ ...(outputMode !== undefined ? { outputMode } : {}),
1764
1823
  ...(defaultReads?.length ? { defaultReads } : {}),
1765
1824
  defaultProgress: frontmatter.defaultProgress === "true",
1766
1825
  interactive: frontmatter.interactive === "true",
@@ -1781,8 +1840,8 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1781
1840
  return { agents, diagnostics };
1782
1841
  }
1783
1842
 
1784
- function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1785
- return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority);
1843
+ function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1844
+ return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority, packageSource);
1786
1845
  }
1787
1846
 
1788
1847
  function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
@@ -1908,7 +1967,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
1908
1967
  userSettingsPath,
1909
1968
  projectSettingsPath,
1910
1969
  );
1911
- const packageLoaded = packageSubagentPaths.agents.map((dir, index) => loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index));
1970
+ const packageLoaded = packageSubagentPaths.agents.map((entry, index) => loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry));
1912
1971
  const packageMap = new Map<string, AgentConfig>();
1913
1972
  for (const loaded of packageLoaded) {
1914
1973
  for (const agent of loaded.agents) {
@@ -1982,8 +2041,8 @@ export function discoverAgentsAll(cwd: string): {
1982
2041
  );
1983
2042
  const packageMap = new Map<string, AgentConfig>();
1984
2043
  const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
1985
- for (const [index, dir] of packageSubagentPaths.agents.entries()) {
1986
- const loaded = loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index);
2044
+ for (const [index, entry] of packageSubagentPaths.agents.entries()) {
2045
+ const loaded = loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry);
1987
2046
  packageAgentDiagnostics.push(...loaded.diagnostics);
1988
2047
  for (const agent of loaded.agents) {
1989
2048
  if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
@@ -0,0 +1,9 @@
1
+ export const BUILTIN_AGENT_NAMES = [
2
+ "advisor",
3
+ "delegate",
4
+ "oracle",
5
+ "researcher",
6
+ "reviewer",
7
+ "scout",
8
+ "worker",
9
+ ] as const;