@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,148 +0,0 @@
1
- /**
2
- * ForEach step executor — iterates over an array, executing the step body
3
- * once per element with the element bound to a named variable in scope.
4
- */
5
- import type { StepSpec, StepResult, ExecutionState, ExpressionScope } from "./types.ts";
6
- import { resolveExpression, resolveExpressions } from "./expression.ts";
7
- import { zeroUsage, addUsage } from "./step-shared.ts";
8
- import { persistStepOutput } from "./output.ts";
9
-
10
- /** Options for forEach execution, matching the StepExecOptions pattern. */
11
- interface ForEachOptions {
12
- ctx: any;
13
- pi: any;
14
- signal?: AbortSignal;
15
- loadAgent: (name: string) => any;
16
- runDir: string;
17
- spec: any;
18
- widgetState: any;
19
- templateEnv?: any;
20
- dispatchFn?: any;
21
- }
22
-
23
- /**
24
- * Execute a forEach step: iterates over an array and executes the step body
25
- * for each element.
26
- *
27
- * @param stepName - the name of the forEach step
28
- * @param stepSpec - the full step spec (including forEach, as, and the body type)
29
- * @param state - current execution state
30
- * @param forEachExpr - the forEach expression string (without ${{ }} delimiters)
31
- * @param asName - the variable name to bind each element (default: "item")
32
- * @param executeSingleStep - the step executor function to delegate to
33
- * @param options - step execution options
34
- * @returns combined StepResult with array output
35
- */
36
- export async function executeForEach(
37
- stepName: string,
38
- stepSpec: StepSpec,
39
- state: ExecutionState,
40
- forEachExpr: string,
41
- asName: string,
42
- executeSingleStep: (name: string, spec: StepSpec, state: ExecutionState, opts: ForEachOptions) => Promise<boolean>,
43
- options: ForEachOptions,
44
- ): Promise<StepResult> {
45
- const startTime = Date.now();
46
- const scope: Record<string, unknown> = { input: state.input, steps: state.steps };
47
-
48
- // Resolve the forEach expression to get the array
49
- const rawExpr = forEachExpr.replace(/^\$\{\{\s*/, "").replace(/\s*\}\}$/, "");
50
- let array: unknown;
51
- try {
52
- array = resolveExpression(rawExpr, scope);
53
- } catch (err) {
54
- return {
55
- step: stepName,
56
- agent: "forEach",
57
- status: "failed",
58
- usage: zeroUsage(),
59
- durationMs: Date.now() - startTime,
60
- error: `forEach expression failed: ${err instanceof Error ? err.message : String(err)}`,
61
- };
62
- }
63
-
64
- if (!Array.isArray(array)) {
65
- return {
66
- step: stepName,
67
- agent: "forEach",
68
- status: "failed",
69
- usage: zeroUsage(),
70
- durationMs: Date.now() - startTime,
71
- error: `forEach expression must resolve to an array, got ${typeof array}`,
72
- };
73
- }
74
-
75
- // Empty array → completed with empty output
76
- if (array.length === 0) {
77
- return {
78
- step: stepName,
79
- agent: "forEach",
80
- status: "completed",
81
- output: [],
82
- textOutput: "[]",
83
- usage: zeroUsage(),
84
- durationMs: Date.now() - startTime,
85
- };
86
- }
87
-
88
- // Create the step body (strip forEach/as fields)
89
- const bodySpec: StepSpec = { ...stepSpec };
90
- delete bodySpec.forEach;
91
- delete bodySpec.as;
92
-
93
- const outputs: unknown[] = [];
94
- const totalUsage = zeroUsage();
95
- let failed = false;
96
-
97
- for (let i = 0; i < array.length; i++) {
98
- const element = array[i];
99
- const iterStepName = `${stepName}[${i}]`;
100
-
101
- // Create a scoped copy of state for this iteration
102
- // The as binding and forEach metadata are added to a proxy state
103
- // that exposes them in the expression scope
104
- const iterState: ExecutionState = {
105
- ...state,
106
- steps: { ...state.steps },
107
- // Store forEach bindings that the executor can pick up
108
- [asName]: element,
109
- forEach: { index: i, length: array.length },
110
- } as ExecutionState & Record<string, unknown>;
111
-
112
- const cont = await executeSingleStep(iterStepName, bodySpec, iterState, options);
113
-
114
- // Copy iteration results back to main state
115
- const iterResult = iterState.steps[iterStepName];
116
- if (iterResult) {
117
- state.steps[iterStepName] = iterResult;
118
- outputs.push(iterResult.output);
119
- addUsage(totalUsage, iterResult.usage);
120
- }
121
-
122
- if (!cont || (iterResult && iterResult.status === "failed")) {
123
- failed = true;
124
- break;
125
- }
126
- }
127
-
128
- const result: StepResult = {
129
- step: stepName,
130
- agent: "forEach",
131
- status: failed ? "failed" : "completed",
132
- output: outputs,
133
- textOutput: JSON.stringify(outputs, null, 2),
134
- usage: totalUsage,
135
- durationMs: Date.now() - startTime,
136
- error: failed ? `forEach iteration failed` : undefined,
137
- };
138
-
139
- // Persist the collected output
140
- if (options.runDir) {
141
- const outputPath = stepSpec.output?.path
142
- ? String(resolveExpressions(stepSpec.output.path, scope))
143
- : undefined;
144
- result.outputPath = persistStepOutput(options.runDir, stepName, outputs, undefined, outputPath);
145
- }
146
-
147
- return result;
148
- }
@@ -1,185 +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 { executeGate } from "./step-gate.ts";
7
- import { zeroUsage } from "./step-shared.ts";
8
-
9
- describe("executeGate", () => {
10
- // Happy path — command passes (exit 0)
11
- it("returns completed with passed:true on exit 0", async (t) => {
12
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
13
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
14
-
15
- const result = await executeGate({ check: "echo pass" }, "gate1", { cwd: tmpDir });
16
- assert.strictEqual(result.status, "completed");
17
- assert.strictEqual((result.output as any).passed, true);
18
- assert.strictEqual((result.output as any).exitCode, 0);
19
- }, { timeout: 10000 });
20
-
21
- it("captures stdout in output.output and textOutput", async (t) => {
22
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
23
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
24
-
25
- const result = await executeGate({ check: "echo hello" }, "gate1", { cwd: tmpDir });
26
- assert.strictEqual((result.output as any).output, "hello");
27
- assert.strictEqual(result.textOutput, "hello");
28
- }, { timeout: 10000 });
29
-
30
- it("sets agent to 'gate'", async (t) => {
31
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
32
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
33
-
34
- const result = await executeGate({ check: "true" }, "gate1", { cwd: tmpDir });
35
- assert.strictEqual(result.agent, "gate");
36
- }, { timeout: 10000 });
37
-
38
- it("has zero usage", async (t) => {
39
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
40
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
41
-
42
- const result = await executeGate({ check: "true" }, "gate1", { cwd: tmpDir });
43
- assert.deepStrictEqual(result.usage, zeroUsage());
44
- }, { timeout: 10000 });
45
-
46
- it("records positive durationMs", async (t) => {
47
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
48
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
49
-
50
- const result = await executeGate({ check: "echo test" }, "gate1", { cwd: tmpDir });
51
- assert.ok(result.durationMs >= 0);
52
- }, { timeout: 10000 });
53
-
54
- // Command fails (non-zero exit)
55
- it("returns completed with passed:false on non-zero exit", async (t) => {
56
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
57
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
58
-
59
- const result = await executeGate({ check: "exit 1" }, "gate1", { cwd: tmpDir });
60
- assert.strictEqual(result.status, "completed");
61
- assert.strictEqual((result.output as any).passed, false);
62
- assert.strictEqual((result.output as any).exitCode, 1);
63
- }, { timeout: 10000 });
64
-
65
- it("captures stderr on failure", async (t) => {
66
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
67
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
68
-
69
- const result = await executeGate({ check: "echo err >&2; exit 1" }, "gate1", { cwd: tmpDir });
70
- assert.ok((result.output as any).output.includes("err"));
71
- }, { timeout: 10000 });
72
-
73
- it("captures stdout when stderr is empty on failure", async (t) => {
74
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
75
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
76
-
77
- const result = await executeGate({ check: "echo stdout-msg; exit 1" }, "gate1", { cwd: tmpDir });
78
- assert.ok((result.output as any).output.includes("stdout-msg"));
79
- }, { timeout: 10000 });
80
-
81
- // Stdout trimming
82
- it("trims whitespace from stdout", async (t) => {
83
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
84
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
85
-
86
- const result = await executeGate({ check: "echo ' trimmed '" }, "gate1", { cwd: tmpDir });
87
- assert.strictEqual((result.output as any).output, "trimmed");
88
- assert.strictEqual(result.textOutput, "trimmed");
89
- }, { timeout: 10000 });
90
-
91
- // Output persistence
92
- it("persists output when runDir is provided", async (t) => {
93
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
94
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
95
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
96
-
97
- const result = await executeGate({ check: "echo persist" }, "gate1", { cwd: tmpDir, runDir: tmpDir });
98
- assert.ok(result.outputPath);
99
- assert.ok(fs.existsSync(result.outputPath!));
100
- }, { timeout: 10000 });
101
-
102
- it("persists to custom outputPath when provided", async (t) => {
103
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
104
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
105
-
106
- const customPath = path.join(tmpDir, "custom-gate.json");
107
- const result = await executeGate({ check: "echo custom" }, "gate1", {
108
- cwd: tmpDir,
109
- runDir: tmpDir,
110
- outputPath: customPath,
111
- });
112
- assert.strictEqual(result.outputPath, customPath);
113
- assert.ok(fs.existsSync(customPath));
114
- }, { timeout: 10000 });
115
-
116
- it("does not persist when runDir is omitted", async (t) => {
117
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
118
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
119
-
120
- const result = await executeGate({ check: "echo nopersist" }, "gate1", { cwd: tmpDir });
121
- assert.strictEqual(result.outputPath, undefined);
122
- }, { timeout: 10000 });
123
-
124
- // Cancellation
125
- it("terminates on abort signal", async (t) => {
126
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
127
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
128
-
129
- const controller = new AbortController();
130
- controller.abort();
131
- const result = await executeGate({ check: "sleep 30" }, "gate1", {
132
- cwd: tmpDir,
133
- signal: controller.signal,
134
- });
135
- // Should resolve (not hang) — the process was killed
136
- assert.ok(result);
137
- }, { timeout: 10000 });
138
-
139
- it("terminates on late abort signal", async (t) => {
140
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
141
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
142
-
143
- const controller = new AbortController();
144
- setTimeout(() => controller.abort(), 200);
145
- const result = await executeGate({ check: "sleep 30" }, "gate1", {
146
- cwd: tmpDir,
147
- signal: controller.signal,
148
- });
149
- assert.ok(result);
150
- // The gate should have failed since the process was killed
151
- assert.strictEqual((result.output as any).passed, false);
152
- }, { timeout: 10000 });
153
-
154
- // Timeout
155
- it("terminates on timeout", async (t) => {
156
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
157
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
158
-
159
- const result = await executeGate({ check: "sleep 30" }, "gate1", {
160
- cwd: tmpDir,
161
- timeoutMs: 500,
162
- });
163
- assert.ok(result);
164
- assert.strictEqual((result.output as any).passed, false);
165
- }, { timeout: 10000 });
166
-
167
- // Edge cases
168
- it("handles command that produces no output", async (t) => {
169
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
170
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
171
-
172
- const result = await executeGate({ check: "true" }, "gate1", { cwd: tmpDir });
173
- assert.strictEqual((result.output as any).output, "");
174
- assert.strictEqual(result.textOutput, "");
175
- }, { timeout: 10000 });
176
-
177
- it("handles command with multi-line stdout", async (t) => {
178
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-"));
179
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
180
-
181
- const result = await executeGate({ check: "echo line1; echo line2" }, "gate1", { cwd: tmpDir });
182
- assert.ok((result.output as any).output.includes("line1"));
183
- assert.ok((result.output as any).output.includes("line2"));
184
- }, { timeout: 10000 });
185
- });
package/src/step-gate.ts DELETED
@@ -1,116 +0,0 @@
1
- /**
2
- * Gate step executor — runs a shell command and passes/fails based on exit code.
3
- */
4
- import type { GateSpec, StepResult } from "./types.ts";
5
- import { zeroUsage, SIGKILL_GRACE_MS } from "./step-shared.ts";
6
- import { persistStepOutput } from "./output.ts";
7
-
8
- /**
9
- * Execute a gate step: runs a shell command, passes/fails based on exit code.
10
- *
11
- * The gate's check command is expected to already have ${{ }} expressions resolved
12
- * before being passed here.
13
- */
14
- export async function executeGate(
15
- gate: GateSpec,
16
- stepName: string,
17
- options: { cwd: string; signal?: AbortSignal; timeoutMs?: number; runDir?: string; outputPath?: string },
18
- ): Promise<StepResult> {
19
- const startTime = Date.now();
20
- try {
21
- const { spawn } = await import("node:child_process");
22
- const output = await new Promise<string>((resolve, reject) => {
23
- const proc = spawn("sh", ["-c", gate.check], {
24
- cwd: options.cwd,
25
- stdio: ["ignore", "pipe", "pipe"],
26
- detached: true,
27
- });
28
-
29
- let stdout = "";
30
- let stderr = "";
31
- let settled = false;
32
- proc.stdout.on("data", (d: Buffer) => { stdout += d.toString(); });
33
- proc.stderr.on("data", (d: Buffer) => { stderr += d.toString(); });
34
-
35
- // Kill the entire process group (sh + children like sleep)
36
- const killProc = () => {
37
- if (!proc.pid) return;
38
- try { process.kill(-proc.pid, "SIGTERM"); } catch { /* already dead */ }
39
- setTimeout(() => {
40
- if (!proc.pid) return;
41
- try { process.kill(-proc.pid, "SIGKILL"); } catch { /* already dead */ }
42
- }, SIGKILL_GRACE_MS);
43
- };
44
-
45
- // Handle abort signal (from parallel cancellation or timeout)
46
- const onAbort = () => { killProc(); };
47
- if (options.signal) {
48
- if (options.signal.aborted) { killProc(); }
49
- else { options.signal.addEventListener("abort", onAbort, { once: true }); }
50
- }
51
-
52
- // Handle step-level timeout
53
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
54
- if (options.timeoutMs) {
55
- timeoutId = setTimeout(() => { killProc(); }, options.timeoutMs);
56
- }
57
-
58
- proc.on("close", (code) => {
59
- if (settled) return;
60
- settled = true;
61
- if (timeoutId) clearTimeout(timeoutId);
62
- if (options.signal) options.signal.removeEventListener("abort", onAbort);
63
- if (code === 0) {
64
- resolve(stdout);
65
- } else {
66
- const err: any = new Error(`Process exited with code ${code}`);
67
- err.status = code;
68
- err.stdout = stdout;
69
- err.stderr = stderr;
70
- reject(err);
71
- }
72
- });
73
-
74
- proc.on("error", (err) => {
75
- if (settled) return;
76
- settled = true;
77
- if (timeoutId) clearTimeout(timeoutId);
78
- if (options.signal) options.signal.removeEventListener("abort", onAbort);
79
- reject(err);
80
- });
81
- });
82
- const gateOutput = { passed: true, exitCode: 0, output: output.trim() };
83
- const result: StepResult = {
84
- step: stepName,
85
- agent: "gate",
86
- status: "completed",
87
- textOutput: output.trim(),
88
- output: gateOutput,
89
- usage: zeroUsage(),
90
- durationMs: Date.now() - startTime,
91
- };
92
- if (options.runDir) {
93
- result.outputPath = persistStepOutput(options.runDir, stepName, gateOutput, undefined, options.outputPath);
94
- }
95
- return result;
96
- } catch (err: unknown) {
97
- const execErr = err as { status?: number; stdout?: string; stderr?: string };
98
- const exitCode = execErr.status ?? 1;
99
- const stderr = execErr.stderr?.trim() ?? "";
100
- const stdout = execErr.stdout?.trim() ?? "";
101
- const gateOutput = { passed: false, exitCode, output: stderr || stdout };
102
- const result: StepResult = {
103
- step: stepName,
104
- agent: "gate",
105
- status: "completed",
106
- textOutput: stderr || stdout,
107
- output: gateOutput,
108
- usage: zeroUsage(),
109
- durationMs: Date.now() - startTime,
110
- };
111
- if (options.runDir) {
112
- result.outputPath = persistStepOutput(options.runDir, stepName, gateOutput, undefined, options.outputPath);
113
- }
114
- return result;
115
- }
116
- }