@davidorex/pi-workflows 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) 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 +172 -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 +675 -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 +156 -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 +57 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +125 -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 +164 -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 +726 -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 +438 -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 +464 -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 +84 -0
  140. package/skills/pi-workflows/SKILL.md +247 -0
  141. package/templates/architecture-designer/task.md +117 -0
  142. package/templates/architecture-inferrer/task.md +61 -0
  143. package/templates/gap-identifier/task.md +103 -0
  144. package/templates/handoff-writer/task.md +91 -0
  145. package/templates/plan-creator/task.md +143 -0
  146. package/templates/project-definer/task.md +67 -0
  147. package/templates/project-inferrer/task.md +56 -0
  148. package/templates/requirements-gatherer/task.md +90 -0
  149. package/workflows/analyze-existing-project.workflow.yaml +74 -0
  150. package/workflows/create-handoff.workflow.yaml +58 -0
  151. package/workflows/create-phase.workflow.yaml +1 -1
  152. package/workflows/do-gap.workflow.yaml +4 -4
  153. package/workflows/fix-audit.workflow.yaml +1 -1
  154. package/workflows/gap-to-phase.workflow.yaml +1 -1
  155. package/workflows/init-new-project.workflow.yaml +55 -0
  156. package/workflows/plan-from-requirements.workflow.yaml +51 -0
  157. package/workflows/self-implement.workflow.yaml +3 -3
  158. package/workflows/typed-analysis.workflow.yaml +4 -4
  159. package/src/agent-spec.test.ts +0 -289
  160. package/src/agent-spec.ts +0 -122
  161. package/src/block-validation.test.ts +0 -350
  162. package/src/checkpoint.test.ts +0 -237
  163. package/src/checkpoint.ts +0 -140
  164. package/src/completion.test.ts +0 -143
  165. package/src/completion.ts +0 -68
  166. package/src/dag.test.ts +0 -350
  167. package/src/dag.ts +0 -219
  168. package/src/dispatch.test.ts +0 -475
  169. package/src/dispatch.ts +0 -353
  170. package/src/expression.test.ts +0 -353
  171. package/src/expression.ts +0 -332
  172. package/src/format.test.ts +0 -80
  173. package/src/format.ts +0 -41
  174. package/src/graduated-failure.test.ts +0 -556
  175. package/src/index.test.ts +0 -114
  176. package/src/index.ts +0 -516
  177. package/src/loop.test.ts +0 -551
  178. package/src/output.test.ts +0 -213
  179. package/src/output.ts +0 -70
  180. package/src/parallel-integration.test.ts +0 -175
  181. package/src/resume.test.ts +0 -192
  182. package/src/state.test.ts +0 -192
  183. package/src/state.ts +0 -254
  184. package/src/step-agent.test.ts +0 -327
  185. package/src/step-agent.ts +0 -178
  186. package/src/step-command.test.ts +0 -330
  187. package/src/step-command.ts +0 -132
  188. package/src/step-foreach.test.ts +0 -647
  189. package/src/step-foreach.ts +0 -148
  190. package/src/step-gate.test.ts +0 -185
  191. package/src/step-gate.ts +0 -116
  192. package/src/step-loop.test.ts +0 -626
  193. package/src/step-loop.ts +0 -323
  194. package/src/step-parallel.test.ts +0 -475
  195. package/src/step-parallel.ts +0 -168
  196. package/src/step-pause.test.ts +0 -30
  197. package/src/step-shared.test.ts +0 -355
  198. package/src/step-shared.ts +0 -157
  199. package/src/step-transform.test.ts +0 -155
  200. package/src/step-transform.ts +0 -47
  201. package/src/template-integration.test.ts +0 -72
  202. package/src/template.test.ts +0 -162
  203. package/src/test-helpers.ts +0 -51
  204. package/src/tui.test.ts +0 -355
  205. package/src/tui.ts +0 -182
  206. package/src/types.ts +0 -195
  207. package/src/verifier-schema.test.ts +0 -226
  208. package/src/workflow-discovery.test.ts +0 -105
  209. package/src/workflow-discovery.ts +0 -114
  210. package/src/workflow-executor.test.ts +0 -1532
  211. package/src/workflow-executor.ts +0 -810
  212. package/src/workflow-sdk.test.ts +0 -238
  213. package/src/workflow-sdk.ts +0 -262
  214. package/src/workflow-spec.test.ts +0 -576
  215. package/src/workflow-spec.ts +0 -471
@@ -1,350 +0,0 @@
1
- /**
2
- * Tests for post-step block validation with filesystem diff.
3
- */
4
- import { describe, it } from "node:test";
5
- import assert from "node:assert";
6
- import fs from "node:fs";
7
- import path from "node:path";
8
- import os from "node:os";
9
- import { snapshotBlockFiles, validateChangedBlocks } from "@davidorex/pi-project/src/block-validation.ts";
10
- import { executeWorkflow } from "./workflow-executor.ts";
11
- import type { WorkflowSpec } from "./types.ts";
12
- import { mockCtx, mockPi } from "./test-helpers.ts";
13
-
14
- // ── Unit tests for snapshotBlockFiles / validateChangedBlocks ──
15
-
16
- describe("snapshotBlockFiles", () => {
17
- it("returns empty map when .project/ does not exist", () => {
18
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-snap-"));
19
- const result = snapshotBlockFiles(tmpDir);
20
- assert.strictEqual(result.size, 0);
21
- fs.rmSync(tmpDir, { recursive: true });
22
- });
23
-
24
- it("snapshots .json files in .project/", () => {
25
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-snap-"));
26
- const wfDir = path.join(tmpDir, ".project");
27
- fs.mkdirSync(wfDir, { recursive: true });
28
- fs.writeFileSync(path.join(wfDir, "gaps.json"), "{}");
29
- fs.writeFileSync(path.join(wfDir, "decisions.json"), "[]");
30
- fs.writeFileSync(path.join(wfDir, "readme.txt"), "not json"); // should be ignored
31
-
32
- const result = snapshotBlockFiles(tmpDir);
33
- assert.strictEqual(result.size, 2);
34
- assert.ok(result.has(path.join(wfDir, "gaps.json")));
35
- assert.ok(result.has(path.join(wfDir, "decisions.json")));
36
- assert.ok(!result.has(path.join(wfDir, "readme.txt")));
37
-
38
- fs.rmSync(tmpDir, { recursive: true });
39
- });
40
- });
41
-
42
- describe("validateChangedBlocks", () => {
43
- it("does nothing when no files changed", () => {
44
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
45
- const wfDir = path.join(tmpDir, ".project");
46
- fs.mkdirSync(wfDir, { recursive: true });
47
- fs.writeFileSync(path.join(wfDir, "gaps.json"), "{}");
48
-
49
- const before = snapshotBlockFiles(tmpDir);
50
- // No changes — should not throw
51
- assert.doesNotThrow(() => validateChangedBlocks(tmpDir, before));
52
-
53
- fs.rmSync(tmpDir, { recursive: true });
54
- });
55
-
56
- it("skips changed files with no matching schema", () => {
57
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
58
- const wfDir = path.join(tmpDir, ".project");
59
- fs.mkdirSync(wfDir, { recursive: true });
60
-
61
- const before = snapshotBlockFiles(tmpDir);
62
-
63
- // Create a new file with no schema
64
- fs.writeFileSync(path.join(wfDir, "no-schema.json"), '{"data": true}');
65
-
66
- // Should not throw — no schema to validate against
67
- assert.doesNotThrow(() => validateChangedBlocks(tmpDir, before));
68
-
69
- fs.rmSync(tmpDir, { recursive: true });
70
- });
71
-
72
- it("validates changed file against its schema — passes", () => {
73
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
74
- const wfDir = path.join(tmpDir, ".project");
75
- const schemasDir = path.join(wfDir, "schemas");
76
- fs.mkdirSync(schemasDir, { recursive: true });
77
-
78
- // Create schema
79
- fs.writeFileSync(path.join(schemasDir, "test-block.schema.json"), JSON.stringify({
80
- type: "object",
81
- required: ["name"],
82
- properties: { name: { type: "string" } },
83
- }));
84
-
85
- const before = snapshotBlockFiles(tmpDir);
86
-
87
- // Create a valid block file
88
- fs.writeFileSync(path.join(wfDir, "test-block.json"), JSON.stringify({ name: "valid" }));
89
-
90
- assert.doesNotThrow(() => validateChangedBlocks(tmpDir, before));
91
-
92
- fs.rmSync(tmpDir, { recursive: true });
93
- });
94
-
95
- it("throws on validation failure", () => {
96
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
97
- const wfDir = path.join(tmpDir, ".project");
98
- const schemasDir = path.join(wfDir, "schemas");
99
- fs.mkdirSync(schemasDir, { recursive: true });
100
-
101
- // Create schema requiring name as string
102
- fs.writeFileSync(path.join(schemasDir, "test-block.schema.json"), JSON.stringify({
103
- type: "object",
104
- required: ["name"],
105
- properties: { name: { type: "string" } },
106
- }));
107
-
108
- const before = snapshotBlockFiles(tmpDir);
109
-
110
- // Create an invalid block file (name should be string, not number)
111
- fs.writeFileSync(path.join(wfDir, "test-block.json"), JSON.stringify({ name: 123 }));
112
-
113
- assert.throws(
114
- () => validateChangedBlocks(tmpDir, before),
115
- (err: unknown) => err instanceof Error && err.message.includes("Block validation failed"),
116
- );
117
-
118
- fs.rmSync(tmpDir, { recursive: true });
119
- });
120
-
121
- it("detects modified files (not just new files)", () => {
122
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
123
- const wfDir = path.join(tmpDir, ".project");
124
- const schemasDir = path.join(wfDir, "schemas");
125
- fs.mkdirSync(schemasDir, { recursive: true });
126
-
127
- // Create schema
128
- fs.writeFileSync(path.join(schemasDir, "data.schema.json"), JSON.stringify({
129
- type: "object",
130
- required: ["value"],
131
- properties: { value: { type: "number" } },
132
- }));
133
-
134
- // Create valid initial file
135
- const blockPath = path.join(wfDir, "data.json");
136
- fs.writeFileSync(blockPath, JSON.stringify({ value: 42 }));
137
-
138
- const before = snapshotBlockFiles(tmpDir);
139
-
140
- // Modify the file to be invalid (need to ensure mtime changes)
141
- // Use a tiny delay or touch the file
142
- const newContent = JSON.stringify({ value: "not a number" });
143
- // Force mtime change by writing with a slight delay
144
- const origMtime = fs.statSync(blockPath).mtimeMs;
145
- fs.writeFileSync(blockPath, newContent);
146
- // If mtime didn't change (same millisecond), force it
147
- const newMtime = fs.statSync(blockPath).mtimeMs;
148
- if (newMtime === origMtime) {
149
- fs.utimesSync(blockPath, new Date(), new Date(Date.now() + 1000));
150
- }
151
-
152
- assert.throws(
153
- () => validateChangedBlocks(tmpDir, before),
154
- (err: unknown) => err instanceof Error && err.message.includes("Block validation failed"),
155
- );
156
-
157
- fs.rmSync(tmpDir, { recursive: true });
158
- });
159
- });
160
-
161
- // ── Integration tests: block validation in workflow executor ──
162
-
163
- describe("post-step block validation in executor", () => {
164
- it("step that does not modify .project/ passes normally", async () => {
165
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
166
- const spec: WorkflowSpec = {
167
- name: "bv-no-change",
168
- description: "step without block changes",
169
- steps: {
170
- compute: {
171
- transform: { mapping: { result: 42 } },
172
- },
173
- },
174
- source: "project",
175
- filePath: path.join(tmpDir, "test.project.yaml"),
176
- };
177
-
178
- const result = await executeWorkflow(spec, {}, {
179
- ctx: mockCtx(tmpDir),
180
- pi: mockPi(),
181
- loadAgent: () => ({ name: "default" }),
182
- });
183
-
184
- assert.strictEqual(result.status, "completed");
185
- assert.strictEqual(result.steps.compute.status, "completed");
186
-
187
- fs.rmSync(tmpDir, { recursive: true });
188
- });
189
-
190
- it("step that writes valid data to a .project/ block passes", async () => {
191
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
192
- const wfDir = path.join(tmpDir, ".project");
193
- const schemasDir = path.join(wfDir, "schemas");
194
- fs.mkdirSync(schemasDir, { recursive: true });
195
-
196
- // Create schema
197
- fs.writeFileSync(path.join(schemasDir, "result.schema.json"), JSON.stringify({
198
- type: "object",
199
- required: ["status"],
200
- properties: { status: { type: "string" } },
201
- }));
202
-
203
- // Use a command step that writes a valid block file
204
- const spec: WorkflowSpec = {
205
- name: "bv-valid-write",
206
- description: "step writes valid block",
207
- steps: {
208
- writeBlock: {
209
- command: `echo '{"status": "ok"}' > ${path.join(wfDir, "result.json")}`,
210
- },
211
- },
212
- source: "project",
213
- filePath: path.join(tmpDir, "test.project.yaml"),
214
- };
215
-
216
- const result = await executeWorkflow(spec, {}, {
217
- ctx: mockCtx(tmpDir),
218
- pi: mockPi(),
219
- loadAgent: () => ({ name: "default" }),
220
- });
221
-
222
- assert.strictEqual(result.status, "completed");
223
- assert.strictEqual(result.steps.writeBlock.status, "completed");
224
-
225
- fs.rmSync(tmpDir, { recursive: true });
226
- });
227
-
228
- it("step that writes invalid data to a .project/ block fails", async () => {
229
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
230
- const wfDir = path.join(tmpDir, ".project");
231
- const schemasDir = path.join(wfDir, "schemas");
232
- fs.mkdirSync(schemasDir, { recursive: true });
233
-
234
- // Create schema requiring status as string
235
- fs.writeFileSync(path.join(schemasDir, "result.schema.json"), JSON.stringify({
236
- type: "object",
237
- required: ["status"],
238
- properties: { status: { type: "string" } },
239
- }));
240
-
241
- // Command step writes invalid data (status is number, not string)
242
- const spec: WorkflowSpec = {
243
- name: "bv-invalid-write",
244
- description: "step writes invalid block",
245
- steps: {
246
- writeBlock: {
247
- command: `echo '{"status": 123}' > ${path.join(wfDir, "result.json")}`,
248
- },
249
- shouldNotRun: {
250
- transform: { mapping: { ran: true } },
251
- },
252
- },
253
- source: "project",
254
- filePath: path.join(tmpDir, "test.project.yaml"),
255
- };
256
-
257
- const result = await executeWorkflow(spec, {}, {
258
- ctx: mockCtx(tmpDir),
259
- pi: mockPi(),
260
- loadAgent: () => ({ name: "default" }),
261
- });
262
-
263
- assert.strictEqual(result.status, "failed");
264
- assert.strictEqual(result.steps.writeBlock.status, "failed");
265
- assert.ok(result.steps.writeBlock.error?.includes("Block validation failed"));
266
- // Fail-fast: next step should not run
267
- assert.ok(!result.steps.shouldNotRun);
268
-
269
- fs.rmSync(tmpDir, { recursive: true });
270
- });
271
-
272
- it("changed .project/ file with no matching schema is skipped", async () => {
273
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
274
- const wfDir = path.join(tmpDir, ".project");
275
- fs.mkdirSync(wfDir, { recursive: true });
276
- // No schemas directory at all
277
-
278
- // Command writes a JSON file with no schema
279
- const spec: WorkflowSpec = {
280
- name: "bv-no-schema",
281
- description: "step writes block with no schema",
282
- steps: {
283
- writeBlock: {
284
- command: `echo '{"anything": "goes"}' > ${path.join(wfDir, "custom.json")}`,
285
- },
286
- },
287
- source: "project",
288
- filePath: path.join(tmpDir, "test.project.yaml"),
289
- };
290
-
291
- const result = await executeWorkflow(spec, {}, {
292
- ctx: mockCtx(tmpDir),
293
- pi: mockPi(),
294
- loadAgent: () => ({ name: "default" }),
295
- });
296
-
297
- assert.strictEqual(result.status, "completed");
298
- assert.strictEqual(result.steps.writeBlock.status, "completed");
299
-
300
- fs.rmSync(tmpDir, { recursive: true });
301
- });
302
-
303
- it("validates multiple changed block files in one step", async () => {
304
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
305
- const wfDir = path.join(tmpDir, ".project");
306
- const schemasDir = path.join(wfDir, "schemas");
307
- fs.mkdirSync(schemasDir, { recursive: true });
308
-
309
- // Schema for file-a: requires name as string
310
- fs.writeFileSync(path.join(schemasDir, "file-a.schema.json"), JSON.stringify({
311
- type: "object",
312
- required: ["name"],
313
- properties: { name: { type: "string" } },
314
- }));
315
- // Schema for file-b: requires count as number
316
- fs.writeFileSync(path.join(schemasDir, "file-b.schema.json"), JSON.stringify({
317
- type: "object",
318
- required: ["count"],
319
- properties: { count: { type: "number" } },
320
- }));
321
-
322
- // Write valid file-a, invalid file-b
323
- const cmd = [
324
- `echo '{"name": "ok"}' > ${path.join(wfDir, "file-a.json")}`,
325
- `echo '{"count": "not-a-number"}' > ${path.join(wfDir, "file-b.json")}`,
326
- ].join(" && ");
327
-
328
- const spec: WorkflowSpec = {
329
- name: "bv-multi",
330
- description: "step writes multiple blocks",
331
- steps: {
332
- writeBlocks: { command: cmd },
333
- },
334
- source: "project",
335
- filePath: path.join(tmpDir, "test.project.yaml"),
336
- };
337
-
338
- const result = await executeWorkflow(spec, {}, {
339
- ctx: mockCtx(tmpDir),
340
- pi: mockPi(),
341
- loadAgent: () => ({ name: "default" }),
342
- });
343
-
344
- assert.strictEqual(result.status, "failed");
345
- assert.strictEqual(result.steps.writeBlocks.status, "failed");
346
- assert.ok(result.steps.writeBlocks.error?.includes("file-b.json"));
347
-
348
- fs.rmSync(tmpDir, { recursive: true });
349
- });
350
- });
@@ -1,237 +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 { findIncompleteRun, validateResumeCompatibility, computeResumePoint, formatIncompleteRun } from "./checkpoint.ts";
7
- import { writeState } from "./state.ts";
8
- import type { ExecutionState } from "./types.ts";
9
- import { makeSpec } from "./test-helpers.ts";
10
-
11
- describe("findIncompleteRun", () => {
12
- it("returns null when no runs directory exists", () => {
13
- const result = findIncompleteRun("/nonexistent", "test-workflow");
14
- assert.strictEqual(result, null);
15
- });
16
-
17
- it("returns null when all runs are completed", (t) => {
18
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
19
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
20
-
21
- const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
22
- fs.mkdirSync(runDir, { recursive: true });
23
- writeState(runDir, { input: {}, steps: {}, status: "completed" });
24
-
25
- const result = findIncompleteRun(tmpDir, "test");
26
- assert.strictEqual(result, null);
27
- });
28
-
29
- it("finds a run with status 'running' (interrupted)", (t) => {
30
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
31
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
32
-
33
- const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
34
- fs.mkdirSync(runDir, { recursive: true });
35
-
36
- const state: ExecutionState = {
37
- input: { path: "/src" },
38
- steps: {
39
- explore: {
40
- step: "explore", agent: "explorer", status: "completed",
41
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 1 },
42
- durationMs: 1000, textOutput: "found stuff",
43
- },
44
- },
45
- status: "running",
46
- };
47
- writeState(runDir, state);
48
-
49
- const result = findIncompleteRun(tmpDir, "test");
50
- assert.ok(result);
51
- assert.strictEqual(result.runId, "test-20260314-120000-abcd");
52
- assert.deepStrictEqual(result.completedSteps, ["explore"]);
53
- assert.strictEqual(result.failedStep, undefined);
54
- });
55
-
56
- it("finds a run with status 'failed'", (t) => {
57
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
58
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
59
-
60
- const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
61
- fs.mkdirSync(runDir, { recursive: true });
62
-
63
- const state: ExecutionState = {
64
- input: {},
65
- steps: {
66
- step1: {
67
- step: "step1", agent: "a", status: "completed",
68
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
69
- durationMs: 0,
70
- },
71
- step2: {
72
- step: "step2", agent: "b", status: "failed",
73
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
74
- durationMs: 0, error: "timeout",
75
- },
76
- },
77
- status: "failed",
78
- };
79
- writeState(runDir, state);
80
-
81
- const result = findIncompleteRun(tmpDir, "test");
82
- assert.ok(result);
83
- assert.deepStrictEqual(result.completedSteps, ["step1"]);
84
- assert.strictEqual(result.failedStep, "step2");
85
- });
86
-
87
- it("returns most recent incomplete run", (t) => {
88
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
89
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
90
-
91
- const runsBase = path.join(tmpDir, ".workflows", "runs", "test", "runs");
92
-
93
- // Older run (completed)
94
- const oldDir = path.join(runsBase, "test-20260314-100000-aaaa");
95
- fs.mkdirSync(oldDir, { recursive: true });
96
- writeState(oldDir, { input: {}, steps: {}, status: "completed" });
97
-
98
- // Newer run (failed)
99
- const newDir = path.join(runsBase, "test-20260314-120000-bbbb");
100
- fs.mkdirSync(newDir, { recursive: true });
101
- writeState(newDir, { input: {}, steps: {}, status: "failed" });
102
-
103
- const result = findIncompleteRun(tmpDir, "test");
104
- assert.ok(result);
105
- assert.strictEqual(result.runId, "test-20260314-120000-bbbb");
106
- });
107
- });
108
-
109
- describe("validateResumeCompatibility", () => {
110
- it("returns null when compatible", () => {
111
- const state: ExecutionState = {
112
- input: {},
113
- steps: {
114
- step1: {
115
- step: "step1", agent: "a", status: "completed",
116
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
117
- durationMs: 0,
118
- },
119
- },
120
- status: "failed",
121
- specVersion: "1",
122
- };
123
- const spec = makeSpec({ version: "1", steps: { step1: { agent: "a" }, step2: { agent: "b" } } });
124
- assert.strictEqual(validateResumeCompatibility(state, spec), null);
125
- });
126
-
127
- it("rejects version mismatch", () => {
128
- const state: ExecutionState = {
129
- input: {},
130
- steps: {},
131
- status: "failed",
132
- specVersion: "1",
133
- };
134
- const spec = makeSpec({ version: "2", steps: { step1: { agent: "a" } } });
135
- const msg = validateResumeCompatibility(state, spec);
136
- assert.ok(msg?.includes("version"));
137
- });
138
-
139
- it("rejects when completed step no longer exists in spec", () => {
140
- const state: ExecutionState = {
141
- input: {},
142
- steps: {
143
- removed_step: {
144
- step: "removed_step", agent: "a", status: "completed",
145
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
146
- durationMs: 0,
147
- },
148
- },
149
- status: "failed",
150
- };
151
- const spec = makeSpec({ steps: { different_step: { agent: "a" } } });
152
- const msg = validateResumeCompatibility(state, spec);
153
- assert.ok(msg?.includes("removed_step"));
154
- });
155
- });
156
-
157
- describe("computeResumePoint", () => {
158
- it("returns first layer with pending steps", () => {
159
- const plan = [
160
- { steps: ["a"] },
161
- { steps: ["b", "c"] },
162
- { steps: ["d"] },
163
- ];
164
- const completed = new Set(["a"]);
165
- const result = computeResumePoint(plan, completed);
166
- assert.ok(result);
167
- assert.strictEqual(result.resumeLayerIndex, 1);
168
- assert.deepStrictEqual(result.pendingStepsInLayer, ["b", "c"]);
169
- });
170
-
171
- it("returns only pending steps in a partially completed layer", () => {
172
- const plan = [
173
- { steps: ["a"] },
174
- { steps: ["b", "c", "d"] },
175
- { steps: ["e"] },
176
- ];
177
- const completed = new Set(["a", "b", "d"]);
178
- const result = computeResumePoint(plan, completed);
179
- assert.ok(result);
180
- assert.strictEqual(result.resumeLayerIndex, 1);
181
- assert.deepStrictEqual(result.pendingStepsInLayer, ["c"]);
182
- });
183
-
184
- it("returns null when all steps are completed", () => {
185
- const plan = [
186
- { steps: ["a"] },
187
- { steps: ["b"] },
188
- ];
189
- const completed = new Set(["a", "b"]);
190
- const result = computeResumePoint(plan, completed);
191
- assert.strictEqual(result, null);
192
- });
193
-
194
- it("handles first layer being the resume point", () => {
195
- const plan = [
196
- { steps: ["a", "b"] },
197
- { steps: ["c"] },
198
- ];
199
- const completed = new Set<string>();
200
- const result = computeResumePoint(plan, completed);
201
- assert.ok(result);
202
- assert.strictEqual(result.resumeLayerIndex, 0);
203
- assert.deepStrictEqual(result.pendingStepsInLayer, ["a", "b"]);
204
- });
205
- });
206
-
207
- describe("formatIncompleteRun", () => {
208
- it("formats an interrupted run", () => {
209
- const run = {
210
- runId: "test-20260314-120000-abcd",
211
- runDir: "/tmp/runs/test-20260314-120000-abcd",
212
- state: { input: {}, steps: {}, status: "running" as const },
213
- completedSteps: ["explore", "analyze"],
214
- updatedAt: "2026-03-14T12:00:00.000Z",
215
- };
216
- const spec = makeSpec({ steps: { explore: { agent: "a" }, analyze: { agent: "b" }, synthesize: { agent: "c" } } });
217
- const msg = formatIncompleteRun(run, spec);
218
- assert.ok(msg.includes("interrupted"));
219
- assert.ok(msg.includes("2/3"));
220
- });
221
-
222
- it("formats a failed run with step name", () => {
223
- const run = {
224
- runId: "test-20260314-120000-abcd",
225
- runDir: "/tmp/runs/test-20260314-120000-abcd",
226
- state: { input: {}, steps: {}, status: "failed" as const },
227
- completedSteps: ["explore"],
228
- failedStep: "analyze",
229
- updatedAt: "2026-03-14T12:00:00.000Z",
230
- };
231
- const spec = makeSpec({ steps: { explore: { agent: "a" }, analyze: { agent: "b" }, synthesize: { agent: "c" } } });
232
- const msg = formatIncompleteRun(run, spec);
233
- assert.ok(msg.includes("failed"));
234
- assert.ok(msg.includes("analyze"));
235
- assert.ok(msg.includes("1/3"));
236
- });
237
- });