gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.97e9e30

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 (115) hide show
  1. package/dist/resources/extensions/gsd/auto/loop.js +80 -0
  2. package/dist/resources/extensions/gsd/auto/phases.js +2 -2
  3. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  4. package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
  5. package/dist/resources/extensions/gsd/auto.js +28 -1
  6. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
  7. package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
  8. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
  9. package/dist/resources/extensions/gsd/context-injector.js +74 -0
  10. package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
  11. package/dist/resources/extensions/gsd/custom-verification.js +145 -0
  12. package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
  14. package/dist/resources/extensions/gsd/definition-loader.js +352 -0
  15. package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
  16. package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
  17. package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
  18. package/dist/resources/extensions/gsd/engine-types.js +8 -0
  19. package/dist/resources/extensions/gsd/execution-policy.js +8 -0
  20. package/dist/resources/extensions/gsd/graph.js +225 -0
  21. package/dist/resources/extensions/gsd/run-manager.js +134 -0
  22. package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
  23. package/dist/resources/skills/create-workflow/SKILL.md +103 -0
  24. package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  25. package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
  26. package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  27. package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  28. package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  29. package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  30. package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  31. package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  32. package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  61. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  62. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  63. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  64. package/package.json +1 -1
  65. package/packages/pi-coding-agent/package.json +1 -1
  66. package/pkg/package.json +1 -1
  67. package/src/resources/extensions/gsd/auto/loop.ts +91 -0
  68. package/src/resources/extensions/gsd/auto/phases.ts +2 -2
  69. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  70. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  71. package/src/resources/extensions/gsd/auto.ts +31 -1
  72. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
  73. package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
  74. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
  75. package/src/resources/extensions/gsd/context-injector.ts +100 -0
  76. package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
  77. package/src/resources/extensions/gsd/custom-verification.ts +180 -0
  78. package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
  79. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
  80. package/src/resources/extensions/gsd/definition-loader.ts +462 -0
  81. package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
  82. package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
  83. package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
  84. package/src/resources/extensions/gsd/engine-types.ts +71 -0
  85. package/src/resources/extensions/gsd/execution-policy.ts +43 -0
  86. package/src/resources/extensions/gsd/graph.ts +312 -0
  87. package/src/resources/extensions/gsd/run-manager.ts +180 -0
  88. package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
  89. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
  90. package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
  91. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
  92. package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
  93. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
  94. package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
  95. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
  96. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
  97. package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
  98. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
  99. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
  100. package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
  101. package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
  102. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
  103. package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
  104. package/src/resources/skills/create-workflow/SKILL.md +103 -0
  105. package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  106. package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
  107. package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  108. package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  109. package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  110. package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  111. package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  112. package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  113. package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  114. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_buildManifest.js +0 -0
  115. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_ssgManifest.js +0 -0
@@ -0,0 +1,476 @@
1
+ /**
2
+ * e2e-workflow-pipeline-integration.test.ts — End-to-end integration test
3
+ * proving the assembled workflow engine pipeline works.
4
+ *
5
+ * Exercises every engine feature in a single multi-step workflow:
6
+ * - Dependency-ordered dispatch
7
+ * - Parameter substitution ({{target}})
8
+ * - Content-heuristic verification (minSize)
9
+ * - Shell-command verification (test -f)
10
+ * - Context injection via context_from
11
+ * - Iterate/fan-out expansion
12
+ * - Dashboard metadata (step N/M)
13
+ * - Completion detection (isComplete: true)
14
+ *
15
+ * Operates at the engine level (CustomWorkflowEngine + CustomExecutionPolicy
16
+ * + real temp directories) — NOT through autoLoop() — to avoid the
17
+ * timing-dependent resolveAgentEnd pattern that causes flakiness.
18
+ *
19
+ * Follows the pattern from iterate-engine-integration.test.ts:
20
+ * real temp dirs via mkdtempSync, dispatch()/reconcile() helpers, afterEach cleanup.
21
+ */
22
+
23
+ import { describe, it, afterEach } from "node:test";
24
+ import assert from "node:assert/strict";
25
+ import {
26
+ mkdtempSync,
27
+ rmSync,
28
+ writeFileSync,
29
+ mkdirSync,
30
+ readFileSync,
31
+ existsSync,
32
+ } from "node:fs";
33
+ import { join } from "node:path";
34
+ import { tmpdir } from "node:os";
35
+ import { stringify, parse } from "yaml";
36
+
37
+ import { CustomWorkflowEngine } from "../custom-workflow-engine.ts";
38
+ import { CustomExecutionPolicy } from "../custom-execution-policy.ts";
39
+ import { createRun, listRuns } from "../run-manager.ts";
40
+ import { readGraph, writeGraph } from "../graph.ts";
41
+ import { validateDefinition } from "../definition-loader.ts";
42
+
43
+ // ─── Helpers ─────────────────────────────────────────────────────────────
44
+
45
+ const tmpDirs: string[] = [];
46
+
47
+ function makeTmpDir(): string {
48
+ const dir = mkdtempSync(join(tmpdir(), "e2e-pipeline-"));
49
+ tmpDirs.push(dir);
50
+ return dir;
51
+ }
52
+
53
+ afterEach(() => {
54
+ for (const d of tmpDirs) {
55
+ try { rmSync(d, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
56
+ }
57
+ tmpDirs.length = 0;
58
+ });
59
+
60
+ /** Drive deriveState → resolveDispatch. */
61
+ async function dispatch(engine: CustomWorkflowEngine) {
62
+ const state = await engine.deriveState("/unused");
63
+ return { state, result: engine.resolveDispatch(state, { basePath: "/unused" }) };
64
+ }
65
+
66
+ /** Drive deriveState → reconcile for a given unitId. */
67
+ async function reconcile(engine: CustomWorkflowEngine, unitId: string) {
68
+ const state = await engine.deriveState("/unused");
69
+ return engine.reconcile(state, {
70
+ unitType: "custom-step",
71
+ unitId,
72
+ startedAt: Date.now() - 1000,
73
+ finishedAt: Date.now(),
74
+ });
75
+ }
76
+
77
+ // ─── The multi-feature YAML definition (snake_case for loadDefinition) ───
78
+
79
+ /**
80
+ * 4-step workflow definition exercising every engine feature:
81
+ *
82
+ * gather → scan (iterate) → analyze (context_from scan) → report (context_from analyze)
83
+ *
84
+ * Note: The scan step prompt uses a literal string instead of {{item}} in the
85
+ * definition YAML because substituteParams() checks for unresolved {{key}}
86
+ * placeholders. After createRun, we patch GRAPH.yaml to add the {{item}}
87
+ * placeholder so iterate expansion produces item-specific prompts.
88
+ */
89
+ const E2E_DEFINITION_YAML = `
90
+ version: 1
91
+ name: e2e-pipeline
92
+ description: End-to-end integration test workflow
93
+ params:
94
+ target: default-target
95
+ steps:
96
+ - id: gather
97
+ name: Gather Information
98
+ prompt: "Gather information about {{target}} and produce a bullet list of findings"
99
+ requires: []
100
+ produces:
101
+ - output/gather-results.md
102
+ verify:
103
+ policy: content-heuristic
104
+ minSize: 10
105
+ - id: scan
106
+ name: Scan Items
107
+ prompt: "Scan item: ITEM_PLACEHOLDER"
108
+ requires:
109
+ - gather
110
+ produces:
111
+ - output/scan-result.txt
112
+ verify:
113
+ policy: shell-command
114
+ command: "test -f output/scan-result.txt"
115
+ iterate:
116
+ source: output/gather-results.md
117
+ pattern: "^- (.+)$"
118
+ - id: analyze
119
+ name: Analyze Results
120
+ prompt: "Analyze all scan results and produce a summary"
121
+ requires:
122
+ - scan
123
+ produces:
124
+ - output/analysis.md
125
+ context_from:
126
+ - scan
127
+ verify:
128
+ policy: content-heuristic
129
+ minSize: 5
130
+ - id: report
131
+ name: Final Report
132
+ prompt: "Write final report for {{target}}"
133
+ requires:
134
+ - analyze
135
+ produces:
136
+ - output/report.md
137
+ context_from:
138
+ - analyze
139
+ `;
140
+
141
+ /**
142
+ * Create a temp project directory with the e2e-pipeline definition YAML,
143
+ * call createRun with param overrides, and patch GRAPH.yaml so the scan
144
+ * step's prompt contains {{item}} for iterate expansion.
145
+ */
146
+ function setupProject(overrides?: Record<string, string>): {
147
+ basePath: string;
148
+ runDir: string;
149
+ } {
150
+ const basePath = makeTmpDir();
151
+ const defsDir = join(basePath, ".gsd", "workflow-defs");
152
+ mkdirSync(defsDir, { recursive: true });
153
+ writeFileSync(join(defsDir, "e2e-pipeline.yaml"), E2E_DEFINITION_YAML, "utf-8");
154
+
155
+ const runDir = createRun(basePath, "e2e-pipeline", overrides);
156
+
157
+ // Patch GRAPH.yaml: replace the scan step's placeholder with {{item}}
158
+ // so iterate expansion produces item-specific prompts. This works around
159
+ // substituteParams() rejecting unresolved {{item}} in the definition.
160
+ const graph = readGraph(runDir);
161
+ const scanStep = graph.steps.find((s) => s.id === "scan");
162
+ if (scanStep) {
163
+ scanStep.prompt = "Scan item: {{item}}";
164
+ writeGraph(runDir, graph);
165
+ }
166
+
167
+ return { basePath, runDir };
168
+ }
169
+
170
+ // ─── Tests ───────────────────────────────────────────────────────────────
171
+
172
+ describe("e2e-workflow-pipeline", () => {
173
+ it("drives the full engine pipeline: create → dispatch → verify → complete", async () => {
174
+ // ── 1. Create run with param overrides ────────────────────────────
175
+ const { basePath, runDir } = setupProject({ target: "my-project" });
176
+
177
+ // Verify run directory structure
178
+ assert.ok(existsSync(join(runDir, "DEFINITION.yaml")), "DEFINITION.yaml should exist");
179
+ assert.ok(existsSync(join(runDir, "GRAPH.yaml")), "GRAPH.yaml should exist");
180
+ assert.ok(existsSync(join(runDir, "PARAMS.json")), "PARAMS.json should exist");
181
+
182
+ // Verify PARAMS.json has the override
183
+ const params = JSON.parse(readFileSync(join(runDir, "PARAMS.json"), "utf-8"));
184
+ assert.deepStrictEqual(params, { target: "my-project" });
185
+
186
+ // Verify the frozen DEFINITION.yaml has substituted params in non-iterate steps
187
+ const frozenDef = readFileSync(join(runDir, "DEFINITION.yaml"), "utf-8");
188
+ assert.ok(
189
+ frozenDef.includes("my-project"),
190
+ "Frozen definition should have substituted 'my-project' for {{target}}",
191
+ );
192
+
193
+ // Instantiate engine and policy
194
+ const engine = new CustomWorkflowEngine(runDir);
195
+ const policy = new CustomExecutionPolicy(runDir);
196
+
197
+ // Verify initial graph has 4 steps all pending
198
+ const initialGraph = readGraph(runDir);
199
+ assert.equal(initialGraph.steps.length, 4, "Initial graph should have 4 steps");
200
+ assert.ok(
201
+ initialGraph.steps.every((s) => s.status === "pending"),
202
+ "All steps should start as pending",
203
+ );
204
+
205
+ // Verify initial state is not complete
206
+ let state = await engine.deriveState("/unused");
207
+ assert.equal(state.isComplete, false, "Workflow should not be complete initially");
208
+
209
+ // Dashboard metadata: 0/4 initially
210
+ let meta = engine.getDisplayMetadata(state);
211
+ assert.equal(meta.stepCount!.completed, 0);
212
+ assert.equal(meta.stepCount!.total, 4);
213
+ assert.equal(meta.progressSummary, "Step 0/4");
214
+
215
+ // ── 2. Step 1: gather ─────────────────────────────────────────────
216
+ const { result: r1 } = await dispatch(engine);
217
+ const d1 = await r1;
218
+ assert.equal(d1.action, "dispatch", "Should dispatch gather step");
219
+ if (d1.action !== "dispatch") throw new Error("unreachable");
220
+
221
+ assert.equal(d1.step.unitId, "e2e-pipeline/gather");
222
+ assert.ok(
223
+ d1.step.prompt.includes("my-project"),
224
+ `Gather prompt should contain substituted param "my-project", got: "${d1.step.prompt}"`,
225
+ );
226
+ assert.ok(
227
+ !d1.step.prompt.includes("default-target"),
228
+ "Gather prompt should NOT contain default param value",
229
+ );
230
+
231
+ // Simulate agent work: write the gather artifact with bullet items for iterate
232
+ const outputDir = join(runDir, "output");
233
+ mkdirSync(outputDir, { recursive: true });
234
+ writeFileSync(
235
+ join(runDir, "output/gather-results.md"),
236
+ "# Findings for my-project\n\n- security-audit\n- performance-review\n- code-quality\n",
237
+ "utf-8",
238
+ );
239
+
240
+ // Reconcile gather
241
+ await reconcile(engine, "e2e-pipeline/gather");
242
+
243
+ // Verify gather: content-heuristic (minSize: 10) should pass
244
+ const gatherVerify = await policy.verify("custom-step", "e2e-pipeline/gather", {
245
+ basePath: "/unused",
246
+ });
247
+ assert.equal(
248
+ gatherVerify,
249
+ "continue",
250
+ "Gather verification (content-heuristic) should pass",
251
+ );
252
+
253
+ // Dashboard after gather: 1 completed (gather), total still 4
254
+ state = await engine.deriveState("/unused");
255
+ meta = engine.getDisplayMetadata(state);
256
+ assert.equal(meta.stepCount!.completed, 1);
257
+ assert.equal(meta.progressSummary, "Step 1/4");
258
+ assert.equal(state.isComplete, false);
259
+
260
+ // ── 3. Step 2: scan with iterate ──────────────────────────────────
261
+ // Dispatch should trigger iterate expansion from gather-results.md
262
+ const { result: r2 } = await dispatch(engine);
263
+ const d2 = await r2;
264
+ assert.equal(d2.action, "dispatch", "Should dispatch first scan instance");
265
+ if (d2.action !== "dispatch") throw new Error("unreachable");
266
+
267
+ // First instance should be scan--001 for "security-audit"
268
+ assert.equal(d2.step.unitId, "e2e-pipeline/scan--001");
269
+ assert.ok(
270
+ d2.step.prompt.includes("security-audit"),
271
+ `First scan instance prompt should contain "security-audit", got: "${d2.step.prompt}"`,
272
+ );
273
+
274
+ // Verify graph expanded: parent "scan" is "expanded", 3 instances exist
275
+ let graph = readGraph(runDir);
276
+ const scanParent = graph.steps.find((s) => s.id === "scan");
277
+ assert.ok(scanParent, "Parent scan step should exist");
278
+ assert.equal(scanParent.status, "expanded", "Parent scan should be expanded");
279
+
280
+ const scanInstances = graph.steps.filter((s) => s.parentStepId === "scan");
281
+ assert.equal(scanInstances.length, 3, "Should have 3 scan instances");
282
+ assert.equal(scanInstances[0].id, "scan--001");
283
+ assert.equal(scanInstances[1].id, "scan--002");
284
+ assert.equal(scanInstances[2].id, "scan--003");
285
+
286
+ // Verify iterate prompts contain item-specific content
287
+ assert.ok(scanInstances[0].prompt.includes("security-audit"));
288
+ assert.ok(scanInstances[1].prompt.includes("performance-review"));
289
+ assert.ok(scanInstances[2].prompt.includes("code-quality"));
290
+
291
+ // Verify dependency rewriting: analyze should now depend on scan--001, scan--002, scan--003
292
+ const analyzeStep = graph.steps.find((s) => s.id === "analyze");
293
+ assert.ok(analyzeStep);
294
+ assert.deepStrictEqual(
295
+ analyzeStep.dependsOn.sort(),
296
+ ["scan--001", "scan--002", "scan--003"],
297
+ "Analyze should depend on all scan instances after expansion",
298
+ );
299
+
300
+ // Graph step count increased: 4 original + 3 instances = 7 (parent stays as "expanded")
301
+ assert.equal(graph.steps.length, 7, "Graph should have 7 steps after expansion");
302
+
303
+ // Dashboard after expansion: total now includes instance steps
304
+ state = await engine.deriveState("/unused");
305
+ meta = engine.getDisplayMetadata(state);
306
+ // completed: gather(1), expanded steps don't count as "complete" in getDisplayMetadata
307
+ assert.equal(meta.stepCount!.completed, 1, "Only gather should be complete");
308
+
309
+ // Write scan artifact (same path for all instances since the verify command checks run-dir-relative path)
310
+ writeFileSync(join(runDir, "output/scan-result.txt"), "scan output data", "utf-8");
311
+
312
+ // Complete scan--001, dispatch scan--002
313
+ await reconcile(engine, "e2e-pipeline/scan--001");
314
+
315
+ // Verify analyze is still blocked (not all scan instances complete)
316
+ const { result: r3a } = await dispatch(engine);
317
+ const d3a = await r3a;
318
+ assert.equal(d3a.action, "dispatch");
319
+ if (d3a.action !== "dispatch") throw new Error("unreachable");
320
+ assert.equal(
321
+ d3a.step.unitId,
322
+ "e2e-pipeline/scan--002",
323
+ "Should dispatch scan--002 (analyze still blocked)",
324
+ );
325
+ assert.ok(d3a.step.prompt.includes("performance-review"));
326
+
327
+ // Complete scan--002, dispatch scan--003
328
+ await reconcile(engine, "e2e-pipeline/scan--002");
329
+ const { result: r3b } = await dispatch(engine);
330
+ const d3b = await r3b;
331
+ assert.equal(d3b.action, "dispatch");
332
+ if (d3b.action !== "dispatch") throw new Error("unreachable");
333
+ assert.equal(d3b.step.unitId, "e2e-pipeline/scan--003");
334
+ assert.ok(d3b.step.prompt.includes("code-quality"));
335
+
336
+ // Complete scan--003 — now analyze should be unblocked
337
+ await reconcile(engine, "e2e-pipeline/scan--003");
338
+
339
+ // Dashboard after all scan instances: 4 complete (gather + 3 instances)
340
+ state = await engine.deriveState("/unused");
341
+ meta = engine.getDisplayMetadata(state);
342
+ assert.equal(meta.stepCount!.completed, 4, "gather + 3 scan instances should be complete");
343
+ assert.equal(state.isComplete, false);
344
+
345
+ // ── 4. Step 3: analyze (with context_from scan) ───────────────────
346
+ const { result: r4 } = await dispatch(engine);
347
+ const d4 = await r4;
348
+ assert.equal(d4.action, "dispatch", "Should dispatch analyze step");
349
+ if (d4.action !== "dispatch") throw new Error("unreachable");
350
+
351
+ assert.equal(d4.step.unitId, "e2e-pipeline/analyze");
352
+
353
+ // Context injection: the analyze prompt should include content from scan's produces
354
+ // scan produces output/scan-result.txt and context_from references "scan"
355
+ assert.ok(
356
+ d4.step.prompt.includes("scan output data"),
357
+ `Analyze prompt should include injected context from scan artifact, got: "${d4.step.prompt.slice(0, 200)}"`,
358
+ );
359
+ assert.ok(
360
+ d4.step.prompt.includes("Analyze all scan results"),
361
+ "Analyze prompt should still contain the original prompt text",
362
+ );
363
+
364
+ // Write analyze artifact
365
+ writeFileSync(
366
+ join(runDir, "output/analysis.md"),
367
+ "# Analysis Summary\n\nAll scans completed successfully with findings.\n",
368
+ "utf-8",
369
+ );
370
+
371
+ await reconcile(engine, "e2e-pipeline/analyze");
372
+
373
+ // Verify analyze: content-heuristic (minSize: 5) should pass
374
+ const analyzeVerify = await policy.verify("custom-step", "e2e-pipeline/analyze", {
375
+ basePath: "/unused",
376
+ });
377
+ assert.equal(
378
+ analyzeVerify,
379
+ "continue",
380
+ "Analyze verification (content-heuristic) should pass",
381
+ );
382
+
383
+ // Dashboard after analyze: 5 complete
384
+ state = await engine.deriveState("/unused");
385
+ meta = engine.getDisplayMetadata(state);
386
+ assert.equal(meta.stepCount!.completed, 5);
387
+ assert.equal(state.isComplete, false, "Should not be complete yet (report remaining)");
388
+
389
+ // ── 5. Step 4: report (with context_from analyze + param) ─────────
390
+ const { result: r5 } = await dispatch(engine);
391
+ const d5 = await r5;
392
+ assert.equal(d5.action, "dispatch", "Should dispatch report step");
393
+ if (d5.action !== "dispatch") throw new Error("unreachable");
394
+
395
+ assert.equal(d5.step.unitId, "e2e-pipeline/report");
396
+
397
+ // Context injection: report prompt should include content from analyze's produces
398
+ assert.ok(
399
+ d5.step.prompt.includes("Analysis Summary"),
400
+ `Report prompt should include injected context from analyze artifact, got: "${d5.step.prompt.slice(0, 200)}"`,
401
+ );
402
+
403
+ // Parameter substitution: report prompt should contain "my-project"
404
+ assert.ok(
405
+ d5.step.prompt.includes("my-project"),
406
+ `Report prompt should contain substituted param "my-project", got: "${d5.step.prompt}"`,
407
+ );
408
+
409
+ // Write report artifact
410
+ writeFileSync(
411
+ join(runDir, "output/report.md"),
412
+ "# Final Report for my-project\n\nComprehensive findings documented.\n",
413
+ "utf-8",
414
+ );
415
+
416
+ await reconcile(engine, "e2e-pipeline/report");
417
+
418
+ // ── 6. Completion ─────────────────────────────────────────────────
419
+ state = await engine.deriveState("/unused");
420
+ assert.equal(state.isComplete, true, "Workflow should be complete after all steps");
421
+ assert.equal(state.phase, "complete");
422
+
423
+ // Dashboard: all steps complete
424
+ meta = engine.getDisplayMetadata(state);
425
+ assert.equal(meta.stepCount!.completed, 6, "All 6 dispatchable steps should be complete");
426
+ assert.equal(meta.currentPhase, "complete");
427
+
428
+ // Dispatch should return stop
429
+ const { result: rFinal } = await dispatch(engine);
430
+ const dFinal = await rFinal;
431
+ assert.equal(dFinal.action, "stop");
432
+ if (dFinal.action === "stop") {
433
+ assert.equal(dFinal.reason, "All steps complete");
434
+ }
435
+
436
+ // Verify shell-command policy works on the scan step (parent, not instance)
437
+ const shellVerify = await policy.verify("custom-step", "e2e-pipeline/scan", {
438
+ basePath: "/unused",
439
+ });
440
+ assert.equal(
441
+ shellVerify,
442
+ "continue",
443
+ "Shell-command verification (test -f output/scan-result.txt) should pass",
444
+ );
445
+ });
446
+
447
+ describe("createRun + listRuns integration", () => {
448
+ it("created run appears in listRuns with correct metadata", () => {
449
+ const { basePath, runDir } = setupProject({ target: "list-test" });
450
+
451
+ const runs = listRuns(basePath, "e2e-pipeline");
452
+ assert.ok(runs.length >= 1, "Should list at least one run");
453
+
454
+ const thisRun = runs.find((r) => r.runDir === runDir);
455
+ assert.ok(thisRun, "Created run should appear in listRuns");
456
+ assert.equal(thisRun.name, "e2e-pipeline");
457
+ assert.equal(thisRun.status, "pending", "New run should have pending status");
458
+ assert.equal(thisRun.steps.total, 4, "Should have 4 steps");
459
+ assert.equal(thisRun.steps.completed, 0);
460
+ assert.equal(thisRun.steps.pending, 4);
461
+ });
462
+ });
463
+
464
+ describe("validateDefinition accepts the e2e definition", () => {
465
+ it("validates the e2e-pipeline YAML as valid V1 schema", () => {
466
+ const parsed = parse(E2E_DEFINITION_YAML);
467
+ const { valid, errors } = validateDefinition(parsed);
468
+ assert.equal(
469
+ valid,
470
+ true,
471
+ `Definition should be valid but got errors: ${errors.join(", ")}`,
472
+ );
473
+ assert.deepStrictEqual(errors, []);
474
+ });
475
+ });
476
+ });