@selesai/code 0.13.2 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/defaults/models.json +85 -13
  3. package/dist/extensions/cost-reconcile.test.ts +200 -4
  4. package/dist/extensions/cost-reconcile.ts +88 -102
  5. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  6. package/dist/extensions/pi-intercom/README.md +4 -5
  7. package/dist/extensions/pi-intercom/config.test.ts +3 -31
  8. package/dist/extensions/pi-intercom/config.ts +0 -15
  9. package/dist/extensions/pi-intercom/index.ts +9 -46
  10. package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
  11. package/dist/extensions/pi-intercom/package.json +1 -1
  12. package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
  13. package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
  14. package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
  15. package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
  16. package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
  17. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  18. package/dist/extensions/pi-subagents/package.json +1 -1
  19. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
  20. package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
  21. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
  22. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  23. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
  24. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  25. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  27. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
  28. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
  29. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
  30. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  31. package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
  32. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
  37. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
  39. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
  40. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
  41. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
  42. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
  43. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
  44. package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
  45. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  46. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
  47. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
  48. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
  49. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
  50. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
  51. package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
  52. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
  53. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
  54. package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
  55. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
  56. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
  57. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
  58. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
  59. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
  60. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
  61. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
  62. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
  63. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
  64. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
  65. package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
  66. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
  68. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
  69. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
  70. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
  71. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
  72. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
  73. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
  74. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
  75. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
  76. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
  77. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
  78. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
  79. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
  80. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
  81. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
  82. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
  83. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
  84. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
  85. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
  86. package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
  87. package/dist/extensions/pi-web-agent/README.md +131 -0
  88. package/dist/extensions/pi-web-agent/package.json +4 -2
  89. package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
  90. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  91. package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
  92. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
  93. package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
  94. package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
  95. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
  96. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
  97. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
  98. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
  99. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
  100. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
  101. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
  102. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
  103. package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
  104. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
  105. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
  106. package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
  107. package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
  108. package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
  109. package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
  110. package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
  111. package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
  112. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
  113. package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
  114. package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
  115. package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
  116. package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
  117. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
  118. package/dist/extensions/pi-web-agent/src/types.ts +19 -4
  119. package/dist/extensions/tokenin-onboarding.ts +302 -0
  120. package/package.json +1 -1
@@ -4,6 +4,7 @@ import type { AsyncRunSummary } from "./async-status.ts";
4
4
  import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
5
5
  import { fallbackResultPayloadPathForSessionRun, resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
6
6
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
7
+ import { projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
7
8
 
8
9
  function asNonEmptyString(value: unknown): string | undefined {
9
10
  return typeof value === "string" && value ? value : undefined;
@@ -65,6 +66,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
65
66
  const error = asNonEmptyString(child.error);
66
67
  const model = asNonEmptyString(child.model);
67
68
  const contextOverflow = child.contextOverflow === true;
69
+ const timeoutRecovery = projectTimeoutRecovery(child.timeoutRecovery);
68
70
  return [{
69
71
  ...(agent ? { agent } : {}),
70
72
  ...(childRunId ? { runId: childRunId } : {}),
@@ -76,6 +78,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
76
78
  ...(model ? { model } : {}),
77
79
  ...(contextOverflow ? { contextOverflow: true } : {}),
78
80
  ...(artifactPaths ? { artifactPaths } : {}),
81
+ ...(timeoutRecovery ? { timeoutRecovery } : {}),
79
82
  }];
80
83
  })
81
84
  : undefined;
@@ -66,7 +66,7 @@ import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
66
66
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
67
67
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
68
68
  import { resolvePermissionRules } from "../shared/permissions.ts";
69
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
69
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
70
70
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
71
71
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
72
72
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
@@ -76,7 +76,7 @@ import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructu
76
76
  import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
77
77
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
78
78
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
79
- import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
79
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
80
80
  import {
81
81
  buildModelCandidates,
82
82
  formatSubagentModelVerificationError,
@@ -380,6 +380,7 @@ async function runSingleAttempt(
380
380
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
381
381
  runFanoutBudget: options.runFanoutBudget,
382
382
  parentSessionId: options.parentSessionId,
383
+ forkCacheKey: options.context === "fork" ? deriveForkPromptCacheKey(options.parentSessionId) : undefined,
383
384
  steerInboxDir: options.steerInboxDir,
384
385
  steerCapabilityPath: options.steerCapabilityPath,
385
386
  steerAckDir: options.steerAckDir,
@@ -1527,9 +1528,17 @@ async function runSingleAttempt(
1527
1528
  result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
1528
1529
  if (result.timedOut) {
1529
1530
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
1531
+ let requiredOutputMissing: boolean | undefined;
1532
+ if (options.outputMode === "file-only" && options.outputPath) {
1533
+ const outputChanged = hasSingleOutputChangedSinceSnapshot(options.outputPath, shared.outputSnapshot);
1534
+ requiredOutputMissing = outputChanged === undefined ? undefined : !outputChanged;
1535
+ } else if (options.structuredOutput) {
1536
+ requiredOutputMissing = !existsSync(options.structuredOutput.outputPath);
1537
+ }
1530
1538
  result.timeoutRecovery = buildTimeoutRecoverySummary({
1531
1539
  termination: "timed-out",
1532
1540
  evidence: mutationEvidence,
1541
+ requiredOutputMissing,
1533
1542
  currentTool: progress.currentTool,
1534
1543
  currentToolArgs: progress.currentToolArgs,
1535
1544
  currentPath: progress.currentPath,
@@ -113,7 +113,7 @@ import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
113
113
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
114
114
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
115
115
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
116
- import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
116
+ import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowLanePlan, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowScriptTraceEntry, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
117
117
  import { executeWorkflowHostCommand, resolveWorkflowHostOutputClaimPath, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "../../workflows/host-command.ts";
118
118
  import { buildWorkflowReceipt, resolveWorkflowReceiptResumeEntry, writeWorkflowReceipt, type WorkflowReceipt, type WorkflowReceiptState } from "../../workflows/workflow-receipt.ts";
119
119
  import { upsertHostStep, validHostStepNodes } from "../shared/host-step-status.ts";
@@ -161,6 +161,9 @@ import {
161
161
  type ToolBudgetConfig,
162
162
  type Usage,
163
163
  type UsageBudgetConfig,
164
+ type WorkflowGraphNode,
165
+ type WorkflowGraphSnapshot,
166
+ type WorkflowNodeStatus,
164
167
  type WorkflowTerminalOutcome,
165
168
  type SubagentRunMode,
166
169
  type SubagentState,
@@ -296,6 +299,7 @@ export interface SubagentParamsLike {
296
299
  type?: string;
297
300
  agent?: string;
298
301
  task?: string;
302
+ capabilities?: boolean;
299
303
  extensionBindings?: ExtensionBindings;
300
304
  /** Retained async child run id. Valid only on workflow runs.run items. */
301
305
  resume?: string;
@@ -4412,6 +4416,90 @@ function formatWorkflowValue(value: unknown): string {
4412
4416
  }
4413
4417
  }
4414
4418
 
4419
+ function buildWorkflowLaneGraph(runId: string, lanes: WorkflowLanePlan[], existing?: WorkflowGraphSnapshot): WorkflowGraphSnapshot {
4420
+ const plannedIds = new Set(lanes.flatMap((lane) => lane.stages.map((stage) => stage.generatedKey)));
4421
+ const previousById = new Map((existing?.nodes ?? []).map((node) => [node.id, node]));
4422
+ const nodes: WorkflowGraphNode[] = [];
4423
+ const phases: WorkflowGraphSnapshot["phases"] = [];
4424
+ let flatIndex = 0;
4425
+ for (const lane of lanes) {
4426
+ const nodeIds: string[] = [];
4427
+ for (const [stageIndex, stage] of lane.stages.entries()) {
4428
+ const previous = previousById.get(stage.generatedKey);
4429
+ const outputName = stage.outputName || previous?.outputName;
4430
+ const structured = stage.structured ?? previous?.structured;
4431
+ const node: WorkflowGraphNode = {
4432
+ id: stage.generatedKey,
4433
+ kind: "step",
4434
+ agent: stage.agent ?? previous?.agent,
4435
+ phase: stage.phase ?? previous?.phase,
4436
+ label: stage.label ?? stage.key,
4437
+ status: previous?.status ?? (stageIndex === 0 ? "running" : "pending"),
4438
+ flatIndex,
4439
+ stepIndex: flatIndex,
4440
+ ...(outputName ? { outputName } : {}),
4441
+ ...(structured !== undefined ? { structured } : {}),
4442
+ ...(previous?.acceptanceStatus ? { acceptanceStatus: previous.acceptanceStatus } : {}),
4443
+ ...(previous?.error ? { error: previous.error } : {}),
4444
+ };
4445
+ nodes.push(node);
4446
+ nodeIds.push(node.id);
4447
+ flatIndex++;
4448
+ }
4449
+ if (nodeIds.length > 0) phases.push({ title: lane.key, nodeIds });
4450
+ }
4451
+ for (const node of existing?.nodes ?? []) {
4452
+ if (!plannedIds.has(node.id)) nodes.push(node);
4453
+ }
4454
+ for (const phase of existing?.phases ?? []) {
4455
+ const retainedNodeIds = phase.nodeIds.filter((nodeId) => !plannedIds.has(nodeId));
4456
+ if (retainedNodeIds.length === 0) continue;
4457
+ const currentPhase = phases.find((candidate) => candidate.title === phase.title);
4458
+ if (currentPhase) currentPhase.nodeIds.push(...retainedNodeIds);
4459
+ else phases.push({ title: phase.title, nodeIds: retainedNodeIds });
4460
+ }
4461
+ const currentNodeId = nodes.find((node) => node.status === "running")?.id ?? existing?.currentNodeId;
4462
+ return { runId, mode: "workflow", phases, nodes, ...(currentNodeId ? { currentNodeId } : {}) };
4463
+ }
4464
+
4465
+ function workflowLaneTraceStatus(state: WorkflowScriptTraceEntry["state"]): WorkflowNodeStatus | undefined {
4466
+ switch (state) {
4467
+ case "started":
4468
+ return "running";
4469
+ case "reused":
4470
+ return undefined;
4471
+ case "completed":
4472
+ return "completed";
4473
+ case "failed":
4474
+ return "failed";
4475
+ case "detached":
4476
+ return "detached";
4477
+ case "stopped":
4478
+ return "stopped";
4479
+ default:
4480
+ return undefined;
4481
+ }
4482
+ }
4483
+
4484
+ function applyWorkflowLaneTrace(graph: WorkflowGraphSnapshot, trace: WorkflowScriptTraceEntry[]): void {
4485
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
4486
+ for (const entry of trace) {
4487
+ if (entry.operation !== "run") continue;
4488
+ const node = nodesById.get(entry.key);
4489
+ if (!node) continue;
4490
+ const status = workflowLaneTraceStatus(entry.state);
4491
+ if (status) node.status = status;
4492
+ if (entry.agent) node.agent = entry.agent;
4493
+ if (entry.phase && (entry.phase !== "auto-resume" || node.phase === undefined)) node.phase = entry.phase;
4494
+ if (entry.label) node.label = entry.label;
4495
+ if (entry.error) node.error = entry.error;
4496
+ else if (status === "completed" || status === "running") delete node.error;
4497
+ }
4498
+ const currentNodeId = graph.nodes.find((node) => node.status === "running")?.id;
4499
+ if (currentNodeId) graph.currentNodeId = currentNodeId;
4500
+ else delete graph.currentNodeId;
4501
+ }
4502
+
4415
4503
  function workflowChatProgressUpdate(
4416
4504
  runId: string,
4417
4505
  chatProgress: WorkflowChatProgressProjection,
@@ -4801,6 +4889,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4801
4889
  liveJob.toolCount = status.toolCount;
4802
4890
  liveJob.currentStep = status.currentStep;
4803
4891
  liveJob.preflight = status.preflight;
4892
+ liveJob.workflowGraph = status.workflowGraph;
4804
4893
  if (status.steps) {
4805
4894
  liveJob.steps = status.steps.map((step, index) => ({ ...step, index }));
4806
4895
  liveJob.agents = status.steps.map((step) => step.agent);
@@ -4884,6 +4973,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4884
4973
  trace: projectedTrace,
4885
4974
  ...(preflightWarnings.length ? { preflightWarnings } : {}),
4886
4975
  };
4976
+ if (status.workflowGraph) applyWorkflowLaneTrace(status.workflowGraph, trace);
4887
4977
  const rebuild = trace.length < projectedTraceLength
4888
4978
  || (projectedTraceLength > 0 && trace[projectedTraceLength - 1] !== projectedTraceTail);
4889
4979
  if (rebuild) {
@@ -4987,6 +5077,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4987
5077
  },
4988
5078
  ...(workflowState ? { state: workflowState } : {}),
4989
5079
  onTrace: updateTrace,
5080
+ onLanePlan: (lanes) => {
5081
+ status.workflowGraph = buildWorkflowLaneGraph(workflowRunId, lanes, status.workflowGraph);
5082
+ applyWorkflowLaneTrace(status.workflowGraph, status.workflow?.trace ?? []);
5083
+ persist({ tolerateStatusWriteFailure: true });
5084
+ },
4990
5085
  host: runHostCommand,
4991
5086
  onHostStep: (hostStep) => {
4992
5087
  status = upsertHostStep({ status, hostStep, persist: (nextStatus) => {
@@ -5122,6 +5217,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5122
5217
  }
5123
5218
  if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, workflowChildren, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), ...(status.steps?.find((step) => step.workflowKey === child.key)?.sessionName ? { sessionName: status.steps?.find((step) => step.workflowKey === child.key)?.sessionName } : {}), ...workflowChildAccountingFields(child), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.outputReference ? { outputReference: child.outputReference } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5124
5219
  persist();
5220
+ deps.refreshResultDelivery?.();
5125
5221
  persistClosed = true;
5126
5222
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
5127
5223
  } catch (error) {
@@ -5167,6 +5263,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5167
5263
  }
5168
5264
  if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, workflowChildren, ...(terminalOutcome ? { terminalOutcome } : {}), results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), ...(status.steps?.find((step) => step.workflowKey === child.key)?.sessionName ? { sessionName: status.steps?.find((step) => step.workflowKey === child.key)?.sessionName } : {}), ...workflowChildAccountingFields(child), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.outputReference ? { outputReference: child.outputReference } : {}), ...(child.terminalOutcome ? { terminalOutcome: child.terminalOutcome } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5169
5265
  persist();
5266
+ deps.refreshResultDelivery?.();
5170
5267
  persistClosed = true;
5171
5268
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(terminalOutcome ? { terminalOutcome } : {}), ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
5172
5269
  } finally {
@@ -2,6 +2,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-t
2
2
  import { formatModelThinking } from "../../shared/formatters.ts";
3
3
  import type { AsyncJobState, AsyncJobStep, HostStepFreshnessV1, HostStepMonitorKind, HostStepNodeV1, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../../shared/types.ts";
4
4
  import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
5
+ import { workflowGraphStageNodes } from "./workflow-graph.ts";
5
6
 
6
7
  export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
7
8
  export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
@@ -15,6 +16,21 @@ const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
15
16
  export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
16
17
  export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
17
18
 
19
+ const ASYNC_STATUS_SNAPSHOT_STATES: Record<AsyncStatusSnapshotState, true> = {
20
+ queued: true,
21
+ running: true,
22
+ complete: true,
23
+ failed: true,
24
+ partial: true,
25
+ paused: true,
26
+ stopped: true,
27
+ rejected: true,
28
+ };
29
+
30
+ function isAsyncStatusSnapshotState(value: string): value is AsyncStatusSnapshotState {
31
+ return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
32
+ }
33
+
18
34
  export interface AsyncStatusSnapshotActivityV1 {
19
35
  state?: string;
20
36
  currentTool?: string;
@@ -87,6 +103,7 @@ export interface AsyncStatusWorkflowRow {
87
103
  state: AsyncJobStep["status"] | HostStepState | "planned";
88
104
  /** Present only for typed host-owned monitor rows; legacy child rows omit it. */
89
105
  kind?: HostStepMonitorKind;
106
+ context?: AsyncJobStep["context"];
90
107
  modelThinking?: string;
91
108
  activity?: string;
92
109
  startedAt?: number;
@@ -155,7 +172,8 @@ function publicCount(value: unknown): number | undefined {
155
172
  function normalizeState(value: string): AsyncStatusSnapshotState {
156
173
  if (value === "completed") return "complete";
157
174
  if (value === "pending") return "queued";
158
- return value as AsyncStatusSnapshotState;
175
+ if (isAsyncStatusSnapshotState(value)) return value;
176
+ return "partial";
159
177
  }
160
178
 
161
179
  function terminalState(state: AsyncStatusSnapshotState): boolean {
@@ -230,6 +248,16 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
230
248
  return node;
231
249
  }
232
250
 
251
+ function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
252
+ const step: AsyncJobStep = {
253
+ agent: node.agent ?? node.label,
254
+ status: workflowGraphStepStatus(node.status),
255
+ workflowKey: node.id,
256
+ label: node.label,
257
+ };
258
+ return projectStep(step, node.flatIndex ?? index, depth, ctx);
259
+ }
260
+
233
261
  function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
234
262
  const state = normalizeState(child.state);
235
263
  const startedAt = publicTime(child.startedAt);
@@ -310,9 +338,14 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
310
338
  };
311
339
  if (ctx.caps.maxDepth > 0) {
312
340
  const stepChildren = job.steps?.map((step, index) => projectStep(step, step.index ?? index, 1, ctx)) ?? [];
341
+ const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
342
+ const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
343
+ const graphChildren = graphStages
344
+ .filter((graphNode) => !loadedKeys.has(graphNode.id))
345
+ .map((graphNode, index) => projectWorkflowGraphNode(graphNode, index, 1, ctx));
313
346
  const nestedChildren = job.nestedChildren?.map((child, index) => projectNestedRun(child, index, 1, ctx)) ?? [];
314
347
  const hostStepChildren = validHostStepList(job.hostSteps).map((hostStep) => projectHostStep(hostStep, ctx));
315
- const ordinaryChildren = [...stepChildren, ...nestedChildren];
348
+ const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren];
316
349
  const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
317
350
  const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
318
351
  const bounded: AsyncStatusSnapshotNodeV1[] = [];
@@ -320,7 +353,10 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
320
353
  ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
321
354
  if (bounded.length) node.children = bounded;
322
355
  } else {
323
- ctx.omitted.children += (job.steps?.length ?? 0) + (job.nestedChildren?.length ?? 0) + validHostStepList(job.hostSteps).length;
356
+ const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
357
+ const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
358
+ const graphCount = graphStages.filter((graphNode) => !loadedKeys.has(graphNode.id)).length;
359
+ ctx.omitted.children += (job.steps?.length ?? 0) + graphCount + (job.nestedChildren?.length ?? 0) + validHostStepList(job.hostSteps).length;
324
360
  }
325
361
  return node;
326
362
  }
@@ -392,6 +428,50 @@ function isWorkflowPreflight(value: readonly HostStepNodeV1[] | WorkflowGraphSna
392
428
  return value !== undefined && !Array.isArray(value) && "lanes" in value;
393
429
  }
394
430
 
431
+ function isWorkflowGraph(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowGraphSnapshot {
432
+ return value !== undefined && !Array.isArray(value) && "nodes" in value;
433
+ }
434
+
435
+ function workflowGraphRowState(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncStatusWorkflowRow["state"] {
436
+ switch (status) {
437
+ case "pending":
438
+ return "planned";
439
+ case "completed":
440
+ return "complete";
441
+ case "detached":
442
+ return "paused";
443
+ default:
444
+ return status;
445
+ }
446
+ }
447
+
448
+ function workflowGraphStepStatus(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncJobStep["status"] {
449
+ switch (status) {
450
+ case "completed":
451
+ return "complete";
452
+ case "detached":
453
+ return "paused";
454
+ default:
455
+ return status;
456
+ }
457
+ }
458
+
459
+ function workflowGraphRowName(node: WorkflowGraphSnapshot["nodes"][number]): string {
460
+ const key = publicText(node.id, "stage", HOST_STEP_MAX_LABEL_CHARS);
461
+ const label = publicOptionalText(node.label, HOST_STEP_MAX_LABEL_CHARS);
462
+ const phase = publicOptionalText(node.phase, HOST_STEP_MAX_LABEL_CHARS);
463
+ const agent = publicOptionalText(node.agent, HOST_STEP_MAX_LABEL_CHARS);
464
+ return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` · ${label}` : ""}${agent ? ` (${agent})` : ""}`;
465
+ }
466
+
467
+ function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
468
+ return {
469
+ name: workflowGraphRowName(node),
470
+ state: workflowGraphRowState(node.status),
471
+ ...(preflight ? { preflight } : {}),
472
+ };
473
+ }
474
+
395
475
  /** Project loaded workflow child facts plus stored preflight hints into compact rows. */
396
476
  export function projectAsyncWorkflowRows(
397
477
  steps: readonly AsyncJobStep[] | undefined,
@@ -399,9 +479,62 @@ export function projectAsyncWorkflowRows(
399
479
  preflightOverride?: WorkflowPreflightV1,
400
480
  ): AsyncStatusWorkflowRow[] {
401
481
  const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
402
- const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) ? undefined : hostStepsOrPreflight;
482
+ const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
483
+ const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
403
484
  const loaded = steps ?? [];
404
485
  const declared = new Map<string, WorkflowPreflightLaneV1>();
486
+ if (graph) {
487
+ const loadedIndexesByKey = new Map<string, number[]>();
488
+ for (const [index, step] of loaded.entries()) {
489
+ if (!step.workflowKey) continue;
490
+ const indexes = loadedIndexesByKey.get(step.workflowKey) ?? [];
491
+ indexes.push(index);
492
+ loadedIndexesByKey.set(step.workflowKey, indexes);
493
+ }
494
+ const consumed = new Set<number>();
495
+ const childRows: AsyncStatusWorkflowRow[] = [];
496
+ for (const lane of preflight?.lanes ?? []) declared.set(lane.key, lane);
497
+ const graphStages = workflowGraphStageNodes(graph);
498
+ const graphKeys = new Set(graphStages.map((node) => node.id));
499
+ const graphPhaseByNodeId = new Map<string, string>();
500
+ for (const phase of graph.phases) {
501
+ for (const nodeId of phase.nodeIds) {
502
+ if (graphKeys.has(nodeId) && !graphPhaseByNodeId.has(nodeId)) graphPhaseByNodeId.set(nodeId, phase.title);
503
+ }
504
+ }
505
+ const graphLaneKeys = new Set(graphPhaseByNodeId.values());
506
+ const preflightForNode = (node: WorkflowGraphSnapshot["nodes"][number]): WorkflowPreflightLaneV1 | undefined => {
507
+ const phaseTitle = graphPhaseByNodeId.get(node.id);
508
+ return declared.get(node.id) ?? (node.phase ? declared.get(node.phase) : undefined) ?? (phaseTitle ? declared.get(phaseTitle) : undefined);
509
+ };
510
+ for (const node of graphStages) {
511
+ const indexes = (loadedIndexesByKey.get(node.id) ?? []).filter((index) => !consumed.has(index));
512
+ if (indexes.length > 0) {
513
+ for (const index of indexes) {
514
+ consumed.add(index);
515
+ childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, preflightForNode(node)));
516
+ }
517
+ } else {
518
+ childRows.push(projectWorkflowGraphRow(node, preflightForNode(node)));
519
+ }
520
+ }
521
+ for (const lane of preflight?.lanes ?? []) {
522
+ if (graphKeys.has(lane.key) || graphLaneKeys.has(lane.key)) continue;
523
+ const indexes = (loadedIndexesByKey.get(lane.key) ?? []).filter((index) => !consumed.has(index));
524
+ if (indexes.length > 0) {
525
+ for (const index of indexes) {
526
+ consumed.add(index);
527
+ childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
528
+ }
529
+ } else {
530
+ childRows.push({ name: lane.key, state: "planned", preflight: lane });
531
+ }
532
+ }
533
+ for (const [index, step] of loaded.entries()) {
534
+ if (!consumed.has(index)) childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
535
+ }
536
+ return [...childRows, ...validHostStepList(graph).map(hostStepRow)];
537
+ }
405
538
  const loadedIndexByKey = new Map<string, number>();
406
539
  for (const [index, step] of loaded.entries()) {
407
540
  if (step.workflowKey !== undefined && !loadedIndexByKey.has(step.workflowKey)) loadedIndexByKey.set(step.workflowKey, index);
@@ -431,6 +564,7 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
431
564
  return {
432
565
  name: workflowStepName(step, index),
433
566
  state: step.status,
567
+ ...(step.context ? { context: step.context } : {}),
434
568
  ...(modelThinking ? { modelThinking } : {}),
435
569
  ...(activity ? { activity } : {}),
436
570
  ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
@@ -0,0 +1,9 @@
1
+ export function backgroundProcessOptions(platform: NodeJS.Platform = process.platform): {
2
+ detached: boolean;
3
+ windowsHide: true;
4
+ } {
5
+ return {
6
+ detached: platform !== "win32",
7
+ windowsHide: true,
8
+ };
9
+ }
@@ -125,11 +125,8 @@ export function formatUnresolvedMcpDirectToolSelectors(selectors: readonly strin
125
125
 
126
126
  function getServerPrefix(serverName: string, mode: McpToolPrefix): string {
127
127
  if (mode === "none") return "";
128
- if (mode === "short") {
129
- const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
130
- return short || "mcp";
131
- }
132
- return serverName.replace(/-/g, "_");
128
+ if (mode === "short") return serverName.replace(/-?mcp$/i, "") || "mcp";
129
+ return serverName;
133
130
  }
134
131
 
135
132
  function formatToolName(toolName: string, serverName: string, prefix: McpToolPrefix): string {
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
- import type { ArtifactPaths, TimeoutRecoverySummary, TrackedMutationEvidence, TrackedMutationFingerprint, TrackedMutationSnapshot } from "../../shared/types.ts";
6
+ import type { ArtifactPaths, TimeoutRecoveryProjection, TimeoutRecoverySummary, TrackedMutationEvidence, TrackedMutationFingerprint, TrackedMutationSnapshot } from "../../shared/types.ts";
7
7
 
8
8
  const MAX_TRACKED_PATHS = 500;
9
9
  const MAX_HASH_BYTES = 1024 * 1024;
@@ -15,7 +15,7 @@ function gitArguments(args: string[]): string[] {
15
15
  }
16
16
 
17
17
  function gitOutput(cwd: string, args: string[], maxBuffer = MAX_HASH_BYTES): string {
18
- return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
18
+ return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer, windowsHide: true });
19
19
  }
20
20
 
21
21
  function splitNul(output: string): string[] {
@@ -26,7 +26,7 @@ function hashLargeDiff(cwd: string, relativePath: string): string {
26
26
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
27
27
  const diffPath = path.join(tempDir, "diff.patch");
28
28
  try {
29
- execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore" });
29
+ execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore", windowsHide: true });
30
30
  const hash = createHash("sha256");
31
31
  const buffer = Buffer.allocUnsafe(64 * 1024);
32
32
  const fd = fs.openSync(diffPath, "r");
@@ -110,9 +110,48 @@ function formatPathList(paths: string[]): string {
110
110
  return paths.length > MAX_TIMEOUT_FILES ? `${shown}, ... (${paths.length - MAX_TIMEOUT_FILES} more)` : shown;
111
111
  }
112
112
 
113
+ /** Keep status and completion details to bounded routing evidence, not raw output or effects. */
114
+ export function projectTimeoutRecovery(value: unknown): TimeoutRecoveryProjection | undefined {
115
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
116
+ const source = value as Record<string, unknown>;
117
+ const termination = source.termination === "timed-out" || source.termination === "stopped" ? source.termination : undefined;
118
+ if (!termination || !Array.isArray(source.changedFiles)) return undefined;
119
+ const allChangedFiles = source.changedFiles.filter((file): file is string => typeof file === "string");
120
+ const changedFiles = allChangedFiles.slice(0, MAX_TIMEOUT_FILES);
121
+ const reportStatus = source.reportStatus === "missing"
122
+ || source.reportStatus === "written"
123
+ || source.reportStatus === "not-requested"
124
+ || source.reportStatus === "unknown"
125
+ ? source.reportStatus
126
+ : undefined;
127
+ return {
128
+ termination,
129
+ changedFiles,
130
+ ...(source.truncated === true || allChangedFiles.length > changedFiles.length ? { truncated: true } : {}),
131
+ ...(source.recoveryNeeded === true ? { recoveryNeeded: true } : {}),
132
+ ...(source.reason === "timed-out-with-dirty-worktree" ? { reason: source.reason } : {}),
133
+ ...(reportStatus ? { reportStatus } : {}),
134
+ };
135
+ }
136
+
137
+ /** Render only the bounded recovery route needed by a parent/operator. */
138
+ export function formatTimeoutRecoveryLines(value: unknown, indent = ""): string[] {
139
+ const recovery = projectTimeoutRecovery(value);
140
+ if (!recovery?.recoveryNeeded) return [];
141
+ const changedFiles = recovery.changedFiles.length > 0
142
+ ? `${recovery.changedFiles.join(", ")}${recovery.truncated ? ", …" : ""}`
143
+ : "none";
144
+ const changedFileCount = recovery.changedFiles.length > 0 ? `${recovery.changedFiles.length}${recovery.truncated ? "+" : ""}` : "0";
145
+ return [
146
+ `${indent}Recovery needed: review the diff and artifacts before resuming or launching dependent stages.`,
147
+ `${indent}Recovery evidence: requested report: ${recovery.reportStatus ?? "unknown"}; changed tracked files: ${changedFiles} (${changedFileCount}); classification: ${recovery.reason ?? recovery.termination}`,
148
+ ];
149
+ }
150
+
113
151
  export function buildTimeoutRecoverySummary(input: {
114
152
  termination: "timed-out" | "stopped";
115
153
  evidence: TrackedMutationEvidence;
154
+ requiredOutputMissing?: boolean;
116
155
  currentTool?: string;
117
156
  currentToolArgs?: string;
118
157
  currentPath?: string;
@@ -122,11 +161,19 @@ export function buildTimeoutRecoverySummary(input: {
122
161
  }): TimeoutRecoverySummary {
123
162
  const warning = "Inspect partial changes before retrying or resuming the child.";
124
163
  const changedFiles = input.evidence.changedFiles.slice(0, MAX_TIMEOUT_FILES);
164
+ let reportStatus: "missing" | "written" | "not-requested" = "not-requested";
165
+ if (input.requiredOutputMissing === true) reportStatus = "missing";
166
+ else if (input.requiredOutputMissing === false) reportStatus = "written";
167
+ const recoveryNeeded = input.termination === "timed-out"
168
+ && reportStatus === "missing"
169
+ && input.evidence.changedFiles.length > 0;
125
170
  const lines = [
126
171
  "Recovery summary:",
127
172
  `- termination: ${input.termination}`,
128
173
  `- changed tracked files: ${input.evidence.unavailable ? `unavailable (${input.evidence.unavailable})` : formatPathList(input.evidence.changedFiles)}`,
129
174
  ];
175
+ if (input.requiredOutputMissing !== undefined) lines.push(`- requested report: ${reportStatus}`);
176
+ if (recoveryNeeded) lines.push("- Recovery needed: review the diff and artifacts before resuming or launching dependent stages.");
130
177
  if (input.currentTool) lines.push(`- active tool: ${input.currentTool}${input.currentToolArgs ? ` — ${input.currentToolArgs}` : ""}`);
131
178
  if (input.currentPath) lines.push(`- active path: ${input.currentPath}`);
132
179
  if (input.sessionFile) lines.push(`- session file: ${input.sessionFile}`);
@@ -138,6 +185,8 @@ export function buildTimeoutRecoverySummary(input: {
138
185
  termination: input.termination,
139
186
  changedFiles,
140
187
  truncated: input.evidence.changedFiles.length > changedFiles.length || input.evidence.truncated || undefined,
188
+ ...(recoveryNeeded ? { recoveryNeeded: true, reason: "timed-out-with-dirty-worktree" as const } : {}),
189
+ reportStatus,
141
190
  currentTool: input.currentTool,
142
191
  currentToolArgs: input.currentToolArgs,
143
192
  currentPath: input.currentPath,