@plateforme-ai/lobster 2026.6.12

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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/VISION.md +249 -0
  4. package/bin/clawd.invoke.js +18 -0
  5. package/bin/lobster.js +24 -0
  6. package/bin/openclaw.invoke.js +21 -0
  7. package/dist/src/cli.js +793 -0
  8. package/dist/src/cli.js.map +1 -0
  9. package/dist/src/commands/commands_list.js +49 -0
  10. package/dist/src/commands/commands_list.js.map +1 -0
  11. package/dist/src/commands/registry.js +66 -0
  12. package/dist/src/commands/registry.js.map +1 -0
  13. package/dist/src/commands/stdlib/approve.js +77 -0
  14. package/dist/src/commands/stdlib/approve.js.map +1 -0
  15. package/dist/src/commands/stdlib/ask.js +171 -0
  16. package/dist/src/commands/stdlib/ask.js.map +1 -0
  17. package/dist/src/commands/stdlib/dedupe.js +55 -0
  18. package/dist/src/commands/stdlib/dedupe.js.map +1 -0
  19. package/dist/src/commands/stdlib/diff_last.js +35 -0
  20. package/dist/src/commands/stdlib/diff_last.js.map +1 -0
  21. package/dist/src/commands/stdlib/email_triage.js +279 -0
  22. package/dist/src/commands/stdlib/email_triage.js.map +1 -0
  23. package/dist/src/commands/stdlib/exec.js +130 -0
  24. package/dist/src/commands/stdlib/exec.js.map +1 -0
  25. package/dist/src/commands/stdlib/gog_gmail_search.js +94 -0
  26. package/dist/src/commands/stdlib/gog_gmail_search.js.map +1 -0
  27. package/dist/src/commands/stdlib/gog_gmail_send.js +104 -0
  28. package/dist/src/commands/stdlib/gog_gmail_send.js.map +1 -0
  29. package/dist/src/commands/stdlib/group_by.js +59 -0
  30. package/dist/src/commands/stdlib/group_by.js.map +1 -0
  31. package/dist/src/commands/stdlib/head.js +34 -0
  32. package/dist/src/commands/stdlib/head.js.map +1 -0
  33. package/dist/src/commands/stdlib/json.js +20 -0
  34. package/dist/src/commands/stdlib/json.js.map +1 -0
  35. package/dist/src/commands/stdlib/llm_invoke.js +758 -0
  36. package/dist/src/commands/stdlib/llm_invoke.js.map +1 -0
  37. package/dist/src/commands/stdlib/llm_task_invoke.js +2 -0
  38. package/dist/src/commands/stdlib/llm_task_invoke.js.map +1 -0
  39. package/dist/src/commands/stdlib/map.js +104 -0
  40. package/dist/src/commands/stdlib/map.js.map +1 -0
  41. package/dist/src/commands/stdlib/openclaw_invoke.js +136 -0
  42. package/dist/src/commands/stdlib/openclaw_invoke.js.map +1 -0
  43. package/dist/src/commands/stdlib/pick.js +45 -0
  44. package/dist/src/commands/stdlib/pick.js.map +1 -0
  45. package/dist/src/commands/stdlib/sort.js +86 -0
  46. package/dist/src/commands/stdlib/sort.js.map +1 -0
  47. package/dist/src/commands/stdlib/state.js +76 -0
  48. package/dist/src/commands/stdlib/state.js.map +1 -0
  49. package/dist/src/commands/stdlib/table.js +57 -0
  50. package/dist/src/commands/stdlib/table.js.map +1 -0
  51. package/dist/src/commands/stdlib/template.js +126 -0
  52. package/dist/src/commands/stdlib/template.js.map +1 -0
  53. package/dist/src/commands/stdlib/where.js +81 -0
  54. package/dist/src/commands/stdlib/where.js.map +1 -0
  55. package/dist/src/commands/types.js +2 -0
  56. package/dist/src/commands/types.js.map +1 -0
  57. package/dist/src/commands/workflows/workflows_list.js +24 -0
  58. package/dist/src/commands/workflows/workflows_list.js.map +1 -0
  59. package/dist/src/commands/workflows/workflows_run.js +74 -0
  60. package/dist/src/commands/workflows/workflows_run.js.map +1 -0
  61. package/dist/src/core/cost_tracker.js +119 -0
  62. package/dist/src/core/cost_tracker.js.map +1 -0
  63. package/dist/src/core/filters.js +102 -0
  64. package/dist/src/core/filters.js.map +1 -0
  65. package/dist/src/core/index.js +7 -0
  66. package/dist/src/core/index.js.map +1 -0
  67. package/dist/src/core/retry.js +89 -0
  68. package/dist/src/core/retry.js.map +1 -0
  69. package/dist/src/core/tool_runtime.js +289 -0
  70. package/dist/src/core/tool_runtime.js.map +1 -0
  71. package/dist/src/input_request.js +430 -0
  72. package/dist/src/input_request.js.map +1 -0
  73. package/dist/src/parser.js +145 -0
  74. package/dist/src/parser.js.map +1 -0
  75. package/dist/src/pipeline_resume_state.js +186 -0
  76. package/dist/src/pipeline_resume_state.js.map +1 -0
  77. package/dist/src/read_line.js +50 -0
  78. package/dist/src/read_line.js.map +1 -0
  79. package/dist/src/recipes/github/index.js +16 -0
  80. package/dist/src/recipes/github/index.js.map +1 -0
  81. package/dist/src/recipes/github/pr-monitor.js +248 -0
  82. package/dist/src/recipes/github/pr-monitor.js.map +1 -0
  83. package/dist/src/recipes/github/stages/pr-view.js +107 -0
  84. package/dist/src/recipes/github/stages/pr-view.js.map +1 -0
  85. package/dist/src/recipes/index.js +7 -0
  86. package/dist/src/recipes/index.js.map +1 -0
  87. package/dist/src/recipes/registry.js +30 -0
  88. package/dist/src/recipes/registry.js.map +1 -0
  89. package/dist/src/renderers/json.js +13 -0
  90. package/dist/src/renderers/json.js.map +1 -0
  91. package/dist/src/resume.js +179 -0
  92. package/dist/src/resume.js.map +1 -0
  93. package/dist/src/runtime.js +230 -0
  94. package/dist/src/runtime.js.map +1 -0
  95. package/dist/src/sdk/Lobster.js +402 -0
  96. package/dist/src/sdk/Lobster.js.map +1 -0
  97. package/dist/src/sdk/index.js +25 -0
  98. package/dist/src/sdk/index.js.map +1 -0
  99. package/dist/src/sdk/primitives/approve.js +47 -0
  100. package/dist/src/sdk/primitives/approve.js.map +1 -0
  101. package/dist/src/sdk/primitives/diff.js +156 -0
  102. package/dist/src/sdk/primitives/diff.js.map +1 -0
  103. package/dist/src/sdk/primitives/exec.js +167 -0
  104. package/dist/src/sdk/primitives/exec.js.map +1 -0
  105. package/dist/src/sdk/primitives/state.js +203 -0
  106. package/dist/src/sdk/primitives/state.js.map +1 -0
  107. package/dist/src/sdk/runtime.js +131 -0
  108. package/dist/src/sdk/runtime.js.map +1 -0
  109. package/dist/src/sdk/token.js +9 -0
  110. package/dist/src/sdk/token.js.map +1 -0
  111. package/dist/src/shell.js +39 -0
  112. package/dist/src/shell.js.map +1 -0
  113. package/dist/src/state/store.js +337 -0
  114. package/dist/src/state/store.js.map +1 -0
  115. package/dist/src/token.js +15 -0
  116. package/dist/src/token.js.map +1 -0
  117. package/dist/src/validation.js +38 -0
  118. package/dist/src/validation.js.map +1 -0
  119. package/dist/src/workflows/file.js +2405 -0
  120. package/dist/src/workflows/file.js.map +1 -0
  121. package/dist/src/workflows/github_pr_monitor.js +167 -0
  122. package/dist/src/workflows/github_pr_monitor.js.map +1 -0
  123. package/dist/src/workflows/graph.js +234 -0
  124. package/dist/src/workflows/graph.js.map +1 -0
  125. package/dist/src/workflows/registry.js +57 -0
  126. package/dist/src/workflows/registry.js.map +1 -0
  127. package/dist/test/approval_id.test.js +171 -0
  128. package/dist/test/approval_id.test.js.map +1 -0
  129. package/dist/test/approve_preview.test.js +38 -0
  130. package/dist/test/approve_preview.test.js.map +1 -0
  131. package/dist/test/clawd_invoke.test.js +124 -0
  132. package/dist/test/clawd_invoke.test.js.map +1 -0
  133. package/dist/test/clawd_invoke_legacy.test.js +63 -0
  134. package/dist/test/clawd_invoke_legacy.test.js.map +1 -0
  135. package/dist/test/cli_run_file_args_json.test.js +27 -0
  136. package/dist/test/cli_run_file_args_json.test.js.map +1 -0
  137. package/dist/test/commands_list.test.js +44 -0
  138. package/dist/test/commands_list.test.js.map +1 -0
  139. package/dist/test/condition_comparison.test.js +127 -0
  140. package/dist/test/condition_comparison.test.js.map +1 -0
  141. package/dist/test/core_tool_runtime.test.js +160 -0
  142. package/dist/test/core_tool_runtime.test.js.map +1 -0
  143. package/dist/test/cost_tracker.test.js +231 -0
  144. package/dist/test/cost_tracker.test.js.map +1 -0
  145. package/dist/test/dedupe.test.js +48 -0
  146. package/dist/test/dedupe.test.js.map +1 -0
  147. package/dist/test/diff_last.test.js +70 -0
  148. package/dist/test/diff_last.test.js.map +1 -0
  149. package/dist/test/doctor.test.js +19 -0
  150. package/dist/test/doctor.test.js.map +1 -0
  151. package/dist/test/dry_run.test.js +502 -0
  152. package/dist/test/dry_run.test.js.map +1 -0
  153. package/dist/test/email_triage.test.js +296 -0
  154. package/dist/test/email_triage.test.js.map +1 -0
  155. package/dist/test/exec_stdin.test.js +43 -0
  156. package/dist/test/exec_stdin.test.js.map +1 -0
  157. package/dist/test/for_each.test.js +228 -0
  158. package/dist/test/for_each.test.js.map +1 -0
  159. package/dist/test/github_pr_notify_format.test.js +19 -0
  160. package/dist/test/github_pr_notify_format.test.js.map +1 -0
  161. package/dist/test/github_pr_summary.test.js +41 -0
  162. package/dist/test/github_pr_summary.test.js.map +1 -0
  163. package/dist/test/group_by.test.js +43 -0
  164. package/dist/test/group_by.test.js.map +1 -0
  165. package/dist/test/llm_invoke.test.js +166 -0
  166. package/dist/test/llm_invoke.test.js.map +1 -0
  167. package/dist/test/llm_task_invoke.test.js +416 -0
  168. package/dist/test/llm_task_invoke.test.js.map +1 -0
  169. package/dist/test/map.test.js +41 -0
  170. package/dist/test/map.test.js.map +1 -0
  171. package/dist/test/multi_approval_resume.test.js +48 -0
  172. package/dist/test/multi_approval_resume.test.js.map +1 -0
  173. package/dist/test/on_error.test.js +151 -0
  174. package/dist/test/on_error.test.js.map +1 -0
  175. package/dist/test/openclaw_invoke_alias.test.js +13 -0
  176. package/dist/test/openclaw_invoke_alias.test.js.map +1 -0
  177. package/dist/test/parallel.test.js +184 -0
  178. package/dist/test/parallel.test.js.map +1 -0
  179. package/dist/test/parser.test.js +39 -0
  180. package/dist/test/parser.test.js.map +1 -0
  181. package/dist/test/read_line.test.js +25 -0
  182. package/dist/test/read_line.test.js.map +1 -0
  183. package/dist/test/request_input.test.js +946 -0
  184. package/dist/test/request_input.test.js.map +1 -0
  185. package/dist/test/resume.test.js +82 -0
  186. package/dist/test/resume.test.js.map +1 -0
  187. package/dist/test/sdk_lobster.test.js +177 -0
  188. package/dist/test/sdk_lobster.test.js.map +1 -0
  189. package/dist/test/shell.test.js +31 -0
  190. package/dist/test/shell.test.js.map +1 -0
  191. package/dist/test/sort.test.js +51 -0
  192. package/dist/test/sort.test.js.map +1 -0
  193. package/dist/test/state.test.js +336 -0
  194. package/dist/test/state.test.js.map +1 -0
  195. package/dist/test/step_retry.test.js +254 -0
  196. package/dist/test/step_retry.test.js.map +1 -0
  197. package/dist/test/step_timeout.test.js +154 -0
  198. package/dist/test/step_timeout.test.js.map +1 -0
  199. package/dist/test/template.test.js +46 -0
  200. package/dist/test/template.test.js.map +1 -0
  201. package/dist/test/template_filters.test.js +107 -0
  202. package/dist/test/template_filters.test.js.map +1 -0
  203. package/dist/test/tool_envelope_version.test.js +15 -0
  204. package/dist/test/tool_envelope_version.test.js.map +1 -0
  205. package/dist/test/tool_mode.test.js +83 -0
  206. package/dist/test/tool_mode.test.js.map +1 -0
  207. package/dist/test/validation.test.js +28 -0
  208. package/dist/test/validation.test.js.map +1 -0
  209. package/dist/test/workflow_args_env.test.js +41 -0
  210. package/dist/test/workflow_args_env.test.js.map +1 -0
  211. package/dist/test/workflow_composition.test.js +238 -0
  212. package/dist/test/workflow_composition.test.js.map +1 -0
  213. package/dist/test/workflow_file.test.js +1399 -0
  214. package/dist/test/workflow_file.test.js.map +1 -0
  215. package/dist/test/workflow_graph.test.js +97 -0
  216. package/dist/test/workflow_graph.test.js.map +1 -0
  217. package/dist/test/workflows.test.js +32 -0
  218. package/dist/test/workflows.test.js.map +1 -0
  219. package/package.json +75 -0
@@ -0,0 +1,2405 @@
1
+ import { promises as fsp } from "node:fs";
2
+ import path from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { randomUUID } from "node:crypto";
5
+ import { isDeepStrictEqual } from "node:util";
6
+ import { PassThrough } from "node:stream";
7
+ import { parsePipeline } from "../parser.js";
8
+ import { runPipeline } from "../runtime.js";
9
+ import { encodeToken, decodeToken } from "../token.js";
10
+ import { createApprovalIndex, deleteStateJson, readStateJson, writeStateJson, } from "../state/store.js";
11
+ import { readLineFromStream } from "../read_line.js";
12
+ import { resolveInlineShellCommand } from "../shell.js";
13
+ import { compileCached } from "../validation.js";
14
+ import { CostTracker } from "../core/cost_tracker.js";
15
+ import { withRetry, resolveRetryConfig } from "../core/retry.js";
16
+ import { RequestInputResumeError, validateCommandInputState, } from "../input_request.js";
17
+ export class WorkflowResumeArgumentError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "WorkflowResumeArgumentError";
21
+ }
22
+ }
23
+ class WorkflowPipelineInputSuspension extends Error {
24
+ stepId;
25
+ request;
26
+ pipelineInput;
27
+ constructor({ stepId, request, pipelineInput, }) {
28
+ super(`Workflow step ${stepId} pipeline requested input`);
29
+ this.name = "WorkflowPipelineInputSuspension";
30
+ this.stepId = stepId;
31
+ this.request = request;
32
+ this.pipelineInput = pipelineInput;
33
+ }
34
+ }
35
+ export async function loadWorkflowFile(filePath) {
36
+ const text = await fsp.readFile(filePath, "utf8");
37
+ const ext = path.extname(filePath).toLowerCase();
38
+ const parsed = ext === ".json" ? JSON.parse(text) : parseYaml(text);
39
+ if (!parsed || typeof parsed !== "object") {
40
+ throw new Error("Workflow file must be a JSON/YAML object");
41
+ }
42
+ const steps = parsed.steps;
43
+ if (!Array.isArray(steps) || steps.length === 0) {
44
+ throw new Error("Workflow file requires a non-empty steps array");
45
+ }
46
+ const costLimit = parsed.cost_limit;
47
+ if (costLimit !== undefined) {
48
+ if (!costLimit || typeof costLimit !== "object" || Array.isArray(costLimit)) {
49
+ throw new Error("Workflow cost_limit must be an object");
50
+ }
51
+ if (!Number.isFinite(Number(costLimit.max_usd)) || Number(costLimit.max_usd) < 0) {
52
+ throw new Error("Workflow cost_limit.max_usd must be a non-negative number");
53
+ }
54
+ if (costLimit.action !== undefined &&
55
+ costLimit.action !== "warn" &&
56
+ costLimit.action !== "stop") {
57
+ throw new Error('Workflow cost_limit.action must be "warn" or "stop"');
58
+ }
59
+ }
60
+ const seen = new Set();
61
+ for (const step of steps) {
62
+ if (!step || typeof step !== "object") {
63
+ throw new Error("Workflow step must be an object");
64
+ }
65
+ if (!step.id || typeof step.id !== "string") {
66
+ throw new Error("Workflow step requires an id");
67
+ }
68
+ if (step.workflow !== undefined && typeof step.workflow !== "string") {
69
+ throw new Error(`Workflow step ${step.id} workflow must be a string (file path)`);
70
+ }
71
+ if (typeof step.workflow === "string" && !step.workflow.trim()) {
72
+ throw new Error(`Workflow step ${step.id} workflow path cannot be blank`);
73
+ }
74
+ if (step.workflow_args !== undefined) {
75
+ if (!step.workflow_args ||
76
+ typeof step.workflow_args !== "object" ||
77
+ Array.isArray(step.workflow_args)) {
78
+ throw new Error(`Workflow step ${step.id} workflow_args must be a plain object`);
79
+ }
80
+ }
81
+ if (step.parallel !== undefined &&
82
+ (!step.parallel || typeof step.parallel !== "object" || Array.isArray(step.parallel))) {
83
+ throw new Error(`Workflow step ${step.id} parallel must be an object`);
84
+ }
85
+ const isParallel = Boolean(step.parallel && typeof step.parallel === "object" && !Array.isArray(step.parallel));
86
+ if (isParallel) {
87
+ const parallel = step.parallel;
88
+ if (!Array.isArray(parallel.branches) || parallel.branches.length === 0) {
89
+ throw new Error(`Workflow step ${step.id} parallel requires a non-empty branches array`);
90
+ }
91
+ if (parallel.wait !== undefined && parallel.wait !== "all" && parallel.wait !== "any") {
92
+ throw new Error(`Workflow step ${step.id} parallel wait must be "all" or "any"`);
93
+ }
94
+ if (parallel.timeout_ms !== undefined &&
95
+ (typeof parallel.timeout_ms !== "number" ||
96
+ !Number.isFinite(parallel.timeout_ms) ||
97
+ !Number.isInteger(parallel.timeout_ms) ||
98
+ parallel.timeout_ms < 1 ||
99
+ parallel.timeout_ms > 2_147_483_647)) {
100
+ throw new Error(`Workflow step ${step.id} parallel timeout_ms must be a positive integer between 1 and 2147483647`);
101
+ }
102
+ const branchIds = new Set();
103
+ for (const branch of parallel.branches) {
104
+ if (!branch || typeof branch !== "object") {
105
+ throw new Error(`Workflow step ${step.id} parallel branches must be objects`);
106
+ }
107
+ if (!branch.id || typeof branch.id !== "string") {
108
+ throw new Error(`Workflow step ${step.id} parallel branch requires an id`);
109
+ }
110
+ if (branch.id === step.id) {
111
+ throw new Error(`Workflow step ${step.id} parallel branch id cannot match the step id`);
112
+ }
113
+ if (branchIds.has(branch.id)) {
114
+ throw new Error(`Workflow step ${step.id} duplicate parallel branch id: ${branch.id}`);
115
+ }
116
+ if (seen.has(branch.id)) {
117
+ throw new Error(`Duplicate workflow id across steps/parallel branches: ${branch.id}`);
118
+ }
119
+ branchIds.add(branch.id);
120
+ const branchShell = typeof branch.run === "string" ? branch.run : branch.command;
121
+ const branchPipeline = typeof branch.pipeline === "string" ? branch.pipeline : undefined;
122
+ const branchExecCount = Number(Boolean(branchShell)) + Number(Boolean(branchPipeline));
123
+ if (branchExecCount === 0) {
124
+ throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} requires run, command, or pipeline`);
125
+ }
126
+ if (branchExecCount > 1) {
127
+ throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} can only define one of run, command, or pipeline`);
128
+ }
129
+ if (branch.run !== undefined && typeof branch.run !== "string") {
130
+ throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} run must be a string`);
131
+ }
132
+ if (branch.command !== undefined && typeof branch.command !== "string") {
133
+ throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} command must be a string`);
134
+ }
135
+ if (branch.pipeline !== undefined && typeof branch.pipeline !== "string") {
136
+ throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} pipeline must be a string`);
137
+ }
138
+ }
139
+ }
140
+ if (step.for_each !== undefined && typeof step.for_each !== "string") {
141
+ throw new Error(`Workflow step ${step.id} for_each must be a string (step reference expression)`);
142
+ }
143
+ const isForEach = typeof step.for_each === "string";
144
+ if (isForEach) {
145
+ if (!Array.isArray(step.steps) || step.steps.length === 0) {
146
+ throw new Error(`Workflow step ${step.id} for_each requires a non-empty steps array`);
147
+ }
148
+ if (step.batch_size !== undefined &&
149
+ (typeof step.batch_size !== "number" ||
150
+ !Number.isInteger(step.batch_size) ||
151
+ step.batch_size < 1)) {
152
+ throw new Error(`Workflow step ${step.id} batch_size must be a positive integer`);
153
+ }
154
+ if (step.pause_ms !== undefined &&
155
+ (typeof step.pause_ms !== "number" || !Number.isFinite(step.pause_ms) || step.pause_ms < 0)) {
156
+ throw new Error(`Workflow step ${step.id} pause_ms must be a finite non-negative number`);
157
+ }
158
+ if (isApprovalStep(step.approval)) {
159
+ throw new Error(`Workflow step ${step.id} for_each steps cannot define approval (use a separate step after the loop)`);
160
+ }
161
+ if (isInputStep(step.input)) {
162
+ throw new Error(`Workflow step ${step.id} for_each steps cannot define input (use a separate step after the loop)`);
163
+ }
164
+ if (step.stdin !== undefined && step.stdin !== null) {
165
+ throw new Error(`Workflow step ${step.id} for_each steps cannot define stdin (loop input comes from the for_each expression)`);
166
+ }
167
+ const loopShell = typeof step.run === "string" ? step.run : step.command;
168
+ const loopPipeline = typeof step.pipeline === "string" ? step.pipeline : undefined;
169
+ if (loopShell || loopPipeline || step.workflow || step.parallel) {
170
+ throw new Error(`Workflow step ${step.id} for_each cannot also define run, command, pipeline, workflow, or parallel`);
171
+ }
172
+ if (step.item_var !== undefined && typeof step.item_var !== "string") {
173
+ throw new Error(`Workflow step ${step.id} item_var must be a string`);
174
+ }
175
+ if (step.index_var !== undefined && typeof step.index_var !== "string") {
176
+ throw new Error(`Workflow step ${step.id} index_var must be a string`);
177
+ }
178
+ const loopItemVar = step.item_var ?? "item";
179
+ const loopIndexVar = step.index_var ?? "index";
180
+ if (loopItemVar === loopIndexVar) {
181
+ throw new Error(`Workflow step ${step.id} item_var and index_var cannot be the same`);
182
+ }
183
+ const subStepIds = new Set();
184
+ for (const sub of step.steps) {
185
+ if (!sub || typeof sub !== "object" || !sub.id || typeof sub.id !== "string") {
186
+ throw new Error(`Workflow step ${step.id} for_each sub-step requires an id`);
187
+ }
188
+ if (sub.id === loopItemVar || sub.id === loopIndexVar) {
189
+ throw new Error(`Workflow step ${step.id} for_each sub-step id '${sub.id}' conflicts with loop variable`);
190
+ }
191
+ if (subStepIds.has(sub.id)) {
192
+ throw new Error(`Workflow step ${step.id} duplicate for_each sub-step id: ${sub.id}`);
193
+ }
194
+ subStepIds.add(sub.id);
195
+ if (isApprovalStep(sub.approval) || isInputStep(sub.input)) {
196
+ throw new Error(`Workflow step ${step.id} for_each sub-steps cannot contain approval or input steps`);
197
+ }
198
+ if (sub.run !== undefined && typeof sub.run !== "string") {
199
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} run must be a string`);
200
+ }
201
+ if (sub.command !== undefined && typeof sub.command !== "string") {
202
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} command must be a string`);
203
+ }
204
+ if (sub.pipeline !== undefined && typeof sub.pipeline !== "string") {
205
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} pipeline must be a string`);
206
+ }
207
+ if (sub.workflow || sub.parallel || sub.for_each) {
208
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} cannot define workflow, parallel, or for_each`);
209
+ }
210
+ const subShell = typeof sub.run === "string" && sub.run.trim()
211
+ ? sub.run
212
+ : typeof sub.command === "string" && sub.command.trim()
213
+ ? sub.command
214
+ : undefined;
215
+ const subPipeline = typeof sub.pipeline === "string" && sub.pipeline.trim() ? sub.pipeline : undefined;
216
+ if (!subShell && !subPipeline) {
217
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} requires run, command, or pipeline`);
218
+ }
219
+ if (Number(Boolean(subShell)) + Number(Boolean(subPipeline)) > 1) {
220
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} can only define one of run, command, or pipeline`);
221
+ }
222
+ }
223
+ }
224
+ const shellCommand = typeof step.run === "string" ? step.run : step.command;
225
+ const pipeline = typeof step.pipeline === "string" ? step.pipeline : undefined;
226
+ const workflowRef = typeof step.workflow === "string" && step.workflow.trim() ? step.workflow : undefined;
227
+ const executionCount = Number(Boolean(shellCommand)) +
228
+ Number(Boolean(pipeline)) +
229
+ Number(Boolean(workflowRef)) +
230
+ Number(isParallel) +
231
+ Number(isForEach);
232
+ if (executionCount === 0 && !isApprovalStep(step.approval) && !isInputStep(step.input)) {
233
+ throw new Error(`Workflow step ${step.id} requires run, command, pipeline, workflow, parallel, for_each, approval, or input`);
234
+ }
235
+ if (executionCount > 1) {
236
+ throw new Error(`Workflow step ${step.id} can only define one of run, command, pipeline, workflow, parallel, or for_each`);
237
+ }
238
+ if (executionCount > 0 && isInputStep(step.input)) {
239
+ throw new Error(`Workflow step ${step.id} input steps cannot define run, command, pipeline, workflow, parallel, or for_each`);
240
+ }
241
+ if (isApprovalStep(step.approval) && isInputStep(step.input)) {
242
+ throw new Error(`Workflow step ${step.id} cannot define both approval and input`);
243
+ }
244
+ if (step.run !== undefined && typeof step.run !== "string") {
245
+ throw new Error(`Workflow step ${step.id} run must be a string`);
246
+ }
247
+ if (step.command !== undefined && typeof step.command !== "string") {
248
+ throw new Error(`Workflow step ${step.id} command must be a string`);
249
+ }
250
+ if (step.pipeline !== undefined && typeof step.pipeline !== "string") {
251
+ throw new Error(`Workflow step ${step.id} pipeline must be a string`);
252
+ }
253
+ if (step.input !== undefined && !isInputStep(step.input)) {
254
+ throw new Error(`Workflow step ${step.id} input must be an object`);
255
+ }
256
+ if (step.input && typeof step.input.prompt !== "string") {
257
+ throw new Error(`Workflow step ${step.id} input.prompt must be a string`);
258
+ }
259
+ if (step.input &&
260
+ (step.input.responseSchema === undefined || typeof step.input.responseSchema !== "object")) {
261
+ throw new Error(`Workflow step ${step.id} input.responseSchema must be an object`);
262
+ }
263
+ if (step.input) {
264
+ try {
265
+ compileCached(step.input.responseSchema);
266
+ }
267
+ catch (err) {
268
+ throw new Error(`Workflow step ${step.id} input.responseSchema is invalid: ${err?.message ?? String(err)}`);
269
+ }
270
+ }
271
+ if (step.approval && typeof step.approval === "object" && !Array.isArray(step.approval)) {
272
+ const approval = step.approval;
273
+ if (approval.initiated_by !== undefined && typeof approval.initiated_by !== "string") {
274
+ throw new Error(`Workflow step ${step.id} approval.initiated_by must be a string`);
275
+ }
276
+ if (approval.initiatedBy !== undefined && typeof approval.initiatedBy !== "string") {
277
+ throw new Error(`Workflow step ${step.id} approval.initiatedBy must be a string`);
278
+ }
279
+ if (approval.required_approver !== undefined &&
280
+ typeof approval.required_approver !== "string") {
281
+ throw new Error(`Workflow step ${step.id} approval.required_approver must be a string`);
282
+ }
283
+ if (approval.requiredApprover !== undefined &&
284
+ typeof approval.requiredApprover !== "string") {
285
+ throw new Error(`Workflow step ${step.id} approval.requiredApprover must be a string`);
286
+ }
287
+ if (approval.require_different_approver !== undefined &&
288
+ typeof approval.require_different_approver !== "boolean") {
289
+ throw new Error(`Workflow step ${step.id} approval.require_different_approver must be a boolean`);
290
+ }
291
+ if (approval.requireDifferentApprover !== undefined &&
292
+ typeof approval.requireDifferentApprover !== "boolean") {
293
+ throw new Error(`Workflow step ${step.id} approval.requireDifferentApprover must be a boolean`);
294
+ }
295
+ }
296
+ if (step.timeout_ms !== undefined &&
297
+ (typeof step.timeout_ms !== "number" ||
298
+ !Number.isFinite(step.timeout_ms) ||
299
+ !Number.isInteger(step.timeout_ms) ||
300
+ step.timeout_ms < 1 ||
301
+ step.timeout_ms > 2_147_483_647)) {
302
+ throw new Error(`Workflow step ${step.id} timeout_ms must be a positive integer between 1 and 2147483647`);
303
+ }
304
+ if (step.on_error !== undefined &&
305
+ step.on_error !== "stop" &&
306
+ step.on_error !== "continue" &&
307
+ step.on_error !== "skip_rest") {
308
+ throw new Error(`Workflow step ${step.id} on_error must be "stop", "continue", or "skip_rest"`);
309
+ }
310
+ if (step.retry !== undefined) {
311
+ if (!step.retry || typeof step.retry !== "object" || Array.isArray(step.retry)) {
312
+ throw new Error(`Workflow step ${step.id} retry must be an object`);
313
+ }
314
+ const r = step.retry;
315
+ if (r.max !== undefined &&
316
+ (typeof r.max !== "number" || !Number.isInteger(r.max) || r.max < 1)) {
317
+ throw new Error(`Workflow step ${step.id} retry.max must be a positive integer`);
318
+ }
319
+ if (r.backoff !== undefined && r.backoff !== "fixed" && r.backoff !== "exponential") {
320
+ throw new Error(`Workflow step ${step.id} retry.backoff must be "fixed" or "exponential"`);
321
+ }
322
+ if (r.delay_ms !== undefined &&
323
+ (typeof r.delay_ms !== "number" || !Number.isFinite(r.delay_ms) || r.delay_ms < 0)) {
324
+ throw new Error(`Workflow step ${step.id} retry.delay_ms must be a finite non-negative number`);
325
+ }
326
+ if (r.max_delay_ms !== undefined &&
327
+ (typeof r.max_delay_ms !== "number" ||
328
+ !Number.isFinite(r.max_delay_ms) ||
329
+ r.max_delay_ms < 0)) {
330
+ throw new Error(`Workflow step ${step.id} retry.max_delay_ms must be a finite non-negative number`);
331
+ }
332
+ if (r.jitter !== undefined && typeof r.jitter !== "boolean") {
333
+ throw new Error(`Workflow step ${step.id} retry.jitter must be a boolean`);
334
+ }
335
+ }
336
+ if (seen.has(step.id)) {
337
+ throw new Error(`Duplicate workflow step id: ${step.id}`);
338
+ }
339
+ if (isParallel) {
340
+ const parallel = step.parallel;
341
+ for (const branch of parallel.branches) {
342
+ seen.add(branch.id);
343
+ }
344
+ }
345
+ seen.add(step.id);
346
+ }
347
+ return parsed;
348
+ }
349
+ export function resolveWorkflowArgs(argDefs, provided) {
350
+ const resolved = {};
351
+ if (argDefs) {
352
+ for (const [key, def] of Object.entries(argDefs)) {
353
+ if (def && typeof def === "object" && "default" in def) {
354
+ resolved[key] = def.default;
355
+ }
356
+ }
357
+ }
358
+ if (provided) {
359
+ for (const [key, value] of Object.entries(provided)) {
360
+ resolved[key] = value;
361
+ }
362
+ }
363
+ return resolved;
364
+ }
365
+ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, response, cancel, }) {
366
+ const consumedResumeStateKey = resume?.stateKey && typeof resume.stateKey === "string"
367
+ ? await resolveWorkflowResumeStateKey(ctx.env, resume.stateKey)
368
+ : null;
369
+ const resumeState = resume?.stateKey
370
+ ? await loadWorkflowResumeState(ctx.env, consumedResumeStateKey ?? resume.stateKey)
371
+ : (resume ?? null);
372
+ if (resumeState?.approvalStepId && resumeState?.inputStepId) {
373
+ throw new Error("Invalid workflow resume state");
374
+ }
375
+ if (resumeState?.approvalStepId) {
376
+ if (response !== undefined) {
377
+ throw new WorkflowResumeArgumentError("Workflow resume requires --approve yes|no for approval requests");
378
+ }
379
+ if (cancel !== true && typeof approved !== "boolean") {
380
+ throw new WorkflowResumeArgumentError("Workflow resume requires --approve yes|no for approval requests");
381
+ }
382
+ if (cancel === true || approved === false) {
383
+ if (consumedResumeStateKey) {
384
+ await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
385
+ }
386
+ return { status: "cancelled", output: [] };
387
+ }
388
+ }
389
+ if (resumeState?.inputStepId && cancel === true) {
390
+ if (consumedResumeStateKey) {
391
+ await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
392
+ }
393
+ return { status: "cancelled", output: [] };
394
+ }
395
+ const resolvedFilePath = filePath ?? resumeState?.filePath;
396
+ if (!resolvedFilePath) {
397
+ throw new Error("Workflow file path required");
398
+ }
399
+ if (!ctx._activeWorkflows) {
400
+ ctx._activeWorkflows = new Set();
401
+ }
402
+ const canonicalFilePath = await fsp.realpath(resolvedFilePath);
403
+ ctx._activeWorkflows.add(canonicalFilePath);
404
+ try {
405
+ const workflow = await loadWorkflowFile(resolvedFilePath);
406
+ const resolvedArgs = resolveWorkflowArgs(workflow.args, args ?? resumeState?.args);
407
+ const steps = workflow.steps;
408
+ const stepIndexById = new Map(steps.map((step, idx) => [step.id, idx]));
409
+ const results = resumeState?.steps
410
+ ? cloneResults(resumeState.steps)
411
+ : {};
412
+ const startIndex = resumeState?.resumeAtIndex ?? 0;
413
+ if (resumeState?.approvalStepId && typeof approved === "boolean") {
414
+ const previous = results[resumeState.approvalStepId] ?? { id: resumeState.approvalStepId };
415
+ const approvedBy = String(ctx.env.LOBSTER_APPROVAL_APPROVED_BY ?? "").trim() || undefined;
416
+ if (approved === true) {
417
+ enforceApprovalIdentity({
418
+ stepId: resumeState.approvalStepId,
419
+ identity: resumeState.approvalIdentity,
420
+ approvedBy,
421
+ });
422
+ }
423
+ previous.approved = approved;
424
+ if (approvedBy)
425
+ previous.approvedBy = approvedBy;
426
+ results[resumeState.approvalStepId] = previous;
427
+ }
428
+ let resumedPipelineInput = null;
429
+ if (resumeState?.inputStepId) {
430
+ if (approved !== undefined) {
431
+ throw new WorkflowResumeArgumentError("Workflow resume requires --response-json for input requests");
432
+ }
433
+ if (response === undefined) {
434
+ throw new WorkflowResumeArgumentError("Workflow resume requires --response-json for input requests");
435
+ }
436
+ if (resumeState.inputKind === "pipeline_command") {
437
+ const resumedStepIndex = stepIndexById.get(resumeState.inputStepId);
438
+ if (resumedStepIndex !== startIndex) {
439
+ throw new RequestInputResumeError("workflow input step changed since input request");
440
+ }
441
+ const pipelineStep = steps[resumedStepIndex];
442
+ if (!pipelineStep || typeof pipelineStep.pipeline !== "string") {
443
+ throw new Error(`Invalid pipeline input step in resume state: ${resumeState.inputStepId}`);
444
+ }
445
+ if (!evaluateCondition(pipelineStep.when ?? pipelineStep.condition, results)) {
446
+ throw new RequestInputResumeError("workflow input step condition changed since input request");
447
+ }
448
+ try {
449
+ validateInputResponse({
450
+ schema: resumeState.inputSchema,
451
+ response,
452
+ stepId: pipelineStep.id,
453
+ });
454
+ }
455
+ catch (err) {
456
+ throw new WorkflowResumeArgumentError(err?.message ?? String(err));
457
+ }
458
+ resumedPipelineInput = {
459
+ stepId: resumeState.inputStepId,
460
+ response,
461
+ pipelineInput: resumeState.pipelineInput,
462
+ onConsumed: consumedResumeStateKey
463
+ ? async () => {
464
+ await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
465
+ }
466
+ : undefined,
467
+ };
468
+ }
469
+ else {
470
+ const inputStep = steps[stepIndexById.get(resumeState.inputStepId) ?? -1];
471
+ if (!inputStep || !isInputStep(inputStep.input)) {
472
+ throw new Error(`Invalid input step in resume state: ${resumeState.inputStepId}`);
473
+ }
474
+ try {
475
+ validateInputResponse({
476
+ schema: resumeState.inputSchema ?? inputStep.input.responseSchema,
477
+ response,
478
+ stepId: inputStep.id,
479
+ });
480
+ }
481
+ catch (err) {
482
+ throw new WorkflowResumeArgumentError(err?.message ?? String(err));
483
+ }
484
+ const previous = results[resumeState.inputStepId] ?? { id: resumeState.inputStepId };
485
+ previous.subject = resumeState.inputSubject ?? null;
486
+ previous.response = response;
487
+ delete previous.skipped;
488
+ results[resumeState.inputStepId] = previous;
489
+ }
490
+ }
491
+ if (ctx.dryRun) {
492
+ return dryRunWorkflow({ steps, resolvedArgs, results, startIndex, ctx });
493
+ }
494
+ const costTracker = new CostTracker(CostTracker.parsePricingFromEnv(ctx.env, ctx.stderr), ctx.stderr);
495
+ let lastStepId = resumeState?.inputStepId ?? findLastCompletedStepId(steps, results);
496
+ for (let idx = startIndex; idx < steps.length; idx++) {
497
+ const step = steps[idx];
498
+ if (!evaluateCondition(step.when ?? step.condition, results)) {
499
+ results[step.id] = { id: step.id, skipped: true };
500
+ continue;
501
+ }
502
+ if (isInputStep(step.input)) {
503
+ const subject = resolveInputSubject({
504
+ step,
505
+ args: resolvedArgs,
506
+ results,
507
+ lastStepId,
508
+ });
509
+ if (ctx.mode === "tool" || !isInteractive(ctx.stdin)) {
510
+ const inputRequest = buildNeedsInputRequest({
511
+ stepId: step.id,
512
+ prompt: step.input.prompt,
513
+ responseSchema: step.input.responseSchema,
514
+ defaults: step.input.defaults,
515
+ subject,
516
+ maxEnvelopeBytes: resolveToolEnvelopeMaxBytes(ctx.env),
517
+ });
518
+ const stateKey = await saveWorkflowResumeState(ctx.env, {
519
+ filePath: resolvedFilePath,
520
+ resumeAtIndex: idx + 1,
521
+ steps: results,
522
+ args: resolvedArgs,
523
+ inputStepId: step.id,
524
+ inputSchema: step.input.responseSchema,
525
+ // Preserve the full resolved subject for resume semantics; the tool
526
+ // envelope may contain a truncated preview to stay within size limits.
527
+ inputSubject: subject,
528
+ createdAt: new Date().toISOString(),
529
+ });
530
+ if (consumedResumeStateKey && consumedResumeStateKey !== stateKey) {
531
+ await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
532
+ }
533
+ const resumeToken = encodeToken({
534
+ protocolVersion: 1,
535
+ v: 1,
536
+ kind: "workflow-file",
537
+ stateKey,
538
+ });
539
+ return {
540
+ status: "needs_input",
541
+ output: [],
542
+ requiresInput: {
543
+ ...inputRequest,
544
+ resumeToken,
545
+ },
546
+ };
547
+ }
548
+ ctx.stdout.write(`${step.input.prompt}\n`);
549
+ ctx.stdout.write("Enter JSON response: ");
550
+ const raw = await readLineFromStream(ctx.stdin, {
551
+ timeoutMs: parseApprovalTimeoutMs(ctx.env),
552
+ });
553
+ const parsed = parseResponseJson(String(raw ?? "").trim());
554
+ validateInputResponse({
555
+ schema: step.input.responseSchema,
556
+ response: parsed,
557
+ stepId: step.id,
558
+ });
559
+ results[step.id] = {
560
+ id: step.id,
561
+ subject,
562
+ response: parsed,
563
+ };
564
+ lastStepId = step.id;
565
+ continue;
566
+ }
567
+ if (typeof step.for_each === "string" && Array.isArray(step.steps)) {
568
+ const itemsRef = resolveInputValue(step.for_each, resolvedArgs, results);
569
+ if (!Array.isArray(itemsRef)) {
570
+ throw new Error(`Workflow step ${step.id} for_each: expected array, got ${typeof itemsRef}`);
571
+ }
572
+ const itemVar = step.item_var ?? "item";
573
+ const indexVar = step.index_var ?? "index";
574
+ const batchSize = step.batch_size ?? 1;
575
+ const iterationResults = [];
576
+ for (let itemIdx = 0; itemIdx < itemsRef.length; itemIdx++) {
577
+ if (step.pause_ms && itemIdx > 0 && itemIdx % batchSize === 0) {
578
+ await abortableSleep(step.pause_ms, ctx.signal);
579
+ }
580
+ const item = itemsRef[itemIdx];
581
+ const scopedResults = { ...results };
582
+ scopedResults[itemVar] = {
583
+ id: itemVar,
584
+ json: item,
585
+ stdout: typeof item === "string" ? item : JSON.stringify(item),
586
+ };
587
+ scopedResults[indexVar] = {
588
+ id: indexVar,
589
+ json: itemIdx,
590
+ stdout: String(itemIdx),
591
+ };
592
+ for (const subStep of step.steps) {
593
+ if (!evaluateCondition(subStep.when ?? subStep.condition, scopedResults)) {
594
+ scopedResults[subStep.id] = { id: subStep.id, skipped: true };
595
+ continue;
596
+ }
597
+ const loopEnvBase = mergeEnv(ctx.env, workflow.env, step.env, resolvedArgs, scopedResults);
598
+ const subEnv = subStep.env
599
+ ? mergeEnv(loopEnvBase, undefined, subStep.env, resolvedArgs, scopedResults)
600
+ : loopEnvBase;
601
+ const subCwd = resolveCwd(subStep.cwd ?? step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
602
+ const subExecution = getStepExecution(subStep);
603
+ let subResult;
604
+ if (subExecution.kind === "shell") {
605
+ const command = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
606
+ const stdinValue = resolveShellStdin(subStep.stdin, resolvedArgs, scopedResults);
607
+ const { stdout } = await runShellCommand({
608
+ command,
609
+ stdin: stdinValue,
610
+ env: subEnv,
611
+ cwd: subCwd,
612
+ signal: ctx.signal,
613
+ });
614
+ subResult = { id: subStep.id, stdout, json: parseJson(stdout) };
615
+ }
616
+ else if (subExecution.kind === "pipeline") {
617
+ if (!ctx.registry) {
618
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${subStep.id} requires a command registry for pipeline execution`);
619
+ }
620
+ const pipelineText = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
621
+ const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
622
+ subResult = await runPipelineStep({
623
+ stepId: subStep.id,
624
+ pipelineText,
625
+ inputValue,
626
+ ctx,
627
+ env: subEnv,
628
+ cwd: subCwd,
629
+ requestInputEnabled: false,
630
+ });
631
+ }
632
+ else {
633
+ const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
634
+ subResult = createSyntheticStepResult(subStep.id, inputValue);
635
+ }
636
+ scopedResults[subStep.id] = subResult;
637
+ trackStepCost(costTracker, `${step.id}.${subStep.id}`, subResult);
638
+ if (workflow.cost_limit) {
639
+ costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
640
+ }
641
+ }
642
+ const iterResult = { [itemVar]: item, [indexVar]: itemIdx };
643
+ for (const subStep of step.steps) {
644
+ const subResult = scopedResults[subStep.id];
645
+ if (subResult && !subResult.skipped) {
646
+ iterResult[subStep.id] =
647
+ subResult.json !== undefined ? subResult.json : subResult.stdout;
648
+ }
649
+ }
650
+ iterationResults.push(iterResult);
651
+ }
652
+ const loopResult = {
653
+ id: step.id,
654
+ json: iterationResults,
655
+ stdout: JSON.stringify(iterationResults),
656
+ };
657
+ results[step.id] = loopResult;
658
+ lastStepId = step.id;
659
+ trackStepCost(costTracker, step.id, loopResult);
660
+ if (workflow.cost_limit) {
661
+ costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
662
+ }
663
+ continue;
664
+ }
665
+ const env = mergeEnv(ctx.env, workflow.env, step.env, resolvedArgs, results);
666
+ const cwd = resolveCwd(step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
667
+ const execution = getStepExecution(step);
668
+ const retryConfig = resolveRetryConfig(step.retry);
669
+ const executeStepAttempt = async () => {
670
+ // Combine external cancellation and optional per-step timeout into one signal.
671
+ let stepSignal = ctx.signal;
672
+ let timeoutId;
673
+ if (step.timeout_ms) {
674
+ const timeoutController = new AbortController();
675
+ timeoutId = setTimeout(() => timeoutController.abort(new Error(`Step '${step.id}' timed out after ${step.timeout_ms}ms`)), step.timeout_ms);
676
+ stepSignal = ctx.signal
677
+ ? AbortSignal.any([ctx.signal, timeoutController.signal])
678
+ : timeoutController.signal;
679
+ }
680
+ let result;
681
+ let parallelBranchResults = null;
682
+ try {
683
+ if (execution.kind === "parallel") {
684
+ const parallel = execution.value;
685
+ const wait = parallel.wait ?? "all";
686
+ const branchAbortController = new AbortController();
687
+ const branchSignal = stepSignal
688
+ ? AbortSignal.any([stepSignal, branchAbortController.signal])
689
+ : branchAbortController.signal;
690
+ const shouldForceKill = Boolean(step.timeout_ms || parallel.timeout_ms);
691
+ const runBranch = async (branch) => {
692
+ const mergedBranchEnv = { ...(step.env ?? {}), ...(branch.env ?? {}) };
693
+ const branchEnv = mergeEnv(ctx.env, workflow.env, mergedBranchEnv, resolvedArgs, results);
694
+ const branchCwd = resolveCwd(branch.cwd ?? step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
695
+ const branchShell = typeof branch.run === "string" ? branch.run : branch.command;
696
+ const branchExec = typeof branch.pipeline === "string" && branch.pipeline.trim()
697
+ ? { kind: "pipeline", value: branch.pipeline }
698
+ : typeof branchShell === "string" && branchShell.trim()
699
+ ? { kind: "shell", value: branchShell }
700
+ : { kind: "none" };
701
+ if (branchExec.kind === "shell") {
702
+ const command = resolveTemplate(branchExec.value, resolvedArgs, results);
703
+ const stdinValue = resolveShellStdin(branch.stdin, resolvedArgs, results);
704
+ const { stdout } = await runShellCommand({
705
+ command,
706
+ stdin: stdinValue,
707
+ env: branchEnv,
708
+ cwd: branchCwd,
709
+ signal: branchSignal,
710
+ ...(shouldForceKill ? { killSignal: "SIGKILL" } : {}),
711
+ });
712
+ return {
713
+ branchId: branch.id,
714
+ result: { id: branch.id, stdout, json: parseJson(stdout) },
715
+ };
716
+ }
717
+ if (branchExec.kind === "pipeline") {
718
+ if (!ctx.registry) {
719
+ throw new Error(`Parallel branch ${branch.id} requires a command registry for pipeline execution`);
720
+ }
721
+ const pipelineText = resolveTemplate(branchExec.value, resolvedArgs, results);
722
+ const inputValue = resolveInputValue(branch.stdin, resolvedArgs, results);
723
+ const branchResult = await runPipelineStep({
724
+ stepId: branch.id,
725
+ pipelineText,
726
+ inputValue,
727
+ ctx: { ...ctx, signal: branchSignal },
728
+ env: branchEnv,
729
+ cwd: branchCwd,
730
+ requestInputEnabled: false,
731
+ });
732
+ return { branchId: branch.id, result: branchResult };
733
+ }
734
+ return { branchId: branch.id, result: { id: branch.id } };
735
+ };
736
+ let parallelTimeoutId;
737
+ const timeoutPromise = parallel.timeout_ms
738
+ ? new Promise((_resolve, reject) => {
739
+ parallelTimeoutId = setTimeout(() => {
740
+ branchAbortController.abort();
741
+ reject(new Error(`Parallel step ${step.id} timed out after ${parallel.timeout_ms}ms`));
742
+ }, parallel.timeout_ms);
743
+ })
744
+ : null;
745
+ try {
746
+ if (wait === "any") {
747
+ const branchPromises = parallel.branches.map((branch) => runBranch(branch));
748
+ const winner = (await (timeoutPromise
749
+ ? Promise.race([...branchPromises, timeoutPromise])
750
+ : Promise.race(branchPromises)));
751
+ parallelBranchResults = { [winner.branchId]: winner.result };
752
+ branchAbortController.abort();
753
+ }
754
+ else {
755
+ const branchPromises = parallel.branches.map((branch) => runBranch(branch));
756
+ const settled = (await (timeoutPromise
757
+ ? Promise.race([Promise.allSettled(branchPromises), timeoutPromise])
758
+ : Promise.allSettled(branchPromises)));
759
+ parallelBranchResults = {};
760
+ for (const entry of settled) {
761
+ if (entry.status === "rejected") {
762
+ throw new Error(`Parallel branch failed: ${entry.reason?.message ?? String(entry.reason)}`);
763
+ }
764
+ parallelBranchResults[entry.value.branchId] = entry.value.result;
765
+ }
766
+ }
767
+ }
768
+ finally {
769
+ if (parallelTimeoutId !== undefined)
770
+ clearTimeout(parallelTimeoutId);
771
+ }
772
+ const merged = {};
773
+ for (const [branchId, branchResult] of Object.entries(parallelBranchResults ?? {})) {
774
+ merged[branchId] = branchResult.json;
775
+ }
776
+ result = {
777
+ id: step.id,
778
+ json: merged,
779
+ stdout: wait === "any"
780
+ ? (Object.values(parallelBranchResults ?? {})[0]?.stdout ?? "")
781
+ : JSON.stringify(merged),
782
+ };
783
+ }
784
+ else if (execution.kind === "workflow") {
785
+ const workflowPath = resolveTemplate(execution.value, resolvedArgs, results);
786
+ const resolvedWorkflowPath = path.isAbsolute(workflowPath)
787
+ ? workflowPath
788
+ : path.resolve(path.dirname(resolvedFilePath), workflowPath);
789
+ const activeWorkflows = ctx._activeWorkflows ?? new Set();
790
+ let canonicalWorkflowPath;
791
+ try {
792
+ canonicalWorkflowPath = await fsp.realpath(resolvedWorkflowPath);
793
+ }
794
+ catch {
795
+ throw new Error(`Workflow step ${step.id} workflow file not found: ${resolvedWorkflowPath}`);
796
+ }
797
+ if (activeWorkflows.has(canonicalWorkflowPath)) {
798
+ throw new Error(`Workflow step ${step.id} creates a cycle: ${canonicalWorkflowPath} is already being executed`);
799
+ }
800
+ const childActive = new Set(activeWorkflows);
801
+ childActive.add(canonicalWorkflowPath);
802
+ const subArgs = resolveWorkflowStepArgs(step.workflow_args, resolvedArgs, results);
803
+ const subResult = await runWorkflowFile({
804
+ filePath: resolvedWorkflowPath,
805
+ args: subArgs,
806
+ ctx: { ...ctx, env, cwd, _activeWorkflows: childActive },
807
+ });
808
+ if (subResult.status === "needs_approval" || subResult.status === "needs_input") {
809
+ const resumeToken = subResult.requiresApproval?.resumeToken ?? subResult.requiresInput?.resumeToken;
810
+ if (resumeToken) {
811
+ try {
812
+ const decoded = decodeToken(resumeToken);
813
+ if (decoded?.stateKey) {
814
+ await deleteStateJson({ env: ctx.env, key: decoded.stateKey }).catch(() => { });
815
+ }
816
+ }
817
+ catch {
818
+ // best-effort cleanup
819
+ }
820
+ }
821
+ throw new Error(`Workflow step ${step.id} sub-workflow halted for ${subResult.status === "needs_approval" ? "approval" : "input"}. Sub-workflow approval/input gates are not supported in composition.`);
822
+ }
823
+ const json = subResult.output.length === 1 ? subResult.output[0] : subResult.output;
824
+ const stdout = subResult.output.length ? serializeValueForStdout(json) : "";
825
+ result = { id: step.id, stdout, json };
826
+ }
827
+ else if (execution.kind === "shell") {
828
+ const command = resolveTemplate(execution.value, resolvedArgs, results);
829
+ const stdinValue = resolveShellStdin(step.stdin, resolvedArgs, results);
830
+ const { stdout } = await runShellCommand({
831
+ command,
832
+ stdin: stdinValue,
833
+ env,
834
+ cwd,
835
+ signal: stepSignal,
836
+ ...(step.timeout_ms ? { killSignal: "SIGKILL" } : {}),
837
+ });
838
+ result = { id: step.id, stdout, json: parseJson(stdout) };
839
+ }
840
+ else if (execution.kind === "pipeline") {
841
+ if (!ctx.registry) {
842
+ throw new Error(`Workflow step ${step.id} requires a command registry for pipeline execution`);
843
+ }
844
+ const pipelineText = resolveTemplate(execution.value, resolvedArgs, results);
845
+ const inputValue = resolveInputValue(step.stdin, resolvedArgs, results);
846
+ result = await runPipelineStep({
847
+ stepId: step.id,
848
+ pipelineText,
849
+ inputValue,
850
+ ctx: { ...ctx, signal: stepSignal },
851
+ env,
852
+ cwd,
853
+ resume: resumedPipelineInput?.stepId === step.id
854
+ ? {
855
+ pipelineInput: resumedPipelineInput.pipelineInput,
856
+ response: resumedPipelineInput.response,
857
+ onConsumed: resumedPipelineInput.onConsumed,
858
+ }
859
+ : undefined,
860
+ });
861
+ }
862
+ else {
863
+ const inputValue = resolveInputValue(step.stdin, resolvedArgs, results);
864
+ result = createSyntheticStepResult(step.id, inputValue);
865
+ }
866
+ return { result, parallelBranchResults };
867
+ }
868
+ finally {
869
+ if (timeoutId !== undefined)
870
+ clearTimeout(timeoutId);
871
+ }
872
+ };
873
+ let result;
874
+ let parallelBranchResults = null;
875
+ try {
876
+ const attemptResult = retryConfig.max > 1
877
+ ? await withRetry(executeStepAttempt, retryConfig, {
878
+ signal: ctx.signal,
879
+ shouldRetry: (error) => {
880
+ if (error instanceof WorkflowPipelineInputSuspension ||
881
+ error instanceof RequestInputResumeError) {
882
+ return false;
883
+ }
884
+ const message = error?.message ?? String(error);
885
+ return !/halted (for approval inside|before completion at) pipeline/.test(message);
886
+ },
887
+ onRetry: (attempt, error, delayMs) => {
888
+ ctx.stderr.write(`[RETRY] Step '${step.id}' failed (attempt ${attempt}/${retryConfig.max}): ${error?.message ?? String(error)}. Retrying in ${delayMs}ms...\n`);
889
+ },
890
+ })
891
+ : await executeStepAttempt();
892
+ result = attemptResult.result;
893
+ parallelBranchResults = attemptResult.parallelBranchResults;
894
+ }
895
+ catch (err) {
896
+ if (err instanceof WorkflowPipelineInputSuspension) {
897
+ const inputRequest = buildNeedsInputRequest({
898
+ stepId: err.stepId,
899
+ prompt: err.request.prompt,
900
+ responseSchema: err.request.responseSchema,
901
+ defaults: err.request.defaults,
902
+ subject: err.request.subject,
903
+ maxEnvelopeBytes: resolveToolEnvelopeMaxBytes(ctx.env),
904
+ });
905
+ const stateKey = await saveWorkflowResumeState(ctx.env, {
906
+ filePath: resolvedFilePath,
907
+ resumeAtIndex: idx,
908
+ steps: results,
909
+ args: resolvedArgs,
910
+ inputStepId: err.stepId,
911
+ inputKind: "pipeline_command",
912
+ inputSchema: err.request.responseSchema,
913
+ inputSubject: err.request.subject,
914
+ pipelineInput: err.pipelineInput,
915
+ createdAt: new Date().toISOString(),
916
+ });
917
+ if (consumedResumeStateKey && consumedResumeStateKey !== stateKey) {
918
+ await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
919
+ }
920
+ const resumeToken = encodeToken({
921
+ protocolVersion: 1,
922
+ v: 1,
923
+ kind: "workflow-file",
924
+ stateKey,
925
+ });
926
+ return {
927
+ status: "needs_input",
928
+ output: [],
929
+ requiresInput: {
930
+ ...inputRequest,
931
+ resumeToken,
932
+ },
933
+ };
934
+ }
935
+ if (err instanceof RequestInputResumeError) {
936
+ throw err;
937
+ }
938
+ if (ctx.signal?.aborted && (err?.name === "AbortError" || err?.code === "ABORT_ERR")) {
939
+ throw err;
940
+ }
941
+ if (err?.message &&
942
+ /halted (for approval inside|before completion at) pipeline/.test(err.message)) {
943
+ throw err;
944
+ }
945
+ const isAbortErr = err?.name === "AbortError" || err?.code === "ABORT_ERR";
946
+ const isTimeout = Boolean(step.timeout_ms) && isAbortErr;
947
+ const errorMessage = isTimeout
948
+ ? `Step '${step.id}' timed out after ${step.timeout_ms}ms`
949
+ : (err?.message ?? String(err));
950
+ const policy = step.on_error ?? "stop";
951
+ if (policy === "stop") {
952
+ throw isTimeout ? new Error(errorMessage) : err;
953
+ }
954
+ results[step.id] = {
955
+ id: step.id,
956
+ error: true,
957
+ errorMessage,
958
+ };
959
+ if (policy === "skip_rest") {
960
+ break;
961
+ }
962
+ continue;
963
+ }
964
+ if (parallelBranchResults) {
965
+ for (const [branchId, branchResult] of Object.entries(parallelBranchResults)) {
966
+ results[branchId] = branchResult;
967
+ trackStepCost(costTracker, branchId, branchResult);
968
+ }
969
+ }
970
+ results[step.id] = result;
971
+ lastStepId = step.id;
972
+ trackStepCost(costTracker, step.id, result);
973
+ if (workflow.cost_limit) {
974
+ costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
975
+ }
976
+ if (isApprovalStep(step.approval)) {
977
+ const approval = extractApprovalRequest(step, results[step.id], ctx.env);
978
+ const approvalIdentity = approvalIdentityFromRequest(approval);
979
+ if (approvalIdentity.initiatedBy) {
980
+ results[step.id].initiatedBy = approvalIdentity.initiatedBy;
981
+ }
982
+ if (ctx.mode === "tool" || !isInteractive(ctx.stdin)) {
983
+ const stateKey = await saveWorkflowResumeState(ctx.env, {
984
+ filePath: resolvedFilePath,
985
+ resumeAtIndex: idx + 1,
986
+ steps: results,
987
+ args: resolvedArgs,
988
+ approvalStepId: step.id,
989
+ approvalIdentity,
990
+ createdAt: new Date().toISOString(),
991
+ });
992
+ let approvalId;
993
+ try {
994
+ approvalId = await createApprovalIndex({ env: ctx.env, stateKey });
995
+ }
996
+ catch (err) {
997
+ await deleteStateJson({ env: ctx.env, key: stateKey }).catch(() => { });
998
+ throw err;
999
+ }
1000
+ if (consumedResumeStateKey && consumedResumeStateKey !== stateKey) {
1001
+ await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
1002
+ }
1003
+ const resumeToken = encodeToken({
1004
+ protocolVersion: 1,
1005
+ v: 1,
1006
+ kind: "workflow-file",
1007
+ stateKey,
1008
+ });
1009
+ return {
1010
+ status: "needs_approval",
1011
+ output: [],
1012
+ requiresApproval: {
1013
+ ...approval,
1014
+ resumeToken,
1015
+ ...(approvalId ? { approvalId } : null),
1016
+ },
1017
+ };
1018
+ }
1019
+ ctx.stdout.write(`${approval.prompt} [y/N] `);
1020
+ const answer = await readLineFromStream(ctx.stdin, {
1021
+ timeoutMs: parseApprovalTimeoutMs(ctx.env),
1022
+ });
1023
+ if (!/^y(es)?$/i.test(String(answer).trim())) {
1024
+ throw new Error("Not approved");
1025
+ }
1026
+ const approvedBy = String(ctx.env.LOBSTER_APPROVAL_APPROVED_BY ?? "").trim() || undefined;
1027
+ enforceApprovalIdentity({
1028
+ stepId: step.id,
1029
+ identity: approvalIdentity,
1030
+ approvedBy,
1031
+ });
1032
+ results[step.id].approved = true;
1033
+ if (approvedBy)
1034
+ results[step.id].approvedBy = approvedBy;
1035
+ }
1036
+ }
1037
+ const output = lastStepId ? toOutputItems(results[lastStepId]) : [];
1038
+ if (consumedResumeStateKey) {
1039
+ await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
1040
+ }
1041
+ const runResult = { status: "ok", output };
1042
+ if (costTracker.hasUsage()) {
1043
+ runResult._meta = { cost: costTracker.getSummary() };
1044
+ }
1045
+ return runResult;
1046
+ }
1047
+ finally {
1048
+ ctx._activeWorkflows?.delete(canonicalFilePath);
1049
+ }
1050
+ }
1051
+ // Returns a human-readable note if a step.stdin value references a prior step's
1052
+ // output. Because dry-run placeholders have no actual stdout/json, we surface
1053
+ // this so users know the value is unknown at plan time rather than silently
1054
+ // resolving to an empty string.
1055
+ function dryRunStdinNote(stdin) {
1056
+ if (stdin === null || stdin === undefined)
1057
+ return null;
1058
+ if (typeof stdin !== "string")
1059
+ return null;
1060
+ const trimmed = stdin.trim();
1061
+ // Strict step ref: '$step-id.stdout' or '$step-id.json'
1062
+ if (/^\$[A-Za-z0-9_-]+\.(stdout|json)$/.test(trimmed)) {
1063
+ return `${trimmed} [output unknown at plan time]`;
1064
+ }
1065
+ // Inline template ref: contains '$stepid.stdout' or '$stepid.json'
1066
+ if (/\$[A-Za-z0-9_-]+\.(stdout|json)/.test(trimmed)) {
1067
+ return `${trimmed} [contains step output refs — unknown at plan time]`;
1068
+ }
1069
+ return null;
1070
+ }
1071
+ function dryRunTemplateNote(input) {
1072
+ if (/\$[A-Za-z0-9_-]+\.(stdout|json)/.test(input)) {
1073
+ return "[contains step output refs — unknown at plan time]";
1074
+ }
1075
+ return null;
1076
+ }
1077
+ function hasDeferredDryRunStageName(input) {
1078
+ return /\$[A-Za-z0-9_-]+\.(stdout|json)/.test(input);
1079
+ }
1080
+ function resolveDryRunTemplate(input, args, results) {
1081
+ const withArgs = resolveArgsTemplate(input, args);
1082
+ return withArgs.replace(/\$([A-Za-z0-9_-]+)\.(stdout|json|approved)/g, (match, id, field) => {
1083
+ if (field === "approved") {
1084
+ const step = results[id];
1085
+ if (!step)
1086
+ return match;
1087
+ return step.approved === true ? "true" : "false";
1088
+ }
1089
+ return match;
1090
+ });
1091
+ }
1092
+ function dryRunWorkflow({ steps, resolvedArgs, results, startIndex, ctx, }) {
1093
+ const lines = [];
1094
+ const totalSteps = steps.length - startIndex;
1095
+ lines.push(`[DRY RUN] Would execute ${totalSteps} step${totalSteps !== 1 ? "s" : ""}:\n`);
1096
+ for (let idx = startIndex; idx < steps.length; idx++) {
1097
+ const step = steps[idx];
1098
+ const num = idx - startIndex + 1;
1099
+ if (!evaluateCondition(step.when ?? step.condition, results)) {
1100
+ results[step.id] = { id: step.id, skipped: true };
1101
+ lines.push(` ${num}. ${step.id} [skipped — condition: false]`);
1102
+ continue;
1103
+ }
1104
+ if (isInputStep(step.input)) {
1105
+ lines.push(` ${num}. ${step.id} [input]`);
1106
+ lines.push(` prompt: ${step.input.prompt}`);
1107
+ lines.push(` [input required]`);
1108
+ results[step.id] = { id: step.id, response: { pending: true } };
1109
+ continue;
1110
+ }
1111
+ if (typeof step.for_each === "string" && Array.isArray(step.steps)) {
1112
+ lines.push(` ${num}. ${step.id} [for_each]`);
1113
+ const forEachRef = step.for_each;
1114
+ const forEachNote = dryRunTemplateNote(forEachRef);
1115
+ lines.push(` for_each: ${forEachRef}${forEachNote ? ` ${forEachNote}` : ""}`);
1116
+ if (forEachRef.trim().startsWith("$")) {
1117
+ try {
1118
+ resolveInputValue(forEachRef, resolvedArgs, results);
1119
+ }
1120
+ catch (err) {
1121
+ throw new Error(`Workflow step ${step.id} for_each: ${err?.message ?? String(err)}`);
1122
+ }
1123
+ }
1124
+ const dryItemVar = step.item_var ?? "item";
1125
+ const dryIndexVar = step.index_var ?? "index";
1126
+ lines.push(` item_var: ${dryItemVar}, index_var: ${dryIndexVar}`);
1127
+ if (step.batch_size)
1128
+ lines.push(` batch_size: ${step.batch_size}`);
1129
+ if (step.pause_ms)
1130
+ lines.push(` pause_ms: ${step.pause_ms}`);
1131
+ lines.push(` sub-steps: ${step.steps.length}`);
1132
+ const loopScopedResults = { ...results };
1133
+ loopScopedResults[dryItemVar] = { id: dryItemVar, json: { _placeholder: true } };
1134
+ loopScopedResults[dryIndexVar] = { id: dryIndexVar, json: 0 };
1135
+ for (let subIdx = 0; subIdx < step.steps.length; subIdx++) {
1136
+ const sub = step.steps[subIdx];
1137
+ if (!evaluateCondition(sub.when ?? sub.condition, loopScopedResults)) {
1138
+ lines.push(` ${subIdx + 1}. ${sub.id} [skipped — condition: false]`);
1139
+ loopScopedResults[sub.id] = { id: sub.id, skipped: true };
1140
+ continue;
1141
+ }
1142
+ if (sub.stdin !== undefined && sub.stdin !== null) {
1143
+ try {
1144
+ resolveInputValue(sub.stdin, resolvedArgs, loopScopedResults);
1145
+ }
1146
+ catch (err) {
1147
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} stdin: ${err?.message ?? String(err)}`);
1148
+ }
1149
+ }
1150
+ const subExec = getStepExecution(sub);
1151
+ if (subExec.kind === "shell") {
1152
+ const command = resolveDryRunTemplate(subExec.value, resolvedArgs, loopScopedResults);
1153
+ lines.push(` ${subIdx + 1}. ${sub.id} [shell] run: ${command}`);
1154
+ }
1155
+ else if (subExec.kind === "pipeline") {
1156
+ if (!ctx.registry) {
1157
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} requires a command registry for pipeline execution`);
1158
+ }
1159
+ const pipelineText = resolveDryRunTemplate(subExec.value, resolvedArgs, loopScopedResults);
1160
+ const stages = parsePipeline(pipelineText);
1161
+ for (const stage of stages) {
1162
+ if (hasDeferredDryRunStageName(stage.name))
1163
+ continue;
1164
+ if (!ctx.registry.get(stage.name)) {
1165
+ throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} pipeline: unknown command: ${stage.name}`);
1166
+ }
1167
+ }
1168
+ lines.push(` ${subIdx + 1}. ${sub.id} [pipeline] pipeline: ${pipelineText}`);
1169
+ }
1170
+ else {
1171
+ lines.push(` ${subIdx + 1}. ${sub.id} [no-op]`);
1172
+ }
1173
+ loopScopedResults[sub.id] = { id: sub.id };
1174
+ }
1175
+ results[step.id] = { id: step.id };
1176
+ continue;
1177
+ }
1178
+ // Validate stdin refs early — throws if a strict ref like '$missing.stdout'
1179
+ // points to a step that doesn't exist at all (real execution would also fail).
1180
+ // We call resolveInputValue with the current results so refs to steps we've
1181
+ // already visited (placeholders) are accepted without throwing.
1182
+ if (step.stdin !== undefined && step.stdin !== null) {
1183
+ try {
1184
+ resolveInputValue(step.stdin, resolvedArgs, results);
1185
+ }
1186
+ catch (err) {
1187
+ throw new Error(`Workflow step ${step.id} stdin: ${err?.message ?? String(err)}`);
1188
+ }
1189
+ }
1190
+ const execution = getStepExecution(step);
1191
+ // Annotate when the resolved command/pipeline references a prior step's output.
1192
+ // Since dry-run placeholders have no actual stdout/json, note it explicitly
1193
+ // rather than silently collapsing the reference to an empty string.
1194
+ const stdinNote = dryRunStdinNote(step.stdin);
1195
+ if (execution.kind === "parallel") {
1196
+ lines.push(` ${num}. ${step.id} [parallel]`);
1197
+ lines.push(` wait: ${step.parallel?.wait ?? "all"}`);
1198
+ if (step.parallel?.timeout_ms) {
1199
+ lines.push(` timeout: ${step.parallel.timeout_ms}ms`);
1200
+ }
1201
+ for (const branch of step.parallel?.branches ?? []) {
1202
+ const branchShell = typeof branch.run === "string" ? branch.run : branch.command;
1203
+ if (typeof branch.pipeline === "string" && branch.pipeline.trim()) {
1204
+ const pipelineText = resolveDryRunTemplate(branch.pipeline, resolvedArgs, results);
1205
+ const pipelineNote = dryRunTemplateNote(pipelineText);
1206
+ if (!ctx.registry) {
1207
+ throw new Error(`Parallel branch ${branch.id} requires a command registry for pipeline execution`);
1208
+ }
1209
+ const stages = parsePipeline(pipelineText);
1210
+ for (const stage of stages) {
1211
+ if (hasDeferredDryRunStageName(stage.name))
1212
+ continue;
1213
+ if (!ctx.registry.get(stage.name)) {
1214
+ throw new Error(`Parallel branch ${branch.id} pipeline references unknown command: ${stage.name}`);
1215
+ }
1216
+ }
1217
+ lines.push(` branch ${branch.id}: [pipeline] ${pipelineText}${pipelineNote ? ` ${pipelineNote}` : ""}`);
1218
+ }
1219
+ else if (typeof branchShell === "string" && branchShell.trim()) {
1220
+ const command = resolveDryRunTemplate(branchShell, resolvedArgs, results);
1221
+ const commandNote = dryRunTemplateNote(command);
1222
+ lines.push(` branch ${branch.id}: [shell] ${command}${commandNote ? ` ${commandNote}` : ""}`);
1223
+ }
1224
+ else {
1225
+ lines.push(` branch ${branch.id}: [no-op]`);
1226
+ }
1227
+ results[branch.id] = { id: branch.id };
1228
+ }
1229
+ }
1230
+ else if (execution.kind === "workflow") {
1231
+ const workflowPath = resolveDryRunTemplate(execution.value, resolvedArgs, results);
1232
+ const pathNote = dryRunTemplateNote(workflowPath);
1233
+ lines.push(` ${num}. ${step.id} [workflow]`);
1234
+ lines.push(` workflow: ${workflowPath}${pathNote ? ` ${pathNote}` : ""}`);
1235
+ if (step.workflow_args && typeof step.workflow_args === "object") {
1236
+ const argKeys = Object.keys(step.workflow_args);
1237
+ if (argKeys.length) {
1238
+ lines.push(` args: ${argKeys.join(", ")}`);
1239
+ }
1240
+ }
1241
+ }
1242
+ else if (execution.kind === "shell") {
1243
+ const command = resolveDryRunTemplate(execution.value, resolvedArgs, results);
1244
+ const commandNote = dryRunTemplateNote(command);
1245
+ lines.push(` ${num}. ${step.id} [shell]`);
1246
+ lines.push(` run: ${command}${commandNote ? ` ${commandNote}` : ""}`);
1247
+ }
1248
+ else if (execution.kind === "pipeline") {
1249
+ const pipelineText = resolveDryRunTemplate(execution.value, resolvedArgs, results);
1250
+ const pipelineNote = dryRunTemplateNote(pipelineText);
1251
+ // Validate pipeline syntax and registry even in dry-run so errors surface early.
1252
+ if (!ctx.registry) {
1253
+ throw new Error(`Workflow step ${step.id} requires a command registry for pipeline execution`);
1254
+ }
1255
+ // Validate that every stage name is a known command.
1256
+ const stages = parsePipeline(pipelineText);
1257
+ for (const stage of stages) {
1258
+ if (hasDeferredDryRunStageName(stage.name)) {
1259
+ continue;
1260
+ }
1261
+ if (!ctx.registry.get(stage.name)) {
1262
+ throw new Error(`Workflow step ${step.id} pipeline references unknown command: ${stage.name}`);
1263
+ }
1264
+ }
1265
+ lines.push(` ${num}. ${step.id} [pipeline]`);
1266
+ lines.push(` pipeline: ${pipelineText}${pipelineNote ? ` ${pipelineNote}` : ""}`);
1267
+ if (stages.some((stage) => hasDeferredDryRunStageName(stage.name))) {
1268
+ lines.push(" [command validation deferred — stage name depends on step output]");
1269
+ }
1270
+ }
1271
+ else {
1272
+ lines.push(` ${num}. ${step.id} [no-op]`);
1273
+ }
1274
+ if (stdinNote)
1275
+ lines.push(` stdin: ${stdinNote}`);
1276
+ if (step.timeout_ms)
1277
+ lines.push(` timeout: ${step.timeout_ms}ms`);
1278
+ if (step.on_error && step.on_error !== "stop")
1279
+ lines.push(` on_error: ${step.on_error}`);
1280
+ if (step.retry && typeof step.retry === "object") {
1281
+ const rc = resolveRetryConfig(step.retry);
1282
+ if (rc.max > 1) {
1283
+ lines.push(` retry: up to ${rc.max} attempts, ${rc.backoff} backoff (base: ${rc.delay_ms}ms${rc.jitter ? ", jitter" : ""})`);
1284
+ }
1285
+ }
1286
+ if (isApprovalStep(step.approval)) {
1287
+ lines.push(` [approval required]`);
1288
+ }
1289
+ // Record a placeholder result so later steps can reference this step in conditions.
1290
+ // For approval steps, model approval as granted so downstream conditions like
1291
+ // $step.approved evaluate correctly in the plan (rather than always being false).
1292
+ // We intentionally omit stdout/json — dryRunStdinNote() surfaces that gap.
1293
+ results[step.id] = isApprovalStep(step.approval)
1294
+ ? { id: step.id, approved: true }
1295
+ : { id: step.id };
1296
+ }
1297
+ lines.push("");
1298
+ ctx.stderr.write(lines.join("\n"));
1299
+ return { status: "ok", output: [] };
1300
+ }
1301
+ export function decodeWorkflowResumePayload(payload) {
1302
+ if (!payload || typeof payload !== "object")
1303
+ return null;
1304
+ const data = payload;
1305
+ if (data.kind !== "workflow-file")
1306
+ return null;
1307
+ if (data.protocolVersion !== 1 || data.v !== 1)
1308
+ throw new Error("Unsupported token version");
1309
+ if (data.stateKey && typeof data.stateKey === "string") {
1310
+ return data;
1311
+ }
1312
+ if (!data.filePath || typeof data.filePath !== "string")
1313
+ throw new Error("Invalid workflow token");
1314
+ if (typeof data.resumeAtIndex !== "number")
1315
+ throw new Error("Invalid workflow token");
1316
+ if (!data.steps || typeof data.steps !== "object")
1317
+ throw new Error("Invalid workflow token");
1318
+ if (!data.args || typeof data.args !== "object")
1319
+ throw new Error("Invalid workflow token");
1320
+ return data;
1321
+ }
1322
+ async function saveWorkflowResumeState(env, state) {
1323
+ const stateKey = `workflow_resume_${randomUUID()}`;
1324
+ await writeStateJson({ env, key: stateKey, value: state });
1325
+ return stateKey;
1326
+ }
1327
+ function alternateWorkflowResumeStateKey(stateKey) {
1328
+ if (stateKey.includes("workflow-resume_")) {
1329
+ return stateKey.replace("workflow-resume_", "workflow_resume_");
1330
+ }
1331
+ if (stateKey.includes("workflow_resume_")) {
1332
+ return stateKey.replace("workflow_resume_", "workflow-resume_");
1333
+ }
1334
+ return null;
1335
+ }
1336
+ async function resolveWorkflowResumeStateKey(env, stateKey) {
1337
+ const stored = await readStateJson({ env, key: stateKey });
1338
+ if (stored && typeof stored === "object") {
1339
+ return stateKey;
1340
+ }
1341
+ const altKey = alternateWorkflowResumeStateKey(stateKey);
1342
+ if (!altKey) {
1343
+ return stateKey;
1344
+ }
1345
+ const altStored = await readStateJson({ env, key: altKey });
1346
+ if (altStored && typeof altStored === "object") {
1347
+ return altKey;
1348
+ }
1349
+ return stateKey;
1350
+ }
1351
+ async function loadWorkflowResumeState(env, stateKey) {
1352
+ let stored = await readStateJson({ env, key: stateKey });
1353
+ if ((!stored || typeof stored !== "object") && typeof stateKey === "string") {
1354
+ const altKey = alternateWorkflowResumeStateKey(stateKey);
1355
+ if (altKey) {
1356
+ stored = await readStateJson({ env, key: altKey });
1357
+ }
1358
+ }
1359
+ if (!stored || typeof stored !== "object") {
1360
+ throw new Error("Workflow resume state not found");
1361
+ }
1362
+ const data = stored;
1363
+ if (!data.filePath || typeof data.filePath !== "string")
1364
+ throw new Error("Invalid workflow resume state");
1365
+ if (typeof data.resumeAtIndex !== "number")
1366
+ throw new Error("Invalid workflow resume state");
1367
+ if (!data.steps || typeof data.steps !== "object")
1368
+ throw new Error("Invalid workflow resume state");
1369
+ if (!data.args || typeof data.args !== "object")
1370
+ throw new Error("Invalid workflow resume state");
1371
+ if (data.inputKind !== undefined &&
1372
+ !["workflow_step", "pipeline_command"].includes(data.inputKind)) {
1373
+ throw new Error("Invalid workflow resume state");
1374
+ }
1375
+ if (data.inputKind === "pipeline_command") {
1376
+ if (typeof data.inputStepId !== "string")
1377
+ throw new Error("Invalid workflow resume state");
1378
+ if (data.inputSchema === undefined)
1379
+ throw new Error("Invalid workflow resume state");
1380
+ data.pipelineInput = validateWorkflowPipelineInputResumeState(data.pipelineInput);
1381
+ }
1382
+ else if (data.pipelineInput !== undefined) {
1383
+ throw new Error("Invalid workflow resume state");
1384
+ }
1385
+ return data;
1386
+ }
1387
+ function validateWorkflowPipelineInputResumeState(value) {
1388
+ if (!value || typeof value !== "object")
1389
+ throw new Error("Invalid workflow resume state");
1390
+ const data = value;
1391
+ if (!Array.isArray(data.pipeline))
1392
+ throw new Error("Invalid workflow resume state");
1393
+ validateWorkflowPipelineShape(data.pipeline);
1394
+ if (typeof data.resumeAtIndex !== "number" ||
1395
+ !Number.isInteger(data.resumeAtIndex) ||
1396
+ data.resumeAtIndex < 0 ||
1397
+ data.resumeAtIndex >= data.pipeline.length) {
1398
+ throw new Error("Invalid workflow resume state");
1399
+ }
1400
+ if (!Array.isArray(data.items))
1401
+ throw new Error("Invalid workflow resume state");
1402
+ data.commandInput = validateCommandInputState(data.commandInput);
1403
+ return data;
1404
+ }
1405
+ function validateWorkflowPipelineShape(pipeline) {
1406
+ for (const stage of pipeline) {
1407
+ if (!stage || typeof stage !== "object")
1408
+ throw new Error("Invalid workflow resume state");
1409
+ const data = stage;
1410
+ if (typeof data.name !== "string" || data.name.length === 0) {
1411
+ throw new Error("Invalid workflow resume state");
1412
+ }
1413
+ if (!data.args || typeof data.args !== "object" || Array.isArray(data.args)) {
1414
+ throw new Error("Invalid workflow resume state");
1415
+ }
1416
+ if (typeof data.raw !== "string")
1417
+ throw new Error("Invalid workflow resume state");
1418
+ }
1419
+ }
1420
+ function mergeEnv(base, workflowEnv, stepEnv, args, results) {
1421
+ const env = { ...base };
1422
+ // Expose resolved args as env vars so shell commands can safely reference them
1423
+ // without embedding raw values into the command string.
1424
+ // Example: $LOBSTER_ARG_TEXT
1425
+ env.LOBSTER_ARGS_JSON = JSON.stringify(args ?? {});
1426
+ for (const [key, value] of Object.entries(args ?? {})) {
1427
+ const normalized = normalizeArgEnvKey(key);
1428
+ if (!normalized)
1429
+ continue;
1430
+ env[`LOBSTER_ARG_${normalized}`] = String(value);
1431
+ }
1432
+ const apply = (source) => {
1433
+ if (!source)
1434
+ return;
1435
+ for (const [key, value] of Object.entries(source)) {
1436
+ if (typeof value === "string") {
1437
+ env[key] = resolveTemplate(value, args, results);
1438
+ }
1439
+ }
1440
+ };
1441
+ // Allow explicit env blocks to override injected defaults.
1442
+ apply(workflowEnv);
1443
+ apply(stepEnv);
1444
+ return env;
1445
+ }
1446
+ function normalizeArgEnvKey(key) {
1447
+ const trimmed = String(key ?? "").trim();
1448
+ if (!trimmed)
1449
+ return null;
1450
+ // Keep it predictable for shells: uppercase and [A-Z0-9_]
1451
+ const up = trimmed.toUpperCase();
1452
+ const normalized = up.replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1453
+ return normalized || null;
1454
+ }
1455
+ function resolveCwd(cwd, args) {
1456
+ if (!cwd)
1457
+ return undefined;
1458
+ return resolveArgsTemplate(cwd, args);
1459
+ }
1460
+ function resolveInputValue(stdin, args, results) {
1461
+ if (stdin === null || stdin === undefined)
1462
+ return null;
1463
+ if (typeof stdin === "string") {
1464
+ const ref = parseStepRef(stdin.trim());
1465
+ if (ref)
1466
+ return getStepRefValue(ref, results, true);
1467
+ return resolveTemplate(stdin, args, results);
1468
+ }
1469
+ return stdin;
1470
+ }
1471
+ function resolveShellStdin(stdin, args, results) {
1472
+ const value = resolveInputValue(stdin, args, results);
1473
+ return encodeShellInput(value);
1474
+ }
1475
+ function resolveTemplate(input, args, results) {
1476
+ const withArgs = resolveArgsTemplate(input, args);
1477
+ return resolveStepRefs(withArgs, results);
1478
+ }
1479
+ function resolveWorkflowStepArgs(workflowArgs, parentArgs, results) {
1480
+ if (!workflowArgs)
1481
+ return {};
1482
+ const resolved = {};
1483
+ for (const [key, value] of Object.entries(workflowArgs)) {
1484
+ if (typeof value === "string") {
1485
+ resolved[key] = resolveTemplate(value, parentArgs, results);
1486
+ }
1487
+ else {
1488
+ resolved[key] = value;
1489
+ }
1490
+ }
1491
+ return resolved;
1492
+ }
1493
+ function resolveArgsTemplate(input, args) {
1494
+ return input.replace(/\$\{([A-Za-z0-9_-]+)\}/g, (match, key) => {
1495
+ if (key in args)
1496
+ return String(args[key]);
1497
+ return match;
1498
+ });
1499
+ }
1500
+ function resolveStepRefs(input, results) {
1501
+ return input.replace(/\$([A-Za-z0-9_-]+)\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)/g, (match, id, pathValue) => {
1502
+ if (!(id in results)) {
1503
+ return match;
1504
+ }
1505
+ const refValue = getStepRefValue({ id, path: pathValue }, results, false);
1506
+ if (refValue === undefined) {
1507
+ if (pathValue === "approved" || pathValue === "skipped")
1508
+ return "false";
1509
+ return "";
1510
+ }
1511
+ return renderTemplateValue(refValue);
1512
+ });
1513
+ }
1514
+ function parseStepRef(value) {
1515
+ const match = value.match(/^\$([A-Za-z0-9_-]+)\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)$/);
1516
+ if (!match)
1517
+ return null;
1518
+ return { id: match[1], path: match[2] };
1519
+ }
1520
+ function getStepRefValue(ref, results, strict) {
1521
+ const step = results[ref.id];
1522
+ if (!step) {
1523
+ if (strict)
1524
+ throw new Error(`Unknown step reference: ${ref.id}.${ref.path}`);
1525
+ return undefined;
1526
+ }
1527
+ return getValueByPath(step, ref.path);
1528
+ }
1529
+ function evaluateCondition(condition, results) {
1530
+ if (condition === undefined || condition === null)
1531
+ return true;
1532
+ if (typeof condition === "boolean")
1533
+ return condition;
1534
+ if (typeof condition !== "string")
1535
+ throw new Error("Unsupported condition type");
1536
+ const trimmed = condition.trim();
1537
+ if (trimmed === "true")
1538
+ return true;
1539
+ if (trimmed === "false")
1540
+ return false;
1541
+ return evaluateConditionExpression(trimmed, results);
1542
+ }
1543
+ function isApprovalStep(approval) {
1544
+ if (approval === true)
1545
+ return true;
1546
+ if (typeof approval === "string" && approval.trim().length > 0)
1547
+ return true;
1548
+ if (approval && typeof approval === "object" && !Array.isArray(approval))
1549
+ return true;
1550
+ return false;
1551
+ }
1552
+ function isInputStep(input) {
1553
+ return Boolean(input && typeof input === "object" && !Array.isArray(input));
1554
+ }
1555
+ function extractApprovalRequest(step, result, env) {
1556
+ const approvalConfig = normalizeApprovalConfig(step.approval);
1557
+ const configIdentity = approvalIdentityFromRaw(approvalConfig);
1558
+ if (!configIdentity.initiatedBy) {
1559
+ const fromEnv = String(env?.LOBSTER_APPROVAL_INITIATED_BY ?? "").trim();
1560
+ if (fromEnv)
1561
+ configIdentity.initiatedBy = fromEnv;
1562
+ }
1563
+ if (!configIdentity.requiredApprover) {
1564
+ const fromEnv = String(env?.LOBSTER_APPROVAL_REQUIRED_APPROVER ?? "").trim();
1565
+ if (fromEnv)
1566
+ configIdentity.requiredApprover = fromEnv;
1567
+ }
1568
+ if (!configIdentity.requireDifferentApprover) {
1569
+ const fromEnv = parseBoolLike(env?.LOBSTER_APPROVAL_REQUIRE_DIFFERENT_APPROVER);
1570
+ if (fromEnv === true)
1571
+ configIdentity.requireDifferentApprover = true;
1572
+ }
1573
+ const fallbackPrompt = approvalConfig.prompt ?? `Approve ${step.id}?`;
1574
+ const json = result.json;
1575
+ if (json && typeof json === "object" && !Array.isArray(json)) {
1576
+ const candidate = json;
1577
+ if (candidate.requiresApproval?.prompt) {
1578
+ const identity = {
1579
+ ...configIdentity,
1580
+ ...approvalIdentityFromRaw(candidate.requiresApproval),
1581
+ };
1582
+ return {
1583
+ type: "approval_request",
1584
+ prompt: candidate.requiresApproval.prompt,
1585
+ items: candidate.requiresApproval.items ?? [],
1586
+ ...(candidate.requiresApproval.preview
1587
+ ? { preview: candidate.requiresApproval.preview }
1588
+ : null),
1589
+ ...(identity.initiatedBy ? { initiatedBy: identity.initiatedBy } : null),
1590
+ ...(identity.requiredApprover ? { requiredApprover: identity.requiredApprover } : null),
1591
+ ...(identity.requireDifferentApprover ? { requireDifferentApprover: true } : null),
1592
+ };
1593
+ }
1594
+ if (candidate.prompt) {
1595
+ const identity = {
1596
+ ...configIdentity,
1597
+ ...approvalIdentityFromRaw(candidate),
1598
+ };
1599
+ return {
1600
+ type: "approval_request",
1601
+ prompt: candidate.prompt,
1602
+ items: candidate.items ?? [],
1603
+ ...(candidate.preview ? { preview: candidate.preview } : null),
1604
+ ...(identity.initiatedBy ? { initiatedBy: identity.initiatedBy } : null),
1605
+ ...(identity.requiredApprover ? { requiredApprover: identity.requiredApprover } : null),
1606
+ ...(identity.requireDifferentApprover ? { requireDifferentApprover: true } : null),
1607
+ };
1608
+ }
1609
+ }
1610
+ const items = approvalConfig.items ?? normalizeApprovalItems(result.json);
1611
+ const preview = approvalConfig.preview ?? buildResultPreview(result);
1612
+ return {
1613
+ type: "approval_request",
1614
+ prompt: fallbackPrompt,
1615
+ items,
1616
+ ...(preview ? { preview } : null),
1617
+ ...(configIdentity.initiatedBy ? { initiatedBy: configIdentity.initiatedBy } : null),
1618
+ ...(configIdentity.requiredApprover
1619
+ ? { requiredApprover: configIdentity.requiredApprover }
1620
+ : null),
1621
+ ...(configIdentity.requireDifferentApprover ? { requireDifferentApprover: true } : null),
1622
+ };
1623
+ }
1624
+ function approvalIdentityFromRaw(raw) {
1625
+ if (!raw)
1626
+ return {};
1627
+ const value = raw;
1628
+ const initiatedBy = String(value.initiatedBy ?? value.initiated_by ?? "").trim() || undefined;
1629
+ const requiredApprover = String(value.requiredApprover ?? value.required_approver ?? "").trim() || undefined;
1630
+ const requireDifferentRaw = value.requireDifferentApprover ?? value.require_different_approver;
1631
+ const requireDifferentApprover = typeof requireDifferentRaw === "boolean" ? requireDifferentRaw : undefined;
1632
+ return {
1633
+ ...(initiatedBy ? { initiatedBy } : null),
1634
+ ...(requiredApprover ? { requiredApprover } : null),
1635
+ ...(requireDifferentApprover === true ? { requireDifferentApprover: true } : null),
1636
+ };
1637
+ }
1638
+ function approvalIdentityFromRequest(request) {
1639
+ if (!request)
1640
+ return {};
1641
+ return {
1642
+ ...(request.initiatedBy ? { initiatedBy: request.initiatedBy } : null),
1643
+ ...(request.requiredApprover ? { requiredApprover: request.requiredApprover } : null),
1644
+ ...(request.requireDifferentApprover ? { requireDifferentApprover: true } : null),
1645
+ };
1646
+ }
1647
+ function enforceApprovalIdentity({ stepId, identity, approvedBy, }) {
1648
+ const policy = identity ?? {};
1649
+ const approver = String(approvedBy ?? "").trim() || undefined;
1650
+ if (!policy.requiredApprover && !policy.requireDifferentApprover)
1651
+ return;
1652
+ if (!approver) {
1653
+ throw new WorkflowResumeArgumentError(`Workflow step ${stepId} approval requires approver identity; set LOBSTER_APPROVAL_APPROVED_BY`);
1654
+ }
1655
+ if (policy.requiredApprover && approver !== policy.requiredApprover) {
1656
+ throw new WorkflowResumeArgumentError(`Workflow step ${stepId} approval requires approver '${policy.requiredApprover}', got '${approver}'`);
1657
+ }
1658
+ if (policy.requireDifferentApprover && policy.initiatedBy && approver === policy.initiatedBy) {
1659
+ throw new WorkflowResumeArgumentError(`Workflow step ${stepId} approval must be granted by someone other than '${policy.initiatedBy}'`);
1660
+ }
1661
+ }
1662
+ function parseBoolLike(value) {
1663
+ if (typeof value === "boolean")
1664
+ return value;
1665
+ if (value === undefined || value === null)
1666
+ return undefined;
1667
+ const normalized = String(value).trim().toLowerCase();
1668
+ if (["1", "true", "yes", "y"].includes(normalized))
1669
+ return true;
1670
+ if (["0", "false", "no", "n"].includes(normalized))
1671
+ return false;
1672
+ return undefined;
1673
+ }
1674
+ function trackStepCost(costTracker, stepId, result) {
1675
+ const json = result.json;
1676
+ if (!json || typeof json !== "object")
1677
+ return;
1678
+ const items = Array.isArray(json) ? json : [json];
1679
+ for (const item of items) {
1680
+ if (!item || typeof item !== "object")
1681
+ continue;
1682
+ const usage = item.usage;
1683
+ if (!usage || typeof usage !== "object")
1684
+ continue;
1685
+ const modelValue = item.model;
1686
+ const model = typeof modelValue === "string" ? modelValue : null;
1687
+ costTracker.recordUsage(stepId, model, usage);
1688
+ }
1689
+ }
1690
+ function parseJson(stdout) {
1691
+ const trimmed = stdout.trim();
1692
+ if (!trimmed)
1693
+ return undefined;
1694
+ try {
1695
+ return JSON.parse(trimmed);
1696
+ }
1697
+ catch {
1698
+ return undefined;
1699
+ }
1700
+ }
1701
+ function toOutputItems(result) {
1702
+ if (!result)
1703
+ return [];
1704
+ if (result.json !== undefined) {
1705
+ return Array.isArray(result.json) ? result.json : [result.json];
1706
+ }
1707
+ if (result.response !== undefined) {
1708
+ return Array.isArray(result.response) ? result.response : [result.response];
1709
+ }
1710
+ if (result.stdout !== undefined) {
1711
+ return result.stdout === "" ? [] : [result.stdout];
1712
+ }
1713
+ return [];
1714
+ }
1715
+ function cloneResults(results) {
1716
+ const out = {};
1717
+ for (const [key, value] of Object.entries(results)) {
1718
+ out[key] = { ...value };
1719
+ }
1720
+ return out;
1721
+ }
1722
+ function findLastCompletedStepId(steps, results) {
1723
+ for (let idx = steps.length - 1; idx >= 0; idx--) {
1724
+ if (results[steps[idx].id])
1725
+ return steps[idx].id;
1726
+ }
1727
+ return null;
1728
+ }
1729
+ function isInteractive(stdin) {
1730
+ return Boolean(stdin.isTTY);
1731
+ }
1732
+ function parseApprovalTimeoutMs(env) {
1733
+ const raw = env?.LOBSTER_APPROVAL_INPUT_TIMEOUT_MS;
1734
+ const value = Number(raw);
1735
+ if (!Number.isFinite(value) || value <= 0)
1736
+ return 0;
1737
+ return Math.floor(value);
1738
+ }
1739
+ const MAX_NEEDS_INPUT_SUBJECT_BYTES = 192_000;
1740
+ const DEFAULT_TOOL_ENVELOPE_MAX_BYTES = 512_000;
1741
+ const RESUME_TOKEN_PLACEHOLDER = "x".repeat(220);
1742
+ function parseResponseJson(raw) {
1743
+ try {
1744
+ return JSON.parse(raw);
1745
+ }
1746
+ catch {
1747
+ throw new Error("Input response must be valid JSON");
1748
+ }
1749
+ }
1750
+ function validateInputResponse(params) {
1751
+ const validator = compileCached(params.schema);
1752
+ const ok = validator(params.response);
1753
+ if (ok)
1754
+ return;
1755
+ const first = validator.errors?.[0];
1756
+ const pathValue = first?.instancePath || "/";
1757
+ const reason = first?.message ? ` ${first.message}` : "";
1758
+ throw new Error(`Workflow input step ${params.stepId} response failed schema validation at ${pathValue}:${reason}`);
1759
+ }
1760
+ function resolveInputSubject(params) {
1761
+ if (params.step.stdin !== undefined) {
1762
+ return resolveInputValue(params.step.stdin, params.args, params.results);
1763
+ }
1764
+ if (!params.lastStepId)
1765
+ return null;
1766
+ const previous = params.results[params.lastStepId];
1767
+ if (!previous)
1768
+ return null;
1769
+ if (previous.json !== undefined)
1770
+ return previous.json;
1771
+ if (previous.response !== undefined)
1772
+ return previous.response;
1773
+ if (previous.stdout !== undefined)
1774
+ return previous.stdout;
1775
+ return null;
1776
+ }
1777
+ function maybeTruncateInputSubject(subject) {
1778
+ let serialized = "";
1779
+ try {
1780
+ serialized = JSON.stringify(subject ?? null);
1781
+ }
1782
+ catch {
1783
+ return {
1784
+ truncated: true,
1785
+ bytes: 0,
1786
+ preview: "[unserializable subject]",
1787
+ };
1788
+ }
1789
+ const byteLength = Buffer.byteLength(serialized, "utf8");
1790
+ if (byteLength <= MAX_NEEDS_INPUT_SUBJECT_BYTES)
1791
+ return subject;
1792
+ return {
1793
+ truncated: true,
1794
+ bytes: byteLength,
1795
+ preview: serialized.slice(0, 2000),
1796
+ };
1797
+ }
1798
+ function resolveToolEnvelopeMaxBytes(env) {
1799
+ const raw = env?.LOBSTER_MAX_TOOL_ENVELOPE_BYTES;
1800
+ const parsed = Number(raw);
1801
+ if (!Number.isFinite(parsed) || parsed < 1024) {
1802
+ return DEFAULT_TOOL_ENVELOPE_MAX_BYTES;
1803
+ }
1804
+ return Math.floor(parsed);
1805
+ }
1806
+ function buildNeedsInputRequest(params) {
1807
+ const base = {
1808
+ type: "input_request",
1809
+ prompt: params.prompt,
1810
+ responseSchema: params.responseSchema,
1811
+ ...(params.defaults !== undefined ? { defaults: params.defaults } : null),
1812
+ };
1813
+ let subject = params.subject;
1814
+ let request = { ...base, subject };
1815
+ if (fitsNeedsInputEnvelope(request, params.maxEnvelopeBytes))
1816
+ return request;
1817
+ subject = maybeTruncateInputSubject(subject);
1818
+ request = { ...base, subject };
1819
+ if (fitsNeedsInputEnvelope(request, params.maxEnvelopeBytes))
1820
+ return request;
1821
+ request = {
1822
+ ...base,
1823
+ subject: {
1824
+ truncated: true,
1825
+ bytes: estimateSerializedBytes(params.subject),
1826
+ preview: "[subject omitted: envelope size limit]",
1827
+ },
1828
+ };
1829
+ if (fitsNeedsInputEnvelope(request, params.maxEnvelopeBytes))
1830
+ return request;
1831
+ throw new Error(`Workflow input step ${params.stepId} needs_input envelope exceeds ${params.maxEnvelopeBytes} bytes even after subject truncation`);
1832
+ }
1833
+ function fitsNeedsInputEnvelope(request, maxEnvelopeBytes) {
1834
+ const envelope = {
1835
+ protocolVersion: 1,
1836
+ ok: true,
1837
+ status: "needs_input",
1838
+ output: [],
1839
+ requiresApproval: null,
1840
+ requiresInput: {
1841
+ ...request,
1842
+ resumeToken: RESUME_TOKEN_PLACEHOLDER,
1843
+ },
1844
+ };
1845
+ return estimateSerializedBytes(envelope) <= maxEnvelopeBytes;
1846
+ }
1847
+ function estimateSerializedBytes(value) {
1848
+ try {
1849
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
1850
+ }
1851
+ catch {
1852
+ return Number.POSITIVE_INFINITY;
1853
+ }
1854
+ }
1855
+ function renderTemplateValue(value) {
1856
+ if (value === undefined || value === null)
1857
+ return "";
1858
+ if (typeof value === "string")
1859
+ return value;
1860
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1861
+ return String(value);
1862
+ }
1863
+ try {
1864
+ return JSON.stringify(value);
1865
+ }
1866
+ catch {
1867
+ return String(value);
1868
+ }
1869
+ }
1870
+ function getValueByPath(value, pathValue) {
1871
+ const fields = pathValue.split(".");
1872
+ let current = value;
1873
+ for (const field of fields) {
1874
+ if (current === null || current === undefined)
1875
+ return undefined;
1876
+ if (Array.isArray(current)) {
1877
+ const idx = Number(field);
1878
+ if (!Number.isInteger(idx) || idx < 0 || idx >= current.length)
1879
+ return undefined;
1880
+ current = current[idx];
1881
+ continue;
1882
+ }
1883
+ if (typeof current !== "object")
1884
+ return undefined;
1885
+ current = current[field];
1886
+ }
1887
+ return current;
1888
+ }
1889
+ function evaluateConditionExpression(expression, results) {
1890
+ const tokens = tokenizeCondition(expression);
1891
+ if (tokens.length === 0) {
1892
+ throw new Error(`Unsupported condition: ${expression}`);
1893
+ }
1894
+ let index = 0;
1895
+ function parseOr() {
1896
+ let left = parseAnd();
1897
+ while (match("or")) {
1898
+ const right = parseAnd();
1899
+ left = Boolean(left) || Boolean(right);
1900
+ }
1901
+ return left;
1902
+ }
1903
+ function parseAnd() {
1904
+ let left = parseEquality();
1905
+ while (match("and")) {
1906
+ const right = parseEquality();
1907
+ left = Boolean(left) && Boolean(right);
1908
+ }
1909
+ return left;
1910
+ }
1911
+ function parseEquality() {
1912
+ const left = parseUnary(false);
1913
+ if (match("eq")) {
1914
+ return compareConditionValues(left, parseUnary(true));
1915
+ }
1916
+ if (match("neq")) {
1917
+ return !compareConditionValues(left, parseUnary(true));
1918
+ }
1919
+ if (match("lt")) {
1920
+ return numericCompare(left, parseUnary(true), (a, b) => a < b);
1921
+ }
1922
+ if (match("lte")) {
1923
+ return numericCompare(left, parseUnary(true), (a, b) => a <= b);
1924
+ }
1925
+ if (match("gt")) {
1926
+ return numericCompare(left, parseUnary(true), (a, b) => a > b);
1927
+ }
1928
+ if (match("gte")) {
1929
+ return numericCompare(left, parseUnary(true), (a, b) => a >= b);
1930
+ }
1931
+ return left;
1932
+ }
1933
+ function parseUnary(allowBareIdentifier) {
1934
+ if (match("not")) {
1935
+ return !Boolean(parseUnary(allowBareIdentifier));
1936
+ }
1937
+ return parsePrimary(allowBareIdentifier);
1938
+ }
1939
+ function parsePrimary(allowBareIdentifier) {
1940
+ const token = tokens[index];
1941
+ if (!token) {
1942
+ throw new Error(`Unsupported condition: ${expression}`);
1943
+ }
1944
+ index += 1;
1945
+ if (token.type === "lparen") {
1946
+ const value = parseOr();
1947
+ expect("rparen");
1948
+ return value;
1949
+ }
1950
+ if (token.type === "step_ref") {
1951
+ return getStepRefValue(token.value, results, true);
1952
+ }
1953
+ if (token.type === "string" ||
1954
+ token.type === "number" ||
1955
+ token.type === "boolean" ||
1956
+ token.type === "null") {
1957
+ return token.value;
1958
+ }
1959
+ if (token.type === "identifier" && allowBareIdentifier) {
1960
+ return token.value;
1961
+ }
1962
+ throw new Error(`Unsupported condition: ${expression}`);
1963
+ }
1964
+ function match(type) {
1965
+ if (tokens[index]?.type !== type)
1966
+ return false;
1967
+ index += 1;
1968
+ return true;
1969
+ }
1970
+ function expect(type) {
1971
+ if (!match(type)) {
1972
+ throw new Error(`Unsupported condition: ${expression}`);
1973
+ }
1974
+ }
1975
+ const value = parseOr();
1976
+ if (index !== tokens.length) {
1977
+ throw new Error(`Unsupported condition: ${expression}`);
1978
+ }
1979
+ return Boolean(value);
1980
+ }
1981
+ function compareConditionValues(left, right) {
1982
+ if (Array.isArray(left) ||
1983
+ Array.isArray(right) ||
1984
+ isPlainConditionObject(left) ||
1985
+ isPlainConditionObject(right)) {
1986
+ return isDeepStrictEqual(left, right);
1987
+ }
1988
+ return Object.is(left, right);
1989
+ }
1990
+ function isStrictlyNumeric(value) {
1991
+ if (typeof value === "number")
1992
+ return !Number.isNaN(value);
1993
+ if (typeof value === "string")
1994
+ return value.trim() !== "" && !Number.isNaN(Number(value));
1995
+ return false;
1996
+ }
1997
+ function numericCompare(left, right, cmp) {
1998
+ if (!isStrictlyNumeric(left) || !isStrictlyNumeric(right))
1999
+ return false;
2000
+ return cmp(Number(left), Number(right));
2001
+ }
2002
+ function isPlainConditionObject(value) {
2003
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2004
+ }
2005
+ function tokenizeCondition(expression) {
2006
+ const tokens = [];
2007
+ let index = 0;
2008
+ while (index < expression.length) {
2009
+ const ch = expression[index];
2010
+ if (/\s/.test(ch)) {
2011
+ index += 1;
2012
+ continue;
2013
+ }
2014
+ if (ch === "(") {
2015
+ tokens.push({ type: "lparen" });
2016
+ index += 1;
2017
+ continue;
2018
+ }
2019
+ if (ch === ")") {
2020
+ tokens.push({ type: "rparen" });
2021
+ index += 1;
2022
+ continue;
2023
+ }
2024
+ if (expression.startsWith("&&", index)) {
2025
+ tokens.push({ type: "and" });
2026
+ index += 2;
2027
+ continue;
2028
+ }
2029
+ if (expression.startsWith("||", index)) {
2030
+ tokens.push({ type: "or" });
2031
+ index += 2;
2032
+ continue;
2033
+ }
2034
+ if (expression.startsWith("==", index)) {
2035
+ tokens.push({ type: "eq" });
2036
+ index += 2;
2037
+ continue;
2038
+ }
2039
+ if (expression.startsWith("!=", index)) {
2040
+ tokens.push({ type: "neq" });
2041
+ index += 2;
2042
+ continue;
2043
+ }
2044
+ if (expression.startsWith("<=", index)) {
2045
+ tokens.push({ type: "lte" });
2046
+ index += 2;
2047
+ continue;
2048
+ }
2049
+ if (expression.startsWith(">=", index)) {
2050
+ tokens.push({ type: "gte" });
2051
+ index += 2;
2052
+ continue;
2053
+ }
2054
+ if (ch === "<") {
2055
+ tokens.push({ type: "lt" });
2056
+ index += 1;
2057
+ continue;
2058
+ }
2059
+ if (ch === ">") {
2060
+ tokens.push({ type: "gt" });
2061
+ index += 1;
2062
+ continue;
2063
+ }
2064
+ if (ch === "!") {
2065
+ tokens.push({ type: "not" });
2066
+ index += 1;
2067
+ continue;
2068
+ }
2069
+ if (ch === "$") {
2070
+ const matched = matchConditionStepRef(expression, index);
2071
+ if (!matched) {
2072
+ throw new Error(`Unsupported condition: ${expression}`);
2073
+ }
2074
+ tokens.push({ type: "step_ref", value: matched.ref });
2075
+ index = matched.nextIndex;
2076
+ continue;
2077
+ }
2078
+ if (ch === '"' || ch === "'") {
2079
+ const parsed = parseQuotedConditionString(expression, index, ch);
2080
+ tokens.push({ type: "string", value: parsed.value });
2081
+ index = parsed.nextIndex;
2082
+ continue;
2083
+ }
2084
+ const numberMatch = expression.slice(index).match(/^-?\d+(?:\.\d+)?/);
2085
+ if (numberMatch) {
2086
+ tokens.push({ type: "number", value: Number(numberMatch[0]) });
2087
+ index += numberMatch[0].length;
2088
+ continue;
2089
+ }
2090
+ const identMatch = expression.slice(index).match(/^[A-Za-z_][A-Za-z0-9_-]*/);
2091
+ if (identMatch) {
2092
+ const raw = identMatch[0];
2093
+ if (raw === "true") {
2094
+ tokens.push({ type: "boolean", value: true });
2095
+ }
2096
+ else if (raw === "false") {
2097
+ tokens.push({ type: "boolean", value: false });
2098
+ }
2099
+ else if (raw === "null") {
2100
+ tokens.push({ type: "null", value: null });
2101
+ }
2102
+ else {
2103
+ tokens.push({ type: "identifier", value: raw });
2104
+ }
2105
+ index += raw.length;
2106
+ continue;
2107
+ }
2108
+ throw new Error(`Unsupported condition: ${expression}`);
2109
+ }
2110
+ return tokens;
2111
+ }
2112
+ function matchConditionStepRef(expression, startIndex) {
2113
+ const match = expression
2114
+ .slice(startIndex)
2115
+ .match(/^\$([A-Za-z0-9_-]+)\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)/);
2116
+ if (!match)
2117
+ return null;
2118
+ return {
2119
+ ref: { id: match[1], path: match[2] },
2120
+ nextIndex: startIndex + match[0].length,
2121
+ };
2122
+ }
2123
+ function parseQuotedConditionString(expression, startIndex, quoteChar) {
2124
+ let value = "";
2125
+ let index = startIndex + 1;
2126
+ while (index < expression.length) {
2127
+ const ch = expression[index];
2128
+ if (ch === "\\") {
2129
+ const next = expression[index + 1];
2130
+ if (next === undefined)
2131
+ break;
2132
+ value += next;
2133
+ index += 2;
2134
+ continue;
2135
+ }
2136
+ if (ch === quoteChar) {
2137
+ return { value, nextIndex: index + 1 };
2138
+ }
2139
+ value += ch;
2140
+ index += 1;
2141
+ }
2142
+ throw new Error(`Unsupported condition: ${expression}`);
2143
+ }
2144
+ async function runShellCommand({ command, stdin, env, cwd, signal, killSignal, }) {
2145
+ const { spawn } = await import("node:child_process");
2146
+ return await new Promise((resolve, reject) => {
2147
+ const shell = resolveInlineShellCommand({ command, env });
2148
+ const child = spawn(shell.command, shell.argv, {
2149
+ env,
2150
+ cwd,
2151
+ signal,
2152
+ killSignal,
2153
+ stdio: ["pipe", "pipe", "pipe"],
2154
+ });
2155
+ let stdout = "";
2156
+ let stderr = "";
2157
+ child.stdout.setEncoding("utf8");
2158
+ child.stderr.setEncoding("utf8");
2159
+ child.stdout.on("data", (d) => {
2160
+ stdout += d;
2161
+ });
2162
+ child.stderr.on("data", (d) => {
2163
+ stderr += d;
2164
+ });
2165
+ if (typeof stdin === "string") {
2166
+ child.stdin.setDefaultEncoding("utf8");
2167
+ child.stdin.write(stdin);
2168
+ }
2169
+ child.stdin.end();
2170
+ child.on("error", reject);
2171
+ child.on("close", (code) => {
2172
+ if (code === 0)
2173
+ return resolve({ stdout, stderr });
2174
+ reject(new Error(`workflow command failed (${code}): ${stderr.trim() || stdout.trim() || command}`));
2175
+ });
2176
+ });
2177
+ }
2178
+ function getStepExecution(step) {
2179
+ if (step.parallel && typeof step.parallel === "object" && !Array.isArray(step.parallel)) {
2180
+ return { kind: "parallel", value: step.parallel };
2181
+ }
2182
+ if (typeof step.workflow === "string" && step.workflow.trim()) {
2183
+ return { kind: "workflow", value: step.workflow };
2184
+ }
2185
+ if (typeof step.pipeline === "string" && step.pipeline.trim()) {
2186
+ return { kind: "pipeline", value: step.pipeline };
2187
+ }
2188
+ const shellCommand = typeof step.run === "string" ? step.run : step.command;
2189
+ if (typeof shellCommand === "string" && shellCommand.trim()) {
2190
+ return { kind: "shell", value: shellCommand };
2191
+ }
2192
+ return { kind: "none" };
2193
+ }
2194
+ async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd, resume, requestInputEnabled = true, }) {
2195
+ let pipeline;
2196
+ try {
2197
+ const currentPipeline = parsePipeline(pipelineText);
2198
+ if (resume) {
2199
+ if (!isDeepStrictEqual(currentPipeline, resume.pipelineInput.pipeline)) {
2200
+ throw new RequestInputResumeError("workflow pipeline changed since input request");
2201
+ }
2202
+ pipeline = resume.pipelineInput.pipeline;
2203
+ }
2204
+ else {
2205
+ pipeline = currentPipeline;
2206
+ }
2207
+ }
2208
+ catch (err) {
2209
+ if (err instanceof RequestInputResumeError)
2210
+ throw err;
2211
+ throw new Error(`Workflow step ${stepId} pipeline parse failed: ${err?.message ?? String(err)}`);
2212
+ }
2213
+ const stdout = new PassThrough();
2214
+ let renderedStdout = "";
2215
+ stdout.setEncoding("utf8");
2216
+ stdout.on("data", (chunk) => {
2217
+ renderedStdout += String(chunk);
2218
+ });
2219
+ const pipelineStartIndex = resume ? resume.pipelineInput.resumeAtIndex : 0;
2220
+ const remainingPipeline = pipeline.slice(pipelineStartIndex);
2221
+ const result = await runPipeline({
2222
+ pipeline: remainingPipeline,
2223
+ registry: ctx.registry,
2224
+ stdin: ctx.stdin,
2225
+ stdout,
2226
+ stderr: ctx.stderr,
2227
+ env,
2228
+ mode: ctx.mode,
2229
+ cwd,
2230
+ signal: ctx.signal,
2231
+ llmAdapters: ctx.llmAdapters,
2232
+ input: resume ? resume.pipelineInput.items : inputValueToPipelineItems(inputValue),
2233
+ requestInputEnabled,
2234
+ requestInputResume: resume
2235
+ ? {
2236
+ state: resume.pipelineInput.commandInput,
2237
+ response: resume.response,
2238
+ onConsumed: resume.onConsumed,
2239
+ }
2240
+ : undefined,
2241
+ });
2242
+ stdout.end();
2243
+ if (result.halted) {
2244
+ const haltedName = result.haltedAt?.stage?.name ?? "unknown";
2245
+ if (result.items.length === 1 && result.items[0]?.type === "approval_request") {
2246
+ throw new Error(`Workflow step ${stepId} halted for approval inside pipeline stage ${haltedName}. Use a separate approval step in the workflow file.`);
2247
+ }
2248
+ const request = result.items.length === 1 && result.items[0]?.type === "input_request"
2249
+ ? result.items[0]
2250
+ : null;
2251
+ if (request?.commandInput) {
2252
+ throw new WorkflowPipelineInputSuspension({
2253
+ stepId,
2254
+ request: {
2255
+ prompt: String(request.prompt),
2256
+ responseSchema: request.responseSchema,
2257
+ ...(request.defaults !== undefined ? { defaults: request.defaults } : null),
2258
+ ...(request.subject !== undefined ? { subject: request.subject } : null),
2259
+ },
2260
+ pipelineInput: {
2261
+ pipeline,
2262
+ resumeAtIndex: pipelineStartIndex + (result.haltedAt?.index ?? 0),
2263
+ items: Array.isArray(request.items) ? request.items : [],
2264
+ commandInput: request.commandInput,
2265
+ },
2266
+ });
2267
+ }
2268
+ throw new Error(`Workflow step ${stepId} halted before completion at pipeline stage ${haltedName}`);
2269
+ }
2270
+ const normalizedStdout = renderedStdout || serializePipelineItemsToStdout(result.items);
2271
+ const json = result.items.length
2272
+ ? result.items.length === 1
2273
+ ? result.items[0]
2274
+ : result.items
2275
+ : parseJson(renderedStdout);
2276
+ return {
2277
+ id: stepId,
2278
+ stdout: normalizedStdout,
2279
+ json,
2280
+ };
2281
+ }
2282
+ function createSyntheticStepResult(stepId, value) {
2283
+ if (value === null || value === undefined) {
2284
+ return { id: stepId };
2285
+ }
2286
+ if (typeof value === "string") {
2287
+ return {
2288
+ id: stepId,
2289
+ stdout: value,
2290
+ json: parseJson(value),
2291
+ };
2292
+ }
2293
+ return {
2294
+ id: stepId,
2295
+ stdout: serializeValueForStdout(value),
2296
+ json: value,
2297
+ };
2298
+ }
2299
+ function abortableSleep(ms, signal) {
2300
+ if (!signal) {
2301
+ return new Promise((resolve) => setTimeout(resolve, ms));
2302
+ }
2303
+ return new Promise((resolve, reject) => {
2304
+ if (signal.aborted) {
2305
+ reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
2306
+ return;
2307
+ }
2308
+ let timer;
2309
+ const onAbort = () => {
2310
+ clearTimeout(timer);
2311
+ reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
2312
+ };
2313
+ timer = setTimeout(() => {
2314
+ signal.removeEventListener("abort", onAbort);
2315
+ resolve();
2316
+ }, ms);
2317
+ signal.addEventListener("abort", onAbort, { once: true });
2318
+ });
2319
+ }
2320
+ function encodeShellInput(value) {
2321
+ if (value === null || value === undefined)
2322
+ return null;
2323
+ if (typeof value === "string")
2324
+ return value;
2325
+ return JSON.stringify(value);
2326
+ }
2327
+ function* inputValueToItems(value) {
2328
+ if (value === null || value === undefined)
2329
+ return;
2330
+ if (Array.isArray(value)) {
2331
+ for (const item of value)
2332
+ yield item;
2333
+ return;
2334
+ }
2335
+ yield value;
2336
+ }
2337
+ function inputValueToPipelineItems(value) {
2338
+ return [...inputValueToItems(value)];
2339
+ }
2340
+ function serializePipelineItemsToStdout(items) {
2341
+ if (!items.length)
2342
+ return "";
2343
+ if (items.every((item) => typeof item === "string")) {
2344
+ return items.map((item) => String(item)).join("\n");
2345
+ }
2346
+ if (items.length === 1) {
2347
+ return serializeValueForStdout(items[0]);
2348
+ }
2349
+ return JSON.stringify(items);
2350
+ }
2351
+ function serializeValueForStdout(value) {
2352
+ if (value === null || value === undefined)
2353
+ return "";
2354
+ if (typeof value === "string")
2355
+ return value;
2356
+ return JSON.stringify(value);
2357
+ }
2358
+ function normalizeApprovalConfig(approval) {
2359
+ if (approval === true ||
2360
+ approval === "required" ||
2361
+ approval === undefined ||
2362
+ approval === false) {
2363
+ return {};
2364
+ }
2365
+ if (typeof approval === "string") {
2366
+ return { prompt: approval };
2367
+ }
2368
+ if (approval && typeof approval === "object" && !Array.isArray(approval)) {
2369
+ const value = approval;
2370
+ return {
2371
+ ...(typeof value.prompt === "string" ? { prompt: value.prompt } : null),
2372
+ ...(Array.isArray(value.items) ? { items: value.items } : null),
2373
+ ...(typeof value.preview === "string" ? { preview: value.preview } : null),
2374
+ ...(typeof value.initiatedBy === "string"
2375
+ ? { initiatedBy: value.initiatedBy }
2376
+ : typeof value.initiated_by === "string"
2377
+ ? { initiatedBy: value.initiated_by }
2378
+ : null),
2379
+ ...(typeof value.requiredApprover === "string"
2380
+ ? { requiredApprover: value.requiredApprover }
2381
+ : typeof value.required_approver === "string"
2382
+ ? { requiredApprover: value.required_approver }
2383
+ : null),
2384
+ ...(typeof value.requireDifferentApprover === "boolean"
2385
+ ? { requireDifferentApprover: value.requireDifferentApprover }
2386
+ : typeof value.require_different_approver === "boolean"
2387
+ ? { requireDifferentApprover: value.require_different_approver }
2388
+ : null),
2389
+ };
2390
+ }
2391
+ return {};
2392
+ }
2393
+ function normalizeApprovalItems(value) {
2394
+ if (value === undefined)
2395
+ return [];
2396
+ return Array.isArray(value) ? value : [value];
2397
+ }
2398
+ function buildResultPreview(result) {
2399
+ if (result.stdout)
2400
+ return result.stdout.trim().slice(0, 2000);
2401
+ if (result.json !== undefined)
2402
+ return serializeValueForStdout(result.json).trim().slice(0, 2000);
2403
+ return undefined;
2404
+ }
2405
+ //# sourceMappingURL=file.js.map