@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,483 +0,0 @@
1
- // GroupOrchestrator — extracted from PipelineEngine.runGroup/runGroupParallel/runGroupSequential()
2
- // Handles group-level orchestration: parallel and sequential iteration with feedback loops.
3
-
4
- import { randomUUID } from 'node:crypto';
5
- import type { StageGroup, StageRun, StageStatus } from '@studio-foundation/contracts';
6
- import {
7
- addStageOutput,
8
- addStageToolResults,
9
- setGroupFeedback,
10
- type PipelineContext,
11
- } from './context-propagation.js';
12
- import type { EngineEvents, PipelineEventEmitter } from '../events.js';
13
- import type { ToolRegistry, AnonymizationMiddleware } from '@studio-foundation/runner';
14
- import type { StageExecutor } from './stage-executor.js';
15
- import type { GroupResult, ProjectPaths, StageResult } from './types.js';
16
-
17
- export interface GroupOrchestratorConfig {
18
- events?: EngineEvents;
19
- emitter: PipelineEventEmitter;
20
- stageExecutor: StageExecutor;
21
- }
22
-
23
- export class GroupOrchestrator {
24
- constructor(private config: GroupOrchestratorConfig) {}
25
-
26
- async run(
27
- group: StageGroup,
28
- context: PipelineContext,
29
- stageOffset: number,
30
- totalStages: number,
31
- userInput: string | Record<string, unknown>,
32
- paths: ProjectPaths,
33
- toolRegistry: ToolRegistry | undefined,
34
- runMiddleware?: AnonymizationMiddleware | null,
35
- runId?: string,
36
- signal?: AbortSignal,
37
- skipSet?: Set<string>,
38
- originalRunId?: string,
39
- ): Promise<GroupResult> {
40
- if (group.mode === 'parallel') {
41
- return this.runParallel(group, context, stageOffset, totalStages, userInput, paths, toolRegistry, runMiddleware, runId, signal);
42
- }
43
- return this.runSequential(group, context, stageOffset, totalStages, userInput, paths, toolRegistry, runMiddleware, runId, signal, skipSet, originalRunId);
44
- }
45
-
46
- private async runParallel(
47
- group: StageGroup,
48
- context: PipelineContext,
49
- stageOffset: number,
50
- totalStages: number,
51
- userInput: string | Record<string, unknown>,
52
- paths: ProjectPaths,
53
- toolRegistry: ToolRegistry | undefined,
54
- runMiddleware?: AnonymizationMiddleware | null,
55
- runId?: string,
56
- signal?: AbortSignal,
57
- ): Promise<GroupResult> {
58
- let totalTokensDelta = 0;
59
- let totalToolCallsDelta = 0;
60
-
61
- this.config.events?.onGroupStart?.({
62
- group_name: group.group,
63
- max_iterations: group.max_iterations,
64
- parallel: true,
65
- });
66
- this.config.emitter.emit({
67
- type: 'group_start',
68
- groupName: group.group,
69
- maxIterations: group.max_iterations,
70
- });
71
-
72
- // Parallel groups run exactly one iteration
73
- this.config.events?.onGroupIteration?.({
74
- group_name: group.group,
75
- iteration: 1,
76
- max_iterations: group.max_iterations,
77
- });
78
- this.config.emitter.emit({
79
- type: 'group_iteration',
80
- groupName: group.group,
81
- iteration: 1,
82
- maxIterations: group.max_iterations,
83
- });
84
-
85
- if (signal?.aborted) {
86
- this.config.events?.onGroupComplete?.({ group_name: group.group, iterations: 1, status: 'cancelled' });
87
- this.config.emitter.emit({ type: 'group_complete', groupName: group.group, iterations: 1, status: 'cancelled' });
88
- return { status: 'cancelled', stageRuns: [], stagesExecuted: group.stages.length, context, totalTokensDelta, totalToolCallsDelta };
89
- }
90
-
91
- // All parallel stages read the same pre-group context snapshot.
92
- // previousStageName = the last stage before this group.
93
- let previousStageName: string | undefined;
94
- for (const [name] of context.stageOutputs) {
95
- previousStageName = name;
96
- }
97
-
98
- // fail-fast: create a shared AbortController to cancel siblings on first failure
99
- const groupAbort = (group.on_failure ?? 'fail-fast') === 'fail-fast'
100
- ? new AbortController()
101
- : null;
102
- if (groupAbort && signal) {
103
- signal.addEventListener('abort', () => groupAbort.abort(), { once: true });
104
- }
105
- const stageSignal = groupAbort?.signal ?? signal;
106
-
107
- // Launch all stages concurrently
108
- const settled = await Promise.allSettled(
109
- group.stages.map(async (stage, i) => {
110
- const result = await this.config.stageExecutor.execute(
111
- stage,
112
- context,
113
- previousStageName,
114
- userInput,
115
- stageOffset + i,
116
- totalStages,
117
- paths,
118
- toolRegistry,
119
- runMiddleware,
120
- runId,
121
- stageSignal,
122
- );
123
- // fail-fast: abort remaining stages on first non-success
124
- if (groupAbort && result.status !== 'success') {
125
- groupAbort.abort();
126
- }
127
- return { stageName: stage.name, result };
128
- }),
129
- );
130
-
131
- // Build result map keyed by stage name
132
- const resultMap = new Map<string, StageResult>();
133
- for (const s of settled) {
134
- if (s.status === 'fulfilled') {
135
- resultMap.set(s.value.stageName, s.value.result);
136
- }
137
- }
138
-
139
- // Collect stage runs in definition order (deterministic output ordering)
140
- const allStageRuns: StageRun[] = [];
141
- for (const stage of group.stages) {
142
- const result = resultMap.get(stage.name);
143
- if (result) allStageRuns.push(result.stageRun);
144
- }
145
-
146
- // After building resultMap, accumulate token/tool call totals
147
- for (const [, result] of resultMap) {
148
- totalTokensDelta += result.tokensDelta ?? 0;
149
- totalToolCallsDelta += result.toolCallsDelta ?? 0;
150
- }
151
-
152
- // Derive group status: cancelled > failed > success
153
- // rejected treated as failed in parallel mode (no feedback loop)
154
- let groupStatus: StageStatus = 'success';
155
- for (const stage of group.stages) {
156
- const result = resultMap.get(stage.name);
157
- if (!result) { groupStatus = 'failed'; continue; }
158
- if (result.status === 'cancelled' && groupStatus === 'success') groupStatus = 'cancelled';
159
- if (result.status === 'failed' || result.status === 'rejected') groupStatus = 'failed';
160
- }
161
-
162
- // If every stage was skipped, the group is skipped (not success)
163
- const allSkipped = group.stages.every(
164
- (s) => resultMap.get(s.name)?.status === 'skipped',
165
- );
166
- if (allSkipped) groupStatus = 'skipped';
167
-
168
- // Merge successful outputs into context in definition order (regardless of group status)
169
- // This preserves observability for collect-all partial failures
170
- for (const stage of group.stages) {
171
- const result = resultMap.get(stage.name);
172
- if (!result || result.status !== 'success') continue;
173
- if (result.lastAgentOutput !== undefined) {
174
- addStageOutput(context, stage.name, result.lastAgentOutput);
175
- }
176
- if (result.toolCalls?.length) {
177
- addStageToolResults(context, stage.name, result.toolCalls);
178
- }
179
- }
180
-
181
- this.config.events?.onGroupComplete?.({ group_name: group.group, iterations: 1, status: groupStatus });
182
- this.config.emitter.emit({ type: 'group_complete', groupName: group.group, iterations: 1, status: groupStatus });
183
-
184
- return {
185
- status: groupStatus,
186
- stageRuns: allStageRuns,
187
- stagesExecuted: group.stages.length,
188
- context,
189
- totalTokensDelta,
190
- totalToolCallsDelta,
191
- };
192
- }
193
-
194
- private async runSequential(
195
- group: StageGroup,
196
- context: PipelineContext,
197
- stageOffset: number,
198
- totalStages: number,
199
- userInput: string | Record<string, unknown>,
200
- paths: ProjectPaths,
201
- toolRegistry: ToolRegistry | undefined,
202
- runMiddleware?: AnonymizationMiddleware | null,
203
- runId?: string,
204
- signal?: AbortSignal,
205
- skipSet?: Set<string>,
206
- originalRunId?: string,
207
- ): Promise<GroupResult> {
208
- const allStageRuns: StageRun[] = [];
209
- let iteration = 0;
210
- let totalTokensDelta = 0;
211
- let totalToolCallsDelta = 0;
212
-
213
- this.config.events?.onGroupStart?.({
214
- group_name: group.group,
215
- max_iterations: group.max_iterations,
216
- });
217
- this.config.emitter.emit({
218
- type: 'group_start',
219
- groupName: group.group,
220
- maxIterations: group.max_iterations,
221
- });
222
-
223
- while (iteration < group.max_iterations) {
224
- // Check cancellation before group iteration
225
- if (signal?.aborted) {
226
- this.config.events?.onGroupComplete?.({
227
- group_name: group.group,
228
- iterations: iteration,
229
- status: 'cancelled',
230
- });
231
- this.config.emitter.emit({
232
- type: 'group_complete',
233
- groupName: group.group,
234
- iterations: iteration,
235
- status: 'cancelled',
236
- });
237
- return {
238
- status: 'cancelled',
239
- stageRuns: allStageRuns,
240
- stagesExecuted: group.stages.length,
241
- context,
242
- totalTokensDelta,
243
- totalToolCallsDelta,
244
- };
245
- }
246
-
247
- iteration++;
248
-
249
- this.config.events?.onGroupIteration?.({
250
- group_name: group.group,
251
- iteration,
252
- max_iterations: group.max_iterations,
253
- });
254
- this.config.emitter.emit({
255
- type: 'group_iteration',
256
- groupName: group.group,
257
- iteration,
258
- maxIterations: group.max_iterations,
259
- });
260
-
261
- let groupSucceeded = true;
262
- let anyStageExecuted = false;
263
- let previousStageName: string | undefined;
264
- // Find the last stage name before this group in the pipeline context
265
- for (const [name] of context.stageOutputs) {
266
- previousStageName = name;
267
- }
268
-
269
- for (let i = 0; i < group.stages.length; i++) {
270
- if (signal?.aborted) break;
271
-
272
- const stage = group.stages[i];
273
- const stageNumber = stageOffset + i;
274
-
275
- // On iteration 1 only, skip stages before resumeFromStage
276
- if (iteration === 1 && skipSet?.has(stage.name)) {
277
- const skippedReason = originalRunId
278
- ? `resumed from run ${originalRunId}`
279
- : 'resumed from prior run';
280
- const skippedRun: StageRun = {
281
- id: randomUUID(),
282
- stage_name: stage.name,
283
- status: 'skipped',
284
- started_at: new Date().toISOString(),
285
- completed_at: new Date().toISOString(),
286
- tasks: [],
287
- skipped_reason: skippedReason,
288
- };
289
- this.config.events?.onStageComplete?.({
290
- stage_name: stage.name,
291
- stage_index: stageNumber,
292
- total_stages: totalStages,
293
- status: 'skipped',
294
- attempts: 0,
295
- duration_ms: 0,
296
- skipped_reason: skippedReason,
297
- });
298
- allStageRuns.push(skippedRun);
299
- previousStageName = stage.name;
300
- continue;
301
- }
302
-
303
- const result = await this.config.stageExecutor.execute(
304
- stage,
305
- context,
306
- previousStageName,
307
- userInput,
308
- stageNumber,
309
- totalStages,
310
- paths,
311
- toolRegistry,
312
- runMiddleware,
313
- runId,
314
- signal,
315
- );
316
-
317
- allStageRuns.push(result.stageRun);
318
- totalTokensDelta += result.tokensDelta ?? 0;
319
- totalToolCallsDelta += result.toolCallsDelta ?? 0;
320
- if (result.status !== 'skipped') anyStageExecuted = true;
321
-
322
- // Cancelled → stop group
323
- if (result.status === 'cancelled') {
324
- this.config.events?.onGroupComplete?.({
325
- group_name: group.group,
326
- iterations: iteration,
327
- status: 'cancelled',
328
- });
329
- this.config.emitter.emit({
330
- type: 'group_complete',
331
- groupName: group.group,
332
- iterations: iteration,
333
- status: 'cancelled',
334
- });
335
- return {
336
- status: 'cancelled',
337
- stageRuns: allStageRuns,
338
- stagesExecuted: group.stages.length,
339
- context,
340
- totalTokensDelta,
341
- totalToolCallsDelta,
342
- };
343
- }
344
-
345
- // Technical failure → stop everything
346
- if (result.status === 'failed') {
347
- this.config.events?.onGroupComplete?.({
348
- group_name: group.group,
349
- iterations: iteration,
350
- status: 'failed',
351
- });
352
- this.config.emitter.emit({
353
- type: 'group_complete',
354
- groupName: group.group,
355
- iterations: iteration,
356
- status: 'failed',
357
- });
358
- return {
359
- status: 'failed',
360
- stageRuns: allStageRuns,
361
- stagesExecuted: group.stages.length,
362
- context,
363
- totalTokensDelta,
364
- totalToolCallsDelta,
365
- };
366
- }
367
-
368
- // Propagate output to context (will be cleared if we loop)
369
- if (result.lastAgentOutput !== undefined) {
370
- addStageOutput(context, stage.name, result.lastAgentOutput);
371
- }
372
- if (result.toolCalls && result.toolCalls.length > 0) {
373
- addStageToolResults(context, stage.name, result.toolCalls);
374
- }
375
- previousStageName = stage.name;
376
-
377
- // Last stage rejected → feedback loop
378
- const isLastStage = i === group.stages.length - 1;
379
- if (result.status === 'rejected' && isLastStage) {
380
- groupSucceeded = false;
381
-
382
- if (iteration < group.max_iterations) {
383
- // Clear group stage outputs and tool results for next iteration
384
- for (const gs of group.stages) {
385
- context.stageOutputs.delete(gs.name);
386
- context.stageToolResults.delete(gs.name);
387
- }
388
-
389
- // Set feedback for next iteration
390
- setGroupFeedback(context, {
391
- iteration,
392
- max_iterations: group.max_iterations,
393
- rejection_reason: result.postValidation?.rejection_reason || 'Rejected',
394
- rejection_details: result.postValidation?.rejection_details,
395
- });
396
-
397
- this.config.events?.onGroupFeedback?.({
398
- group_name: group.group,
399
- iteration,
400
- rejection_reason: result.postValidation?.rejection_reason || 'Rejected',
401
- rejection_details: result.postValidation?.rejection_details || [],
402
- });
403
- this.config.emitter.emit({
404
- type: 'group_feedback',
405
- groupName: group.group,
406
- iteration,
407
- rejectionReason: result.postValidation?.rejection_reason || 'Rejected',
408
- });
409
- }
410
-
411
- break; // Exit inner stage loop, retry group
412
- }
413
-
414
- // Non-gate stage rejected → stop
415
- if (result.status === 'rejected') {
416
- this.config.events?.onGroupComplete?.({
417
- group_name: group.group,
418
- iterations: iteration,
419
- status: 'rejected',
420
- });
421
- this.config.emitter.emit({
422
- type: 'group_complete',
423
- groupName: group.group,
424
- iterations: iteration,
425
- status: 'rejected',
426
- });
427
- return {
428
- status: 'rejected',
429
- stageRuns: allStageRuns,
430
- stagesExecuted: group.stages.length,
431
- context,
432
- totalTokensDelta,
433
- totalToolCallsDelta,
434
- };
435
- }
436
- }
437
-
438
- if (groupSucceeded) {
439
- const groupStatus = anyStageExecuted ? 'success' : 'skipped';
440
- this.config.events?.onGroupComplete?.({
441
- group_name: group.group,
442
- iterations: iteration,
443
- status: groupStatus,
444
- });
445
- this.config.emitter.emit({
446
- type: 'group_complete',
447
- groupName: group.group,
448
- iterations: iteration,
449
- status: groupStatus,
450
- });
451
- return {
452
- status: groupStatus,
453
- stageRuns: allStageRuns,
454
- stagesExecuted: group.stages.length,
455
- context,
456
- totalTokensDelta,
457
- totalToolCallsDelta,
458
- };
459
- }
460
- }
461
-
462
- // Max iterations exhausted
463
- this.config.events?.onGroupComplete?.({
464
- group_name: group.group,
465
- iterations: iteration,
466
- status: 'rejected',
467
- });
468
- this.config.emitter.emit({
469
- type: 'group_complete',
470
- groupName: group.group,
471
- iterations: iteration,
472
- status: 'rejected',
473
- });
474
- return {
475
- status: 'rejected',
476
- stageRuns: allStageRuns,
477
- stagesExecuted: group.stages.length,
478
- context,
479
- totalTokensDelta,
480
- totalToolCallsDelta,
481
- };
482
- }
483
- }
@@ -1,121 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { renderHookCommand, runStageHook, runToolHook } from './hook-executor.js';
3
-
4
- describe('renderHookCommand', () => {
5
- it('substitutes {{tool.argName}} with tool argument value', () => {
6
- const result = renderHookCommand(
7
- 'npx prettier --write {{tool.path}}',
8
- { path: '/tmp/foo.ts' }
9
- );
10
- expect(result).toBe('npx prettier --write /tmp/foo.ts');
11
- });
12
-
13
- it('returns empty string for missing tool argument', () => {
14
- const result = renderHookCommand('do something {{tool.missing}}', {});
15
- expect(result).toBe('do something ');
16
- });
17
-
18
- it('substitutes multiple occurrences of the same placeholder', () => {
19
- const result = renderHookCommand('cp {{tool.src}} {{tool.dst}}', { src: 'a.ts', dst: 'b.ts' });
20
- expect(result).toBe('cp a.ts b.ts');
21
- });
22
-
23
- it('leaves non-tool placeholders unchanged', () => {
24
- const result = renderHookCommand('echo {{other}}', { other: 'x' });
25
- // {{other}} is not a {{tool.*}} pattern — left as-is
26
- expect(result).toBe('echo {{other}}');
27
- });
28
-
29
- it('substitutes {{output.field}} with value from outputContext', () => {
30
- const result = renderHookCommand(
31
- 'npx eslint {{output.files_changed}}',
32
- {},
33
- { files_changed: 'src/foo.ts' }
34
- );
35
- expect(result).toBe('npx eslint src/foo.ts');
36
- });
37
-
38
- it('space-joins array values from outputContext', () => {
39
- const result = renderHookCommand(
40
- 'npx eslint {{output.files_changed}}',
41
- {},
42
- { files_changed: ['src/foo.ts', 'src/bar.ts'] }
43
- );
44
- expect(result).toBe('npx eslint src/foo.ts src/bar.ts');
45
- });
46
-
47
- it('returns empty string for missing output field', () => {
48
- const result = renderHookCommand(
49
- 'npx eslint {{output.missing}}',
50
- {},
51
- {}
52
- );
53
- expect(result).toBe('npx eslint ');
54
- });
55
-
56
- it('handles mixed {{tool.*}} and {{output.*}} in same command', () => {
57
- const result = renderHookCommand(
58
- 'run {{tool.script}} on {{output.files_changed}}',
59
- { script: 'check.sh' },
60
- { files_changed: 'src/foo.ts' }
61
- );
62
- expect(result).toBe('run check.sh on src/foo.ts');
63
- });
64
-
65
- it('leaves {{tool.*}} unchanged when outputContext not provided', () => {
66
- const result = renderHookCommand('echo {{tool.path}}', { path: 'x.ts' });
67
- expect(result).toBe('echo x.ts');
68
- });
69
- });
70
-
71
- describe('runStageHook', () => {
72
- it('returns success with stdout when command exits 0', async () => {
73
- const result = await runStageHook(
74
- { command: 'echo hello', on_failure: 'warn' },
75
- '/tmp'
76
- );
77
- expect(result.success).toBe(true);
78
- expect(result.stdout).toBe('hello');
79
- expect(result.stderr).toBe('');
80
- });
81
-
82
- it('returns failure with stderr when command exits non-zero', async () => {
83
- const result = await runStageHook(
84
- { command: 'sh -c "echo boom >&2; exit 1"', on_failure: 'warn' },
85
- '/tmp'
86
- );
87
- expect(result.success).toBe(false);
88
- expect(result.stderr).toContain('boom');
89
- });
90
-
91
- it('resolves {{output.files_changed}} from outputContext in command', async () => {
92
- const result = await runStageHook(
93
- { command: 'echo {{output.files_changed}}', on_failure: 'warn' },
94
- '/tmp',
95
- { files_changed: ['src/foo.ts', 'src/bar.ts'] }
96
- );
97
- expect(result.success).toBe(true);
98
- expect(result.stdout).toBe('src/foo.ts src/bar.ts');
99
- });
100
- });
101
-
102
- describe('runToolHook', () => {
103
- it('renders template and executes command', async () => {
104
- const result = await runToolHook(
105
- { matcher: 'repo_manager-write_file', command: 'echo {{tool.path}}', on_failure: 'warn' },
106
- { path: '/tmp/test.ts' },
107
- '/tmp'
108
- );
109
- expect(result.success).toBe(true);
110
- expect(result.stdout).toBe('/tmp/test.ts');
111
- });
112
-
113
- it('returns failure when rendered command exits non-zero', async () => {
114
- const result = await runToolHook(
115
- { matcher: 'any-tool', command: 'exit 1', on_failure: 'warn' },
116
- {},
117
- '/tmp'
118
- );
119
- expect(result.success).toBe(false);
120
- });
121
- });
@@ -1,87 +0,0 @@
1
- // Hook executor — runs shell commands at lifecycle points within a stage
2
- // Mirrors startup-executor.ts but with on_failure semantics and tool arg templates
3
-
4
- import { exec } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import type { StageHookDef, ToolHookDef } from '@studio-foundation/contracts';
7
-
8
- const execAsync = promisify(exec);
9
- const HOOK_TIMEOUT_MS = 30_000;
10
-
11
- export interface HookResult {
12
- success: boolean;
13
- stdout: string;
14
- stderr: string;
15
- }
16
-
17
- /**
18
- * Renders {{tool.argName}} and {{output.field}} placeholders.
19
- * Arrays in outputContext are space-joined for CLI argument passing.
20
- * Unknown keys → empty string.
21
- *
22
- * Note: values are substituted verbatim into the command string.
23
- * Hook commands are authored by pipeline owners (trusted), not end users.
24
- * Do not use with untrusted input sources.
25
- */
26
- export function renderHookCommand(
27
- command: string,
28
- toolArgs: Record<string, unknown>,
29
- outputContext: Record<string, unknown> = {}
30
- ): string {
31
- return command
32
- .replace(
33
- /\{\{tool\.(\w+)\}\}/g,
34
- (_, key: string) => (toolArgs[key] !== undefined ? String(toolArgs[key]) : '')
35
- )
36
- .replace(/\{\{output\.(\w+)\}\}/g, (_, key: string) => {
37
- const val = outputContext[key];
38
- if (val === undefined) return '';
39
- if (Array.isArray(val)) return val.join(' ');
40
- return String(val);
41
- });
42
- }
43
-
44
- /**
45
- * Run a stage-level hook command (on_stage_start, on_stage_complete).
46
- * outputContext provides {{output.<field>}} substitution values.
47
- * on_stage_start hooks omit outputContext (no output available before the stage runs).
48
- */
49
- export async function runStageHook(
50
- hook: StageHookDef,
51
- cwd: string,
52
- outputContext: Record<string, unknown> = {}
53
- ): Promise<HookResult> {
54
- const command = renderHookCommand(hook.command, {}, outputContext);
55
- return execHook(command, cwd);
56
- }
57
-
58
- /**
59
- * Run a tool-level hook command (pre_tool_use, post_tool_use).
60
- * The command may reference tool arguments via {{tool.argName}}.
61
- */
62
- export async function runToolHook(
63
- hook: ToolHookDef,
64
- toolArgs: Record<string, unknown>,
65
- cwd: string
66
- ): Promise<HookResult> {
67
- const command = renderHookCommand(hook.command, toolArgs);
68
- return execHook(command, cwd);
69
- }
70
-
71
- async function execHook(command: string, cwd: string): Promise<HookResult> {
72
- try {
73
- const { stdout, stderr } = await execAsync(command, {
74
- cwd,
75
- timeout: HOOK_TIMEOUT_MS,
76
- maxBuffer: 1024 * 1024 * 10,
77
- });
78
- return { success: true, stdout: stdout.trim(), stderr: stderr.trim() };
79
- } catch (err: unknown) {
80
- const e = err as { stdout?: string; stderr?: string };
81
- return {
82
- success: false,
83
- stdout: e.stdout?.trim() ?? '',
84
- stderr: e.stderr?.trim() ?? String(err),
85
- };
86
- }
87
- }