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