pi-subagents 0.40.0 → 0.42.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 (121) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +54 -1879
  3. package/agents/oracle.md +1 -0
  4. package/inspector-runner.mjs +11 -0
  5. package/package.json +13 -5
  6. package/prompts/parallel-context-build.md +1 -1
  7. package/prompts/parallel-handoff-plan.md +1 -1
  8. package/prompts/review-loop.md +1 -1
  9. package/skills/pi-subagents/SKILL.md +9 -7
  10. package/skills/pi-subagents/references/constraints-and-recipes.md +24 -32
  11. package/skills/pi-subagents/references/execution-controls.md +99 -97
  12. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  13. package/skills/pi-subagents/references/prompting-and-roles.md +23 -30
  14. package/src/agents/agent-management.ts +155 -65
  15. package/src/agents/agent-serializer.ts +19 -0
  16. package/src/agents/agents.ts +154 -71
  17. package/src/agents/chain-serializer.ts +10 -7
  18. package/src/agents/frontmatter.ts +5 -3
  19. package/src/agents/identity.ts +1 -1
  20. package/src/agents/proactive-skills.ts +13 -10
  21. package/src/agents/skills.ts +23 -6
  22. package/src/api/control-channel.ts +4 -0
  23. package/src/api/delegation.ts +26 -194
  24. package/src/api/external-runs.ts +129 -0
  25. package/src/api/intercom-bridge.ts +3 -0
  26. package/src/api/pi-args.ts +5 -0
  27. package/src/api/preflight.ts +3 -3
  28. package/src/api/shared-types.ts +19 -0
  29. package/src/extension/config.ts +10 -0
  30. package/src/extension/control-notices.ts +5 -39
  31. package/src/extension/doctor.ts +10 -9
  32. package/src/extension/fanout-child.ts +7 -4
  33. package/src/extension/index.ts +232 -68
  34. package/src/extension/rpc.ts +18 -12
  35. package/src/extension/schemas.ts +48 -37
  36. package/src/extension/tool-description.ts +36 -86
  37. package/src/inspectors/herdr/actions.ts +229 -0
  38. package/src/inspectors/herdr/client.ts +130 -0
  39. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  40. package/src/inspectors/herdr/project-panes.ts +154 -0
  41. package/src/integrations/herdr-status.ts +330 -0
  42. package/src/intercom/intercom-bridge.ts +3 -2
  43. package/src/intercom/result-intercom.ts +5 -1
  44. package/src/missions/actions.ts +374 -0
  45. package/src/missions/lifecycle.ts +314 -0
  46. package/src/missions/store.ts +442 -0
  47. package/src/missions/types.ts +135 -0
  48. package/src/policy/authority.ts +46 -0
  49. package/src/profiles/profiles.ts +29 -3
  50. package/src/runs/background/async-execution.ts +110 -50
  51. package/src/runs/background/async-job-tracker.ts +27 -4
  52. package/src/runs/background/async-resume.ts +6 -6
  53. package/src/runs/background/async-status.ts +29 -1
  54. package/src/runs/background/auto-drain.ts +3 -3
  55. package/src/runs/background/chain-append.ts +3 -2
  56. package/src/runs/background/control-channel.ts +9 -7
  57. package/src/runs/background/fleet-view.ts +3 -4
  58. package/src/runs/background/notify.ts +2 -1
  59. package/src/runs/background/process-terminal.ts +5 -5
  60. package/src/runs/background/result-watcher.ts +13 -5
  61. package/src/runs/background/run-id-resolver.ts +3 -3
  62. package/src/runs/background/run-status.ts +35 -8
  63. package/src/runs/background/scheduled-runs.ts +602 -375
  64. package/src/runs/background/stale-run-reconciler.ts +3 -3
  65. package/src/runs/background/subagent-runner.ts +608 -445
  66. package/src/runs/background/subagent-wait.ts +50 -9
  67. package/src/runs/background/wait-subscriptions.ts +253 -0
  68. package/src/runs/background/wait-tool.ts +12 -4
  69. package/src/runs/foreground/async-steering-action.ts +3 -3
  70. package/src/runs/foreground/chain-clarify.ts +8 -4
  71. package/src/runs/foreground/chain-execution.ts +56 -30
  72. package/src/runs/foreground/execution.ts +34 -9
  73. package/src/runs/foreground/subagent-executor.ts +1036 -274
  74. package/src/runs/shared/acceptance.ts +28 -6
  75. package/src/runs/shared/child-protocol.ts +302 -22
  76. package/src/runs/shared/dynamic-fanout.ts +1 -1
  77. package/src/runs/shared/external-cli-runner.ts +130 -0
  78. package/src/runs/shared/long-running-guard.ts +42 -1
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -0
  80. package/src/runs/shared/nested-events.ts +59 -5
  81. package/src/runs/shared/nested-render.ts +9 -4
  82. package/src/runs/shared/parallel-handoff.ts +86 -2
  83. package/src/runs/shared/parallel-utils.ts +11 -2
  84. package/src/runs/shared/permissions.ts +95 -0
  85. package/src/runs/shared/pi-args.ts +468 -109
  86. package/src/runs/shared/pi-spawn.ts +11 -1
  87. package/src/runs/shared/run-history.ts +1 -1
  88. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  89. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  90. package/src/runs/shared/turn-budget.ts +6 -6
  91. package/src/runs/shared/worktree.ts +122 -12
  92. package/src/shared/accessible-dir.ts +29 -7
  93. package/src/shared/artifacts.ts +143 -1
  94. package/src/shared/fork-context.ts +3 -2
  95. package/src/shared/launch-contract.ts +1 -0
  96. package/src/shared/settings.ts +10 -0
  97. package/src/shared/types.ts +156 -14
  98. package/src/shared/utils.ts +8 -6
  99. package/src/slash/delegation-adapters.ts +32 -194
  100. package/src/slash/delegation-request.ts +43 -126
  101. package/src/slash/prompt-template-bridge.ts +158 -205
  102. package/src/slash/prompt-workflows.ts +21 -57
  103. package/src/slash/slash-bridge.ts +14 -0
  104. package/src/slash/slash-commands.ts +31 -632
  105. package/src/slash/subagents-admin.ts +18 -14
  106. package/src/tui/fleet-status.ts +156 -21
  107. package/src/tui/fleet-transcript.ts +110 -5
  108. package/src/tui/fleet.ts +57 -25
  109. package/src/tui/render.ts +368 -152
  110. package/src/types/pi-runtime-compat.d.ts +8 -0
  111. package/src/watchdog/lsp-diagnostics.ts +12 -7
  112. package/src/watchdog/model-selection.ts +2 -2
  113. package/src/watchdog/permission-arbiter.ts +145 -0
  114. package/src/watchdog/register-child.ts +1 -1
  115. package/src/watchdog/register-main.ts +1 -1
  116. package/src/watchdog/review.ts +4 -1
  117. package/src/watchdog/runtime.ts +3 -2
  118. package/src/workflows/chat-progress.ts +140 -0
  119. package/src/workflows/scripted-workflow.ts +415 -0
  120. package/agents/advisor.md +0 -73
  121. package/src/extension/chain-validation.ts +0 -181
@@ -28,7 +28,7 @@ import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
28
28
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
29
29
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
30
30
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
31
- import { resolveEffectiveAcceptance } from "../shared/acceptance.ts";
31
+ import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
32
32
  import {
33
33
  type AcceptanceInput,
34
34
  type AgentContract,
@@ -40,11 +40,11 @@ import {
40
40
  type ResolvedControlConfig,
41
41
  type ResolvedTurnBudget,
42
42
  type ResolvedToolBudget,
43
+ type ToolBudgetConfig,
43
44
  type SubagentRunMode,
44
45
  type SteeringRecoveryDescriptor,
45
46
  type UsageBudgetConfig,
46
- ASYNC_DIR,
47
- RESULTS_DIR,
47
+ DIRS,
48
48
  SUBAGENT_ASYNC_STARTED_EVENT,
49
49
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
50
50
  TEMP_ROOT_DIR,
@@ -61,6 +61,7 @@ import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal
61
61
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
62
62
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
63
63
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
64
+ import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
64
65
 
65
66
  const require = createRequire(import.meta.url);
66
67
  const piPackageRoot = resolvePiPackageRoot();
@@ -117,6 +118,7 @@ interface AsyncExecutionContext {
117
118
  currentSessionId: string;
118
119
  /** Parent session id used by permission-system ask forwarding. */
119
120
  parentSessionId?: string;
121
+ permissions?: PermissionConfig;
120
122
  currentModelProvider?: string;
121
123
  currentModel?: ParentModel;
122
124
  /** Optional model-scope enforcement resolved from subagent settings. */
@@ -166,6 +168,8 @@ interface AsyncChainParams {
166
168
  /** Global cap on simultaneously-running subagent tasks within the async run. */
167
169
  globalConcurrencyLimit?: number;
168
170
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
171
+ parentWorkflowRunId?: string;
172
+ workflowKey?: string;
169
173
  }
170
174
 
171
175
  interface AsyncSingleParams {
@@ -206,11 +210,14 @@ interface AsyncSingleParams {
206
210
  acceptance?: AcceptanceInput;
207
211
  timeoutMs?: number;
208
212
  absoluteDeadlineAt?: number;
209
- turnBudget?: ResolvedTurnBudget;
210
- toolBudget?: ResolvedToolBudget;
213
+ turnBudget?: { maxTurns: number; graceTurns?: number };
214
+ toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
211
215
  usageBudget?: UsageBudgetConfig;
212
216
  configToolBudget?: ResolvedToolBudget;
217
+ allowZeroToolBudget?: boolean;
213
218
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
219
+ parentWorkflowRunId?: string;
220
+ workflowKey?: string;
214
221
  }
215
222
 
216
223
  interface AsyncExecutionResult {
@@ -373,18 +380,12 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
373
380
  }
374
381
 
375
382
  function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
376
- const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
377
- try {
378
- fs.writeFileSync(tempPath, JSON.stringify(payload), { encoding: "utf-8", mode: 0o600 });
379
- fs.renameSync(tempPath, filePath);
380
- } catch (error) {
381
- try {
382
- fs.rmSync(tempPath, { force: true });
383
- } catch {
384
- // Preserve the startup-control write error.
385
- }
386
- throw error;
387
- }
383
+ // Delegate to the shared atomic JSON writer (temp file + rename, retrying
384
+ // transient Windows EPERM/EBUSY/EACCES locks and cleaning up the temp file
385
+ // on failure), so the startup handshake gets the same locking resilience as
386
+ // every other async control/result file. This is exercised by
387
+ // test/unit/atomic-json.test.ts.
388
+ writePrivateAtomicJson(filePath, payload);
388
389
  }
389
390
 
390
391
  function runnerIsAlive(pid: number): boolean {
@@ -430,9 +431,10 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
430
431
  fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
431
432
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
432
433
  const nodeCommand = resolveAsyncRunnerNodeCommand();
433
- const startupPath = typeof (launchConfig as { revivalLease?: unknown; asyncDir?: unknown }).revivalLease === "object"
434
- && typeof (launchConfig as { asyncDir?: unknown }).asyncDir === "string"
435
- ? path.join((launchConfig as { asyncDir: string }).asyncDir, "runner-startup.json")
434
+ const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; revivalLease?: unknown };
435
+ const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
436
+ const startupPath = typeof launchForStartup.revivalLease === "object" && launchAsyncDir
437
+ ? path.join(launchAsyncDir, "runner-startup.json")
436
438
  : undefined;
437
439
  const startupAckPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-ack.json") : undefined;
438
440
  const startupProceedPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-proceed.json") : undefined;
@@ -520,7 +522,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
520
522
  proc.unref();
521
523
  if (startupPath && startupAckPath && startupProceedPath) {
522
524
  const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
523
- if (!ready.ok) {
525
+ if (ready.ok === false) {
524
526
  terminateRunnerBeforeProceed(proc.pid);
525
527
  return { error: ready.error };
526
528
  }
@@ -531,7 +533,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
531
533
  return { error: `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}` };
532
534
  }
533
535
  const acknowledged = waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token);
534
- if (!acknowledged.ok) {
536
+ if (acknowledged.ok === false) {
535
537
  terminateRunnerBeforeProceed(proc.pid);
536
538
  return { error: acknowledged.error };
537
539
  }
@@ -643,6 +645,16 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
643
645
  };
644
646
  const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }) => {
645
647
  const a = agents.find((x) => x.name === s.agent)!;
648
+ const externalRunner = a.runner?.type === "external-cli";
649
+ if (externalRunner) {
650
+ const unsupported: string[] = [];
651
+ if (s.model !== undefined) unsupported.push("model override");
652
+ if (s.outputSchema !== undefined) unsupported.push("structured output");
653
+ if (s.acceptance !== undefined || params.agentContract !== undefined || s.agentContract !== undefined) unsupported.push("acceptance/agent contract");
654
+ if (s.toolBudget !== undefined || params.toolBudget !== undefined || a.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
655
+ if (params.contextForAgent?.(s.agent) === "fork") unsupported.push("fork context");
656
+ if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='external-cli' and does not support: ${unsupported.join(", ")}.`);
657
+ }
646
658
  try {
647
659
  assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])));
648
660
  } catch (error) {
@@ -658,7 +670,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
658
670
  if (inheritedRelativeParallelOutput && parallelOutputNamespace.taskIndex !== undefined) {
659
671
  behavior = {
660
672
  ...behavior,
661
- output: path.join(`parallel-${parallelOutputNamespace.stepIndex}`, `${parallelOutputNamespace.taskIndex}-${s.agent}`, behavior.output),
673
+ output: path.join(`parallel-${parallelOutputNamespace.stepIndex}`, `${parallelOutputNamespace.taskIndex}-${s.agent}`, behavior.output as string),
662
674
  };
663
675
  }
664
676
  const namespaceOutputPath = Boolean(inheritedRelativeParallelOutput && parallelOutputNamespace.taskIndex === undefined);
@@ -696,7 +708,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
696
708
  const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
697
709
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
698
710
 
699
- const primaryModel = resolveEffectiveSubagentModel(
711
+ const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
700
712
  s.model,
701
713
  a.model,
702
714
  ctx.currentModel,
@@ -705,8 +717,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
705
717
  { scope: ctx.modelScope },
706
718
  );
707
719
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
708
- const effectiveThinking = thinkingOverride ?? a.thinking;
709
- const model = applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
720
+ const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
721
+ const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
710
722
  const agentContract = s.agentContract ?? params.agentContract;
711
723
  const toolPlan = resolvePiLaunchToolPlan({
712
724
  tools: a.tools,
@@ -720,12 +732,18 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
720
732
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
721
733
  agentName: a.name,
722
734
  });
723
- const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
735
+ const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
736
+ const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
737
+ if (externalRunner && permissionRules) {
738
+ throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='external-cli', which cannot enforce native Pi child permission rules.`);
739
+ }
724
740
  return {
725
741
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
742
+ permissionRules,
726
743
  ...(params.capabilityCeiling ? { capabilityCeiling: params.capabilityCeiling } : {}),
727
744
  agent: s.agent,
728
745
  task,
746
+ ...(a.runner ? { runner: a.runner } : {}),
729
747
  ...(params.contextForAgent ? { context: params.contextForAgent(s.agent) } : {}),
730
748
  ...(agentContract ? { agentContract } : {}),
731
749
  phase: s.phase,
@@ -736,7 +754,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
736
754
  model,
737
755
  thinking: resolveEffectiveThinking(model, effectiveThinking),
738
756
  launchResolvedExtensions,
739
- modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
757
+ modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
740
758
  applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
741
759
  ),
742
760
  tools: a.tools,
@@ -889,7 +907,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
889
907
  seen.set(task.outputPath, { index, agent: task.agent });
890
908
  }
891
909
  }
892
- return { steps, runnerCwd, workflowGraph, eventChain: graphChain, ...(originalTask !== undefined ? { originalTask } : {}) };
910
+ return { steps: steps as RunnerStep[], runnerCwd, workflowGraph, eventChain: graphChain, ...(originalTask !== undefined ? { originalTask } : {}) };
893
911
  } catch (error) {
894
912
  if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return { error: error.message };
895
913
  throw error;
@@ -925,12 +943,21 @@ export function executeAsyncChain(
925
943
  nestedRoute,
926
944
  } = params;
927
945
  const resultMode = params.resultMode ?? "chain";
946
+ const acceptanceErrors = validateExecutionAcceptance({
947
+ chain: chain.map((step) => {
948
+ if (isCheckpointStep(step)) return {};
949
+ if (isParallelStep(step)) return { parallel: step.parallel };
950
+ if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
951
+ return { acceptance: step.acceptance };
952
+ }),
953
+ });
954
+ if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
928
955
  const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
929
956
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
930
957
  const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
931
958
  const asyncDir = inheritedNestedRoute
932
959
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
933
- : path.join(ASYNC_DIR, id);
960
+ : path.join(DIRS.async, id);
934
961
  try {
935
962
  fs.mkdirSync(asyncDir, { recursive: true });
936
963
  } catch (error) {
@@ -981,7 +1008,7 @@ export function executeAsyncChain(
981
1008
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
982
1009
  let childTargetIndex = 0;
983
1010
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
984
- if (!("parallel" in step) && step.importAsyncRoot) {
1011
+ if (!("parallel" in step) && "importAsyncRoot" in step && step.importAsyncRoot) {
985
1012
  childTargetIndex++;
986
1013
  return [undefined];
987
1014
  }
@@ -992,7 +1019,7 @@ export function executeAsyncChain(
992
1019
  }
993
1020
  return step.parallel.map((task) => childIntercomTarget(task.agent, childTargetIndex++));
994
1021
  }
995
- return [childIntercomTarget(step.agent, childTargetIndex++)];
1022
+ return "agent" in step ? [childIntercomTarget(step.agent, childTargetIndex++)] : [undefined];
996
1023
  }) : undefined;
997
1024
 
998
1025
  let spawnResult: { pid?: number; error?: string } = {};
@@ -1001,7 +1028,7 @@ export function executeAsyncChain(
1001
1028
  {
1002
1029
  id,
1003
1030
  steps,
1004
- resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
1031
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1005
1032
  cwd: runnerCwd,
1006
1033
  placeholder: "{previous}",
1007
1034
  maxOutput,
@@ -1029,6 +1056,8 @@ export function executeAsyncChain(
1029
1056
  deadlineAt,
1030
1057
  globalConcurrencyLimit: params.globalConcurrencyLimit,
1031
1058
  workflowGraph,
1059
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1060
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1032
1061
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1033
1062
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
1034
1063
  parentRunId: nestedAddress.parentRunId,
@@ -1052,6 +1081,9 @@ export function executeAsyncChain(
1052
1081
 
1053
1082
  if (spawnResult.pid) {
1054
1083
  const eventFirstStep = eventChain[0];
1084
+ if (!eventFirstStep) {
1085
+ return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': event chain has no steps`);
1086
+ }
1055
1087
  const firstAgents = isParallelStep(eventFirstStep)
1056
1088
  ? eventFirstStep.parallel.map((t) => t.agent)
1057
1089
  : isDynamicParallelStep(eventFirstStep)
@@ -1140,6 +1172,8 @@ export function executeAsyncChain(
1140
1172
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1141
1173
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
1142
1174
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1175
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1176
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1143
1177
  nestedRoute,
1144
1178
  });
1145
1179
  }
@@ -1152,7 +1186,7 @@ export function executeAsyncChain(
1152
1186
 
1153
1187
  return {
1154
1188
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`, ctx.interactive === true) }],
1155
- details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) },
1189
+ details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}), ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) },
1156
1190
  };
1157
1191
  }
1158
1192
 
@@ -1184,6 +1218,22 @@ export function executeAsyncSingle(
1184
1218
  nestedRoute,
1185
1219
  } = params;
1186
1220
  const task = params.task ?? "";
1221
+ const acceptanceErrors = validateAcceptanceInput(params.acceptance);
1222
+ if (acceptanceErrors.length > 0) return formatAsyncStartError("single", acceptanceErrors.join(" "));
1223
+ const externalRunner = agentConfig.runner?.type === "external-cli";
1224
+ const permissionRules = resolvePermissionRules(ctx.permissions, agentConfig.permissions);
1225
+ if (externalRunner) {
1226
+ const unsupported: string[] = [];
1227
+ if (params.modelOverride !== undefined) unsupported.push("model override");
1228
+ if (params.thinkingOverride !== undefined) unsupported.push("thinking override");
1229
+ if (params.structuredOutputSchema !== undefined) unsupported.push("structured output");
1230
+ if (params.acceptance !== undefined || params.agentContract !== undefined) unsupported.push("acceptance/agent contract");
1231
+ if (params.toolBudget !== undefined || agentConfig.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
1232
+ if (params.context === "fork") unsupported.push("fork context");
1233
+ if ((params.skills?.length ?? 0) > 0) unsupported.push("skills");
1234
+ if (permissionRules) unsupported.push("native Pi child permissions");
1235
+ if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='external-cli' and does not support: ${unsupported.join(", ")}.`);
1236
+ }
1187
1237
  const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
1188
1238
  try {
1189
1239
  assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
@@ -1215,7 +1265,7 @@ export function executeAsyncSingle(
1215
1265
  const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
1216
1266
  const asyncDir = inheritedNestedRoute
1217
1267
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
1218
- : path.join(ASYNC_DIR, id);
1268
+ : path.join(DIRS.async, id);
1219
1269
  try {
1220
1270
  fs.mkdirSync(asyncDir, { recursive: true });
1221
1271
  } catch (error) {
@@ -1234,14 +1284,14 @@ export function executeAsyncSingle(
1234
1284
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
1235
1285
  if (validationError) return formatAsyncStartError("single", validationError);
1236
1286
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
1237
- const primaryModel = resolveSubagentModelOverride(
1287
+ const primaryModel = externalRunner ? undefined : resolveSubagentModelOverride(
1238
1288
  params.modelOverride ?? agentConfig.model,
1239
1289
  ctx.currentModel,
1240
1290
  availableModels,
1241
1291
  ctx.currentModelProvider,
1242
1292
  );
1243
- const effectiveThinking = params.thinkingOverride ?? agentConfig.thinking;
1244
- const model = applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1293
+ const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1294
+ const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1245
1295
  const toolBudgetInput = params.toolBudget ?? agentConfig.toolBudget ?? params.configToolBudget;
1246
1296
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, params.toolBudget ? "toolBudget" : agentConfig.toolBudget ? "agent.toolBudget" : "config.toolBudget");
1247
1297
  if (resolvedToolBudget.error) return formatAsyncStartError("single", resolvedToolBudget.error);
@@ -1256,9 +1306,11 @@ export function executeAsyncSingle(
1256
1306
  const structuredOutput = params.structuredOutputSchema
1257
1307
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1258
1308
  : undefined;
1259
- const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
1260
- applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
1261
- );
1309
+ const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1310
+ .flatMap((candidate) => {
1311
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1312
+ return resolved ? [resolved] : [];
1313
+ });
1262
1314
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1263
1315
  const toolPlan = resolvePiLaunchToolPlan({
1264
1316
  tools: agentConfig.tools,
@@ -1272,7 +1324,7 @@ export function executeAsyncSingle(
1272
1324
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1273
1325
  agentName: agentConfig.name,
1274
1326
  });
1275
- const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1327
+ const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1276
1328
  const launchContractDigest = launchBindingDigest({
1277
1329
  definitionDigest: agentDefinitionDigest(agentConfig),
1278
1330
  task,
@@ -1331,7 +1383,7 @@ export function executeAsyncSingle(
1331
1383
  ...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
1332
1384
  ...(controlConfig ? { controlConfig } : {}),
1333
1385
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1334
- ...(params.turnBudget ? { initialTurnBudget: params.turnBudget } : {}),
1386
+ ...(initialTurnBudget ? { initialTurnBudget } : {}),
1335
1387
  ...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
1336
1388
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1337
1389
  ...(maxOutput ? { maxOutput } : {}),
@@ -1341,10 +1393,12 @@ export function executeAsyncSingle(
1341
1393
  artifactConfig,
1342
1394
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1343
1395
  };
1344
- try {
1345
- writePrivateAtomicJson(path.join(asyncDir, "recovery-descriptor.json"), recoveryDescriptor);
1346
- } catch (error) {
1347
- return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
1396
+ if (!externalRunner) {
1397
+ try {
1398
+ writePrivateAtomicJson(path.join(asyncDir, "recovery-descriptor.json"), recoveryDescriptor);
1399
+ } catch (error) {
1400
+ return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
1401
+ }
1348
1402
  }
1349
1403
  let spawnResult: { pid?: number; error?: string } = {};
1350
1404
  try {
@@ -1354,9 +1408,11 @@ export function executeAsyncSingle(
1354
1408
  steps: [
1355
1409
  {
1356
1410
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
1411
+ permissionRules,
1357
1412
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1358
1413
  agent,
1359
1414
  task: taskWithOutputInstruction,
1415
+ ...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
1360
1416
  ...(params.context ? { context: params.context } : {}),
1361
1417
  cwd: runnerCwd,
1362
1418
  model,
@@ -1374,7 +1430,7 @@ export function executeAsyncSingle(
1374
1430
  skills: resolvedSkills.map((r) => r.name),
1375
1431
  outputPath,
1376
1432
  outputMode,
1377
- sessionFile,
1433
+ ...(!externalRunner && sessionFile ? { sessionFile } : {}),
1378
1434
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1379
1435
  waitToolEnabled: params.waitToolEnabled,
1380
1436
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
@@ -1388,7 +1444,7 @@ export function executeAsyncSingle(
1388
1444
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1389
1445
  },
1390
1446
  ],
1391
- resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
1447
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1392
1448
  cwd: runnerCwd,
1393
1449
  placeholder: "{previous}",
1394
1450
  maxOutput,
@@ -1415,6 +1471,8 @@ export function executeAsyncSingle(
1415
1471
  resultMode: "single",
1416
1472
  launchContractDigest,
1417
1473
  launchResolvedExtensions,
1474
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1475
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1418
1476
  ...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
1419
1477
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1420
1478
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
@@ -1487,6 +1545,8 @@ export function executeAsyncSingle(
1487
1545
  asyncDir,
1488
1546
  launchContractDigest,
1489
1547
  launchResolvedExtensions,
1548
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1549
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1490
1550
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}),
1491
1551
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1492
1552
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
@@ -1497,6 +1557,6 @@ export function executeAsyncSingle(
1497
1557
 
1498
1558
  return {
1499
1559
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
1500
- details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, launchResolvedExtensions, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) },
1560
+ details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, launchResolvedExtensions, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: resolvedToolBudget.budget ?? params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) } as Details,
1501
1561
  };
1502
1562
  }
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { renderWidget, widgetRenderKey } from "../../tui/render.ts";
4
+ import { renderWidget, requestWidgetRender, widgetRenderKey } from "../../tui/render.ts";
5
5
  import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
6
6
  import {
7
7
  type AsyncJobState,
@@ -10,7 +10,7 @@ import {
10
10
  type SteeringNotice,
11
11
  type SubagentState,
12
12
  POLL_INTERVAL_MS,
13
- RESULTS_DIR,
13
+ DIRS,
14
14
  SUBAGENT_CONTROL_EVENT,
15
15
  SUBAGENT_CONTROL_INTERCOM_EVENT,
16
16
  SUBAGENT_STEERING_NOTICE_EVENT,
@@ -34,6 +34,7 @@ const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
34
34
  const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
35
35
  const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
36
36
  const MAX_RECENT_FLEET_JOBS = 20;
37
+ const WIDGET_ANIMATION_REFRESH_MS = 500;
37
38
 
38
39
  function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
39
40
  state.fleetJobs ??= new Map();
@@ -54,11 +55,16 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
54
55
  } {
55
56
  const completionRetentionMs = options.completionRetentionMs ?? 10000;
56
57
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
57
- const resultsDir = options.resultsDir ?? RESULTS_DIR;
58
+ const resultsDir = options.resultsDir ?? DIRS.results;
58
59
  const steeringNoticeSeen = new Map<string, number>();
60
+ let lastWidgetAnimationAt = 0;
61
+ const requestStatusRender = (ctx: ExtensionContext) => {
62
+ if (requestWidgetRender()) return;
63
+ (ctx.ui as { requestRender?: () => void }).requestRender?.();
64
+ };
59
65
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
60
66
  renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
61
- ctx.ui.requestRender?.();
67
+ requestStatusRender(ctx);
62
68
  };
63
69
  const rerenderLastWidget = (jobs = Array.from(state.asyncJobs.values())) => {
64
70
  const ctx = state.lastUiContext;
@@ -74,6 +80,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
74
80
  }
75
81
  };
76
82
  const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
83
+ const hasRunningWidgetJobs = () => options.widgetEnabled !== false && [...state.asyncJobs.values()].some((job) => job.status === "running");
84
+ const refreshWidgetAnimation = () => {
85
+ if (!hasRunningWidgetJobs()) return;
86
+ const now = Date.now();
87
+ if (now - lastWidgetAnimationAt < WIDGET_ANIMATION_REFRESH_MS) return;
88
+ lastWidgetAnimationAt = now;
89
+ requestWidgetRender();
90
+ };
77
91
  const restoredControlEventCursor = (asyncDir: string) => {
78
92
  try {
79
93
  return fs.statSync(path.join(asyncDir, "events.jsonl")).size;
@@ -131,6 +145,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
131
145
  sessionFile: run.sessionFile,
132
146
  controlEventCursor: restoredControlEventCursor(run.asyncDir),
133
147
  nestedChildren: run.nestedChildren,
148
+ parentWorkflowRunId: run.parentWorkflowRunId,
149
+ workflowKey: run.workflowKey,
150
+ workflow: run.workflow,
134
151
  };
135
152
  };
136
153
  const cancelCleanup = (asyncId: string) => {
@@ -332,6 +349,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
332
349
  job.toolCount = status.toolCount ?? job.toolCount;
333
350
  job.steering = status.steering ?? job.steering;
334
351
  job.mode = status.mode;
352
+ job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
353
+ job.workflowKey = status.workflowKey ?? job.workflowKey;
354
+ job.workflow = status.workflow ?? job.workflow;
335
355
  job.currentStep = status.currentStep ?? job.currentStep;
336
356
  job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
337
357
  job.startedAt = status.startedAt ?? job.startedAt;
@@ -394,6 +414,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
394
414
  }
395
415
 
396
416
  if (widgetChanged) rerenderLastWidget();
417
+ else refreshWidgetAnimation();
397
418
  }, pollIntervalMs);
398
419
  state.poller.unref?.();
399
420
  };
@@ -432,6 +453,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
432
453
  timeoutMs: info.timeoutMs,
433
454
  deadlineAt: info.deadlineAt,
434
455
  turnBudget: info.turnBudget,
456
+ parentWorkflowRunId: info.parentWorkflowRunId,
457
+ workflowKey: info.workflowKey,
435
458
  controlEventCursor: 0,
436
459
  });
437
460
  rememberFleetJob(state, state.asyncJobs.get(info.id)!);
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { ASYNC_DIR, RESULTS_DIR, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
3
+ import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
4
4
  import type { AgentConfig } from "../../agents/agents.ts";
5
5
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
6
6
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
@@ -381,8 +381,8 @@ function validateResumeSessionFile(runId: string, sessionFile: string): string {
381
381
  }
382
382
 
383
383
  export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}, options: AsyncResumeOptions = {}): AsyncResumeTarget {
384
- const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
385
- const resultsDir = deps.resultsDir ?? RESULTS_DIR;
384
+ const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
385
+ const resultsDir = deps.resultsDir ?? DIRS.results;
386
386
  const requireSessionFile = options.requireSessionFile ?? true;
387
387
  const location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
388
388
  if (!location.asyncDir && !location.resultPath) {
@@ -394,7 +394,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
394
394
  : undefined;
395
395
  const status = reconciliation?.status ?? null;
396
396
  validateStatusForResume(status, location.asyncDir ? path.join(location.asyncDir, "status.json") : "status.json");
397
- const recoveryDescriptor = readAsyncRecoveryDescriptor(location.asyncDir);
397
+ const recoveryDescriptor = readAsyncRecoveryDescriptor(location.asyncDir ?? undefined);
398
398
  const result = location.resultPath ? readResultFile(location.resultPath) : undefined;
399
399
  const runId = status?.runId ?? result?.runId ?? result?.id ?? location.resolvedId ?? (location.asyncDir ? path.basename(location.asyncDir) : "unknown");
400
400
  if (options.sessionId && ((status && status.sessionId !== options.sessionId) || (result && result.sessionId !== options.sessionId))) {
@@ -508,13 +508,13 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
508
508
  extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
509
509
  subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
510
510
  mcpDirectTools: descriptor.mcpDirectTools ? [...descriptor.mcpDirectTools] : undefined,
511
- systemPrompt: descriptor.systemPrompt,
511
+ systemPrompt: descriptor.systemPrompt ?? agentConfig.systemPrompt,
512
512
  systemPromptMode: descriptor.systemPromptMode,
513
513
  inheritProjectContext: descriptor.inheritProjectContext,
514
514
  inheritSkills: descriptor.inheritSkills,
515
515
  skills: descriptor.skills ? [...descriptor.skills] : undefined,
516
516
  skillPath: descriptor.skillPath ? [...descriptor.skillPath] : undefined,
517
- filePath: descriptor.agentFilePath,
517
+ filePath: descriptor.agentFilePath as string,
518
518
  completionGuard: descriptor.completionGuard,
519
519
  memory: descriptor.memory ? { ...descriptor.memory } : undefined,
520
520
  output: descriptor.outputPath,
@@ -2,7 +2,7 @@ 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
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type ChainCheckpointState } from "../../shared/types.ts";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type ChainCheckpointState } from "../../shared/types.ts";
6
6
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
7
7
  import { readStatus } from "../../shared/utils.ts";
8
8
  import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
@@ -108,12 +108,17 @@ export interface AsyncRunSummary {
108
108
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
109
109
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
110
110
  capabilityAudit?: SubagentCapabilityAudit;
111
+ parentWorkflowRunId?: string;
112
+ workflowKey?: string;
113
+ workflow?: Details["workflow"];
111
114
  }
112
115
 
113
116
  interface AsyncRunListOptions {
114
117
  states?: Array<AsyncRunSummary["state"]>;
115
118
  sessionId?: string;
116
119
  limit?: number;
120
+ /** Limits status-file reads after candidates are ordered by status mtime. */
121
+ entryLimit?: number;
117
122
  resultsDir?: string;
118
123
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
119
124
  now?: () => number;
@@ -321,6 +326,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
321
326
  ...(status.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: status.runtimeAcknowledgedExtensions } : {}),
322
327
  ...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
323
328
  ...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
329
+ ...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
330
+ ...(status.workflowKey ? { workflowKey: status.workflowKey } : {}),
331
+ ...(status.workflow ? { workflow: status.workflow } : {}),
324
332
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
325
333
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
326
334
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
@@ -343,6 +351,7 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
343
351
  case "stopped": return 2;
344
352
  case "paused": return 2;
345
353
  case "complete": return 3;
354
+ default: return 4;
346
355
  }
347
356
  };
348
357
  return [...runs].sort((a, b) => {
@@ -377,6 +386,25 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
377
386
  });
378
387
  }
379
388
 
389
+ if (options.entryLimit !== undefined) {
390
+ const limit = Math.max(0, Math.floor(options.entryLimit));
391
+ entries = entries
392
+ .map((entry) => {
393
+ try {
394
+ return { entry, mtimeMs: fs.statSync(path.join(asyncDirRoot, entry, "status.json")).mtimeMs };
395
+ } catch (error) {
396
+ if (isNotFoundError(error)) return undefined;
397
+ throw new Error(`Failed to inspect async status file for '${entry}': ${getErrorMessage(error)}`, {
398
+ cause: error instanceof Error ? error : undefined,
399
+ });
400
+ }
401
+ })
402
+ .filter((candidate): candidate is { entry: string; mtimeMs: number } => candidate !== undefined)
403
+ .sort((left, right) => right.mtimeMs - left.mtimeMs)
404
+ .slice(0, limit)
405
+ .map((candidate) => candidate.entry);
406
+ }
407
+
380
408
  const allowedStates = options.states ? new Set(options.states) : undefined;
381
409
  const runs: AsyncRunSummary[] = [];
382
410
  // Route resolution for every run shares a single index built from the