@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,355 +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 nunjucks from "nunjucks";
7
- import {
8
- SIGKILL_GRACE_MS,
9
- WIDGET_ID,
10
- DEFAULT_MAX_ATTEMPTS,
11
- zeroUsage,
12
- addUsage,
13
- resolveSchemaPath,
14
- buildPrompt,
15
- persistStep,
16
- compileAgentSpec,
17
- } from "./step-shared.ts";
18
- import type { StepUsage, StepResult, ExecutionState, AgentSpec } from "./types.ts";
19
-
20
- describe("constants", () => {
21
- it("SIGKILL_GRACE_MS is 3000", () => {
22
- assert.strictEqual(SIGKILL_GRACE_MS, 3000);
23
- });
24
-
25
- it("WIDGET_ID is 'workflow-progress'", () => {
26
- assert.strictEqual(WIDGET_ID, "workflow-progress");
27
- });
28
-
29
- it("DEFAULT_MAX_ATTEMPTS is 3", () => {
30
- assert.strictEqual(DEFAULT_MAX_ATTEMPTS, 3);
31
- });
32
- });
33
-
34
- describe("zeroUsage", () => {
35
- it("returns all-zero StepUsage with correct keys", () => {
36
- const u = zeroUsage();
37
- assert.strictEqual(u.input, 0);
38
- assert.strictEqual(u.output, 0);
39
- assert.strictEqual(u.cacheRead, 0);
40
- assert.strictEqual(u.cacheWrite, 0);
41
- assert.strictEqual(u.cost, 0);
42
- assert.strictEqual(u.turns, 0);
43
- assert.deepStrictEqual(Object.keys(u).sort(), ["cacheRead", "cacheWrite", "cost", "input", "output", "turns"]);
44
- });
45
-
46
- it("returns a fresh object each call (no shared reference)", () => {
47
- const a = zeroUsage();
48
- const b = zeroUsage();
49
- assert.notStrictEqual(a, b);
50
- a.input = 999;
51
- assert.strictEqual(b.input, 0);
52
- });
53
- });
54
-
55
- describe("addUsage", () => {
56
- it("accumulates all six fields into total", () => {
57
- const total = zeroUsage();
58
- const step: StepUsage = { input: 10, output: 20, cacheRead: 30, cacheWrite: 40, cost: 0.5, turns: 2 };
59
- addUsage(total, step);
60
- assert.strictEqual(total.input, 10);
61
- assert.strictEqual(total.output, 20);
62
- assert.strictEqual(total.cacheRead, 30);
63
- assert.strictEqual(total.cacheWrite, 40);
64
- assert.strictEqual(total.cost, 0.5);
65
- assert.strictEqual(total.turns, 2);
66
- });
67
-
68
- it("handles adding zero usage (no change)", () => {
69
- const total: StepUsage = { input: 5, output: 10, cacheRead: 15, cacheWrite: 20, cost: 0.1, turns: 1 };
70
- addUsage(total, zeroUsage());
71
- assert.strictEqual(total.input, 5);
72
- assert.strictEqual(total.output, 10);
73
- assert.strictEqual(total.cacheRead, 15);
74
- assert.strictEqual(total.cacheWrite, 20);
75
- assert.strictEqual(total.cost, 0.1);
76
- assert.strictEqual(total.turns, 1);
77
- });
78
-
79
- it("accumulates across multiple additions", () => {
80
- const total = zeroUsage();
81
- addUsage(total, { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0.1, turns: 1 });
82
- addUsage(total, { input: 20, output: 15, cacheRead: 5, cacheWrite: 3, cost: 0.2, turns: 2 });
83
- addUsage(total, { input: 30, output: 25, cacheRead: 10, cacheWrite: 7, cost: 0.3, turns: 3 });
84
- assert.strictEqual(total.input, 60);
85
- assert.strictEqual(total.output, 45);
86
- assert.strictEqual(total.cacheRead, 15);
87
- assert.strictEqual(total.cacheWrite, 10);
88
- assert.strictEqual(total.cost, 0.1 + 0.2 + 0.3);
89
- assert.strictEqual(total.turns, 6);
90
- });
91
- });
92
-
93
- describe("resolveSchemaPath", () => {
94
- it("returns absolute path unchanged", () => {
95
- assert.strictEqual(resolveSchemaPath("/abs/path/schema.json", "/some/spec.yaml"), "/abs/path/schema.json");
96
- });
97
-
98
- it("resolves relative path against spec file directory", () => {
99
- const result = resolveSchemaPath("schema.json", "/project/specs/workflow.yaml");
100
- assert.strictEqual(result, path.resolve("/project/specs", "schema.json"));
101
- });
102
-
103
- it("handles spec file in nested directory", () => {
104
- const result = resolveSchemaPath("schemas/output.json", "/a/b/c/spec.yaml");
105
- assert.strictEqual(result, path.resolve("/a/b/c", "schemas/output.json"));
106
- });
107
-
108
- it("handles schema path with parent traversal (../schemas/out.json)", () => {
109
- const result = resolveSchemaPath("../schemas/out.json", "/project/specs/workflow.yaml");
110
- assert.strictEqual(result, path.resolve("/project/specs", "../schemas/out.json"));
111
- });
112
- });
113
-
114
- describe("buildPrompt", () => {
115
- it("includes task template when agentSpec.taskTemplate is set", () => {
116
- const step = { agent: "test" };
117
- const agentSpec: AgentSpec = { name: "test", taskTemplate: "Do this task with {{ data }}" };
118
- const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
119
- assert.ok(prompt.includes("Do this task with {{ data }}"));
120
- });
121
-
122
- it("serializes non-empty object input as JSON code block when no task template", () => {
123
- const step = { agent: "test" };
124
- const agentSpec: AgentSpec = { name: "test" };
125
- const prompt = buildPrompt(step, agentSpec, { key: "value" }, "/tmp/run", "step1");
126
- assert.ok(prompt.includes("## Input"));
127
- assert.ok(prompt.includes("```json"));
128
- assert.ok(prompt.includes('"key": "value"'));
129
- assert.ok(prompt.includes("```"));
130
- });
131
-
132
- it("passes through string input directly when no task template", () => {
133
- const step = { agent: "test" };
134
- const agentSpec: AgentSpec = { name: "test" };
135
- const prompt = buildPrompt(step, agentSpec, "raw string input", "/tmp/run", "step1");
136
- assert.ok(prompt.includes("raw string input"));
137
- assert.ok(!prompt.includes("## Input"));
138
- });
139
-
140
- it("produces empty-ish prompt for empty object input with no task template", () => {
141
- const step = { agent: "test" };
142
- const agentSpec: AgentSpec = { name: "test" };
143
- const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
144
- assert.ok(!prompt.includes("## Input"));
145
- assert.ok(!prompt.includes("```json"));
146
- });
147
-
148
- it("appends output instructions when output.format is json", () => {
149
- const step = { agent: "test", output: { format: "json" as const } };
150
- const agentSpec: AgentSpec = { name: "test" };
151
- const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
152
- assert.ok(prompt.includes("**Output:**"));
153
- assert.ok(prompt.includes("step1.json"));
154
- });
155
-
156
- it("appends output instructions with schema path when output.schema is set", () => {
157
- const step = { agent: "test", output: { schema: "schemas/out.json" } };
158
- const agentSpec: AgentSpec = { name: "test" };
159
- const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
160
- assert.ok(prompt.includes("**Output:**"));
161
- assert.ok(prompt.includes("JSON Schema"));
162
- assert.ok(prompt.includes("schemas/out.json"));
163
- });
164
-
165
- it("omits output instructions when no output spec", () => {
166
- const step = { agent: "test" };
167
- const agentSpec: AgentSpec = { name: "test" };
168
- const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
169
- assert.ok(!prompt.includes("**Output:**"));
170
- });
171
-
172
- it("combines task template with output instructions", () => {
173
- const step = { agent: "test", output: { format: "json" as const } };
174
- const agentSpec: AgentSpec = { name: "test", taskTemplate: "Analyze this" };
175
- const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
176
- assert.ok(prompt.includes("Analyze this"));
177
- assert.ok(prompt.includes("**Output:**"));
178
- });
179
- });
180
-
181
- describe("persistStep", () => {
182
- it("writes result into state.steps[stepName]", (t) => {
183
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
184
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
185
-
186
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
187
- const result: StepResult = {
188
- step: "myStep",
189
- agent: "test-agent",
190
- status: "completed",
191
- usage: zeroUsage(),
192
- durationMs: 100,
193
- };
194
- const widgetState = {
195
- spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
196
- state,
197
- startTime: Date.now(),
198
- };
199
- const ctx = { hasUI: false, ui: { setWidget: () => {}, notify: () => {} } };
200
-
201
- persistStep(state, "myStep", result, tmpDir, widgetState, ctx);
202
- assert.strictEqual(state.steps.myStep, result);
203
- });
204
-
205
- it("calls writeState (state.json is written to disk)", (t) => {
206
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
207
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
208
-
209
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
210
- const result: StepResult = {
211
- step: "s1",
212
- agent: "agent",
213
- status: "completed",
214
- usage: zeroUsage(),
215
- durationMs: 50,
216
- };
217
- const widgetState = {
218
- spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
219
- state,
220
- startTime: Date.now(),
221
- };
222
- const ctx = { hasUI: false, ui: { setWidget: () => {}, notify: () => {} } };
223
-
224
- persistStep(state, "s1", result, tmpDir, widgetState, ctx);
225
- assert.ok(fs.existsSync(path.join(tmpDir, "state.json")));
226
- const written = JSON.parse(fs.readFileSync(path.join(tmpDir, "state.json"), "utf-8"));
227
- assert.ok(written.steps.s1);
228
- assert.strictEqual(written.steps.s1.status, "completed");
229
- });
230
-
231
- it("calls ctx.ui.setWidget when ctx.hasUI is true", (t) => {
232
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
233
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
234
-
235
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
236
- const result: StepResult = {
237
- step: "s1",
238
- agent: "agent",
239
- status: "completed",
240
- usage: zeroUsage(),
241
- durationMs: 50,
242
- };
243
- const widgetState = {
244
- spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
245
- state,
246
- startTime: Date.now(),
247
- };
248
- let widgetCalled = false;
249
- const ctx = {
250
- hasUI: true,
251
- ui: {
252
- setWidget: (id: string, _w: unknown) => {
253
- widgetCalled = true;
254
- assert.strictEqual(id, WIDGET_ID);
255
- },
256
- notify: () => {},
257
- },
258
- };
259
-
260
- persistStep(state, "s1", result, tmpDir, widgetState, ctx);
261
- assert.ok(widgetCalled, "setWidget should have been called");
262
- });
263
-
264
- it("does not call ctx.ui.setWidget when ctx.hasUI is false", (t) => {
265
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
266
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
267
-
268
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
269
- const result: StepResult = {
270
- step: "s1",
271
- agent: "agent",
272
- status: "completed",
273
- usage: zeroUsage(),
274
- durationMs: 50,
275
- };
276
- const widgetState = {
277
- spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
278
- state,
279
- startTime: Date.now(),
280
- };
281
- let widgetCalled = false;
282
- const ctx = {
283
- hasUI: false,
284
- ui: {
285
- setWidget: () => { widgetCalled = true; },
286
- notify: () => {},
287
- },
288
- };
289
-
290
- persistStep(state, "s1", result, tmpDir, widgetState, ctx);
291
- assert.ok(!widgetCalled, "setWidget should not have been called");
292
- });
293
- });
294
-
295
- describe("compileAgentSpec", () => {
296
- it("returns agentSpec unchanged when no templateEnv", () => {
297
- const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
298
- const result = compileAgentSpec(agentSpec, { name: "world" });
299
- assert.strictEqual(result, agentSpec);
300
- assert.strictEqual(result.systemPrompt, "Hello {{ name }}");
301
- });
302
-
303
- it("renders inline systemPrompt through Nunjucks with input context", () => {
304
- const env = new nunjucks.Environment(undefined, { autoescape: false, throwOnUndefined: false });
305
- const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
306
- const result = compileAgentSpec(agentSpec, { name: "world" }, env);
307
- assert.strictEqual(result.systemPrompt, "Hello world");
308
- });
309
-
310
- it("renders promptTemplate file path through Nunjucks (replaces with rendered text, clears promptTemplate)", (t) => {
311
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-compile-"));
312
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
313
-
314
- fs.writeFileSync(path.join(tmpDir, "system.md"), "System for {{ role }}");
315
- const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(tmpDir), {
316
- autoescape: false,
317
- throwOnUndefined: false,
318
- });
319
-
320
- const agentSpec: AgentSpec = { name: "test", promptTemplate: "system.md" };
321
- const result = compileAgentSpec(agentSpec, { role: "analyzer" }, env);
322
- assert.strictEqual(result.systemPrompt, "System for analyzer");
323
- assert.strictEqual(result.promptTemplate, undefined);
324
- });
325
-
326
- it("renders taskTemplate file path through Nunjucks", (t) => {
327
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-compile-"));
328
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
329
-
330
- fs.writeFileSync(path.join(tmpDir, "task.md"), "Analyze {{ target }}");
331
- const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(tmpDir), {
332
- autoescape: false,
333
- throwOnUndefined: false,
334
- });
335
-
336
- const agentSpec: AgentSpec = { name: "test", taskTemplate: "task.md" };
337
- const result = compileAgentSpec(agentSpec, { target: "codebase" }, env);
338
- assert.strictEqual(result.taskTemplate, "Analyze codebase");
339
- });
340
-
341
- it("handles non-object resolvedInput (uses empty context)", () => {
342
- const env = new nunjucks.Environment(undefined, { autoescape: false, throwOnUndefined: false });
343
- const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
344
- const result = compileAgentSpec(agentSpec, "string input", env);
345
- // {{ name }} renders to "" with throwOnUndefined: false
346
- assert.strictEqual(result.systemPrompt, "Hello ");
347
- });
348
-
349
- it("handles null resolvedInput", () => {
350
- const env = new nunjucks.Environment(undefined, { autoescape: false, throwOnUndefined: false });
351
- const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
352
- const result = compileAgentSpec(agentSpec, null, env);
353
- assert.strictEqual(result.systemPrompt, "Hello ");
354
- });
355
- });
@@ -1,157 +0,0 @@
1
- /**
2
- * Shared helpers for step executors — constants, usage aggregation,
3
- * prompt building, schema resolution, state persistence, and template resolution.
4
- */
5
- import path from "node:path";
6
- import type { StepUsage, StepResult, ExecutionState, AgentSpec } from "./types.ts";
7
- import type { ProgressWidgetState } from "./tui.ts";
8
- import { writeState } from "./state.ts";
9
- import { createProgressWidget } from "./tui.ts";
10
- import { renderTemplate, renderTemplateFile } from "./template.ts";
11
- import type nunjucks from "nunjucks";
12
-
13
- /** Grace period (ms) between SIGTERM and SIGKILL when killing subprocesses. */
14
- export const SIGKILL_GRACE_MS = 3000;
15
-
16
- /** Widget ID used for the workflow progress widget. */
17
- export const WIDGET_ID = "workflow-progress";
18
-
19
- /** Default max loop attempts when not specified. */
20
- export const DEFAULT_MAX_ATTEMPTS = 3;
21
-
22
- /**
23
- * Helper that returns a StepUsage with all zeroes.
24
- */
25
- export function zeroUsage(): StepUsage {
26
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
27
- }
28
-
29
- /**
30
- * Add step usage into a running total (mutates `total`).
31
- */
32
- export function addUsage(total: StepUsage, step: StepUsage): void {
33
- total.input += step.input;
34
- total.output += step.output;
35
- total.cacheRead += step.cacheRead;
36
- total.cacheWrite += step.cacheWrite;
37
- total.cost += step.cost;
38
- total.turns += step.turns;
39
- }
40
-
41
- /**
42
- * Resolve a schema path relative to the workflow spec file.
43
- * If the schema path is absolute, return as-is.
44
- * If relative, resolve against the directory containing the workflow spec.
45
- */
46
- export function resolveSchemaPath(schemaPath: string, specFilePath: string): string {
47
- if (path.isAbsolute(schemaPath)) return schemaPath;
48
- return path.resolve(path.dirname(specFilePath), schemaPath);
49
- }
50
-
51
- /**
52
- * Build the prompt string sent to the subprocess.
53
- *
54
- * The prompt includes:
55
- * 1. The compiled task template (if set), or the resolved input as context
56
- * 2. Output instructions (if schema-bound)
57
- */
58
- export function buildPrompt(
59
- step: { agent?: string; input?: Record<string, unknown>; output?: { format?: string; schema?: string } },
60
- agentSpec: AgentSpec,
61
- resolvedInput: unknown,
62
- runDir: string,
63
- stepName: string,
64
- ): string {
65
- const parts: string[] = [];
66
-
67
- // Task template was compiled by compileAgentSpec — use it
68
- if (agentSpec.taskTemplate) {
69
- parts.push(agentSpec.taskTemplate);
70
- } else if (resolvedInput && typeof resolvedInput === "object" && Object.keys(resolvedInput).length > 0) {
71
- // No task template — serialize input as JSON
72
- parts.push("## Input\n");
73
- parts.push("```json");
74
- parts.push(JSON.stringify(resolvedInput, null, 2));
75
- parts.push("```\n");
76
- } else if (typeof resolvedInput === "string") {
77
- parts.push(resolvedInput);
78
- }
79
-
80
- // Output instructions (if schema-bound)
81
- if (step.output?.format === "json" || step.output?.schema) {
82
- const outputPath = path.join(runDir, "outputs", `${stepName}.json`);
83
- parts.push("\n---");
84
- parts.push(`**Output:** Write your result as valid JSON to: ${outputPath}`);
85
- if (step.output.schema) {
86
- parts.push(`The output must conform to the JSON Schema at: ${resolveSchemaPath(step.output.schema, "")}`);
87
- }
88
- }
89
-
90
- return parts.join("\n");
91
- }
92
-
93
- /**
94
- * Persist step result to state and update TUI widget.
95
- * Replaces the repeated writeState + setWidget pattern.
96
- */
97
- export function persistStep(
98
- state: ExecutionState,
99
- stepName: string,
100
- result: StepResult,
101
- runDir: string,
102
- widgetState: ProgressWidgetState,
103
- ctx: { hasUI: boolean; ui: { setWidget(id: string, w: unknown): void; notify?(msg: string, level: string): void } },
104
- ): void {
105
- state.steps[stepName] = result;
106
- // Clear activity buffer for completed step
107
- widgetState.activities?.delete(stepName);
108
- try {
109
- writeState(runDir, state);
110
- } catch (err) {
111
- if (ctx.hasUI && ctx.ui.notify) {
112
- const msg = err instanceof Error ? err.message : String(err);
113
- ctx.ui.notify(`State write failed after step '${stepName}': ${msg}`, "error");
114
- }
115
- throw err; // re-throw — state write failure is fatal
116
- }
117
- if (ctx.hasUI) {
118
- ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
119
- }
120
- }
121
-
122
- /**
123
- * Compile an agent spec: render system and task templates through Nunjucks.
124
- *
125
- * Every agent's prompts go through Nunjucks. Plain text without template
126
- * tags renders to itself. The .md that pi receives is compiled output.
127
- */
128
- export function compileAgentSpec(
129
- agentSpec: AgentSpec,
130
- resolvedInput: unknown,
131
- templateEnv?: nunjucks.Environment,
132
- ): AgentSpec {
133
- if (!templateEnv) return agentSpec;
134
-
135
- const ctx = typeof resolvedInput === "object" && resolvedInput !== null
136
- ? resolvedInput as Record<string, unknown>
137
- : {};
138
-
139
- let result = agentSpec;
140
-
141
- // System prompt: file template or inline — always rendered
142
- if (agentSpec.promptTemplate) {
143
- const rendered = renderTemplateFile(templateEnv, agentSpec.promptTemplate, ctx);
144
- result = { ...result, systemPrompt: rendered, promptTemplate: undefined };
145
- } else if (agentSpec.systemPrompt) {
146
- const rendered = renderTemplate(templateEnv, agentSpec.systemPrompt, ctx);
147
- result = { ...result, systemPrompt: rendered };
148
- }
149
-
150
- // Task prompt: file template — rendered from typed input
151
- if (agentSpec.taskTemplate) {
152
- const rendered = renderTemplateFile(templateEnv, agentSpec.taskTemplate, ctx);
153
- result = { ...result, taskTemplate: rendered };
154
- }
155
-
156
- return result;
157
- }
@@ -1,155 +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 { executeTransform } from "./step-transform.ts";
7
- import { zeroUsage } from "./step-shared.ts";
8
-
9
- describe("executeTransform", () => {
10
- // Happy path
11
- it("resolves simple mapping to output object", () => {
12
- const result = executeTransform({ mapping: { greeting: "hello" } }, "step1", {});
13
- assert.strictEqual(result.status, "completed");
14
- assert.deepStrictEqual(result.output, { greeting: "hello" });
15
- });
16
-
17
- it("resolves ${{ }} expressions in mapping values", () => {
18
- const result = executeTransform(
19
- { mapping: { name: "${{ input.name }}" } },
20
- "step1",
21
- { input: { name: "Alice" } },
22
- );
23
- assert.strictEqual(result.status, "completed");
24
- assert.strictEqual((result.output as any).name, "Alice");
25
- });
26
-
27
- it("resolves nested expressions", () => {
28
- const result = executeTransform(
29
- { mapping: { val: "${{ steps.x.output.field }}" } },
30
- "step1",
31
- {
32
- input: {},
33
- steps: {
34
- x: {
35
- step: "x",
36
- agent: "test",
37
- status: "completed",
38
- output: { field: "deep-value" },
39
- usage: zeroUsage(),
40
- durationMs: 0,
41
- },
42
- },
43
- },
44
- );
45
- assert.strictEqual(result.status, "completed");
46
- assert.strictEqual((result.output as any).val, "deep-value");
47
- });
48
-
49
- it("preserves non-expression values (numbers, booleans, null)", () => {
50
- const result = executeTransform(
51
- { mapping: { count: 42, flag: true, empty: null } },
52
- "step1",
53
- {},
54
- );
55
- assert.strictEqual(result.status, "completed");
56
- const output = result.output as any;
57
- assert.strictEqual(output.count, 42);
58
- assert.strictEqual(output.flag, true);
59
- assert.strictEqual(output.empty, null);
60
- });
61
-
62
- it("sets agent to 'transform'", () => {
63
- const result = executeTransform({ mapping: {} }, "step1", {});
64
- assert.strictEqual(result.agent, "transform");
65
- });
66
-
67
- it("sets step name correctly", () => {
68
- const result = executeTransform({ mapping: {} }, "my-transform", {});
69
- assert.strictEqual(result.step, "my-transform");
70
- });
71
-
72
- it("has zero usage", () => {
73
- const result = executeTransform({ mapping: {} }, "step1", {});
74
- assert.deepStrictEqual(result.usage, zeroUsage());
75
- });
76
-
77
- it("has non-negative durationMs", () => {
78
- const result = executeTransform({ mapping: {} }, "step1", {});
79
- assert.ok(result.durationMs >= 0);
80
- });
81
-
82
- it("includes textOutput as JSON-stringified output", () => {
83
- const result = executeTransform({ mapping: { a: 1 } }, "step1", {});
84
- assert.strictEqual(result.textOutput, JSON.stringify({ a: 1 }, null, 2));
85
- });
86
-
87
- // Output persistence
88
- it("persists output to runDir when runDir is provided", (t) => {
89
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-transform-"));
90
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
91
-
92
- const result = executeTransform({ mapping: { x: 1 } }, "step1", {}, tmpDir);
93
- assert.ok(result.outputPath);
94
- assert.ok(fs.existsSync(result.outputPath!));
95
- const persisted = JSON.parse(fs.readFileSync(result.outputPath!, "utf-8"));
96
- assert.deepStrictEqual(persisted, { x: 1 });
97
- });
98
-
99
- it("persists output to outputPath when provided", (t) => {
100
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-transform-"));
101
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
102
-
103
- const customPath = path.join(tmpDir, "custom-out.json");
104
- const result = executeTransform({ mapping: { y: 2 } }, "step1", {}, tmpDir, customPath);
105
- assert.strictEqual(result.outputPath, customPath);
106
- assert.ok(fs.existsSync(customPath));
107
- });
108
-
109
- it("does not persist when runDir is omitted", () => {
110
- const result = executeTransform({ mapping: { z: 3 } }, "step1", {});
111
- assert.strictEqual(result.outputPath, undefined);
112
- });
113
-
114
- // Error paths
115
- it("returns failed status when expression resolution throws", () => {
116
- const result = executeTransform(
117
- { mapping: { bad: "${{ steps.nonexistent.output }}" } },
118
- "step1",
119
- { input: {}, steps: {} },
120
- );
121
- assert.strictEqual(result.status, "failed");
122
- assert.ok(result.error);
123
- assert.ok(result.error!.length > 0);
124
- });
125
-
126
- it("returns failed status with error message string", () => {
127
- const result = executeTransform(
128
- { mapping: { bad: "${{ steps.nonexistent.output }}" } },
129
- "step1",
130
- { input: {}, steps: {} },
131
- );
132
- assert.strictEqual(result.status, "failed");
133
- assert.strictEqual(typeof result.error, "string");
134
- assert.ok(result.error!.includes("nonexistent"));
135
- });
136
-
137
- // Edge cases
138
- it("handles empty mapping object", () => {
139
- const result = executeTransform({ mapping: {} }, "step1", {});
140
- assert.strictEqual(result.status, "completed");
141
- assert.deepStrictEqual(result.output, {});
142
- });
143
-
144
- it("handles mapping with mixed expressions and literals", () => {
145
- const result = executeTransform(
146
- { mapping: { resolved: "${{ input.x }}", literal: "plain" } },
147
- "step1",
148
- { input: { x: 42 } },
149
- );
150
- assert.strictEqual(result.status, "completed");
151
- const output = result.output as any;
152
- assert.strictEqual(output.resolved, 42);
153
- assert.strictEqual(output.literal, "plain");
154
- });
155
- });