@davidorex/pi-workflows 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/README.md +12 -2
  3. package/agents/architecture-designer.agent.yaml +58 -0
  4. package/agents/architecture-inferrer.agent.yaml +60 -0
  5. package/agents/gap-identifier.agent.yaml +66 -0
  6. package/agents/handoff-writer.agent.yaml +64 -0
  7. package/agents/plan-creator.agent.yaml +63 -0
  8. package/agents/project-definer.agent.yaml +53 -0
  9. package/agents/project-inferrer.agent.yaml +63 -0
  10. package/agents/requirements-gatherer.agent.yaml +58 -0
  11. package/dist/agent-spec.d.ts +27 -0
  12. package/dist/agent-spec.d.ts.map +1 -0
  13. package/dist/agent-spec.js +112 -0
  14. package/dist/agent-spec.js.map +1 -0
  15. package/dist/checkpoint.d.ts +50 -0
  16. package/dist/checkpoint.d.ts.map +1 -0
  17. package/dist/checkpoint.js +109 -0
  18. package/dist/checkpoint.js.map +1 -0
  19. package/dist/completion.d.ts +18 -0
  20. package/dist/completion.d.ts.map +1 -0
  21. package/dist/completion.js +58 -0
  22. package/dist/completion.js.map +1 -0
  23. package/dist/dag.d.ts +56 -0
  24. package/dist/dag.d.ts.map +1 -0
  25. package/dist/dag.js +178 -0
  26. package/dist/dag.js.map +1 -0
  27. package/dist/dispatch.d.ts +35 -0
  28. package/dist/dispatch.d.ts.map +1 -0
  29. package/dist/dispatch.js +311 -0
  30. package/dist/dispatch.js.map +1 -0
  31. package/dist/expression.d.ts +64 -0
  32. package/dist/expression.d.ts.map +1 -0
  33. package/dist/expression.js +294 -0
  34. package/dist/expression.js.map +1 -0
  35. package/dist/format.d.ts +19 -0
  36. package/dist/format.d.ts.map +1 -0
  37. package/dist/format.js +42 -0
  38. package/dist/format.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +724 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/output.d.ts +12 -0
  44. package/dist/output.d.ts.map +1 -0
  45. package/dist/output.js +59 -0
  46. package/dist/output.js.map +1 -0
  47. package/dist/state.d.ts +66 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +227 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/step-agent.d.ts +35 -0
  52. package/dist/step-agent.d.ts.map +1 -0
  53. package/dist/step-agent.js +171 -0
  54. package/dist/step-agent.js.map +1 -0
  55. package/dist/step-command.d.ts +21 -0
  56. package/dist/step-command.d.ts.map +1 -0
  57. package/dist/step-command.js +150 -0
  58. package/dist/step-command.js.map +1 -0
  59. package/dist/step-foreach.d.ts +33 -0
  60. package/dist/step-foreach.d.ts.map +1 -0
  61. package/dist/step-foreach.js +112 -0
  62. package/dist/step-foreach.js.map +1 -0
  63. package/dist/step-gate.d.ts +18 -0
  64. package/dist/step-gate.d.ts.map +1 -0
  65. package/dist/step-gate.js +137 -0
  66. package/dist/step-gate.js.map +1 -0
  67. package/dist/step-loop.d.ts +32 -0
  68. package/dist/step-loop.d.ts.map +1 -0
  69. package/dist/step-loop.js +278 -0
  70. package/dist/step-loop.js.map +1 -0
  71. package/dist/step-monitor.d.ts +56 -0
  72. package/dist/step-monitor.d.ts.map +1 -0
  73. package/dist/step-monitor.js +317 -0
  74. package/dist/step-monitor.js.map +1 -0
  75. package/dist/step-parallel.d.ts +43 -0
  76. package/dist/step-parallel.d.ts.map +1 -0
  77. package/dist/step-parallel.js +122 -0
  78. package/dist/step-parallel.js.map +1 -0
  79. package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
  80. package/dist/step-pause.d.ts.map +1 -0
  81. package/dist/step-pause.js +22 -0
  82. package/dist/step-pause.js.map +1 -0
  83. package/dist/step-shared.d.ts +63 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +136 -0
  86. package/dist/step-shared.js.map +1 -0
  87. package/dist/step-transform.d.ts +11 -0
  88. package/dist/step-transform.d.ts.map +1 -0
  89. package/dist/step-transform.js +41 -0
  90. package/dist/step-transform.js.map +1 -0
  91. package/dist/template.d.ts +51 -0
  92. package/dist/template.d.ts.map +1 -0
  93. package/{src/template.ts → dist/template.js} +32 -38
  94. package/dist/template.js.map +1 -0
  95. package/dist/test-helpers.d.ts +17 -0
  96. package/dist/test-helpers.d.ts.map +1 -0
  97. package/dist/test-helpers.js +46 -0
  98. package/dist/test-helpers.js.map +1 -0
  99. package/dist/tui.d.ts +47 -0
  100. package/dist/tui.d.ts.map +1 -0
  101. package/dist/tui.js +143 -0
  102. package/dist/tui.js.map +1 -0
  103. package/dist/types.d.ts +165 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +3 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/workflow-discovery.d.ts +23 -0
  108. package/dist/workflow-discovery.d.ts.map +1 -0
  109. package/dist/workflow-discovery.js +103 -0
  110. package/dist/workflow-discovery.js.map +1 -0
  111. package/dist/workflow-executor.d.ts +50 -0
  112. package/dist/workflow-executor.d.ts.map +1 -0
  113. package/dist/workflow-executor.js +738 -0
  114. package/dist/workflow-executor.js.map +1 -0
  115. package/dist/workflow-sdk.d.ts +44 -0
  116. package/dist/workflow-sdk.d.ts.map +1 -0
  117. package/dist/workflow-sdk.js +457 -0
  118. package/dist/workflow-sdk.js.map +1 -0
  119. package/dist/workflow-spec.d.ts +33 -0
  120. package/dist/workflow-spec.d.ts.map +1 -0
  121. package/dist/workflow-spec.js +476 -0
  122. package/dist/workflow-spec.js.map +1 -0
  123. package/dist/workflows-dir.d.ts +3 -0
  124. package/dist/workflows-dir.d.ts.map +1 -0
  125. package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
  126. package/dist/workflows-dir.js.map +1 -0
  127. package/package.json +53 -38
  128. package/schemas/decomposition-specs.schema.json +48 -48
  129. package/schemas/execution-results.schema.json +48 -48
  130. package/schemas/investigation-findings.schema.json +43 -43
  131. package/schemas/pattern-analysis.schema.json +41 -41
  132. package/schemas/phase.schema.json +48 -0
  133. package/schemas/plan-breakdown.schema.json +20 -20
  134. package/schemas/quality-analysis.schema.json +33 -33
  135. package/schemas/research-findings.schema.json +42 -42
  136. package/schemas/structure-analysis.schema.json +40 -40
  137. package/schemas/synthesis.schema.json +31 -31
  138. package/schemas/verifier-output.schema.json +94 -94
  139. package/skill-narrative.md +107 -0
  140. package/skills/pi-workflows/SKILL.md +185 -0
  141. package/skills/pi-workflows/references/bundled-resources.md +87 -0
  142. package/templates/architecture-designer/task.md +117 -0
  143. package/templates/architecture-inferrer/task.md +61 -0
  144. package/templates/gap-identifier/task.md +103 -0
  145. package/templates/handoff-writer/task.md +91 -0
  146. package/templates/plan-creator/task.md +143 -0
  147. package/templates/project-definer/task.md +67 -0
  148. package/templates/project-inferrer/task.md +56 -0
  149. package/templates/requirements-gatherer/task.md +90 -0
  150. package/workflows/analyze-existing-project.workflow.yaml +94 -0
  151. package/workflows/create-handoff.workflow.yaml +66 -0
  152. package/workflows/create-phase.workflow.yaml +1 -1
  153. package/workflows/do-gap.workflow.yaml +4 -4
  154. package/workflows/fix-audit.workflow.yaml +1 -1
  155. package/workflows/gap-to-phase.workflow.yaml +1 -1
  156. package/workflows/init-new-project.workflow.yaml +81 -0
  157. package/workflows/plan-from-requirements.workflow.yaml +59 -0
  158. package/workflows/self-implement.workflow.yaml +3 -3
  159. package/workflows/typed-analysis.workflow.yaml +4 -4
  160. package/src/agent-spec.test.ts +0 -289
  161. package/src/agent-spec.ts +0 -122
  162. package/src/block-validation.test.ts +0 -350
  163. package/src/checkpoint.test.ts +0 -237
  164. package/src/checkpoint.ts +0 -140
  165. package/src/completion.test.ts +0 -143
  166. package/src/completion.ts +0 -68
  167. package/src/dag.test.ts +0 -350
  168. package/src/dag.ts +0 -219
  169. package/src/dispatch.test.ts +0 -475
  170. package/src/dispatch.ts +0 -353
  171. package/src/expression.test.ts +0 -353
  172. package/src/expression.ts +0 -332
  173. package/src/format.test.ts +0 -80
  174. package/src/format.ts +0 -41
  175. package/src/graduated-failure.test.ts +0 -556
  176. package/src/index.test.ts +0 -114
  177. package/src/index.ts +0 -516
  178. package/src/loop.test.ts +0 -551
  179. package/src/output.test.ts +0 -213
  180. package/src/output.ts +0 -70
  181. package/src/parallel-integration.test.ts +0 -175
  182. package/src/resume.test.ts +0 -192
  183. package/src/state.test.ts +0 -192
  184. package/src/state.ts +0 -254
  185. package/src/step-agent.test.ts +0 -327
  186. package/src/step-agent.ts +0 -178
  187. package/src/step-command.test.ts +0 -330
  188. package/src/step-command.ts +0 -132
  189. package/src/step-foreach.test.ts +0 -647
  190. package/src/step-foreach.ts +0 -148
  191. package/src/step-gate.test.ts +0 -185
  192. package/src/step-gate.ts +0 -116
  193. package/src/step-loop.test.ts +0 -626
  194. package/src/step-loop.ts +0 -323
  195. package/src/step-parallel.test.ts +0 -475
  196. package/src/step-parallel.ts +0 -168
  197. package/src/step-pause.test.ts +0 -30
  198. package/src/step-shared.test.ts +0 -355
  199. package/src/step-shared.ts +0 -157
  200. package/src/step-transform.test.ts +0 -155
  201. package/src/step-transform.ts +0 -47
  202. package/src/template-integration.test.ts +0 -72
  203. package/src/template.test.ts +0 -162
  204. package/src/test-helpers.ts +0 -51
  205. package/src/tui.test.ts +0 -355
  206. package/src/tui.ts +0 -182
  207. package/src/types.ts +0 -195
  208. package/src/verifier-schema.test.ts +0 -226
  209. package/src/workflow-discovery.test.ts +0 -105
  210. package/src/workflow-discovery.ts +0 -114
  211. package/src/workflow-executor.test.ts +0 -1532
  212. package/src/workflow-executor.ts +0 -810
  213. package/src/workflow-sdk.test.ts +0 -238
  214. package/src/workflow-sdk.ts +0 -262
  215. package/src/workflow-spec.test.ts +0 -576
  216. package/src/workflow-spec.ts +0 -471
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
- }