pi-subagents 0.40.0 → 0.41.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -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;
@@ -930,7 +948,7 @@ export function executeAsyncChain(
930
948
  const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
931
949
  const asyncDir = inheritedNestedRoute
932
950
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
933
- : path.join(ASYNC_DIR, id);
951
+ : path.join(DIRS.async, id);
934
952
  try {
935
953
  fs.mkdirSync(asyncDir, { recursive: true });
936
954
  } catch (error) {
@@ -981,7 +999,7 @@ export function executeAsyncChain(
981
999
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
982
1000
  let childTargetIndex = 0;
983
1001
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
984
- if (!("parallel" in step) && step.importAsyncRoot) {
1002
+ if (!("parallel" in step) && "importAsyncRoot" in step && step.importAsyncRoot) {
985
1003
  childTargetIndex++;
986
1004
  return [undefined];
987
1005
  }
@@ -992,7 +1010,7 @@ export function executeAsyncChain(
992
1010
  }
993
1011
  return step.parallel.map((task) => childIntercomTarget(task.agent, childTargetIndex++));
994
1012
  }
995
- return [childIntercomTarget(step.agent, childTargetIndex++)];
1013
+ return "agent" in step ? [childIntercomTarget(step.agent, childTargetIndex++)] : [undefined];
996
1014
  }) : undefined;
997
1015
 
998
1016
  let spawnResult: { pid?: number; error?: string } = {};
@@ -1001,7 +1019,7 @@ export function executeAsyncChain(
1001
1019
  {
1002
1020
  id,
1003
1021
  steps,
1004
- resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
1022
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1005
1023
  cwd: runnerCwd,
1006
1024
  placeholder: "{previous}",
1007
1025
  maxOutput,
@@ -1029,6 +1047,8 @@ export function executeAsyncChain(
1029
1047
  deadlineAt,
1030
1048
  globalConcurrencyLimit: params.globalConcurrencyLimit,
1031
1049
  workflowGraph,
1050
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1051
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1032
1052
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1033
1053
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
1034
1054
  parentRunId: nestedAddress.parentRunId,
@@ -1052,6 +1072,9 @@ export function executeAsyncChain(
1052
1072
 
1053
1073
  if (spawnResult.pid) {
1054
1074
  const eventFirstStep = eventChain[0];
1075
+ if (!eventFirstStep) {
1076
+ return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': event chain has no steps`);
1077
+ }
1055
1078
  const firstAgents = isParallelStep(eventFirstStep)
1056
1079
  ? eventFirstStep.parallel.map((t) => t.agent)
1057
1080
  : isDynamicParallelStep(eventFirstStep)
@@ -1140,6 +1163,8 @@ export function executeAsyncChain(
1140
1163
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1141
1164
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
1142
1165
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1166
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1167
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1143
1168
  nestedRoute,
1144
1169
  });
1145
1170
  }
@@ -1152,7 +1177,7 @@ export function executeAsyncChain(
1152
1177
 
1153
1178
  return {
1154
1179
  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 } : {}) },
1180
+ 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
1181
  };
1157
1182
  }
1158
1183
 
@@ -1184,6 +1209,20 @@ export function executeAsyncSingle(
1184
1209
  nestedRoute,
1185
1210
  } = params;
1186
1211
  const task = params.task ?? "";
1212
+ const externalRunner = agentConfig.runner?.type === "external-cli";
1213
+ const permissionRules = resolvePermissionRules(ctx.permissions, agentConfig.permissions);
1214
+ if (externalRunner) {
1215
+ const unsupported: string[] = [];
1216
+ if (params.modelOverride !== undefined) unsupported.push("model override");
1217
+ if (params.thinkingOverride !== undefined) unsupported.push("thinking override");
1218
+ if (params.structuredOutputSchema !== undefined) unsupported.push("structured output");
1219
+ if (params.acceptance !== undefined || params.agentContract !== undefined) unsupported.push("acceptance/agent contract");
1220
+ if (params.toolBudget !== undefined || agentConfig.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
1221
+ if (params.context === "fork") unsupported.push("fork context");
1222
+ if ((params.skills?.length ?? 0) > 0) unsupported.push("skills");
1223
+ if (permissionRules) unsupported.push("native Pi child permissions");
1224
+ if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='external-cli' and does not support: ${unsupported.join(", ")}.`);
1225
+ }
1187
1226
  const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
1188
1227
  try {
1189
1228
  assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
@@ -1215,7 +1254,7 @@ export function executeAsyncSingle(
1215
1254
  const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
1216
1255
  const asyncDir = inheritedNestedRoute
1217
1256
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
1218
- : path.join(ASYNC_DIR, id);
1257
+ : path.join(DIRS.async, id);
1219
1258
  try {
1220
1259
  fs.mkdirSync(asyncDir, { recursive: true });
1221
1260
  } catch (error) {
@@ -1234,14 +1273,14 @@ export function executeAsyncSingle(
1234
1273
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
1235
1274
  if (validationError) return formatAsyncStartError("single", validationError);
1236
1275
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
1237
- const primaryModel = resolveSubagentModelOverride(
1276
+ const primaryModel = externalRunner ? undefined : resolveSubagentModelOverride(
1238
1277
  params.modelOverride ?? agentConfig.model,
1239
1278
  ctx.currentModel,
1240
1279
  availableModels,
1241
1280
  ctx.currentModelProvider,
1242
1281
  );
1243
- const effectiveThinking = params.thinkingOverride ?? agentConfig.thinking;
1244
- const model = applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1282
+ const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1283
+ const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1245
1284
  const toolBudgetInput = params.toolBudget ?? agentConfig.toolBudget ?? params.configToolBudget;
1246
1285
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, params.toolBudget ? "toolBudget" : agentConfig.toolBudget ? "agent.toolBudget" : "config.toolBudget");
1247
1286
  if (resolvedToolBudget.error) return formatAsyncStartError("single", resolvedToolBudget.error);
@@ -1256,9 +1295,11 @@ export function executeAsyncSingle(
1256
1295
  const structuredOutput = params.structuredOutputSchema
1257
1296
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1258
1297
  : undefined;
1259
- const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
1260
- applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
1261
- );
1298
+ const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1299
+ .flatMap((candidate) => {
1300
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1301
+ return resolved ? [resolved] : [];
1302
+ });
1262
1303
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1263
1304
  const toolPlan = resolvePiLaunchToolPlan({
1264
1305
  tools: agentConfig.tools,
@@ -1272,7 +1313,7 @@ export function executeAsyncSingle(
1272
1313
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1273
1314
  agentName: agentConfig.name,
1274
1315
  });
1275
- const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1316
+ const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1276
1317
  const launchContractDigest = launchBindingDigest({
1277
1318
  definitionDigest: agentDefinitionDigest(agentConfig),
1278
1319
  task,
@@ -1331,7 +1372,7 @@ export function executeAsyncSingle(
1331
1372
  ...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
1332
1373
  ...(controlConfig ? { controlConfig } : {}),
1333
1374
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1334
- ...(params.turnBudget ? { initialTurnBudget: params.turnBudget } : {}),
1375
+ ...(initialTurnBudget ? { initialTurnBudget } : {}),
1335
1376
  ...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
1336
1377
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1337
1378
  ...(maxOutput ? { maxOutput } : {}),
@@ -1341,10 +1382,12 @@ export function executeAsyncSingle(
1341
1382
  artifactConfig,
1342
1383
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1343
1384
  };
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)}`);
1385
+ if (!externalRunner) {
1386
+ try {
1387
+ writePrivateAtomicJson(path.join(asyncDir, "recovery-descriptor.json"), recoveryDescriptor);
1388
+ } catch (error) {
1389
+ return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
1390
+ }
1348
1391
  }
1349
1392
  let spawnResult: { pid?: number; error?: string } = {};
1350
1393
  try {
@@ -1354,9 +1397,11 @@ export function executeAsyncSingle(
1354
1397
  steps: [
1355
1398
  {
1356
1399
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
1400
+ permissionRules,
1357
1401
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1358
1402
  agent,
1359
1403
  task: taskWithOutputInstruction,
1404
+ ...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
1360
1405
  ...(params.context ? { context: params.context } : {}),
1361
1406
  cwd: runnerCwd,
1362
1407
  model,
@@ -1374,7 +1419,7 @@ export function executeAsyncSingle(
1374
1419
  skills: resolvedSkills.map((r) => r.name),
1375
1420
  outputPath,
1376
1421
  outputMode,
1377
- sessionFile,
1422
+ ...(!externalRunner && sessionFile ? { sessionFile } : {}),
1378
1423
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1379
1424
  waitToolEnabled: params.waitToolEnabled,
1380
1425
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
@@ -1388,7 +1433,7 @@ export function executeAsyncSingle(
1388
1433
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1389
1434
  },
1390
1435
  ],
1391
- resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
1436
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1392
1437
  cwd: runnerCwd,
1393
1438
  placeholder: "{previous}",
1394
1439
  maxOutput,
@@ -1415,6 +1460,8 @@ export function executeAsyncSingle(
1415
1460
  resultMode: "single",
1416
1461
  launchContractDigest,
1417
1462
  launchResolvedExtensions,
1463
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1464
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1418
1465
  ...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
1419
1466
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1420
1467
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
@@ -1487,6 +1534,8 @@ export function executeAsyncSingle(
1487
1534
  asyncDir,
1488
1535
  launchContractDigest,
1489
1536
  launchResolvedExtensions,
1537
+ ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1538
+ ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1490
1539
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}),
1491
1540
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1492
1541
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
@@ -1497,6 +1546,6 @@ export function executeAsyncSingle(
1497
1546
 
1498
1547
  return {
1499
1548
  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 } : {}) },
1549
+ 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
1550
  };
1502
1551
  }
@@ -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,
@@ -54,7 +54,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
54
54
  } {
55
55
  const completionRetentionMs = options.completionRetentionMs ?? 10000;
56
56
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
57
- const resultsDir = options.resultsDir ?? RESULTS_DIR;
57
+ const resultsDir = options.resultsDir ?? DIRS.results;
58
58
  const steeringNoticeSeen = new Map<string, number>();
59
59
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
60
60
  renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
@@ -131,6 +131,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
131
131
  sessionFile: run.sessionFile,
132
132
  controlEventCursor: restoredControlEventCursor(run.asyncDir),
133
133
  nestedChildren: run.nestedChildren,
134
+ parentWorkflowRunId: run.parentWorkflowRunId,
135
+ workflowKey: run.workflowKey,
136
+ workflow: run.workflow,
134
137
  };
135
138
  };
136
139
  const cancelCleanup = (asyncId: string) => {
@@ -332,6 +335,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
332
335
  job.toolCount = status.toolCount ?? job.toolCount;
333
336
  job.steering = status.steering ?? job.steering;
334
337
  job.mode = status.mode;
338
+ job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
339
+ job.workflowKey = status.workflowKey ?? job.workflowKey;
340
+ job.workflow = status.workflow ?? job.workflow;
335
341
  job.currentStep = status.currentStep ?? job.currentStep;
336
342
  job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
337
343
  job.startedAt = status.startedAt ?? job.startedAt;
@@ -432,6 +438,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
432
438
  timeoutMs: info.timeoutMs,
433
439
  deadlineAt: info.deadlineAt,
434
440
  turnBudget: info.turnBudget,
441
+ parentWorkflowRunId: info.parentWorkflowRunId,
442
+ workflowKey: info.workflowKey,
435
443
  controlEventCursor: 0,
436
444
  });
437
445
  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
@@ -1,6 +1,6 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import { snapshotBackgroundWork } from "../../api/background-work.ts";
3
- import { ASYNC_DIR, RESULTS_DIR, type Details, type SubagentState } from "../../shared/types.ts";
3
+ import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
4
4
  import { listAsyncRuns } from "./async-status.ts";
5
5
  import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
6
6
 
@@ -24,10 +24,10 @@ function resultText(value: AgentToolResult<Details>): string {
24
24
  }
25
25
 
26
26
  function hasOutstandingWork(sessionId: string, nowMs: number): boolean {
27
- const asyncRuns = listAsyncRuns(ASYNC_DIR, {
27
+ const asyncRuns = listAsyncRuns(DIRS.async, {
28
28
  states: ["queued", "running"],
29
29
  sessionId,
30
- resultsDir: RESULTS_DIR,
30
+ resultsDir: DIRS.results,
31
31
  now: () => nowMs,
32
32
  });
33
33
  return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0;
@@ -104,9 +104,10 @@ export function enqueueChainAppendRequest(input: {
104
104
  function readAppendRequest(filePath: string): ChainAppendRequest | undefined {
105
105
  const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Partial<ChainAppendRequest>;
106
106
  if (!raw.id || typeof raw.id !== "string") return undefined;
107
- if (!Number.isFinite(raw.createdAt)) return undefined;
107
+ const createdAt = raw.createdAt;
108
+ if (typeof createdAt !== "number" || !Number.isFinite(createdAt)) return undefined;
108
109
  if (!Array.isArray(raw.steps) || raw.steps.length === 0) return undefined;
109
- return { id: raw.id, createdAt: raw.createdAt, steps: raw.steps as RunnerStep[] };
110
+ return { id: raw.id, createdAt, steps: raw.steps as RunnerStep[] };
110
111
  }
111
112
 
112
113
  export function readPendingChainAppendRequests(asyncDir: string): ChainAppendRequest[] {
@@ -331,19 +331,21 @@ function parseSteerCapability(raw: unknown): SteerCapability | undefined {
331
331
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
332
332
  const input = raw as Partial<SteerCapability>;
333
333
  if (input.type !== "steer-capability" || input.protocolVersion !== 1) return undefined;
334
- if (!Number.isInteger(input.index) || input.index < 0 || input.index > 1_000_000) return undefined;
335
- if (!Number.isInteger(input.pid) || input.pid <= 0 || !Number.isFinite(input.readyAt) || input.readyAt <= 0 || typeof input.supported !== "boolean") return undefined;
336
- return { type: "steer-capability", protocolVersion: 1, index: input.index, pid: input.pid, readyAt: input.readyAt, supported: input.supported };
334
+ const { index, pid, readyAt, supported } = input;
335
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index > 1_000_000) return undefined;
336
+ if (typeof pid !== "number" || typeof readyAt !== "number" || !Number.isInteger(pid) || pid <= 0 || !Number.isFinite(readyAt) || readyAt <= 0 || typeof supported !== "boolean") return undefined;
337
+ return { type: "steer-capability", protocolVersion: 1, index, pid, readyAt, supported };
337
338
  }
338
339
 
339
340
  function parseSteerAck(raw: unknown): SteerAck | undefined {
340
341
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
341
342
  const input = raw as Partial<SteerAck>;
342
343
  if (input.type !== "steer-ack" || input.protocolVersion !== 1 || typeof input.requestId !== "string" || !/^[^\s]+$/.test(input.requestId) || input.requestId.length > 256) return undefined;
343
- if (!Number.isInteger(input.index) || input.index < 0 || input.index > 1_000_000 || !Number.isFinite(input.ts) || input.ts <= 0) return undefined;
344
- if (input.state !== "delivered" && input.state !== "failed") return undefined;
345
- if (typeof input.message !== "string" || !input.message.trim() || input.message.length > 1000) return undefined;
346
- return { type: "steer-ack", protocolVersion: 1, requestId: input.requestId, index: input.index, ts: input.ts, state: input.state, message: input.message.trim() };
344
+ const { index, ts, state, message } = input;
345
+ if (typeof index !== "number" || typeof ts !== "number" || !Number.isInteger(index) || index < 0 || index > 1_000_000 || !Number.isFinite(ts) || ts <= 0) return undefined;
346
+ if (state !== "delivered" && state !== "failed") return undefined;
347
+ if (typeof message !== "string" || !message.trim() || message.length > 1000) return undefined;
348
+ return { type: "steer-ack", protocolVersion: 1, requestId: input.requestId, index, ts, state, message: message.trim() };
347
349
  }
348
350
 
349
351
  export function readSteerCapability(asyncDir: string, index: number): SteerCapability | undefined {