pi-subagents 0.38.0 → 0.40.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 (59) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +85 -13
  3. package/agents/worker.md +1 -0
  4. package/package.json +1 -1
  5. package/prompts/review-loop.md +1 -1
  6. package/skills/pi-subagents/SKILL.md +4 -3
  7. package/skills/pi-subagents/references/execution-controls.md +22 -3
  8. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -1
  9. package/skills/pi-subagents/references/prompting-and-roles.md +17 -3
  10. package/src/agents/agent-management.ts +71 -30
  11. package/src/agents/agent-serializer.ts +4 -0
  12. package/src/agents/agents.ts +60 -1
  13. package/src/api/preflight.ts +16 -12
  14. package/src/extension/chain-validation.ts +17 -1
  15. package/src/extension/rpc.ts +2 -0
  16. package/src/extension/schemas.ts +16 -3
  17. package/src/extension/tool-description.ts +6 -5
  18. package/src/intercom/result-intercom.ts +33 -4
  19. package/src/runs/background/async-execution.ts +63 -16
  20. package/src/runs/background/async-job-tracker.ts +5 -1
  21. package/src/runs/background/async-resume.ts +1 -0
  22. package/src/runs/background/async-status.ts +18 -2
  23. package/src/runs/background/chain-append.ts +48 -5
  24. package/src/runs/background/control-channel.ts +60 -0
  25. package/src/runs/background/notify.ts +26 -4
  26. package/src/runs/background/result-watcher.ts +20 -3
  27. package/src/runs/background/run-status.ts +29 -3
  28. package/src/runs/background/subagent-runner.ts +302 -18
  29. package/src/runs/foreground/async-stop-action.ts +65 -0
  30. package/src/runs/foreground/chain-execution.ts +143 -43
  31. package/src/runs/foreground/execution.ts +384 -202
  32. package/src/runs/foreground/foreground-control.ts +30 -0
  33. package/src/runs/foreground/subagent-executor.ts +314 -77
  34. package/src/runs/shared/capability-ceiling.ts +51 -19
  35. package/src/runs/shared/chain-outputs.ts +3 -1
  36. package/src/runs/shared/dynamic-fanout.ts +1 -1
  37. package/src/runs/shared/nested-events.ts +97 -20
  38. package/src/runs/shared/parallel-utils.ts +43 -12
  39. package/src/runs/shared/pi-args.ts +45 -3
  40. package/src/runs/shared/process-signal.ts +19 -0
  41. package/src/runs/shared/run-history.ts +45 -9
  42. package/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  43. package/src/runs/shared/subagent-prompt-runtime.ts +30 -0
  44. package/src/runs/shared/usage-budget.ts +65 -0
  45. package/src/runs/shared/workflow-graph.ts +26 -1
  46. package/src/shared/settings.ts +17 -1
  47. package/src/shared/types.ts +124 -8
  48. package/src/shared/utils.ts +12 -1
  49. package/src/slash/slash-commands.ts +43 -1
  50. package/src/slash/slash-live-state.ts +5 -3
  51. package/src/tui/fleet-status.ts +12 -2
  52. package/src/tui/fleet.ts +181 -8
  53. package/src/tui/render.ts +4 -2
  54. package/src/watchdog/register-main.ts +14 -7
  55. package/src/watchdog/review.ts +4 -3
  56. package/src/watchdog/runtime.ts +170 -17
  57. package/src/watchdog/scope.ts +62 -0
  58. package/src/watchdog/settings.ts +41 -1
  59. package/src/watchdog/types.ts +10 -0
@@ -19,6 +19,7 @@ import {
19
19
  mergeBuiltinAgentOverride,
20
20
  removeBuiltinAgentOverride,
21
21
  removeBuiltinAgentOverrideFields,
22
+ resolveAgentName,
22
23
  } from "./agents.ts";
23
24
  import { serializeAgent } from "./agent-serializer.ts";
24
25
  import { mergeAgentsForScope } from "./agent-selection.ts";
@@ -35,10 +36,11 @@ import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
35
36
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
36
37
  import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
37
38
  import { getProjectConfigDir } from "../shared/utils.ts";
39
+ import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
38
40
 
39
41
  type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
40
42
  type ManagementScope = "user" | "project";
41
- type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig };
43
+ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string };
42
44
 
43
45
  interface ManagementParams {
44
46
  action?: string;
@@ -113,8 +115,13 @@ function findAgents(name: string, cwd: string, scope: AgentScope = "both"): Agen
113
115
  const d = discoverAgentsAll(cwd);
114
116
  const raw = name.trim();
115
117
  const sanitized = sanitizeName(raw);
116
- return allAgents(d)
117
- .filter((a) => (scope === "both" || a.source === scope) && (a.name === raw || a.name === sanitized))
118
+ const scoped = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package);
119
+ let resolved = resolveAgentName(raw, scoped);
120
+ if (!resolved.agent && !resolved.error && sanitized !== raw) resolved = resolveAgentName(sanitized, scoped);
121
+ if (resolved.agent) return scoped.filter((agent) => agent.name === resolved.agent!.name).sort((a, b) => a.source.localeCompare(b.source));
122
+ return scoped
123
+ .filter((agent) => Boolean(resolveAgentName(raw, [agent]).agent)
124
+ || (sanitized !== raw && Boolean(resolveAgentName(sanitized, [agent]).agent)))
118
125
  .sort((a, b) => a.source.localeCompare(b.source));
119
126
  }
120
127
 
@@ -128,15 +135,20 @@ function findChains(name: string, cwd: string, scope: AgentScope = "both"): Chai
128
135
 
129
136
  const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
130
137
 
131
- // Returns the highest-precedence agent for a name (project > user > package > builtin,
132
- // matching mergeAgentsForScope for "both"), including disabled agents so disable/enable/reset
133
- // can locate agents that runtime discovery filters out.
134
- function pickEffectiveAgent(d: ReturnType<typeof discoverAgentsAll>, name: string): AgentConfig | undefined {
138
+ // Returns the highest-precedence definition for a resolved canonical name (project > user > package > builtin),
139
+ // matching mergeAgentsForScope for "both", including disabled agents so disable/enable can locate hidden targets.
140
+ function resolveEffectiveAgent(d: ReturnType<typeof discoverAgentsAll>, name: string): { agent?: AgentConfig; error?: string } {
135
141
  const raw = name.trim();
136
- const sanitized = sanitizeName(raw);
137
- const matches = allAgents(d).filter((a) => a.name === raw || a.name === sanitized);
138
- if (matches.length === 0) return undefined;
139
- return matches.reduce((best, agent) => (AGENT_SOURCE_PRECEDENCE[agent.source] > AGENT_SOURCE_PRECEDENCE[best.source] ? agent : best));
142
+ const candidates = allAgents(d);
143
+ let resolved = resolveAgentName(raw, candidates);
144
+ if (!resolved.agent && !resolved.error) {
145
+ const sanitized = sanitizeName(raw);
146
+ if (sanitized !== raw) resolved = resolveAgentName(sanitized, candidates);
147
+ }
148
+ if (resolved.error) return { error: resolved.error };
149
+ if (!resolved.agent) return {};
150
+ const matches = candidates.filter((agent) => agent.name === resolved.agent!.name);
151
+ return { agent: matches.reduce((best, agent) => (AGENT_SOURCE_PRECEDENCE[agent.source] > AGENT_SOURCE_PRECEDENCE[best.source] ? agent : best)) };
140
152
  }
141
153
 
142
154
  function nameExistsInScope(cwd: string, scope: ManagementScope, name: string, excludePath?: string): boolean {
@@ -156,8 +168,9 @@ function isMutableSource(source: AgentSource): source is ManagementScope {
156
168
 
157
169
  function unknownChainAgents(cwd: string, steps: ChainStepConfig[]): string[] {
158
170
  const d = discoverAgentsAll(cwd);
159
- const known = new Set(allAgents(d).map((a) => a.name));
160
- return [...new Set(steps.map((s) => s.agent).filter((a) => !known.has(a)))].sort((a, b) => a.localeCompare(b));
171
+ const agents = allAgents(d);
172
+ return [...new Set(steps.map((s) => s.agent).filter((agentName): agentName is string => typeof agentName === "string" && !resolveAgentName(agentName, agents).agent))]
173
+ .sort((a, b) => a.localeCompare(b));
161
174
  }
162
175
 
163
176
  function chainStepWarnings(ctx: ManagementContext, steps: ChainStepConfig[]): string[] {
@@ -251,6 +264,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
251
264
  if (hasKey(cfg, "name")) changed("name");
252
265
  if (hasKey(cfg, "package")) changed("package");
253
266
  if (hasKey(cfg, "description")) changed("description");
267
+ if (hasKey(cfg, "aliases")) changed("alias", "aliases");
254
268
  if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
255
269
  if (hasKey(cfg, "model")) changed("model");
256
270
  if (hasKey(cfg, "fallbackModels")) changed("fallbackModels");
@@ -370,6 +384,16 @@ function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[]
370
384
  }
371
385
 
372
386
  function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): string | undefined {
387
+ if (hasKey(cfg, "aliases")) {
388
+ if (cfg.aliases === false || cfg.aliases === "") target.aliases = undefined;
389
+ else if (typeof cfg.aliases === "string") {
390
+ const aliases = parseCsv(cfg.aliases).filter((alias) => alias !== target.name);
391
+ target.aliases = aliases.length ? aliases : undefined;
392
+ } else if (Array.isArray(cfg.aliases) && cfg.aliases.every((entry) => typeof entry === "string")) {
393
+ const aliases = [...new Set(cfg.aliases.map((entry) => entry.trim()).filter(Boolean).filter((alias) => alias !== target.name))];
394
+ target.aliases = aliases.length ? aliases : undefined;
395
+ } else return "config.aliases must be a comma-separated string, string array, or false when provided.";
396
+ }
373
397
  if (hasKey(cfg, "systemPrompt")) {
374
398
  if (cfg.systemPrompt === false || cfg.systemPrompt === "") target.systemPrompt = "";
375
399
  else if (typeof cfg.systemPrompt === "string") target.systemPrompt = cfg.systemPrompt;
@@ -513,13 +537,17 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
513
537
  return undefined;
514
538
  }
515
539
 
516
- function resolveTarget<T extends { source: AgentSource; filePath: string }>(
540
+ function resolveTarget<T extends { name: string; source: AgentSource; filePath: string }>(
517
541
  kind: "agent" | "chain",
518
542
  name: string,
519
543
  matches: T[],
520
544
  cwd: string,
521
545
  scopeHint?: string,
522
546
  ): T | AgentToolResult<Details> {
547
+ const distinctNames = [...new Set(matches.map((m) => m.name))];
548
+ if (distinctNames.length > 1) {
549
+ return result(`Ambiguous ${kind} alias or name '${name}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`, true);
550
+ }
523
551
  const mutable = matches.filter((m): m is T & { source: ManagementScope } => isMutableSource(m.source));
524
552
  if (mutable.length === 0) {
525
553
  if (matches.length > 0) {
@@ -564,6 +592,7 @@ function formatAgentDetail(agent: AgentConfig): string {
564
592
  lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
565
593
  lines.push(`Package: ${agent.packageName}`);
566
594
  }
595
+ if (agent.aliases?.length) lines.push(`Aliases: ${agent.aliases.join(", ")}`);
567
596
  if (agent.model) lines.push(`Model: ${agent.model}`);
568
597
  if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
569
598
  if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
@@ -648,7 +677,11 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
648
677
  const d = discoverAgentsAll(ctx.cwd);
649
678
  const scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package)
650
679
  .sort((a, b) => a.name.localeCompare(b.name));
651
- const agents = scopedAgents.filter((a) => !a.disabled);
680
+ const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
681
+ const visibleAgents = scopedAgents.filter((a) => !a.disabled);
682
+ const agents = visibleAgents.filter((a) => isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
683
+ const restrictedAgents = visibleAgents.filter((a) => !isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
684
+ const restrictedSources = capabilityCeilingAgentRestrictionSources(capabilityCeiling);
652
685
  const chains = d.chains.filter((c) => scope === "both" || c.source === "package" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
653
686
  const diagnostics = d.chainDiagnostics.filter((entry) => scope === "both" || entry.source === scope);
654
687
  const proactiveSuggestions = buildProactiveSkillSubagentRecommendationLines({
@@ -660,8 +693,13 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
660
693
  const lines = [
661
694
  "Executable agents:",
662
695
  ...(agents.length
663
- ? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}): ${a.description}`)
696
+ ? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`)
664
697
  : ["- (none)"]),
698
+ ...(restrictedAgents.length ? [
699
+ "",
700
+ `Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
701
+ ...restrictedAgents.map((a) => `- ${a.name} (${a.source}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`),
702
+ ] : []),
665
703
  "",
666
704
  "Chains:",
667
705
  ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
@@ -760,12 +798,13 @@ function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolR
760
798
  const blocks: string[] = [];
761
799
  let anyFound = false;
762
800
  if (params.agent) {
763
- const raw = params.agent.trim();
764
- const sanitized = sanitizeName(raw);
765
- const d = discoverAgentsAll(ctx.cwd);
766
- const matches = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package)
767
- .filter((agent) => agent.name === raw || agent.name === sanitized);
768
- if (!matches.length) {
801
+ const matches = findAgents(params.agent, ctx.cwd, scope);
802
+ const distinctNames = [...new Set(matches.map((agent) => agent.name))];
803
+ if (distinctNames.length > 1) {
804
+ const msg = `Ambiguous agent alias or name '${params.agent}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`;
805
+ if (!hasBoth) return result(msg, true);
806
+ blocks.push(msg);
807
+ } else if (!matches.length) {
769
808
  const msg = `Agent '${params.agent}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`;
770
809
  if (!hasBoth) return result(msg, true);
771
810
  blocks.push(msg);
@@ -1027,13 +1066,14 @@ function handleDisable(params: ManagementParams, ctx: ManagementContext): AgentT
1027
1066
  if (scope === "project" && d.projectSettingsPath === null) {
1028
1067
  return result("Project override is not available here: no project config root (.pi or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1029
1068
  }
1030
- const effective = pickEffectiveAgent(d, raw);
1031
- if (!effective) {
1069
+ const effective = resolveEffectiveAgent(d, raw);
1070
+ if (effective.error) return result(effective.error, true);
1071
+ if (!effective.agent) {
1032
1072
  return result(`Agent '${raw}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
1033
1073
  }
1034
- const runtimeName = effective.name;
1074
+ const runtimeName = effective.agent.name;
1035
1075
  const settingsPath = mergeBuiltinAgentOverride(ctx.cwd, runtimeName, scope, { disabled: true });
1036
- const after = pickEffectiveAgent(discoverAgentsAll(ctx.cwd), raw);
1076
+ const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1037
1077
  if (after?.disabled === true) {
1038
1078
  return result(`Disabled agent '${runtimeName}' via ${scope} settings override at ${settingsPath}. It is now hidden from runtime discovery and { action: "list" }.`);
1039
1079
  }
@@ -1050,13 +1090,14 @@ function handleEnable(params: ManagementParams, ctx: ManagementContext): AgentTo
1050
1090
  if (scope === "project" && d.projectSettingsPath === null) {
1051
1091
  return result("Project override is not available here: no project config root (.pi or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1052
1092
  }
1053
- const effective = pickEffectiveAgent(d, raw);
1054
- if (!effective) {
1093
+ const effective = resolveEffectiveAgent(d, raw);
1094
+ if (effective.error) return result(effective.error, true);
1095
+ if (!effective.agent) {
1055
1096
  return result(`Agent '${raw}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
1056
1097
  }
1057
- const runtimeName = effective.name;
1098
+ const runtimeName = effective.agent.name;
1058
1099
  const { path: settingsPath, removed } = removeBuiltinAgentOverrideFields(ctx.cwd, runtimeName, scope, ["disabled"]);
1059
- const after = pickEffectiveAgent(discoverAgentsAll(ctx.cwd), raw);
1100
+ const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1060
1101
  if (after && after.disabled !== true) {
1061
1102
  if (removed) return result(`Enabled agent '${runtimeName}' (removed disabled override at ${settingsPath}).`);
1062
1103
  return result(`Agent '${runtimeName}' is already enabled.`);
@@ -5,6 +5,8 @@ export const KNOWN_FIELDS = new Set([
5
5
  "name",
6
6
  "package",
7
7
  "description",
8
+ "alias",
9
+ "aliases",
8
10
  "tools",
9
11
  "model",
10
12
  "fallbackModels",
@@ -50,6 +52,8 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
50
52
  lines.push(`name: ${frontmatterNameForConfig(config)}`);
51
53
  if (config.packageName) lines.push(`package: ${config.packageName}`);
52
54
  lines.push(`description: ${config.description}`);
55
+ const aliasesValue = joinComma(config.aliases);
56
+ if (aliasesValue || preserve("alias", "aliases")) lines.push(`aliases: ${aliasesValue ?? ""}`);
53
57
 
54
58
  const tools = [
55
59
  ...(config.tools ?? []),
@@ -59,6 +59,7 @@ export function defaultInheritSkills(): boolean {
59
59
  }
60
60
 
61
61
  export interface BuiltinAgentOverrideBase {
62
+ description?: string;
62
63
  model?: string;
63
64
  fallbackModels?: string[];
64
65
  thinking?: string | false;
@@ -80,6 +81,7 @@ export interface BuiltinAgentOverrideBase {
80
81
  }
81
82
 
82
83
  interface BuiltinAgentOverrideConfig {
84
+ description?: string;
83
85
  model?: string | false;
84
86
  fallbackModels?: string[] | false;
85
87
  thinking?: string | false;
@@ -116,6 +118,7 @@ export interface AgentConfig {
116
118
  localName?: string;
117
119
  packageName?: string;
118
120
  description: string;
121
+ aliases?: string[];
119
122
  tools?: string[];
120
123
  mcpDirectTools?: string[];
121
124
  model?: string;
@@ -473,6 +476,41 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
473
476
  return { agents, chains };
474
477
  }
475
478
 
479
+ function normalizeAgentAliases(rawAliases: string[] | undefined, agentName: string): string[] | undefined {
480
+ const aliases = [...new Set((rawAliases ?? []).map((alias) => alias.trim()).filter(Boolean))]
481
+ .filter((alias) => alias !== agentName);
482
+ return aliases.length > 0 ? aliases : undefined;
483
+ }
484
+
485
+ function effectiveAgentMatch(matches: AgentConfig[]): { agent?: AgentConfig; error?: string } {
486
+ const distinctNames = [...new Set(matches.map((agent) => agent.name))];
487
+ if (distinctNames.length === 1) {
488
+ const sourceRank = new Map<AgentConfig["source"], number>([["builtin", 0], ["package", 1], ["user", 2], ["project", 3]]);
489
+ return { agent: [...matches].sort((a, b) => (sourceRank.get(b.source) ?? 0) - (sourceRank.get(a.source) ?? 0))[0] };
490
+ }
491
+ return {};
492
+ }
493
+
494
+ export function resolveAgentName(name: string, agents: AgentConfig[]): { agent?: AgentConfig; error?: string } {
495
+ const raw = name.trim();
496
+ const exact = agents.filter((agent) => agent.name === raw || agent.localName === raw);
497
+ if (exact.length === 1) return { agent: exact[0] };
498
+ if (exact.length > 1) {
499
+ const effective = effectiveAgentMatch(exact);
500
+ if (effective.agent) return effective;
501
+ return { error: `Ambiguous agent name '${name}': ${exact.map((agent) => agent.name).join(", ")}` };
502
+ }
503
+
504
+ const aliases = agents.filter((agent) => agent.aliases?.includes(raw));
505
+ if (aliases.length === 1) return { agent: aliases[0] };
506
+ if (aliases.length > 1) {
507
+ const effective = effectiveAgentMatch(aliases);
508
+ if (effective.agent) return effective;
509
+ return { error: `Ambiguous agent alias '${name}': ${aliases.map((agent) => agent.name).join(", ")}` };
510
+ }
511
+ return {};
512
+ }
513
+
476
514
  function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
477
515
  const mcpDirectTools: string[] = [];
478
516
  const tools: string[] = [];
@@ -509,6 +547,7 @@ function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean
509
547
 
510
548
  function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
511
549
  return {
550
+ description: agent.description,
512
551
  model: agent.model,
513
552
  fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : undefined,
514
553
  thinking: agent.thinking,
@@ -532,6 +571,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
532
571
 
533
572
  function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
534
573
  return {
574
+ ...(override.description !== undefined ? { description: override.description } : {}),
535
575
  ...(override.model !== undefined ? { model: override.model } : {}),
536
576
  ...(override.fallbackModels !== undefined
537
577
  ? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
@@ -684,6 +724,14 @@ function parseBuiltinOverrideEntry(
684
724
  const input = value as Record<string, unknown>;
685
725
  const override: BuiltinAgentOverrideConfig = {};
686
726
 
727
+ if ("description" in input) {
728
+ if (typeof input.description === "string" && input.description.trim()) {
729
+ override.description = input.description.trim();
730
+ } else {
731
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'description'; expected a non-empty string.`);
732
+ }
733
+ }
734
+
687
735
  if ("model" in input) {
688
736
  if (typeof input.model === "string" || input.model === false) override.model = input.model;
689
737
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
@@ -931,6 +979,7 @@ function applyBuiltinOverride(
931
979
  override: { ...meta, base: cloneOverrideBase(agent) },
932
980
  };
933
981
 
982
+ if (override.description !== undefined) next.description = override.description;
934
983
  if (override.model !== undefined) next.model = override.model === false ? undefined : override.model;
935
984
  if (override.fallbackModels !== undefined) {
936
985
  next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels];
@@ -1051,6 +1100,10 @@ function applyCustomAgentOverride(
1051
1100
  anyFilled = true;
1052
1101
  };
1053
1102
 
1103
+ if (override.description !== undefined) {
1104
+ mutable().description = override.description;
1105
+ anyFilled = true;
1106
+ }
1054
1107
  if (override.model !== undefined) {
1055
1108
  fill("model", ["model"], override.model === false ? undefined : override.model);
1056
1109
  }
@@ -1141,10 +1194,14 @@ function applyCustomAgentOverrides(
1141
1194
 
1142
1195
  export function buildBuiltinOverrideConfig(
1143
1196
  base: BuiltinAgentOverrideBase,
1144
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget">,
1197
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description">>,
1145
1198
  ): BuiltinAgentOverrideConfig | undefined {
1146
1199
  const override: BuiltinAgentOverrideConfig = {};
1147
1200
 
1201
+ if (draft.description !== undefined) {
1202
+ const description = draft.description.trim();
1203
+ if (description && description !== base.description) override.description = description;
1204
+ }
1148
1205
  if (draft.model !== base.model) override.model = draft.model ?? false;
1149
1206
  if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
1150
1207
  if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
@@ -1385,6 +1442,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1385
1442
  const tools = parsedTools.tools ?? [];
1386
1443
  const mcpDirectTools = parsedTools.mcpDirectTools ?? [];
1387
1444
  const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
1445
+ const aliases = normalizeAgentAliases(parseFrontmatterList(frontmatter.aliases ?? frontmatter.alias), runtimeName);
1388
1446
  const skillStr = frontmatter.skill || frontmatter.skills;
1389
1447
  const skills = parseFrontmatterList(skillStr);
1390
1448
  const skillPath = parseFrontmatterList(frontmatter.skillPath);
@@ -1465,6 +1523,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1465
1523
  localName,
1466
1524
  packageName,
1467
1525
  description: frontmatter.description,
1526
+ aliases,
1468
1527
  tools: rawTools !== undefined ? tools : undefined,
1469
1528
  mcpDirectTools: mcpDirectTools.length > 0 ? mcpDirectTools : undefined,
1470
1529
  model: frontmatter.model,
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
5
+ import { discoverAgents, discoverAgentsAll, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
6
6
  import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
7
7
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
8
8
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
@@ -12,7 +12,7 @@ import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSin
12
12
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
13
13
  import { resolveEffectiveThinking } from "../shared/model-info.ts";
14
14
  import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
15
- import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
15
+ import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
16
16
  import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
17
17
  import type { ResolvedTurnBudget } from "../shared/types.ts";
18
18
  import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
@@ -31,7 +31,8 @@ export type SubagentLaunchContractReasonCode =
31
31
  | "denied_required_tool"
32
32
  | "invalid_artifact_dir"
33
33
  | "invalid_cwd"
34
- | "unsupported_mode";
34
+ | "unsupported_mode"
35
+ | "restricted_agent";
35
36
 
36
37
  export interface SubagentLaunchContractDiagnostic {
37
38
  code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
@@ -191,7 +192,7 @@ function normalizeAvailableModels(models: SubagentLaunchContractInput["available
191
192
  function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] {
192
193
  const all = discoverAgentsAll(cwd);
193
194
  return [...all.builtin, ...all.package, ...all.user, ...all.project]
194
- .filter((agent) => agent.name === inputAgent || agent.localName === inputAgent)
195
+ .filter((agent) => Boolean(resolveAgentName(inputAgent, [agent]).agent))
195
196
  .map((agent) => ({
196
197
  name: agent.name,
197
198
  ...(agent.localName ? { localName: agent.localName } : {}),
@@ -225,14 +226,17 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
225
226
  }
226
227
  const scope = resolveExecutionAgentScope(input.agentScope);
227
228
  const discovered = discoverAgents(effectiveCwd, scope);
228
- const matches = discovered.agents.filter((agent) => agent.name === input.agent || agent.localName === input.agent);
229
- if (matches.length === 0) {
230
- return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
229
+ const resolvedAgent = resolveAgentName(input.agent, discovered.agents);
230
+ if (resolvedAgent.error) {
231
+ return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
231
232
  }
232
- if (matches.length > 1) {
233
- return { ok: false, code: "ambiguous_agent", message: `Ambiguous agent: ${input.agent}`, diagnostics };
233
+ if (!resolvedAgent.agent) {
234
+ return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
234
235
  }
235
- const agent = matches[0]!;
236
+ const agent = resolvedAgent.agent;
237
+ const effectiveCapabilityCeiling = intersectSubagentCapabilityCeilings(input.capabilityCeiling, input.inheritedCapabilityCeiling);
238
+ const restrictionMessage = capabilityCeilingAgentRestrictionMessage(agent.name, effectiveCapabilityCeiling);
239
+ if (restrictionMessage) return { ok: false, code: "restricted_agent", message: restrictionMessage, diagnostics };
236
240
  const runId = input.runId ?? "preflight";
237
241
  const skillInput = normalizeSkillInput(input.skill);
238
242
  const outputOverride = normalizeSingleOutputOverride(input.output, agent.output);
@@ -272,8 +276,8 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
272
276
  cwd: effectiveCwd,
273
277
  requireReadTool: resolvedSkills.resolved.length > 0,
274
278
  structuredOutput: Boolean(input.outputSchema),
275
- capabilityCeiling: input.capabilityCeiling,
276
- inheritedCapabilityCeiling: input.inheritedCapabilityCeiling,
279
+ capabilityCeiling: effectiveCapabilityCeiling,
280
+ agentName: agent.name,
277
281
  });
278
282
  } catch (error) {
279
283
  const message = error instanceof Error ? error.message : String(error);
@@ -43,7 +43,7 @@ export const EXPAND_KEYS = allowedKeysOf(DynamicExpandSchema);
43
43
  export const EXPAND_FROM_KEYS = allowedKeysOf(ExpandFromSchema);
44
44
  export const COLLECT_KEYS = allowedKeysOf(DynamicCollectSchema);
45
45
 
46
- const CHAIN_STEP_EXAMPLE = '{"agent": "worker", "task": "do X"}';
46
+ const CHAIN_STEP_EXAMPLE = '{"agent": "worker", "task": "do X"} or {"checkpoint": "review", "message": "Approve before implementation?"}';
47
47
  const PARALLEL_TASK_EXAMPLE = '{"agent": "worker", "task": "do X"}';
48
48
  const DYNAMIC_TEMPLATE_EXAMPLE = '{"agent": "worker", "task": "Review {item.path}"}';
49
49
  const EXPAND_EXAMPLE = '{"from": {"output": "targets", "path": "/items"}, "maxItems": 4}';
@@ -58,6 +58,21 @@ function disallowedKeys(value: Record<string, unknown>, allowed: readonly string
58
58
  return Object.keys(value).filter((key) => !allowed.includes(key));
59
59
  }
60
60
 
61
+ function validateCheckpointShape(step: Record<string, unknown>, path: string): void {
62
+ if (step.checkpoint === undefined) return;
63
+ const allowed = new Set(["checkpoint", "message", "phase", "label"]);
64
+ const mixed = Object.keys(step).filter((key) => !allowed.has(key));
65
+ if (mixed.length > 0) {
66
+ throw new Error(`subagent ${path}: checkpoint steps cannot be mixed with child-launch properties ${quoteList(mixed)}.\n\nExample: {"checkpoint": "review", "message": "Approve before implementation?"}`);
67
+ }
68
+ if (typeof step.checkpoint !== "string" || !step.checkpoint.trim()) {
69
+ throw new Error(`subagent ${path}.checkpoint: expected a non-empty string.`);
70
+ }
71
+ if (step.message !== undefined && typeof step.message !== "string") {
72
+ throw new Error(`subagent ${path}.message: expected a string.`);
73
+ }
74
+ }
75
+
61
76
  function typeName(value: unknown): string {
62
77
  if (Array.isArray(value)) return "array";
63
78
  return typeof value;
@@ -120,6 +135,7 @@ export function validateChainInput(args: unknown): void {
120
135
  const stepPath = `chain step validation failed at chain[${index}]`;
121
136
  expectObject(step, stepPath, CHAIN_STEP_KEYS, CHAIN_STEP_EXAMPLE);
122
137
  checkNoExtraKeys(step, stepPath, ChainItem, CHAIN_STEP_KEYS, CHAIN_STEP_EXAMPLE);
138
+ validateCheckpointShape(step, stepPath);
123
139
 
124
140
  if (step.expand !== undefined) {
125
141
  const expandPath = `${stepPath}.expand`;
@@ -386,6 +386,8 @@ function pingData(ctx: ExtensionContext | null) {
386
386
  interrupt: true,
387
387
  stop: true,
388
388
  resume: true,
389
+ launchResolvedExtensions: { version: 1, source: "launch-resolved" },
390
+ runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", event: "subagent:acknowledge-extension" },
389
391
  processTerminalProof: { version: 1, lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION },
390
392
  },
391
393
  events: {
@@ -119,6 +119,16 @@ const ToolBudgetOverride = Type.Object({
119
119
  block: Type.Optional(ToolBudgetBlock),
120
120
  }, { additionalProperties: false, description: "Optional child tool-call budget. soft nudges the child; after hard, block tools (default read/grep/find/ls, or '*' for all tools) are blocked so the child can finalize." });
121
121
 
122
+ const UsageBudgetLimitOverride = Type.Object({
123
+ soft: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
124
+ hard: Type.Number({ exclusiveMinimum: 0 }),
125
+ }, { additionalProperties: false });
126
+
127
+ const UsageBudgetOverride = Type.Object({
128
+ tokens: Type.Optional(UsageBudgetLimitOverride),
129
+ costUsd: Type.Optional(UsageBudgetLimitOverride),
130
+ }, { additionalProperties: false, description: "Optional root-only reported-usage budget. Hard limits prevent future child launches; running children are not stopped." });
131
+
122
132
  const TaskItem = Type.Object({
123
133
  agent: Type.String(),
124
134
  task: Type.String(),
@@ -195,6 +205,8 @@ export const DynamicCollectSchema = Type.Object({
195
205
 
196
206
  // Flattened so chain steps do not need an object-shape anyOf/oneOf union.
197
207
  export const ChainItem = Type.Object({
208
+ checkpoint: Type.Optional(Type.String({ description: "Approval checkpoint name. Pauses the chain without launching a child until approve-checkpoint or reject-checkpoint is called." })),
209
+ message: Type.Optional(Type.String({ description: "Optional approval message shown while the checkpoint is paused." })),
198
210
  agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
199
211
  task: Type.Optional(Type.String({
200
212
  description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
@@ -229,7 +241,7 @@ export const ChainItem = Type.Object({
229
241
  description: "Create isolated git worktrees for each parallel task."
230
242
  })),
231
243
  }, {
232
- description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
244
+ description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, {expand, parallel: {...}, collect} for dynamic fanout, or {checkpoint: name, message?} for an approval pause.",
233
245
  additionalProperties: false,
234
246
  });
235
247
 
@@ -256,10 +268,10 @@ const SubagentParamsSchema = Type.Object({
256
268
  description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task}, {tasks}, or {chain}); use it only for management/control actions."
257
269
  })),
258
270
  id: Type.Optional(Type.String({
259
- description: "Run id or prefix for action='status', action='interrupt', action='stop', action='resume', action='steer', or action='append-step'."
271
+ description: "Run id or prefix for action='status', action='interrupt', action='stop', action='resume', action='steer', action='append-step', action='approve-checkpoint', or action='reject-checkpoint'."
260
272
  })),
261
273
  runId: Type.Optional(Type.String({
262
- description: "Target run ID for action='interrupt', action='stop', action='resume', action='steer', or action='append-step'. Prefer id for new calls."
274
+ description: "Target run ID for action='interrupt', action='stop', action='resume', action='steer', action='append-step', action='approve-checkpoint', or action='reject-checkpoint'. Prefer id for new calls."
263
275
  })),
264
276
  dir: Type.Optional(Type.String({
265
277
  description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
@@ -306,6 +318,7 @@ const SubagentParamsSchema = Type.Object({
306
318
  maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs; foreground defaults to 30m absent call/agent." })),
307
319
  turnBudget: Type.Optional(TurnBudgetOverride),
308
320
  toolBudget: Type.Optional(ToolBudgetOverride),
321
+ usageBudget: Type.Optional(UsageBudgetOverride),
309
322
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
310
323
  cwd: Type.Optional(Type.String()),
311
324
  artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
@@ -8,7 +8,7 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
8
 
9
9
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
10
10
  • Use { action: "list" } before execution and only run executable/non-disabled agents or chains.
11
- • Keep execution and management separate: omit action for SINGLE/PARALLEL/CHAIN execution; use action only for list/get/models/create/update/delete/status/grant-spawn-budget/interrupt/stop/resume/append-step/doctor.
11
+ • Keep execution and management separate: omit action for SINGLE/PARALLEL/CHAIN execution; use action only for list/get/models/create/update/delete/status/grant-spawn-budget/interrupt/stop/resume/steer/append-step/approve-checkpoint/reject-checkpoint/doctor.
12
12
  • Async/background runs: launch with async:true only when work can proceed independently. Do not sleep or poll status just to wait. In an interactive session, normally return control and let Pi wake you; do not call subagent_wait merely to wait. Override that default and call subagent_wait when the current request is run-to-completion — for example, the user asked you to report results back before continuing or a skill must finish in one turn. Headless sessions auto-drain current-session work at agent_end; use subagent_wait when this turn must receive results before it ends.
13
13
  • Child-safety boundary: ordinary child subagents are not orchestrators and must not run subagents. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
14
14
  • Writing/review safety: keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers/validators for independent review, then have the parent synthesize and apply fixes as the sole writer unless an isolated worktree was intentionally requested.
@@ -19,7 +19,7 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with { age
19
19
  EXECUTION (use exactly ONE mode):
20
20
  • Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
21
21
  • SINGLE: { agent, task? } - one task; omit task for self-contained agents
22
- • CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
22
+ • CHAIN: { chain: [{agent:"agent-a"}, {checkpoint:"review"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional approval checkpoints and parallel fan-out
23
23
  • PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
24
24
  • Optional context: { context: "fresh" | "fork" } (explicit value overrides every child; when omitted, each requested agent uses its own defaultContext, otherwise "fresh"; inspect agent defaults via { action: "list" })
25
25
  • Fork thinking: model strings accept a thinking suffix (provider/model:off|minimal|low|medium|high|xhigh|max). Forking over a parent transcript that carries signed Anthropic thinking blocks forces thinking off only when a child's effective primary or fallback model resolves to the Anthropic provider or anthropic-messages API; unresolved models are treated conservatively. The result notes affected children, including on failures. Use fresh context when an Anthropic child needs thinking.
@@ -35,7 +35,7 @@ CHAIN TEMPLATE VARIABLES (use in task strings):
35
35
  CHAIN EXAMPLES (quick reference for the nested schema):
36
36
  • Sequential: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
37
37
  • Parallel fan-out: { chain: [{parallel: [{agent:"agent-a", task:"Check part of {task}", count: 3}]}] }
38
- • Mixed: { chain: [{agent:"agent-a", task:"Research {task}"}, {parallel: [{agent:"agent-b", task:"Review {previous}", count: 2}]}, {agent:"agent-c", task:"Summarize {previous}"}] }
38
+ • Mixed: { chain: [{agent:"agent-a", task:"Research {task}"}, {checkpoint:"review", message:"Approve implementation?"}, {parallel: [{agent:"agent-b", task:"Review {previous}", count: 2}]}, {agent:"agent-c", task:"Summarize {previous}"}] }
39
39
 
40
40
  MANAGEMENT (use action field, omit agent/task/chain/tasks):
41
41
  • { action: "list" } - discover executable agents/chains
@@ -63,6 +63,7 @@ CONTROL:
63
63
  • { action: "resume", id: "...", message: "...", index?: 0 } - revive a paused, completed, or failed async/foreground child from its session; stopped runs are non-resumable; routed nested runs may accept live follow-ups; use steer for a live top-level async child
64
64
  • { action: "steer", id: "...", message: "...", index?: 0 } - await correlated child-Pi input acceptance for up to 3 seconds; returns delivered, scheduled, pending, partial, recovered, or failed with a request id. Only top-level single runs may recover after a further 15-second pause/revival bound; chain, parallel, and nested runs never auto-interrupt.
65
65
  • { action: "append-step", id: "...", chain: [{agent:"agent-c", task:"Use {previous}"}] } - append one step to the tail of a running async chain
66
+ • { action: "approve-checkpoint", id: "..." } / { action: "reject-checkpoint", id: "..." } - decide a paused current-session async chain checkpoint
66
67
 
67
68
  SCHEDULE (opt-in; requires { "scheduledRuns": { "enabled": true } } in config.json):
68
69
  • { action: "schedule", agent, task?, schedule: "+10m" | "2030-01-01T09:00:00Z", scheduleName? } - defer a subagent launch until a future time. Also accepts tasks[] or chain[]. Scheduled runs always launch async with fresh context; they become normal tracked async runs once they fire. Only schedule explicit delayed runs the user asked for.
@@ -79,7 +80,7 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with {
79
80
 
80
81
  EXECUTE:
81
82
  • Before execution, call { action: "list" }; run only executable/non-disabled configured agents/chains.
82
- • SINGLE {agent, task?}; PARALLEL {tasks:[{agent,task,count?,output?,reads?,progress?}], concurrency?, worktree?}; CHAIN {chain:[{agent,task?},{parallel:[...]}]}.
83
+ • SINGLE {agent, task?}; PARALLEL {tasks:[{agent,task,count?,output?,reads?,progress?}], concurrency?, worktree?}; CHAIN {chain:[{agent,task?},{checkpoint:"review"},{parallel:[...]}]}.
83
84
  • context can be "fresh" or "fork"; omitted uses each agent defaultContext, otherwise fresh. timeoutMs/maxRuntimeMs apply to foreground and async/background runs; foreground defaults to 30 minutes only when neither value nor an agent timeout is provided.
84
85
  • Omit acceptance for reviewer/read-only calls. Evidence levels end at verified; use acceptance.review.required for independent writer review. reviewed is an achieved status, never an explicit input.
85
86
  • Chain templates may use {task}, {previous}, {chain_dir}, and named outputs. Parallel worktree isolation requires a clean git repo.
@@ -89,7 +90,7 @@ EXECUTE:
89
90
  MANAGE / CONTROL:
90
91
  • Use action without execution fields: list, get, models, create, update, delete, eject, disable, enable, reset, grant-spawn-budget, doctor, watchdog.status, watchdog.check, watchdog.recommend-model, watchdog.configure.
91
92
  • Agent acceptanceRole (read-only or writer) affects inferred acceptance only, never tools. Explicit task intent wins; omission keeps name heuristics. Update with false or an empty string to clear it.
92
- • Async control actions: status, interrupt, stop, resume, steer, append-step. Use stop with an id for current-session top-level async runs. Use status view:"fleet" for active-run overview, view:"transcript" to tail child output, steer for acknowledged top-level live async guidance, and resume for paused/completed/failed revival or a routed nested follow-up. Stopped runs are non-resumable. Steering delivery means Pi accepted the correlated user input, not model compliance; use index for a specific child.
93
+ • Async control actions: status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint. Use stop with an id for current-session top-level async runs. Use status view:"fleet" for active-run overview, view:"transcript" to tail child output, steer for acknowledged top-level live async guidance, and resume for paused/completed/failed revival or a routed nested follow-up. Stopped runs are non-resumable. Steering delivery means Pi accepted the correlated user input, not model compliance; use index for a specific child.
93
94
  • Opt-in schedule actions: schedule, schedule-list, schedule-status, schedule-cancel. Schedule only explicit delayed runs the user asked for.
94
95
 
95
96
  ASYNC / WAIT: