@davidorex/pi-workflows 0.2.0 → 0.3.1

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 (216) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/README.md +12 -2
  3. package/agents/architecture-designer.agent.yaml +58 -0
  4. package/agents/architecture-inferrer.agent.yaml +60 -0
  5. package/agents/gap-identifier.agent.yaml +66 -0
  6. package/agents/handoff-writer.agent.yaml +64 -0
  7. package/agents/plan-creator.agent.yaml +63 -0
  8. package/agents/project-definer.agent.yaml +53 -0
  9. package/agents/project-inferrer.agent.yaml +63 -0
  10. package/agents/requirements-gatherer.agent.yaml +58 -0
  11. package/dist/agent-spec.d.ts +27 -0
  12. package/dist/agent-spec.d.ts.map +1 -0
  13. package/dist/agent-spec.js +112 -0
  14. package/dist/agent-spec.js.map +1 -0
  15. package/dist/checkpoint.d.ts +50 -0
  16. package/dist/checkpoint.d.ts.map +1 -0
  17. package/dist/checkpoint.js +109 -0
  18. package/dist/checkpoint.js.map +1 -0
  19. package/dist/completion.d.ts +18 -0
  20. package/dist/completion.d.ts.map +1 -0
  21. package/dist/completion.js +58 -0
  22. package/dist/completion.js.map +1 -0
  23. package/dist/dag.d.ts +56 -0
  24. package/dist/dag.d.ts.map +1 -0
  25. package/dist/dag.js +178 -0
  26. package/dist/dag.js.map +1 -0
  27. package/dist/dispatch.d.ts +35 -0
  28. package/dist/dispatch.d.ts.map +1 -0
  29. package/dist/dispatch.js +311 -0
  30. package/dist/dispatch.js.map +1 -0
  31. package/dist/expression.d.ts +64 -0
  32. package/dist/expression.d.ts.map +1 -0
  33. package/dist/expression.js +294 -0
  34. package/dist/expression.js.map +1 -0
  35. package/dist/format.d.ts +19 -0
  36. package/dist/format.d.ts.map +1 -0
  37. package/dist/format.js +42 -0
  38. package/dist/format.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +724 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/output.d.ts +12 -0
  44. package/dist/output.d.ts.map +1 -0
  45. package/dist/output.js +59 -0
  46. package/dist/output.js.map +1 -0
  47. package/dist/state.d.ts +66 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +227 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/step-agent.d.ts +35 -0
  52. package/dist/step-agent.d.ts.map +1 -0
  53. package/dist/step-agent.js +171 -0
  54. package/dist/step-agent.js.map +1 -0
  55. package/dist/step-command.d.ts +21 -0
  56. package/dist/step-command.d.ts.map +1 -0
  57. package/dist/step-command.js +150 -0
  58. package/dist/step-command.js.map +1 -0
  59. package/dist/step-foreach.d.ts +33 -0
  60. package/dist/step-foreach.d.ts.map +1 -0
  61. package/dist/step-foreach.js +112 -0
  62. package/dist/step-foreach.js.map +1 -0
  63. package/dist/step-gate.d.ts +18 -0
  64. package/dist/step-gate.d.ts.map +1 -0
  65. package/dist/step-gate.js +137 -0
  66. package/dist/step-gate.js.map +1 -0
  67. package/dist/step-loop.d.ts +32 -0
  68. package/dist/step-loop.d.ts.map +1 -0
  69. package/dist/step-loop.js +278 -0
  70. package/dist/step-loop.js.map +1 -0
  71. package/dist/step-monitor.d.ts +56 -0
  72. package/dist/step-monitor.d.ts.map +1 -0
  73. package/dist/step-monitor.js +317 -0
  74. package/dist/step-monitor.js.map +1 -0
  75. package/dist/step-parallel.d.ts +43 -0
  76. package/dist/step-parallel.d.ts.map +1 -0
  77. package/dist/step-parallel.js +122 -0
  78. package/dist/step-parallel.js.map +1 -0
  79. package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
  80. package/dist/step-pause.d.ts.map +1 -0
  81. package/dist/step-pause.js +22 -0
  82. package/dist/step-pause.js.map +1 -0
  83. package/dist/step-shared.d.ts +63 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +136 -0
  86. package/dist/step-shared.js.map +1 -0
  87. package/dist/step-transform.d.ts +11 -0
  88. package/dist/step-transform.d.ts.map +1 -0
  89. package/dist/step-transform.js +41 -0
  90. package/dist/step-transform.js.map +1 -0
  91. package/dist/template.d.ts +51 -0
  92. package/dist/template.d.ts.map +1 -0
  93. package/{src/template.ts → dist/template.js} +32 -38
  94. package/dist/template.js.map +1 -0
  95. package/dist/test-helpers.d.ts +17 -0
  96. package/dist/test-helpers.d.ts.map +1 -0
  97. package/dist/test-helpers.js +46 -0
  98. package/dist/test-helpers.js.map +1 -0
  99. package/dist/tui.d.ts +47 -0
  100. package/dist/tui.d.ts.map +1 -0
  101. package/dist/tui.js +143 -0
  102. package/dist/tui.js.map +1 -0
  103. package/dist/types.d.ts +165 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +3 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/workflow-discovery.d.ts +23 -0
  108. package/dist/workflow-discovery.d.ts.map +1 -0
  109. package/dist/workflow-discovery.js +103 -0
  110. package/dist/workflow-discovery.js.map +1 -0
  111. package/dist/workflow-executor.d.ts +50 -0
  112. package/dist/workflow-executor.d.ts.map +1 -0
  113. package/dist/workflow-executor.js +738 -0
  114. package/dist/workflow-executor.js.map +1 -0
  115. package/dist/workflow-sdk.d.ts +44 -0
  116. package/dist/workflow-sdk.d.ts.map +1 -0
  117. package/dist/workflow-sdk.js +457 -0
  118. package/dist/workflow-sdk.js.map +1 -0
  119. package/dist/workflow-spec.d.ts +33 -0
  120. package/dist/workflow-spec.d.ts.map +1 -0
  121. package/dist/workflow-spec.js +476 -0
  122. package/dist/workflow-spec.js.map +1 -0
  123. package/dist/workflows-dir.d.ts +3 -0
  124. package/dist/workflows-dir.d.ts.map +1 -0
  125. package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
  126. package/dist/workflows-dir.js.map +1 -0
  127. package/package.json +53 -38
  128. package/schemas/decomposition-specs.schema.json +48 -48
  129. package/schemas/execution-results.schema.json +48 -48
  130. package/schemas/investigation-findings.schema.json +43 -43
  131. package/schemas/pattern-analysis.schema.json +41 -41
  132. package/schemas/phase.schema.json +48 -0
  133. package/schemas/plan-breakdown.schema.json +20 -20
  134. package/schemas/quality-analysis.schema.json +33 -33
  135. package/schemas/research-findings.schema.json +42 -42
  136. package/schemas/structure-analysis.schema.json +40 -40
  137. package/schemas/synthesis.schema.json +31 -31
  138. package/schemas/verifier-output.schema.json +94 -94
  139. package/skill-narrative.md +107 -0
  140. package/skills/pi-workflows/SKILL.md +185 -0
  141. package/skills/pi-workflows/references/bundled-resources.md +87 -0
  142. package/templates/architecture-designer/task.md +117 -0
  143. package/templates/architecture-inferrer/task.md +61 -0
  144. package/templates/gap-identifier/task.md +103 -0
  145. package/templates/handoff-writer/task.md +91 -0
  146. package/templates/plan-creator/task.md +143 -0
  147. package/templates/project-definer/task.md +67 -0
  148. package/templates/project-inferrer/task.md +56 -0
  149. package/templates/requirements-gatherer/task.md +90 -0
  150. package/workflows/analyze-existing-project.workflow.yaml +94 -0
  151. package/workflows/create-handoff.workflow.yaml +66 -0
  152. package/workflows/create-phase.workflow.yaml +1 -1
  153. package/workflows/do-gap.workflow.yaml +4 -4
  154. package/workflows/fix-audit.workflow.yaml +1 -1
  155. package/workflows/gap-to-phase.workflow.yaml +1 -1
  156. package/workflows/init-new-project.workflow.yaml +81 -0
  157. package/workflows/plan-from-requirements.workflow.yaml +59 -0
  158. package/workflows/self-implement.workflow.yaml +3 -3
  159. package/workflows/typed-analysis.workflow.yaml +4 -4
  160. package/src/agent-spec.test.ts +0 -289
  161. package/src/agent-spec.ts +0 -122
  162. package/src/block-validation.test.ts +0 -350
  163. package/src/checkpoint.test.ts +0 -237
  164. package/src/checkpoint.ts +0 -140
  165. package/src/completion.test.ts +0 -143
  166. package/src/completion.ts +0 -68
  167. package/src/dag.test.ts +0 -350
  168. package/src/dag.ts +0 -219
  169. package/src/dispatch.test.ts +0 -475
  170. package/src/dispatch.ts +0 -353
  171. package/src/expression.test.ts +0 -353
  172. package/src/expression.ts +0 -332
  173. package/src/format.test.ts +0 -80
  174. package/src/format.ts +0 -41
  175. package/src/graduated-failure.test.ts +0 -556
  176. package/src/index.test.ts +0 -114
  177. package/src/index.ts +0 -516
  178. package/src/loop.test.ts +0 -551
  179. package/src/output.test.ts +0 -213
  180. package/src/output.ts +0 -70
  181. package/src/parallel-integration.test.ts +0 -175
  182. package/src/resume.test.ts +0 -192
  183. package/src/state.test.ts +0 -192
  184. package/src/state.ts +0 -254
  185. package/src/step-agent.test.ts +0 -327
  186. package/src/step-agent.ts +0 -178
  187. package/src/step-command.test.ts +0 -330
  188. package/src/step-command.ts +0 -132
  189. package/src/step-foreach.test.ts +0 -647
  190. package/src/step-foreach.ts +0 -148
  191. package/src/step-gate.test.ts +0 -185
  192. package/src/step-gate.ts +0 -116
  193. package/src/step-loop.test.ts +0 -626
  194. package/src/step-loop.ts +0 -323
  195. package/src/step-parallel.test.ts +0 -475
  196. package/src/step-parallel.ts +0 -168
  197. package/src/step-pause.test.ts +0 -30
  198. package/src/step-shared.test.ts +0 -355
  199. package/src/step-shared.ts +0 -157
  200. package/src/step-transform.test.ts +0 -155
  201. package/src/step-transform.ts +0 -47
  202. package/src/template-integration.test.ts +0 -72
  203. package/src/template.test.ts +0 -162
  204. package/src/test-helpers.ts +0 -51
  205. package/src/tui.test.ts +0 -355
  206. package/src/tui.ts +0 -182
  207. package/src/types.ts +0 -195
  208. package/src/verifier-schema.test.ts +0 -226
  209. package/src/workflow-discovery.test.ts +0 -105
  210. package/src/workflow-discovery.ts +0 -114
  211. package/src/workflow-executor.test.ts +0 -1532
  212. package/src/workflow-executor.ts +0 -810
  213. package/src/workflow-sdk.test.ts +0 -238
  214. package/src/workflow-sdk.ts +0 -262
  215. package/src/workflow-spec.test.ts +0 -576
  216. package/src/workflow-spec.ts +0 -471
@@ -1,647 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import os from "node:os";
6
- import { executeWorkflow } from "./workflow-executor.ts";
7
- import type { WorkflowSpec, StepResult, StepSpec, AgentSpec } from "./types.ts";
8
- import { mockCtx, mockPi, makeSpec } from "./test-helpers.ts";
9
- import { zeroUsage } from "./step-shared.ts";
10
-
11
- describe("forEach steps", () => {
12
- it("iterates over array with transform body", async () => {
13
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
14
- const spec: WorkflowSpec = {
15
- name: "test-foreach",
16
- description: "test forEach step",
17
- input: {
18
- type: "object",
19
- properties: {
20
- items: { type: "array" },
21
- },
22
- },
23
- steps: {
24
- process: {
25
- forEach: "${{ input.items }}",
26
- transform: {
27
- mapping: {
28
- value: "${{ item.name }}",
29
- upper: "${{ item.name }}",
30
- },
31
- },
32
- },
33
- },
34
- source: "project",
35
- filePath: path.join(tmpDir, "test.workflow.yaml"),
36
- };
37
-
38
- const result = await executeWorkflow(spec, { items: [{ name: "alice" }, { name: "bob" }] }, {
39
- ctx: mockCtx(tmpDir),
40
- pi: mockPi(),
41
- loadAgent: () => ({ name: "default" }),
42
- });
43
-
44
- assert.strictEqual(result.status, "completed");
45
- assert.strictEqual(result.steps.process.status, "completed");
46
- const output = result.steps.process.output as unknown[];
47
- assert.strictEqual(output.length, 2);
48
- assert.deepStrictEqual(output[0], { value: "alice", upper: "alice" });
49
- assert.deepStrictEqual(output[1], { value: "bob", upper: "bob" });
50
-
51
- fs.rmSync(tmpDir, { recursive: true });
52
- });
53
-
54
- it("uses custom as binding name", async () => {
55
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
56
- const spec: WorkflowSpec = {
57
- name: "test-foreach-as",
58
- description: "test forEach with as",
59
- input: {
60
- type: "object",
61
- properties: {
62
- users: { type: "array" },
63
- },
64
- },
65
- steps: {
66
- greet: {
67
- forEach: "${{ input.users }}",
68
- as: "user",
69
- transform: {
70
- mapping: {
71
- greeting: "Hello ${{ user.name }}",
72
- },
73
- },
74
- },
75
- },
76
- source: "project",
77
- filePath: path.join(tmpDir, "test.workflow.yaml"),
78
- };
79
-
80
- const result = await executeWorkflow(spec, { users: [{ name: "Alice" }, { name: "Bob" }] }, {
81
- ctx: mockCtx(tmpDir),
82
- pi: mockPi(),
83
- loadAgent: () => ({ name: "default" }),
84
- });
85
-
86
- assert.strictEqual(result.status, "completed");
87
- const output = result.steps.greet.output as unknown[];
88
- assert.strictEqual(output.length, 2);
89
- assert.deepStrictEqual(output[0], { greeting: "Hello Alice" });
90
- assert.deepStrictEqual(output[1], { greeting: "Hello Bob" });
91
-
92
- fs.rmSync(tmpDir, { recursive: true });
93
- });
94
-
95
- it("returns completed with empty output for empty array", async () => {
96
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
97
- const spec: WorkflowSpec = {
98
- name: "test-foreach-empty",
99
- description: "test forEach empty array",
100
- input: {
101
- type: "object",
102
- properties: {
103
- items: { type: "array" },
104
- },
105
- },
106
- steps: {
107
- process: {
108
- forEach: "${{ input.items }}",
109
- transform: {
110
- mapping: { value: "${{ item }}" },
111
- },
112
- },
113
- },
114
- source: "project",
115
- filePath: path.join(tmpDir, "test.workflow.yaml"),
116
- };
117
-
118
- const result = await executeWorkflow(spec, { items: [] }, {
119
- ctx: mockCtx(tmpDir),
120
- pi: mockPi(),
121
- loadAgent: () => ({ name: "default" }),
122
- });
123
-
124
- assert.strictEqual(result.status, "completed");
125
- assert.strictEqual(result.steps.process.status, "completed");
126
- const output = result.steps.process.output as unknown[];
127
- assert.deepStrictEqual(output, []);
128
-
129
- fs.rmSync(tmpDir, { recursive: true });
130
- });
131
-
132
- it("fails when forEach expression resolves to non-array", async () => {
133
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
134
- const spec: WorkflowSpec = {
135
- name: "test-foreach-nonarr",
136
- description: "test forEach non-array",
137
- input: {
138
- type: "object",
139
- properties: {
140
- data: { type: "string" },
141
- },
142
- },
143
- steps: {
144
- process: {
145
- forEach: "${{ input.data }}",
146
- transform: {
147
- mapping: { value: "${{ item }}" },
148
- },
149
- },
150
- },
151
- source: "project",
152
- filePath: path.join(tmpDir, "test.workflow.yaml"),
153
- };
154
-
155
- const result = await executeWorkflow(spec, { data: "not-an-array" }, {
156
- ctx: mockCtx(tmpDir),
157
- pi: mockPi(),
158
- loadAgent: () => ({ name: "default" }),
159
- });
160
-
161
- assert.strictEqual(result.status, "failed");
162
- assert.strictEqual(result.steps.process.status, "failed");
163
- assert.ok(result.steps.process.error?.includes("must resolve to an array"));
164
-
165
- fs.rmSync(tmpDir, { recursive: true });
166
- });
167
-
168
- it("fails when one iteration fails", async () => {
169
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
170
- const spec: WorkflowSpec = {
171
- name: "test-foreach-fail",
172
- description: "test forEach iteration failure",
173
- input: {
174
- type: "object",
175
- properties: {
176
- commands: { type: "array" },
177
- },
178
- },
179
- steps: {
180
- run: {
181
- forEach: "${{ input.commands }}",
182
- command: "${{ item }}",
183
- },
184
- },
185
- source: "project",
186
- filePath: path.join(tmpDir, "test.workflow.yaml"),
187
- };
188
-
189
- const result = await executeWorkflow(spec, { commands: ["echo ok", "exit 1", "echo never"] }, {
190
- ctx: mockCtx(tmpDir),
191
- pi: mockPi(),
192
- loadAgent: () => ({ name: "default" }),
193
- });
194
-
195
- assert.strictEqual(result.status, "failed");
196
- assert.strictEqual(result.steps.run.status, "failed");
197
- // First iteration should have succeeded
198
- assert.strictEqual(result.steps["run[0]"].status, "completed");
199
- // Second iteration failed
200
- assert.strictEqual(result.steps["run[1]"].status, "failed");
201
- // Third iteration should not have run
202
- assert.ok(!result.steps["run[2]"]);
203
-
204
- fs.rmSync(tmpDir, { recursive: true });
205
- });
206
-
207
- it("exposes forEach.index and forEach.length in scope", async () => {
208
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
209
- const spec: WorkflowSpec = {
210
- name: "test-foreach-meta",
211
- description: "test forEach metadata",
212
- input: {
213
- type: "object",
214
- properties: {
215
- items: { type: "array" },
216
- },
217
- },
218
- steps: {
219
- process: {
220
- forEach: "${{ input.items }}",
221
- transform: {
222
- mapping: {
223
- idx: "${{ forEach.index }}",
224
- len: "${{ forEach.length }}",
225
- val: "${{ item }}",
226
- },
227
- },
228
- },
229
- },
230
- source: "project",
231
- filePath: path.join(tmpDir, "test.workflow.yaml"),
232
- };
233
-
234
- const result = await executeWorkflow(spec, { items: ["a", "b", "c"] }, {
235
- ctx: mockCtx(tmpDir),
236
- pi: mockPi(),
237
- loadAgent: () => ({ name: "default" }),
238
- });
239
-
240
- assert.strictEqual(result.status, "completed");
241
- const output = result.steps.process.output as any[];
242
- assert.strictEqual(output.length, 3);
243
- assert.deepStrictEqual(output[0], { idx: 0, len: 3, val: "a" });
244
- assert.deepStrictEqual(output[1], { idx: 1, len: 3, val: "b" });
245
- assert.deepStrictEqual(output[2], { idx: 2, len: 3, val: "c" });
246
-
247
- fs.rmSync(tmpDir, { recursive: true });
248
- });
249
-
250
- it("collects outputs into array", async () => {
251
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
252
- const spec: WorkflowSpec = {
253
- name: "test-foreach-collect",
254
- description: "test forEach output collection",
255
- input: {
256
- type: "object",
257
- properties: {
258
- numbers: { type: "array" },
259
- },
260
- },
261
- steps: {
262
- double: {
263
- forEach: "${{ input.numbers }}",
264
- command: "echo ${{ item }}",
265
- },
266
- },
267
- source: "project",
268
- filePath: path.join(tmpDir, "test.workflow.yaml"),
269
- };
270
-
271
- const result = await executeWorkflow(spec, { numbers: [1, 2, 3] }, {
272
- ctx: mockCtx(tmpDir),
273
- pi: mockPi(),
274
- loadAgent: () => ({ name: "default" }),
275
- });
276
-
277
- assert.strictEqual(result.status, "completed");
278
- const output = result.steps.double.output as any[];
279
- assert.strictEqual(output.length, 3);
280
- // Each output is { text: "N" }
281
- assert.deepStrictEqual(output[0], { text: "1" });
282
- assert.deepStrictEqual(output[1], { text: "2" });
283
- assert.deepStrictEqual(output[2], { text: "3" });
284
-
285
- fs.rmSync(tmpDir, { recursive: true });
286
- });
287
-
288
- it("forEach with gate body", async () => {
289
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
290
- const spec: WorkflowSpec = {
291
- name: "test-foreach-gate",
292
- description: "test forEach with gate step body",
293
- input: {
294
- type: "object",
295
- properties: {
296
- checks: { type: "array" },
297
- },
298
- },
299
- steps: {
300
- verify: {
301
- forEach: "${{ input.checks }}",
302
- gate: {
303
- check: "${{ item }}",
304
- },
305
- },
306
- },
307
- source: "project",
308
- filePath: path.join(tmpDir, "test.workflow.yaml"),
309
- };
310
-
311
- const result = await executeWorkflow(spec, { checks: ["echo pass1", "echo pass2"] }, {
312
- ctx: mockCtx(tmpDir),
313
- pi: mockPi(),
314
- loadAgent: () => ({ name: "default" }),
315
- });
316
-
317
- assert.strictEqual(result.status, "completed");
318
- assert.strictEqual(result.steps.verify.status, "completed");
319
- const output = result.steps.verify.output as any[];
320
- assert.strictEqual(output.length, 2);
321
-
322
- fs.rmSync(tmpDir, { recursive: true });
323
- });
324
-
325
- it("forEach over array of strings", async () => {
326
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
327
- const spec: WorkflowSpec = {
328
- name: "test-foreach-strings",
329
- description: "test forEach over strings",
330
- input: {
331
- type: "object",
332
- properties: {
333
- names: { type: "array" },
334
- },
335
- },
336
- steps: {
337
- greet: {
338
- forEach: "${{ input.names }}",
339
- transform: {
340
- mapping: {
341
- msg: "Hi ${{ item }}",
342
- },
343
- },
344
- },
345
- },
346
- source: "project",
347
- filePath: path.join(tmpDir, "test.workflow.yaml"),
348
- };
349
-
350
- const result = await executeWorkflow(spec, { names: ["Alice", "Bob"] }, {
351
- ctx: mockCtx(tmpDir),
352
- pi: mockPi(),
353
- loadAgent: () => ({ name: "default" }),
354
- });
355
-
356
- assert.strictEqual(result.status, "completed");
357
- const output = result.steps.greet.output as any[];
358
- assert.deepStrictEqual(output[0], { msg: "Hi Alice" });
359
- assert.deepStrictEqual(output[1], { msg: "Hi Bob" });
360
-
361
- fs.rmSync(tmpDir, { recursive: true });
362
- });
363
-
364
- it("forEach with when conditional on the forEach step itself", async () => {
365
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
366
- const spec: WorkflowSpec = {
367
- name: "test-foreach-when",
368
- description: "test forEach with when (global condition)",
369
- input: {
370
- type: "object",
371
- properties: {
372
- enabled: { type: "boolean" },
373
- items: { type: "array" },
374
- },
375
- },
376
- steps: {
377
- process: {
378
- forEach: "${{ input.items }}",
379
- when: "${{ input.enabled }}",
380
- transform: {
381
- mapping: {
382
- val: "${{ item }}",
383
- },
384
- },
385
- },
386
- },
387
- source: "project",
388
- filePath: path.join(tmpDir, "test.workflow.yaml"),
389
- };
390
-
391
- // When enabled is false, the forEach step is skipped entirely
392
- const resultSkipped = await executeWorkflow(spec, { enabled: false, items: ["a", "b"] }, {
393
- ctx: mockCtx(tmpDir),
394
- pi: mockPi(),
395
- loadAgent: () => ({ name: "default" }),
396
- });
397
- assert.strictEqual(resultSkipped.status, "completed");
398
- assert.strictEqual(resultSkipped.steps.process.status, "skipped");
399
-
400
- // When enabled is true, the forEach step runs
401
- const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
402
- const spec2 = { ...spec, filePath: path.join(tmpDir2, "test.workflow.yaml") };
403
- const resultRan = await executeWorkflow(spec2, { enabled: true, items: ["a", "b"] }, {
404
- ctx: mockCtx(tmpDir2),
405
- pi: mockPi(),
406
- loadAgent: () => ({ name: "default" }),
407
- });
408
- assert.strictEqual(resultRan.status, "completed");
409
- assert.strictEqual(resultRan.steps.process.status, "completed");
410
- const output = resultRan.steps.process.output as any[];
411
- assert.strictEqual(output.length, 2);
412
-
413
- fs.rmSync(tmpDir, { recursive: true });
414
- fs.rmSync(tmpDir2, { recursive: true });
415
- });
416
-
417
- it("forEach downstream step can reference forEach output", async () => {
418
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
419
- const spec: WorkflowSpec = {
420
- name: "test-foreach-downstream",
421
- description: "test forEach output consumed by next step",
422
- input: {
423
- type: "object",
424
- properties: {
425
- items: { type: "array" },
426
- },
427
- },
428
- steps: {
429
- process: {
430
- forEach: "${{ input.items }}",
431
- transform: {
432
- mapping: {
433
- val: "${{ item }}",
434
- },
435
- },
436
- },
437
- summary: {
438
- transform: {
439
- mapping: {
440
- results: "${{ steps.process.output }}",
441
- },
442
- },
443
- },
444
- },
445
- source: "project",
446
- filePath: path.join(tmpDir, "test.workflow.yaml"),
447
- };
448
-
449
- const result = await executeWorkflow(spec, { items: ["x", "y"] }, {
450
- ctx: mockCtx(tmpDir),
451
- pi: mockPi(),
452
- loadAgent: () => ({ name: "default" }),
453
- });
454
-
455
- assert.strictEqual(result.status, "completed");
456
- const summaryOutput = result.steps.summary.output as any;
457
- assert.ok(Array.isArray(summaryOutput.results));
458
- assert.strictEqual(summaryOutput.results.length, 2);
459
-
460
- fs.rmSync(tmpDir, { recursive: true });
461
- });
462
-
463
- it("forEach with output path", async () => {
464
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
465
- const spec: WorkflowSpec = {
466
- name: "test-foreach-outpath",
467
- description: "test forEach output path",
468
- input: {
469
- type: "object",
470
- properties: {
471
- items: { type: "array" },
472
- },
473
- },
474
- steps: {
475
- process: {
476
- forEach: "${{ input.items }}",
477
- transform: {
478
- mapping: {
479
- val: "${{ item }}",
480
- },
481
- },
482
- },
483
- },
484
- source: "project",
485
- filePath: path.join(tmpDir, "test.workflow.yaml"),
486
- };
487
-
488
- const result = await executeWorkflow(spec, { items: ["a", "b"] }, {
489
- ctx: mockCtx(tmpDir),
490
- pi: mockPi(),
491
- loadAgent: () => ({ name: "default" }),
492
- });
493
-
494
- assert.strictEqual(result.status, "completed");
495
- // The forEach step itself should have persisted output
496
- assert.ok(result.steps.process.outputPath);
497
-
498
- fs.rmSync(tmpDir, { recursive: true });
499
- });
500
-
501
- it("forEach with command body", async () => {
502
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
503
- const spec: WorkflowSpec = {
504
- name: "test-foreach-cmd",
505
- description: "test forEach with command body",
506
- input: {
507
- type: "object",
508
- properties: {
509
- files: { type: "array" },
510
- },
511
- },
512
- steps: {
513
- run: {
514
- forEach: "${{ input.files }}",
515
- command: "echo processing ${{ item }}",
516
- },
517
- },
518
- source: "project",
519
- filePath: path.join(tmpDir, "test.workflow.yaml"),
520
- };
521
-
522
- const result = await executeWorkflow(spec, { files: ["a.txt", "b.txt"] }, {
523
- ctx: mockCtx(tmpDir),
524
- pi: mockPi(),
525
- loadAgent: () => ({ name: "default" }),
526
- });
527
-
528
- assert.strictEqual(result.status, "completed");
529
- const output = result.steps.run.output as any[];
530
- assert.strictEqual(output.length, 2);
531
- assert.deepStrictEqual(output[0], { text: "processing a.txt" });
532
- assert.deepStrictEqual(output[1], { text: "processing b.txt" });
533
-
534
- fs.rmSync(tmpDir, { recursive: true });
535
- });
536
-
537
- it("forEach costs nothing for transform body", async () => {
538
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
539
- const spec: WorkflowSpec = {
540
- name: "test-foreach-cost",
541
- description: "test forEach zero cost",
542
- input: {
543
- type: "object",
544
- properties: {
545
- items: { type: "array" },
546
- },
547
- },
548
- steps: {
549
- process: {
550
- forEach: "${{ input.items }}",
551
- transform: {
552
- mapping: { val: "${{ item }}" },
553
- },
554
- },
555
- },
556
- source: "project",
557
- filePath: path.join(tmpDir, "test.workflow.yaml"),
558
- };
559
-
560
- const result = await executeWorkflow(spec, { items: ["a", "b", "c"] }, {
561
- ctx: mockCtx(tmpDir),
562
- pi: mockPi(),
563
- loadAgent: () => ({ name: "default" }),
564
- });
565
-
566
- assert.strictEqual(result.status, "completed");
567
- assert.strictEqual(result.steps.process.usage.cost, 0);
568
- assert.strictEqual(result.steps.process.usage.turns, 0);
569
- assert.strictEqual(result.totalUsage.cost, 0);
570
-
571
- fs.rmSync(tmpDir, { recursive: true });
572
- });
573
-
574
- it("as binding is visible in agent step input expressions", async () => {
575
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-foreach-"));
576
- const capture = { calls: [] as any[] };
577
-
578
- const mockDispatchFn = async (step: StepSpec, agentSpec: AgentSpec, prompt: string, opts: any): Promise<StepResult> => {
579
- capture.calls.push({ step, agentSpec, prompt, opts });
580
- return {
581
- step: opts.stepName,
582
- agent: step.agent ?? "mock",
583
- status: "completed" as const,
584
- usage: zeroUsage(),
585
- durationMs: 100,
586
- textOutput: JSON.stringify({ result: "done" }),
587
- output: { result: "done" },
588
- };
589
- };
590
-
591
- const spec: WorkflowSpec = {
592
- name: "test-foreach-agent-as",
593
- description: "test forEach as binding in agent step input",
594
- input: {
595
- type: "object",
596
- properties: {
597
- tasks: { type: "array" },
598
- },
599
- },
600
- steps: {
601
- process: {
602
- forEach: "${{ input.tasks }}",
603
- as: "task",
604
- agent: "test-agent",
605
- input: {
606
- task: "${{ task }}",
607
- description: "${{ task.description }}",
608
- extra: "${{ input.extra }}",
609
- },
610
- },
611
- },
612
- source: "project",
613
- filePath: path.join(tmpDir, "test.workflow.yaml"),
614
- };
615
-
616
- const inputData = {
617
- tasks: [
618
- { description: "Fix bug A", file: "a.ts" },
619
- { description: "Fix bug B", file: "b.ts" },
620
- ],
621
- extra: "shared-context",
622
- };
623
-
624
- const result = await executeWorkflow(spec, inputData, {
625
- ctx: mockCtx(tmpDir),
626
- pi: mockPi(),
627
- loadAgent: () => ({ name: "test-agent" }),
628
- dispatchFn: mockDispatchFn,
629
- });
630
-
631
- assert.strictEqual(result.status, "completed", `Workflow failed: ${JSON.stringify(result.steps)}`);
632
- assert.strictEqual(result.steps.process.status, "completed");
633
-
634
- // Verify dispatch was called twice (once per task)
635
- assert.strictEqual(capture.calls.length, 2);
636
-
637
- // Verify the prompt contains the resolved task data (not "${{ task }}")
638
- const prompt0 = capture.calls[0].prompt;
639
- assert.ok(prompt0.includes("Fix bug A"), `Expected prompt to contain task description, got: ${prompt0}`);
640
- assert.ok(prompt0.includes("shared-context"), `Expected prompt to contain extra, got: ${prompt0}`);
641
-
642
- const prompt1 = capture.calls[1].prompt;
643
- assert.ok(prompt1.includes("Fix bug B"), `Expected prompt to contain task description, got: ${prompt1}`);
644
-
645
- fs.rmSync(tmpDir, { recursive: true });
646
- });
647
- });