@studio-foundation/engine 0.3.0-beta.1 → 0.3.0-beta.6

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 (246) hide show
  1. package/dist/__tests__/engine.conditions-group.test.d.ts +2 -0
  2. package/dist/__tests__/engine.conditions-group.test.d.ts.map +1 -0
  3. package/dist/__tests__/engine.conditions-group.test.js +222 -0
  4. package/dist/__tests__/engine.conditions-group.test.js.map +1 -0
  5. package/dist/__tests__/engine.conditions.test.d.ts +2 -0
  6. package/dist/__tests__/engine.conditions.test.d.ts.map +1 -0
  7. package/dist/__tests__/engine.conditions.test.js +164 -0
  8. package/dist/__tests__/engine.conditions.test.js.map +1 -0
  9. package/dist/__tests__/engine.resume.test.d.ts +2 -0
  10. package/dist/__tests__/engine.resume.test.d.ts.map +1 -0
  11. package/dist/__tests__/engine.resume.test.js +93 -0
  12. package/dist/__tests__/engine.resume.test.js.map +1 -0
  13. package/dist/__tests__/engine.script-stage.test.d.ts +2 -0
  14. package/dist/__tests__/engine.script-stage.test.d.ts.map +1 -0
  15. package/dist/__tests__/engine.script-stage.test.js +108 -0
  16. package/dist/__tests__/engine.script-stage.test.js.map +1 -0
  17. package/dist/db/client.d.ts +3 -0
  18. package/dist/db/client.d.ts.map +1 -0
  19. package/{src/db/client.ts → dist/db/client.js} +1 -2
  20. package/dist/db/client.js.map +1 -0
  21. package/dist/engine.context-event.test.d.ts +2 -0
  22. package/dist/engine.context-event.test.d.ts.map +1 -0
  23. package/dist/engine.context-event.test.js +111 -0
  24. package/dist/engine.context-event.test.js.map +1 -0
  25. package/dist/engine.d.ts +52 -0
  26. package/dist/engine.d.ts.map +1 -0
  27. package/dist/engine.js +364 -0
  28. package/dist/engine.js.map +1 -0
  29. package/dist/events.d.ts +163 -0
  30. package/dist/events.d.ts.map +1 -0
  31. package/dist/events.js +12 -0
  32. package/dist/events.js.map +1 -0
  33. package/{src/index.ts → dist/index.d.ts} +3 -45
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +24 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/pipeline/agent-loader.d.ts +4 -0
  38. package/dist/pipeline/agent-loader.d.ts.map +1 -0
  39. package/dist/pipeline/agent-loader.js +28 -0
  40. package/dist/pipeline/agent-loader.js.map +1 -0
  41. package/dist/pipeline/agent-loader.test.d.ts +2 -0
  42. package/dist/pipeline/agent-loader.test.d.ts.map +1 -0
  43. package/dist/pipeline/agent-loader.test.js +134 -0
  44. package/dist/pipeline/agent-loader.test.js.map +1 -0
  45. package/dist/pipeline/condition-evaluator.d.ts +6 -0
  46. package/dist/pipeline/condition-evaluator.d.ts.map +1 -0
  47. package/dist/pipeline/condition-evaluator.js +105 -0
  48. package/dist/pipeline/condition-evaluator.js.map +1 -0
  49. package/dist/pipeline/condition-evaluator.test.d.ts +2 -0
  50. package/dist/pipeline/condition-evaluator.test.d.ts.map +1 -0
  51. package/dist/pipeline/condition-evaluator.test.js +103 -0
  52. package/dist/pipeline/condition-evaluator.test.js.map +1 -0
  53. package/dist/pipeline/context-pack-loader.d.ts +3 -0
  54. package/dist/pipeline/context-pack-loader.d.ts.map +1 -0
  55. package/dist/pipeline/context-pack-loader.js +51 -0
  56. package/dist/pipeline/context-pack-loader.js.map +1 -0
  57. package/dist/pipeline/context-propagation.d.ts +28 -0
  58. package/dist/pipeline/context-propagation.d.ts.map +1 -0
  59. package/dist/pipeline/context-propagation.js +161 -0
  60. package/dist/pipeline/context-propagation.js.map +1 -0
  61. package/dist/pipeline/context-propagation.test.d.ts +2 -0
  62. package/dist/pipeline/context-propagation.test.d.ts.map +1 -0
  63. package/dist/pipeline/context-propagation.test.js +194 -0
  64. package/dist/pipeline/context-propagation.test.js.map +1 -0
  65. package/dist/pipeline/contract-loader.d.ts +4 -0
  66. package/dist/pipeline/contract-loader.d.ts.map +1 -0
  67. package/dist/pipeline/contract-loader.js +30 -0
  68. package/dist/pipeline/contract-loader.js.map +1 -0
  69. package/dist/pipeline/group-orchestrator.d.ts +19 -0
  70. package/dist/pipeline/group-orchestrator.d.ts.map +1 -0
  71. package/dist/pipeline/group-orchestrator.js +376 -0
  72. package/dist/pipeline/group-orchestrator.js.map +1 -0
  73. package/dist/pipeline/hook-executor.d.ts +28 -0
  74. package/dist/pipeline/hook-executor.d.ts.map +1 -0
  75. package/dist/pipeline/hook-executor.js +63 -0
  76. package/dist/pipeline/hook-executor.js.map +1 -0
  77. package/dist/pipeline/hook-executor.test.d.ts +2 -0
  78. package/dist/pipeline/hook-executor.test.d.ts.map +1 -0
  79. package/dist/pipeline/hook-executor.test.js +71 -0
  80. package/dist/pipeline/hook-executor.test.js.map +1 -0
  81. package/dist/pipeline/invariants-loader.d.ts +6 -0
  82. package/dist/pipeline/invariants-loader.d.ts.map +1 -0
  83. package/dist/pipeline/invariants-loader.js +15 -0
  84. package/dist/pipeline/invariants-loader.js.map +1 -0
  85. package/dist/pipeline/invariants-loader.test.d.ts +2 -0
  86. package/dist/pipeline/invariants-loader.test.d.ts.map +1 -0
  87. package/dist/pipeline/invariants-loader.test.js +39 -0
  88. package/dist/pipeline/invariants-loader.test.js.map +1 -0
  89. package/dist/pipeline/loader.d.ts +5 -0
  90. package/dist/pipeline/loader.d.ts.map +1 -0
  91. package/dist/pipeline/loader.js +132 -0
  92. package/dist/pipeline/loader.js.map +1 -0
  93. package/dist/pipeline/loader.test.d.ts +2 -0
  94. package/dist/pipeline/loader.test.d.ts.map +1 -0
  95. package/dist/pipeline/loader.test.js +120 -0
  96. package/dist/pipeline/loader.test.js.map +1 -0
  97. package/dist/pipeline/output-validator.d.ts +10 -0
  98. package/dist/pipeline/output-validator.d.ts.map +1 -0
  99. package/dist/pipeline/output-validator.js +20 -0
  100. package/dist/pipeline/output-validator.js.map +1 -0
  101. package/dist/pipeline/output-validator.test.d.ts +2 -0
  102. package/dist/pipeline/output-validator.test.d.ts.map +1 -0
  103. package/dist/pipeline/output-validator.test.js +77 -0
  104. package/dist/pipeline/output-validator.test.js.map +1 -0
  105. package/dist/pipeline/post-validator.d.ts +8 -0
  106. package/dist/pipeline/post-validator.d.ts.map +1 -0
  107. package/dist/pipeline/post-validator.js +103 -0
  108. package/dist/pipeline/post-validator.js.map +1 -0
  109. package/dist/pipeline/skill-loader.d.ts +10 -0
  110. package/dist/pipeline/skill-loader.d.ts.map +1 -0
  111. package/dist/pipeline/skill-loader.js +26 -0
  112. package/dist/pipeline/skill-loader.js.map +1 -0
  113. package/dist/pipeline/skill-loader.test.d.ts +2 -0
  114. package/dist/pipeline/skill-loader.test.d.ts.map +1 -0
  115. package/dist/pipeline/skill-loader.test.js +38 -0
  116. package/dist/pipeline/skill-loader.test.js.map +1 -0
  117. package/dist/pipeline/stage-executor.d.ts +26 -0
  118. package/dist/pipeline/stage-executor.d.ts.map +1 -0
  119. package/dist/pipeline/stage-executor.js +540 -0
  120. package/dist/pipeline/stage-executor.js.map +1 -0
  121. package/dist/pipeline/stage-resolver.d.ts +3 -0
  122. package/dist/pipeline/stage-resolver.d.ts.map +1 -0
  123. package/dist/pipeline/stage-resolver.js +6 -0
  124. package/dist/pipeline/stage-resolver.js.map +1 -0
  125. package/dist/pipeline/startup-executor.d.ts +3 -0
  126. package/dist/pipeline/startup-executor.d.ts.map +1 -0
  127. package/dist/pipeline/startup-executor.js +23 -0
  128. package/dist/pipeline/startup-executor.js.map +1 -0
  129. package/dist/pipeline/startup-executor.test.d.ts +2 -0
  130. package/dist/pipeline/startup-executor.test.d.ts.map +1 -0
  131. package/dist/pipeline/startup-executor.test.js +34 -0
  132. package/dist/pipeline/startup-executor.test.js.map +1 -0
  133. package/dist/pipeline/types.d.ts +28 -0
  134. package/dist/pipeline/types.d.ts.map +1 -0
  135. package/dist/pipeline/types.js +12 -0
  136. package/dist/pipeline/types.js.map +1 -0
  137. package/dist/repo-resolver.d.ts +10 -0
  138. package/dist/repo-resolver.d.ts.map +1 -0
  139. package/dist/repo-resolver.js +39 -0
  140. package/dist/repo-resolver.js.map +1 -0
  141. package/dist/spawners/direct-engine-spawner.d.ts +8 -0
  142. package/dist/spawners/direct-engine-spawner.d.ts.map +1 -0
  143. package/dist/spawners/direct-engine-spawner.js +24 -0
  144. package/dist/spawners/direct-engine-spawner.js.map +1 -0
  145. package/dist/state/run-store.d.ts +74 -0
  146. package/dist/state/run-store.d.ts.map +1 -0
  147. package/dist/state/run-store.js +269 -0
  148. package/dist/state/run-store.js.map +1 -0
  149. package/dist/state/run-store.test.d.ts +2 -0
  150. package/dist/state/run-store.test.d.ts.map +1 -0
  151. package/dist/state/run-store.test.js +130 -0
  152. package/dist/state/run-store.test.js.map +1 -0
  153. package/dist/state/state-machine.d.ts +6 -0
  154. package/dist/state/state-machine.d.ts.map +1 -0
  155. package/dist/state/state-machine.js +27 -0
  156. package/dist/state/state-machine.js.map +1 -0
  157. package/dist/state/status-derivation.d.ts +12 -0
  158. package/dist/state/status-derivation.d.ts.map +1 -0
  159. package/dist/state/status-derivation.js +30 -0
  160. package/dist/state/status-derivation.js.map +1 -0
  161. package/package.json +7 -4
  162. package/ARCHITECTURE.md +0 -57
  163. package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +0 -4
  164. package/src/__tests__/engine.conditions-group.test.ts +0 -244
  165. package/src/__tests__/engine.conditions.test.ts +0 -186
  166. package/src/__tests__/engine.resume.test.ts +0 -108
  167. package/src/__tests__/engine.script-stage.test.ts +0 -125
  168. package/src/engine.context-event.test.ts +0 -175
  169. package/src/engine.ts +0 -491
  170. package/src/events.ts +0 -167
  171. package/src/pipeline/agent-loader.test.ts +0 -151
  172. package/src/pipeline/agent-loader.ts +0 -39
  173. package/src/pipeline/condition-evaluator.test.ts +0 -129
  174. package/src/pipeline/condition-evaluator.ts +0 -121
  175. package/src/pipeline/context-pack-loader.ts +0 -63
  176. package/src/pipeline/context-propagation.test.ts +0 -237
  177. package/src/pipeline/context-propagation.ts +0 -235
  178. package/src/pipeline/contract-loader.ts +0 -41
  179. package/src/pipeline/group-orchestrator.ts +0 -483
  180. package/src/pipeline/hook-executor.test.ts +0 -121
  181. package/src/pipeline/hook-executor.ts +0 -87
  182. package/src/pipeline/invariants-loader.test.ts +0 -51
  183. package/src/pipeline/invariants-loader.ts +0 -14
  184. package/src/pipeline/loader.test.ts +0 -128
  185. package/src/pipeline/loader.ts +0 -149
  186. package/src/pipeline/output-validator.test.ts +0 -100
  187. package/src/pipeline/output-validator.ts +0 -40
  188. package/src/pipeline/post-validator.ts +0 -124
  189. package/src/pipeline/skill-loader.test.ts +0 -44
  190. package/src/pipeline/skill-loader.ts +0 -32
  191. package/src/pipeline/stage-executor.ts +0 -654
  192. package/src/pipeline/stage-resolver.ts +0 -8
  193. package/src/pipeline/startup-executor.test.ts +0 -37
  194. package/src/pipeline/startup-executor.ts +0 -32
  195. package/src/pipeline/types.ts +0 -42
  196. package/src/repo-resolver.ts +0 -61
  197. package/src/spawners/direct-engine-spawner.ts +0 -26
  198. package/src/state/run-store.test.ts +0 -157
  199. package/src/state/run-store.ts +0 -362
  200. package/src/state/state-machine.ts +0 -35
  201. package/src/state/status-derivation.ts +0 -36
  202. package/tests/context-pack-loader.test.ts +0 -113
  203. package/tests/context-propagation.test.ts +0 -267
  204. package/tests/direct-engine-spawner.test.ts +0 -102
  205. package/tests/e2e/feature-v5.test.ts +0 -57
  206. package/tests/events.test.ts +0 -56
  207. package/tests/fixtures/agents/test-agent.agent.yaml +0 -5
  208. package/tests/fixtures/contracts/code-gen.contract.yaml +0 -6
  209. package/tests/fixtures/contracts/qa-gate.contract.yaml +0 -15
  210. package/tests/fixtures/contracts/test-contract.contract.yaml +0 -6
  211. package/tests/fixtures/pipelines/group-test.pipeline.yaml +0 -40
  212. package/tests/fixtures/pipelines/simple.pipeline.yaml +0 -15
  213. package/tests/fixtures/pipelines/two-stage.pipeline.yaml +0 -24
  214. package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +0 -75
  215. package/tests/fixtures/test-project/agents/test-agent.agent.yaml +0 -5
  216. package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +0 -6
  217. package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +0 -6
  218. package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +0 -8
  219. package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +0 -15
  220. package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +0 -7
  221. package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +0 -6
  222. package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +0 -26
  223. package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +0 -40
  224. package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +0 -19
  225. package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +0 -19
  226. package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +0 -15
  227. package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +0 -39
  228. package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +0 -38
  229. package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +0 -39
  230. package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +0 -39
  231. package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +0 -38
  232. package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +0 -15
  233. package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +0 -24
  234. package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +0 -19
  235. package/tests/loader.test.ts +0 -385
  236. package/tests/post-validator.test.ts +0 -297
  237. package/tests/repo-resolver.test.ts +0 -102
  238. package/tests/run-store.test.ts +0 -143
  239. package/tests/state-machine.test.ts +0 -110
  240. package/tests/unit/context-propagation.test.ts +0 -45
  241. package/tests/unit/engine.test.ts +0 -770
  242. package/tests/unit/group-loop.test.ts +0 -389
  243. package/tests/unit/group-parallel.test.ts +0 -423
  244. package/tests/unit/state/status-derivation.test.ts +0 -88
  245. package/tsconfig.json +0 -24
  246. package/vitest.config.ts +0 -14
@@ -1,770 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { join } from 'node:path';
3
- import { PipelineEngine, type EngineConfig, type RunInput } from '../../src/engine.js';
4
- import { InMemoryRunStore } from '../../src/state/run-store.js';
5
- import type { EngineEvents } from '../../src/events.js';
6
- import { ToolRegistry } from '@studio-foundation/runner';
7
-
8
- // Provider mock that hangs forever unless the abort signal fires.
9
- // Using a never-resolving inner promise means the abort is the ONLY way
10
- // the test completes — no timing race possible.
11
- function createHangingProvider() {
12
- return {
13
- name: 'anthropic',
14
- call: vi.fn().mockImplementation(
15
- (_req: unknown, _onToken: unknown, signal?: AbortSignal) => {
16
- if (signal?.aborted) {
17
- return Promise.reject(new DOMException('Aborted', 'AbortError'));
18
- }
19
- return new Promise<never>((_, reject) => {
20
- if (!signal) return; // never resolves (only reached if no signal)
21
- signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
22
- });
23
- }
24
- ),
25
- };
26
- }
27
-
28
- // Minimal mock for ProviderRegistry
29
- function createMockProviderRegistry() {
30
- const mockProvider = {
31
- name: 'anthropic',
32
- call: vi.fn().mockResolvedValue({
33
- content: JSON.stringify({
34
- summary: 'Test summary',
35
- requirements: ['req1'],
36
- acceptance_criteria: ['ac1'],
37
- steps: ['step1'],
38
- files_to_modify: ['file.ts'],
39
- risks: ['none'],
40
- files_changed: ['file.ts'],
41
- status: 'pass',
42
- issues: [],
43
- }),
44
- tool_calls: [],
45
- finish_reason: 'stop',
46
- usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
47
- }),
48
- };
49
-
50
- return {
51
- get: vi.fn().mockReturnValue(mockProvider),
52
- register: vi.fn(),
53
- };
54
- }
55
-
56
- // Minimal mock for ToolRegistry
57
- function createMockToolRegistry() {
58
- return {
59
- register: vi.fn(),
60
- get: vi.fn(),
61
- has: vi.fn().mockReturnValue(false),
62
- list: vi.fn().mockReturnValue([]),
63
- toToolDefinitions: vi.fn().mockReturnValue([]),
64
- filter: vi.fn().mockReturnThis(),
65
- getActiveSnippets: vi.fn().mockReturnValue([]),
66
- };
67
- }
68
-
69
- const FIXTURES_DIR = join(import.meta.dirname, '..', 'fixtures');
70
- const PROJECT_DIR = join(FIXTURES_DIR, 'test-project');
71
- const PIPELINES_DIR = join(PROJECT_DIR, 'pipelines');
72
- const AGENTS_DIR = join(PROJECT_DIR, 'agents');
73
- const CONTRACTS_DIR = join(PROJECT_DIR, 'contracts');
74
-
75
- // Create minimal test fixtures
76
- import { mkdirSync, writeFileSync } from 'node:fs';
77
-
78
- function setupTestFixtures() {
79
- mkdirSync(PIPELINES_DIR, { recursive: true });
80
- mkdirSync(AGENTS_DIR, { recursive: true });
81
- mkdirSync(CONTRACTS_DIR, { recursive: true });
82
-
83
- writeFileSync(join(PIPELINES_DIR, 'simple.pipeline.yaml'), `
84
- name: simple
85
- description: Simple test pipeline
86
- version: 1
87
- stages:
88
- - name: analysis
89
- kind: analysis
90
- agent: test-agent
91
- contract: test-contract
92
- ralph:
93
- max_attempts: 2
94
- retry_strategy: none
95
- context:
96
- include:
97
- - input
98
- `);
99
-
100
- writeFileSync(join(PIPELINES_DIR, 'two-stage.pipeline.yaml'), `
101
- name: two-stage
102
- description: Two stage pipeline
103
- version: 1
104
- stages:
105
- - name: stage-1
106
- kind: analysis
107
- agent: test-agent
108
- ralph:
109
- max_attempts: 1
110
- retry_strategy: none
111
- context:
112
- include:
113
- - input
114
- - name: stage-2
115
- kind: planning
116
- agent: test-agent
117
- ralph:
118
- max_attempts: 1
119
- retry_strategy: none
120
- context:
121
- include:
122
- - input
123
- - previous_stage_output
124
- `);
125
-
126
- writeFileSync(join(AGENTS_DIR, 'test-agent.agent.yaml'), `
127
- name: test-agent
128
- provider: anthropic
129
- model: claude-sonnet-4-20250514
130
- temperature: 0.3
131
- `);
132
-
133
- writeFileSync(join(CONTRACTS_DIR, 'test-contract.contract.yaml'), `
134
- name: test-contract
135
- version: 1
136
- schema:
137
- required_fields:
138
- - summary
139
- `);
140
- writeFileSync(join(PIPELINES_DIR, 'hook-output-template.pipeline.yaml'), `
141
- name: hook-output-template
142
- description: Pipeline that verifies on_stage_complete hook receives stage output as template context
143
- version: 1
144
- stages:
145
- - name: analysis
146
- kind: analysis
147
- agent: test-agent
148
- contract: test-contract
149
- hooks:
150
- on_stage_complete:
151
- - command: "sh -c 'echo {{output.files_changed}} | grep -qx file.ts'"
152
- on_failure: reject
153
- ralph:
154
- max_attempts: 1
155
- retry_strategy: none
156
- context:
157
- include:
158
- - input
159
- `);
160
- writeFileSync(join(PIPELINES_DIR, 'hook-reject-on-failure.pipeline.yaml'), `
161
- name: hook-reject-on-failure
162
- description: Pipeline where on_stage_complete hook fails and rejects the stage
163
- version: 1
164
- stages:
165
- - name: analysis
166
- kind: analysis
167
- agent: test-agent
168
- contract: test-contract
169
- hooks:
170
- on_stage_complete:
171
- - command: "sh -c 'echo hook-error-output >&2; exit 1'"
172
- on_failure: reject
173
- ralph:
174
- max_attempts: 1
175
- retry_strategy: none
176
- context:
177
- include:
178
- - input
179
- `);
180
-
181
- // Fixtures for maximum-only tool_calls contract
182
- writeFileSync(join(CONTRACTS_DIR, 'maximum-only.contract.yaml'), `
183
- name: maximum-only
184
- version: 1
185
- schema:
186
- required_fields:
187
- - summary
188
- tool_calls:
189
- maximum: 2
190
- `);
191
- writeFileSync(join(PIPELINES_DIR, 'maximum-only.pipeline.yaml'), `
192
- name: maximum-only
193
- description: Pipeline with a contract that only sets maximum tool_calls
194
- version: 1
195
- stages:
196
- - name: analysis
197
- kind: analysis
198
- agent: test-agent
199
- contract: maximum-only
200
- ralph:
201
- max_attempts: 1
202
- retry_strategy: none
203
- context:
204
- include:
205
- - input
206
- `);
207
-
208
- // Fixture for on_pipeline_start integration test
209
- writeFileSync(join(PIPELINES_DIR, 'with-startup.pipeline.yaml'), `
210
- name: with-startup
211
- description: Pipeline that exercises on_pipeline_start context injection
212
- version: 1
213
- on_pipeline_start:
214
- - command: "echo git-status-output"
215
- inject_as: git_status
216
- stages:
217
- - name: analysis
218
- kind: analysis
219
- agent: test-agent
220
- contract: test-contract
221
- ralph:
222
- max_attempts: 1
223
- retry_strategy: none
224
- context:
225
- include:
226
- - input
227
- - pipeline_start_context
228
- `);
229
-
230
- writeFileSync(join(PIPELINES_DIR, 'group-simple.pipeline.yaml'), `
231
- name: group-simple
232
- description: Pipeline with a simple group for cancellation testing
233
- version: 1
234
- stages:
235
- - group: review-loop
236
- max_iterations: 2
237
- stages:
238
- - name: review-stage-1
239
- kind: analysis
240
- agent: test-agent
241
- ralph:
242
- max_attempts: 1
243
- retry_strategy: none
244
- context:
245
- include:
246
- - input
247
- - name: review-stage-2
248
- kind: analysis
249
- agent: test-agent
250
- ralph:
251
- max_attempts: 1
252
- retry_strategy: none
253
- context:
254
- include:
255
- - input
256
- `);
257
- }
258
-
259
- // Setup fixtures before all tests
260
- setupTestFixtures();
261
-
262
- function createTestEngine(overrides: Partial<EngineConfig> = {}): PipelineEngine {
263
- return new PipelineEngine({
264
- configsDir: PROJECT_DIR,
265
- providerRegistry: createMockProviderRegistry() as any,
266
- toolRegistry: createMockToolRegistry() as any,
267
- db: new InMemoryRunStore(),
268
- ...overrides,
269
- });
270
- }
271
-
272
- describe('PipelineEngine', () => {
273
- it('runs a simple single-stage pipeline', async () => {
274
- const engine = createTestEngine();
275
- const result = await engine.run({
276
- pipeline: 'simple',
277
- input: 'Add a FAQ section',
278
- });
279
-
280
- expect(result.status).toBe('success');
281
- expect(result.pipeline_name).toBe('simple');
282
- expect(result.stages).toHaveLength(1);
283
- expect(result.stages[0].stage_name).toBe('analysis');
284
- expect(result.stages[0].status).toBe('success');
285
- });
286
-
287
- it('runs a multi-stage pipeline', async () => {
288
- const engine = createTestEngine();
289
- const result = await engine.run({
290
- pipeline: 'two-stage',
291
- input: 'Build feature X',
292
- });
293
-
294
- expect(result.status).toBe('success');
295
- expect(result.stages).toHaveLength(2);
296
- expect(result.stages[0].stage_name).toBe('stage-1');
297
- expect(result.stages[1].stage_name).toBe('stage-2');
298
- });
299
-
300
- it('persists run to store', async () => {
301
- const store = new InMemoryRunStore();
302
- const engine = createTestEngine({ db: store });
303
-
304
- const result = await engine.run({
305
- pipeline: 'simple',
306
- input: 'Test persist',
307
- });
308
-
309
- const stored = store.getPipelineRun(result.id);
310
- expect(stored).not.toBeNull();
311
- expect(stored!.id).toBe(result.id);
312
- expect(stored!.status).toBe('success');
313
- });
314
-
315
- it('emits lifecycle events', async () => {
316
- const events: string[] = [];
317
- const engineEvents: EngineEvents = {
318
- onPipelineStart: () => events.push('pipeline_start'),
319
- onPipelineComplete: () => events.push('pipeline_complete'),
320
- onStageStart: () => events.push('stage_start'),
321
- onStageComplete: () => events.push('stage_complete'),
322
- };
323
-
324
- const engine = new PipelineEngine(
325
- {
326
- configsDir: PROJECT_DIR,
327
- providerRegistry: createMockProviderRegistry() as any,
328
- toolRegistry: createMockToolRegistry() as any,
329
- },
330
- engineEvents
331
- );
332
-
333
- await engine.run({ pipeline: 'simple', input: 'test events' });
334
-
335
- expect(events).toEqual([
336
- 'pipeline_start',
337
- 'stage_start',
338
- 'stage_complete',
339
- 'pipeline_complete',
340
- ]);
341
- });
342
-
343
- it('stops pipeline on stage failure (validation fails)', async () => {
344
- // Provider returns output missing required 'summary' field
345
- const badProvider = {
346
- name: 'anthropic',
347
- call: vi.fn().mockResolvedValue({
348
- content: JSON.stringify({ no_summary: true }),
349
- tool_calls: [],
350
- finish_reason: 'stop',
351
- }),
352
- };
353
-
354
- const engine = createTestEngine({
355
- providerRegistry: { get: vi.fn().mockReturnValue(badProvider), register: vi.fn() } as any,
356
- });
357
-
358
- const result = await engine.run({
359
- pipeline: 'simple',
360
- input: 'This should fail validation',
361
- });
362
-
363
- expect(result.status).toBe('failed');
364
- expect(result.stages[0].status).toBe('failed');
365
- });
366
-
367
- it('sets completed_at on pipeline run', async () => {
368
- const engine = createTestEngine();
369
- const result = await engine.run({
370
- pipeline: 'simple',
371
- input: 'test timestamps',
372
- });
373
-
374
- expect(result.started_at).toBeDefined();
375
- expect(result.completed_at).toBeDefined();
376
- });
377
-
378
- it('creates proper stage run with tasks and agent runs', async () => {
379
- const engine = createTestEngine();
380
- const result = await engine.run({
381
- pipeline: 'simple',
382
- input: 'test structure',
383
- });
384
-
385
- const stage = result.stages[0];
386
- expect(stage.tasks).toHaveLength(1);
387
-
388
- const task = stage.tasks[0];
389
- expect(task.task_name).toBe('analysis');
390
- expect(task.agent_runs.length).toBeGreaterThanOrEqual(1);
391
-
392
- const agentRun = task.agent_runs[0];
393
- expect(agentRun.agent_name).toBe('test-agent');
394
- expect(agentRun.attempt).toBe(1);
395
- });
396
-
397
- it('throws for non-existent pipeline', async () => {
398
- const engine = createTestEngine();
399
- await expect(
400
- engine.run({ pipeline: 'nonexistent', input: 'test' })
401
- ).rejects.toThrow('Failed to load pipeline');
402
- });
403
-
404
- it('emits enriched stage complete events with summary and tokens', async () => {
405
- const stageEvents: any[] = [];
406
- const engineEvents: EngineEvents = {
407
- onPipelineStart: () => {},
408
- onPipelineComplete: () => {},
409
- onStageStart: () => {},
410
- onStageComplete: (e) => stageEvents.push(e),
411
- };
412
-
413
- const engine = new PipelineEngine(
414
- {
415
- configsDir: PROJECT_DIR,
416
- providerRegistry: createMockProviderRegistry() as any,
417
- toolRegistry: createMockToolRegistry() as any,
418
- },
419
- engineEvents
420
- );
421
-
422
- await engine.run({ pipeline: 'simple', input: 'test enriched events' });
423
-
424
- expect(stageEvents).toHaveLength(1);
425
- const e = stageEvents[0];
426
- expect(e.stage_name).toBe('analysis');
427
- expect(e.stage_index).toBe(0);
428
- expect(e.total_stages).toBe(1);
429
- expect(e.status).toBe('success');
430
- expect(e.attempts).toBe(1);
431
- expect(e.duration_ms).toBeGreaterThanOrEqual(0);
432
- expect(e.output_summary).toBeDefined();
433
- expect(typeof e.output_summary).toBe('string');
434
- });
435
-
436
- it('emits pipeline complete with totals', async () => {
437
- const pipelineEvents: any[] = [];
438
- const engineEvents: EngineEvents = {
439
- onPipelineComplete: (e) => pipelineEvents.push(e),
440
- };
441
-
442
- const engine = new PipelineEngine(
443
- {
444
- configsDir: PROJECT_DIR,
445
- providerRegistry: createMockProviderRegistry() as any,
446
- toolRegistry: createMockToolRegistry() as any,
447
- },
448
- engineEvents
449
- );
450
-
451
- await engine.run({ pipeline: 'simple', input: 'test totals' });
452
-
453
- expect(pipelineEvents).toHaveLength(1);
454
- const e = pipelineEvents[0];
455
- expect(e.status).toBe('success');
456
- expect(typeof e.total_tokens).toBe('number');
457
- expect(typeof e.total_tool_calls).toBe('number');
458
- expect(typeof e.duration_ms).toBe('number');
459
- });
460
-
461
- it('emits pipeline_complete with status failed when stage fails', async () => {
462
- const pipelineEvents: any[] = [];
463
- const badProvider = {
464
- name: 'anthropic',
465
- call: vi.fn().mockResolvedValue({
466
- content: JSON.stringify({ no_summary: true }),
467
- tool_calls: [],
468
- finish_reason: 'stop',
469
- }),
470
- };
471
-
472
- const engine = new PipelineEngine(
473
- {
474
- configsDir: PROJECT_DIR,
475
- providerRegistry: { get: vi.fn().mockReturnValue(badProvider), register: vi.fn() } as any,
476
- toolRegistry: createMockToolRegistry() as any,
477
- },
478
- { onPipelineComplete: (e) => pipelineEvents.push(e) }
479
- );
480
-
481
- const result = await engine.run({ pipeline: 'simple', input: 'will fail' });
482
-
483
- expect(result.status).toBe('failed');
484
- expect(pipelineEvents).toHaveLength(1);
485
- expect(pipelineEvents[0].status).toBe('failed');
486
- expect(typeof pipelineEvents[0].duration_ms).toBe('number');
487
- expect(typeof pipelineEvents[0].total_tokens).toBe('number');
488
- expect(typeof pipelineEvents[0].total_tool_calls).toBe('number');
489
- });
490
-
491
- it('emits pipeline_complete with status rejected when stage is rejected', async () => {
492
- const pipelineEvents: any[] = [];
493
-
494
- const engine = new PipelineEngine(
495
- {
496
- configsDir: PROJECT_DIR,
497
- providerRegistry: createMockProviderRegistry() as any,
498
- toolRegistry: createMockToolRegistry() as any,
499
- },
500
- { onPipelineComplete: (e) => pipelineEvents.push(e) }
501
- );
502
-
503
- const result = await engine.run({ pipeline: 'hook-reject-on-failure', input: 'will reject' });
504
-
505
- expect(result.status).toBe('rejected');
506
- expect(pipelineEvents).toHaveLength(1);
507
- expect(pipelineEvents[0].status).toBe('rejected');
508
- expect(typeof pipelineEvents[0].duration_ms).toBe('number');
509
- });
510
-
511
- it('on_stage_complete hook receives stage output as template context', async () => {
512
- const engine = createTestEngine();
513
- const result = await engine.run({
514
- pipeline: 'hook-output-template',
515
- input: 'test',
516
- });
517
-
518
- // The hook checks {{output.files_changed}} == 'file.ts'
519
- // The mock provider returns files_changed: ['file.ts'] → renders as 'file.ts'
520
- // Without the fix: template not resolved → hook fails → stage rejected
521
- // With the fix: template resolves correctly → hook passes → stage succeeds
522
- expect(result.status).toBe('success');
523
- expect(result.stages[0].status).toBe('success');
524
- });
525
-
526
- it('on_stage_complete hook failure with on_failure:reject causes stage rejection', async () => {
527
- const engine = createTestEngine();
528
- const result = await engine.run({
529
- pipeline: 'hook-reject-on-failure',
530
- input: 'test',
531
- });
532
-
533
- expect(result.status).toBe('rejected');
534
- expect(result.stages[0].status).toBe('rejected');
535
- expect(result.stages[0]).toMatchObject({
536
- status: 'rejected',
537
- });
538
- });
539
-
540
- it('returns cancelled when signal is aborted before stages run', async () => {
541
- const controller = new AbortController();
542
- controller.abort(); // Pre-aborted
543
-
544
- const engine = createTestEngine();
545
- const result = await engine.run({
546
- pipeline: 'simple',
547
- input: 'test input',
548
- signal: controller.signal,
549
- });
550
-
551
- expect(result.status).toBe('cancelled');
552
- expect(result.stages).toHaveLength(0);
553
- });
554
-
555
- it('cancels cleanly when signal is aborted while a stage is executing', async () => {
556
- const controller = new AbortController();
557
-
558
- const engine = createTestEngine({
559
- providerRegistry: { get: vi.fn().mockReturnValue(createHangingProvider()), register: vi.fn() } as any,
560
- });
561
-
562
- // Abort after the engine has started (next microtask), before the hanging provider resolves
563
- queueMicrotask(() => controller.abort());
564
-
565
- const result = await engine.run({
566
- pipeline: 'simple',
567
- input: 'test input',
568
- signal: controller.signal,
569
- });
570
-
571
- expect(result.status).toBe('cancelled');
572
- });
573
-
574
- it('cancels between stages when signal is aborted after first stage completes', async () => {
575
- const controller = new AbortController();
576
- let stage1Completed = false;
577
-
578
- const events: EngineEvents = {
579
- onStageComplete: (e) => {
580
- if (e.stage_name === 'stage-1') {
581
- stage1Completed = true;
582
- controller.abort(); // abort after stage 1 finishes
583
- }
584
- },
585
- };
586
-
587
- const engine = new PipelineEngine(
588
- {
589
- configsDir: PROJECT_DIR,
590
- providerRegistry: createMockProviderRegistry() as any,
591
- toolRegistry: createMockToolRegistry() as any,
592
- db: new InMemoryRunStore(),
593
- },
594
- events
595
- );
596
-
597
- const result = await engine.run({
598
- pipeline: 'two-stage',
599
- input: 'test input',
600
- signal: controller.signal,
601
- });
602
-
603
- expect(result.status).toBe('cancelled');
604
- expect(stage1Completed).toBe(true);
605
- expect(result.stages).toHaveLength(1);
606
- expect(result.stages[0].stage_name).toBe('stage-1');
607
- expect(result.stages[0].status).toBe('success');
608
- });
609
-
610
- it('cancels cleanly when signal is aborted during a group stage', async () => {
611
- const controller = new AbortController();
612
-
613
- const engine = createTestEngine({
614
- providerRegistry: { get: vi.fn().mockReturnValue(createHangingProvider()), register: vi.fn() } as any,
615
- });
616
-
617
- // Abort after the engine has started (next microtask), before the hanging provider resolves
618
- queueMicrotask(() => controller.abort());
619
-
620
- const result = await engine.run({
621
- pipeline: 'group-simple',
622
- input: 'test input',
623
- signal: controller.signal,
624
- });
625
-
626
- expect(result.status).toBe('cancelled');
627
- });
628
-
629
- it('on_pipeline_start output is injected into stage context', async () => {
630
- // The 'with-startup' pipeline runs `echo git-status-output` and injects it
631
- // as 'git_status'. The stage includes 'pipeline_start_context'.
632
- // The prompt-builder formats startup_context as:
633
- // ## Pipeline Startup Context
634
- // ### git_status
635
- // ```
636
- // git-status-output
637
- // ```
638
- // So we assert that the provider received a user message containing that value.
639
- const capturedRequests: any[] = [];
640
- const provider = {
641
- name: 'anthropic',
642
- call: vi.fn().mockImplementation(async (req: any) => {
643
- capturedRequests.push(req);
644
- return {
645
- content: JSON.stringify({ summary: 'done', requirements: [], acceptance_criteria: [] }),
646
- tool_calls: [],
647
- finish_reason: 'stop',
648
- usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
649
- };
650
- }),
651
- };
652
-
653
- const engine = createTestEngine({
654
- providerRegistry: { get: vi.fn().mockReturnValue(provider), register: vi.fn() } as any,
655
- });
656
-
657
- const result = await engine.run({ pipeline: 'with-startup', input: 'test startup' });
658
-
659
- expect(result.status).toBe('success');
660
- expect(capturedRequests).toHaveLength(1);
661
-
662
- const userMessage = capturedRequests[0].messages.find((m: any) => m.role === 'user');
663
- expect(userMessage).toBeDefined();
664
- expect(userMessage.content).toContain('git-status-output');
665
- });
666
-
667
- it('fails stage when tool_calls maximum is exceeded (maximum-only contract)', async () => {
668
- // A contract with only `maximum: 2` and no `minimum`.
669
- // Before the guard fix, this validator was silently skipped.
670
- // After the fix it must be activated and fail when 3 successful tool calls are made.
671
-
672
- // Create a real ToolRegistry with a succeeding test tool
673
- const toolRegistry = new ToolRegistry();
674
- toolRegistry.register({
675
- name: 'test-tool',
676
- description: 'A simple test tool',
677
- parameters: { type: 'object', properties: {}, required: [] },
678
- execute: async () => ({ success: true, output: 'ok' }),
679
- });
680
-
681
- // Mock provider: returns 3 tool calls on the first call, then valid output
682
- let callCount = 0;
683
- const provider = {
684
- name: 'anthropic',
685
- call: vi.fn().mockImplementation(async () => {
686
- callCount++;
687
- if (callCount === 1) {
688
- // First turn: 3 tool calls (exceeds maximum of 2)
689
- return {
690
- content: '',
691
- tool_calls: [
692
- { id: 'tc-1', name: 'test-tool', arguments: {} },
693
- { id: 'tc-2', name: 'test-tool', arguments: {} },
694
- { id: 'tc-3', name: 'test-tool', arguments: {} },
695
- ],
696
- finish_reason: 'tool_calls',
697
- };
698
- }
699
- // Second turn: final response with valid output
700
- return {
701
- content: JSON.stringify({ summary: 'done' }),
702
- tool_calls: [],
703
- finish_reason: 'stop',
704
- usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 },
705
- };
706
- }),
707
- };
708
-
709
- const engine = new PipelineEngine({
710
- configsDir: PROJECT_DIR,
711
- providerRegistry: { get: vi.fn().mockReturnValue(provider), register: vi.fn() } as any,
712
- toolRegistry: toolRegistry as any,
713
- db: new InMemoryRunStore(),
714
- });
715
-
716
- const result = await engine.run({ pipeline: 'maximum-only', input: 'test maximum' });
717
-
718
- expect(result.status).toBe('failed');
719
- expect(result.stages[0].status).toBe('failed');
720
- });
721
-
722
- it('succeeds stage when tool_calls count is within maximum (maximum-only contract)', async () => {
723
- // Same contract (maximum: 2), but only 1 tool call made — should pass.
724
-
725
- const toolRegistry = new ToolRegistry();
726
- toolRegistry.register({
727
- name: 'test-tool',
728
- description: 'A simple test tool',
729
- parameters: { type: 'object', properties: {}, required: [] },
730
- execute: async () => ({ success: true, output: 'ok' }),
731
- });
732
-
733
- let callCount = 0;
734
- const provider = {
735
- name: 'anthropic',
736
- call: vi.fn().mockImplementation(async () => {
737
- callCount++;
738
- if (callCount === 1) {
739
- // First turn: 1 tool call (within maximum of 2)
740
- return {
741
- content: '',
742
- tool_calls: [
743
- { id: 'tc-1', name: 'test-tool', arguments: {} },
744
- ],
745
- finish_reason: 'tool_calls',
746
- };
747
- }
748
- // Second turn: final response with valid output
749
- return {
750
- content: JSON.stringify({ summary: 'done' }),
751
- tool_calls: [],
752
- finish_reason: 'stop',
753
- usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 },
754
- };
755
- }),
756
- };
757
-
758
- const engine = new PipelineEngine({
759
- configsDir: PROJECT_DIR,
760
- providerRegistry: { get: vi.fn().mockReturnValue(provider), register: vi.fn() } as any,
761
- toolRegistry: toolRegistry as any,
762
- db: new InMemoryRunStore(),
763
- });
764
-
765
- const result = await engine.run({ pipeline: 'maximum-only', input: 'test maximum within bounds' });
766
-
767
- expect(result.status).toBe('success');
768
- expect(result.stages[0].status).toBe('success');
769
- });
770
- });