@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.
Files changed (215) 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 +172 -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 +675 -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 +156 -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 +57 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +125 -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 +164 -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 +726 -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 +438 -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 +464 -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 +84 -0
  140. package/skills/pi-workflows/SKILL.md +247 -0
  141. package/templates/architecture-designer/task.md +117 -0
  142. package/templates/architecture-inferrer/task.md +61 -0
  143. package/templates/gap-identifier/task.md +103 -0
  144. package/templates/handoff-writer/task.md +91 -0
  145. package/templates/plan-creator/task.md +143 -0
  146. package/templates/project-definer/task.md +67 -0
  147. package/templates/project-inferrer/task.md +56 -0
  148. package/templates/requirements-gatherer/task.md +90 -0
  149. package/workflows/analyze-existing-project.workflow.yaml +74 -0
  150. package/workflows/create-handoff.workflow.yaml +58 -0
  151. package/workflows/create-phase.workflow.yaml +1 -1
  152. package/workflows/do-gap.workflow.yaml +4 -4
  153. package/workflows/fix-audit.workflow.yaml +1 -1
  154. package/workflows/gap-to-phase.workflow.yaml +1 -1
  155. package/workflows/init-new-project.workflow.yaml +55 -0
  156. package/workflows/plan-from-requirements.workflow.yaml +51 -0
  157. package/workflows/self-implement.workflow.yaml +3 -3
  158. package/workflows/typed-analysis.workflow.yaml +4 -4
  159. package/src/agent-spec.test.ts +0 -289
  160. package/src/agent-spec.ts +0 -122
  161. package/src/block-validation.test.ts +0 -350
  162. package/src/checkpoint.test.ts +0 -237
  163. package/src/checkpoint.ts +0 -140
  164. package/src/completion.test.ts +0 -143
  165. package/src/completion.ts +0 -68
  166. package/src/dag.test.ts +0 -350
  167. package/src/dag.ts +0 -219
  168. package/src/dispatch.test.ts +0 -475
  169. package/src/dispatch.ts +0 -353
  170. package/src/expression.test.ts +0 -353
  171. package/src/expression.ts +0 -332
  172. package/src/format.test.ts +0 -80
  173. package/src/format.ts +0 -41
  174. package/src/graduated-failure.test.ts +0 -556
  175. package/src/index.test.ts +0 -114
  176. package/src/index.ts +0 -516
  177. package/src/loop.test.ts +0 -551
  178. package/src/output.test.ts +0 -213
  179. package/src/output.ts +0 -70
  180. package/src/parallel-integration.test.ts +0 -175
  181. package/src/resume.test.ts +0 -192
  182. package/src/state.test.ts +0 -192
  183. package/src/state.ts +0 -254
  184. package/src/step-agent.test.ts +0 -327
  185. package/src/step-agent.ts +0 -178
  186. package/src/step-command.test.ts +0 -330
  187. package/src/step-command.ts +0 -132
  188. package/src/step-foreach.test.ts +0 -647
  189. package/src/step-foreach.ts +0 -148
  190. package/src/step-gate.test.ts +0 -185
  191. package/src/step-gate.ts +0 -116
  192. package/src/step-loop.test.ts +0 -626
  193. package/src/step-loop.ts +0 -323
  194. package/src/step-parallel.test.ts +0 -475
  195. package/src/step-parallel.ts +0 -168
  196. package/src/step-pause.test.ts +0 -30
  197. package/src/step-shared.test.ts +0 -355
  198. package/src/step-shared.ts +0 -157
  199. package/src/step-transform.test.ts +0 -155
  200. package/src/step-transform.ts +0 -47
  201. package/src/template-integration.test.ts +0 -72
  202. package/src/template.test.ts +0 -162
  203. package/src/test-helpers.ts +0 -51
  204. package/src/tui.test.ts +0 -355
  205. package/src/tui.ts +0 -182
  206. package/src/types.ts +0 -195
  207. package/src/verifier-schema.test.ts +0 -226
  208. package/src/workflow-discovery.test.ts +0 -105
  209. package/src/workflow-discovery.ts +0 -114
  210. package/src/workflow-executor.test.ts +0 -1532
  211. package/src/workflow-executor.ts +0 -810
  212. package/src/workflow-sdk.test.ts +0 -238
  213. package/src/workflow-sdk.ts +0 -262
  214. package/src/workflow-spec.test.ts +0 -576
  215. 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
- }