pi-subagents 0.57.0 → 0.58.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/docs/agents.md +13 -6
  3. package/docs/extension-api.md +39 -0
  4. package/docs/models.md +3 -3
  5. package/docs/tool-reference.md +3 -2
  6. package/docs/workflows.md +24 -0
  7. package/package.json +1 -1
  8. package/skills/pi-subagents/SKILL.md +2 -0
  9. package/skills/pi-subagents/references/execution-controls.md +1 -1
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  12. package/src/agents/agent-management.ts +23 -0
  13. package/src/agents/agent-serializer.ts +5 -0
  14. package/src/agents/agents.ts +38 -3
  15. package/src/agents/runtime-agent-events.ts +70 -0
  16. package/src/agents/runtime-agent-registry.ts +10 -2
  17. package/src/api/agents.ts +10 -5
  18. package/src/api/preflight.ts +26 -1
  19. package/src/extension/index.ts +15 -4
  20. package/src/extension/rpc.ts +35 -14
  21. package/src/extension/schemas.ts +2 -2
  22. package/src/extension/tool-description.ts +8 -4
  23. package/src/integrations/herdr-status.ts +5 -0
  24. package/src/runs/background/async-execution.ts +29 -12
  25. package/src/runs/background/async-resume.ts +6 -2
  26. package/src/runs/background/async-status.ts +2 -0
  27. package/src/runs/background/chain-append.ts +2 -0
  28. package/src/runs/background/notify.ts +26 -3
  29. package/src/runs/background/result-delivery-ownership.ts +45 -0
  30. package/src/runs/background/result-watcher.ts +27 -10
  31. package/src/runs/background/subagent-runner.ts +71 -21
  32. package/src/runs/foreground/execution.ts +20 -1
  33. package/src/runs/foreground/subagent-executor.ts +57 -26
  34. package/src/runs/foreground/workflow-detach-reconcile.ts +128 -17
  35. package/src/runs/shared/completion-guard.ts +4 -3
  36. package/src/runs/shared/dynamic-fanout.ts +3 -3
  37. package/src/runs/shared/fast-mode-extension.ts +5 -5
  38. package/src/runs/shared/launch-cwd.ts +16 -0
  39. package/src/runs/shared/long-running-guard.ts +2 -1
  40. package/src/runs/shared/mcp-config-sources.ts +386 -0
  41. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  42. package/src/runs/shared/model-exclusions.ts +12 -0
  43. package/src/runs/shared/model-fallback.ts +16 -4
  44. package/src/runs/shared/parallel-utils.ts +8 -1
  45. package/src/runs/shared/pi-args.ts +61 -6
  46. package/src/runs/shared/single-output.ts +17 -0
  47. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  48. package/src/shared/formatters.ts +6 -0
  49. package/src/shared/launch-contract.ts +4 -0
  50. package/src/shared/types.ts +42 -1
  51. package/src/shared/utils.ts +6 -29
  52. package/src/slash/subagents-admin.ts +3 -0
  53. package/src/tui/render.ts +14 -6
  54. package/src/workflows/scripted-workflow.ts +52 -5
  55. package/src/workflows/workflow-receipt.ts +39 -4
@@ -6,7 +6,7 @@ import { registerNativeSupervisorClient } from "../../intercom/native-supervisor
6
6
  import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
7
7
  import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
8
8
  import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
9
- import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
9
+ import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
10
10
  import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
11
11
  import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
12
12
  import {
@@ -20,7 +20,7 @@ import {
20
20
  import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, decodeToolBudgetEnv, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
21
21
  import type { JsonSchemaObject, ResolvedToolBudget, SubagentState } from "../../shared/types.ts";
22
22
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
23
- import { resolveWatchPath } from "../../shared/utils.ts";
23
+ import { getAgentDir, resolveWatchPath } from "../../shared/utils.ts";
24
24
  import { registerChildWatchdog } from "../../watchdog/register-child.ts";
25
25
  import { CHILD_WATCHDOG_CONFIG_ENV, decodeChildWatchdogConfig } from "../../watchdog/child-status.ts";
26
26
  import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
@@ -68,7 +68,8 @@ const PARENT_ONLY_CUSTOM_MESSAGE_TYPES = new Set([
68
68
  "subagent-control-notice",
69
69
  ]);
70
70
  const SUBAGENT_ORCHESTRATION_SKILL_NAME_PATTERN = /<name>\s*pi-subagents\s*<\/name>/;
71
- const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
71
+ const PROJECT_CONTEXT_XML_HEADER = "\n\n<project_context>\n\n";
72
+ const PROJECT_CONTEXT_LEGACY_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
72
73
  const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
73
74
  const DATE_HEADER = "\nCurrent date:";
74
75
 
@@ -148,10 +149,80 @@ function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[
148
149
  }
149
150
 
150
151
  export function stripProjectContext(prompt: string): string {
151
- const startIndex = prompt.indexOf(PROJECT_CONTEXT_HEADER);
152
- if (startIndex === -1) return prompt;
153
- const endIndex = findSectionEnd(prompt, startIndex + PROJECT_CONTEXT_HEADER.length, [SKILLS_HEADER, DATE_HEADER]);
154
- return `${prompt.slice(0, startIndex)}${prompt.slice(endIndex)}`;
152
+ const xmlStartIndex = prompt.indexOf(PROJECT_CONTEXT_XML_HEADER);
153
+ if (xmlStartIndex !== -1) {
154
+ const closingTag = "</project_context>";
155
+ const closingIndex = prompt.indexOf(closingTag, xmlStartIndex + PROJECT_CONTEXT_XML_HEADER.length);
156
+ if (closingIndex !== -1) {
157
+ return `${prompt.slice(0, xmlStartIndex)}${prompt.slice(closingIndex + closingTag.length)}`;
158
+ }
159
+ }
160
+ const legacyStartIndex = prompt.indexOf(PROJECT_CONTEXT_LEGACY_HEADER);
161
+ if (legacyStartIndex === -1) return prompt;
162
+ const endIndex = findSectionEnd(prompt, legacyStartIndex + PROJECT_CONTEXT_LEGACY_HEADER.length, [SKILLS_HEADER, DATE_HEADER]);
163
+ return `${prompt.slice(0, legacyStartIndex)}${prompt.slice(endIndex)}`;
164
+ }
165
+
166
+ const GLOBAL_CONTEXT_FILE_NAMES = new Set(["agents.md", "agents.override.md", "claude.md"]);
167
+
168
+ function canonicalDirectory(dir: string): string {
169
+ try {
170
+ return fs.realpathSync(dir);
171
+ } catch {
172
+ return path.resolve(dir);
173
+ }
174
+ }
175
+
176
+ function expandContextPath(filePath: string): string {
177
+ const home = process.env.HOME ?? process.env.USERPROFILE;
178
+ return filePath === "~"
179
+ ? home ?? filePath
180
+ : /^~[\\/]/.test(filePath)
181
+ ? path.join(home ?? "~", filePath.slice(2))
182
+ : filePath;
183
+ }
184
+
185
+ function isContextFilePath(filePath: string): boolean {
186
+ return GLOBAL_CONTEXT_FILE_NAMES.has(path.basename(expandContextPath(filePath)).toLowerCase());
187
+ }
188
+
189
+ function isGlobalContextFile(filePath: string): boolean {
190
+ const expanded = expandContextPath(filePath);
191
+ if (!isContextFilePath(filePath)) return false;
192
+ return canonicalDirectory(path.dirname(expanded)) === canonicalDirectory(getAgentDir());
193
+ }
194
+
195
+ function stripGlobalInstructionsFromXmlContext(context: string): string {
196
+ const block = /<project_instructions\s+path=(["'])(.*?)\1\s*>[\s\S]*?<\/project_instructions>\s*/gi;
197
+ return context.replace(block, (match, _quote: string, filePath: string) => isGlobalContextFile(filePath) ? "" : match);
198
+ }
199
+
200
+ function stripGlobalInstructionsFromLegacyContext(context: string): string {
201
+ const sections = [...context.matchAll(/^## ([^\r\n]+)(?:\r?\n|$)/gm)]
202
+ .filter((match) => isContextFilePath(match[1]!.trim()));
203
+ let rewritten = "";
204
+ let cursor = 0;
205
+ for (let index = 0; index < sections.length; index++) {
206
+ const section = sections[index]!;
207
+ const start = section.index!;
208
+ const end = sections[index + 1]?.index ?? context.length;
209
+ rewritten += context.slice(cursor, start);
210
+ if (!isGlobalContextFile(section[1]!.trim())) rewritten += context.slice(start, end);
211
+ cursor = end;
212
+ }
213
+ return `${rewritten}${context.slice(cursor)}`;
214
+ }
215
+
216
+ export function stripGlobalContext(prompt: string): string {
217
+ const rewrittenXml = prompt.replace(/<project_context>[\s\S]*?<\/project_context>/gi, (context) => {
218
+ const rewritten = stripGlobalInstructionsFromXmlContext(context);
219
+ return /<project_instructions\b/i.test(rewritten) ? rewritten : "";
220
+ });
221
+ const legacyStartIndex = rewrittenXml.indexOf(PROJECT_CONTEXT_LEGACY_HEADER);
222
+ if (legacyStartIndex === -1) return rewrittenXml;
223
+ const legacyEndIndex = findSectionEnd(rewrittenXml, legacyStartIndex + PROJECT_CONTEXT_LEGACY_HEADER.length, [SKILLS_HEADER, DATE_HEADER]);
224
+ const legacyContext = rewrittenXml.slice(legacyStartIndex, legacyEndIndex);
225
+ return `${rewrittenXml.slice(0, legacyStartIndex)}${stripGlobalInstructionsFromLegacyContext(legacyContext)}${rewrittenXml.slice(legacyEndIndex)}`;
155
226
  }
156
227
 
157
228
  export function stripInheritedSkills(prompt: string): string {
@@ -177,12 +248,15 @@ function stripChildBoundaryInstructions(prompt: string): string {
177
248
 
178
249
  export function rewriteSubagentPrompt(
179
250
  prompt: string,
180
- options: { inheritProjectContext: boolean; inheritSkills: boolean; fanoutChild?: boolean },
251
+ options: { inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean; fanoutChild?: boolean },
181
252
  ): string {
182
253
  let rewritten = prompt;
183
254
  if (!options.inheritProjectContext) {
184
255
  rewritten = stripProjectContext(rewritten);
185
256
  }
257
+ if (!options.inheritGlobalContext) {
258
+ rewritten = stripGlobalContext(rewritten);
259
+ }
186
260
  if (!options.inheritSkills) {
187
261
  rewritten = stripInheritedSkills(rewritten);
188
262
  }
@@ -688,12 +762,14 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
688
762
  }
689
763
 
690
764
  const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
765
+ const inheritGlobalContext = readBooleanEnv(SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV);
691
766
  const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
692
767
  const fanoutChild = readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV);
693
768
  let rewritten = event.systemPrompt;
694
- if (inheritProjectContext !== undefined || inheritSkills !== undefined || fanoutChild !== undefined) {
769
+ if (inheritProjectContext !== undefined || inheritGlobalContext !== undefined || inheritSkills !== undefined || fanoutChild !== undefined) {
695
770
  rewritten = rewriteSubagentPrompt(event.systemPrompt, {
696
771
  inheritProjectContext: inheritProjectContext ?? true,
772
+ inheritGlobalContext: inheritGlobalContext ?? true,
697
773
  inheritSkills: inheritSkills ?? true,
698
774
  fanoutChild: fanoutChild === true,
699
775
  });
@@ -22,6 +22,12 @@ export function formatTokenUsage(usage: TokenUsage, legacyLabel = "tok"): string
22
22
  ? `${formatTokens(usage.window)} window · ${formatTokens(usage.total)} spent`
23
23
  : `${formatTokens(usage.total)} ${legacyLabel}`;
24
24
  }
25
+ export function formatContextUsage(usage: Pick<TokenUsage, "window" | "windowPeak">, contextLimit: number): string | undefined {
26
+ if (usage.window === undefined || !Number.isFinite(usage.window) || !Number.isFinite(contextLimit) || contextLimit <= 0) return undefined;
27
+ const peak = usage.windowPeak !== undefined && Number.isFinite(usage.windowPeak) ? usage.windowPeak : usage.window;
28
+ const used = Math.max(0, usage.window, peak);
29
+ return `ctx ${formatTokens(used)}/${formatTokens(contextLimit)} (${Math.round((used / contextLimit) * 100)}%)`;
30
+ }
25
31
 
26
32
  export function formatModelThinking(model?: string, thinking?: string): string {
27
33
  const parsed = model ? splitKnownThinkingSuffix(model) : undefined;
@@ -43,6 +43,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
43
43
  systemPrompt: agent.systemPrompt,
44
44
  systemPromptMode: agent.systemPromptMode,
45
45
  inheritProjectContext: agent.inheritProjectContext,
46
+ inheritGlobalContext: agent.inheritGlobalContext,
46
47
  inheritSkills: agent.inheritSkills,
47
48
  model: agent.model,
48
49
  modelProvider: agent.modelProvider,
@@ -53,6 +54,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
53
54
  mcpDirectTools: agent.mcpDirectTools,
54
55
  extensions: agent.extensions,
55
56
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
57
+ mutationTools: agent.mutationTools,
56
58
  skills: agent.skills,
57
59
  skillPath: agent.skillPath,
58
60
  output: agent.output,
@@ -87,6 +89,7 @@ export interface LaunchBindingInput {
87
89
  systemPrompt?: string | null;
88
90
  systemPromptMode?: AgentConfig["systemPromptMode"];
89
91
  inheritProjectContext: boolean;
92
+ inheritGlobalContext: boolean;
90
93
  inheritSkills: boolean;
91
94
  skills?: string[];
92
95
  tools?: string[];
@@ -113,6 +116,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
113
116
  systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : stableJsonDigest(input.systemPrompt),
114
117
  systemPromptMode: input.systemPromptMode,
115
118
  inheritProjectContext: input.inheritProjectContext,
119
+ inheritGlobalContext: input.inheritGlobalContext,
116
120
  inheritSkills: input.inheritSkills,
117
121
  skills: input.skills,
118
122
  tools: input.tools,
@@ -76,6 +76,20 @@ export interface WorkflowGraphSnapshot {
76
76
 
77
77
  export type WorkflowReceiptState = "complete" | "failed" | "paused" | "stopped";
78
78
 
79
+ export type WorkflowTerminalResolution = "settled-awaiting-resume" | "failed-child" | "interrupted-child";
80
+
81
+ export interface WorkflowTerminalOutcome {
82
+ state: "partial";
83
+ reason: "budget_exhausted" | "timeout";
84
+ }
85
+
86
+ export interface WorkflowRecoveryAction {
87
+ key: string;
88
+ call: "runs.run";
89
+ resume: { workflowRunId: string; key: string; latest: true };
90
+ taskRequired: true;
91
+ }
92
+
79
93
  export interface WorkflowChildSummaryV1 {
80
94
  version: 1;
81
95
  parentToolCallId: string;
@@ -98,6 +112,7 @@ type WorkflowReceiptEntryResumability =
98
112
 
99
113
  export type WorkflowReceiptEntry = WorkflowReceiptEntryResumability & {
100
114
  key: string;
115
+ terminalOutcome?: WorkflowTerminalOutcome;
101
116
  agent?: string;
102
117
  requestedContext?: "fresh" | "fork";
103
118
  resolvedContext?: "fresh" | "fork" | "mixed";
@@ -113,6 +128,9 @@ export interface WorkflowReceipt {
113
128
  createdAt: number;
114
129
  entries: Record<string, WorkflowReceiptEntry>;
115
130
  workflowChildren?: WorkflowChildSummaryV1;
131
+ workflowResolution?: WorkflowTerminalResolution;
132
+ terminalOutcome?: WorkflowTerminalOutcome;
133
+ recovery?: WorkflowRecoveryAction[];
116
134
  }
117
135
 
118
136
  export interface SavedOutputReference {
@@ -375,8 +393,24 @@ export interface FileMutationEffect {
375
393
  evidence?: TrackedMutationEvidence;
376
394
  }
377
395
 
396
+ export interface SettlementDiagnostic {
397
+ finalTextPresent: boolean;
398
+ mutation: {
399
+ expected: boolean;
400
+ attempted: boolean;
401
+ observed: boolean;
402
+ };
403
+ requiredOutput?: {
404
+ kind: "file-only" | "structured";
405
+ path: string;
406
+ missing: boolean;
407
+ };
408
+ afterCompactionSettlement?: boolean;
409
+ }
410
+
378
411
  export interface EffectsProjection {
379
412
  fileMutation?: FileMutationEffect;
413
+ settlementDiagnostic?: SettlementDiagnostic;
380
414
  }
381
415
 
382
416
  export interface TrackedMutationSnapshot {
@@ -605,9 +639,11 @@ export interface SteeringRecoveryDescriptor {
605
639
  extensions?: string[];
606
640
  subagentOnlyExtensions?: string[];
607
641
  mcpDirectTools?: string[];
642
+ mutationTools?: string[];
608
643
  systemPrompt?: string;
609
644
  systemPromptMode: "append" | "replace";
610
645
  inheritProjectContext: boolean;
646
+ inheritGlobalContext: boolean;
611
647
  inheritSkills: boolean;
612
648
  skills?: string[];
613
649
  skillPath?: string[];
@@ -1622,6 +1658,7 @@ export interface AsyncStatus {
1622
1658
  skills?: string[];
1623
1659
  model?: string;
1624
1660
  thinking?: string;
1661
+ contextLimit?: number;
1625
1662
  thinkingCeiling?: ThinkingLevel;
1626
1663
  attemptedModels?: string[];
1627
1664
  modelAttempts?: ModelAttempt[];
@@ -2027,6 +2064,8 @@ export interface RunSyncOptions {
2027
2064
  /** Resolved launch context for this child. */
2028
2065
  context?: "fresh" | "fork";
2029
2066
  cwd?: string;
2067
+ /** Original cwd input retained for launch diagnostics. */
2068
+ requestedCwd?: string;
2030
2069
  signal?: AbortSignal;
2031
2070
  interruptSignal?: AbortSignal;
2032
2071
  timeoutMs?: number;
@@ -2085,9 +2124,11 @@ export interface RunSyncOptions {
2085
2124
  /** Package-internal one-use authorization for one foreground workflow child. */
2086
2125
  workflowChildPermitLaunch?: WorkflowChildPermitContext;
2087
2126
  /** Registry models available for heuristic bare-model resolution */
2088
- availableModels?: Array<{ provider: string; id: string; fullId: string }>;
2127
+ availableModels?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
2089
2128
  /** Current parent-session provider to prefer for ambiguous bare model ids */
2090
2129
  preferredModelProvider?: string;
2130
+ /** Parent Pi event host used to snapshot runtime-registered MCP servers before child launch. */
2131
+ runtimeSnapshotHost?: import("../runs/shared/mcp-direct-tool-allowlist.ts").McpRuntimeSnapshotHost;
2091
2132
  /** Optional subagent model-scope enforcement for fallback candidates */
2092
2133
  modelScope?: ModelScopeRule | ModelScopeRule[];
2093
2134
  /** Skills to make available (overrides agent default if provided) */
@@ -1,7 +1,3 @@
1
- /**
2
- * General utility functions for the subagent extension
3
- */
4
-
5
1
  import * as fs from "node:fs";
6
2
  import * as os from "node:os";
7
3
  import * as path from "node:path";
@@ -10,10 +6,6 @@ import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "./
10
6
  import { formatToolCall } from "./formatters.ts";
11
7
  import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
12
8
 
13
- // ============================================================================
14
- // File System Utilities
15
- // ============================================================================
16
-
17
9
  const DEFAULT_CONFIG_DIR_NAME = ".pi";
18
10
  const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
19
11
  export const PI_CODING_AGENT_PACKAGE_ROOT_ENV = "PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT";
@@ -96,9 +88,10 @@ export function getProjectConfigDir(projectRoot: string): string {
96
88
 
97
89
  export function getAgentDir(): string {
98
90
  const configured = process.env.PI_CODING_AGENT_DIR;
99
- if (configured === "~") return os.homedir();
100
- if (configured?.startsWith("~/")) return path.join(os.homedir(), configured.slice(2));
101
- return configured || path.join(os.homedir(), getConfigDirName(), "agent");
91
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
92
+ if (configured === "~") return home;
93
+ if (configured?.startsWith("~/") || configured?.startsWith("~\\")) return path.join(home, configured.slice(2));
94
+ return configured || path.join(home, getConfigDirName(), "agent");
102
95
  }
103
96
 
104
97
  const statusCache = new Map<string, { mtime: number; ctime: number; size: number; ino: number; status: AsyncStatus }>();
@@ -253,10 +246,7 @@ function getOutputTail(outputFile: string | undefined, maxLines: number = 3): st
253
246
  }
254
247
  }
255
248
 
256
- /**
257
- * Get human-readable last activity time for a file
258
- */
259
- export function getLastActivity(outputFile: string | undefined): string {
249
+ export function getLastActivity(outputFile: string | undefined): string {
260
250
  if (!outputFile) return "";
261
251
  try {
262
252
  const stat = fs.statSync(outputFile);
@@ -265,14 +255,11 @@ function getOutputTail(outputFile: string | undefined, maxLines: number = 3): st
265
255
  if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
266
256
  return `active ${Math.floor(ago / 60000)}m ago`;
267
257
  } catch {
268
- // Last-activity text is best effort; missing files should simply omit the hint.
258
+ // Last-activity text is best effort; missing files should omit the hint.
269
259
  return "";
270
260
  }
271
261
  }
272
262
 
273
- /**
274
- * Find the latest session file in a directory
275
- */
276
263
  export function findLatestSessionFile(sessionDir: string): string | null {
277
264
  if (!fs.existsSync(sessionDir)) return null;
278
265
  const files = fs.readdirSync(sessionDir)
@@ -289,9 +276,6 @@ export function findLatestSessionFile(sessionDir: string): string | null {
289
276
  return latest ? latest.path : null;
290
277
  }
291
278
 
292
- /**
293
- * Write a prompt to a temporary file
294
- */
295
279
  function writePrompt(agent: string, prompt: string): { dir: string; path: string } {
296
280
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
297
281
  const p = path.join(dir, `${agent.replace(/[^\w.-]/g, "_")}.md`);
@@ -299,13 +283,6 @@ function writePrompt(agent: string, prompt: string): { dir: string; path: string
299
283
  return { dir, path: p };
300
284
  }
301
285
 
302
- // ============================================================================
303
- // Message Parsing Utilities
304
- // ============================================================================
305
-
306
- /**
307
- * Get the final text output from a list of messages
308
- */
309
286
  export function getFinalOutput(messages: Message[]): string {
310
287
  const validTextParts: string[] = [];
311
288
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -89,6 +89,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
89
89
  ...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
90
90
  systemPromptMode: agent.systemPromptMode,
91
91
  inheritProjectContext: agent.inheritProjectContext,
92
+ inheritGlobalContext: agent.inheritGlobalContext,
92
93
  inheritSkills: agent.inheritSkills,
93
94
  ...(agent.defaultContext !== undefined ? { defaultContext: agent.defaultContext } : {}),
94
95
  ...(agent.acceptanceRole !== undefined ? { acceptanceRole: agent.acceptanceRole } : {}),
@@ -98,6 +99,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
98
99
  ...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
99
100
  ...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
100
101
  ...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
102
+ ...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
101
103
  ...(agent.completionGuard !== undefined ? { completionGuard: agent.completionGuard } : {}),
102
104
  ...(agent.toolBudget !== undefined ? { toolBudget: agent.toolBudget } : {}),
103
105
  };
@@ -189,6 +191,7 @@ function metadataFor(agent: AgentConfig): string {
189
191
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
190
192
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
191
193
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
194
+ lines.push(`Inherit global context: ${agent.inheritGlobalContext ? "true" : "false"}`);
192
195
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
193
196
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
194
197
  if (agent.output) lines.push(`Output: ${agent.output}`);
package/src/tui/render.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  } from "../shared/types.ts";
24
24
  import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
25
25
  import { FLEET_OPEN_SHORTCUT, formatShortcutLabel } from "../shared/shortcuts.ts";
26
- import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
26
+ import { formatContextUsage, formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
27
27
  import { getDisplayItems, getSingleResultOutput, PROMPT_REDACTED } from "../shared/utils.ts";
28
28
  import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
29
29
  import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
@@ -267,9 +267,13 @@ function oneLine(text: string): string {
267
267
  const COMPACT_TASK_MAX_CHARS = 96;
268
268
 
269
269
  export function compactTaskText(task: string | undefined, label?: string): string | undefined {
270
- const source = label?.trim() || task?.trim();
271
- if (!source || source === PROMPT_REDACTED) return undefined;
272
- const normalized = oneLine(source);
270
+ const taskText = task?.trim();
271
+ const labelText = label?.trim();
272
+ const normalizedTask = taskText && taskText !== PROMPT_REDACTED ? oneLine(taskText) : "";
273
+ const normalizedLabel = labelText && labelText !== PROMPT_REDACTED ? oneLine(labelText) : "";
274
+ const normalized = normalizedLabel && normalizedTask && normalizedLabel !== normalizedTask
275
+ ? `${normalizedLabel} — ${normalizedTask}`
276
+ : normalizedLabel || normalizedTask;
273
277
  if (!normalized) return undefined;
274
278
  return previewDisplayText(normalized, COMPACT_TASK_MAX_CHARS);
275
279
  }
@@ -794,7 +798,7 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
794
798
  const activity = widgetStepActivity(step, job.updatedAt);
795
799
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
796
800
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
797
- const label = compactTaskText(undefined, step.label);
801
+ const label = compactTaskText(step.description, step.label);
798
802
  const display = label ? `${label} (${step.agent})` : step.agent;
799
803
  lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${display} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
800
804
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
@@ -1083,7 +1087,11 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
1083
1087
  return statJoin(theme, [
1084
1088
  step.turnCount !== undefined ? `${step.turnCount} turns` : "",
1085
1089
  step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
1086
- step.tokens?.total ? formatTokenUsage(step.tokens, "token") : "",
1090
+ step.tokens
1091
+ ? step.contextLimit !== undefined
1092
+ ? formatContextUsage(step.tokens, step.contextLimit) ?? formatTokenUsage(step.tokens, "token")
1093
+ : step.tokens.total ? formatTokenUsage(step.tokens, "token") : ""
1094
+ : "",
1087
1095
  step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
1088
1096
  ]);
1089
1097
  }
@@ -654,6 +654,7 @@ parentPort.on("message", async (message) => {
654
654
  export interface WorkflowScriptChildResult {
655
655
  key: string;
656
656
  ok: boolean;
657
+ terminalOutcome?: import("../shared/types.ts").WorkflowTerminalOutcome;
657
658
  stopped?: boolean;
658
659
  /** Canonical child agent name when launch resolution produced one. */
659
660
  agent?: string;
@@ -666,6 +667,7 @@ export interface WorkflowScriptChildResult {
666
667
  requestedContext?: "fresh" | "fork";
667
668
  resolvedContext?: "fresh" | "fork" | "mixed";
668
669
  outputReference?: string;
670
+ outputPathMapping?: { requestedPath: string; savedPath: string };
669
671
  externalAdapter?: import("../shared/types.ts").ExternalCliReceiptMetadata;
670
672
  resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
671
673
  continuation?: { runIds: string[] };
@@ -722,9 +724,9 @@ export interface WorkflowScriptResult {
722
724
 
723
725
  export class WorkflowScriptError extends Error {
724
726
  readonly partial: Omit<WorkflowScriptResult, "value">;
725
- readonly errorKind?: "detached-child";
727
+ readonly errorKind?: "detached-child" | "timeout";
726
728
 
727
- constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child") {
729
+ constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child" | "timeout") {
728
730
  super(message);
729
731
  this.name = "WorkflowScriptError";
730
732
  this.partial = partial;
@@ -1109,6 +1111,41 @@ function resolveWorkflowParserEntry(): string {
1109
1111
  }
1110
1112
  }
1111
1113
 
1114
+ const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "turnBudget", "worktree"] as const;
1115
+
1116
+ function isZeroUsage(usage: unknown): boolean {
1117
+ if (!isRecord(usage)) return false;
1118
+ const cost = usage.cost;
1119
+ return (usage.input ?? 0) === 0
1120
+ && (usage.output ?? 0) === 0
1121
+ && (usage.cacheRead ?? 0) === 0
1122
+ && (usage.cacheWrite ?? 0) === 0
1123
+ && (!isRecord(cost) || (cost.total ?? 0) === 0);
1124
+ }
1125
+
1126
+ function setupAbortResumeParams(params: Record<string, unknown>, result: WorkflowScriptChildResult, signal: AbortSignal): Record<string, unknown> | undefined {
1127
+ if (signal.aborted || result.ok || result.stopped || result.interrupted || !result.runId) return undefined;
1128
+ const childResult = Array.isArray(result.results) && result.results.length === 1 && isRecord(result.results[0]) ? result.results[0] : undefined;
1129
+ const error = typeof childResult?.error === "string" ? childResult.error : result.error;
1130
+ if (error !== "This operation was aborted" || !isZeroUsage(childResult?.usage)) return undefined;
1131
+ const messages = Array.isArray(childResult?.messages) ? childResult.messages : [];
1132
+ const message = messages.findLast((entry) => isRecord(entry) && entry.role === "assistant");
1133
+ if (message !== undefined) {
1134
+ if (!isRecord(message)) return undefined;
1135
+ if (message.stopReason !== "error" || message.errorMessage !== error) return undefined;
1136
+ if (!Array.isArray(message.content) || message.content.length > 0 || !isZeroUsage(message.usage)) return undefined;
1137
+ if (Object.hasOwn(message, "diagnostics") || Object.hasOwn(message, "responseId")) return undefined;
1138
+ }
1139
+ const task = typeof params.task === "string" && params.task.trim() ? params.task.trim() : "Continue after the setup abort.";
1140
+ const resumeParams: Record<string, unknown> = { resume: result.runId, task };
1141
+ for (const key of AUTO_RESUME_PARAM_KEYS) {
1142
+ if (Object.hasOwn(params, key)) resumeParams[key] = params[key];
1143
+ }
1144
+ resumeParams.resume = result.runId;
1145
+ resumeParams.task = task;
1146
+ return resumeParams;
1147
+ }
1148
+
1112
1149
  export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
1113
1150
  if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
1114
1151
  if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
@@ -1194,7 +1231,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1194
1231
  return unobservedSteers.length > 0 ? new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
1195
1232
  })()
1196
1233
  : undefined;
1197
- if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" ? "detached-child" : undefined));
1234
+ if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" || outcome.error.workflowErrorKind === "timeout" ? outcome.error.workflowErrorKind : undefined));
1198
1235
  else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
1199
1236
  else resolve({ value: outcome.value, ...partial() });
1200
1237
  });
@@ -1225,7 +1262,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1225
1262
  };
1226
1263
  const timer = options.timeoutMs === undefined
1227
1264
  ? undefined
1228
- : setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
1265
+ : setTimeout(() => {
1266
+ const error = new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) as Error & { workflowErrorKind: "timeout" };
1267
+ error.workflowErrorKind = "timeout";
1268
+ finish({ error });
1269
+ }, options.timeoutMs);
1229
1270
  options.signal?.addEventListener("abort", onAbort, { once: true });
1230
1271
  if (options.signal?.aborted) return onAbort();
1231
1272
 
@@ -1487,7 +1528,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1487
1528
  if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
1488
1529
  }
1489
1530
  const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
1490
- return options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
1531
+ const result = await options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
1532
+ const autoResumeParams = setupAbortResumeParams(params, result, childSignal);
1533
+ if (!autoResumeParams) return result;
1534
+ resolvedResumeLineage = [...new Set([...(resolvedResumeLineage ?? []), result.runId!])];
1535
+ trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(autoResumeParams), phase: "auto-resume", runId: result.runId });
1536
+ traceChanged();
1537
+ return options.launch(key, autoResumeParams, childSignal, { admitted: true, batch: batch !== undefined });
1491
1538
  }).then((result) => {
1492
1539
  let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
1493
1540
  if (resolvedResumeLineage?.length && normalized.runId) {
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
4
- import type { ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState } from "../shared/types.ts";
4
+ import type { ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
5
5
  import type { WorkflowReceiptResumeReference, WorkflowScriptChildResult } from "./scripted-workflow.ts";
6
6
  import { parseWorkflowChildSummary } from "./workflow-child-summary.ts";
7
7
 
@@ -34,6 +34,7 @@ export function buildWorkflowReceipt(input: {
34
34
  state: WorkflowReceiptState;
35
35
  children: WorkflowScriptChildResult[];
36
36
  workflowChildren?: WorkflowReceipt["workflowChildren"];
37
+ terminalOutcome?: WorkflowTerminalOutcome;
37
38
  createdAt?: number;
38
39
  }): WorkflowReceipt {
39
40
  const workflowRunId = assertSafeRunId(input.workflowRunId, "workflowRunId");
@@ -48,6 +49,7 @@ export function buildWorkflowReceipt(input: {
48
49
  if (resumability.state === "resumable" && !latestRunId) throw new Error(`Workflow receipt child '${key}' is resumable but has no retained run id.`);
49
50
  const base = {
50
51
  key,
52
+ ...(child.terminalOutcome ? { terminalOutcome: child.terminalOutcome } : {}),
51
53
  ...(child.agent ? { agent: child.agent } : {}),
52
54
  ...(child.requestedContext ? { requestedContext: child.requestedContext } : {}),
53
55
  ...(child.resolvedContext ? { resolvedContext: child.resolvedContext } : {}),
@@ -59,7 +61,7 @@ export function buildWorkflowReceipt(input: {
59
61
  ? { ...base, latestRunId: latestRunId!, resumability }
60
62
  : { ...base, ...(latestRunId ? { latestRunId } : {}), resumability };
61
63
  }
62
- return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}) };
64
+ return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}), ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}) };
63
65
  }
64
66
 
65
67
  export function writeWorkflowReceipt(asyncDir: string, receipt: WorkflowReceipt): string {
@@ -170,6 +172,14 @@ function parseExternalCliReceiptMetadata(value: unknown, key: string, source: st
170
172
  return value as ExternalCliReceiptMetadata;
171
173
  }
172
174
 
175
+ function parseTerminalOutcome(value: unknown, label: string): WorkflowTerminalOutcome | undefined {
176
+ if (value === undefined) return undefined;
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
178
+ const outcome = value as Record<string, unknown>;
179
+ if (outcome.state !== "partial" || (outcome.reason !== "budget_exhausted" && outcome.reason !== "timeout")) throw new Error(`${label} is invalid.`);
180
+ return { state: "partial", reason: outcome.reason };
181
+ }
182
+
173
183
  function parseEntry(value: unknown, key: string, source: string): WorkflowReceiptEntry {
174
184
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' must be an object.`);
175
185
  const entry = value as Record<string, unknown>;
@@ -190,8 +200,30 @@ function parseEntry(value: unknown, key: string, source: string): WorkflowReceip
190
200
  const reason = (resumability as Record<string, unknown>).reason;
191
201
  if (state === "resumable" && latestRunId === undefined) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' resumable entry has no retained run id.`);
192
202
  if (state === "not-resumable" && (typeof reason !== "string" || !reason.trim())) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' non-resumable reason is missing.`);
203
+ const terminalOutcome = parseTerminalOutcome(entry.terminalOutcome, `Invalid workflow receipt '${source}': entry '${key}' terminalOutcome`);
193
204
  parseExternalCliReceiptMetadata(entry.externalAdapter, key, source);
194
- return value as WorkflowReceiptEntry;
205
+ return { ...(value as WorkflowReceiptEntry), ...(terminalOutcome ? { terminalOutcome } : {}) };
206
+ }
207
+
208
+ function parseWorkflowResolution(value: unknown, source: string): WorkflowTerminalResolution | undefined {
209
+ if (value === undefined) return undefined;
210
+ if (value !== "settled-awaiting-resume" && value !== "failed-child" && value !== "interrupted-child") throw new Error(`Invalid workflow receipt '${source}': workflowResolution is invalid.`);
211
+ return value;
212
+ }
213
+
214
+ function parseRecovery(value: unknown, workflowRunId: string, entries: Record<string, WorkflowReceiptEntry>, source: string): WorkflowRecoveryAction[] | undefined {
215
+ if (value === undefined) return undefined;
216
+ if (!Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': recovery must be an array.`);
217
+ return value.map((item, index) => {
218
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error(`Invalid workflow receipt '${source}': recovery[${index}] must be an object.`);
219
+ const action = item as Record<string, unknown>;
220
+ const key = action.key;
221
+ const resume = action.resume;
222
+ if (typeof key !== "string" || !entries[key] || action.call !== "runs.run" || action.taskRequired !== true || !resume || typeof resume !== "object" || Array.isArray(resume)) throw new Error(`Invalid workflow receipt '${source}': recovery[${index}] is invalid.`);
223
+ const reference = resume as Record<string, unknown>;
224
+ if (reference.workflowRunId !== workflowRunId || reference.key !== key || reference.latest !== true || entries[key].resumability.state !== "resumable") throw new Error(`Invalid workflow receipt '${source}': recovery[${index}] does not identify a resumable entry.`);
225
+ return { key, call: "runs.run", resume: { workflowRunId, key, latest: true }, taskRequired: true };
226
+ });
195
227
  }
196
228
 
197
229
  export function readWorkflowReceipt(asyncDirRoot: string, workflowRunId: string): WorkflowReceipt {
@@ -222,7 +254,10 @@ export function readWorkflowReceipt(asyncDirRoot: string, workflowRunId: string)
222
254
  for (const [key, entry] of Object.entries(receipt.entries as Record<string, unknown>)) entries[assertKey(key, "workflow receipt key")] = parseEntry(entry, key, receiptPath);
223
255
  const workflowChildren = parseWorkflowChildSummary(receipt.workflowChildren);
224
256
  if (workflowChildren && workflowChildren.workflowRunId !== workflowRunId) throw new Error(`Workflow receipt '${receiptPath}' is stale: workflowChildren.workflowRunId does not match.`);
225
- return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(workflowChildren ? { workflowChildren } : {}) };
257
+ const workflowResolution = parseWorkflowResolution(receipt.workflowResolution, receiptPath);
258
+ const terminalOutcome = parseTerminalOutcome(receipt.terminalOutcome, `Invalid workflow receipt '${receiptPath}': terminalOutcome`);
259
+ const recovery = parseRecovery(receipt.recovery, workflowRunId, entries, receiptPath);
260
+ return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(workflowChildren ? { workflowChildren } : {}), ...(workflowResolution ? { workflowResolution } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(recovery ? { recovery } : {}) };
226
261
  }
227
262
 
228
263
  export function resolveWorkflowReceiptResumeEntry(input: {