@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,556 +0,0 @@
1
- /**
2
- * Tests for graduated failure / retry support.
3
- */
4
- import { describe, it, beforeEach, afterEach } from "node:test";
5
- import assert from "node:assert/strict";
6
- import fs from "node:fs";
7
- import path from "node:path";
8
- import os from "node:os";
9
- import { snapshotBlockFiles, validateChangedBlocks, rollbackBlockFiles } from "@davidorex/pi-project/src/block-validation.ts";
10
- import type { BlockSnapshot } from "@davidorex/pi-project/src/block-validation.ts";
11
- import { parseWorkflowSpec } from "./workflow-spec.ts";
12
- import { executeWorkflow } from "./workflow-executor.ts";
13
- import type { StepResult, StepUsage } from "./types.ts";
14
-
15
- /** Create a temp directory for test fixtures. */
16
- function makeTmpDir(): string {
17
- return fs.mkdtempSync(path.join(os.tmpdir(), "retry-test-"));
18
- }
19
-
20
- /** Clean up temp directory. */
21
- function cleanDir(dir: string): void {
22
- try {
23
- fs.rmSync(dir, { recursive: true, force: true });
24
- } catch { /* ignore */ }
25
- }
26
-
27
- function zeroUsage(): StepUsage {
28
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
29
- }
30
-
31
- // ── Block validation / rollback tests ──
32
-
33
- describe("snapshotBlockFiles", () => {
34
- let tmpDir: string;
35
-
36
- beforeEach(() => {
37
- tmpDir = makeTmpDir();
38
- });
39
-
40
- afterEach(() => {
41
- cleanDir(tmpDir);
42
- });
43
-
44
- it("captures file content", () => {
45
- const wfDir = path.join(tmpDir, ".project");
46
- fs.mkdirSync(wfDir, { recursive: true });
47
- fs.writeFileSync(path.join(wfDir, "state.json"), '{"version": 1}');
48
- fs.writeFileSync(path.join(wfDir, "gaps.json"), '{"gaps": []}');
49
-
50
- const snapshot = snapshotBlockFiles(tmpDir);
51
- assert.equal(snapshot.size, 2);
52
-
53
- const stateSnap = snapshot.get(path.join(wfDir, "state.json"));
54
- assert.ok(stateSnap);
55
- assert.equal(stateSnap.content, '{"version": 1}');
56
- assert.equal(typeof stateSnap.mtime, "number");
57
-
58
- const gapsSnap = snapshot.get(path.join(wfDir, "gaps.json"));
59
- assert.ok(gapsSnap);
60
- assert.equal(gapsSnap.content, '{"gaps": []}');
61
- });
62
-
63
- it("returns empty map when .project/ does not exist", () => {
64
- const snapshot = snapshotBlockFiles(tmpDir);
65
- assert.equal(snapshot.size, 0);
66
- });
67
- });
68
-
69
- describe("rollbackBlockFiles", () => {
70
- let tmpDir: string;
71
-
72
- beforeEach(() => {
73
- tmpDir = makeTmpDir();
74
- });
75
-
76
- afterEach(() => {
77
- cleanDir(tmpDir);
78
- });
79
-
80
- it("restores changed file content", () => {
81
- const wfDir = path.join(tmpDir, ".project");
82
- fs.mkdirSync(wfDir, { recursive: true });
83
- fs.writeFileSync(path.join(wfDir, "state.json"), '{"version": 1}');
84
-
85
- const snapshot = snapshotBlockFiles(tmpDir);
86
-
87
- // Simulate a step modifying the file
88
- fs.writeFileSync(path.join(wfDir, "state.json"), '{"version": 2}');
89
-
90
- const rolled = rollbackBlockFiles(tmpDir, snapshot);
91
- assert.equal(rolled.length, 1);
92
-
93
- const content = fs.readFileSync(path.join(wfDir, "state.json"), "utf-8");
94
- assert.equal(content, '{"version": 1}');
95
- });
96
-
97
- it("deletes new files not in snapshot", () => {
98
- const wfDir = path.join(tmpDir, ".project");
99
- fs.mkdirSync(wfDir, { recursive: true });
100
-
101
- const snapshot = snapshotBlockFiles(tmpDir);
102
-
103
- // Simulate a step creating a new file
104
- fs.writeFileSync(path.join(wfDir, "new-file.json"), '{"new": true}');
105
-
106
- const rolled = rollbackBlockFiles(tmpDir, snapshot);
107
- assert.equal(rolled.length, 1);
108
- assert.equal(fs.existsSync(path.join(wfDir, "new-file.json")), false);
109
- });
110
-
111
- it("does not touch unchanged files", () => {
112
- const wfDir = path.join(tmpDir, ".project");
113
- fs.mkdirSync(wfDir, { recursive: true });
114
- fs.writeFileSync(path.join(wfDir, "state.json"), '{"version": 1}');
115
-
116
- const snapshot = snapshotBlockFiles(tmpDir);
117
-
118
- // Don't modify anything
119
- const rolled = rollbackBlockFiles(tmpDir, snapshot);
120
- assert.equal(rolled.length, 0);
121
-
122
- const content = fs.readFileSync(path.join(wfDir, "state.json"), "utf-8");
123
- assert.equal(content, '{"version": 1}');
124
- });
125
- });
126
-
127
- // ── Workflow spec parsing tests ──
128
-
129
- describe("workflow-spec retry parsing", () => {
130
- it("parses retry config from step YAML", () => {
131
- const yaml = `
132
- name: test-retry
133
- description: test
134
- steps:
135
- analyze:
136
- agent: code-analyzer
137
- retry:
138
- maxAttempts: 3
139
- onExhausted: skip
140
- steeringMessage: Focus on valid JSON output.
141
- `;
142
- const spec = parseWorkflowSpec(yaml, "/test.project.yaml", "project");
143
- const step = spec.steps.analyze;
144
- assert.ok(step.retry);
145
- assert.equal(step.retry.maxAttempts, 3);
146
- assert.equal(step.retry.onExhausted, "skip");
147
- assert.equal(step.retry.steeringMessage, "Focus on valid JSON output.");
148
- });
149
-
150
- it("step without retry config has no retry field", () => {
151
- const yaml = `
152
- name: test-no-retry
153
- description: test
154
- steps:
155
- analyze:
156
- agent: code-analyzer
157
- `;
158
- const spec = parseWorkflowSpec(yaml, "/test.project.yaml", "project");
159
- assert.equal(spec.steps.analyze.retry, undefined);
160
- });
161
-
162
- it("rejects invalid retry.maxAttempts", () => {
163
- const yaml = `
164
- name: test-invalid
165
- description: test
166
- steps:
167
- analyze:
168
- agent: code-analyzer
169
- retry:
170
- maxAttempts: -1
171
- `;
172
- assert.throws(() => {
173
- parseWorkflowSpec(yaml, "/test.project.yaml", "project");
174
- }, /retry\.maxAttempts must be a positive integer/);
175
- });
176
-
177
- it("rejects invalid retry.onExhausted", () => {
178
- const yaml = `
179
- name: test-invalid
180
- description: test
181
- steps:
182
- analyze:
183
- agent: code-analyzer
184
- retry:
185
- maxAttempts: 2
186
- onExhausted: retry
187
- `;
188
- assert.throws(() => {
189
- parseWorkflowSpec(yaml, "/test.project.yaml", "project");
190
- }, /retry\.onExhausted must be 'fail' or 'skip'/);
191
- });
192
- });
193
-
194
- // ── Retry integration tests (using injectable dispatch) ──
195
-
196
- describe("graduated failure retry", () => {
197
- let tmpDir: string;
198
-
199
- beforeEach(() => {
200
- tmpDir = makeTmpDir();
201
- // Create minimal run dir structure
202
- const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs");
203
- fs.mkdirSync(runDir, { recursive: true });
204
- });
205
-
206
- afterEach(() => {
207
- cleanDir(tmpDir);
208
- });
209
-
210
- /** Minimal mock context */
211
- function mockCtx() {
212
- return {
213
- cwd: tmpDir,
214
- hasUI: false,
215
- ui: { setWidget() {}, notify() {}, setWorkingMessage() {} },
216
- };
217
- }
218
-
219
- /** Minimal mock pi */
220
- function mockPi() {
221
- const messages: any[] = [];
222
- return {
223
- sendMessage(msg: any, opts?: any) { messages.push({ msg, opts }); },
224
- messages,
225
- };
226
- }
227
-
228
- /** Build a dispatch function that fails N times then succeeds */
229
- function buildDispatchFn(failCount: number, errorMessage = "Agent failed") {
230
- let callCount = 0;
231
- return async (_stepSpec: any, _agentSpec: any, prompt: string, _opts: any): Promise<StepResult> => {
232
- callCount++;
233
- if (callCount <= failCount) {
234
- return {
235
- step: _opts.stepName,
236
- agent: _stepSpec.agent ?? "test-agent",
237
- status: "failed",
238
- usage: zeroUsage(),
239
- durationMs: 100,
240
- error: errorMessage,
241
- };
242
- }
243
- return {
244
- step: _opts.stepName,
245
- agent: _stepSpec.agent ?? "test-agent",
246
- status: "completed",
247
- output: { result: "success" },
248
- textOutput: '{"result": "success"}',
249
- usage: zeroUsage(),
250
- durationMs: 100,
251
- };
252
- };
253
- }
254
-
255
- it("agent step fails first, succeeds on attempt 2", async () => {
256
- const yaml = `
257
- name: test-retry
258
- description: test
259
- steps:
260
- analyze:
261
- agent: test-agent
262
- retry:
263
- maxAttempts: 3
264
- `;
265
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
266
- const dispatchFn = buildDispatchFn(1);
267
-
268
- const result = await executeWorkflow(spec, {}, {
269
- ctx: mockCtx(),
270
- pi: mockPi(),
271
- loadAgent: () => ({ name: "test-agent" }),
272
- dispatchFn,
273
- });
274
-
275
- assert.equal(result.status, "completed");
276
- const stepResult = result.steps.analyze;
277
- assert.equal(stepResult.status, "completed");
278
- assert.equal(stepResult.attempt, 2);
279
- assert.equal(stepResult.totalAttempts, 2);
280
- assert.ok(stepResult.priorErrors);
281
- assert.equal(stepResult.priorErrors.length, 1);
282
- assert.equal(stepResult.priorErrors[0], "Agent failed");
283
- });
284
-
285
- it("agent step exhausts 3 attempts with onExhausted=fail", async () => {
286
- const yaml = `
287
- name: test-retry-fail
288
- description: test
289
- steps:
290
- analyze:
291
- agent: test-agent
292
- retry:
293
- maxAttempts: 3
294
- onExhausted: fail
295
- `;
296
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
297
- const dispatchFn = buildDispatchFn(5); // always fails
298
-
299
- const result = await executeWorkflow(spec, {}, {
300
- ctx: mockCtx(),
301
- pi: mockPi(),
302
- loadAgent: () => ({ name: "test-agent" }),
303
- dispatchFn,
304
- });
305
-
306
- assert.equal(result.status, "failed");
307
- const stepResult = result.steps.analyze;
308
- assert.equal(stepResult.status, "failed");
309
- assert.equal(stepResult.attempt, 3);
310
- assert.equal(stepResult.totalAttempts, 3);
311
- assert.ok(stepResult.priorErrors);
312
- assert.equal(stepResult.priorErrors.length, 3);
313
- });
314
-
315
- it("agent step exhausts attempts with onExhausted=skip — workflow continues", async () => {
316
- const yaml = `
317
- name: test-retry-skip
318
- description: test
319
- steps:
320
- analyze:
321
- agent: test-agent
322
- retry:
323
- maxAttempts: 2
324
- onExhausted: skip
325
- followup:
326
- agent: test-agent
327
- `;
328
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
329
- let callCount = 0;
330
- const dispatchFn = async (_stepSpec: any, _agentSpec: any, _prompt: string, opts: any): Promise<StepResult> => {
331
- callCount++;
332
- // analyze always fails, followup succeeds
333
- if (opts.stepName === "analyze") {
334
- return {
335
- step: opts.stepName, agent: "test-agent", status: "failed",
336
- usage: zeroUsage(), durationMs: 100, error: "fail",
337
- };
338
- }
339
- return {
340
- step: opts.stepName, agent: "test-agent", status: "completed",
341
- output: { ok: true }, textOutput: '{"ok":true}',
342
- usage: zeroUsage(), durationMs: 100,
343
- };
344
- };
345
-
346
- const result = await executeWorkflow(spec, {}, {
347
- ctx: mockCtx(),
348
- pi: mockPi(),
349
- loadAgent: () => ({ name: "test-agent" }),
350
- dispatchFn,
351
- });
352
-
353
- assert.equal(result.status, "completed");
354
- assert.equal(result.steps.analyze.status, "skipped");
355
- assert.ok(result.steps.analyze.warnings);
356
- assert.ok(result.steps.analyze.warnings.some((w: string) => w.includes("onExhausted: skip")));
357
- assert.equal(result.steps.followup.status, "completed");
358
- });
359
-
360
- it("command step with retry config is ignored — fails immediately", async () => {
361
- const yaml = `
362
- name: test-command-retry
363
- description: test
364
- steps:
365
- check:
366
- command: "exit 1"
367
- retry:
368
- maxAttempts: 3
369
- `;
370
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
371
-
372
- const result = await executeWorkflow(spec, {}, {
373
- ctx: mockCtx(),
374
- pi: mockPi(),
375
- loadAgent: () => ({ name: "test-agent" }),
376
- });
377
-
378
- assert.equal(result.status, "failed");
379
- const stepResult = result.steps.check;
380
- assert.equal(stepResult.status, "failed");
381
- // Should not have retried — no attempt tracking beyond 1
382
- assert.equal(stepResult.attempt, 1);
383
- assert.equal(stepResult.totalAttempts, 1);
384
- });
385
-
386
- it("block validation failure triggers rollback and retry", async () => {
387
- // Create .project dir with a schema that demands version=1
388
- const wfDir = path.join(tmpDir, ".project");
389
- const schemasDir = path.join(wfDir, "schemas");
390
- fs.mkdirSync(schemasDir, { recursive: true });
391
- fs.writeFileSync(path.join(wfDir, "state.json"), '{"version": 1}');
392
- fs.writeFileSync(path.join(schemasDir, "state.schema.json"), JSON.stringify({
393
- type: "object",
394
- properties: { version: { type: "number", const: 1 } },
395
- required: ["version"],
396
- }));
397
-
398
- const yaml = `
399
- name: test-block-retry
400
- description: test
401
- steps:
402
- update:
403
- agent: test-agent
404
- retry:
405
- maxAttempts: 3
406
- `;
407
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
408
-
409
- let attemptCount = 0;
410
- const dispatchFn = async (_stepSpec: any, _agentSpec: any, _prompt: string, opts: any): Promise<StepResult> => {
411
- attemptCount++;
412
- // On first attempt, corrupt the block file. On second, leave it alone.
413
- if (attemptCount === 1) {
414
- fs.writeFileSync(path.join(wfDir, "state.json"), '{"version": 2}');
415
- }
416
- return {
417
- step: opts.stepName, agent: "test-agent", status: "completed",
418
- output: { done: true }, textOutput: '{"done":true}',
419
- usage: zeroUsage(), durationMs: 100,
420
- };
421
- };
422
-
423
- const result = await executeWorkflow(spec, {}, {
424
- ctx: mockCtx(),
425
- pi: mockPi(),
426
- loadAgent: () => ({ name: "test-agent" }),
427
- dispatchFn,
428
- });
429
-
430
- assert.equal(result.status, "completed");
431
- assert.equal(result.steps.update.status, "completed");
432
- assert.equal(result.steps.update.attempt, 2);
433
- // Verify file was rolled back before second attempt
434
- const content = fs.readFileSync(path.join(wfDir, "state.json"), "utf-8");
435
- assert.equal(content, '{"version": 1}');
436
- });
437
-
438
- it("block validation rollback deletes new files created during step", async () => {
439
- const wfDir = path.join(tmpDir, ".project");
440
- const schemasDir = path.join(wfDir, "schemas");
441
- fs.mkdirSync(schemasDir, { recursive: true });
442
- // Schema that always fails for newfile
443
- fs.writeFileSync(path.join(schemasDir, "newfile.schema.json"), JSON.stringify({
444
- type: "object",
445
- properties: { valid: { type: "boolean", const: true } },
446
- required: ["valid"],
447
- }));
448
-
449
- const yaml = `
450
- name: test-new-file-rollback
451
- description: test
452
- steps:
453
- create:
454
- agent: test-agent
455
- retry:
456
- maxAttempts: 2
457
- `;
458
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
459
-
460
- let attemptCount = 0;
461
- const dispatchFn = async (_stepSpec: any, _agentSpec: any, _prompt: string, opts: any): Promise<StepResult> => {
462
- attemptCount++;
463
- if (attemptCount === 1) {
464
- // Create a new block file that fails validation
465
- fs.writeFileSync(path.join(wfDir, "newfile.json"), '{"valid": false}');
466
- }
467
- // Second attempt: don't create the file
468
- return {
469
- step: opts.stepName, agent: "test-agent", status: "completed",
470
- output: { done: true }, textOutput: '{"done":true}',
471
- usage: zeroUsage(), durationMs: 100,
472
- };
473
- };
474
-
475
- const result = await executeWorkflow(spec, {}, {
476
- ctx: mockCtx(),
477
- pi: mockPi(),
478
- loadAgent: () => ({ name: "test-agent" }),
479
- dispatchFn,
480
- });
481
-
482
- assert.equal(result.status, "completed");
483
- // The new file should have been deleted during rollback
484
- assert.equal(fs.existsSync(path.join(wfDir, "newfile.json")), false);
485
- });
486
-
487
- it("step with no retry config — backward compatible, same as current", async () => {
488
- const yaml = `
489
- name: test-no-retry
490
- description: test
491
- steps:
492
- analyze:
493
- agent: test-agent
494
- `;
495
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
496
- const dispatchFn = buildDispatchFn(0); // succeeds immediately
497
-
498
- const result = await executeWorkflow(spec, {}, {
499
- ctx: mockCtx(),
500
- pi: mockPi(),
501
- loadAgent: () => ({ name: "test-agent" }),
502
- dispatchFn,
503
- });
504
-
505
- assert.equal(result.status, "completed");
506
- const stepResult = result.steps.analyze;
507
- assert.equal(stepResult.status, "completed");
508
- assert.equal(stepResult.attempt, 1);
509
- assert.equal(stepResult.totalAttempts, 1);
510
- assert.equal(stepResult.priorErrors, undefined);
511
- });
512
-
513
- it("cancelled workflow during retry — cancellation respected between attempts", async () => {
514
- const yaml = `
515
- name: test-cancel-retry
516
- description: test
517
- steps:
518
- analyze:
519
- agent: test-agent
520
- retry:
521
- maxAttempts: 5
522
- `;
523
- const spec = parseWorkflowSpec(yaml, path.join(tmpDir, "test.project.yaml"), "project");
524
- const controller = new AbortController();
525
-
526
- let callCount = 0;
527
- const dispatchFn = async (_stepSpec: any, _agentSpec: any, _prompt: string, opts: any): Promise<StepResult> => {
528
- callCount++;
529
- // Fail first attempt, then abort synchronously before returning
530
- if (callCount === 1) {
531
- controller.abort();
532
- return {
533
- step: opts.stepName, agent: "test-agent", status: "failed",
534
- usage: zeroUsage(), durationMs: 100, error: "first fail",
535
- };
536
- }
537
- return {
538
- step: opts.stepName, agent: "test-agent", status: "completed",
539
- output: {}, textOutput: "{}",
540
- usage: zeroUsage(), durationMs: 100,
541
- };
542
- };
543
-
544
- const result = await executeWorkflow(spec, {}, {
545
- ctx: mockCtx(),
546
- pi: mockPi(),
547
- signal: controller.signal,
548
- loadAgent: () => ({ name: "test-agent" }),
549
- dispatchFn,
550
- });
551
-
552
- assert.equal(result.status, "failed");
553
- // Should have only made 1 dispatch call before cancellation
554
- assert.equal(callCount, 1);
555
- });
556
- });
package/src/index.test.ts DELETED
@@ -1,114 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { parseAgentYaml, createAgentLoader } from "./agent-spec.ts";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
7
-
8
- describe("parseAgentYaml", () => {
9
- it("parses YAML agent spec with all fields", (t) => {
10
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-agent-"));
11
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
12
-
13
- const specPath = path.join(tmpDir, "test.agent.yaml");
14
- fs.writeFileSync(specPath, `
15
- name: test-agent
16
- role: sensor
17
- description: A test agent
18
- model: claude-sonnet-4-6
19
- tools: [read, bash]
20
- thinking: low
21
- input:
22
- type: object
23
- required: [path]
24
- properties:
25
- path: { type: string }
26
- output:
27
- format: json
28
- schema: test.schema.json
29
- prompt:
30
- system: test/system.md
31
- task: test/task.md
32
- `);
33
-
34
- const spec = parseAgentYaml(specPath);
35
- assert.strictEqual(spec.name, "test-agent");
36
- assert.strictEqual(spec.role, "sensor");
37
- assert.strictEqual(spec.description, "A test agent");
38
- assert.strictEqual(spec.model, "claude-sonnet-4-6");
39
- assert.deepStrictEqual(spec.tools, ["read", "bash"]);
40
- assert.strictEqual(spec.thinking, "low");
41
- assert.strictEqual(spec.promptTemplate, "test/system.md");
42
- assert.strictEqual(spec.taskTemplate, "test/task.md");
43
- assert.strictEqual(spec.outputFormat, "json");
44
- assert.strictEqual(spec.outputSchema, "test.schema.json");
45
- assert.deepStrictEqual(spec.inputSchema?.required, ["path"]);
46
- });
47
-
48
- it("uses filename as name when spec has no name field", (t) => {
49
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-agent-"));
50
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
51
-
52
- const specPath = path.join(tmpDir, "my-agent.agent.yaml");
53
- fs.writeFileSync(specPath, `
54
- tools: [read]
55
- prompt:
56
- system: my/system.md
57
- `);
58
-
59
- const spec = parseAgentYaml(specPath);
60
- assert.strictEqual(spec.name, "my-agent");
61
- });
62
-
63
- it("parses minimal spec", (t) => {
64
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-agent-"));
65
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
66
-
67
- const specPath = path.join(tmpDir, "minimal.agent.yaml");
68
- fs.writeFileSync(specPath, `
69
- name: minimal
70
- tools: [read]
71
- `);
72
-
73
- const spec = parseAgentYaml(specPath);
74
- assert.strictEqual(spec.name, "minimal");
75
- assert.deepStrictEqual(spec.tools, ["read"]);
76
- assert.strictEqual(spec.promptTemplate, undefined);
77
- assert.strictEqual(spec.taskTemplate, undefined);
78
- assert.strictEqual(spec.inputSchema, undefined);
79
- });
80
- });
81
-
82
- describe("createAgentLoader", () => {
83
- it("loads agent from project .pi/agents/ directory", (t) => {
84
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-loader-"));
85
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
86
-
87
- const agentDir = path.join(tmpDir, ".pi", "agents");
88
- fs.mkdirSync(agentDir, { recursive: true });
89
- fs.writeFileSync(path.join(agentDir, "my-agent.agent.yaml"), `
90
- name: my-agent
91
- role: action
92
- tools: [bash, edit]
93
- prompt:
94
- system: my/system.md
95
- `);
96
-
97
- const loader = createAgentLoader(tmpDir);
98
- const spec = loader("my-agent");
99
- assert.strictEqual(spec.name, "my-agent");
100
- assert.strictEqual(spec.role, "action");
101
- assert.deepStrictEqual(spec.tools, ["bash", "edit"]);
102
- });
103
-
104
- it("throws AgentNotFoundError for unknown agents", (t) => {
105
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-loader-"));
106
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
107
-
108
- const loader = createAgentLoader(tmpDir);
109
- assert.throws(
110
- () => loader("nonexistent"),
111
- (err: any) => err.name === "AgentNotFoundError",
112
- );
113
- });
114
- });