@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/state.test.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import {
|
|
4
|
-
generateRunId, initRunDir, writeState, readState,
|
|
5
|
-
writeStepOutput, writeMetrics, buildResult, formatResult, aggregateUsage,
|
|
6
|
-
} from "./state.ts";
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import type { ExecutionState, WorkflowSpec, StepResult, StepUsage } from "./types.ts";
|
|
11
|
-
|
|
12
|
-
describe("generateRunId", () => {
|
|
13
|
-
it("includes workflow name", () => {
|
|
14
|
-
const id = generateRunId("bugfix");
|
|
15
|
-
assert.ok(id.startsWith("bugfix-"));
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("generates unique IDs", () => {
|
|
19
|
-
const a = generateRunId("test");
|
|
20
|
-
const b = generateRunId("test");
|
|
21
|
-
assert.notStrictEqual(a, b);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("initRunDir", () => {
|
|
26
|
-
it("creates directory structure under workflow name", () => {
|
|
27
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-state-"));
|
|
28
|
-
const runDir = initRunDir(tmpDir, "test-wf", "test-run-1");
|
|
29
|
-
|
|
30
|
-
assert.ok(fs.existsSync(runDir));
|
|
31
|
-
assert.ok(runDir.includes(path.join("test-wf", "runs", "test-run-1")));
|
|
32
|
-
assert.ok(fs.existsSync(path.join(runDir, "sessions")));
|
|
33
|
-
assert.ok(fs.existsSync(path.join(runDir, "outputs")));
|
|
34
|
-
|
|
35
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("writeState / readState", () => {
|
|
40
|
-
it("round-trips execution state", () => {
|
|
41
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-state-"));
|
|
42
|
-
const runDir = path.join(tmpDir, "run");
|
|
43
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
44
|
-
|
|
45
|
-
const state: ExecutionState = {
|
|
46
|
-
input: { description: "test" },
|
|
47
|
-
steps: {},
|
|
48
|
-
status: "running",
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
writeState(runDir, state);
|
|
52
|
-
const loaded = readState(runDir);
|
|
53
|
-
assert.deepStrictEqual(loaded, state);
|
|
54
|
-
|
|
55
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns null for nonexistent state", () => {
|
|
59
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-state-"));
|
|
60
|
-
assert.strictEqual(readState(tmpDir), null);
|
|
61
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("aggregateUsage", () => {
|
|
66
|
-
it("sums usage across steps", () => {
|
|
67
|
-
const usage = (n: number): StepUsage => ({
|
|
68
|
-
input: n * 100, output: n * 50, cacheRead: 0, cacheWrite: 0,
|
|
69
|
-
cost: n * 0.01, turns: n,
|
|
70
|
-
});
|
|
71
|
-
const steps: Record<string, StepResult> = {
|
|
72
|
-
a: { step: "a", agent: "x", status: "completed", usage: usage(1), durationMs: 1000 },
|
|
73
|
-
b: { step: "b", agent: "y", status: "completed", usage: usage(2), durationMs: 2000 },
|
|
74
|
-
};
|
|
75
|
-
const total = aggregateUsage(steps);
|
|
76
|
-
assert.strictEqual(total.input, 300);
|
|
77
|
-
assert.strictEqual(total.output, 150);
|
|
78
|
-
assert.strictEqual(total.turns, 3);
|
|
79
|
-
assert.ok(Math.abs(total.cost - 0.03) < 0.001);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("formatResult", () => {
|
|
84
|
-
it("includes workflow name and step summary", () => {
|
|
85
|
-
const result = buildResult(
|
|
86
|
-
{ name: "test", steps: { s: { agent: "a" } } } as WorkflowSpec,
|
|
87
|
-
"test-run-1",
|
|
88
|
-
"/tmp/run",
|
|
89
|
-
{
|
|
90
|
-
input: {},
|
|
91
|
-
steps: {
|
|
92
|
-
s: {
|
|
93
|
-
step: "s", agent: "a", status: "completed",
|
|
94
|
-
textOutput: "done",
|
|
95
|
-
usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 },
|
|
96
|
-
durationMs: 5000,
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
status: "completed",
|
|
100
|
-
},
|
|
101
|
-
"completed",
|
|
102
|
-
);
|
|
103
|
-
const text = formatResult(result);
|
|
104
|
-
assert.ok(text.includes("test"));
|
|
105
|
-
assert.ok(text.includes("completed"));
|
|
106
|
-
assert.ok(text.includes("$0.01") || text.includes("0.01"));
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("writeState error handling", () => {
|
|
111
|
-
it("throws with context when write fails", (t) => {
|
|
112
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-state-err-"));
|
|
113
|
-
t.after(() => {
|
|
114
|
-
try { fs.chmodSync(tmpDir, 0o755); } catch { /* ignore */ }
|
|
115
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Make directory read-only
|
|
119
|
-
fs.chmodSync(tmpDir, 0o444);
|
|
120
|
-
|
|
121
|
-
const state: ExecutionState = {
|
|
122
|
-
input: {},
|
|
123
|
-
steps: {},
|
|
124
|
-
status: "running",
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
assert.throws(
|
|
128
|
-
() => writeState(tmpDir, state),
|
|
129
|
-
(err: Error) => {
|
|
130
|
-
return err.message.includes("Failed to write workflow state")
|
|
131
|
-
&& err.message.includes("running");
|
|
132
|
-
},
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("cleans up tmp file on rename failure", (t) => {
|
|
137
|
-
// Verify that after a write failure, no .state.json.tmp remains.
|
|
138
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-state-err-"));
|
|
139
|
-
t.after(() => {
|
|
140
|
-
try { fs.chmodSync(tmpDir, 0o755); } catch { /* ignore */ }
|
|
141
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
fs.chmodSync(tmpDir, 0o444);
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
writeState(tmpDir, { input: {}, steps: {}, status: "running" });
|
|
148
|
-
} catch { /* expected */ }
|
|
149
|
-
|
|
150
|
-
// Verify no tmp file leaked (may not exist if writeFileSync itself failed)
|
|
151
|
-
const tmpPath = path.join(tmpDir, ".state.json.tmp");
|
|
152
|
-
// Restore permissions to check
|
|
153
|
-
try { fs.chmodSync(tmpDir, 0o755); } catch { /* ignore */ }
|
|
154
|
-
assert.strictEqual(fs.existsSync(tmpPath), false);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("error message includes the file path", (t) => {
|
|
158
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-state-err-"));
|
|
159
|
-
t.after(() => {
|
|
160
|
-
try { fs.chmodSync(tmpDir, 0o755); } catch { /* ignore */ }
|
|
161
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
fs.chmodSync(tmpDir, 0o444);
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
writeState(tmpDir, { input: {}, steps: {}, status: "completed" });
|
|
168
|
-
assert.fail("Should have thrown");
|
|
169
|
-
} catch (err) {
|
|
170
|
-
assert.ok(err instanceof Error);
|
|
171
|
-
assert.ok(err.message.includes("state.json"));
|
|
172
|
-
assert.ok(err.message.includes("completed"));
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe("writeMetrics error handling", () => {
|
|
178
|
-
it("does not throw when write fails", (t) => {
|
|
179
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-metrics-err-"));
|
|
180
|
-
t.after(() => {
|
|
181
|
-
try { fs.chmodSync(tmpDir, 0o755); } catch { /* ignore */ }
|
|
182
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
fs.chmodSync(tmpDir, 0o444);
|
|
186
|
-
|
|
187
|
-
// Should not throw
|
|
188
|
-
assert.doesNotThrow(() => {
|
|
189
|
-
writeMetrics(tmpDir, {});
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
});
|
package/src/state.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
|
-
import type { ExecutionState, WorkflowResult, WorkflowSpec, StepResult, StepUsage } from "./types.ts";
|
|
5
|
-
import { formatDuration, formatCost } from "./format.ts";
|
|
6
|
-
import { WORKFLOWS_DIR } from "./workflows-dir.ts";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Generate a unique run ID.
|
|
10
|
-
* Format: <workflow-name>-<yyyymmdd>-<hhmmss>-<4 hex chars>
|
|
11
|
-
* Example: "bugfix-20260312-214041-a3f2"
|
|
12
|
-
*/
|
|
13
|
-
export function generateRunId(workflowName: string): string {
|
|
14
|
-
const now = new Date();
|
|
15
|
-
const yyyy = now.getFullYear().toString();
|
|
16
|
-
const mm = (now.getMonth() + 1).toString().padStart(2, "0");
|
|
17
|
-
const dd = now.getDate().toString().padStart(2, "0");
|
|
18
|
-
const hh = now.getHours().toString().padStart(2, "0");
|
|
19
|
-
const min = now.getMinutes().toString().padStart(2, "0");
|
|
20
|
-
const ss = now.getSeconds().toString().padStart(2, "0");
|
|
21
|
-
const hex = crypto.randomBytes(2).toString("hex");
|
|
22
|
-
return `${workflowName}-${yyyy}${mm}${dd}-${hh}${min}${ss}-${hex}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Initialize the run directory structure.
|
|
27
|
-
* Creates:
|
|
28
|
-
* .workflows/runs/<workflowName>/runs/<runId>/
|
|
29
|
-
* .workflows/runs/<workflowName>/runs/<runId>/sessions/
|
|
30
|
-
* .workflows/runs/<workflowName>/runs/<runId>/outputs/
|
|
31
|
-
*
|
|
32
|
-
* Each workflow owns a directory under .workflows/runs/<name>/.
|
|
33
|
-
* Run state goes in runs/<runId>/; artifacts live at the workflow level.
|
|
34
|
-
*
|
|
35
|
-
* @param cwd - project root
|
|
36
|
-
* @param workflowName - name of the workflow
|
|
37
|
-
* @param runId - unique run identifier
|
|
38
|
-
* @returns absolute path to the run directory
|
|
39
|
-
*/
|
|
40
|
-
export function initRunDir(cwd: string, workflowName: string, runId: string): string {
|
|
41
|
-
const runDir = path.join(cwd, WORKFLOWS_DIR, "runs", workflowName, "runs", runId);
|
|
42
|
-
fs.mkdirSync(path.join(runDir, "sessions"), { recursive: true });
|
|
43
|
-
fs.mkdirSync(path.join(runDir, "outputs"), { recursive: true });
|
|
44
|
-
return runDir;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get the workflow output directory (parent of runs/).
|
|
49
|
-
* This is where artifacts are written.
|
|
50
|
-
*
|
|
51
|
-
* @param cwd - project root
|
|
52
|
-
* @param workflowName - name of the workflow
|
|
53
|
-
* @returns absolute path to .workflows/runs/<workflowName>/
|
|
54
|
-
*/
|
|
55
|
-
export function getWorkflowDir(cwd: string, workflowName: string): string {
|
|
56
|
-
return path.join(cwd, WORKFLOWS_DIR, "runs", workflowName);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Write execution state to state.json in the run directory.
|
|
61
|
-
* Overwrites on each call (not append).
|
|
62
|
-
* Uses atomic write: write to .state.json.tmp, then fs.renameSync to state.json.
|
|
63
|
-
*/
|
|
64
|
-
export function writeState(runDir: string, state: ExecutionState): void {
|
|
65
|
-
state.updatedAt = new Date().toISOString();
|
|
66
|
-
const tmpPath = path.join(runDir, ".state.json.tmp");
|
|
67
|
-
const finalPath = path.join(runDir, "state.json");
|
|
68
|
-
try {
|
|
69
|
-
fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
70
|
-
fs.renameSync(tmpPath, finalPath);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
// Best-effort cleanup of partial tmp file
|
|
73
|
-
try { fs.unlinkSync(tmpPath); } catch { /* ignore cleanup failure */ }
|
|
74
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
-
throw new Error(
|
|
76
|
-
`Failed to write workflow state to ${finalPath} (status: ${state.status}): ${msg}`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Read execution state from state.json.
|
|
83
|
-
* Returns null if file doesn't exist.
|
|
84
|
-
*/
|
|
85
|
-
export function readState(runDir: string): ExecutionState | null {
|
|
86
|
-
const statePath = path.join(runDir, "state.json");
|
|
87
|
-
try {
|
|
88
|
-
const content = fs.readFileSync(statePath, "utf-8");
|
|
89
|
-
return JSON.parse(content) as ExecutionState;
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Write a step's structured output to outputs/<stepName>.json.
|
|
97
|
-
*/
|
|
98
|
-
export function writeStepOutput(runDir: string, stepName: string, output: unknown): void {
|
|
99
|
-
const outputPath = path.join(runDir, "outputs", `${stepName}.json`);
|
|
100
|
-
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2), "utf-8");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Write aggregated metrics to metrics.json.
|
|
105
|
-
*/
|
|
106
|
-
export function writeMetrics(runDir: string, steps: Record<string, StepResult>): void {
|
|
107
|
-
const total = aggregateUsage(steps);
|
|
108
|
-
const totalDurationMs = Object.values(steps).reduce((sum, s) => sum + s.durationMs, 0);
|
|
109
|
-
const metrics = {
|
|
110
|
-
totalUsage: total,
|
|
111
|
-
totalDurationMs,
|
|
112
|
-
steps: Object.fromEntries(
|
|
113
|
-
Object.entries(steps).map(([name, s]) => [name, { usage: s.usage, durationMs: s.durationMs }]),
|
|
114
|
-
),
|
|
115
|
-
};
|
|
116
|
-
try {
|
|
117
|
-
fs.writeFileSync(path.join(runDir, "metrics.json"), JSON.stringify(metrics, null, 2), "utf-8");
|
|
118
|
-
} catch (err) {
|
|
119
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
120
|
-
process.stderr.write(
|
|
121
|
-
`[pi-workflows] Warning: failed to write metrics to ${runDir}/metrics.json: ${msg}\n`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Aggregate usage across all steps.
|
|
128
|
-
*/
|
|
129
|
-
export function aggregateUsage(steps: Record<string, StepResult>): StepUsage {
|
|
130
|
-
const total: StepUsage = {
|
|
131
|
-
input: 0,
|
|
132
|
-
output: 0,
|
|
133
|
-
cacheRead: 0,
|
|
134
|
-
cacheWrite: 0,
|
|
135
|
-
cost: 0,
|
|
136
|
-
turns: 0,
|
|
137
|
-
};
|
|
138
|
-
for (const step of Object.values(steps)) {
|
|
139
|
-
if (step.usage) {
|
|
140
|
-
total.input += step.usage.input;
|
|
141
|
-
total.output += step.usage.output;
|
|
142
|
-
total.cacheRead += step.usage.cacheRead;
|
|
143
|
-
total.cacheWrite += step.usage.cacheWrite;
|
|
144
|
-
total.cost += step.usage.cost;
|
|
145
|
-
total.turns += step.usage.turns;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return total;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Build a WorkflowResult from execution state.
|
|
153
|
-
* Aggregates usage across steps, computes total duration,
|
|
154
|
-
* sets output to the last completed step's output (or explicit workflow output if defined).
|
|
155
|
-
*/
|
|
156
|
-
export function buildResult(
|
|
157
|
-
spec: WorkflowSpec,
|
|
158
|
-
runId: string,
|
|
159
|
-
runDir: string,
|
|
160
|
-
state: ExecutionState,
|
|
161
|
-
status: "completed" | "failed" | "paused",
|
|
162
|
-
): WorkflowResult {
|
|
163
|
-
const totalUsage = aggregateUsage(state.steps);
|
|
164
|
-
const totalDurationMs = Object.values(state.steps).reduce((sum, s) => sum + s.durationMs, 0);
|
|
165
|
-
|
|
166
|
-
// Determine output: last completed step's output
|
|
167
|
-
let output: unknown = undefined;
|
|
168
|
-
const stepNames = Object.keys(spec.steps);
|
|
169
|
-
for (let i = stepNames.length - 1; i >= 0; i--) {
|
|
170
|
-
const stepName = stepNames[i];
|
|
171
|
-
const stepResult = state.steps[stepName];
|
|
172
|
-
if (stepResult && stepResult.status === "completed") {
|
|
173
|
-
output = stepResult.output ?? stepResult.textOutput;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
workflow: spec.name,
|
|
180
|
-
runId,
|
|
181
|
-
status,
|
|
182
|
-
steps: state.steps,
|
|
183
|
-
output,
|
|
184
|
-
totalUsage,
|
|
185
|
-
totalDurationMs,
|
|
186
|
-
runDir,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Format a WorkflowResult as human-readable text for injection into the conversation.
|
|
192
|
-
*/
|
|
193
|
-
export function formatResult(result: WorkflowResult): string {
|
|
194
|
-
const stepEntries = Object.values(result.steps);
|
|
195
|
-
const totalSteps = stepEntries.length;
|
|
196
|
-
const completedSteps = stepEntries.filter((s) => s.status === "completed").length;
|
|
197
|
-
const duration = formatDuration(result.totalDurationMs);
|
|
198
|
-
const cost = formatCost(result.totalUsage.cost);
|
|
199
|
-
|
|
200
|
-
const lines: string[] = [];
|
|
201
|
-
|
|
202
|
-
if (result.status === "completed") {
|
|
203
|
-
lines.push(`Workflow '${result.workflow}' completed (${totalSteps} steps, ${duration}, ${cost})`);
|
|
204
|
-
} else if (result.status === "paused") {
|
|
205
|
-
lines.push(
|
|
206
|
-
`Workflow '${result.workflow}' paused (${completedSteps}/${totalSteps} steps completed, ${duration}, ${cost})`,
|
|
207
|
-
);
|
|
208
|
-
} else {
|
|
209
|
-
// Find the failed step name
|
|
210
|
-
const failedStep = stepEntries.find((s) => s.status === "failed");
|
|
211
|
-
const failedName = failedStep ? failedStep.step : "unknown";
|
|
212
|
-
lines.push(
|
|
213
|
-
`Workflow '${result.workflow}' failed at step '${failedName}' (${completedSteps}/${totalSteps} steps, ${duration}, ${cost})`,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
lines.push("");
|
|
218
|
-
lines.push("Steps:");
|
|
219
|
-
|
|
220
|
-
for (const step of stepEntries) {
|
|
221
|
-
const stepDuration = formatDuration(step.durationMs);
|
|
222
|
-
const stepCost = formatCost(step.usage.cost);
|
|
223
|
-
|
|
224
|
-
if (step.status === "completed") {
|
|
225
|
-
lines.push(` \u2713 ${step.step} ${stepDuration} ${stepCost} (${step.usage.turns} turns)`);
|
|
226
|
-
} else if (step.status === "failed") {
|
|
227
|
-
const errorPreview = step.error || "Unknown error";
|
|
228
|
-
lines.push(` \u2717 ${step.step} ${stepDuration} ${stepCost} ${errorPreview}`);
|
|
229
|
-
} else {
|
|
230
|
-
// skipped
|
|
231
|
-
lines.push(` \u00b7 ${step.step}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (result.status === "completed") {
|
|
236
|
-
lines.push("");
|
|
237
|
-
lines.push(
|
|
238
|
-
`Total: ${result.totalUsage.input} input + ${result.totalUsage.output} output tokens, ${cost}`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (result.artifacts && Object.keys(result.artifacts).length > 0) {
|
|
243
|
-
lines.push("");
|
|
244
|
-
lines.push("Artifacts:");
|
|
245
|
-
for (const [name, artifactPath] of Object.entries(result.artifacts)) {
|
|
246
|
-
lines.push(` ${name} \u2192 ${artifactPath}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
lines.push("");
|
|
251
|
-
lines.push(`Session logs: ${result.runDir}/sessions/`);
|
|
252
|
-
|
|
253
|
-
return lines.join("\n");
|
|
254
|
-
}
|