@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/checkpoint.ts DELETED
@@ -1,140 +0,0 @@
1
- /**
2
- * Checkpoint and resume logic for workflow runs.
3
- *
4
- * Finds incomplete runs, validates they match the current spec,
5
- * and provides state needed to resume execution.
6
- */
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import type { ExecutionState, WorkflowSpec } from "./types.ts";
10
- import { readState } from "./state.ts";
11
- import { WORKFLOWS_DIR } from "./workflows-dir.ts";
12
-
13
- export interface IncompleteRun {
14
- runId: string;
15
- runDir: string;
16
- state: ExecutionState;
17
- completedSteps: string[]; // step names with status completed or skipped
18
- failedStep?: string; // name of the step that failed (if any)
19
- updatedAt?: string; // last update timestamp
20
- }
21
-
22
- /**
23
- * Find the most recent incomplete run for a workflow.
24
- *
25
- * Scans .workflows/runs/<name>/runs/ for state.json files
26
- * with status "running" or "failed". Returns the most recent one
27
- * (by directory name, which encodes timestamp).
28
- *
29
- * Returns null if no incomplete runs exist.
30
- */
31
- export function findIncompleteRun(cwd: string, workflowName: string): IncompleteRun | null {
32
- const runsDir = path.join(cwd, WORKFLOWS_DIR, "runs", workflowName, "runs");
33
- if (!fs.existsSync(runsDir)) return null;
34
-
35
- const entries = fs.readdirSync(runsDir)
36
- .filter(e => {
37
- const stat = fs.statSync(path.join(runsDir, e));
38
- return stat.isDirectory();
39
- })
40
- .sort() // lexicographic — timestamp-based IDs sort chronologically
41
- .reverse(); // most recent first
42
-
43
- for (const runId of entries) {
44
- const runDir = path.join(runsDir, runId);
45
- const state = readState(runDir);
46
- if (!state) continue;
47
-
48
- if (state.status === "running" || state.status === "failed" || state.status === "paused") {
49
- const completedSteps = Object.entries(state.steps)
50
- .filter(([, r]) => r.status === "completed" || r.status === "skipped")
51
- .map(([name]) => name);
52
-
53
- const failedStep = Object.entries(state.steps)
54
- .find(([, r]) => r.status === "failed")
55
- ?.[0];
56
-
57
- return {
58
- runId,
59
- runDir,
60
- state,
61
- completedSteps,
62
- failedStep,
63
- updatedAt: state.updatedAt,
64
- };
65
- }
66
- }
67
-
68
- return null;
69
- }
70
-
71
- /**
72
- * Validate that a saved state is compatible with the current workflow spec.
73
- *
74
- * Checks:
75
- * 1. Spec version matches (if both specify one)
76
- * 2. All completed steps still exist in the spec
77
- *
78
- * Returns null if compatible, or a string describing the incompatibility.
79
- */
80
- export function validateResumeCompatibility(
81
- state: ExecutionState,
82
- spec: WorkflowSpec,
83
- ): string | null {
84
- // Check version match
85
- if (state.specVersion && spec.version && state.specVersion !== spec.version) {
86
- return `Spec version changed: run has '${state.specVersion}', current is '${spec.version}'`;
87
- }
88
-
89
- // Check all completed steps still exist in spec
90
- const specStepNames = new Set(Object.keys(spec.steps));
91
- for (const stepName of Object.keys(state.steps)) {
92
- if (!specStepNames.has(stepName)) {
93
- return `Step '${stepName}' was completed in the saved run but no longer exists in the workflow spec`;
94
- }
95
- }
96
-
97
- return null; // compatible
98
- }
99
-
100
- /**
101
- * Determine which steps need to run when resuming.
102
- *
103
- * Given the execution plan (layers of step names) and the set of
104
- * already-completed steps, returns the layer index to resume from
105
- * and which steps within that layer still need to run.
106
- *
107
- * For a partially-complete parallel layer (some steps done, some not),
108
- * returns only the incomplete steps from that layer.
109
- */
110
- export function computeResumePoint(
111
- plan: Array<{ steps: string[] }>,
112
- completedSteps: Set<string>,
113
- ): { resumeLayerIndex: number; pendingStepsInLayer: string[] } | null {
114
- for (let i = 0; i < plan.length; i++) {
115
- const layer = plan[i];
116
- const pending = layer.steps.filter(s => !completedSteps.has(s));
117
-
118
- if (pending.length > 0) {
119
- return { resumeLayerIndex: i, pendingStepsInLayer: pending };
120
- }
121
- }
122
-
123
- // All steps completed — nothing to resume
124
- return null;
125
- }
126
-
127
- /**
128
- * Format a human-readable summary of an incomplete run for display.
129
- */
130
- export function formatIncompleteRun(run: IncompleteRun, spec: WorkflowSpec): string {
131
- const totalSteps = Object.keys(spec.steps).length;
132
- const completed = run.completedSteps.length;
133
- const status = run.state.status === "paused" ? "paused"
134
- : run.state.status === "running" ? "interrupted"
135
- : "failed";
136
- const failedInfo = run.failedStep ? ` at step '${run.failedStep}'` : "";
137
- const timeInfo = run.updatedAt ? ` (last updated: ${run.updatedAt})` : "";
138
-
139
- return `Found ${status} run${failedInfo}: ${completed}/${totalSteps} steps completed${timeInfo}. Run ID: ${run.runId}`;
140
- }
@@ -1,143 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { resolveCompletion } from "./completion.ts";
4
- import type { CompletionSpec, WorkflowResult, StepResult, StepUsage } from "./types.ts";
5
-
6
- const usage: StepUsage = { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, cost: 0.03, turns: 2 };
7
-
8
- const stepResult: StepResult = {
9
- step: "summarize",
10
- agent: "summarizer",
11
- status: "completed",
12
- output: { headline: "Found 3 issues" },
13
- textOutput: "The codebase has 3 critical issues.",
14
- usage,
15
- durationMs: 45000,
16
- };
17
-
18
- const result: WorkflowResult = {
19
- workflow: "explore-summarize",
20
- runId: "explore-summarize-20260313-120000-abcd",
21
- status: "completed",
22
- steps: { summarize: stepResult },
23
- output: { headline: "Found 3 issues" },
24
- totalUsage: { input: 2000, output: 1000, cacheRead: 0, cacheWrite: 0, cost: 0.05, turns: 4 },
25
- totalDurationMs: 92000,
26
- runDir: "/tmp/runs/explore-summarize-20260313-120000-abcd",
27
- };
28
-
29
- const input = { path: "/src", question: "What are the main components?" };
30
-
31
- describe("resolveCompletion", () => {
32
- describe("template form", () => {
33
- it("resolves expressions in template", () => {
34
- const spec: CompletionSpec = {
35
- template: "## Results\n\n${{ steps.summarize.textOutput }}\n\nDone in ${{ totalDurationMs | duration }}.",
36
- };
37
- const content = resolveCompletion(spec, result, input);
38
- assert.ok(content.includes("## Results"));
39
- assert.ok(content.includes("The codebase has 3 critical issues."));
40
- assert.ok(content.includes("Done in 1m32s."));
41
- });
42
-
43
- it("resolves input and workflow metadata in template", () => {
44
- const spec: CompletionSpec = {
45
- template: "Explored ${{ input.path }} for workflow '${{ workflow }}'. Status: ${{ status }}.",
46
- };
47
- const content = resolveCompletion(spec, result, input);
48
- assert.strictEqual(content, "Explored /src for workflow 'explore-summarize'. Status: completed.");
49
- });
50
-
51
- it("applies currency filter in template", () => {
52
- const spec: CompletionSpec = {
53
- template: "Cost: ${{ totalUsage.cost | currency }}",
54
- };
55
- const content = resolveCompletion(spec, result, input);
56
- assert.strictEqual(content, "Cost: $0.05");
57
- });
58
-
59
- it("includes runDir in template", () => {
60
- const spec: CompletionSpec = {
61
- template: "Logs at: ${{ runDir }}/sessions/",
62
- };
63
- const content = resolveCompletion(spec, result, input);
64
- assert.strictEqual(content, "Logs at: /tmp/runs/explore-summarize-20260313-120000-abcd/sessions/");
65
- });
66
-
67
- it("renders undefined as empty in template", () => {
68
- const spec: CompletionSpec = {
69
- template: "Answer: ${{ input.nonexistent }}",
70
- };
71
- const content = resolveCompletion(spec, result, input);
72
- assert.strictEqual(content, "Answer: ");
73
- });
74
- });
75
-
76
- describe("message form", () => {
77
- it("resolves message with no include", () => {
78
- const spec: CompletionSpec = {
79
- message: "Present the findings to the user. The workflow '${{ workflow }}' is done.",
80
- };
81
- const content = resolveCompletion(spec, result, input);
82
- assert.ok(content.includes("Present the findings to the user."));
83
- assert.ok(content.includes("'explore-summarize' is done."));
84
- });
85
-
86
- it("resolves message with include paths", () => {
87
- const spec: CompletionSpec = {
88
- message: "Show the user these results.",
89
- include: ["steps.summarize.textOutput", "totalUsage"],
90
- };
91
- const content = resolveCompletion(spec, result, input);
92
- assert.ok(content.includes("Show the user these results."));
93
- assert.ok(content.includes("steps.summarize.textOutput"));
94
- assert.ok(content.includes("The codebase has 3 critical issues."));
95
- assert.ok(content.includes("totalUsage"));
96
- assert.ok(content.includes('"cost"'));
97
- });
98
-
99
- it("formats included objects as JSON blocks", () => {
100
- const spec: CompletionSpec = {
101
- message: "Review:",
102
- include: ["steps.summarize.output"],
103
- };
104
- const content = resolveCompletion(spec, result, input);
105
- assert.ok(content.includes("```json"));
106
- assert.ok(content.includes('"headline"'));
107
- });
108
-
109
- it("formats included primitives as plain text", () => {
110
- const spec: CompletionSpec = {
111
- message: "Data:",
112
- include: ["steps.summarize.textOutput"],
113
- };
114
- const content = resolveCompletion(spec, result, input);
115
- assert.ok(content.includes("The codebase has 3 critical issues."));
116
- // Primitive values should NOT be in JSON blocks
117
- assert.ok(!content.includes("```json\nThe codebase"));
118
- });
119
- });
120
-
121
- describe("error handling", () => {
122
- it("throws on invalid expression in template", () => {
123
- const spec: CompletionSpec = {
124
- template: "Bad: ${{ steps.nonexistent.output }}",
125
- };
126
- assert.throws(
127
- () => resolveCompletion(spec, result, input),
128
- (err: unknown) => err instanceof Error && err.message.includes("nonexistent"),
129
- );
130
- });
131
-
132
- it("throws on invalid include path", () => {
133
- const spec: CompletionSpec = {
134
- message: "ok",
135
- include: ["steps.nonexistent.output"],
136
- };
137
- assert.throws(
138
- () => resolveCompletion(spec, result, input),
139
- (err: unknown) => err instanceof Error && err.message.includes("nonexistent"),
140
- );
141
- });
142
- });
143
- });
package/src/completion.ts DELETED
@@ -1,68 +0,0 @@
1
- import type { CompletionSpec, CompletionScope, WorkflowResult } from "./types.ts";
2
- import { resolveExpression, resolveExpressions } from "./expression.ts";
3
-
4
- /**
5
- * Resolve a completion spec into the final message string injected into
6
- * the main LLM conversation after workflow execution.
7
- *
8
- * Two forms:
9
- * - Template: the template string is resolved with ${{ }} expressions
10
- * and returned as the full content.
11
- * - Message + include: the message is resolved, then each include path
12
- * is resolved and appended as structured data.
13
- *
14
- * @param spec - the CompletionSpec from the workflow YAML
15
- * @param result - the completed WorkflowResult
16
- * @param executionInput - the original workflow input
17
- * @returns the resolved message string
18
- */
19
- export function resolveCompletion(
20
- spec: CompletionSpec,
21
- result: WorkflowResult,
22
- executionInput: unknown,
23
- ): string {
24
- const scope: CompletionScope = {
25
- input: executionInput,
26
- steps: result.steps,
27
- totalUsage: result.totalUsage,
28
- totalDurationMs: result.totalDurationMs,
29
- runDir: result.runDir,
30
- runId: result.runId,
31
- workflow: result.workflow,
32
- status: result.status,
33
- output: result.output,
34
- };
35
-
36
- if (spec.template) {
37
- const resolved = resolveExpressions(spec.template, scope);
38
- return String(resolved ?? "");
39
- }
40
-
41
- // Message form
42
- const parts: string[] = [];
43
-
44
- if (spec.message) {
45
- const resolvedMessage = resolveExpressions(spec.message, scope);
46
- parts.push(String(resolvedMessage ?? ""));
47
- }
48
-
49
- if (spec.include && spec.include.length > 0) {
50
- const included: Record<string, unknown> = {};
51
- for (const path of spec.include) {
52
- const value = resolveExpression(path, scope);
53
- included[path] = value;
54
- }
55
-
56
- parts.push("");
57
- parts.push("---");
58
- for (const [path, value] of Object.entries(included)) {
59
- if (value !== null && typeof value === "object") {
60
- parts.push(`\n### ${path}\n\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``);
61
- } else {
62
- parts.push(`\n### ${path}\n${String(value ?? "")}`);
63
- }
64
- }
65
- }
66
-
67
- return parts.join("\n");
68
- }
package/src/dag.test.ts DELETED
@@ -1,350 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { extractDependencies, buildExecutionPlan, isSequential } from "./dag.ts";
4
- import type { StepSpec } from "./types.ts";
5
- import { makeSpec as makeSpecFull } from "./test-helpers.ts";
6
-
7
- /**
8
- * Helper: build a minimal WorkflowSpec from a steps object.
9
- */
10
- function makeSpec(steps: Record<string, StepSpec>) {
11
- return makeSpecFull({ steps });
12
- }
13
-
14
- describe("extractDependencies", () => {
15
- it("returns empty deps for steps with no expressions", () => {
16
- const spec = makeSpec({
17
- a: { agent: "foo" },
18
- b: { agent: "bar" },
19
- });
20
- const deps = extractDependencies(spec);
21
- assert.strictEqual(deps.get("a")!.size, 0);
22
- assert.strictEqual(deps.get("b")!.size, 0);
23
- });
24
-
25
- it("extracts deps from input expressions", () => {
26
- const spec = makeSpec({
27
- a: { agent: "foo" },
28
- b: {
29
- agent: "bar",
30
- input: {
31
- context: "${{ steps.a.textOutput }}",
32
- },
33
- },
34
- });
35
- const deps = extractDependencies(spec);
36
- assert.strictEqual(deps.get("a")!.size, 0);
37
- assert.ok(deps.get("b")!.has("a"));
38
- });
39
-
40
- it("extracts deps from when condition", () => {
41
- const spec = makeSpec({
42
- a: { agent: "foo" },
43
- b: {
44
- agent: "bar",
45
- when: "${{ steps.a.output.shouldRun }}",
46
- },
47
- });
48
- const deps = extractDependencies(spec);
49
- assert.ok(deps.get("b")!.has("a"));
50
- });
51
-
52
- it("extracts deps from bare when condition (no ${{ }})", () => {
53
- const spec = makeSpec({
54
- a: { agent: "foo" },
55
- b: {
56
- agent: "bar",
57
- when: "steps.a.output.shouldRun",
58
- },
59
- });
60
- const deps = extractDependencies(spec);
61
- assert.ok(deps.get("b")!.has("a"));
62
- });
63
-
64
- it("extracts deps from gate check", () => {
65
- const spec = makeSpec({
66
- a: { agent: "foo" },
67
- b: {
68
- gate: { check: "${{ steps.a.output.reproCommand }}" },
69
- },
70
- });
71
- const deps = extractDependencies(spec);
72
- assert.ok(deps.get("b")!.has("a"));
73
- });
74
-
75
- it("extracts deps from transform mapping", () => {
76
- const spec = makeSpec({
77
- a: { agent: "foo" },
78
- b: { agent: "bar" },
79
- c: {
80
- transform: {
81
- mapping: {
82
- fromA: "${{ steps.a.textOutput }}",
83
- fromB: "${{ steps.b.output.data }}",
84
- },
85
- },
86
- },
87
- });
88
- const deps = extractDependencies(spec);
89
- assert.ok(deps.get("c")!.has("a"));
90
- assert.ok(deps.get("c")!.has("b"));
91
- });
92
-
93
- it("extracts multiple deps from a single expression string", () => {
94
- const spec = makeSpec({
95
- a: { agent: "foo" },
96
- b: { agent: "bar" },
97
- c: {
98
- agent: "baz",
99
- input: {
100
- combined: "${{ steps.a.textOutput }}\n\n${{ steps.b.textOutput }}",
101
- },
102
- },
103
- });
104
- const deps = extractDependencies(spec);
105
- assert.ok(deps.get("c")!.has("a"));
106
- assert.ok(deps.get("c")!.has("b"));
107
- });
108
-
109
- it("extracts deps from nested input objects", () => {
110
- const spec = makeSpec({
111
- a: { agent: "foo" },
112
- b: {
113
- agent: "bar",
114
- input: {
115
- nested: {
116
- deep: {
117
- value: "${{ steps.a.output.x }}",
118
- },
119
- },
120
- },
121
- },
122
- });
123
- const deps = extractDependencies(spec);
124
- assert.ok(deps.get("b")!.has("a"));
125
- });
126
-
127
- it("extracts deps from input arrays", () => {
128
- const spec = makeSpec({
129
- a: { agent: "foo" },
130
- b: {
131
- agent: "bar",
132
- input: {
133
- items: ["${{ steps.a.output.x }}", "static"],
134
- },
135
- },
136
- });
137
- const deps = extractDependencies(spec);
138
- assert.ok(deps.get("b")!.has("a"));
139
- });
140
-
141
- it("ignores input-only references (no step deps)", () => {
142
- const spec = makeSpec({
143
- a: {
144
- agent: "foo",
145
- input: {
146
- path: "${{ input.path }}",
147
- },
148
- },
149
- });
150
- const deps = extractDependencies(spec);
151
- assert.strictEqual(deps.get("a")!.size, 0);
152
- });
153
-
154
- it("ignores self-references", () => {
155
- const spec = makeSpec({
156
- a: {
157
- agent: "foo",
158
- input: {
159
- self: "${{ steps.a.output }}",
160
- },
161
- },
162
- });
163
- const deps = extractDependencies(spec);
164
- assert.strictEqual(deps.get("a")!.size, 0);
165
- });
166
-
167
- it("ignores references to non-existent steps", () => {
168
- const spec = makeSpec({
169
- a: {
170
- agent: "foo",
171
- input: {
172
- bad: "${{ steps.nonexistent.output }}",
173
- },
174
- },
175
- });
176
- const deps = extractDependencies(spec);
177
- assert.strictEqual(deps.get("a")!.size, 0);
178
- });
179
-
180
- it("does not descend into loop sub-steps", () => {
181
- const spec = makeSpec({
182
- a: { agent: "foo" },
183
- b: {
184
- loop: {
185
- maxAttempts: 3,
186
- steps: {
187
- inner: {
188
- agent: "bar",
189
- input: { ctx: "${{ steps.a.textOutput }}" },
190
- },
191
- },
192
- },
193
- },
194
- });
195
- const deps = extractDependencies(spec);
196
- // The loop step itself should depend on 'a' via its sub-step's expression,
197
- // BUT per the design: we do NOT descend into loop sub-steps for top-level DAG.
198
- // The loop step's own `input` or `when` would declare its real deps.
199
- assert.strictEqual(deps.get("b")!.size, 0);
200
- });
201
- });
202
-
203
- describe("buildExecutionPlan", () => {
204
- it("fully sequential: linear chain", () => {
205
- const spec = makeSpec({
206
- a: { agent: "foo" },
207
- b: {
208
- agent: "bar",
209
- input: { ctx: "${{ steps.a.textOutput }}" },
210
- },
211
- c: {
212
- agent: "baz",
213
- input: { ctx: "${{ steps.b.output }}" },
214
- },
215
- });
216
- const plan = buildExecutionPlan(spec);
217
- assert.strictEqual(plan.length, 3);
218
- assert.deepStrictEqual(plan[0].steps, ["a"]);
219
- assert.deepStrictEqual(plan[1].steps, ["b"]);
220
- assert.deepStrictEqual(plan[2].steps, ["c"]);
221
- assert.ok(isSequential(plan));
222
- });
223
-
224
- it("fully parallel: no dependencies", () => {
225
- const spec = makeSpec({
226
- a: { agent: "foo", input: { x: "${{ input.path }}" } },
227
- b: { agent: "bar", input: { x: "${{ input.path }}" } },
228
- c: { agent: "baz", input: { x: "${{ input.path }}" } },
229
- });
230
- const plan = buildExecutionPlan(spec);
231
- assert.strictEqual(plan.length, 1);
232
- assert.strictEqual(plan[0].steps.length, 3);
233
- assert.ok(plan[0].steps.includes("a"));
234
- assert.ok(plan[0].steps.includes("b"));
235
- assert.ok(plan[0].steps.includes("c"));
236
- assert.ok(!isSequential(plan));
237
- });
238
-
239
- it("fan-out then merge: diamond pattern", () => {
240
- const spec = makeSpec({
241
- source: { agent: "foo" },
242
- analyzerA: {
243
- agent: "bar",
244
- input: { ctx: "${{ steps.source.textOutput }}" },
245
- },
246
- analyzerB: {
247
- agent: "baz",
248
- input: { ctx: "${{ steps.source.textOutput }}" },
249
- },
250
- merge: {
251
- transform: {
252
- mapping: {
253
- a: "${{ steps.analyzerA.output }}",
254
- b: "${{ steps.analyzerB.output }}",
255
- },
256
- },
257
- },
258
- });
259
- const plan = buildExecutionPlan(spec);
260
- assert.strictEqual(plan.length, 3);
261
- assert.deepStrictEqual(plan[0].steps, ["source"]);
262
- assert.strictEqual(plan[1].steps.length, 2);
263
- assert.ok(plan[1].steps.includes("analyzerA"));
264
- assert.ok(plan[1].steps.includes("analyzerB"));
265
- assert.deepStrictEqual(plan[2].steps, ["merge"]);
266
- });
267
-
268
- it("mixed: some parallel, some sequential", () => {
269
- const spec = makeSpec({
270
- a: { agent: "foo" },
271
- b: { agent: "bar" },
272
- c: {
273
- agent: "baz",
274
- input: { ctx: "${{ steps.a.textOutput }}" },
275
- },
276
- d: {
277
- transform: {
278
- mapping: {
279
- b: "${{ steps.b.output }}",
280
- c: "${{ steps.c.output }}",
281
- },
282
- },
283
- },
284
- });
285
- const plan = buildExecutionPlan(spec);
286
- assert.strictEqual(plan.length, 3);
287
- // Layer 0: a, b (both independent)
288
- assert.strictEqual(plan[0].steps.length, 2);
289
- assert.ok(plan[0].steps.includes("a"));
290
- assert.ok(plan[0].steps.includes("b"));
291
- // Layer 1: c (depends on a)
292
- assert.deepStrictEqual(plan[1].steps, ["c"]);
293
- // Layer 2: d (depends on b and c)
294
- assert.deepStrictEqual(plan[2].steps, ["d"]);
295
- });
296
-
297
- it("detects cycle", () => {
298
- const spec = makeSpec({
299
- a: {
300
- agent: "foo",
301
- input: { ctx: "${{ steps.b.output }}" },
302
- },
303
- b: {
304
- agent: "bar",
305
- input: { ctx: "${{ steps.a.output }}" },
306
- },
307
- });
308
- assert.throws(
309
- () => buildExecutionPlan(spec),
310
- (err: Error) => err.message.includes("cycle"),
311
- );
312
- });
313
-
314
- it("single step", () => {
315
- const spec = makeSpec({
316
- only: { agent: "foo" },
317
- });
318
- const plan = buildExecutionPlan(spec);
319
- assert.strictEqual(plan.length, 1);
320
- assert.deepStrictEqual(plan[0].steps, ["only"]);
321
- assert.ok(isSequential(plan));
322
- });
323
-
324
- it("preserves YAML order within layers", () => {
325
- // Steps with no deps should appear in their YAML declaration order
326
- const spec = makeSpec({
327
- z: { agent: "foo" },
328
- a: { agent: "bar" },
329
- m: { agent: "baz" },
330
- });
331
- const plan = buildExecutionPlan(spec);
332
- assert.strictEqual(plan.length, 1);
333
- // Should preserve insertion order: z, a, m
334
- assert.deepStrictEqual(plan[0].steps, ["z", "a", "m"]);
335
- });
336
-
337
- it("step with when referencing another step", () => {
338
- const spec = makeSpec({
339
- check: { gate: { check: "echo ok" } },
340
- conditional: {
341
- agent: "foo",
342
- when: "${{ steps.check.output.passed }}",
343
- },
344
- });
345
- const plan = buildExecutionPlan(spec);
346
- assert.strictEqual(plan.length, 2);
347
- assert.deepStrictEqual(plan[0].steps, ["check"]);
348
- assert.deepStrictEqual(plan[1].steps, ["conditional"]);
349
- });
350
- });