@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,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
- });