pi-subagents 0.61.0 → 0.63.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 (63) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/docs/agents.md +11 -6
  3. package/docs/configuration.md +31 -5
  4. package/docs/extension-api.md +2 -2
  5. package/docs/models.md +5 -5
  6. package/docs/observability.md +5 -2
  7. package/docs/tool-reference.md +3 -3
  8. package/install.mjs +0 -1
  9. package/package.json +1 -1
  10. package/skills/pi-subagents/references/execution-controls.md +3 -4
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  12. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  13. package/src/agents/agent-management.ts +41 -4
  14. package/src/agents/agent-serializer.ts +3 -0
  15. package/src/agents/agents.ts +120 -124
  16. package/src/agents/runtime-agent-registry.ts +5 -1
  17. package/src/api/preflight.ts +4 -0
  18. package/src/api/shared-types.ts +3 -0
  19. package/src/extension/config.ts +20 -0
  20. package/src/extension/public-execution.ts +1 -0
  21. package/src/extension/schemas.ts +6 -2
  22. package/src/extension/tool-description.ts +1 -1
  23. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  24. package/src/runs/background/active-async-capacity.ts +26 -8
  25. package/src/runs/background/async-execution.ts +100 -23
  26. package/src/runs/background/async-resume.ts +6 -2
  27. package/src/runs/background/async-status.ts +18 -2
  28. package/src/runs/background/notify.ts +13 -1
  29. package/src/runs/background/process-terminal.ts +16 -0
  30. package/src/runs/background/run-status.ts +22 -2
  31. package/src/runs/background/scheduled-runs.ts +63 -6
  32. package/src/runs/background/steering.ts +4 -1
  33. package/src/runs/background/subagent-runner.ts +44 -9
  34. package/src/runs/background/wait-completions.ts +13 -0
  35. package/src/runs/background/wait-tool.ts +1 -7
  36. package/src/runs/foreground/execution.ts +14 -7
  37. package/src/runs/foreground/subagent-executor.ts +38 -5
  38. package/src/runs/shared/acceptance.ts +85 -18
  39. package/src/runs/shared/capability-ceiling.ts +1 -0
  40. package/src/runs/shared/dynamic-fanout.ts +1 -1
  41. package/src/runs/shared/lane-metadata.ts +24 -3
  42. package/src/runs/shared/parallel-handoff.ts +4 -0
  43. package/src/runs/shared/parallel-utils.ts +2 -6
  44. package/src/runs/shared/permissions.ts +1 -1
  45. package/src/runs/shared/pi-args.ts +32 -14
  46. package/src/runs/shared/pi-spawn.ts +69 -35
  47. package/src/runs/shared/structured-output.ts +33 -6
  48. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  49. package/src/runs/shared/task-intent.ts +21 -7
  50. package/src/runs/shared/tool-timeout.ts +1 -1
  51. package/src/runs/shared/worktree.ts +467 -63
  52. package/src/shared/atomic-json.ts +3 -1
  53. package/src/shared/fork-context.ts +0 -12
  54. package/src/shared/fork-session-cwd.ts +27 -0
  55. package/src/shared/launch-contract.ts +3 -0
  56. package/src/shared/types.ts +32 -1
  57. package/src/shared/utils.ts +18 -7
  58. package/src/slash/slash-commands.ts +1 -1
  59. package/src/slash/subagents-admin.ts +26 -12
  60. package/src/tui/fleet-status.ts +61 -2
  61. package/src/tui/fleet.ts +12 -7
  62. package/src/tui/render.ts +222 -14
  63. package/src/workflows/workflow-checklist.ts +441 -0
@@ -41,7 +41,8 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
41
41
  lines.push("");
42
42
  }
43
43
  lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
44
- const controls = [input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
44
+ const acceptsPlainGuidance = input.index !== undefined || status.mode === "single";
45
+ const controls = [input.allowSteer === false || !acceptsPlainGuidance ? undefined : "type guidance", input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
45
46
  lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
46
47
  return lines.join("\n");
47
48
  }
@@ -81,6 +82,20 @@ function isTerminal(status: AsyncStatus): boolean {
81
82
  return status.state !== "queued" && status.state !== "running";
82
83
  }
83
84
 
85
+ function queueInspectorSteer(options: RunnerOptions, status: AsyncStatus, message: string): string {
86
+ if (options.allowSteer === false) throw new Error("Authority policy does not allow steer from this inspector.");
87
+ if (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be steered.`);
88
+ const runningIndexes = (status.steps ?? []).map((step, index) => step.status === "running" ? index : undefined).filter((index): index is number => index !== undefined);
89
+ const targetIndex = options.index ?? (status.mode === "single" ? 0 : undefined);
90
+ if (targetIndex === undefined && runningIndexes.length === 0) throw new Error("No running child is available to steer. Open a child-specific inspector for a pending child.");
91
+ requestAsyncSteer(options.asyncDir, {
92
+ message,
93
+ ...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
94
+ source: "herdr-inspector",
95
+ });
96
+ return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
97
+ }
98
+
84
99
  export function submitInspectorControl(options: RunnerOptions, line: string): string {
85
100
  const command = line.trim();
86
101
  if (!command || command === "status") return "Status refreshed.";
@@ -93,22 +108,13 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
93
108
  return `Stop requested for run ${options.runId}.`;
94
109
  }
95
110
  if (command.startsWith("steer ")) {
96
- if (options.allowSteer === false) throw new Error("Authority policy does not allow steer from this inspector.");
97
111
  const message = command.slice("steer ".length).trim();
98
112
  if (!message) throw new Error("steer requires a message.");
99
- if (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be steered.`);
100
- const runningIndexes = (status.steps ?? []).map((step, index) => step.status === "running" ? index : undefined).filter((index): index is number => index !== undefined);
101
- const targetIndex = options.index ?? (status.mode === "single" ? 0 : undefined);
102
- if (targetIndex === undefined && runningIndexes.length === 0) throw new Error("No running child is available to steer. Open a child-specific inspector for a pending child.");
103
- requestAsyncSteer(options.asyncDir, {
104
- message,
105
- ...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
106
- source: "herdr-inspector",
107
- });
108
- return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
113
+ return queueInspectorSteer(options, status, message);
109
114
  }
110
115
  if (command.startsWith("reply ")) throw new Error("Supervisor replies are owned by the parent Pi session; use subagent_supervisor/intercom there.");
111
- throw new Error("Unknown control. Use steer <message>, stop, or status.");
116
+ if (options.index === undefined && status.mode !== "single") throw new Error("Plain guidance requires a child-specific inspector. Use steer <message> to target all running children from the aggregate inspector.");
117
+ return queueInspectorSteer(options, status, command);
112
118
  }
113
119
 
114
120
  export function runInspector(argv = process.argv.slice(2)): void {
@@ -33,6 +33,7 @@ export interface ActiveAsyncCapacityHandle {
33
33
  markStarted(runnerProcessInstanceId: string): void;
34
34
  markWorkflowStarted(): void;
35
35
  rollback(): boolean;
36
+ rollbackBeforeRunnerProceed(runnerProcessInstanceId: string): boolean;
36
37
  reconcile(liveWorkflowRunIds?: ReadonlySet<string>): ActiveAsyncCapacitySnapshot;
37
38
  }
38
39
 
@@ -43,6 +44,7 @@ interface CapacityOptions {
43
44
  abandonedSlotReleaseAfterMs?: number | false;
44
45
  pidLiveness?: (pid: number) => PidLiveness;
45
46
  afterSlotRename?: (releasedDir: string) => void;
47
+ writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwnerV1) => void;
46
48
  }
47
49
 
48
50
  export interface ActiveAsyncCapacityReleaseEvidence {
@@ -269,7 +271,6 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
269
271
  if (liveWorkflowRunIds.has(owner.runId)) return { state: "retained", reason: "workflow controller is still live" };
270
272
  for (const step of status.steps ?? []) {
271
273
  const label = step.workflowKey ?? step.agent;
272
- if (step.status === "pending" || step.status === "running" || step.status === "paused") return { state: "retained", reason: `workflow child ${label} is still ${step.status}` };
273
274
  if (typeof step.async !== "boolean") return { state: "retained", reason: `workflow child ${label} is missing async classification` };
274
275
  if (!step.async) continue;
275
276
  if (!step.runId) return { state: "retained", reason: `async workflow child ${label} is missing run id` };
@@ -380,6 +381,7 @@ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean
380
381
 
381
382
  function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwnerV1): ActiveAsyncCapacityHandle {
382
383
  const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
384
+ const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
383
385
  const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
384
386
  return {
385
387
  owner,
@@ -388,14 +390,10 @@ function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: Ca
388
390
  const current = matchingOwner(dir, owner);
389
391
  if (!current) return false;
390
392
  const next = { ...current, runnerProcessInstanceId, runnerStartedAt: options.now?.() ?? Date.now() };
391
- // Mark memory first. If persistence fails after the process starts, caller
392
- // cleanup must retain the occupied slot instead of rolling it back.
393
+ // Mark memory first so pre-proceed cleanup can distinguish a failed durable
394
+ // bind from an unrelated unstarted reservation.
393
395
  Object.assign(owner, next);
394
- try {
395
- writePrivateAtomicJson(path.join(dir, "owner.json"), next);
396
- } catch (error) {
397
- console.error(`Failed to bind active async capacity to runner '${runnerProcessInstanceId}'; capacity will remain occupied:`, error);
398
- }
396
+ writeOwner(path.join(dir, "owner.json"), next);
399
397
  return true;
400
398
  });
401
399
  if (!claimed.acquired || !claimed.value) throw new Error(`Active async capacity ownership changed for run '${owner.runId}'.`);
@@ -427,6 +425,26 @@ function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: Ca
427
425
  });
428
426
  return claimed.acquired && claimed.value;
429
427
  },
428
+ rollbackBeforeRunnerProceed(runnerProcessInstanceId) {
429
+ const claimed = withSlotClaim(dir, () => {
430
+ const current = matchingOwner(dir, owner);
431
+ if (!current) return false;
432
+ const boundToRunner = current.runnerProcessInstanceId === runnerProcessInstanceId;
433
+ const bindingFailedBeforeProceed = current.runnerProcessInstanceId === undefined && current.runnerStartedAt === undefined && owner.runnerProcessInstanceId === runnerProcessInstanceId;
434
+ if (!boundToRunner && !bindingFailedBeforeProceed) return false;
435
+ if (rollbackOwner) {
436
+ writePrivateAtomicJson(path.join(dir, "owner.json"), rollbackOwner);
437
+ Object.assign(owner, rollbackOwner);
438
+ return true;
439
+ }
440
+ const releasedDir = path.join(path.dirname(dir), `.${path.basename(dir)}.released-${randomUUID()}`);
441
+ fs.renameSync(dir, releasedDir);
442
+ options.afterSlotRename?.(releasedDir);
443
+ fs.rmSync(releasedDir, { recursive: true, force: true });
444
+ return true;
445
+ });
446
+ return claimed.acquired && claimed.value;
447
+ },
430
448
  reconcile(liveWorkflowRunIds) {
431
449
  return reconcileActiveAsyncCapacity(owner.ownerSessionId, limit, { ...options, rootDir, liveWorkflowRunIds });
432
450
  },
@@ -11,15 +11,15 @@ import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
13
13
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
14
- import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
14
+ import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
15
15
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
16
  import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
18
18
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
- import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
19
+ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
20
20
  import type { RunnerStep } from "../shared/parallel-utils.ts";
21
21
  import type { ContextMode } from "../shared/context-mode.ts";
22
- import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
22
+ import { resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
23
23
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
24
24
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
25
25
  import { backgroundProcessOptions } from "../shared/background-process-options.ts";
@@ -31,11 +31,11 @@ import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout
31
31
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
32
32
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
33
33
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
34
- import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
34
+ import { resolveExpectedWorktreeAgentCwd, resolveWorktreeProvider, shouldDeferWorktreeCwd, WORKTREE_AGENT_CWD_PLACEHOLDER } from "../shared/worktree.ts";
35
35
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
36
36
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
37
37
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
38
- import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
38
+ import { resolveAcceptanceReportMode, resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
39
39
  import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
40
40
  import { validateImplementationToolContract } from "../shared/completion-guard.ts";
41
41
  import {
@@ -70,7 +70,7 @@ import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
70
70
  import { usageBudgetState } from "../shared/usage-budget.ts";
71
71
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
72
72
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
73
- import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
73
+ import { finalizeProcessTerminal, initializeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
74
74
  import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
75
75
  import { statusStepDescription } from "./chain-append.ts";
76
76
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
@@ -81,7 +81,7 @@ import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBin
81
81
  import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
82
82
 
83
83
  const require = createRequire(import.meta.url);
84
- const piPackageRoot = resolvePiPackageRoot();
84
+ const piPackageRoot = resolvePiPackageRoot() ?? resolveInstalledPiPackageRoot();
85
85
 
86
86
  function resolveJitiCliFromPackageJson(packageJsonPath: string): string | undefined {
87
87
  if (!fs.existsSync(packageJsonPath)) return undefined;
@@ -178,6 +178,8 @@ interface AsyncChainParams {
178
178
  worktreeSetupHook?: string;
179
179
  worktreeSetupHookTimeoutMs?: number;
180
180
  worktreeBaseDir?: string;
181
+ worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
182
+ worktreeBranchPrefix?: string;
181
183
  controlConfig?: ResolvedControlConfig;
182
184
  controlIntercomTarget?: string;
183
185
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
@@ -245,6 +247,8 @@ interface AsyncSingleParams {
245
247
  worktreeSetupHook?: string;
246
248
  worktreeSetupHookTimeoutMs?: number;
247
249
  worktreeBaseDir?: string;
250
+ worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
251
+ worktreeBranchPrefix?: string;
248
252
  worktree?: boolean;
249
253
  controlConfig?: ResolvedControlConfig;
250
254
  intercomBridge?: IntercomBridgeConfig;
@@ -310,6 +314,8 @@ export interface AsyncRunnerStepBuildParams {
310
314
  waitToolEnabled?: boolean;
311
315
  waitToolDefaultTimeoutMs?: number;
312
316
  worktreeBaseDir?: string;
317
+ worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
318
+ worktreeBranchPrefix?: string;
313
319
  asyncDir: string;
314
320
  outputBaseDir?: string;
315
321
  validateOutputBindings?: boolean;
@@ -432,13 +438,15 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
432
438
  return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'.`, startupDidNotProceed: true };
433
439
  }
434
440
 
441
+ const writePrivateStartupControlJson = createAtomicJsonWriter({ mode: 0o600, ignoreCleanupErrorAfterSuccess: true });
442
+
435
443
  function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
436
444
  // Delegate to the shared atomic JSON writer (temp file + rename, retrying
437
445
  // transient Windows EPERM/EBUSY/EACCES locks and cleaning up the temp file
438
446
  // on failure), so the startup handshake gets the same locking resilience as
439
447
  // every other async control/result file. This is exercised by
440
448
  // test/unit/atomic-json.test.ts.
441
- writePrivateAtomicJson(filePath, payload);
449
+ writePrivateStartupControlJson(filePath, payload);
442
450
  }
443
451
 
444
452
  function runnerIsAlive(pid: number): boolean {
@@ -520,7 +528,7 @@ export function emitProcessTerminalEvent(ctx: AsyncExecutionContext, proof: unkn
520
528
  }
521
529
  }
522
530
 
523
- function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, requestedCwd = cwd): SpawnRunnerResult {
531
+ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, onBeforeProceed?: (runnerProcessInstanceId: string) => void, requestedCwd = cwd): SpawnRunnerResult {
524
532
  const cwdError = preflightLaunchCwd(requestedCwd, cwd);
525
533
  if (cwdError) return { error: cwdError };
526
534
 
@@ -637,6 +645,24 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
637
645
  });
638
646
  } catch (error) {
639
647
  const message = `Failed to persist initial async status: ${error instanceof Error ? error.message : String(error)}`;
648
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
649
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
650
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
651
+ }
652
+ try {
653
+ if (!launchAsyncDir) throw new Error("Async runner is missing its lifecycle directory.");
654
+ initializeProcessTerminal(launchAsyncDir, launchRunId, runnerProcessInstanceId);
655
+ } catch (error) {
656
+ const message = `Failed to establish async runner lifecycle sidecar: ${error instanceof Error ? error.message : String(error)}`;
657
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
658
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
659
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
660
+ }
661
+ try {
662
+ onBeforeProceed?.(runnerProcessInstanceId);
663
+ } catch (error) {
664
+ const message = `Failed to establish async runner capacity ownership: ${error instanceof Error ? error.message : String(error)}`;
665
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
640
666
  const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
641
667
  return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
642
668
  }
@@ -720,6 +746,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
720
746
  thinkingOverridesByFlatIndex,
721
747
  maxSubagentDepth,
722
748
  worktreeBaseDir,
749
+ worktreeProvider,
750
+ worktreeBranchPrefix,
723
751
  asyncDir,
724
752
  } = params;
725
753
  const outputBaseDir = params.outputBaseDir;
@@ -727,6 +755,15 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
727
755
  const chainSkills = params.chainSkills ?? [];
728
756
  const availableModels = params.availableModels;
729
757
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
758
+ let managedWorktreeProvider: "native" | "worktrunk" | undefined;
759
+ try {
760
+ if (chain.some((step) => "worktree" in step && step.worktree === true)) {
761
+ const resolved = resolveWorktreeProvider(worktreeProvider, worktreeBaseDir);
762
+ managedWorktreeProvider = shouldDeferWorktreeCwd(worktreeProvider, worktreeBaseDir) ? "worktrunk" : resolved;
763
+ }
764
+ } catch (error) {
765
+ return { error: error instanceof Error ? error.message : String(error) };
766
+ }
730
767
  const progressDir = params.progressDir ?? runnerCwd;
731
768
  const graphChain: ChainStep[] = params.attachRoot
732
769
  ? [{
@@ -853,7 +890,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
853
890
  if (validationError) throw new AsyncStartValidationError(validationError);
854
891
  let taskTemplate = s.task ?? "{previous}";
855
892
  taskTemplate = taskTemplate.replace(/\{task\}/g, originalTask ?? "");
856
- taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
893
+ taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, behaviorCwd ?? runnerCwd);
857
894
  const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
858
895
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
859
896
 
@@ -905,6 +942,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
905
942
  const fast = s.fast ?? params.fast ?? a.fast;
906
943
  const toolPlan = resolvePiLaunchToolPlan({
907
944
  tools: a.tools,
945
+ excludeTools: a.excludeTools,
908
946
  allowNestedSubagents: a.allowNestedSubagents,
909
947
  extensions: a.extensions,
910
948
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -967,6 +1005,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
967
1005
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
968
1006
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
969
1007
  tools: a.tools,
1008
+ excludeTools: a.excludeTools,
970
1009
  allowNestedSubagents: a.allowNestedSubagents,
971
1010
  extensions: a.extensions,
972
1011
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -1002,7 +1041,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1002
1041
  acceptanceRole: a.acceptanceRole,
1003
1042
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
1004
1043
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
1005
- ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: s.acceptance !== false }) } : {}),
1044
+ ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(s.acceptance) }) } : {}),
1006
1045
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1007
1046
  ...(s.worktree ? { worktree: true } : {}),
1008
1047
  };
@@ -1036,7 +1075,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1036
1075
  return {
1037
1076
  parallel: s.parallel.map((t, taskIndex) => {
1038
1077
  let behaviorCwd: string | undefined;
1039
- if (s.worktree) {
1078
+ if (s.worktree && managedWorktreeProvider === "worktrunk") {
1079
+ behaviorCwd = WORKTREE_AGENT_CWD_PLACEHOLDER;
1080
+ } else if (s.worktree && managedWorktreeProvider === "native") {
1040
1081
  try {
1041
1082
  behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex, worktreeBaseDir);
1042
1083
  } catch {
@@ -1091,7 +1132,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1091
1132
  }
1092
1133
  const sequential = s as SequentialStep;
1093
1134
  let behaviorCwd: string | undefined;
1094
- if (sequential.worktree) {
1135
+ if (sequential.worktree && managedWorktreeProvider === "worktrunk") {
1136
+ behaviorCwd = WORKTREE_AGENT_CWD_PLACEHOLDER;
1137
+ } else if (sequential.worktree && managedWorktreeProvider === "native") {
1095
1138
  try {
1096
1139
  behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, 0, worktreeBaseDir);
1097
1140
  } catch {
@@ -1161,6 +1204,8 @@ export function executeAsyncChain(
1161
1204
  worktreeSetupHook,
1162
1205
  worktreeSetupHookTimeoutMs,
1163
1206
  worktreeBaseDir,
1207
+ worktreeProvider,
1208
+ worktreeBranchPrefix,
1164
1209
  controlConfig,
1165
1210
  controlIntercomTarget,
1166
1211
  childIntercomTarget,
@@ -1171,7 +1216,7 @@ export function executeAsyncChain(
1171
1216
  chain: chain.map((step) => {
1172
1217
  if (isParallelStep(step)) return { parallel: step.parallel };
1173
1218
  if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
1174
- return { acceptance: step.acceptance };
1219
+ return { acceptance: step.acceptance, outputSchema: step.outputSchema };
1175
1220
  }),
1176
1221
  });
1177
1222
  if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
@@ -1217,6 +1262,8 @@ export function executeAsyncChain(
1217
1262
  waitToolEnabled: params.waitToolEnabled,
1218
1263
  waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
1219
1264
  worktreeBaseDir,
1265
+ worktreeProvider,
1266
+ worktreeBranchPrefix,
1220
1267
  asyncDir,
1221
1268
  fast: params.fast,
1222
1269
  toolBudget: params.toolBudget,
@@ -1292,6 +1339,8 @@ export function executeAsyncChain(
1292
1339
  worktreeSetupHook,
1293
1340
  worktreeSetupHookTimeoutMs,
1294
1341
  worktreeBaseDir,
1342
+ worktreeProvider,
1343
+ worktreeBranchPrefix,
1295
1344
  controlConfig,
1296
1345
  toolBudget: params.toolBudget,
1297
1346
  usageBudget: params.usageBudget,
@@ -1332,6 +1381,7 @@ export function executeAsyncChain(
1332
1381
  },
1333
1382
  path.join(asyncDir, "status.json"),
1334
1383
  (proof) => emitProcessTerminalEvent(ctx, proof),
1384
+ (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1335
1385
  );
1336
1386
  } catch (error) {
1337
1387
  params.activeAsyncCapacity?.rollback();
@@ -1340,7 +1390,10 @@ export function executeAsyncChain(
1340
1390
  }
1341
1391
 
1342
1392
  if (spawnResult.error) {
1343
- if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
1393
+ if (spawnResult.startupDidNotProceed) {
1394
+ if (!spawnResult.runnerProcessInstanceId || params.activeAsyncCapacity?.rollbackBeforeRunnerProceed(spawnResult.runnerProcessInstanceId) !== true) params.activeAsyncCapacity?.rollback();
1395
+ }
1396
+ else if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) params.activeAsyncCapacity?.rollback();
1344
1397
  else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1345
1398
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
1346
1399
  }
@@ -1348,8 +1401,6 @@ export function executeAsyncChain(
1348
1401
  params.activeAsyncCapacity?.rollback();
1349
1402
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': runner identity unavailable`);
1350
1403
  }
1351
- params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1352
-
1353
1404
  if (spawnResult.pid) {
1354
1405
  const eventFirstStep = eventChain[0];
1355
1406
  if (!eventFirstStep) {
@@ -1487,6 +1538,8 @@ export function executeAsyncSingle(
1487
1538
  worktreeSetupHook,
1488
1539
  worktreeSetupHookTimeoutMs,
1489
1540
  worktreeBaseDir,
1541
+ worktreeProvider,
1542
+ worktreeBranchPrefix,
1490
1543
  controlConfig,
1491
1544
  controlIntercomTarget,
1492
1545
  childIntercomTarget,
@@ -1532,7 +1585,18 @@ export function executeAsyncSingle(
1532
1585
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1533
1586
  }
1534
1587
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1535
- const instructionCwd = params.worktree === true
1588
+ let managedWorktreeProvider: "native" | "worktrunk" | undefined;
1589
+ if (params.worktree === true) {
1590
+ try {
1591
+ const resolved = resolveWorktreeProvider(params.worktreeProvider, worktreeBaseDir);
1592
+ managedWorktreeProvider = shouldDeferWorktreeCwd(params.worktreeProvider, worktreeBaseDir) ? "worktrunk" : resolved;
1593
+ } catch (error) {
1594
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1595
+ }
1596
+ }
1597
+ const instructionCwd = params.worktree === true && managedWorktreeProvider === "worktrunk"
1598
+ ? WORKTREE_AGENT_CWD_PLACEHOLDER
1599
+ : params.worktree === true && managedWorktreeProvider === "native"
1536
1600
  ? resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s0`, 0, worktreeBaseDir)
1537
1601
  : runnerCwd;
1538
1602
  const readExistenceCwd = params.worktree === true ? runnerCwd : instructionCwd;
@@ -1586,7 +1650,11 @@ export function executeAsyncSingle(
1586
1650
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
1587
1651
  // absolute paths pass through; relative paths resolve against the child cwd.
1588
1652
  const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
1589
- const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, readExistenceCwd) : [];
1653
+ const readPaths = Array.isArray(reads)
1654
+ ? managedWorktreeProvider === "worktrunk"
1655
+ ? resolveExistingReadInstructionPaths(reads, instructionCwd, readExistenceCwd)
1656
+ : resolveExistingReadPaths(reads, readExistenceCwd)
1657
+ : [];
1590
1658
  const readsInstruction = readPaths.length > 0
1591
1659
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
1592
1660
  : "";
@@ -1647,7 +1715,7 @@ export function executeAsyncSingle(
1647
1715
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1648
1716
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1649
1717
  const structuredOutput = params.structuredOutputSchema
1650
- ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: params.acceptance !== false })
1718
+ ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
1651
1719
  : undefined;
1652
1720
  let modelCandidates: string[] = [];
1653
1721
  if (!externalRunner) {
@@ -1667,6 +1735,7 @@ export function executeAsyncSingle(
1667
1735
  }
1668
1736
  const toolPlan = resolvePiLaunchToolPlan({
1669
1737
  tools: agentConfig.tools,
1738
+ excludeTools: agentConfig.excludeTools,
1670
1739
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1671
1740
  extensions: agentConfig.extensions,
1672
1741
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1713,6 +1782,7 @@ export function executeAsyncSingle(
1713
1782
  inheritSkills: agentConfig.inheritSkills,
1714
1783
  skills: resolvedSkills.map((skill) => skill.name),
1715
1784
  tools: toolPlan.effectiveToolAllowlist,
1785
+ ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1716
1786
  extensions: toolPlan.extensionArgs,
1717
1787
  mcpDirectTools: toolPlan.effectiveMcpTools,
1718
1788
  ...(outputPath ? { outputPath } : {}),
@@ -1751,6 +1821,7 @@ export function executeAsyncSingle(
1751
1821
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1752
1822
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1753
1823
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1824
+ ...(recoveryAgentConfig.excludeTools ? { excludeTools: [...recoveryAgentConfig.excludeTools] } : {}),
1754
1825
  ...(recoveryAgentConfig.allowNestedSubagents !== undefined ? { allowNestedSubagents: recoveryAgentConfig.allowNestedSubagents } : {}),
1755
1826
  ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1756
1827
  ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
@@ -1818,6 +1889,7 @@ export function executeAsyncSingle(
1818
1889
  ...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
1819
1890
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1820
1891
  tools: agentConfig.tools,
1892
+ excludeTools: agentConfig.excludeTools,
1821
1893
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1822
1894
  extensions: agentConfig.extensions,
1823
1895
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1846,6 +1918,7 @@ export function executeAsyncSingle(
1846
1918
  ...(extensionBindings ? { extensionBindings } : {}),
1847
1919
  launchResolvedExtensions,
1848
1920
  effectiveAcceptance: resolvedAcceptance,
1921
+ acceptanceInput: params.acceptance,
1849
1922
  ...(structuredOutput ? { structuredOutput } : {}),
1850
1923
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1851
1924
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
@@ -1872,6 +1945,8 @@ export function executeAsyncSingle(
1872
1945
  worktreeSetupHook,
1873
1946
  worktreeSetupHookTimeoutMs,
1874
1947
  worktreeBaseDir,
1948
+ worktreeProvider,
1949
+ worktreeBranchPrefix,
1875
1950
  controlConfig,
1876
1951
  timeoutMs,
1877
1952
  deadlineAt,
@@ -1914,6 +1989,7 @@ export function executeAsyncSingle(
1914
1989
  },
1915
1990
  path.join(asyncDir, "status.json"),
1916
1991
  (proof) => emitProcessTerminalEvent(ctx, proof),
1992
+ (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1917
1993
  params.requestedCwd ?? runnerCwd,
1918
1994
  );
1919
1995
  } catch (error) {
@@ -1923,7 +1999,10 @@ export function executeAsyncSingle(
1923
1999
  }
1924
2000
 
1925
2001
  if (spawnResult.error) {
1926
- if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
2002
+ if (spawnResult.startupDidNotProceed) {
2003
+ if (!spawnResult.runnerProcessInstanceId || params.activeAsyncCapacity?.rollbackBeforeRunnerProceed(spawnResult.runnerProcessInstanceId) !== true) params.activeAsyncCapacity?.rollback();
2004
+ }
2005
+ else if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) params.activeAsyncCapacity?.rollback();
1927
2006
  else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1928
2007
  return formatAsyncStartError("single", `Failed to start async run '${id}': ${spawnResult.error}`);
1929
2008
  }
@@ -1931,8 +2010,6 @@ export function executeAsyncSingle(
1931
2010
  params.activeAsyncCapacity?.rollback();
1932
2011
  return formatAsyncStartError("single", `Failed to start async run '${id}': runner identity unavailable`);
1933
2012
  }
1934
- params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1935
-
1936
2013
  if (spawnResult.pid) {
1937
2014
  if (inheritedNestedRoute && nestedAddress) {
1938
2015
  const now = Date.now();
@@ -45,6 +45,8 @@ export type AsyncResumeTarget = {
45
45
  sessionName?: string;
46
46
  index: number;
47
47
  cwd?: string;
48
+ /** True when cwd is the retained managed worktree recorded by the handoff. */
49
+ managedWorktree?: boolean;
48
50
  sessionFile?: string;
49
51
  model?: string;
50
52
  thinking?: string;
@@ -315,7 +317,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
315
317
  const parsed = value as Record<string, unknown>;
316
318
  const allowedFields = new Set([
317
319
  "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
318
- "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
320
+ "subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
319
321
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
320
322
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
321
323
  "launchResolvedExtensions", "runFanoutBudget", "lane",
@@ -356,7 +358,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
356
358
  else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
357
359
  if (parsed.allowNestedSubagents !== undefined && typeof parsed.allowNestedSubagents !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': allowNestedSubagents must be a boolean.`);
358
360
  if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
359
- for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
361
+ for (const field of ["fallbackModels", "tools", "excludeTools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
360
362
  const item = parsed[field];
361
363
  if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
362
364
  }
@@ -569,6 +571,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
569
571
  ...(statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName ? { sessionName: statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName } : {}),
570
572
  index,
571
573
  ...(resumeCwd ? { cwd: resumeCwd } : {}),
574
+ ...(managedWorktreeCwd ? { managedWorktree: true } : {}),
572
575
  ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
573
576
  ...(stepModel ? { model: stepModel } : {}),
574
577
  ...(stepThinking ? { thinking: stepThinking } : {}),
@@ -590,6 +593,7 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
590
593
  thinking: descriptor.thinking,
591
594
  maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
592
595
  tools: descriptor.tools ? [...descriptor.tools] : undefined,
596
+ excludeTools: descriptor.excludeTools ? [...descriptor.excludeTools] : undefined,
593
597
  allowNestedSubagents: descriptor.allowNestedSubagents,
594
598
  extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
595
599
  subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { previewDisplayText } from "../../shared/display-text.ts";
5
5
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
6
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1, type WorkflowGraphSnapshot } from "../../shared/types.ts";
6
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorktreeNaming, type WorkflowPreflightV1, type WorkflowGraphSnapshot } from "../../shared/types.ts";
7
7
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
8
8
  import { readStatus } from "../../shared/utils.ts";
9
9
  import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
@@ -24,6 +24,7 @@ import { validateAsyncStatusLaneMetadata } from "../shared/lane-metadata.ts";
24
24
  import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
25
25
  import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
26
26
  import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
27
+ import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
27
28
 
28
29
  interface AsyncRunStepSummary {
29
30
  index: number;
@@ -39,6 +40,8 @@ interface AsyncRunStepSummary {
39
40
  lane?: AsyncJobStep["lane"];
40
41
  worktreePath?: string;
41
42
  branch?: string;
43
+ provider?: "native" | "worktrunk";
44
+ naming?: WorktreeNaming;
42
45
  runId?: string;
43
46
  outputName?: string;
44
47
  structured?: boolean;
@@ -337,6 +340,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
337
340
  ...(step.lane ? { lane: step.lane } : {}),
338
341
  ...(step.worktreePath ? { worktreePath: step.worktreePath } : {}),
339
342
  ...(step.branch ? { branch: step.branch } : {}),
343
+ ...(step.provider ? { provider: step.provider } : {}),
344
+ ...(step.naming ? { naming: step.naming } : {}),
340
345
  ...(step.runId ? { runId: step.runId } : {}),
341
346
  ...(step.outputName ? { outputName: step.outputName } : {}),
342
347
  ...(step.structured ? { structured: step.structured } : {}),
@@ -614,7 +619,7 @@ function formatStepLine(step: AsyncRunStepSummary): string {
614
619
  if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
615
620
  if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
616
621
  if (step.lane) parts.push(`lane ${step.lane.key}`);
617
- if (step.worktreePath) parts.push(`worktree ${shortenPath(step.worktreePath)} · branch ${step.branch ?? "unknown"}`);
622
+ if (step.worktreePath) parts.push(`worktree ${shortenPath(step.worktreePath)} · branch ${step.branch ?? "unknown"}${step.provider ? ` · provider ${step.provider}` : ""}`);
618
623
  return parts.join(" | ");
619
624
  }
620
625
 
@@ -707,6 +712,17 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
707
712
  if (run.preflight) lines.push(formatWorkflowPreflightPlanSummary(run.preflight, { indent: " " }));
708
713
  const preflightWarning = formatWorkflowPreflightWarningSummary(run.workflow?.preflightWarnings, { indent: " " });
709
714
  if (preflightWarning) lines.push(preflightWarning);
715
+ if (run.mode === "workflow") {
716
+ const checklist = projectWorkflowChecklist({
717
+ graph: run.workflowGraph,
718
+ steps: run.steps,
719
+ hostSteps: run.hostSteps,
720
+ preflight: run.preflight,
721
+ trace: run.workflow?.trace,
722
+ now: run.lastUpdate ?? run.endedAt ?? Date.now(),
723
+ });
724
+ lines.push(...formatWorkflowChecklistText(checklist, " ", { includeItems: false }));
725
+ }
710
726
  for (const step of run.steps) {
711
727
  lines.push(` ${formatStepLine(step)}`);
712
728
  lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));