@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,213 +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 { persistStepOutput } from "./output.ts";
7
-
8
- describe("persistStepOutput", () => {
9
- it("writes structured output as JSON", (t) => {
10
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
11
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
12
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
13
-
14
- const result = persistStepOutput(tmpDir, "analyze", { findings: ["a", "b"] });
15
- assert.ok(result);
16
- assert.ok(result.endsWith("analyze.json"));
17
- const content = JSON.parse(fs.readFileSync(result, "utf-8"));
18
- assert.deepStrictEqual(content, { findings: ["a", "b"] });
19
- });
20
-
21
- it("wraps string output as JSON", (t) => {
22
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
23
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
24
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
25
-
26
- const result = persistStepOutput(tmpDir, "explore", undefined, "Found patterns");
27
- assert.ok(result);
28
- assert.ok(result.endsWith("explore.json"));
29
- const content = JSON.parse(fs.readFileSync(result, "utf-8"));
30
- assert.deepStrictEqual(content, { text: "Found patterns" });
31
- });
32
-
33
- it("prefers structured output over textOutput", (t) => {
34
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
35
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
36
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
37
-
38
- const result = persistStepOutput(tmpDir, "step1", { key: "val" }, "text fallback");
39
- assert.ok(result);
40
- const content = JSON.parse(fs.readFileSync(result, "utf-8"));
41
- assert.deepStrictEqual(content, { key: "val" });
42
- });
43
-
44
- it("wraps string output field as JSON", (t) => {
45
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
46
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
47
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
48
-
49
- const result = persistStepOutput(tmpDir, "step1", "plain string");
50
- assert.ok(result);
51
- assert.ok(result.endsWith(".json"));
52
- const content = JSON.parse(fs.readFileSync(result, "utf-8"));
53
- assert.deepStrictEqual(content, { text: "plain string" });
54
- });
55
-
56
- it("returns undefined when nothing to write", (t) => {
57
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
58
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
59
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
60
- assert.strictEqual(persistStepOutput(tmpDir, "empty", undefined, undefined), undefined);
61
- });
62
-
63
- it("returns undefined for empty string", (t) => {
64
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
65
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
66
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
67
- assert.strictEqual(persistStepOutput(tmpDir, "empty", undefined, ""), undefined);
68
- });
69
-
70
- it("writes array output as JSON", (t) => {
71
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
72
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
73
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
74
-
75
- const result = persistStepOutput(tmpDir, "items", ["a", "b", "c"]);
76
- assert.ok(result);
77
- assert.ok(result.endsWith(".json"));
78
- assert.deepStrictEqual(JSON.parse(fs.readFileSync(result, "utf-8")), ["a", "b", "c"]);
79
- });
80
-
81
- it("all outputs are JSON regardless of input type", (t) => {
82
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
83
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
84
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
85
-
86
- const r1 = persistStepOutput(tmpDir, "s1", { a: 1 });
87
- const r2 = persistStepOutput(tmpDir, "s2", "hello");
88
- const r3 = persistStepOutput(tmpDir, "s3", undefined, "world");
89
- assert.ok(r1!.endsWith(".json"));
90
- assert.ok(r2!.endsWith(".json"));
91
- assert.ok(r3!.endsWith(".json"));
92
- });
93
-
94
- it("creates outputs directory if missing", (t) => {
95
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
96
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
97
- // Don't create outputs/ — persistStepOutput should handle it
98
-
99
- const result = persistStepOutput(tmpDir, "auto", { data: true });
100
- assert.ok(result);
101
- assert.ok(fs.existsSync(result));
102
- assert.deepStrictEqual(JSON.parse(fs.readFileSync(result, "utf-8")), { data: true });
103
- });
104
-
105
- it("writes to author-declared output path", (t) => {
106
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
107
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
108
-
109
- const customPath = path.join(tmpDir, "reports", "analysis", "structure.json");
110
- const result = persistStepOutput(tmpDir, "step1", { key: "val" }, undefined, customPath);
111
- assert.strictEqual(result, customPath);
112
- assert.ok(fs.existsSync(customPath));
113
- assert.deepStrictEqual(JSON.parse(fs.readFileSync(customPath, "utf-8")), { key: "val" });
114
- });
115
-
116
- it("creates parent directories for author-declared path", (t) => {
117
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
118
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
119
-
120
- const deepPath = path.join(tmpDir, "a", "b", "c", "result.json");
121
- const result = persistStepOutput(tmpDir, "step1", ["x"], undefined, deepPath);
122
- assert.strictEqual(result, deepPath);
123
- assert.deepStrictEqual(JSON.parse(fs.readFileSync(deepPath, "utf-8")), ["x"]);
124
- });
125
-
126
- it("defaults to run dir when no output path declared", (t) => {
127
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
128
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
129
-
130
- const result = persistStepOutput(tmpDir, "my-step", { a: 1 });
131
- assert.ok(result);
132
- assert.strictEqual(result, path.join(tmpDir, "outputs", "my-step.json"));
133
- });
134
- });
135
-
136
- describe("persistStepOutput error handling", () => {
137
- it("returns undefined when directory creation fails", (t) => {
138
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
139
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
140
-
141
- // Create a file where the outputs directory should be — mkdirSync will fail with ENOTDIR
142
- const blocker = path.join(tmpDir, "outputs");
143
- fs.writeFileSync(blocker, "not a directory");
144
-
145
- const result = persistStepOutput(tmpDir, "step1", { data: true });
146
- assert.strictEqual(result, undefined);
147
- });
148
-
149
- it("returns undefined when write to author-declared path fails", (t) => {
150
- // Use /dev/null/impossible on unix — a known-bad path
151
- const result = persistStepOutput(
152
- "/tmp", "step1", { data: true }, undefined,
153
- "/dev/null/impossible/path/output.json",
154
- );
155
- assert.strictEqual(result, undefined);
156
- });
157
-
158
- it("returns undefined when file write fails due to unwritable directory", (t) => {
159
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
160
- t.after(() => {
161
- // Restore permissions for cleanup
162
- try { fs.chmodSync(path.join(tmpDir, "outputs"), 0o755); } catch { /* ignore */ }
163
- fs.rmSync(tmpDir, { recursive: true, force: true });
164
- });
165
-
166
- // Create outputs dir then make it read-only
167
- const outputsDir = path.join(tmpDir, "outputs");
168
- fs.mkdirSync(outputsDir, { recursive: true });
169
- fs.chmodSync(outputsDir, 0o444);
170
-
171
- const result = persistStepOutput(tmpDir, "step1", { data: true });
172
- assert.strictEqual(result, undefined);
173
- });
174
-
175
- it("writes warning to stderr on failure", (t) => {
176
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
177
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
178
-
179
- // Create a file where outputs dir should be
180
- fs.writeFileSync(path.join(tmpDir, "outputs"), "blocker");
181
-
182
- // Capture stderr
183
- const originalWrite = process.stderr.write;
184
- let captured = "";
185
- process.stderr.write = ((chunk: string) => {
186
- captured += chunk;
187
- return true;
188
- }) as typeof process.stderr.write;
189
- t.after(() => { process.stderr.write = originalWrite; });
190
-
191
- persistStepOutput(tmpDir, "step1", { data: true });
192
- assert.ok(captured.includes("step1"));
193
- assert.ok(captured.includes("[pi-workflows]"));
194
- });
195
-
196
- it("returns undefined for string output when directory creation fails", (t) => {
197
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
198
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
199
-
200
- // Create a file where the outputs directory should be
201
- fs.writeFileSync(path.join(tmpDir, "outputs"), "not a directory");
202
-
203
- const result = persistStepOutput(tmpDir, "step1", undefined, "some text");
204
- assert.strictEqual(result, undefined);
205
- });
206
-
207
- it("does not throw on write failure — existing tests still pass", () => {
208
- // Verify that all the existing happy-path tests still work unchanged.
209
- // This test is a meta-check — if output.test.ts runs without error,
210
- // the non-throwing behavior is confirmed alongside existing behavior.
211
- assert.ok(true);
212
- });
213
- });
package/src/output.ts DELETED
@@ -1,70 +0,0 @@
1
- /**
2
- * Step output persistence.
3
- *
4
- * Every step that produces output writes it as JSON to disk.
5
- * The workflow author controls the output path via the step spec's
6
- * output.path field. When no path is declared, defaults to
7
- * outputs/<stepName>.json in the run directory.
8
- *
9
- * JSON is the intermediate representation between steps — structured,
10
- * validated, consumable by templates, filterable by transforms, mergeable
11
- * across parallel runs. Text is rendered FROM JSON via templates. It's
12
- * never the data format.
13
- */
14
- import fs from "node:fs";
15
- import path from "node:path";
16
-
17
- /**
18
- * Persist a step's output as JSON to disk.
19
- *
20
- * @param runDir - workflow run directory (default location for outputs)
21
- * @param stepName - step name (used for default filename)
22
- * @param output - structured output (object/array) or string
23
- * @param textOutput - fallback text output if output is undefined
24
- * @param outputPath - author-declared output path (from step spec output.path, already resolved)
25
- * @returns absolute path to the written JSON file, or undefined if nothing to write
26
- */
27
- export function persistStepOutput(
28
- runDir: string,
29
- stepName: string,
30
- output: unknown,
31
- textOutput?: string,
32
- outputPath?: string,
33
- ): string | undefined {
34
- // Author-declared path takes priority; default to run dir
35
- const filePath = outputPath ?? path.join(runDir, "outputs", `${stepName}.json`);
36
- const dir = path.dirname(filePath);
37
-
38
- // Structured output — the primary case
39
- if (output !== undefined && output !== null && typeof output === "object") {
40
- try {
41
- fs.mkdirSync(dir, { recursive: true });
42
- fs.writeFileSync(filePath, JSON.stringify(output, null, 2), "utf-8");
43
- return filePath;
44
- } catch (err) {
45
- const msg = err instanceof Error ? err.message : String(err);
46
- process.stderr.write(
47
- `[pi-workflows] Warning: failed to persist output for step '${stepName}' to ${filePath}: ${msg}\n`,
48
- );
49
- return undefined;
50
- }
51
- }
52
-
53
- // String output — wrap in JSON to maintain uniform contract
54
- const text = typeof output === "string" ? output : textOutput;
55
- if (text) {
56
- try {
57
- fs.mkdirSync(dir, { recursive: true });
58
- fs.writeFileSync(filePath, JSON.stringify({ text }, null, 2), "utf-8");
59
- return filePath;
60
- } catch (err) {
61
- const msg = err instanceof Error ? err.message : String(err);
62
- process.stderr.write(
63
- `[pi-workflows] Warning: failed to persist output for step '${stepName}' to ${filePath}: ${msg}\n`,
64
- );
65
- return undefined;
66
- }
67
- }
68
-
69
- return undefined;
70
- }
@@ -1,175 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { buildExecutionPlan } from "./dag.ts";
4
- import { parseWorkflowSpec } from "./workflow-spec.ts";
5
- import { executeWorkflow } from "./workflow-executor.ts";
6
- import type { WorkflowSpec } from "./types.ts";
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import os from "node:os";
10
-
11
- // Mock ExtensionContext and ExtensionAPI for testing
12
- function mockCtx(cwd: string) {
13
- return {
14
- cwd,
15
- hasUI: false,
16
- ui: {
17
- setWidget: () => {},
18
- notify: () => {},
19
- setStatus: () => {},
20
- },
21
- } as any;
22
- }
23
-
24
- function mockPi() {
25
- const messages: any[] = [];
26
- return {
27
- sendMessage: (msg: any, opts: any) => messages.push({ msg, opts }),
28
- _messages: messages,
29
- } as any;
30
- }
31
-
32
- function mockOptions(tmpDir?: string) {
33
- const cwd = tmpDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "wf-parallel-"));
34
- return {
35
- ctx: mockCtx(cwd),
36
- pi: mockPi(),
37
- loadAgent: (name: string) => ({ name }),
38
- };
39
- }
40
-
41
- function makeSpec(overrides: Partial<WorkflowSpec> & { steps: WorkflowSpec["steps"] }): WorkflowSpec {
42
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-parallel-"));
43
- return {
44
- name: "test-parallel",
45
- description: "test",
46
- source: "project" as const,
47
- filePath: path.join(tmpDir, "test.workflow.yaml"),
48
- ...overrides,
49
- };
50
- }
51
-
52
- describe("parallel integration — DAG analysis", () => {
53
- it("DAG-inferred parallel workflow produces correct execution plan", () => {
54
- const yaml = fs.readFileSync(
55
- path.resolve(import.meta.dirname, "../workflows/parallel-analysis.workflow.yaml"),
56
- "utf-8",
57
- );
58
- const spec = parseWorkflowSpec(yaml, "parallel-analysis.workflow.yaml", "project");
59
- const plan = buildExecutionPlan(spec);
60
-
61
- // Layer 0: explore (no deps)
62
- assert.deepStrictEqual(plan[0].steps, ["explore"]);
63
-
64
- // Layer 1: three analyzers (all depend only on explore)
65
- assert.strictEqual(plan[1].steps.length, 3);
66
- assert.ok(plan[1].steps.includes("analyze-structure"));
67
- assert.ok(plan[1].steps.includes("analyze-quality"));
68
- assert.ok(plan[1].steps.includes("analyze-patterns"));
69
-
70
- // Layer 2: synthesize (depends on all three analyzers)
71
- assert.deepStrictEqual(plan[2].steps, ["synthesize"]);
72
- });
73
-
74
- it("explicit parallel workflow parses correctly", () => {
75
- const yaml = fs.readFileSync(
76
- path.resolve(import.meta.dirname, "../workflows/parallel-explicit.workflow.yaml"),
77
- "utf-8",
78
- );
79
- const spec = parseWorkflowSpec(yaml, "parallel-explicit.workflow.yaml", "project");
80
-
81
- assert.ok(spec.steps.analyzers);
82
- assert.ok(spec.steps.analyzers.parallel);
83
- assert.ok(spec.steps.analyzers.parallel!.structure);
84
- assert.ok(spec.steps.analyzers.parallel!.quality);
85
- });
86
- });
87
-
88
- describe("parallel integration — execution", () => {
89
- it("parallel gate steps complete faster than sequential", async () => {
90
- // Two 2-second sleeps in parallel should take ~2s, not ~4s.
91
- // Steps need explicit ${{ steps.X }} deps to be parallelized
92
- // (conservative plan adds implicit sequential deps for steps without them).
93
- const spec = makeSpec({
94
- name: "timing-test",
95
- steps: {
96
- source: {
97
- transform: { mapping: { ready: true } },
98
- },
99
- a: {
100
- gate: { check: "sleep 2 && echo done-a" },
101
- input: { trigger: "${{ steps.source.output.ready }}" },
102
- },
103
- b: {
104
- gate: { check: "sleep 2 && echo done-b" },
105
- input: { trigger: "${{ steps.source.output.ready }}" },
106
- },
107
- },
108
- });
109
-
110
- const startTime = Date.now();
111
- const result = await executeWorkflow(spec, {}, mockOptions());
112
- const elapsed = Date.now() - startTime;
113
-
114
- assert.strictEqual(result.status, "completed");
115
- // If parallel: ~2s. If sequential: ~4s. Allow margin.
116
- assert.ok(elapsed < 3500, `Expected parallel execution (~2s) but took ${elapsed}ms`);
117
- });
118
-
119
- it("timeout kills a slow gate step", async () => {
120
- const spec = makeSpec({
121
- name: "timeout-test",
122
- steps: {
123
- slow: {
124
- gate: { check: "sleep 60 && echo done" },
125
- timeout: { seconds: 2 },
126
- },
127
- },
128
- });
129
-
130
- const startTime = Date.now();
131
- const result = await executeWorkflow(spec, {}, mockOptions());
132
- const elapsed = Date.now() - startTime;
133
-
134
- // Gate check failed because process was killed by timeout
135
- assert.strictEqual(result.status, "failed");
136
- assert.ok(elapsed < 10000, `Timeout should have killed step quickly, took ${elapsed}ms`);
137
- });
138
-
139
- it("parallel step failure cancels siblings", async () => {
140
- // fast_fail and slow_success both depend on source → parallel layer.
141
- // fast_fail exits immediately with error → slow_success should be cancelled.
142
- const spec = makeSpec({
143
- name: "cancel-test",
144
- steps: {
145
- source: {
146
- transform: { mapping: { ready: true } },
147
- },
148
- fast_fail: {
149
- gate: { check: "exit 1", onFail: "fail" },
150
- input: { trigger: "${{ steps.source.output.ready }}" },
151
- },
152
- slow_success: {
153
- gate: { check: "sleep 10 && echo done" },
154
- input: { trigger: "${{ steps.source.output.ready }}" },
155
- },
156
- after: {
157
- transform: {
158
- mapping: {
159
- a: "${{ steps.fast_fail.output }}",
160
- b: "${{ steps.slow_success.output }}",
161
- },
162
- },
163
- },
164
- },
165
- });
166
-
167
- const startTime = Date.now();
168
- const result = await executeWorkflow(spec, {}, mockOptions());
169
- const elapsed = Date.now() - startTime;
170
-
171
- assert.strictEqual(result.status, "failed");
172
- // Should not have waited 10s for slow_success
173
- assert.ok(elapsed < 5000, `Should have cancelled quickly, took ${elapsed}ms`);
174
- });
175
- });
@@ -1,192 +0,0 @@
1
- /**
2
- * Integration tests for checkpoint/resume — proves the full cycle
3
- * using mock dispatch (no real subprocesses).
4
- */
5
- import { describe, it } from "node:test";
6
- import assert from "node:assert";
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import os from "node:os";
10
- import { executeWorkflow, requestPause } from "./workflow-executor.ts";
11
- import { readState } from "./state.ts";
12
- import { mockCtx, mockPi, makeSpec } from "./test-helpers.ts";
13
- import type { StepResult, StepUsage } from "./types.ts";
14
-
15
- const zeroUsage: StepUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
16
-
17
- function mockResult(stepName: string, text: string): StepResult {
18
- return {
19
- step: stepName,
20
- agent: "mock",
21
- status: "completed" as const,
22
- textOutput: text,
23
- usage: { ...zeroUsage },
24
- durationMs: 100,
25
- };
26
- }
27
-
28
- describe("resume: crash recovery", () => {
29
- it("skips completed steps when resuming after failure", async (t) => {
30
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-resume-"));
31
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
32
-
33
- const dispatched: string[] = [];
34
-
35
- const spec = makeSpec({
36
- steps: {
37
- step1: { agent: "a" },
38
- step2: { agent: "b" },
39
- step3: { agent: "c" },
40
- },
41
- });
42
-
43
- // First run: step1 succeeds, step2 fails
44
- const failDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
45
- dispatched.push(opts.stepName);
46
- if (opts.stepName === "step2") {
47
- return {
48
- ...mockResult(opts.stepName, ""),
49
- status: "failed" as const,
50
- error: "Simulated failure",
51
- };
52
- }
53
- return mockResult(opts.stepName, `output-${opts.stepName}`);
54
- };
55
-
56
- const result1 = await executeWorkflow(spec, {}, {
57
- ctx: mockCtx(tmpDir),
58
- pi: mockPi(),
59
- loadAgent: () => ({ name: "mock" }),
60
- dispatchFn: failDispatch,
61
- });
62
-
63
- assert.strictEqual(result1.status, "failed");
64
- assert.ok(dispatched.includes("step1"));
65
- assert.ok(dispatched.includes("step2"));
66
-
67
- // Verify state on disk
68
- const savedState = readState(result1.runDir)!;
69
- assert.ok(savedState, "state should be persisted");
70
- assert.strictEqual(savedState.steps.step1.status, "completed");
71
- assert.strictEqual(savedState.steps.step2.status, "failed");
72
-
73
- // Second run: resume — step1 NOT dispatched, step2 re-runs successfully, step3 runs
74
- dispatched.length = 0;
75
- const successDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
76
- dispatched.push(opts.stepName);
77
- return mockResult(opts.stepName, `output-${opts.stepName}`);
78
- };
79
-
80
- const result2 = await executeWorkflow(spec, {}, {
81
- ctx: mockCtx(tmpDir),
82
- pi: mockPi(),
83
- loadAgent: () => ({ name: "mock" }),
84
- dispatchFn: successDispatch,
85
- resume: {
86
- runId: result1.runId,
87
- runDir: result1.runDir,
88
- state: savedState,
89
- },
90
- });
91
-
92
- assert.strictEqual(result2.status, "completed");
93
- assert.ok(!dispatched.includes("step1"), "step1 should be skipped on resume");
94
- assert.ok(dispatched.includes("step2"), "step2 should re-run");
95
- assert.ok(dispatched.includes("step3"), "step3 should run");
96
- });
97
- });
98
-
99
- describe("resume: pause step", () => {
100
- it("pauses at a pause step and resumes from the next step", async (t) => {
101
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-resume-"));
102
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
103
-
104
- const dispatched: string[] = [];
105
- const mockDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
106
- dispatched.push(opts.stepName);
107
- return mockResult(opts.stepName, `output-${opts.stepName}`);
108
- };
109
-
110
- const spec = makeSpec({
111
- steps: {
112
- step1: { agent: "a" },
113
- checkpoint: { pause: "Review step1 results" },
114
- step2: { agent: "b" },
115
- },
116
- });
117
-
118
- // Run 1: should pause after step1 + checkpoint
119
- const result1 = await executeWorkflow(spec, {}, {
120
- ctx: mockCtx(tmpDir),
121
- pi: mockPi(),
122
- loadAgent: () => ({ name: "mock" }),
123
- dispatchFn: mockDispatch,
124
- });
125
-
126
- assert.strictEqual(result1.status, "paused");
127
- assert.ok(dispatched.includes("step1"));
128
- assert.ok(!dispatched.includes("step2"), "step2 should not run before resume");
129
-
130
- // Verify state on disk
131
- const savedState = readState(result1.runDir)!;
132
- assert.ok(savedState, "state should be persisted");
133
- assert.strictEqual(savedState.status, "paused");
134
- assert.strictEqual(savedState.steps.step1.status, "completed");
135
- assert.strictEqual(savedState.steps.checkpoint.status, "completed");
136
-
137
- // Run 2: resume — step1 and checkpoint skipped, step2 runs
138
- dispatched.length = 0;
139
- const result2 = await executeWorkflow(spec, {}, {
140
- ctx: mockCtx(tmpDir),
141
- pi: mockPi(),
142
- loadAgent: () => ({ name: "mock" }),
143
- dispatchFn: mockDispatch,
144
- resume: {
145
- runId: result1.runId,
146
- runDir: result1.runDir,
147
- state: savedState,
148
- },
149
- });
150
-
151
- assert.strictEqual(result2.status, "completed");
152
- assert.ok(!dispatched.includes("step1"), "step1 should be skipped");
153
- assert.ok(!dispatched.includes("checkpoint"), "checkpoint should be skipped");
154
- assert.ok(dispatched.includes("step2"), "step2 should run");
155
- });
156
- });
157
-
158
- describe("resume: keybinding-initiated pause", () => {
159
- it("pauses between steps when requestPause is called", async (t) => {
160
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-resume-"));
161
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
162
-
163
- const dispatched: string[] = [];
164
- const mockDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
165
- dispatched.push(opts.stepName);
166
- // After step1 dispatch, request pause
167
- if (opts.stepName === "step1") {
168
- requestPause();
169
- }
170
- return mockResult(opts.stepName, `output-${opts.stepName}`);
171
- };
172
-
173
- const spec = makeSpec({
174
- steps: {
175
- step1: { agent: "a" },
176
- step2: { agent: "b" },
177
- step3: { agent: "c" },
178
- },
179
- });
180
-
181
- const result = await executeWorkflow(spec, {}, {
182
- ctx: mockCtx(tmpDir),
183
- pi: mockPi(),
184
- loadAgent: () => ({ name: "mock" }),
185
- dispatchFn: mockDispatch,
186
- });
187
-
188
- assert.strictEqual(result.status, "paused");
189
- assert.ok(dispatched.includes("step1"));
190
- assert.ok(!dispatched.includes("step2"), "step2 should not run — paused after step1");
191
- });
192
- });