@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/dag.ts DELETED
@@ -1,219 +0,0 @@
1
- /**
2
- * DAG dependency analysis and execution planning for workflow steps.
3
- * Extracts `${{ steps.X }}` references, builds a dependency graph,
4
- * and produces layered execution plans for parallel dispatch.
5
- */
6
- import type { WorkflowSpec, StepSpec } from "./types.ts";
7
-
8
- /**
9
- * A single layer in the execution plan.
10
- * All steps in a layer are independent and can run concurrently.
11
- */
12
- export interface ExecutionLayer {
13
- steps: string[]; // step names in this layer
14
- }
15
-
16
- /**
17
- * The full execution plan: an ordered list of layers.
18
- * Steps in the same layer have no mutual dependencies.
19
- * Steps in layer N depend only on steps in layers 0..N-1.
20
- */
21
- export type ExecutionPlan = ExecutionLayer[];
22
-
23
- /**
24
- * Extract all step dependencies from a workflow spec.
25
- *
26
- * Scans all expression-bearing fields in each step:
27
- * - `input` values (recursive — expressions can be nested in objects/arrays)
28
- * - `when` condition
29
- * - `gate.check` (may contain ${{ }})
30
- * - `transform.mapping` values (recursive)
31
- * - `loop.attempts` (may be an expression)
32
- *
33
- * Does NOT descend into loop sub-steps — a loop step's internal steps
34
- * are the loop's own concern, not part of the top-level DAG.
35
- *
36
- * Returns a map: stepName → Set of step names it depends on.
37
- */
38
- export function extractDependencies(spec: WorkflowSpec): Map<string, Set<string>> {
39
- const stepNames = new Set(Object.keys(spec.steps));
40
- const deps = new Map<string, Set<string>>();
41
-
42
- for (const [name, step] of Object.entries(spec.steps)) {
43
- const stepDeps = new Set<string>();
44
-
45
- // Collect all expression strings from this step
46
- const expressions = collectExpressions(step);
47
-
48
- // Parse each expression for `steps.<name>` references
49
- for (const expr of expressions) {
50
- const referenced = extractStepReferences(expr, stepNames);
51
- for (const ref of referenced) {
52
- if (ref !== name) { // no self-dependencies
53
- stepDeps.add(ref);
54
- }
55
- }
56
- }
57
-
58
- deps.set(name, stepDeps);
59
- }
60
-
61
- return deps;
62
- }
63
-
64
- /**
65
- * Collect all raw expression strings from a step spec.
66
- * Walks `input`, `when`, `gate.check`, `transform.mapping`, `loop.attempts`.
67
- */
68
- function collectExpressions(step: StepSpec): string[] {
69
- const exprs: string[] = [];
70
-
71
- // input values (recursive)
72
- if (step.input) {
73
- collectExpressionsFromValue(step.input, exprs);
74
- }
75
-
76
- // when condition
77
- if (step.when) {
78
- exprs.push(step.when);
79
- }
80
-
81
- // gate check
82
- if (step.gate) {
83
- collectExpressionsFromValue(step.gate.check, exprs);
84
- }
85
-
86
- // transform mapping (recursive)
87
- if (step.transform) {
88
- collectExpressionsFromValue(step.transform.mapping, exprs);
89
- }
90
-
91
- // loop attempts expression
92
- if (step.loop?.attempts) {
93
- exprs.push(step.loop.attempts);
94
- }
95
-
96
- // forEach expression
97
- if (step.forEach) {
98
- collectExpressionsFromValue(step.forEach, exprs);
99
- }
100
-
101
- // command expression (may contain ${{ }} references)
102
- if (step.command) {
103
- collectExpressionsFromValue(step.command, exprs);
104
- }
105
-
106
- return exprs;
107
- }
108
-
109
- /**
110
- * Recursively collect expression strings from a value.
111
- * Walks objects, arrays, and strings looking for ${{ }} patterns.
112
- */
113
- function collectExpressionsFromValue(value: unknown, exprs: string[]): void {
114
- if (typeof value === "string") {
115
- // Extract all ${{ ... }} patterns from the string
116
- const regex = /\$\{\{([^}]+)\}\}/g;
117
- let match: RegExpExecArray | null;
118
- while ((match = regex.exec(value)) !== null) {
119
- exprs.push(match[1].trim());
120
- }
121
- // Also handle bare expressions (no ${{ }} wrapper, e.g. in `when`)
122
- if (!value.includes("${{") && value.includes("steps.")) {
123
- exprs.push(value);
124
- }
125
- } else if (Array.isArray(value)) {
126
- for (const item of value) {
127
- collectExpressionsFromValue(item, exprs);
128
- }
129
- } else if (value !== null && typeof value === "object") {
130
- for (const v of Object.values(value)) {
131
- collectExpressionsFromValue(v, exprs);
132
- }
133
- }
134
- }
135
-
136
- /**
137
- * Extract step name references from an expression string.
138
- *
139
- * Looks for patterns like:
140
- * - `steps.diagnose.output`
141
- * - `steps.diagnose.textOutput`
142
- * - `steps.diagnose.status`
143
- *
144
- * Returns the set of step names referenced.
145
- */
146
- function extractStepReferences(expr: string, validStepNames: Set<string>): Set<string> {
147
- const refs = new Set<string>();
148
- // Match `steps.<name>` — name is a word (alphanumeric + underscore + hyphen)
149
- const regex = /steps\.([a-zA-Z_][\w-]*)/g;
150
- let match: RegExpExecArray | null;
151
- while ((match = regex.exec(expr)) !== null) {
152
- const name = match[1];
153
- if (validStepNames.has(name)) {
154
- refs.add(name);
155
- }
156
- }
157
- return refs;
158
- }
159
-
160
- /**
161
- * Build an execution plan from a pre-computed dependency map.
162
- * Performs topological sort, grouping independent steps into layers.
163
- * Throws if the dependency graph contains a cycle.
164
- */
165
- export function buildPlanFromDeps(
166
- allSteps: string[],
167
- deps: Map<string, Set<string>>,
168
- ): ExecutionPlan {
169
- const plan: ExecutionPlan = [];
170
- const placed = new Set<string>();
171
-
172
- while (placed.size < allSteps.length) {
173
- const layer: string[] = [];
174
-
175
- for (const step of allSteps) {
176
- if (placed.has(step)) continue;
177
-
178
- const stepDeps = deps.get(step) ?? new Set();
179
- if ([...stepDeps].every((d) => placed.has(d))) {
180
- layer.push(step);
181
- }
182
- }
183
-
184
- if (layer.length === 0) {
185
- const remaining = allSteps.filter((s) => !placed.has(s));
186
- throw new Error(
187
- `Dependency cycle detected among steps: ${remaining.join(", ")}`,
188
- );
189
- }
190
-
191
- plan.push({ steps: layer });
192
- for (const s of layer) {
193
- placed.add(s);
194
- }
195
- }
196
-
197
- return plan;
198
- }
199
-
200
- /**
201
- * Build an execution plan from a workflow spec.
202
- *
203
- * Performs topological sort, grouping independent steps into layers.
204
- * Steps within a layer can execute concurrently.
205
- *
206
- * Throws if the dependency graph contains a cycle.
207
- */
208
- export function buildExecutionPlan(spec: WorkflowSpec): ExecutionPlan {
209
- const deps = extractDependencies(spec);
210
- return buildPlanFromDeps(Object.keys(spec.steps), deps);
211
- }
212
-
213
- /**
214
- * Check if an execution plan is fully sequential
215
- * (every layer has exactly one step).
216
- */
217
- export function isSequential(plan: ExecutionPlan): boolean {
218
- return plan.every((layer) => layer.steps.length === 1);
219
- }
@@ -1,475 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { dispatch, buildArgs, extractText, extractToolArgsPreview } from "./dispatch.ts";
4
- import type { StepSpec, AgentSpec, StepResult } from "./types.ts";
5
- import fs from "node:fs";
6
- import path from "node:path";
7
- import os from "node:os";
8
-
9
- // Skip integration tests unless RUN_INTEGRATION=1 and pi is available
10
- let hasPi = false;
11
- if (process.env.RUN_INTEGRATION === "1") {
12
- try {
13
- const { execSync } = await import("node:child_process");
14
- execSync("pi --version", { stdio: "ignore" });
15
- hasPi = true;
16
- } catch {}
17
- }
18
-
19
- // ── Unit tests: extractText ──
20
-
21
- describe("extractText", () => {
22
- it("extracts text from a content array with a text block", () => {
23
- const content = [{ type: "text", text: "Hello world" }];
24
- assert.strictEqual(extractText(content), "Hello world");
25
- });
26
-
27
- it("returns empty string for empty array", () => {
28
- assert.strictEqual(extractText([]), "");
29
- });
30
-
31
- it("returns empty string for null/undefined", () => {
32
- assert.strictEqual(extractText(null), "");
33
- assert.strictEqual(extractText(undefined), "");
34
- });
35
-
36
- it("returns empty string for non-array", () => {
37
- assert.strictEqual(extractText("just a string"), "");
38
- assert.strictEqual(extractText(42), "");
39
- });
40
-
41
- it("returns first text block when multiple exist", () => {
42
- const content = [
43
- { type: "text", text: "First" },
44
- { type: "text", text: "Second" },
45
- ];
46
- assert.strictEqual(extractText(content), "First");
47
- });
48
-
49
- it("skips non-text blocks", () => {
50
- const content = [
51
- { type: "tool_use", name: "bash" },
52
- { type: "text", text: "After tool" },
53
- ];
54
- assert.strictEqual(extractText(content), "After tool");
55
- });
56
-
57
- it("returns empty string for array with no text blocks", () => {
58
- const content = [
59
- { type: "tool_use", name: "bash" },
60
- { type: "image", source: {} },
61
- ];
62
- assert.strictEqual(extractText(content), "");
63
- });
64
- });
65
-
66
- // ── Unit tests: extractToolArgsPreview ──
67
-
68
- describe("extractToolArgsPreview", () => {
69
- it("returns empty string for null/undefined", () => {
70
- assert.strictEqual(extractToolArgsPreview(null), "");
71
- assert.strictEqual(extractToolArgsPreview(undefined), "");
72
- });
73
-
74
- it("returns empty string for non-object", () => {
75
- assert.strictEqual(extractToolArgsPreview("string"), "");
76
- assert.strictEqual(extractToolArgsPreview(42), "");
77
- });
78
-
79
- it("extracts command arg", () => {
80
- assert.strictEqual(extractToolArgsPreview({ command: "ls -la" }), "ls -la");
81
- });
82
-
83
- it("extracts path arg", () => {
84
- assert.strictEqual(extractToolArgsPreview({ path: "/foo/bar.ts" }), "/foo/bar.ts");
85
- });
86
-
87
- it("extracts pattern arg", () => {
88
- assert.strictEqual(extractToolArgsPreview({ pattern: "*.ts" }), "*.ts");
89
- });
90
-
91
- it("extracts query arg", () => {
92
- assert.strictEqual(extractToolArgsPreview({ query: "SELECT 1" }), "SELECT 1");
93
- });
94
-
95
- it("extracts task arg", () => {
96
- assert.strictEqual(extractToolArgsPreview({ task: "do something" }), "do something");
97
- });
98
-
99
- it("prefers command over other keys", () => {
100
- assert.strictEqual(
101
- extractToolArgsPreview({ command: "npm test", path: "/foo" }),
102
- "npm test",
103
- );
104
- });
105
-
106
- it("truncates long values to 60 chars", () => {
107
- const longVal = "a".repeat(100);
108
- const result = extractToolArgsPreview({ command: longVal });
109
- assert.strictEqual(result.length, 60);
110
- assert.ok(result.endsWith("..."));
111
- assert.strictEqual(result, "a".repeat(57) + "...");
112
- });
113
-
114
- it("does not truncate values at exactly 60 chars", () => {
115
- const val60 = "b".repeat(60);
116
- assert.strictEqual(extractToolArgsPreview({ command: val60 }), val60);
117
- });
118
-
119
- it("returns empty string when no recognized keys present", () => {
120
- assert.strictEqual(extractToolArgsPreview({ foo: "bar", baz: 42 }), "");
121
- });
122
-
123
- it("skips non-string values for recognized keys", () => {
124
- assert.strictEqual(extractToolArgsPreview({ command: 123, path: true }), "");
125
- });
126
- });
127
-
128
- // ── Unit tests: buildArgs ──
129
-
130
- describe("buildArgs", () => {
131
- const baseOptions = {
132
- cwd: "/tmp",
133
- sessionLogDir: "/tmp/sessions",
134
- stepName: "test-step",
135
- };
136
-
137
- it("produces minimal args for a bare step and agent", () => {
138
- const step: StepSpec = { agent: "default" };
139
- const agent: AgentSpec = { name: "default" };
140
- const args = buildArgs(step, agent, "hello", baseOptions);
141
-
142
- assert.deepStrictEqual(args.slice(0, 2), ["--mode", "json"]);
143
- assert.ok(args.includes("--session-dir"));
144
- assert.ok(args.includes("/tmp/sessions"));
145
- // Ends with -p hello
146
- assert.strictEqual(args[args.length - 2], "-p");
147
- assert.strictEqual(args[args.length - 1], "hello");
148
- // No model args
149
- assert.ok(!args.includes("--models"));
150
- });
151
-
152
- it("includes model from agent spec", () => {
153
- const step: StepSpec = { agent: "coder" };
154
- const agent: AgentSpec = { name: "coder", model: "claude-sonnet-4-20250514" };
155
- const args = buildArgs(step, agent, "do it", baseOptions);
156
-
157
- const idx = args.indexOf("--models");
158
- assert.ok(idx >= 0);
159
- assert.strictEqual(args[idx + 1], "claude-sonnet-4-20250514:off");
160
- });
161
-
162
- it("step model overrides agent model", () => {
163
- const step: StepSpec = { agent: "coder", model: "claude-opus-4-20250514" };
164
- const agent: AgentSpec = { name: "coder", model: "claude-sonnet-4-20250514" };
165
- const args = buildArgs(step, agent, "do it", baseOptions);
166
-
167
- const idx = args.indexOf("--models");
168
- assert.strictEqual(args[idx + 1], "claude-opus-4-20250514:off");
169
- });
170
-
171
- it("appends thinking suffix to model", () => {
172
- const step: StepSpec = { agent: "coder" };
173
- const agent: AgentSpec = { name: "coder", model: "claude-sonnet-4-20250514", thinking: "16k" };
174
- const args = buildArgs(step, agent, "do it", baseOptions);
175
-
176
- const idx = args.indexOf("--models");
177
- assert.strictEqual(args[idx + 1], "claude-sonnet-4-20250514:16k");
178
- });
179
-
180
- it("resolves model from modelConfig by role when agent has no model", () => {
181
- const step: StepSpec = { agent: "coder" };
182
- const agent: AgentSpec = { name: "coder", role: "action" };
183
- const opts = { ...baseOptions, modelConfig: { default: "fallback-model", by_role: { action: "opus-4" } } };
184
- const args = buildArgs(step, agent, "do it", opts);
185
-
186
- const idx = args.indexOf("--models");
187
- assert.ok(idx >= 0);
188
- assert.strictEqual(args[idx + 1], "opus-4:off");
189
- });
190
-
191
- it("resolves model from modelConfig default when no role match", () => {
192
- const step: StepSpec = { agent: "coder" };
193
- const agent: AgentSpec = { name: "coder", role: "unknown-role" };
194
- const opts = { ...baseOptions, modelConfig: { default: "fallback-model", by_role: { action: "opus-4" } } };
195
- const args = buildArgs(step, agent, "do it", opts);
196
-
197
- const idx = args.indexOf("--models");
198
- assert.ok(idx >= 0);
199
- assert.strictEqual(args[idx + 1], "fallback-model:off");
200
- });
201
-
202
- it("step model overrides modelConfig", () => {
203
- const step: StepSpec = { agent: "coder", model: "step-override" };
204
- const agent: AgentSpec = { name: "coder", role: "action" };
205
- const opts = { ...baseOptions, modelConfig: { default: "fallback-model", by_role: { action: "opus-4" } } };
206
- const args = buildArgs(step, agent, "do it", opts);
207
-
208
- const idx = args.indexOf("--models");
209
- assert.strictEqual(args[idx + 1], "step-override:off");
210
- });
211
-
212
- it("agent model overrides modelConfig", () => {
213
- const step: StepSpec = { agent: "coder" };
214
- const agent: AgentSpec = { name: "coder", model: "agent-model", role: "action" };
215
- const opts = { ...baseOptions, modelConfig: { default: "fallback-model", by_role: { action: "opus-4" } } };
216
- const args = buildArgs(step, agent, "do it", opts);
217
-
218
- const idx = args.indexOf("--models");
219
- assert.strictEqual(args[idx + 1], "agent-model:off");
220
- });
221
-
222
- it("handles builtin tools", () => {
223
- const step: StepSpec = { agent: "coder" };
224
- const agent: AgentSpec = { name: "coder", tools: ["bash", "read", "write"] };
225
- const args = buildArgs(step, agent, "do it", baseOptions);
226
-
227
- const idx = args.indexOf("--tools");
228
- assert.ok(idx >= 0);
229
- assert.strictEqual(args[idx + 1], "bash,read,write");
230
- });
231
-
232
- it("handles extension tool paths", () => {
233
- const step: StepSpec = { agent: "coder" };
234
- const agent: AgentSpec = { name: "coder", tools: ["bash", "./ext/my-tool.ts", "/abs/tool.js"] };
235
- const args = buildArgs(step, agent, "do it", baseOptions);
236
-
237
- const toolsIdx = args.indexOf("--tools");
238
- assert.ok(toolsIdx >= 0);
239
- assert.strictEqual(args[toolsIdx + 1], "bash");
240
-
241
- // Should have two --extension entries for the paths
242
- const extIndices = args.reduce<number[]>((acc, a, i) => {
243
- if (a === "--extension") acc.push(i);
244
- return acc;
245
- }, []);
246
- assert.strictEqual(extIndices.length, 2);
247
- assert.strictEqual(args[extIndices[0] + 1], "./ext/my-tool.ts");
248
- assert.strictEqual(args[extIndices[1] + 1], "/abs/tool.js");
249
- });
250
-
251
- it("handles extensions scoping", () => {
252
- const step: StepSpec = { agent: "coder" };
253
- const agent: AgentSpec = { name: "coder", extensions: ["./ext1.ts", "./ext2.ts"] };
254
- const args = buildArgs(step, agent, "do it", baseOptions);
255
-
256
- assert.ok(args.includes("--no-extensions"));
257
- const extIndices = args.reduce<number[]>((acc, a, i) => {
258
- if (a === "--extension") acc.push(i);
259
- return acc;
260
- }, []);
261
- assert.strictEqual(extIndices.length, 2);
262
- });
263
-
264
- it("handles skills scoping", () => {
265
- const step: StepSpec = { agent: "coder" };
266
- const agent: AgentSpec = { name: "coder", skills: ["coding", "testing"] };
267
- const args = buildArgs(step, agent, "do it", baseOptions);
268
-
269
- assert.ok(args.includes("--no-skills"));
270
- });
271
- });
272
-
273
- // ── Integration tests: dispatch (require pi on PATH) ──
274
-
275
- describe("dispatch", { skip: !hasPi ? "pi not available" : undefined }, () => {
276
- it("runs a simple prompt and returns text output", async () => {
277
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-dispatch-"));
278
- const sessDir = path.join(tmpDir, "sessions");
279
- fs.mkdirSync(sessDir, { recursive: true });
280
-
281
- const result = await dispatch(
282
- { agent: "default" }, // no specific agent — use pi defaults
283
- { name: "default" }, // minimal agent spec
284
- "Respond with exactly: HELLO WORKFLOW",
285
- { cwd: process.cwd(), sessionLogDir: sessDir, stepName: "test" },
286
- );
287
-
288
- assert.strictEqual(result.status, "completed");
289
- assert.ok(result.textOutput.includes("HELLO WORKFLOW"));
290
- assert.ok(result.usage.turns >= 1);
291
- assert.ok(result.durationMs > 0);
292
-
293
- fs.rmSync(tmpDir, { recursive: true });
294
- }, { timeout: 60000 });
295
-
296
- it("returns failed status on bad prompt/agent", async () => {
297
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-dispatch-"));
298
- const sessDir = path.join(tmpDir, "sessions");
299
- fs.mkdirSync(sessDir, { recursive: true });
300
-
301
- // --tools none should cause pi to fail or at least limit capabilities
302
- const result = await dispatch(
303
- { agent: "nonexistent-agent-xyz" },
304
- { name: "nonexistent-agent-xyz" },
305
- "This should fail",
306
- { cwd: process.cwd(), sessionLogDir: sessDir, stepName: "test" },
307
- );
308
-
309
- // Exact failure mode depends on pi behavior with unknown agents
310
- // At minimum we should get a result back (not throw)
311
- assert.ok(result.durationMs > 0);
312
-
313
- fs.rmSync(tmpDir, { recursive: true });
314
- }, { timeout: 30000 });
315
-
316
- it("supports cancellation via AbortSignal", async () => {
317
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-dispatch-"));
318
- const sessDir = path.join(tmpDir, "sessions");
319
- fs.mkdirSync(sessDir, { recursive: true });
320
-
321
- const controller = new AbortController();
322
-
323
- // Cancel after 2 seconds
324
- setTimeout(() => controller.abort(), 2000);
325
-
326
- const result = await dispatch(
327
- { agent: "default" },
328
- { name: "default" },
329
- "Write a very long essay about the history of computing. Make it at least 5000 words.",
330
- { cwd: process.cwd(), sessionLogDir: sessDir, stepName: "test", signal: controller.signal },
331
- );
332
-
333
- assert.strictEqual(result.status, "failed");
334
-
335
- fs.rmSync(tmpDir, { recursive: true });
336
- }, { timeout: 30000 });
337
-
338
- it("fires onEvent callback for process events", async () => {
339
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-dispatch-"));
340
- const sessDir = path.join(tmpDir, "sessions");
341
- fs.mkdirSync(sessDir, { recursive: true });
342
-
343
- const events: string[] = [];
344
-
345
- const result = await dispatch(
346
- { agent: "default" },
347
- { name: "default" },
348
- "Respond with exactly: TEST",
349
- {
350
- cwd: process.cwd(),
351
- sessionLogDir: sessDir,
352
- stepName: "test",
353
- onEvent: (evt) => events.push(evt.type),
354
- },
355
- );
356
-
357
- assert.ok(events.includes("agent_start"));
358
- assert.ok(events.includes("message_end"));
359
-
360
- fs.rmSync(tmpDir, { recursive: true });
361
- }, { timeout: 60000 });
362
- });
363
-
364
- // ── Unit tests: StepResult truncation fields ──
365
-
366
- describe("StepResult truncation fields", () => {
367
- it("truncated and warnings are optional on StepResult", () => {
368
- // Verify the type contract: a StepResult without truncated/warnings is valid
369
- const result: StepResult = {
370
- step: "test",
371
- agent: "agent",
372
- status: "completed",
373
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
374
- durationMs: 0,
375
- };
376
- assert.strictEqual(result.truncated, undefined);
377
- assert.strictEqual(result.warnings, undefined);
378
-
379
- // A truncated result is also valid
380
- const truncResult: StepResult = {
381
- ...result,
382
- truncated: true,
383
- warnings: ["Stdout exceeded 10MB limit"],
384
- };
385
- assert.strictEqual(truncResult.truncated, true);
386
- assert.strictEqual(truncResult.warnings!.length, 1);
387
- });
388
- });
389
-
390
- // ── Unit tests: truncation detection logic ──
391
-
392
- describe("truncation detection logic", () => {
393
- it("sets truncated flag when accumulated bytes exceed threshold", () => {
394
- // Simulate the accumulation logic from dispatch's data handler
395
- const MAX = 100; // small threshold for testing
396
- let bufBytes = 0;
397
- let stdoutTruncated = false;
398
- const processedLines: string[] = [];
399
- let buf = "";
400
-
401
- function handleChunk(chunk: string) {
402
- bufBytes += Buffer.byteLength(chunk);
403
- if (bufBytes > MAX) {
404
- if (!stdoutTruncated) {
405
- stdoutTruncated = true;
406
- if (buf.trim()) {
407
- processedLines.push(buf.trim());
408
- buf = "";
409
- }
410
- }
411
- return;
412
- }
413
- buf += chunk;
414
- const lines = buf.split("\n");
415
- buf = lines.pop() || "";
416
- for (const line of lines) {
417
- if (line.trim()) processedLines.push(line.trim());
418
- }
419
- }
420
-
421
- // Send chunks that total over 100 bytes
422
- handleChunk('{"type":"event1"}\n'); // ~18 bytes
423
- handleChunk('{"type":"event2"}\n'); // ~36 total
424
- handleChunk('{"type":"event3"}\n'); // ~54 total
425
- handleChunk('{"type":"event4"}\n'); // ~72 total
426
- handleChunk('{"type":"event5"}\n'); // ~90 total
427
- handleChunk('{"type":"event6"}\n'); // ~108 total — exceeds 100
428
-
429
- assert.strictEqual(stdoutTruncated, true);
430
- // Events before the threshold should have been processed
431
- assert.ok(processedLines.length >= 4);
432
- assert.ok(processedLines.length < 6, "Post-threshold events should not be processed");
433
- });
434
-
435
- it("does not set truncated flag when under threshold", () => {
436
- const MAX = 10000;
437
- let bufBytes = 0;
438
- let stdoutTruncated = false;
439
-
440
- function handleChunk(chunk: string) {
441
- bufBytes += Buffer.byteLength(chunk);
442
- if (bufBytes > MAX) {
443
- stdoutTruncated = true;
444
- return;
445
- }
446
- }
447
-
448
- handleChunk('{"type":"small"}\n');
449
- assert.strictEqual(stdoutTruncated, false);
450
- });
451
- });
452
-
453
- // ── Integration test: dispatch truncation contract ──
454
-
455
- describe("dispatch truncation", { skip: !hasPi ? "pi not available" : undefined }, () => {
456
- it("sets truncated flag when stdout exceeds buffer limit", async () => {
457
- // A normal-sized response should NOT have truncated set.
458
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-dispatch-trunc-"));
459
- const sessDir = path.join(tmpDir, "sessions");
460
- fs.mkdirSync(sessDir, { recursive: true });
461
-
462
- const result = await dispatch(
463
- { agent: "default" },
464
- { name: "default" },
465
- "Respond with exactly: OK",
466
- { cwd: process.cwd(), sessionLogDir: sessDir, stepName: "test" },
467
- );
468
-
469
- assert.strictEqual(result.status, "completed");
470
- assert.strictEqual(result.truncated, undefined, "Normal response should not be truncated");
471
- assert.strictEqual(result.warnings, undefined, "Normal response should have no warnings");
472
-
473
- fs.rmSync(tmpDir, { recursive: true });
474
- }, { timeout: 60000 });
475
- });