@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.
- 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 +178 -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 +724 -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 +171 -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 +63 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +136 -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 +165 -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 +738 -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 +457 -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 +476 -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 +107 -0
- package/skills/pi-workflows/SKILL.md +185 -0
- package/skills/pi-workflows/references/bundled-resources.md +87 -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 +94 -0
- package/workflows/create-handoff.workflow.yaml +66 -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 +81 -0
- package/workflows/plan-from-requirements.workflow.yaml +59 -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
|
@@ -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
|
-
});
|