@selesai/code 0.13.2 → 0.13.4

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 (120) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/defaults/models.json +85 -13
  3. package/dist/extensions/cost-reconcile.test.ts +200 -4
  4. package/dist/extensions/cost-reconcile.ts +88 -102
  5. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  6. package/dist/extensions/pi-intercom/README.md +4 -5
  7. package/dist/extensions/pi-intercom/config.test.ts +3 -31
  8. package/dist/extensions/pi-intercom/config.ts +0 -15
  9. package/dist/extensions/pi-intercom/index.ts +9 -46
  10. package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
  11. package/dist/extensions/pi-intercom/package.json +1 -1
  12. package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
  13. package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
  14. package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
  15. package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
  16. package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
  17. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  18. package/dist/extensions/pi-subagents/package.json +1 -1
  19. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
  20. package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
  21. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
  22. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  23. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
  24. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  25. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  27. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
  28. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
  29. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
  30. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  31. package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
  32. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
  37. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
  39. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
  40. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
  41. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
  42. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
  43. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
  44. package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
  45. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  46. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
  47. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
  48. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
  49. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
  50. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
  51. package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
  52. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
  53. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
  54. package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
  55. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
  56. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
  57. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
  58. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
  59. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
  60. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
  61. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
  62. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
  63. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
  64. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
  65. package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
  66. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
  68. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
  69. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
  70. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
  71. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
  72. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
  73. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
  74. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
  75. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
  76. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
  77. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
  78. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
  79. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
  80. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
  81. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
  82. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
  83. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
  84. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
  85. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
  86. package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
  87. package/dist/extensions/pi-web-agent/README.md +131 -0
  88. package/dist/extensions/pi-web-agent/package.json +4 -2
  89. package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
  90. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  91. package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
  92. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
  93. package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
  94. package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
  95. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
  96. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
  97. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
  98. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
  99. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
  100. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
  101. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
  102. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
  103. package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
  104. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
  105. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
  106. package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
  107. package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
  108. package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
  109. package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
  110. package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
  111. package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
  112. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
  113. package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
  114. package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
  115. package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
  116. package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
  117. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
  118. package/dist/extensions/pi-web-agent/src/types.ts +19 -4
  119. package/dist/extensions/tokenin-onboarding.ts +302 -0
  120. package/package.json +1 -1
@@ -32,9 +32,10 @@ import { toModelInfo } from "../shared/model-info.ts";
32
32
  import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
33
33
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
34
34
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
35
- import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
36
- import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
35
+ import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, resolveExternalCliRunnerStatus, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
36
+ import type { AcceptanceInput, AgentCapabilitiesSnapshot, AgentCapabilityRow, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
37
37
  import { getProjectConfigDir } from "../shared/utils.ts";
38
+ import { previewDisplayText } from "../shared/display-text.ts";
38
39
  import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
39
40
  import { listRuntimeAgentConfigs, mergeRuntimeAgents, type RuntimeAgentOwner } from "./runtime-agent-registry.ts";
40
41
  import { listExternalJobProviders } from "../api/external-job-provider.ts";
@@ -47,11 +48,21 @@ interface ManagementParams {
47
48
  action?: string;
48
49
  agent?: string;
49
50
  agentScope?: unknown;
51
+ capabilities?: unknown;
50
52
  config?: unknown;
51
53
  }
52
54
 
53
- function result(text: string, isError = false): AgentToolResult<Details> {
54
- return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [] } };
55
+ function result(text: string, isError = false, details?: Partial<Details>): AgentToolResult<Details> {
56
+ return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [], ...details } };
57
+ }
58
+
59
+ function jsonDetails<T>(value: T): T {
60
+ return JSON.parse(JSON.stringify(value)) as T;
61
+ }
62
+
63
+ function presentDetails<T extends Record<string, unknown>>(value: T): T | undefined {
64
+ const cleaned = jsonDetails(value);
65
+ return Object.keys(cleaned).length > 0 ? cleaned : undefined;
55
66
  }
56
67
 
57
68
  function parseCsv(value: string): string[] {
@@ -645,18 +656,135 @@ function runnerListBadge(agent: AgentConfig, providerNames: Set<string> | undefi
645
656
  return undefined;
646
657
  }
647
658
 
648
- function formatAgentListLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
659
+ function agentListMetadata(agent: AgentConfig, providerNames: Set<string> | undefined): string {
649
660
  const source = agent.source === "package" ? packageSourceLabel(agent) : agent.source;
650
- const parts = [
661
+ return [
651
662
  source,
652
663
  runnerListBadge(agent, providerNames),
653
664
  agent.defaultContext ? `context: ${agent.defaultContext}` : undefined,
654
665
  agent.aliases?.length ? `aliases: ${agent.aliases.join(", ")}` : undefined,
655
- ].filter((part): part is string => Boolean(part));
656
- return `- ${agent.name} (${parts.join(", ")}): ${agent.description}`;
666
+ ].filter((part): part is string => Boolean(part)).join(", ");
667
+ }
668
+
669
+ function formatAgentListLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
670
+ return `- ${agent.name} (${agentListMetadata(agent, providerNames)}): ${agent.description}`;
671
+ }
672
+
673
+ function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
674
+ const declaredTools = [
675
+ ...(agent.tools ?? []),
676
+ ...(agent.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
677
+ ];
678
+ let tools = "none";
679
+ if (agent.tools === undefined && agent.mcpDirectTools === undefined) {
680
+ tools = "default/ambient";
681
+ } else if (declaredTools.length > 0) {
682
+ tools = declaredTools.join(", ");
683
+ }
684
+ let model = "inherits current session";
685
+ if (agent.model !== undefined) {
686
+ model = agent.model;
687
+ if (agent.modelProvider && !agent.model.includes("/")) model = `${agent.modelProvider}/${agent.model}`;
688
+ }
689
+ const thinking = agent.thinking === false ? "off" : agent.thinking ?? "default";
690
+ return `- ${agent.name} (${agentListMetadata(agent, providerNames)}): Description: ${previewDisplayText(agent.description, 240)}; Tools: ${tools}; Model: ${model}; Thinking: ${thinking}`;
691
+ }
692
+
693
+ const EXTERNAL_JOB_CAPABILITIES = { stop: false, steer: false, resume: false, structuredOutput: false, toolEvents: false } as const;
694
+ const PI_AGENT_RUNNER = { type: "pi" } as const;
695
+
696
+ function listOrEmpty<T>(values: T[] | undefined): T[] {
697
+ return values ?? [];
698
+ }
699
+
700
+ function agentCapabilityRunner(agent: AgentConfig, providerNames: Set<string> | undefined): AgentCapabilityRow["runner"] {
701
+ const runner = agent.runner;
702
+ if (!runner || runner.type === "pi") return PI_AGENT_RUNNER;
703
+ if (runner.type === "external-cli") return { type: "external-cli", adapter: runner.adapter, capabilities: resolveExternalCliRunnerStatus(runner).capabilities };
704
+ return { type: "external-job", provider: runner.provider, available: providerNames?.has(runner.provider), capabilities: EXTERNAL_JOB_CAPABILITIES };
705
+ }
706
+
707
+ function agentCapabilityTools(agent: AgentConfig): AgentCapabilityRow["tools"] {
708
+ return {
709
+ ambient: agent.tools === undefined && agent.mcpDirectTools === undefined,
710
+ names: listOrEmpty(agent.tools),
711
+ mcpDirectTools: listOrEmpty(agent.mcpDirectTools),
712
+ mutationTools: agent.mutationTools,
713
+ };
714
+ }
715
+
716
+ function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean; providerNames?: Set<string>; restrictionSources?: string[] }): AgentCapabilityRow {
717
+ return {
718
+ name: agent.name,
719
+ description: previewDisplayText(agent.description, 1000),
720
+ source: agent.source,
721
+ executable: options.executable,
722
+ restrictionSources: options.executable ? undefined : options.restrictionSources ?? [],
723
+ aliases: agent.aliases ? [...agent.aliases] : undefined,
724
+ runner: agentCapabilityRunner(agent, options.providerNames),
725
+ tools: agentCapabilityTools(agent),
726
+ model: presentDetails({ value: agent.model, fallbackModels: agent.fallbackModels, thinking: agent.thinking }),
727
+ execution: presentDetails({ defaultAsync: agent.defaultAsync, timeoutMs: agent.defaultTimeoutMs }),
728
+ output: presentDetails({ path: agent.output, mode: agent.outputMode }),
729
+ extensions: presentDetails({ names: agent.extensions, subagentOnly: agent.subagentOnlyExtensions, skills: agent.skills }),
730
+ };
657
731
  }
658
732
 
659
- function formatAgentListSections(agents: AgentConfig[], providerNames: Set<string> | undefined): string[] {
733
+ function agentCapabilitiesSnapshot(input: { agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; restrictedSources?: string[] }): AgentCapabilitiesSnapshot {
734
+ return {
735
+ agents: [
736
+ ...input.agents.map((agent) => agentCapabilityRow(agent, { executable: true, providerNames: input.providerNames })),
737
+ ...input.restrictedAgents.map((agent) => agentCapabilityRow(agent, { executable: false, providerNames: input.providerNames, restrictionSources: input.restrictedSources })),
738
+ ],
739
+ restrictedCount: input.restrictedAgents.length,
740
+ ...(input.restrictedSources?.length ? { capabilityCeilingSources: [...input.restrictedSources] } : {}),
741
+ };
742
+ }
743
+
744
+ function providerNames(status: ExternalJobProviderStatus): Set<string> | undefined {
745
+ return status.ok ? status.names : undefined;
746
+ }
747
+
748
+ function appendRestrictedAgentLines(input: { lines: string[]; agents: AgentConfig[]; sources?: string[]; providerNames?: Set<string>; formatLine: (agent: AgentConfig, providerNames: Set<string> | undefined) => string }): void {
749
+ if (input.agents.length === 0) return;
750
+ input.lines.push(
751
+ "",
752
+ `Restricted agents (not executable in this session${input.sources?.length ? `; capability ceiling: ${input.sources.join(", ")}` : ""}):`,
753
+ ...input.agents.map((agent) => input.formatLine(agent, input.providerNames)),
754
+ );
755
+ }
756
+
757
+ function appendExternalJobRegistryLine(lines: string[], agents: AgentConfig[], status: ExternalJobProviderStatus): void {
758
+ if (status.ok || !agents.some((agent) => agent.runner?.type === "external-job")) return;
759
+ lines.push("", `External-job provider registry unavailable: ${status.error}`);
760
+ }
761
+
762
+ function appendAgentDiagnosticLines(lines: string[], diagnostics: AgentDiscoveryDiagnostic[] | undefined): void {
763
+ if (!diagnostics?.length) return;
764
+ lines.push(
765
+ "",
766
+ "Invalid agent definitions:",
767
+ ...diagnostics.map((diagnostic) => `- ${diagnostic.name ?? diagnostic.filePath} (${diagnostic.source}): ${diagnostic.error}`),
768
+ );
769
+ }
770
+
771
+ function agentCapabilityDetails(input: { capabilityMode: boolean; agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; restrictedSources?: string[] }): Partial<Details> | undefined {
772
+ if (!input.capabilityMode) return undefined;
773
+ return {
774
+ agentCapabilities: jsonDetails(agentCapabilitiesSnapshot({
775
+ agents: input.agents,
776
+ restrictedAgents: input.restrictedAgents,
777
+ providerNames: input.providerNames,
778
+ restrictedSources: input.restrictedSources,
779
+ })),
780
+ };
781
+ }
782
+
783
+ function formatAgentListSections(
784
+ agents: AgentConfig[],
785
+ providerNames: Set<string> | undefined,
786
+ formatLine: (agent: AgentConfig, providerNames: Set<string> | undefined) => string = formatAgentListLine,
787
+ ): string[] {
660
788
  if (agents.length === 0) return ["- (none)"];
661
789
  const sections: Array<[AgentSource, string]> = [
662
790
  ["package", "Package agents"],
@@ -670,7 +798,7 @@ function formatAgentListSections(agents: AgentConfig[], providerNames: Set<strin
670
798
  const matches = agents.filter((agent) => agent.source === source);
671
799
  if (matches.length === 0) continue;
672
800
  if (lines.length > 0) lines.push("");
673
- lines.push(label, ...matches.map((agent) => formatAgentListLine(agent, providerNames)));
801
+ lines.push(label, ...matches.map((agent) => formatLine(agent, providerNames)));
674
802
  }
675
803
  return lines;
676
804
  }
@@ -758,23 +886,24 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
758
886
  discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
759
887
  });
760
888
  const providerStatus = registeredExternalJobProviderStatus();
889
+ const providerNameSet = providerNames(providerStatus);
890
+ const capabilityMode = params.capabilities === true;
891
+ const formatLine = capabilityMode ? formatAgentCapabilitiesLine : formatAgentListLine;
761
892
  const lines = [
762
- "Executable agents:",
763
- ...formatAgentListSections(agents, providerStatus.ok ? providerStatus.names : undefined),
764
- ...(restrictedAgents.length ? [
765
- "",
766
- `Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
767
- ...restrictedAgents.map((a) => formatAgentListLine(a, providerStatus.ok ? providerStatus.names : undefined)),
768
- ] : []),
769
- ...(!providerStatus.ok && [...agents, ...restrictedAgents].some((agent) => agent.runner?.type === "external-job") ? ["", `External-job provider registry unavailable: ${providerStatus.error}`] : []),
770
- ...(d.agentDiagnostics?.length ? [
771
- "",
772
- "Invalid agent definitions:",
773
- ...d.agentDiagnostics.map((diagnostic) => `- ${diagnostic.name ?? diagnostic.filePath} (${diagnostic.source}): ${diagnostic.error}`),
774
- ] : []),
775
- ...(proactiveSuggestions.length ? ["", ...proactiveSuggestions] : []),
893
+ capabilityMode ? "Executable agents (capabilities):" : "Executable agents:",
894
+ ...formatAgentListSections(agents, providerNameSet, formatLine),
776
895
  ];
777
- return result(lines.join("\n"));
896
+ appendRestrictedAgentLines({ lines, agents: restrictedAgents, sources: restrictedSources, providerNames: providerNameSet, formatLine });
897
+ appendExternalJobRegistryLine(lines, [...agents, ...restrictedAgents], providerStatus);
898
+ appendAgentDiagnosticLines(lines, d.agentDiagnostics);
899
+ if (proactiveSuggestions.length) lines.push("", ...proactiveSuggestions);
900
+ return result(lines.join("\n"), false, agentCapabilityDetails({
901
+ capabilityMode,
902
+ agents,
903
+ restrictedAgents,
904
+ providerNames: providerNameSet,
905
+ restrictedSources,
906
+ }));
778
907
  }
779
908
 
780
909
  function formatModelSource(agent: AgentConfig, currentModel: ParentModel | undefined): string {
@@ -1066,7 +1195,7 @@ function handleDisable(params: ManagementParams, ctx: ManagementContext): AgentT
1066
1195
  const scope = parsedScope.scope!;
1067
1196
  const d = discoverAgentsAll(ctx.cwd);
1068
1197
  if (scope === "project" && d.projectSettingsPath === null) {
1069
- 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);
1198
+ return result("Project override is not available here: no project config root (.selesai or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1070
1199
  }
1071
1200
  const effective = resolveEffectiveAgent(d, raw);
1072
1201
  if (effective.error) return result(effective.error, true);
@@ -1090,7 +1219,7 @@ function handleEnable(params: ManagementParams, ctx: ManagementContext): AgentTo
1090
1219
  const scope = parsedScope.scope!;
1091
1220
  const d = discoverAgentsAll(ctx.cwd);
1092
1221
  if (scope === "project" && d.projectSettingsPath === null) {
1093
- 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);
1222
+ return result("Project override is not available here: no project config root (.selesai or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1094
1223
  }
1095
1224
  const effective = resolveEffectiveAgent(d, raw);
1096
1225
  if (effective.error) return result(effective.error, true);
@@ -1119,7 +1248,7 @@ function handleReset(params: ManagementParams, ctx: ManagementContext): AgentToo
1119
1248
  const scope = parsedScope.scope!;
1120
1249
  const d = discoverAgentsAll(ctx.cwd);
1121
1250
  if (scope === "project" && d.projectSettingsPath === null) {
1122
- 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);
1251
+ return result("Project override is not available here: no project config root (.selesai or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1123
1252
  }
1124
1253
  const bundled = [...d.package, ...d.builtin].find((a) => a.name === raw || a.name === sanitized);
1125
1254
  if (!bundled) {
@@ -5,6 +5,8 @@ export {
5
5
  type AcceptanceInput,
6
6
  type AcceptanceLedger,
7
7
  type AcceptanceLevel,
8
+ type AgentCapabilitiesSnapshot,
9
+ type AgentCapabilityRow,
8
10
  type AsyncStartedEvent,
9
11
  type ControlEvent,
10
12
  type Details,
@@ -1,5 +1,6 @@
1
1
  export interface PublicSubagentExecutionParams {
2
2
  action?: unknown;
3
+ capabilities?: unknown;
3
4
  mode?: unknown;
4
5
  repo?: unknown;
5
6
  planId?: unknown;
@@ -282,6 +282,7 @@ const SubagentParamProperties = {
282
282
  action: Type.Optional(Type.String({ minLength: 1,
283
283
  description: "Optional management/control action. Use action='validate' with workflowScript or workflowScriptPath for offline checks. Omit this field for structured single-child or workflow execution; otherwise, use it only for management/control actions."
284
284
  })),
285
+ capabilities: Type.Optional(Type.Boolean({ description: "For action='list', return compact capability rows and structured details without system prompts." })),
285
286
  name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
286
287
  id: Type.Optional(Type.String({
287
288
  description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
@@ -9,7 +9,9 @@ const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (codex-exec, codex-exe
9
9
  const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.";
10
10
  const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
11
11
  const WORKFLOW_LANES_GUIDANCE = "For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed.";
12
- const WORKFLOW_HOST_GUIDANCE = "For one non-interactive operator-owned command, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). v1 supports only command steps; output is bounded and command failure fails the workflow.";
12
+ const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains instead.";
13
+ const WORKFLOW_HOST_GUIDANCE = "For one non-interactive operator-owned command, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
14
+ const AGENT_CAPABILITY_GUIDANCE = "For capability selection, use { action: \"list\", capabilities: true } for compact prompt-free rows.";
13
15
 
14
16
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
15
17
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
@@ -66,6 +68,7 @@ ASYNC / SAFETY:
66
68
  • Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
67
69
  • ${WORKFLOW_RESUME_KEY_GUIDANCE}
68
70
  • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
71
+ • ${WORKFLOW_HOST_GUIDANCE}
69
72
  • Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
70
73
  • Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
71
74
  • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
@@ -20,6 +20,7 @@ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveE
20
20
  import type { RunnerStep } from "../shared/parallel-utils.ts";
21
21
  import type { ContextMode } from "../shared/context-mode.ts";
22
22
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
23
+ import { backgroundProcessOptions } from "../shared/background-process-options.ts";
23
24
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
24
25
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
25
26
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
@@ -562,9 +563,8 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
562
563
  }
563
564
  const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
564
565
  cwd,
565
- detached: true,
566
+ ...backgroundProcessOptions(),
566
567
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
567
- windowsHide: true,
568
568
  env: {
569
569
  ...omitExtensionBindingsEnv(process.env),
570
570
  ...(piPackageRoot ? { [SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot } : {}),
@@ -146,6 +146,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
146
146
  chainStepCount: run.chainStepCount,
147
147
  parallelGroups: groups,
148
148
  hostSteps: run.hostSteps,
149
+ ...(run.mode === "workflow" && run.workflowGraph ? { workflowGraph: run.workflowGraph } : {}),
149
150
  preflight: run.preflight,
150
151
  steps: visibleSteps,
151
152
  stepsTotal: visibleSteps.length,
@@ -401,6 +402,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
401
402
  job.workflowKey = status.workflowKey ?? job.workflowKey;
402
403
  job.lane = status.lane ?? job.lane;
403
404
  job.workflow = status.workflow ?? job.workflow;
405
+ if (status.mode === "workflow") job.workflowGraph = status.workflowGraph ?? job.workflowGraph;
404
406
  job.hostSteps = validHostStepNodes(status.workflowGraph);
405
407
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
406
408
  if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
@@ -627,6 +629,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
627
629
  turnBudget: info.turnBudget,
628
630
  parentWorkflowRunId: info.parentWorkflowRunId,
629
631
  workflowKey: info.workflowKey,
632
+ ...(info.mode === "workflow" && info.workflowGraph ? { workflowGraph: info.workflowGraph } : {}),
630
633
  controlEventCursor: 0,
631
634
  });
632
635
  const job = state.asyncJobs.get(info.id)!;
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
+ import { previewDisplayText } from "../../shared/display-text.ts";
4
5
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1 } from "../../shared/types.ts";
6
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1, type WorkflowGraphSnapshot } from "../../shared/types.ts";
6
7
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
7
8
  import { readStatus } from "../../shared/utils.ts";
8
9
  import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
@@ -21,6 +22,8 @@ import { assertWorkflowGraphHostSteps, hostStepReportName, hostStepVerdictLabel,
21
22
  import { projectAsyncWorkflowRows } from "../shared/async-status-projection.ts";
22
23
  import { validateAsyncStatusLaneMetadata } from "../shared/lane-metadata.ts";
23
24
  import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
25
+ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
26
+ import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
24
27
 
25
28
  interface AsyncRunStepSummary {
26
29
  index: number;
@@ -75,6 +78,7 @@ interface AsyncRunStepSummary {
75
78
  review?: AsyncJobStep["review"];
76
79
  effects?: AsyncJobStep["effects"];
77
80
  processTerminal?: AsyncJobStep["processTerminal"];
81
+ timeoutRecovery?: TimeoutRecoveryProjection;
78
82
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
79
83
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
80
84
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -116,6 +120,7 @@ export interface AsyncRunSummary {
116
120
  pendingAppends?: number;
117
121
  parallelGroups?: AsyncParallelGroupStatus[];
118
122
  hostSteps?: HostStepNodeV1[];
123
+ workflowGraph?: AsyncStatus["workflowGraph"];
119
124
  steps: AsyncRunStepSummary[];
120
125
  sessionDir?: string;
121
126
  outputFile?: string;
@@ -282,6 +287,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
282
287
  const summarizedSteps = steps.map((step, index) => {
283
288
  const stepActivityState = step.activityState;
284
289
  const stepLastActivityAt = step.lastActivityAt;
290
+ const timeoutRecovery = projectTimeoutRecovery(step.timeoutRecovery);
285
291
  return {
286
292
  index,
287
293
  childId: asyncStatusChildIdentity(step, index),
@@ -341,6 +347,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
341
347
  ...(step.effects ? { effects: step.effects } : {}),
342
348
  ...(step.watchdog ? { watchdog: step.watchdog } : {}),
343
349
  ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
350
+ ...(timeoutRecovery ? { timeoutRecovery } : {}),
344
351
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
345
352
  ...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
346
353
  ...(step.children?.length ? { children: step.children } : {}),
@@ -381,6 +388,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
381
388
  ...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
382
389
  ...(parallelGroups.length ? { parallelGroups } : {}),
383
390
  ...(hostSteps.length ? { hostSteps } : {}),
391
+ ...(status.mode === "workflow" && status.workflowGraph ? { workflowGraph: status.workflowGraph } : {}),
384
392
  steps: summarizedSteps,
385
393
  ...(nestedChildren.length ? { nestedChildren } : {}),
386
394
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
@@ -575,12 +583,41 @@ function formatHostStepLine(row: ReturnType<typeof projectAsyncWorkflowRows>[num
575
583
  return `host ${row.kind}: ${row.name} | ${state}${details.length ? ` | ${details.join(" | ")}` : ""}`;
576
584
  }
577
585
 
586
+ function workflowStageStateLabel(status: WorkflowGraphSnapshot["nodes"][number]["status"]): string {
587
+ switch (status) {
588
+ case "completed":
589
+ return "complete";
590
+ case "detached":
591
+ return "paused";
592
+ default:
593
+ return status;
594
+ }
595
+ }
596
+
597
+ export function formatWorkflowStageLine(node: WorkflowGraphSnapshot["nodes"][number], index: number, total: number): string {
598
+ const state = workflowStageStateLabel(node.status);
599
+ const id = previewDisplayText(node.id, 160);
600
+ const label = node.label ? previewDisplayText(node.label, 160) : "";
601
+ const display = label && label !== id ? ` | ${label}` : "";
602
+ const agent = node.agent ? ` | ${previewDisplayText(node.agent, 80)}` : "";
603
+ const error = node.error ? ` | ${previewDisplayText(node.error, 240)}` : "";
604
+ return `stage ${index + 1}/${total}: ${id}${display}${agent} | ${state}${error}`;
605
+ }
606
+
578
607
  export function formatAsyncRunOutputPath(run: Pick<AsyncRunSummary, "asyncDir" | "outputFile">): string | undefined {
579
608
  if (!run.outputFile) return undefined;
580
609
  return path.isAbsolute(run.outputFile) ? run.outputFile : path.join(run.asyncDir, run.outputFile);
581
610
  }
582
611
 
583
- export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
612
+ export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps"> & { workflowGraph?: WorkflowGraphSnapshot }): string {
613
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
614
+ if (graphStages.length > 0) {
615
+ const currentNode = graphStages.find((node) => node.id === run.workflowGraph?.currentNodeId);
616
+ if (currentNode && currentNode.status !== "completed") return `stage ${graphStages.indexOf(currentNode) + 1}/${graphStages.length}`;
617
+ const activeNode = graphStages.find((node) => node.status === "running") ?? graphStages.find((node) => node.status !== "completed");
618
+ if (activeNode) return `stage ${graphStages.indexOf(activeNode) + 1}/${graphStages.length}`;
619
+ return `stage ${graphStages.length}/${graphStages.length}`;
620
+ }
584
621
  const stepCount = run.steps.length || 1;
585
622
  const chainStepCount = run.chainStepCount ?? stepCount;
586
623
  const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, chainStepCount);
@@ -622,8 +659,14 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
622
659
  if (preflightWarning) lines.push(preflightWarning);
623
660
  for (const step of run.steps) {
624
661
  lines.push(` ${formatStepLine(step)}`);
662
+ lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
625
663
  lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
626
664
  }
665
+ const loadedWorkflowKeys = new Set(run.steps.flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
666
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
667
+ for (const [index, node] of graphStages.entries()) {
668
+ if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
669
+ }
627
670
  for (const row of projectAsyncWorkflowRows([], run.hostSteps)) {
628
671
  const line = formatHostStepLine(row);
629
672
  if (line) lines.push(` ${line}`);
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Cross-OS control channel for async subagent runs.
3
3
  *
4
- * Background runs are detached OS processes. The original control path delivered
5
- * an interrupt with `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
4
+ * Background runs use child processes. Unix detaches them from the parent process.
5
+ * The original control path delivered an interrupt with
6
+ * `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
6
7
  * deliver those signals cross-process via `process.kill` and throws `ENOSYS`,
7
8
  * which left async runs uninterruptible (no stop, no live steer) on Windows.
8
9
  *
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { safeTerminalText } from "../../shared/display-text.ts";
5
- import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
+ import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, formatWorkflowStageLine, listAsyncRuns } from "./async-status.ts";
6
6
  import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
7
7
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
8
8
  import { formatModelThinking } from "../../shared/formatters.ts";
@@ -25,7 +25,9 @@ import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts"
25
25
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
26
26
  import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
27
27
  import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
28
+ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
28
29
  import { getExternalJobProvider } from "../../api/external-job-provider.ts";
30
+ import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
29
31
 
30
32
  interface RunStatusParams {
31
33
  action?: string;
@@ -481,6 +483,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
481
483
  currentStep: status.currentStep,
482
484
  chainStepCount: status.chainStepCount,
483
485
  parallelGroups: status.parallelGroups,
486
+ workflowGraph: status.workflowGraph,
484
487
  steps: (status.steps ?? []).map((step, index) => ({ index, agent: step.agent, status: step.status })),
485
488
  });
486
489
  const started = new Date(status.startedAt).toISOString();
@@ -548,6 +551,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
548
551
  const display = runStatusStepDisplayName(step);
549
552
  const phase = step.phase ? `[${step.phase}] ` : "";
550
553
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
554
+ lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
551
555
  if (step.runner?.type === "external-cli") {
552
556
  const runner = normalizeExternalCliRunnerStatus(step.runner);
553
557
  if (runner) {
@@ -593,6 +597,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
593
597
  lines.push(" Steer: unavailable; external runners do not accept live messages.");
594
598
  }
595
599
  }
600
+ const loadedWorkflowKeys = new Set((status.steps ?? []).flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
601
+ const graphStages = status.mode === "workflow" ? workflowGraphStageNodes(status.workflowGraph) : [];
602
+ for (const [index, node] of graphStages.entries()) {
603
+ if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
604
+ }
596
605
  const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
597
606
  const unattached = nestedChildren.filter((child) => !attached.has(child.id));
598
607
  lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
@@ -632,7 +641,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
632
641
  }
633
642
  try {
634
643
  const raw = fs.readFileSync(resultPath, "utf-8");
635
- const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
644
+ const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; timeoutRecovery?: unknown; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null; timeoutRecovery?: unknown }> };
636
645
  if (params.view === "transcript") {
637
646
  try {
638
647
  return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
@@ -660,6 +669,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
660
669
  const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
661
670
  if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
662
671
  const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
672
+ lines.push(...formatTimeoutRecoveryLines(data.timeoutRecovery, " "));
673
+ for (const child of children) lines.push(...formatTimeoutRecoveryLines(child.timeoutRecovery, " "));
663
674
  lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
664
675
  if (data.summary) lines.push("", data.summary);
665
676
  const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);
@@ -75,7 +75,7 @@ import {
75
75
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
76
76
  Semaphore,
77
77
  } from "../shared/parallel-utils.ts";
78
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
78
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
79
79
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
80
80
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
81
81
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
@@ -1641,6 +1641,7 @@ async function runSingleStepInner(
1641
1641
  let writerAttemptCount = 0;
1642
1642
  const attemptNotes: string[] = [];
1643
1643
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1644
+ let finalRequiredOutputMissing: boolean | undefined;
1644
1645
  let finalResult: RunPiStreamingResult | undefined;
1645
1646
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
1646
1647
  let structuredAcceptanceReport: unknown;
@@ -1698,6 +1699,7 @@ async function runSingleStepInner(
1698
1699
  : undefined;
1699
1700
  const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1700
1701
  parentSessionId: step.parentSessionId,
1702
+ forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1701
1703
  baseArgs: ["--mode", "json", "-p"],
1702
1704
  task: attemptTask,
1703
1705
  taskDelivery: taskDeliveryOverride,
@@ -1930,6 +1932,7 @@ async function runSingleStepInner(
1930
1932
  ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1931
1933
  : undefined;
1932
1934
  const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1935
+ finalRequiredOutputMissing = requiredOutput?.missing;
1933
1936
  const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1934
1937
  const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
1935
1938
  ? 1
@@ -2117,6 +2120,7 @@ async function runSingleStepInner(
2117
2120
  ? buildTimeoutRecoverySummary({
2118
2121
  termination: "timed-out",
2119
2122
  evidence: finalMutationEvidence,
2123
+ requiredOutputMissing: finalRequiredOutputMissing,
2120
2124
  currentTool: finalResult?.currentTool,
2121
2125
  currentToolArgs: finalResult?.currentToolArgs,
2122
2126
  currentPath: finalResult?.currentPath,
@@ -62,6 +62,7 @@ import {
62
62
  type WaitCompletion,
63
63
  } from "../../shared/types.ts";
64
64
  import { formatDuration, shortenPath } from "../../shared/formatters.ts";
65
+ import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
65
66
  import { toAgentToolUsage } from "../../shared/utils.ts";
66
67
  import { collectWaitCompletions } from "./wait-completions.ts";
67
68
  import { formatResumeFirstFailedRunsNote } from "./resume-guidance.ts";
@@ -349,6 +350,12 @@ function result(text: string, isError = false, completions?: WaitCompletion[]):
349
350
  };
350
351
  }
351
352
 
353
+ function formatCompletionRecovery(completions: WaitCompletion[] | undefined): string {
354
+ const lines = (completions ?? []).flatMap((completion) =>
355
+ (completion.results ?? []).flatMap((child) => formatTimeoutRecoveryLines(child.timeoutRecovery)));
356
+ return lines.length > 0 ? `\n${lines.join("\n")}` : "";
357
+ }
358
+
352
359
  function windowElapsedResult(
353
360
  text: string,
354
361
  activeRunIds: string[],
@@ -685,6 +692,7 @@ export async function waitForSubagents(
685
692
  + providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
686
693
  const elapsed = formatDuration(now() - startedAt);
687
694
  const outcome = terminalSummary ? ` Outcome: ${terminalSummary}.` : "";
695
+ const recoveryNote = formatCompletionRecovery(completions);
688
696
 
689
697
  if (waitForAll) {
690
698
  const scope = params.id
@@ -694,7 +702,7 @@ export async function waitForSubagents(
694
702
  : `${initialAsyncIds.size} async run(s) and ${initialProviderIds.size} provider item(s)`;
695
703
  const status = relevantAttention.length > 0 ? "attention required" : "done";
696
704
  return result(
697
- `Waited ${elapsed} for ${scope}; ${status}.${outcome}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
705
+ `Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
698
706
  (deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
699
707
  completions,
700
708
  );
@@ -711,7 +719,7 @@ export async function waitForSubagents(
711
719
  ? `${relevantAttention.length} of ${initialCount} ${subject} need attention`
712
720
  : `${finishedCount} of ${initialCount} ${subject} finished`;
713
721
  return result(
714
- `Waited ${elapsed}; ${progress}.${outcome}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
722
+ `Waited ${elapsed}; ${progress}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
715
723
  (deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
716
724
  completions,
717
725
  );