pi-subagents 0.53.0 → 0.55.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 (76) hide show
  1. package/CHANGELOG.md +66 -5
  2. package/README.md +1 -1
  3. package/agents/reviewer.md +12 -2
  4. package/docs/agents.md +4 -4
  5. package/docs/configuration.md +6 -6
  6. package/docs/extension-api.md +14 -4
  7. package/docs/models.md +41 -6
  8. package/docs/observability.md +14 -3
  9. package/docs/tool-reference.md +21 -6
  10. package/docs/workflows.md +7 -1
  11. package/index.ts +10 -1
  12. package/package.json +1 -1
  13. package/prompts/council.md +19 -7
  14. package/prompts/parallel-review.md +5 -1
  15. package/prompts/review-loop.md +9 -5
  16. package/skills/council-mode/SKILL.md +50 -26
  17. package/skills/pi-subagents/SKILL.md +5 -1
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  19. package/skills/pi-subagents/references/execution-controls.md +13 -4
  20. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  21. package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
  22. package/src/agents/agent-management.ts +115 -14
  23. package/src/agents/agent-serializer.ts +2 -2
  24. package/src/agents/agents.ts +195 -47
  25. package/src/api/external-job-provider.ts +10 -1
  26. package/src/api/preflight.ts +33 -19
  27. package/src/api/project-panes.ts +2 -0
  28. package/src/extension/doctor.ts +10 -0
  29. package/src/extension/fanout-child.ts +3 -2
  30. package/src/extension/index.ts +24 -4
  31. package/src/extension/public-execution.ts +12 -9
  32. package/src/extension/rpc.ts +77 -3
  33. package/src/extension/schemas.ts +2 -1
  34. package/src/extension/tool-description.ts +9 -6
  35. package/src/extension/tool-result.ts +19 -0
  36. package/src/inspectors/herdr/client.ts +3 -3
  37. package/src/inspectors/herdr/focus.ts +55 -0
  38. package/src/inspectors/herdr/project-panes.ts +228 -44
  39. package/src/integrations/herdr-status.ts +26 -4
  40. package/src/runs/background/async-execution.ts +112 -13
  41. package/src/runs/background/async-job-tracker.ts +33 -14
  42. package/src/runs/background/async-resume.ts +20 -2
  43. package/src/runs/background/async-retention.ts +1 -1
  44. package/src/runs/background/async-status.ts +10 -0
  45. package/src/runs/background/chain-root-attachment.ts +5 -0
  46. package/src/runs/background/control-channel.ts +98 -10
  47. package/src/runs/background/notify.ts +62 -1
  48. package/src/runs/background/result-watcher.ts +4 -3
  49. package/src/runs/background/run-status.ts +15 -1
  50. package/src/runs/background/stale-run-reconciler.ts +3 -0
  51. package/src/runs/background/subagent-runner.ts +251 -38
  52. package/src/runs/background/wait-completions.ts +2 -0
  53. package/src/runs/background/wait-tool.ts +4 -3
  54. package/src/runs/foreground/async-stop-action.ts +23 -2
  55. package/src/runs/foreground/execution.ts +110 -8
  56. package/src/runs/foreground/subagent-executor.ts +361 -45
  57. package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
  58. package/src/runs/shared/acceptance.ts +1 -0
  59. package/src/runs/shared/child-identity.ts +36 -0
  60. package/src/runs/shared/completion-guard.ts +50 -1
  61. package/src/runs/shared/external-job-bridge.ts +53 -37
  62. package/src/runs/shared/external-job-runner.ts +126 -23
  63. package/src/runs/shared/model-fallback.ts +46 -15
  64. package/src/runs/shared/model-scope.ts +106 -39
  65. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  66. package/src/runs/shared/parallel-utils.ts +12 -0
  67. package/src/runs/shared/pi-args.ts +31 -1
  68. package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
  69. package/src/runs/shared/tool-availability.ts +1 -3
  70. package/src/shared/launch-contract.ts +6 -5
  71. package/src/shared/thinking-ceiling.ts +52 -0
  72. package/src/shared/types.ts +72 -5
  73. package/src/tui/fleet-status.ts +66 -13
  74. package/src/tui/render.ts +5 -4
  75. package/src/watchdog/permission-arbiter.ts +59 -51
  76. package/src/workflows/scripted-workflow.ts +107 -10
@@ -37,6 +37,7 @@ import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from
37
37
  import { getProjectConfigDir } from "../shared/utils.ts";
38
38
  import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
39
39
  import { listRuntimeAgentConfigs, mergeRuntimeAgents, type RuntimeAgentOwner } from "./runtime-agent-registry.ts";
40
+ import { listExternalJobProviders } from "../api/external-job-provider.ts";
40
41
 
41
42
  type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
42
43
  type ManagementScope = "user" | "project";
@@ -57,18 +58,23 @@ function parseCsv(value: string): string[] {
57
58
  return [...new Set(value.split(",").map((v) => v.trim()).filter(Boolean))];
58
59
  }
59
60
 
60
- function configObject(config: unknown): { value?: Record<string, unknown>; error?: string } {
61
+ type ConfigObjectResult =
62
+ | { status: "ok"; value: Record<string, unknown> }
63
+ | { status: "missing" }
64
+ | { status: "error"; message: string };
65
+
66
+ function configObject(config: unknown): ConfigObjectResult {
61
67
  let val = config;
62
68
  if (typeof val === "string") {
63
69
  try {
64
70
  val = JSON.parse(val);
65
71
  } catch (error) {
66
72
  const message = error instanceof Error ? error.message : String(error);
67
- return { error: `config must be valid JSON: ${message}` };
73
+ return { status: "error", message: `config must be valid JSON: ${message}` };
68
74
  }
69
75
  }
70
- if (!val || typeof val !== "object" || Array.isArray(val)) return {};
71
- return { value: val as Record<string, unknown> };
76
+ if (!val || typeof val !== "object" || Array.isArray(val)) return { status: "missing" };
77
+ return { status: "ok", value: val as Record<string, unknown> };
72
78
  }
73
79
 
74
80
  function hasKey(obj: Record<string, unknown>, key: string): boolean {
@@ -176,7 +182,8 @@ function isMutableSource(source: AgentSource): source is ManagementScope {
176
182
  function modelWarning(ctx: ManagementContext, model: string | undefined): string | undefined {
177
183
  if (!model) return undefined;
178
184
  const found = ctx.modelRegistry.getAvailable().some((m) => `${m.provider}/${m.id}` === model || m.id === model);
179
- return found ? undefined : `Warning: model '${model}' is not in the current model registry.`;
185
+ if (found) return undefined;
186
+ return `Warning: model '${model}' is not in the current model registry. Run subagent({ action: "models" }) to list valid provider/id selectors, then use the exact provider/id form (bare ids resolve only when unique).`;
180
187
  }
181
188
 
182
189
  function fallbackModelsWarning(ctx: ManagementContext, fallbackModels: string[] | undefined): string | undefined {
@@ -214,7 +221,9 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
214
221
  const base = agent.override?.base;
215
222
  const {
216
223
  override: _override,
224
+ output: _output,
217
225
  outputMode: _outputMode,
226
+ defaultReads: _defaultReads,
218
227
  model: _model,
219
228
  fallbackModels: _fallbackModels,
220
229
  thinking: _thinking,
@@ -242,7 +251,9 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
242
251
 
243
252
  return withDeclaredExtensionPaths({
244
253
  ...editable,
254
+ ...(base.output !== undefined ? { output: base.output } : {}),
245
255
  ...(base.outputMode !== undefined ? { outputMode: base.outputMode } : {}),
256
+ ...(base.defaultReads !== undefined ? { defaultReads: [...base.defaultReads] } : {}),
246
257
  ...(base.model !== undefined ? { model: base.model } : {}),
247
258
  ...(base.fallbackModels !== undefined ? { fallbackModels: [...base.fallbackModels] } : {}),
248
259
  ...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
@@ -590,9 +601,83 @@ function renamePath(currentPath: string, newName: string, scope: ManagementScope
590
601
  return { filePath };
591
602
  }
592
603
 
604
+ function packageSourceLabel(agent: AgentConfig): string {
605
+ if (!agent.packageSourceName) return agent.source;
606
+ return agent.packageSourceVersion ? `${agent.packageSourceName}@${agent.packageSourceVersion}` : agent.packageSourceName;
607
+ }
608
+
609
+ type ExternalJobProviderStatus =
610
+ | { ok: true; names: Set<string> }
611
+ | { ok: false; error: string };
612
+
613
+ function errorMessage(error: unknown): string {
614
+ return error instanceof Error ? error.message : String(error);
615
+ }
616
+
617
+ function registeredExternalJobProviderStatus(): ExternalJobProviderStatus {
618
+ try {
619
+ return { ok: true, names: new Set(listExternalJobProviders().map((provider) => provider.name)) };
620
+ } catch (error) {
621
+ return { ok: false, error: errorMessage(error) };
622
+ }
623
+ }
624
+
625
+ function externalJobProviderSuffix(provider: string, names: Set<string> | undefined): string {
626
+ if (!names) return "?";
627
+ return names.has(provider) ? "✓" : "missing";
628
+ }
629
+
630
+ function runnerListBadge(agent: AgentConfig, providerNames: Set<string> | undefined): string | undefined {
631
+ if (agent.runner?.type === "external-job") return `external-job:${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
632
+ if (agent.runner?.type === "external-cli") return "external-cli";
633
+ return undefined;
634
+ }
635
+
636
+ function formatAgentListLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
637
+ const source = agent.source === "package" ? packageSourceLabel(agent) : agent.source;
638
+ const parts = [
639
+ source,
640
+ runnerListBadge(agent, providerNames),
641
+ agent.defaultContext ? `context: ${agent.defaultContext}` : undefined,
642
+ agent.aliases?.length ? `aliases: ${agent.aliases.join(", ")}` : undefined,
643
+ ].filter((part): part is string => Boolean(part));
644
+ return `- ${agent.name} (${parts.join(", ")}): ${agent.description}`;
645
+ }
646
+
647
+ function formatAgentListSections(agents: AgentConfig[], providerNames: Set<string> | undefined): string[] {
648
+ if (agents.length === 0) return ["- (none)"];
649
+ const sections: Array<[AgentSource, string]> = [
650
+ ["package", "Package agents"],
651
+ ["user", "User agents"],
652
+ ["project", "Project agents"],
653
+ ["runtime", "Runtime agents"],
654
+ ["builtin", "Builtin agents"],
655
+ ];
656
+ const lines: string[] = [];
657
+ for (const [source, label] of sections) {
658
+ const matches = agents.filter((agent) => agent.source === source);
659
+ if (matches.length === 0) continue;
660
+ if (lines.length > 0) lines.push("");
661
+ lines.push(label, ...matches.map((agent) => formatAgentListLine(agent, providerNames)));
662
+ }
663
+ return lines;
664
+ }
665
+
666
+ function formatRunnerDetail(agent: AgentConfig, providerNames: Set<string> | undefined): string | undefined {
667
+ if (!agent.runner) return undefined;
668
+ if (agent.runner.type === "external-job") return `Runner: external-job via ${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
669
+ if (agent.runner.type === "external-cli") return `Runner: external-cli ${agent.runner.command}`;
670
+ return `Runner: ${JSON.stringify(agent.runner)}`;
671
+ }
672
+
593
673
  function formatAgentDetail(agent: AgentConfig): string {
594
674
  const tools = [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
675
+ const providerStatus = registeredExternalJobProviderStatus();
595
676
  const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
677
+ if (agent.source === "package" && agent.packageSourceName) {
678
+ lines.push(`Source package: ${packageSourceLabel(agent)}`);
679
+ if (agent.packageSourceRoot) lines.push(`Package root: ${agent.packageSourceRoot}`);
680
+ }
596
681
  if (agent.packageName) {
597
682
  lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
598
683
  lines.push(`Package: ${agent.packageName}`);
@@ -604,7 +689,12 @@ function formatAgentDetail(agent: AgentConfig): string {
604
689
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
605
690
  if (agent.skillPath?.length) lines.push(`Skill paths: ${agent.skillPath.join(", ")}`);
606
691
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
607
- if (agent.runner) lines.push(`Runner: ${JSON.stringify(agent.runner)}`);
692
+ const runnerDetail = formatRunnerDetail(agent, providerStatus.ok ? providerStatus.names : undefined);
693
+ if (runnerDetail) {
694
+ lines.push(runnerDetail);
695
+ if (agent.runner?.type === "external-job" && !providerStatus.ok) lines.push(`External-job provider registry unavailable: ${providerStatus.error}`);
696
+ if (agent.runner?.type === "external-job" && agent.runner.options) lines.push(`Runner options: ${JSON.stringify(agent.runner.options)}`);
697
+ }
608
698
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
609
699
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
610
700
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
@@ -654,16 +744,16 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
654
744
  ...(ctx.config?.proactiveSkillSubagents !== undefined ? { config: ctx.config.proactiveSkillSubagents } : {}),
655
745
  discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
656
746
  });
747
+ const providerStatus = registeredExternalJobProviderStatus();
657
748
  const lines = [
658
749
  "Executable agents:",
659
- ...(agents.length
660
- ? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`)
661
- : ["- (none)"]),
750
+ ...formatAgentListSections(agents, providerStatus.ok ? providerStatus.names : undefined),
662
751
  ...(restrictedAgents.length ? [
663
752
  "",
664
753
  `Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
665
- ...restrictedAgents.map((a) => `- ${a.name} (${a.source}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`),
754
+ ...restrictedAgents.map((a) => formatAgentListLine(a, providerStatus.ok ? providerStatus.names : undefined)),
666
755
  ] : []),
756
+ ...(!providerStatus.ok && [...agents, ...restrictedAgents].some((agent) => agent.runner?.type === "external-job") ? ["", `External-job provider registry unavailable: ${providerStatus.error}`] : []),
667
757
  ...(d.agentDiagnostics?.length ? [
668
758
  "",
669
759
  "Invalid agent definitions:",
@@ -752,6 +842,17 @@ function handleModels(params: ManagementParams, ctx: ManagementContext): AgentTo
752
842
  lines.push("");
753
843
  }
754
844
 
845
+ const availableFullIds = availableModels.map((m) => m.fullId).sort();
846
+ if (availableFullIds.length > 0) {
847
+ lines.push("Available models in this session's registry (copy an exact provider/id when passing model):");
848
+ lines.push("");
849
+ const shown = availableFullIds.slice(0, 80);
850
+ for (const fullId of shown) lines.push(` ${fullId}`);
851
+ if (availableFullIds.length > shown.length) lines.push(` ... and ${availableFullIds.length - shown.length} more`);
852
+ lines.push("");
853
+ lines.push("Use an exact provider/id from this list when you pass model; bare ids resolve only when unique in the registry.");
854
+ }
855
+
755
856
  return result(lines.join("\n"));
756
857
  }
757
858
 
@@ -777,9 +878,9 @@ function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolR
777
878
 
778
879
  export function handleCreate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
779
880
  const parsedConfig = configObject(params.config);
780
- if (parsedConfig.error) return result(parsedConfig.error, true);
881
+ if (parsedConfig.status === "error") return result(parsedConfig.message, true);
882
+ if (parsedConfig.status === "missing") return result("config required for create.", true);
781
883
  const cfg = parsedConfig.value;
782
- if (!cfg) return result("config required for create.", true);
783
884
  if (typeof cfg.name !== "string" || !cfg.name.trim()) return result("config.name is required and must be a non-empty string.", true);
784
885
  if (typeof cfg.description !== "string" || !cfg.description.trim()) return result("config.description is required and must be a non-empty string.", true);
785
886
  const name = sanitizeName(cfg.name);
@@ -827,9 +928,9 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
827
928
  export function handleUpdate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
828
929
  if (!params.agent) return result("Specify 'agent' for update.", true);
829
930
  const parsedConfig = configObject(params.config);
830
- if (parsedConfig.error) return result(parsedConfig.error, true);
931
+ if (parsedConfig.status === "error") return result(parsedConfig.message, true);
932
+ if (parsedConfig.status === "missing") return result("config required for update.", true);
831
933
  const cfg = parsedConfig.value;
832
- if (!cfg) return result("config required for update.", true);
833
934
  if (hasKey(cfg, "steps")) return result("Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", true);
834
935
  const warnings: string[] = [];
835
936
  const scopeHint = asDisambiguationScope(params.agentScope);
@@ -113,11 +113,11 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
113
113
  lines.push(`subagentOnlyExtensions: ${subagentOnlyExtensionsValue ?? ""}`);
114
114
  }
115
115
 
116
- if (config.output) lines.push(`output: ${config.output}`);
116
+ if (config.output || preserve("output")) lines.push(`output: ${config.output ?? ""}`);
117
117
  if (config.outputMode || preserve("outputMode")) lines.push(`outputMode: ${config.outputMode ?? ""}`);
118
118
 
119
119
  const readsValue = joinComma(config.defaultReads);
120
- if (readsValue) lines.push(`defaultReads: ${readsValue}`);
120
+ if (readsValue || preserve("defaultReads")) lines.push(`defaultReads: ${readsValue ?? ""}`);
121
121
 
122
122
  if (config.defaultProgress) lines.push("defaultProgress: true");
123
123
  if (config.interactive) lines.push("interactive: true");