@davidorex/pi-workflows 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +172 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +156 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +57 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +125 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +726 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +438 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +464 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +84 -0
- package/skills/pi-workflows/SKILL.md +247 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +74 -0
- package/workflows/create-handoff.workflow.yaml +58 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +55 -0
- package/workflows/plan-from-requirements.workflow.yaml +51 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- package/src/workflow-spec.ts +0 -471
package/src/step-command.test.ts
DELETED
|
@@ -1,330 +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 { executeWorkflow } from "./workflow-executor.ts";
|
|
7
|
-
import type { WorkflowSpec } from "./types.ts";
|
|
8
|
-
import { mockCtx, mockPi, makeSpec } from "./test-helpers.ts";
|
|
9
|
-
|
|
10
|
-
describe("command steps", () => {
|
|
11
|
-
it("captures stdout as text output on exit 0", async () => {
|
|
12
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
13
|
-
const spec: WorkflowSpec = {
|
|
14
|
-
name: "test-command",
|
|
15
|
-
description: "test command step",
|
|
16
|
-
steps: {
|
|
17
|
-
run: {
|
|
18
|
-
command: "echo hello world",
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
source: "project",
|
|
22
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const result = await executeWorkflow(spec, {}, {
|
|
26
|
-
ctx: mockCtx(tmpDir),
|
|
27
|
-
pi: mockPi(),
|
|
28
|
-
loadAgent: () => ({ name: "default" }),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
assert.strictEqual(result.status, "completed");
|
|
32
|
-
assert.strictEqual(result.steps.run.status, "completed");
|
|
33
|
-
assert.strictEqual(result.steps.run.agent, "command");
|
|
34
|
-
const output = result.steps.run.output as { text: string };
|
|
35
|
-
assert.strictEqual(output.text, "hello world");
|
|
36
|
-
assert.strictEqual(result.steps.run.textOutput, "hello world");
|
|
37
|
-
|
|
38
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("parses stdout as JSON when output.format is json", async () => {
|
|
42
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
43
|
-
const spec: WorkflowSpec = {
|
|
44
|
-
name: "test-command-json",
|
|
45
|
-
description: "test command JSON output",
|
|
46
|
-
steps: {
|
|
47
|
-
run: {
|
|
48
|
-
command: 'echo \'{"key": "value", "count": 42}\'',
|
|
49
|
-
output: { format: "json" },
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
source: "project",
|
|
53
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const result = await executeWorkflow(spec, {}, {
|
|
57
|
-
ctx: mockCtx(tmpDir),
|
|
58
|
-
pi: mockPi(),
|
|
59
|
-
loadAgent: () => ({ name: "default" }),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
assert.strictEqual(result.status, "completed");
|
|
63
|
-
assert.strictEqual(result.steps.run.status, "completed");
|
|
64
|
-
const output = result.steps.run.output as { key: string; count: number };
|
|
65
|
-
assert.strictEqual(output.key, "value");
|
|
66
|
-
assert.strictEqual(output.count, 42);
|
|
67
|
-
|
|
68
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("fails on non-zero exit code", async () => {
|
|
72
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
73
|
-
const spec: WorkflowSpec = {
|
|
74
|
-
name: "test-command-fail",
|
|
75
|
-
description: "test command failure",
|
|
76
|
-
steps: {
|
|
77
|
-
run: {
|
|
78
|
-
command: "exit 1",
|
|
79
|
-
},
|
|
80
|
-
after: {
|
|
81
|
-
command: "echo should not run",
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
source: "project",
|
|
85
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const result = await executeWorkflow(spec, {}, {
|
|
89
|
-
ctx: mockCtx(tmpDir),
|
|
90
|
-
pi: mockPi(),
|
|
91
|
-
loadAgent: () => ({ name: "default" }),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
assert.strictEqual(result.status, "failed");
|
|
95
|
-
assert.strictEqual(result.steps.run.status, "failed");
|
|
96
|
-
assert.ok(result.steps.run.error?.includes("Command failed"));
|
|
97
|
-
assert.ok(result.steps.run.error?.includes("exit 1"));
|
|
98
|
-
assert.ok(!result.steps.after);
|
|
99
|
-
|
|
100
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("captures stderr on failure", async () => {
|
|
104
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
105
|
-
const spec: WorkflowSpec = {
|
|
106
|
-
name: "test-command-stderr",
|
|
107
|
-
description: "test command stderr",
|
|
108
|
-
steps: {
|
|
109
|
-
run: {
|
|
110
|
-
command: "echo error-msg >&2; exit 1",
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
source: "project",
|
|
114
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const result = await executeWorkflow(spec, {}, {
|
|
118
|
-
ctx: mockCtx(tmpDir),
|
|
119
|
-
pi: mockPi(),
|
|
120
|
-
loadAgent: () => ({ name: "default" }),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
assert.strictEqual(result.status, "failed");
|
|
124
|
-
assert.ok(result.steps.run.error?.includes("error-msg"));
|
|
125
|
-
|
|
126
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("resolves ${{ }} expressions in command string", async () => {
|
|
130
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
131
|
-
const spec: WorkflowSpec = {
|
|
132
|
-
name: "test-command-expr",
|
|
133
|
-
description: "test command expressions",
|
|
134
|
-
input: {
|
|
135
|
-
type: "object",
|
|
136
|
-
properties: { greeting: { type: "string" } },
|
|
137
|
-
},
|
|
138
|
-
steps: {
|
|
139
|
-
run: {
|
|
140
|
-
command: "echo ${{ input.greeting }}",
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
source: "project",
|
|
144
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const result = await executeWorkflow(spec, { greeting: "howdy" }, {
|
|
148
|
-
ctx: mockCtx(tmpDir),
|
|
149
|
-
pi: mockPi(),
|
|
150
|
-
loadAgent: () => ({ name: "default" }),
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
assert.strictEqual(result.status, "completed");
|
|
154
|
-
const output = result.steps.run.output as { text: string };
|
|
155
|
-
assert.strictEqual(output.text, "howdy");
|
|
156
|
-
|
|
157
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("persists output to disk", async () => {
|
|
161
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
162
|
-
const spec: WorkflowSpec = {
|
|
163
|
-
name: "test-command-persist",
|
|
164
|
-
description: "test command output persistence",
|
|
165
|
-
steps: {
|
|
166
|
-
run: {
|
|
167
|
-
command: "echo persisted",
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
source: "project",
|
|
171
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const result = await executeWorkflow(spec, {}, {
|
|
175
|
-
ctx: mockCtx(tmpDir),
|
|
176
|
-
pi: mockPi(),
|
|
177
|
-
loadAgent: () => ({ name: "default" }),
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
assert.strictEqual(result.status, "completed");
|
|
181
|
-
assert.ok(result.steps.run.outputPath);
|
|
182
|
-
const persisted = JSON.parse(fs.readFileSync(result.steps.run.outputPath!, "utf-8"));
|
|
183
|
-
assert.strictEqual(persisted.text, "persisted");
|
|
184
|
-
|
|
185
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("handles empty stdout", async () => {
|
|
189
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
190
|
-
const spec: WorkflowSpec = {
|
|
191
|
-
name: "test-command-empty",
|
|
192
|
-
description: "test command empty stdout",
|
|
193
|
-
steps: {
|
|
194
|
-
run: {
|
|
195
|
-
command: "true", // exit 0, no output
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
source: "project",
|
|
199
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const result = await executeWorkflow(spec, {}, {
|
|
203
|
-
ctx: mockCtx(tmpDir),
|
|
204
|
-
pi: mockPi(),
|
|
205
|
-
loadAgent: () => ({ name: "default" }),
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
assert.strictEqual(result.status, "completed");
|
|
209
|
-
assert.strictEqual(result.steps.run.status, "completed");
|
|
210
|
-
const output = result.steps.run.output as { text: string };
|
|
211
|
-
assert.strictEqual(output.text, "");
|
|
212
|
-
|
|
213
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("costs nothing (usage.cost === 0)", async () => {
|
|
217
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
218
|
-
const spec: WorkflowSpec = {
|
|
219
|
-
name: "test-command-cost",
|
|
220
|
-
description: "test command zero cost",
|
|
221
|
-
steps: {
|
|
222
|
-
run: {
|
|
223
|
-
command: "echo free",
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
source: "project",
|
|
227
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const result = await executeWorkflow(spec, {}, {
|
|
231
|
-
ctx: mockCtx(tmpDir),
|
|
232
|
-
pi: mockPi(),
|
|
233
|
-
loadAgent: () => ({ name: "default" }),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
assert.strictEqual(result.steps.run.usage.cost, 0);
|
|
237
|
-
assert.strictEqual(result.steps.run.usage.turns, 0);
|
|
238
|
-
assert.strictEqual(result.steps.run.usage.input, 0);
|
|
239
|
-
|
|
240
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("supports timeout", async () => {
|
|
244
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
245
|
-
const spec: WorkflowSpec = {
|
|
246
|
-
name: "test-command-timeout",
|
|
247
|
-
description: "test command timeout",
|
|
248
|
-
steps: {
|
|
249
|
-
run: {
|
|
250
|
-
command: "sleep 10",
|
|
251
|
-
timeout: { seconds: 1 },
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
source: "project",
|
|
255
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const result = await executeWorkflow(spec, {}, {
|
|
259
|
-
ctx: mockCtx(tmpDir),
|
|
260
|
-
pi: mockPi(),
|
|
261
|
-
loadAgent: () => ({ name: "default" }),
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
assert.strictEqual(result.status, "failed");
|
|
265
|
-
assert.strictEqual(result.steps.run.status, "failed");
|
|
266
|
-
assert.ok(result.steps.run.error?.includes("Command failed"));
|
|
267
|
-
|
|
268
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
269
|
-
}, { timeout: 15000 });
|
|
270
|
-
|
|
271
|
-
it("supports signal cancellation", async () => {
|
|
272
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
273
|
-
const spec: WorkflowSpec = {
|
|
274
|
-
name: "test-command-signal",
|
|
275
|
-
description: "test command signal",
|
|
276
|
-
steps: {
|
|
277
|
-
run: {
|
|
278
|
-
command: "sleep 10",
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
source: "project",
|
|
282
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const controller = new AbortController();
|
|
286
|
-
// Abort shortly after starting
|
|
287
|
-
setTimeout(() => controller.abort(), 200);
|
|
288
|
-
|
|
289
|
-
const result = await executeWorkflow(spec, {}, {
|
|
290
|
-
ctx: mockCtx(tmpDir),
|
|
291
|
-
pi: mockPi(),
|
|
292
|
-
signal: controller.signal,
|
|
293
|
-
loadAgent: () => ({ name: "default" }),
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
assert.strictEqual(result.status, "failed");
|
|
297
|
-
|
|
298
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
299
|
-
}, { timeout: 15000 });
|
|
300
|
-
|
|
301
|
-
it("persists output to custom path", async () => {
|
|
302
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-cmd-"));
|
|
303
|
-
const outputFile = path.join(tmpDir, "custom-output.json");
|
|
304
|
-
const spec: WorkflowSpec = {
|
|
305
|
-
name: "test-command-path",
|
|
306
|
-
description: "test command custom output path",
|
|
307
|
-
steps: {
|
|
308
|
-
run: {
|
|
309
|
-
command: "echo custom",
|
|
310
|
-
output: { path: outputFile },
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
source: "project",
|
|
314
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const result = await executeWorkflow(spec, {}, {
|
|
318
|
-
ctx: mockCtx(tmpDir),
|
|
319
|
-
pi: mockPi(),
|
|
320
|
-
loadAgent: () => ({ name: "default" }),
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
assert.strictEqual(result.status, "completed");
|
|
324
|
-
assert.ok(fs.existsSync(outputFile));
|
|
325
|
-
const persisted = JSON.parse(fs.readFileSync(outputFile, "utf-8"));
|
|
326
|
-
assert.strictEqual(persisted.text, "custom");
|
|
327
|
-
|
|
328
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
329
|
-
});
|
|
330
|
-
});
|
package/src/step-command.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command step executor — runs a shell command and captures output as data.
|
|
3
|
-
*
|
|
4
|
-
* Unlike gate (which judges pass/fail), command captures stdout as structured
|
|
5
|
-
* or text output for downstream steps. Non-zero exit codes produce a failed result.
|
|
6
|
-
*/
|
|
7
|
-
import type { StepResult } from "./types.ts";
|
|
8
|
-
import { zeroUsage, SIGKILL_GRACE_MS } from "./step-shared.ts";
|
|
9
|
-
import { persistStepOutput } from "./output.ts";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Execute a command step: runs a shell command, captures stdout as output.
|
|
13
|
-
*
|
|
14
|
-
* The command string is expected to already have ${{ }} expressions resolved
|
|
15
|
-
* before being passed here.
|
|
16
|
-
*/
|
|
17
|
-
export async function executeCommand(
|
|
18
|
-
command: string,
|
|
19
|
-
stepName: string,
|
|
20
|
-
options: { cwd: string; signal?: AbortSignal; timeoutMs?: number; runDir?: string; outputPath?: string },
|
|
21
|
-
outputFormat?: "json" | "text",
|
|
22
|
-
): Promise<StepResult> {
|
|
23
|
-
const startTime = Date.now();
|
|
24
|
-
try {
|
|
25
|
-
const { spawn } = await import("node:child_process");
|
|
26
|
-
const { stdout, stderr } = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
|
27
|
-
const proc = spawn("sh", ["-c", command], {
|
|
28
|
-
cwd: options.cwd,
|
|
29
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
30
|
-
detached: true,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
let stdout = "";
|
|
34
|
-
let stderr = "";
|
|
35
|
-
let settled = false;
|
|
36
|
-
proc.stdout.on("data", (d: Buffer) => { stdout += d.toString(); });
|
|
37
|
-
proc.stderr.on("data", (d: Buffer) => { stderr += d.toString(); });
|
|
38
|
-
|
|
39
|
-
const killProc = () => {
|
|
40
|
-
if (!proc.pid) return;
|
|
41
|
-
try { process.kill(-proc.pid, "SIGTERM"); } catch { /* already dead */ }
|
|
42
|
-
setTimeout(() => {
|
|
43
|
-
if (!proc.pid) return;
|
|
44
|
-
try { process.kill(-proc.pid, "SIGKILL"); } catch { /* already dead */ }
|
|
45
|
-
}, SIGKILL_GRACE_MS);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const onAbort = () => { killProc(); };
|
|
49
|
-
if (options.signal) {
|
|
50
|
-
if (options.signal.aborted) { killProc(); }
|
|
51
|
-
else { options.signal.addEventListener("abort", onAbort, { once: true }); }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
55
|
-
if (options.timeoutMs) {
|
|
56
|
-
timeoutId = setTimeout(() => { killProc(); }, options.timeoutMs);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
proc.on("close", (code) => {
|
|
60
|
-
if (settled) return;
|
|
61
|
-
settled = true;
|
|
62
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
63
|
-
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
64
|
-
if (code === 0) {
|
|
65
|
-
resolve({ stdout, stderr });
|
|
66
|
-
} else {
|
|
67
|
-
const err: any = new Error(`Process exited with code ${code}`);
|
|
68
|
-
err.status = code;
|
|
69
|
-
err.stdout = stdout;
|
|
70
|
-
err.stderr = stderr;
|
|
71
|
-
reject(err);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
proc.on("error", (err) => {
|
|
76
|
-
if (settled) return;
|
|
77
|
-
settled = true;
|
|
78
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
79
|
-
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
80
|
-
reject(err);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Exit 0: parse output
|
|
85
|
-
let output: unknown;
|
|
86
|
-
let textOutput: string | undefined;
|
|
87
|
-
|
|
88
|
-
if (outputFormat === "json") {
|
|
89
|
-
try {
|
|
90
|
-
output = JSON.parse(stdout);
|
|
91
|
-
} catch {
|
|
92
|
-
output = { text: stdout.trim() };
|
|
93
|
-
}
|
|
94
|
-
textOutput = stdout.trim();
|
|
95
|
-
} else {
|
|
96
|
-
textOutput = stdout.trim();
|
|
97
|
-
output = { text: textOutput };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const result: StepResult = {
|
|
101
|
-
step: stepName,
|
|
102
|
-
agent: "command",
|
|
103
|
-
status: "completed",
|
|
104
|
-
output,
|
|
105
|
-
textOutput,
|
|
106
|
-
usage: zeroUsage(),
|
|
107
|
-
durationMs: Date.now() - startTime,
|
|
108
|
-
};
|
|
109
|
-
if (options.runDir) {
|
|
110
|
-
result.outputPath = persistStepOutput(options.runDir, stepName, output, textOutput, options.outputPath);
|
|
111
|
-
}
|
|
112
|
-
return result;
|
|
113
|
-
} catch (err: unknown) {
|
|
114
|
-
const execErr = err as { status?: number; stdout?: string; stderr?: string; message?: string };
|
|
115
|
-
const exitCode = execErr.status ?? 1;
|
|
116
|
-
const stderr = execErr.stderr?.trim() ?? "";
|
|
117
|
-
const stdout = execErr.stdout?.trim() ?? "";
|
|
118
|
-
|
|
119
|
-
const result: StepResult = {
|
|
120
|
-
step: stepName,
|
|
121
|
-
agent: "command",
|
|
122
|
-
status: "failed",
|
|
123
|
-
usage: zeroUsage(),
|
|
124
|
-
durationMs: Date.now() - startTime,
|
|
125
|
-
error: `Command failed (exit ${exitCode}): ${stderr || stdout || execErr.message || "unknown error"}`,
|
|
126
|
-
};
|
|
127
|
-
if (options.runDir) {
|
|
128
|
-
persistStepOutput(options.runDir, stepName, { error: result.error, exitCode }, undefined, options.outputPath);
|
|
129
|
-
}
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
|
-
}
|