@studio-foundation/engine 0.3.0-beta.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 (90) hide show
  1. package/ARCHITECTURE.md +57 -0
  2. package/LICENSE +663 -0
  3. package/README.md +118 -0
  4. package/package.json +47 -0
  5. package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +4 -0
  6. package/src/__tests__/engine.conditions-group.test.ts +244 -0
  7. package/src/__tests__/engine.conditions.test.ts +186 -0
  8. package/src/__tests__/engine.resume.test.ts +108 -0
  9. package/src/__tests__/engine.script-stage.test.ts +125 -0
  10. package/src/db/client.ts +5 -0
  11. package/src/engine.context-event.test.ts +175 -0
  12. package/src/engine.ts +491 -0
  13. package/src/events.ts +167 -0
  14. package/src/index.ts +64 -0
  15. package/src/pipeline/agent-loader.test.ts +151 -0
  16. package/src/pipeline/agent-loader.ts +39 -0
  17. package/src/pipeline/condition-evaluator.test.ts +129 -0
  18. package/src/pipeline/condition-evaluator.ts +121 -0
  19. package/src/pipeline/context-pack-loader.ts +63 -0
  20. package/src/pipeline/context-propagation.test.ts +237 -0
  21. package/src/pipeline/context-propagation.ts +235 -0
  22. package/src/pipeline/contract-loader.ts +41 -0
  23. package/src/pipeline/group-orchestrator.ts +483 -0
  24. package/src/pipeline/hook-executor.test.ts +121 -0
  25. package/src/pipeline/hook-executor.ts +87 -0
  26. package/src/pipeline/invariants-loader.test.ts +51 -0
  27. package/src/pipeline/invariants-loader.ts +14 -0
  28. package/src/pipeline/loader.test.ts +128 -0
  29. package/src/pipeline/loader.ts +149 -0
  30. package/src/pipeline/output-validator.test.ts +100 -0
  31. package/src/pipeline/output-validator.ts +40 -0
  32. package/src/pipeline/post-validator.ts +124 -0
  33. package/src/pipeline/skill-loader.test.ts +44 -0
  34. package/src/pipeline/skill-loader.ts +32 -0
  35. package/src/pipeline/stage-executor.ts +654 -0
  36. package/src/pipeline/stage-resolver.ts +8 -0
  37. package/src/pipeline/startup-executor.test.ts +37 -0
  38. package/src/pipeline/startup-executor.ts +32 -0
  39. package/src/pipeline/types.ts +42 -0
  40. package/src/repo-resolver.ts +61 -0
  41. package/src/spawners/direct-engine-spawner.ts +26 -0
  42. package/src/state/run-store.test.ts +157 -0
  43. package/src/state/run-store.ts +362 -0
  44. package/src/state/state-machine.ts +35 -0
  45. package/src/state/status-derivation.ts +36 -0
  46. package/tests/context-pack-loader.test.ts +113 -0
  47. package/tests/context-propagation.test.ts +267 -0
  48. package/tests/direct-engine-spawner.test.ts +102 -0
  49. package/tests/e2e/feature-v5.test.ts +57 -0
  50. package/tests/events.test.ts +56 -0
  51. package/tests/fixtures/agents/test-agent.agent.yaml +5 -0
  52. package/tests/fixtures/contracts/code-gen.contract.yaml +6 -0
  53. package/tests/fixtures/contracts/qa-gate.contract.yaml +15 -0
  54. package/tests/fixtures/contracts/test-contract.contract.yaml +6 -0
  55. package/tests/fixtures/pipelines/group-test.pipeline.yaml +40 -0
  56. package/tests/fixtures/pipelines/simple.pipeline.yaml +15 -0
  57. package/tests/fixtures/pipelines/two-stage.pipeline.yaml +24 -0
  58. package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +75 -0
  59. package/tests/fixtures/test-project/agents/test-agent.agent.yaml +5 -0
  60. package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +6 -0
  61. package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +6 -0
  62. package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +8 -0
  63. package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +15 -0
  64. package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +7 -0
  65. package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +6 -0
  66. package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +26 -0
  67. package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +40 -0
  68. package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +19 -0
  69. package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +19 -0
  70. package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +15 -0
  71. package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +39 -0
  72. package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +38 -0
  73. package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +39 -0
  74. package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +39 -0
  75. package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +38 -0
  76. package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +15 -0
  77. package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +24 -0
  78. package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +19 -0
  79. package/tests/loader.test.ts +385 -0
  80. package/tests/post-validator.test.ts +297 -0
  81. package/tests/repo-resolver.test.ts +102 -0
  82. package/tests/run-store.test.ts +143 -0
  83. package/tests/state-machine.test.ts +110 -0
  84. package/tests/unit/context-propagation.test.ts +45 -0
  85. package/tests/unit/engine.test.ts +770 -0
  86. package/tests/unit/group-loop.test.ts +389 -0
  87. package/tests/unit/group-parallel.test.ts +423 -0
  88. package/tests/unit/state/status-derivation.test.ts +88 -0
  89. package/tsconfig.json +24 -0
  90. package/vitest.config.ts +14 -0
@@ -0,0 +1,267 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createInitialContext,
4
+ addStageOutput,
5
+ addStageToolResults,
6
+ getContextForStage,
7
+ setGroupFeedback,
8
+ clearGroupFeedback,
9
+ type GroupFeedback,
10
+ } from '../src/pipeline/context-propagation.js';
11
+ import type { StageDefinition, ToolCall } from '@studio-foundation/contracts';
12
+
13
+ function makeStage(overrides: Partial<StageDefinition> = {}): StageDefinition {
14
+ return {
15
+ name: 'test-stage',
16
+ kind: 'analysis',
17
+ agent: 'analyst',
18
+ ...overrides,
19
+ } as StageDefinition;
20
+ }
21
+
22
+ describe('createInitialContext', () => {
23
+ it('creates context with input', () => {
24
+ const ctx = createInitialContext('Build a FAQ page');
25
+ expect(ctx.input).toBe('Build a FAQ page');
26
+ expect(ctx.stageOutputs.size).toBe(0);
27
+ expect(ctx.stageToolResults.size).toBe(0);
28
+ });
29
+
30
+ it('optionally includes repoPath', () => {
31
+ const ctx = createInitialContext('test', '/path/to/repo');
32
+ expect(ctx.repoPath).toBe('/path/to/repo');
33
+ });
34
+ });
35
+
36
+ describe('addStageOutput', () => {
37
+ it('adds stage output to context', () => {
38
+ const ctx = createInitialContext('test');
39
+ addStageOutput(ctx, 'brief-analysis', { summary: 'done' });
40
+
41
+ expect(ctx.stageOutputs.get('brief-analysis')).toEqual({ summary: 'done' });
42
+ });
43
+
44
+ it('accumulates multiple stage outputs', () => {
45
+ const ctx = createInitialContext('test');
46
+ addStageOutput(ctx, 'stage-1', { result: 'a' });
47
+ addStageOutput(ctx, 'stage-2', { result: 'b' });
48
+
49
+ expect(ctx.stageOutputs.size).toBe(2);
50
+ expect(ctx.stageOutputs.get('stage-1')).toEqual({ result: 'a' });
51
+ expect(ctx.stageOutputs.get('stage-2')).toEqual({ result: 'b' });
52
+ });
53
+ });
54
+
55
+ describe('structured input', () => {
56
+ it('passes structured input as YAML string in additional_context', () => {
57
+ const structuredInput = {
58
+ brief_summary: 'Add FAQ to About page',
59
+ target_page: 'src/pages/about.tsx',
60
+ acceptance_criteria: ['FAQ section appears', 'Accordion style'],
61
+ };
62
+ const context = createInitialContext(structuredInput);
63
+ const agentCtx = getContextForStage(context, {
64
+ name: 'test',
65
+ kind: 'analysis',
66
+ agent: 'test-agent',
67
+ context: { include: ['input'] },
68
+ });
69
+ // Structured input should be serialized as YAML in additional_context
70
+ expect(agentCtx.additional_context).toContain('brief_summary');
71
+ expect(agentCtx.additional_context).toContain('Add FAQ to About page');
72
+ expect(agentCtx.additional_context).toContain('target_page');
73
+ });
74
+
75
+ it('passes string input unchanged', () => {
76
+ const context = createInitialContext('Simple string input');
77
+ const agentCtx = getContextForStage(context, {
78
+ name: 'test',
79
+ kind: 'analysis',
80
+ agent: 'test-agent',
81
+ context: { include: ['input'] },
82
+ });
83
+ expect(agentCtx.additional_context).toBe('Simple string input');
84
+ });
85
+ });
86
+
87
+ describe('getContextForStage', () => {
88
+ it('with "input" includes user input as additional_context', () => {
89
+ const ctx = createInitialContext('Build a FAQ');
90
+ const stage = makeStage({ context: { include: ['input'] } });
91
+
92
+ const agentCtx = getContextForStage(ctx, stage);
93
+ expect(agentCtx.additional_context).toBe('Build a FAQ');
94
+ });
95
+
96
+ it('with "previous_stage_output" includes the last stage output', () => {
97
+ const ctx = createInitialContext('test');
98
+ addStageOutput(ctx, 'analysis', { summary: 'looks good' });
99
+ addStageOutput(ctx, 'planning', { steps: [1, 2, 3] });
100
+
101
+ const stage = makeStage({ context: { include: ['previous_stage_output'] } });
102
+ const agentCtx = getContextForStage(ctx, stage, 'planning');
103
+
104
+ expect(agentCtx.previous_outputs).toEqual({
105
+ planning: { steps: [1, 2, 3] },
106
+ });
107
+ });
108
+
109
+ it('with "all_stage_outputs" includes all accumulated outputs', () => {
110
+ const ctx = createInitialContext('test');
111
+ addStageOutput(ctx, 'stage-1', { result: 'a' });
112
+ addStageOutput(ctx, 'stage-2', { result: 'b' });
113
+
114
+ const stage = makeStage({ context: { include: ['all_stage_outputs'] } });
115
+ const agentCtx = getContextForStage(ctx, stage);
116
+
117
+ expect(agentCtx.previous_outputs).toEqual({
118
+ 'stage-1': { result: 'a' },
119
+ 'stage-2': { result: 'b' },
120
+ });
121
+ });
122
+
123
+ it('with "repo_files" sets repo_files to empty array (engine fills later)', () => {
124
+ const ctx = createInitialContext('test', '/repo');
125
+ const stage = makeStage({ context: { include: ['repo_files'] } });
126
+
127
+ const agentCtx = getContextForStage(ctx, stage);
128
+ expect(agentCtx.repo_files).toEqual([]);
129
+ });
130
+
131
+ it('defaults to "input" when no context.include specified', () => {
132
+ const ctx = createInitialContext('default input');
133
+ const stage = makeStage({});
134
+
135
+ const agentCtx = getContextForStage(ctx, stage);
136
+ expect(agentCtx.additional_context).toBe('default input');
137
+ });
138
+
139
+ it('combines multiple includes', () => {
140
+ const ctx = createInitialContext('user input');
141
+ addStageOutput(ctx, 'prev', { data: 42 });
142
+
143
+ const stage = makeStage({
144
+ context: { include: ['input', 'previous_stage_output', 'repo_files'] },
145
+ });
146
+ const agentCtx = getContextForStage(ctx, stage, 'prev');
147
+
148
+ expect(agentCtx.additional_context).toBe('user input');
149
+ expect(agentCtx.previous_outputs).toEqual({ prev: { data: 42 } });
150
+ expect(agentCtx.repo_files).toEqual([]);
151
+ });
152
+ });
153
+
154
+ describe('group feedback', () => {
155
+ it('setGroupFeedback adds feedback to context', () => {
156
+ const ctx = createInitialContext('test');
157
+ const feedback: GroupFeedback = {
158
+ iteration: 1,
159
+ max_iterations: 3,
160
+ rejection_reason: 'Missing error handling',
161
+ };
162
+ setGroupFeedback(ctx, feedback);
163
+ expect(ctx.groupFeedback).toEqual(feedback);
164
+ });
165
+
166
+ it('clearGroupFeedback removes feedback from context', () => {
167
+ const ctx = createInitialContext('test');
168
+ setGroupFeedback(ctx, {
169
+ iteration: 1,
170
+ max_iterations: 3,
171
+ rejection_reason: 'test',
172
+ });
173
+ clearGroupFeedback(ctx);
174
+ expect(ctx.groupFeedback).toBeUndefined();
175
+ });
176
+
177
+ it('getContextForStage populates group_feedback as dedicated field', () => {
178
+ const ctx = createInitialContext('Build a FAQ');
179
+ setGroupFeedback(ctx, {
180
+ iteration: 1,
181
+ max_iterations: 3,
182
+ rejection_reason: 'Props not passed to component',
183
+ rejection_details: ['Missing onClick handler', 'Wrong prop type'],
184
+ });
185
+
186
+ const stage = makeStage({
187
+ context: { include: ['input', 'group_feedback'] },
188
+ });
189
+ const agentCtx = getContextForStage(ctx, stage);
190
+
191
+ // Input goes to additional_context, feedback goes to dedicated field
192
+ expect(agentCtx.additional_context).toBe('Build a FAQ');
193
+ expect(agentCtx.group_feedback).toEqual({
194
+ iteration: 1,
195
+ max_iterations: 3,
196
+ rejection_reason: 'Props not passed to component',
197
+ rejection_details: ['Missing onClick handler', 'Wrong prop type'],
198
+ });
199
+ });
200
+
201
+ it('group_feedback is ignored when no feedback is set', () => {
202
+ const ctx = createInitialContext('Build a FAQ');
203
+ const stage = makeStage({
204
+ context: { include: ['input', 'group_feedback'] },
205
+ });
206
+ const agentCtx = getContextForStage(ctx, stage);
207
+
208
+ // Only input is present, no feedback
209
+ expect(agentCtx.additional_context).toBe('Build a FAQ');
210
+ expect(agentCtx.group_feedback).toBeUndefined();
211
+ });
212
+ });
213
+
214
+ describe('addStageToolResults', () => {
215
+ it('stores tool calls by stage name', () => {
216
+ const ctx = createInitialContext('test');
217
+ const toolCalls: ToolCall[] = [
218
+ { id: '1', name: 'search-search_codebase', arguments: { pattern: 'about' }, result: { matches: [] } },
219
+ ];
220
+ addStageToolResults(ctx, 'brief-analysis', toolCalls);
221
+ expect(ctx.stageToolResults.get('brief-analysis')).toEqual(toolCalls);
222
+ });
223
+
224
+ it('accumulates tool results across stages', () => {
225
+ const ctx = createInitialContext('test');
226
+ addStageToolResults(ctx, 'stage-1', [{ id: '1', name: 'tool-a', arguments: {}, result: 'r1' }]);
227
+ addStageToolResults(ctx, 'stage-2', [{ id: '2', name: 'tool-b', arguments: {}, result: 'r2' }]);
228
+ expect(ctx.stageToolResults.size).toBe(2);
229
+ });
230
+ });
231
+
232
+ describe('getContextForStage — previous_stage_tool_results', () => {
233
+ it('includes tool calls from the previous stage', () => {
234
+ const ctx = createInitialContext('test');
235
+ const toolCalls: ToolCall[] = [
236
+ { id: '1', name: 'search-search_codebase', arguments: { pattern: 'about' }, result: { matches: ['about.tsx'] } },
237
+ ];
238
+ addStageToolResults(ctx, 'brief-analysis', toolCalls);
239
+
240
+ const stage = makeStage({ context: { include: ['previous_stage_tool_results'] } });
241
+ const agentCtx = getContextForStage(ctx, stage, 'brief-analysis');
242
+
243
+ expect(agentCtx.previous_tool_results).toEqual({ 'brief-analysis': toolCalls });
244
+ });
245
+
246
+ it('returns empty when no previous stage tool results', () => {
247
+ const ctx = createInitialContext('test');
248
+ const stage = makeStage({ context: { include: ['previous_stage_tool_results'] } });
249
+ const agentCtx = getContextForStage(ctx, stage, 'nonexistent');
250
+ expect(agentCtx.previous_tool_results).toBeUndefined();
251
+ });
252
+ });
253
+
254
+ describe('getContextForStage — all_stage_tool_results', () => {
255
+ it('includes tool calls from all stages', () => {
256
+ const ctx = createInitialContext('test');
257
+ const tc1: ToolCall[] = [{ id: '1', name: 'tool-a', arguments: {}, result: 'r1' }];
258
+ const tc2: ToolCall[] = [{ id: '2', name: 'tool-b', arguments: {}, result: 'r2' }];
259
+ addStageToolResults(ctx, 'stage-1', tc1);
260
+ addStageToolResults(ctx, 'stage-2', tc2);
261
+
262
+ const stage = makeStage({ context: { include: ['all_stage_tool_results'] } });
263
+ const agentCtx = getContextForStage(ctx, stage);
264
+
265
+ expect(agentCtx.previous_tool_results).toEqual({ 'stage-1': tc1, 'stage-2': tc2 });
266
+ });
267
+ });
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { DirectEngineSpawner } from '../src/spawners/direct-engine-spawner.js';
3
+ import type { EngineConfig } from '../src/engine.js';
4
+ import type { PipelineRun } from '@studio-foundation/contracts';
5
+
6
+ function makeSuccessRun(overrides?: Partial<PipelineRun>): PipelineRun {
7
+ return {
8
+ id: 'child-run-1',
9
+ pipeline_name: 'test-pipe',
10
+ status: 'success',
11
+ started_at: new Date().toISOString(),
12
+ stages: [
13
+ {
14
+ id: 's1',
15
+ stage_name: 'final',
16
+ status: 'success',
17
+ started_at: new Date().toISOString(),
18
+ tasks: [],
19
+ output: { answer: 42 },
20
+ },
21
+ ],
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ // We mock PipelineEngine to avoid real execution
27
+ vi.mock('../src/engine.js', () => ({
28
+ PipelineEngine: vi.fn(function () {
29
+ return { run: vi.fn() };
30
+ }),
31
+ }));
32
+
33
+ describe('DirectEngineSpawner', () => {
34
+ it('calls child engine.run() with correct args', async () => {
35
+ const { PipelineEngine } = await import('../src/engine.js');
36
+ const mockRun = vi.fn().mockResolvedValue(makeSuccessRun());
37
+ (PipelineEngine as any).mockImplementation(function () { return { run: mockRun }; });
38
+
39
+ const spawner = new DirectEngineSpawner({} as EngineConfig);
40
+ await spawner.spawnAndWait({
41
+ pipeline: 'recipe-developer',
42
+ input: { dish: 'pasta' },
43
+ parentRunId: 'parent-1',
44
+ depth: 1,
45
+ });
46
+
47
+ expect(mockRun).toHaveBeenCalledWith(
48
+ expect.objectContaining({
49
+ pipeline: 'recipe-developer',
50
+ input: { dish: 'pasta' },
51
+ parentRunId: 'parent-1',
52
+ depth: 1,
53
+ })
54
+ );
55
+ });
56
+
57
+ it('returns run_id, status, and last stage output on success', async () => {
58
+ const { PipelineEngine } = await import('../src/engine.js');
59
+ const successRun = makeSuccessRun();
60
+ (PipelineEngine as any).mockImplementation(function () { return {
61
+ run: vi.fn().mockResolvedValue(successRun),
62
+ }; });
63
+
64
+ const spawner = new DirectEngineSpawner({} as EngineConfig);
65
+ const result = await spawner.spawnAndWait({
66
+ pipeline: 'test',
67
+ input: {},
68
+ parentRunId: 'p1',
69
+ depth: 1,
70
+ });
71
+
72
+ expect(result.run_id).toBe('child-run-1');
73
+ expect(result.status).toBe('success');
74
+ expect(result.output).toEqual({ answer: 42 });
75
+ });
76
+
77
+ it('throws when child run fails', async () => {
78
+ const { PipelineEngine } = await import('../src/engine.js');
79
+ const failedRun = makeSuccessRun({ id: 'child-fail', status: 'failed', stages: [] });
80
+ (PipelineEngine as any).mockImplementation(function () { return {
81
+ run: vi.fn().mockResolvedValue(failedRun),
82
+ }; });
83
+
84
+ const spawner = new DirectEngineSpawner({} as EngineConfig);
85
+ await expect(
86
+ spawner.spawnAndWait({ pipeline: 'bad', input: {}, parentRunId: 'p1', depth: 1 })
87
+ ).rejects.toThrow('Child run child-fail failed');
88
+ });
89
+
90
+ it('throws when child run is rejected', async () => {
91
+ const { PipelineEngine } = await import('../src/engine.js');
92
+ const rejectedRun = makeSuccessRun({ id: 'child-rej', status: 'rejected', stages: [] });
93
+ (PipelineEngine as any).mockImplementation(function () { return {
94
+ run: vi.fn().mockResolvedValue(rejectedRun),
95
+ }; });
96
+
97
+ const spawner = new DirectEngineSpawner({} as EngineConfig);
98
+ await expect(
99
+ spawner.spawnAndWait({ pipeline: 'qa', input: {}, parentRunId: 'p1', depth: 1 })
100
+ ).rejects.toThrow('Child run child-rej rejected');
101
+ });
102
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { join } from 'node:path';
3
+
4
+ // Feature-builder E2E test
5
+ // "Add FAQ to About page" — this is THE test that must pass 10/10
6
+ //
7
+ // Requires:
8
+ // - API key (ANTHROPIC_API_KEY)
9
+ // - A test repo to modify
10
+ // - All pipeline infrastructure running
11
+ //
12
+ // Unskip when everything is fully connected
13
+
14
+ describe.skip('feature-builder E2E', () => {
15
+ const ROOT = join(import.meta.dirname, '..', '..');
16
+
17
+ it('should add FAQ section to About page', async () => {
18
+ const { PipelineEngine } = await import('../../src/engine.js');
19
+ const { InMemoryRunStore } = await import('../../src/state/run-store.js');
20
+ const { ToolRegistry, createRepoManagerTools, createShellTools, createSearchTools } = await import('@studio-foundation/runner');
21
+ const { createDefaultRegistry } = await import('@studio-foundation/runner');
22
+
23
+ // Setup tools
24
+ const toolRegistry = new ToolRegistry();
25
+ const testRepoPath = '/tmp/studio-test-repo'; // TODO: create temp repo
26
+ for (const tool of createRepoManagerTools(testRepoPath)) {
27
+ toolRegistry.register(tool);
28
+ }
29
+ for (const tool of createShellTools(testRepoPath)) {
30
+ toolRegistry.register(tool);
31
+ }
32
+ for (const tool of createSearchTools(testRepoPath)) {
33
+ toolRegistry.register(tool);
34
+ }
35
+
36
+ const providerRegistry = createDefaultRegistry({
37
+ anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! },
38
+ });
39
+
40
+ const engine = new PipelineEngine({
41
+ configsDir: join(ROOT, 'configs'),
42
+ repoPath: testRepoPath,
43
+ providerRegistry,
44
+ toolRegistry,
45
+ db: new InMemoryRunStore(),
46
+ });
47
+
48
+ const result = await engine.run({
49
+ pipeline: 'software/feature-builder',
50
+ input: 'Add a FAQ section to the About page',
51
+ });
52
+
53
+ expect(result.status).toBe('success');
54
+ expect(result.stages).toHaveLength(4);
55
+ expect(result.stages.every(s => s.status === 'success')).toBe(true);
56
+ });
57
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { StageRetryEvent, StagedToolCallStartEvent, StagedToolCallCompleteEvent } from '../src/events.js';
3
+
4
+ describe('StageRetryEvent', () => {
5
+ it('includes max_attempts field', () => {
6
+ const event: StageRetryEvent = {
7
+ stage: 'code-generation',
8
+ attempt: 2,
9
+ failures: ['missing field: summary'],
10
+ max_attempts: 3,
11
+ };
12
+ expect(event.max_attempts).toBe(3);
13
+ });
14
+
15
+ it('includes optional agent_output_raw and tool_calls_count', () => {
16
+ const event: StageRetryEvent = {
17
+ stage: 'code-generation',
18
+ attempt: 1,
19
+ failures: ['validation failed'],
20
+ max_attempts: 5,
21
+ agent_output_raw: '{"summary": "incomplete"}',
22
+ tool_calls_count: 3,
23
+ };
24
+ expect(event.agent_output_raw).toBe('{"summary": "incomplete"}');
25
+ expect(event.tool_calls_count).toBe(3);
26
+ });
27
+ });
28
+
29
+ describe('StagedToolCallStartEvent', () => {
30
+ it('includes stage, tool, and params fields', () => {
31
+ const event: StagedToolCallStartEvent = {
32
+ stage: 'code-generation',
33
+ tool: 'repo_manager-write_file',
34
+ params: { path: 'src/foo.ts' },
35
+ timestamp: 1700000000000,
36
+ };
37
+ expect(event.stage).toBe('code-generation');
38
+ expect(event.tool).toBe('repo_manager-write_file');
39
+ expect(event.params).toEqual({ path: 'src/foo.ts' });
40
+ });
41
+ });
42
+
43
+ describe('StagedToolCallCompleteEvent', () => {
44
+ it('includes stage, tool, result, and duration_ms fields', () => {
45
+ const event: StagedToolCallCompleteEvent = {
46
+ stage: 'code-generation',
47
+ tool: 'repo_manager-write_file',
48
+ result: 'written',
49
+ duration_ms: 120,
50
+ timestamp: 1700000000000,
51
+ };
52
+ expect(event.stage).toBe('code-generation');
53
+ expect(event.tool).toBe('repo_manager-write_file');
54
+ expect(event.duration_ms).toBe(120);
55
+ });
56
+ });
@@ -0,0 +1,5 @@
1
+
2
+ name: test-agent
3
+ provider: anthropic
4
+ model: claude-sonnet-4-20250514
5
+ temperature: 0.3
@@ -0,0 +1,6 @@
1
+
2
+ name: code-gen
3
+ version: 1
4
+ schema:
5
+ required_fields:
6
+ - files_changed
@@ -0,0 +1,15 @@
1
+
2
+ name: qa-gate
3
+ version: 1
4
+ schema:
5
+ required_fields:
6
+ - status
7
+ - issues
8
+ post_validation:
9
+ rejection_detection:
10
+ field: status
11
+ approved_values:
12
+ - approved
13
+ - pass
14
+ details_field: issues
15
+ summary_field: summary
@@ -0,0 +1,6 @@
1
+
2
+ name: test-contract
3
+ version: 1
4
+ schema:
5
+ required_fields:
6
+ - summary
@@ -0,0 +1,40 @@
1
+
2
+ name: group-test
3
+ description: Test pipeline with a feedback loop group
4
+ version: 2
5
+ stages:
6
+ - name: analysis
7
+ kind: analysis
8
+ agent: test-agent
9
+ ralph:
10
+ max_attempts: 1
11
+ retry_strategy: none
12
+ context:
13
+ include:
14
+ - input
15
+ - group: impl-review
16
+ max_iterations: 3
17
+ stages:
18
+ - name: code-gen
19
+ kind: code_generation
20
+ agent: test-agent
21
+ contract: code-gen
22
+ ralph:
23
+ max_attempts: 1
24
+ retry_strategy: none
25
+ context:
26
+ include:
27
+ - input
28
+ - all_stage_outputs
29
+ - group_feedback
30
+ - name: qa-review
31
+ kind: qa
32
+ agent: test-agent
33
+ contract: qa-gate
34
+ ralph:
35
+ max_attempts: 1
36
+ retry_strategy: none
37
+ context:
38
+ include:
39
+ - input
40
+ - all_stage_outputs
@@ -0,0 +1,15 @@
1
+
2
+ name: simple
3
+ description: Simple test pipeline
4
+ version: 1
5
+ stages:
6
+ - name: analysis
7
+ kind: analysis
8
+ agent: test-agent
9
+ contract: test-contract
10
+ ralph:
11
+ max_attempts: 2
12
+ retry_strategy: none
13
+ context:
14
+ include:
15
+ - input
@@ -0,0 +1,24 @@
1
+
2
+ name: two-stage
3
+ description: Two stage pipeline
4
+ version: 1
5
+ stages:
6
+ - name: stage-1
7
+ kind: analysis
8
+ agent: test-agent
9
+ ralph:
10
+ max_attempts: 1
11
+ retry_strategy: none
12
+ context:
13
+ include:
14
+ - input
15
+ - name: stage-2
16
+ kind: planning
17
+ agent: test-agent
18
+ ralph:
19
+ max_attempts: 1
20
+ retry_strategy: none
21
+ context:
22
+ include:
23
+ - input
24
+ - previous_stage_output
@@ -0,0 +1,75 @@
1
+ name: feature-builder
2
+ description: Build a feature from a user description
3
+ version: 2
4
+
5
+ repo:
6
+ url: https://github.com/arianeguay/pipelines-test-repo
7
+ branch: main
8
+
9
+ stages:
10
+ - name: brief-analysis
11
+ kind: analysis
12
+ agent: analyst
13
+ contract: brief-analysis
14
+ ralph:
15
+ max_attempts: 3
16
+ retry_strategy: exponential
17
+ max_tool_calls: 5
18
+ context:
19
+ include:
20
+ - input
21
+
22
+ - name: implementation-plan
23
+ kind: planning
24
+ agent: analyst
25
+ contract: implementation-plan
26
+ ralph:
27
+ max_attempts: 3
28
+ retry_strategy: exponential
29
+ max_tool_calls: 5
30
+ context:
31
+ include:
32
+ - input
33
+ - previous_stage_output
34
+
35
+ - group: implementation-review
36
+ max_iterations: 3
37
+ stages:
38
+ - name: code-generation
39
+ kind: code_generation
40
+ agent: coder
41
+ contract: code-generation
42
+ hooks:
43
+ on_stage_complete:
44
+ - command: "npx tsc --noEmit"
45
+ on_failure: reject
46
+ - command: "npx eslint --rule 'no-empty: error' --rule 'no-unused-vars: warn' {{output.files_changed}}"
47
+ on_failure: reject
48
+ ralph:
49
+ max_attempts: 5
50
+ retry_strategy: exponential
51
+ max_tool_calls: 200
52
+ tools:
53
+ required:
54
+ - repo_manager-write_file
55
+ context:
56
+ include:
57
+ - input
58
+ - all_stage_outputs
59
+ - repo_files
60
+ - group_feedback
61
+ packs:
62
+ - example-conventions
63
+
64
+ - name: qa-review
65
+ kind: qa
66
+ agent: analyst
67
+ contract: qa-review
68
+ ralph:
69
+ max_attempts: 3
70
+ retry_strategy: exponential
71
+ max_tool_calls: 5
72
+ context:
73
+ include:
74
+ - input
75
+ - all_stage_outputs
@@ -0,0 +1,5 @@
1
+
2
+ name: test-agent
3
+ provider: anthropic
4
+ model: claude-sonnet-4-20250514
5
+ temperature: 0.3
@@ -0,0 +1,6 @@
1
+
2
+ name: basic-result
3
+ version: 1
4
+ schema:
5
+ required_fields:
6
+ - result