@studio-foundation/engine 0.4.0-beta → 0.6.0

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/README.md +1 -1
  2. package/dist/__tests__/engine.script-stage.test.js +13 -32
  3. package/dist/__tests__/engine.script-stage.test.js.map +1 -1
  4. package/dist/engine.d.ts +2 -0
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +139 -1
  7. package/dist/engine.js.map +1 -1
  8. package/dist/events.d.ts +50 -0
  9. package/dist/events.d.ts.map +1 -1
  10. package/dist/events.js.map +1 -1
  11. package/dist/index.d.ts +7 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +5 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/pipeline/call-input.d.ts +4 -0
  16. package/dist/pipeline/call-input.d.ts.map +1 -0
  17. package/dist/pipeline/call-input.js +28 -0
  18. package/dist/pipeline/call-input.js.map +1 -0
  19. package/dist/pipeline/call-orchestrator.d.ts +21 -0
  20. package/dist/pipeline/call-orchestrator.d.ts.map +1 -0
  21. package/dist/pipeline/call-orchestrator.js +127 -0
  22. package/dist/pipeline/call-orchestrator.js.map +1 -0
  23. package/dist/pipeline/condition-evaluator.d.ts +12 -0
  24. package/dist/pipeline/condition-evaluator.d.ts.map +1 -1
  25. package/dist/pipeline/condition-evaluator.js +27 -9
  26. package/dist/pipeline/condition-evaluator.js.map +1 -1
  27. package/dist/pipeline/context-propagation.d.ts +1 -0
  28. package/dist/pipeline/context-propagation.d.ts.map +1 -1
  29. package/dist/pipeline/context-propagation.js +18 -0
  30. package/dist/pipeline/context-propagation.js.map +1 -1
  31. package/dist/pipeline/contract-loader.d.ts.map +1 -1
  32. package/dist/pipeline/contract-loader.js +67 -0
  33. package/dist/pipeline/contract-loader.js.map +1 -1
  34. package/dist/pipeline/external-validator.d.ts +3 -0
  35. package/dist/pipeline/external-validator.d.ts.map +1 -0
  36. package/dist/pipeline/external-validator.js +65 -0
  37. package/dist/pipeline/external-validator.js.map +1 -0
  38. package/dist/pipeline/external-validator.test.d.ts +2 -0
  39. package/dist/pipeline/external-validator.test.d.ts.map +1 -0
  40. package/dist/pipeline/external-validator.test.js +40 -0
  41. package/dist/pipeline/external-validator.test.js.map +1 -0
  42. package/dist/pipeline/loader.d.ts.map +1 -1
  43. package/dist/pipeline/loader.js +158 -1
  44. package/dist/pipeline/loader.js.map +1 -1
  45. package/dist/pipeline/loader.test.js +30 -0
  46. package/dist/pipeline/loader.test.js.map +1 -1
  47. package/dist/pipeline/map-input.d.ts +13 -0
  48. package/dist/pipeline/map-input.d.ts.map +1 -0
  49. package/dist/pipeline/map-input.js +80 -0
  50. package/dist/pipeline/map-input.js.map +1 -0
  51. package/dist/pipeline/map-item-cache.d.ts +47 -0
  52. package/dist/pipeline/map-item-cache.d.ts.map +1 -0
  53. package/dist/pipeline/map-item-cache.js +0 -0
  54. package/dist/pipeline/map-item-cache.js.map +1 -0
  55. package/dist/pipeline/map-orchestrator.d.ts +42 -0
  56. package/dist/pipeline/map-orchestrator.d.ts.map +1 -0
  57. package/dist/pipeline/map-orchestrator.js +283 -0
  58. package/dist/pipeline/map-orchestrator.js.map +1 -0
  59. package/dist/pipeline/output-checker.d.ts +3 -0
  60. package/dist/pipeline/output-checker.d.ts.map +1 -0
  61. package/dist/pipeline/output-checker.js +40 -0
  62. package/dist/pipeline/output-checker.js.map +1 -0
  63. package/dist/pipeline/output-checker.test.d.ts +2 -0
  64. package/dist/pipeline/output-checker.test.d.ts.map +1 -0
  65. package/dist/pipeline/output-checker.test.js +62 -0
  66. package/dist/pipeline/output-checker.test.js.map +1 -0
  67. package/dist/pipeline/stage-executor.d.ts.map +1 -1
  68. package/dist/pipeline/stage-executor.js +48 -9
  69. package/dist/pipeline/stage-executor.js.map +1 -1
  70. package/dist/pipeline/stage-resolver.js +1 -1
  71. package/dist/pipeline/stage-resolver.js.map +1 -1
  72. package/dist/pipeline/strict-fields.d.ts +20 -0
  73. package/dist/pipeline/strict-fields.d.ts.map +1 -0
  74. package/dist/pipeline/strict-fields.js +62 -0
  75. package/dist/pipeline/strict-fields.js.map +1 -0
  76. package/dist/pipeline/task-input.d.ts +14 -0
  77. package/dist/pipeline/task-input.d.ts.map +1 -0
  78. package/dist/pipeline/task-input.js +22 -0
  79. package/dist/pipeline/task-input.js.map +1 -0
  80. package/dist/pipeline/task-input.test.d.ts +2 -0
  81. package/dist/pipeline/task-input.test.d.ts.map +1 -0
  82. package/dist/pipeline/task-input.test.js +33 -0
  83. package/dist/pipeline/task-input.test.js.map +1 -0
  84. package/dist/pipeline/template-interpolation.d.ts +9 -0
  85. package/dist/pipeline/template-interpolation.d.ts.map +1 -0
  86. package/dist/pipeline/template-interpolation.js +33 -0
  87. package/dist/pipeline/template-interpolation.js.map +1 -0
  88. package/dist/state/status-derivation.js +3 -3
  89. package/dist/state/status-derivation.js.map +1 -1
  90. package/package.json +5 -5
@@ -0,0 +1,283 @@
1
+ // MapOrchestrator — executes a fan-out (map) stage.
2
+ //
3
+ // A map stage runs a sub-pipeline once per item of a list and collects the
4
+ // structured outputs. It replaces the "shell `studio run <pipeline>` per item
5
+ // + parse stdout + scrape the run log" glue: child runs are spawned in-process
6
+ // through the engine's RunSpawner and each returns its last-stage output
7
+ // directly — no scraping, no tempfiles.
8
+ //
9
+ // Concurrency is bounded by `concurrency` (default 1 = sequential). The
10
+ // per-item failure policy is `on_item_failure`:
11
+ // - fail-fast (default): stop launching new items on the first failure; the
12
+ // stage fails. In-flight items are allowed to finish.
13
+ // - collect-all: run every item regardless; the stage succeeds as long as at
14
+ // least one item succeeded (or the list was empty). Per-item failures are
15
+ // surfaced in the output, never fatal — the pipeline keeps going.
16
+ import { randomUUID } from 'node:crypto';
17
+ import { resolveContextPath, evaluateCondition } from './condition-evaluator.js';
18
+ import { buildItemInput, mapItemLabel } from './map-input.js';
19
+ import { hashItemInput } from './map-item-cache.js';
20
+ function errorMessage(err) {
21
+ return err instanceof Error ? err.message : String(err);
22
+ }
23
+ export class MapOrchestrator {
24
+ config;
25
+ constructor(config) {
26
+ this.config = config;
27
+ }
28
+ async run(map, context, stageIndex, totalStages, runId, depth, pipelineName, signal) {
29
+ const startedAt = new Date().toISOString();
30
+ const stageRun = {
31
+ id: randomUUID(),
32
+ stage_name: map.map,
33
+ status: 'running',
34
+ started_at: startedAt,
35
+ tasks: [],
36
+ };
37
+ const finish = (status, output, skippedReason) => {
38
+ stageRun.status = status;
39
+ stageRun.completed_at = new Date().toISOString();
40
+ if (output !== undefined)
41
+ stageRun.output = output;
42
+ if (skippedReason)
43
+ stageRun.skipped_reason = skippedReason;
44
+ this.config.events?.onStageComplete?.({
45
+ stage_name: map.map,
46
+ stage_index: stageIndex,
47
+ total_stages: totalStages,
48
+ status,
49
+ attempts: 1,
50
+ duration_ms: Date.now() - new Date(startedAt).getTime(),
51
+ ...(output ? { output } : {}),
52
+ ...(skippedReason ? { skipped_reason: skippedReason } : {}),
53
+ });
54
+ this.config.emitter.emit({ type: 'stage_complete', stageId: stageRun.id, stageName: map.map });
55
+ return { status, stageRun, output };
56
+ };
57
+ // Technical failure (bad `over`, missing spawner, depth limit) — record the
58
+ // reason on the stage as a failed task so it surfaces in `studio status`,
59
+ // the run JSONL, and the CLI's "Errors:" line, then fail the stage.
60
+ const failTechnical = (reason) => {
61
+ const now = new Date().toISOString();
62
+ const taskRun = {
63
+ id: randomUUID(),
64
+ task_name: map.map,
65
+ status: 'failed',
66
+ started_at: startedAt,
67
+ completed_at: now,
68
+ agent_runs: [{
69
+ id: randomUUID(),
70
+ agent_name: `map:${map.pipeline}`,
71
+ attempt: 1,
72
+ status: 'failed',
73
+ tool_calls: 0,
74
+ started_at: startedAt,
75
+ completed_at: now,
76
+ error: reason,
77
+ }],
78
+ };
79
+ stageRun.tasks = [taskRun];
80
+ return finish('failed');
81
+ };
82
+ this.config.events?.onStageStart?.({
83
+ stage_name: map.map,
84
+ stage_index: stageIndex,
85
+ total_stages: totalStages,
86
+ max_attempts: 1,
87
+ });
88
+ this.config.emitter.emit({ type: 'stage_start', stageId: stageRun.id, stageName: map.map });
89
+ // Condition — skip the whole fan-out if false
90
+ if (map.condition !== undefined) {
91
+ const shouldRun = evaluateCondition(map.condition, {
92
+ input: context.input,
93
+ stageOutputs: context.stageOutputs,
94
+ });
95
+ if (!shouldRun) {
96
+ return finish('skipped', undefined, `condition not met: ${map.condition}`);
97
+ }
98
+ }
99
+ if (signal?.aborted)
100
+ return finish('cancelled');
101
+ // A map stage spawns child runs — it needs a spawner.
102
+ if (!this.config.spawner) {
103
+ return failTechnical(`Map stage '${map.map}' requires a run spawner, but none is configured. ` +
104
+ `Fan-out stages spawn sub-pipeline runs; construct the engine with a 'spawner'.`);
105
+ }
106
+ const spawner = this.config.spawner;
107
+ // Resolve the list to iterate.
108
+ const resolved = resolveContextPath(map.over, {
109
+ input: context.input,
110
+ stageOutputs: context.stageOutputs,
111
+ });
112
+ if (!Array.isArray(resolved)) {
113
+ return failTechnical(`Map stage '${map.map}': 'over: ${map.over}' did not resolve to an array ` +
114
+ `(got ${resolved === undefined ? 'undefined' : typeof resolved}). ` +
115
+ `It must reference a list, e.g. stages.<name>.output.items or input.items.`);
116
+ }
117
+ const items = resolved;
118
+ const concurrency = Math.max(1, map.concurrency ?? 1);
119
+ const failFast = (map.on_item_failure ?? 'fail-fast') === 'fail-fast';
120
+ this.config.events?.onMapStart?.({ map_name: map.map, total_items: items.length, concurrency });
121
+ this.config.emitter.emit({ type: 'map_start', mapName: map.map, totalItems: items.length });
122
+ if (items.length === 0) {
123
+ const emptyOutput = { total: 0, succeeded: 0, failed: 0, resumed: 0, results: [], outputs: [] };
124
+ this.config.events?.onMapComplete?.({ map_name: map.map, total: 0, succeeded: 0, failed: 0, status: 'success' });
125
+ this.config.emitter.emit({ type: 'map_complete', mapName: map.map, succeeded: 0, failed: 0, status: 'success' });
126
+ return finish('success', emptyOutput);
127
+ }
128
+ // Depth guard — mirror studio_run's recursion limit.
129
+ if (depth + 1 > this.config.maxDepth) {
130
+ return failTechnical(`Map stage '${map.map}' would spawn runs at depth ${depth + 1}, exceeding maxDepth ${this.config.maxDepth}. ` +
131
+ `Recursive fan-out is not allowed at this nesting level.`);
132
+ }
133
+ // Per-item resume — skip items a previous run already completed. Opt-in
134
+ // (`resume: true`) and a no-op without a configured cache. The key is the
135
+ // item *input*, so a reordered/filtered list still hits and a changed item
136
+ // misses (see MapStage.resume docs). Failures are never cached, so they
137
+ // retry next run; a cache-served item counts as a success and never trips
138
+ // fail-fast.
139
+ const resumeEnabled = map.resume === true && this.config.cache !== undefined;
140
+ const cache = this.config.cache;
141
+ const namespace = {
142
+ pipeline: pipelineName,
143
+ stage: map.map,
144
+ subPipeline: map.pipeline,
145
+ };
146
+ const results = new Array(items.length);
147
+ let cursor = 0;
148
+ let abortLaunch = false; // fail-fast: stop pulling new items after a failure
149
+ const worker = async () => {
150
+ for (;;) {
151
+ if (signal?.aborted || abortLaunch)
152
+ return;
153
+ const i = cursor++;
154
+ if (i >= items.length)
155
+ return;
156
+ const label = mapItemLabel(items[i], i);
157
+ // Resume check — before announcing the item as started, so a cache hit
158
+ // is served without a spinner. buildItemInput can throw for a bad item;
159
+ // fall through to the normal path, which surfaces that as an item failure.
160
+ let itemInput;
161
+ let cached;
162
+ if (resumeEnabled && cache) {
163
+ try {
164
+ itemInput = buildItemInput(map, items[i], i, context.input);
165
+ cached = await cache.get(namespace, hashItemInput(itemInput));
166
+ }
167
+ catch {
168
+ itemInput = undefined;
169
+ cached = undefined;
170
+ }
171
+ }
172
+ if (cached) {
173
+ const itemResult = {
174
+ index: i,
175
+ status: 'success',
176
+ output: cached.output,
177
+ cached: true,
178
+ ...(cached.run_id ? { run_id: cached.run_id } : {}),
179
+ };
180
+ results[i] = itemResult;
181
+ this.config.events?.onMapItemComplete?.({
182
+ map_name: map.map,
183
+ index: i,
184
+ total_items: items.length,
185
+ status: 'success',
186
+ label,
187
+ cached: true,
188
+ ...(cached.run_id ? { run_id: cached.run_id } : {}),
189
+ });
190
+ this.config.emitter.emit({ type: 'map_item_complete', mapName: map.map, index: i, status: 'success' });
191
+ continue;
192
+ }
193
+ // Name the item as it goes in flight, so a --live operator sees which
194
+ // items are running right now (not just an advancing count).
195
+ this.config.events?.onMapItemStart?.({
196
+ map_name: map.map,
197
+ index: i,
198
+ total_items: items.length,
199
+ label,
200
+ });
201
+ let itemResult;
202
+ try {
203
+ const input = itemInput ?? buildItemInput(map, items[i], i, context.input);
204
+ const spawn = await spawner.spawnAndWait({
205
+ pipeline: map.pipeline,
206
+ input,
207
+ parentRunId: runId,
208
+ depth: depth + 1,
209
+ });
210
+ itemResult = { index: i, status: 'success', output: spawn.output, run_id: spawn.run_id };
211
+ // Only successful items are cached — a failure stays un-cached so it
212
+ // retries next run.
213
+ if (resumeEnabled && cache) {
214
+ await cache.set(namespace, hashItemInput(input), {
215
+ output: spawn.output,
216
+ run_id: spawn.run_id,
217
+ cached_at: new Date().toISOString(),
218
+ });
219
+ }
220
+ }
221
+ catch (err) {
222
+ itemResult = { index: i, status: 'failed', error: errorMessage(err) };
223
+ if (failFast)
224
+ abortLaunch = true;
225
+ }
226
+ results[i] = itemResult;
227
+ this.config.events?.onMapItemComplete?.({
228
+ map_name: map.map,
229
+ index: i,
230
+ total_items: items.length,
231
+ status: itemResult.status,
232
+ label,
233
+ ...(itemResult.run_id ? { run_id: itemResult.run_id } : {}),
234
+ ...(itemResult.error ? { error: itemResult.error } : {}),
235
+ });
236
+ this.config.emitter.emit({ type: 'map_item_complete', mapName: map.map, index: i, status: itemResult.status });
237
+ }
238
+ };
239
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
240
+ // Cancelled mid-flight → cancelled (don't report success/failure of a partial batch).
241
+ if (signal?.aborted)
242
+ return finish('cancelled');
243
+ const settled = results.filter((r) => r !== undefined);
244
+ settled.sort((a, b) => a.index - b.index);
245
+ const succeeded = settled.filter(r => r.status === 'success');
246
+ const failed = settled.filter(r => r.status === 'failed');
247
+ const output = {
248
+ total: items.length,
249
+ succeeded: succeeded.length,
250
+ failed: failed.length,
251
+ resumed: succeeded.filter(r => r.cached).length,
252
+ results: settled,
253
+ outputs: succeeded.map(r => r.output),
254
+ };
255
+ // Status policy:
256
+ // fail-fast → any failure fails the stage.
257
+ // collect-all → succeeds as long as at least one item succeeded; a batch
258
+ // where every item failed is a real failure, not per-item noise.
259
+ let status;
260
+ if (failFast) {
261
+ status = failed.length > 0 ? 'failed' : 'success';
262
+ }
263
+ else {
264
+ status = succeeded.length > 0 ? 'success' : 'failed';
265
+ }
266
+ this.config.events?.onMapComplete?.({
267
+ map_name: map.map,
268
+ total: items.length,
269
+ succeeded: succeeded.length,
270
+ failed: failed.length,
271
+ status,
272
+ });
273
+ this.config.emitter.emit({
274
+ type: 'map_complete',
275
+ mapName: map.map,
276
+ succeeded: succeeded.length,
277
+ failed: failed.length,
278
+ status,
279
+ });
280
+ return finish(status, output);
281
+ }
282
+ }
283
+ //# sourceMappingURL=map-orchestrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-orchestrator.js","sourceRoot":"","sources":["../../src/pipeline/map-orchestrator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { ExpectedOutputs, ValidationResult } from '@studio-foundation/contracts';
2
+ export declare function checkExpectedOutputs(expected: ExpectedOutputs | undefined, cwd: string): Promise<ValidationResult>;
3
+ //# sourceMappingURL=output-checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-checker.d.ts","sourceRoot":"","sources":["../../src/pipeline/output-checker.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEtF,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,gBAAgB,CAAC,CAa3B"}
@@ -0,0 +1,40 @@
1
+ // Post-execution filesystem check for a stage's declared outputs.
2
+ //
3
+ // A stage returning "success" only proves the agent finished — not that it
4
+ // actually wrote the files it was supposed to. This is the orchestrator
5
+ // responsibility that was previously bolted onto callers (e.g. run_wiki.py's
6
+ // `check_outputs`/`required_files`: "succeeded but expected files are missing").
7
+ // Declaring `expected_outputs.files` in the contract moves that check into the
8
+ // engine, where it belongs.
9
+ //
10
+ // Each entry is a path or glob relative to the repo workspace and must match at
11
+ // least one existing file. A miss is a validation error, so it flows into the
12
+ // RALPH loop like any other validator: the agent gets a retry with enriched
13
+ // feedback, and the stage fails only once attempts are exhausted.
14
+ import { glob } from 'node:fs/promises';
15
+ export async function checkExpectedOutputs(expected, cwd) {
16
+ if (!expected?.files || expected.files.length === 0) {
17
+ return { valid: true, errors: [], warnings: [] };
18
+ }
19
+ const errors = [];
20
+ for (const pattern of expected.files) {
21
+ if (!(await matchesAny(pattern, cwd))) {
22
+ errors.push(`Expected output missing: no file matches '${pattern}'`);
23
+ }
24
+ }
25
+ return { valid: errors.length === 0, errors, warnings: [] };
26
+ }
27
+ /** True as soon as one path matches the pattern under cwd. Fails closed on error. */
28
+ async function matchesAny(pattern, cwd) {
29
+ try {
30
+ for await (const _match of glob(pattern, { cwd })) {
31
+ return true;
32
+ }
33
+ }
34
+ catch {
35
+ // An unreadable directory or bad pattern → treat as "not found", not a crash.
36
+ return false;
37
+ }
38
+ return false;
39
+ }
40
+ //# sourceMappingURL=output-checker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-checker.js","sourceRoot":"","sources":["../../src/pipeline/output-checker.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2EAA2E;AAC3E,wEAAwE;AACxE,6EAA6E;AAC7E,iFAAiF;AACjF,+EAA+E;AAC/E,4BAA4B;AAC5B,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,4EAA4E;AAC5E,kEAAkE;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,QAAqC,EACrC,GAAW;IAEX,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IACnD,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QACrC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,IAAI,CAAC,6CAA6C,OAAO,GAAG,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAC9D,CAAC;AAED,qFAAqF;AACrF,KAAK,UAAU,UAAU,CAAC,OAAe,EAAE,GAAW;IACpD,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,8EAA8E;QAC9E,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=output-checker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-checker.test.d.ts","sourceRoot":"","sources":["../../src/pipeline/output-checker.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkExpectedOutputs } from './output-checker.js';
6
+ describe('checkExpectedOutputs', () => {
7
+ let dir;
8
+ beforeEach(async () => {
9
+ dir = await mkdtemp(join(tmpdir(), 'output-checker-'));
10
+ });
11
+ afterEach(async () => {
12
+ await rm(dir, { recursive: true, force: true });
13
+ });
14
+ it('is valid when there are no expected outputs', async () => {
15
+ expect(await checkExpectedOutputs(undefined, dir)).toEqual({
16
+ valid: true,
17
+ errors: [],
18
+ warnings: [],
19
+ });
20
+ expect((await checkExpectedOutputs({ files: [] }, dir)).valid).toBe(true);
21
+ });
22
+ it('passes when a declared literal file exists', async () => {
23
+ await writeFile(join(dir, 'wiki_pages.json'), '{}');
24
+ const res = await checkExpectedOutputs({ files: ['wiki_pages.json'] }, dir);
25
+ expect(res.valid).toBe(true);
26
+ expect(res.errors).toEqual([]);
27
+ });
28
+ it('fails when a declared literal file is missing', async () => {
29
+ const res = await checkExpectedOutputs({ files: ['wiki_pages.json'] }, dir);
30
+ expect(res.valid).toBe(false);
31
+ expect(res.errors).toEqual([
32
+ "Expected output missing: no file matches 'wiki_pages.json'",
33
+ ]);
34
+ });
35
+ it('passes when a glob pattern matches at least one file', async () => {
36
+ await writeFile(join(dir, 'batch_1.json'), '{}');
37
+ await writeFile(join(dir, 'batch_2.json'), '{}');
38
+ const res = await checkExpectedOutputs({ files: ['batch_*.json'] }, dir);
39
+ expect(res.valid).toBe(true);
40
+ });
41
+ it('fails when a glob pattern matches nothing', async () => {
42
+ const res = await checkExpectedOutputs({ files: ['batch_*.json'] }, dir);
43
+ expect(res.valid).toBe(false);
44
+ expect(res.errors[0]).toContain('batch_*.json');
45
+ });
46
+ it('matches files in nested directories', async () => {
47
+ await mkdir(join(dir, 'out'), { recursive: true });
48
+ await writeFile(join(dir, 'out', 'result.md'), '# hi');
49
+ expect((await checkExpectedOutputs({ files: ['out/result.md'] }, dir)).valid).toBe(true);
50
+ expect((await checkExpectedOutputs({ files: ['**/*.md'] }, dir)).valid).toBe(true);
51
+ });
52
+ it('reports one error per missing entry and passes the present ones', async () => {
53
+ await writeFile(join(dir, 'present.json'), '{}');
54
+ const res = await checkExpectedOutputs({ files: ['present.json', 'missing_a.json', 'missing_b.json'] }, dir);
55
+ expect(res.valid).toBe(false);
56
+ expect(res.errors).toHaveLength(2);
57
+ expect(res.errors.some((e) => e.includes('missing_a.json'))).toBe(true);
58
+ expect(res.errors.some((e) => e.includes('missing_b.json'))).toBe(true);
59
+ expect(res.errors.some((e) => e.includes('present.json'))).toBe(false);
60
+ });
61
+ });
62
+ //# sourceMappingURL=output-checker.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-checker.test.js","sourceRoot":"","sources":["../../src/pipeline/output-checker.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"stage-executor.d.ts","sourceRoot":"","sources":["../../src/pipeline/stage-executor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,eAAe,EAMhB,MAAM,8BAA8B,CAAC;AAgBtC,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAErB,uBAAuB,EACxB,MAAM,2BAA2B,CAAC;AAKnC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAMlC,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAY5D,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,mBAAmB;IAEzC,OAAO,CACX,QAAQ,EAAE,eAAe,EACzB,eAAe,EAAE,eAAe,EAChC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,YAAY,EACnB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,aAAa,CAAC,EAAE,uBAAuB,GAAG,IAAI,EAC9C,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC;IA6dvB,OAAO,CAAC,cAAc;IAsDtB,OAAO,CAAC,oBAAoB;YAgBd,aAAa;CAa5B"}
1
+ {"version":3,"file":"stage-executor.d.ts","sourceRoot":"","sources":["../../src/pipeline/stage-executor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,eAAe,EAMhB,MAAM,8BAA8B,CAAC;AAgBtC,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAErB,uBAAuB,EACxB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAQlC,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAY5D,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,mBAAmB;IAEzC,OAAO,CACX,QAAQ,EAAE,eAAe,EACzB,eAAe,EAAE,eAAe,EAChC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,YAAY,EACnB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,aAAa,CAAC,EAAE,uBAAuB,GAAG,IAAI,EAC9C,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC;IAofvB,OAAO,CAAC,cAAc;IAqEtB,OAAO,CAAC,oBAAoB;YAgBd,aAAa;CAa5B"}
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
5
  import { ralph, validateSchema, validateToolCalls, validateRequiredTools, validateCountedTools, validateToolGroups, compose, exponentialBackoff, fixedDelay, noDelay, } from '@studio-foundation/ralph';
6
6
  import { runAgent, runScript, AnonymizationMiddleware, } from '@studio-foundation/runner';
7
+ import { buildTaskInput } from './task-input.js';
7
8
  import { loadAgentProfile } from './agent-loader.js';
8
9
  import { loadContract } from './contract-loader.js';
9
10
  import { loadSkillFiles } from './skill-loader.js';
@@ -13,6 +14,8 @@ import { evaluateCondition } from './condition-evaluator.js';
13
14
  import { deriveStageStatus } from '../state/status-derivation.js';
14
15
  import { transition } from '../state/state-machine.js';
15
16
  import { postValidate } from './post-validator.js';
17
+ import { runExternalValidators } from './external-validator.js';
18
+ import { checkExpectedOutputs } from './output-checker.js';
16
19
  import { loadContextPacks } from './context-pack-loader.js';
17
20
  // Module-level helpers — verbatim from engine.ts lines 73-95
18
21
  function summarizeOutput(output) {
@@ -139,7 +142,7 @@ export class StageExecutor {
139
142
  };
140
143
  this.config.events.onStageContext(contextEvent);
141
144
  }
142
- // Create a single task run (v7: 1 stage = 1 task = 1 ralph call)
145
+ // Create a single task run (1 stage = 1 task = 1 ralph call)
143
146
  const taskRun = {
144
147
  id: randomUUID(),
145
148
  task_name: stageDef.name,
@@ -246,10 +249,7 @@ export class StageExecutor {
246
249
  executor: async (execContext) => {
247
250
  const agentRunId = randomUUID();
248
251
  const agentRunStartedAt = new Date().toISOString();
249
- const taskInput = {
250
- description: typeof userInput === 'string' ? userInput : JSON.stringify(userInput),
251
- contract_name: contract?.name,
252
- };
252
+ const taskInput = buildTaskInput(userInput, contract?.name);
253
253
  // Map ralph context to runner context
254
254
  const runnerExecContext = {
255
255
  attempt: execContext.attempt,
@@ -299,16 +299,21 @@ export class StageExecutor {
299
299
  cwd: this.config.repoPath ?? this.config.configsDir,
300
300
  timeoutMs: stageDef.timeout_ms,
301
301
  });
302
- // Record agent run
302
+ // Record agent run. A terminal executor error (a crashed script, max
303
+ // tool iterations) must land on the run as status 'failed' + error —
304
+ // otherwise the reason the runner already captured (e.g. a python
305
+ // stage's stderr) never reaches `studio status`, the run JSONL, or the
306
+ // CLI's "Errors:" line, which read the last agent run's error. (STU-568)
303
307
  const agentRun = {
304
308
  id: agentRunId,
305
309
  agent_name: agentConfig?.name ?? `script:${stageDef.script ?? 'unknown'}`,
306
310
  attempt: execContext.attempt,
307
- status: 'success',
311
+ status: result.error ? 'failed' : 'success',
308
312
  tool_calls: result.tool_calls_count,
309
313
  started_at: agentRunStartedAt,
310
314
  completed_at: new Date().toISOString(),
311
315
  output: result.output,
316
+ ...(result.error ? { error: result.error } : {}),
312
317
  };
313
318
  taskRun.agent_runs.push(agentRun);
314
319
  return result;
@@ -316,6 +321,12 @@ export class StageExecutor {
316
321
  validator: ralphValidator,
317
322
  maxAttempts: stageDef.ralph?.max_attempts ?? 3,
318
323
  retryStrategy,
324
+ // A script stage is deterministic: re-running the same command on the
325
+ // same stdin yields the same result, so a process-level error (a startup
326
+ // crash, a non-zero exit, a timeout) will not heal on retry. Fail fast
327
+ // instead of burning every attempt on an identical crash. Agent stages
328
+ // are stochastic and keep their retries. (STU-568)
329
+ isFatal: stageDef.agent ? undefined : (result) => result.error != null,
319
330
  signal,
320
331
  onRetry: async (event) => {
321
332
  // Extract raw output for diagnostic logging
@@ -343,6 +354,21 @@ export class StageExecutor {
343
354
  catch (err) {
344
355
  // AbortError from signal propagation is handled inside ralph (returns 'cancelled').
345
356
  // Any other throw is a technical failure (network, timeout, etc.) — mark stage failed.
357
+ // Record it as a failed agent run: the executor throws before it can push one, so
358
+ // without this every reader (`studio status`, `logs`, the run JSONL, the CLI's own
359
+ // "Errors:" line) reports a stage that failed for no stated reason (STU-561).
360
+ taskRun.agent_runs.push({
361
+ id: randomUUID(),
362
+ agent_name: agentConfig?.name ?? `script:${stageDef.script ?? 'unknown'}`,
363
+ attempt: 1,
364
+ status: 'failed',
365
+ tool_calls: 0,
366
+ started_at: stageStartedAt,
367
+ completed_at: new Date().toISOString(),
368
+ error: err instanceof Error ? err.message : String(err),
369
+ });
370
+ taskRun.status = 'failed';
371
+ taskRun.completed_at = new Date().toISOString();
346
372
  stageRun.status = transition('running', 'fail');
347
373
  stageRun.completed_at = new Date().toISOString();
348
374
  stageRun.tasks = [taskRun];
@@ -478,8 +504,8 @@ export class StageExecutor {
478
504
  }
479
505
  // Compose ralph's built-in validators
480
506
  const validators = [errorCheck];
481
- // Schema validation (required_fields)
482
- if (contract.schema?.required_fields) {
507
+ // Schema validation (required_fields presence + field-level type/enum/nested)
508
+ if (contract.schema?.required_fields || contract.schema?.fields) {
483
509
  validators.push((result) => validateSchema(result.output, contract));
484
510
  }
485
511
  // Normalize tool call requirements from contract or stage definition
@@ -501,6 +527,19 @@ export class StageExecutor {
501
527
  if (toolCallReqs?.required_tool_groups?.length) {
502
528
  validators.push((result) => validateToolGroups(result.tool_calls, toolCallReqs));
503
529
  }
530
+ // External validators — run a command against the REAL output (not tool args).
531
+ // Enforces what the declarative schema can't (cross-field rules, computed
532
+ // constraints); field-level types/enums/nesting live in schema.fields.
533
+ if (contract.validators?.length) {
534
+ const cwd = this.config.repoPath ?? this.config.configsDir;
535
+ validators.push((result) => runExternalValidators(result.output, contract.validators, cwd));
536
+ }
537
+ // Expected outputs — check the filesystem for the files the stage must leave
538
+ // on disk. A "success" return code doesn't prove the artifacts exist; this does.
539
+ if (contract.expected_outputs?.files?.length) {
540
+ const cwd = this.config.repoPath ?? this.config.configsDir;
541
+ validators.push(() => checkExpectedOutputs(contract.expected_outputs, cwd));
542
+ }
504
543
  if (validators.length === 0) {
505
544
  return () => ({ valid: true, errors: [], warnings: [] });
506
545
  }