singleton-pipeline 0.4.0-beta.13 → 0.4.0-beta.14

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 (77) hide show
  1. package/dist/packages/cli/src/assets/singleton-logo.txt +10 -0
  2. package/dist/packages/cli/src/commands/new.js +763 -0
  3. package/dist/packages/cli/src/commands/repl.js +557 -0
  4. package/dist/packages/cli/src/commands/usage.js +49 -0
  5. package/dist/packages/cli/src/executor/debug-loop.js +525 -0
  6. package/dist/packages/cli/src/executor/inputs.js +226 -0
  7. package/dist/packages/cli/src/executor/outputs.js +134 -0
  8. package/dist/packages/cli/src/executor/preflight.js +605 -0
  9. package/dist/packages/cli/src/executor/replay-loop.js +120 -0
  10. package/dist/packages/cli/src/executor/run-report.js +209 -0
  11. package/dist/packages/cli/src/executor/run-setup.js +114 -0
  12. package/dist/packages/cli/src/executor/security-review.js +97 -0
  13. package/dist/packages/cli/src/executor/snapshot-manager.js +349 -0
  14. package/dist/packages/cli/src/executor/step-runner.js +241 -0
  15. package/dist/packages/cli/src/executor.js +584 -0
  16. package/dist/packages/cli/src/index.js +107 -0
  17. package/dist/packages/cli/src/parser.js +89 -0
  18. package/dist/packages/cli/src/runners/_shared.js +96 -0
  19. package/dist/packages/cli/src/runners/claude.js +103 -0
  20. package/dist/packages/cli/src/runners/codex-instructions.js +69 -0
  21. package/dist/packages/cli/src/runners/codex.js +141 -0
  22. package/dist/packages/cli/src/runners/copilot.js +209 -0
  23. package/dist/packages/cli/src/runners/index.js +18 -0
  24. package/dist/packages/cli/src/runners/opencode.js +240 -0
  25. package/dist/packages/cli/src/scanner.js +43 -0
  26. package/dist/packages/cli/src/security/policy.js +115 -0
  27. package/dist/packages/cli/src/sentinels.js +1 -0
  28. package/dist/packages/cli/src/shell.js +753 -0
  29. package/dist/packages/cli/src/theme.js +39 -0
  30. package/dist/packages/cli/src/timeline.js +238 -0
  31. package/dist/packages/cli/src/types.js +1 -0
  32. package/dist/packages/cli/src/usage/aggregator.js +44 -0
  33. package/dist/packages/cli/src/usage/reader.js +30 -0
  34. package/dist/packages/cli/src/usage/types.js +1 -0
  35. package/dist/packages/server/src/index.js +36 -0
  36. package/dist/packages/server/src/routes/agents.js +31 -0
  37. package/dist/packages/server/src/routes/files.js +45 -0
  38. package/dist/packages/server/src/routes/pipelines.js +74 -0
  39. package/docs/reference.md +28 -0
  40. package/package.json +15 -14
  41. package/packages/web/dist/assets/{index-CnKytBly.js → index-9S0goZlQ.js} +1 -1
  42. package/packages/web/dist/assets/{index-CCFWfCA2.css → index-iV4UtXoN.css} +1 -1
  43. package/packages/web/dist/assets/logo-COSyZmgk.png +0 -0
  44. package/packages/web/dist/index.html +2 -2
  45. package/packages/cli/package.json +0 -18
  46. package/packages/cli/src/commands/new.js +0 -786
  47. package/packages/cli/src/commands/repl.js +0 -548
  48. package/packages/cli/src/executor/debug-loop.js +0 -587
  49. package/packages/cli/src/executor/inputs.js +0 -202
  50. package/packages/cli/src/executor/outputs.js +0 -140
  51. package/packages/cli/src/executor/preflight.js +0 -459
  52. package/packages/cli/src/executor/replay-loop.js +0 -172
  53. package/packages/cli/src/executor/run-report.js +0 -189
  54. package/packages/cli/src/executor/run-setup.js +0 -93
  55. package/packages/cli/src/executor/security-review.js +0 -108
  56. package/packages/cli/src/executor/snapshot-manager.js +0 -335
  57. package/packages/cli/src/executor/step-runner.js +0 -266
  58. package/packages/cli/src/executor.js +0 -652
  59. package/packages/cli/src/index.js +0 -107
  60. package/packages/cli/src/parser.js +0 -78
  61. package/packages/cli/src/runners/_shared.js +0 -83
  62. package/packages/cli/src/runners/claude.js +0 -122
  63. package/packages/cli/src/runners/codex-instructions.js +0 -75
  64. package/packages/cli/src/runners/codex.js +0 -165
  65. package/packages/cli/src/runners/copilot.js +0 -224
  66. package/packages/cli/src/runners/index.js +0 -20
  67. package/packages/cli/src/runners/opencode.js +0 -265
  68. package/packages/cli/src/scanner.js +0 -47
  69. package/packages/cli/src/security/policy.js +0 -126
  70. package/packages/cli/src/shell.js +0 -732
  71. package/packages/cli/src/theme.js +0 -46
  72. package/packages/cli/src/timeline.js +0 -180
  73. package/packages/server/package.json +0 -11
  74. package/packages/server/src/index.js +0 -43
  75. package/packages/server/src/routes/agents.js +0 -32
  76. package/packages/server/src/routes/files.js +0 -42
  77. package/packages/server/src/routes/pipelines.js +0 -74
@@ -0,0 +1,584 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parseAgentFileDetailed } from './parser.js';
4
+ import { G, S } from './shell.js';
5
+ import { getRunner } from './runners/index.js';
6
+ import { loadProjectSecurityConfig, resolveSecurityPolicyWithConfig, } from './security/policy.js';
7
+ import { SnapshotManager, } from './executor/snapshot-manager.js';
8
+ import { resolveDebugInputOverridesFromEdit, resolveInput, } from './executor/inputs.js';
9
+ import { moveAttemptArtifactsToAttemptDir, } from './executor/outputs.js';
10
+ import { resolveModel, resolvePermissionMode, resolveProvider, resolveRunnerAgent, runPreflightChecks, } from './executor/preflight.js';
11
+ import { DEFAULT_MAX_DEBUG_REPLAYS, logSnapshotCoverage, promptDebugPostStepDecision, promptDebugStepDecision, pushDebugEvent, } from './executor/debug-loop.js';
12
+ import { runStepAttempt, } from './executor/step-runner.js';
13
+ import { renderRunSummary, writeLatestRunPointer, writeRunManifest, } from './executor/run-report.js';
14
+ import { getViolationDiffPreview, handlePostRunViolations, } from './executor/security-review.js';
15
+ import { collectPipelineInputs, createRunTimeline, createRunWorkspace, isNonInteractiveRuntime, loadPipeline, logRunStart, resolveProjectRoot, } from './executor/run-setup.js';
16
+ import { prepareReplayAttempt, } from './executor/replay-loop.js';
17
+ export { detectSnapshotChanges } from './executor/snapshot-manager.js';
18
+ export { validatePostRunChanges } from './executor/step-runner.js';
19
+ export { loadPipeline } from './executor/run-setup.js';
20
+ /**
21
+ * @param {TimelineController} timeline
22
+ * @param {number} index
23
+ * @param {unknown} shortMessage
24
+ * @param {unknown} [fullMessage]
25
+ * @returns {never}
26
+ */
27
+ function failStep(timeline, index, shortMessage, fullMessage = shortMessage) {
28
+ timeline.setError(index, String(shortMessage).slice(0, 60));
29
+ throw new Error(String(fullMessage));
30
+ }
31
+ /**
32
+ * @param {unknown} s
33
+ * @returns {string}
34
+ */
35
+ function stripBlessedTags(s) {
36
+ return String(s || '').replace(/\{[^}]+\}/g, '');
37
+ }
38
+ /**
39
+ * @param {string} label
40
+ * @returns {string}
41
+ */
42
+ function sectionTitle(label) {
43
+ return `${G.hline}${G.hline} ${label} ${G.hline}${G.hline}`;
44
+ }
45
+ /**
46
+ * @param {string} filePath
47
+ * @param {{ dryRun?: boolean, verbose?: boolean, debug?: boolean, shell?: any, quiet?: boolean, nonInteractive?: boolean, maxDebugReplays?: number, debugDecision?: Function, debugPostDecision?: Function }} [opts]
48
+ */
49
+ export async function runPipeline(filePath, opts = {}) {
50
+ const abs = path.resolve(filePath);
51
+ const pipeline = await loadPipeline(abs);
52
+ const pipelineDir = path.dirname(abs);
53
+ const cwd = resolveProjectRoot(pipelineDir);
54
+ const dryRun = !!opts.dryRun;
55
+ const verbose = !!opts.verbose;
56
+ const debug = !!opts.debug;
57
+ const shell = opts.shell || null;
58
+ const quiet = !!opts.quiet;
59
+ const nonInteractive = isNonInteractiveRuntime({ shell, nonInteractive: opts.nonInteractive ?? null });
60
+ const requestedMaxDebugReplays = opts.maxDebugReplays;
61
+ const maxDebugReplays = Number.isInteger(requestedMaxDebugReplays)
62
+ ? Math.max(0, Number(requestedMaxDebugReplays))
63
+ : DEFAULT_MAX_DEBUG_REPLAYS;
64
+ const securityConfig = await loadProjectSecurityConfig(cwd);
65
+ const snapshotManager = dryRun ? null : await SnapshotManager.create({ root: cwd });
66
+ const beforeSnapshot = snapshotManager ? await snapshotManager.captureState() : null;
67
+ let currentSnapshot = beforeSnapshot;
68
+ const { runId, runDir } = await createRunWorkspace({ cwd, pipeline, dryRun, debug });
69
+ logRunStart({ pipeline, cwd, runDir, dryRun, debug, shell, quiet });
70
+ const { inputDefs, inputValues } = await collectPipelineInputs({ pipeline, dryRun, shell, nonInteractive, quiet });
71
+ const timeline = createRunTimeline({ pipeline, quiet, shell, nonInteractive });
72
+ const registry = {};
73
+ const fileWrites = [];
74
+ const stats = [];
75
+ const debugEvents = [];
76
+ const debugInputOverrides = {};
77
+ let runError = null;
78
+ try {
79
+ timeline.setRunning(0);
80
+ const preflightStarted = Date.now();
81
+ const preflight = await runPreflightChecks({ pipeline, cwd, inputDefs, inputValues, dryRun, securityConfig });
82
+ const preflightSeconds = (Date.now() - preflightStarted) / 1000;
83
+ if (preflight.infos.length) {
84
+ timeline.log(sectionTitle('preflight info'));
85
+ for (const info of preflight.infos)
86
+ timeline.logMuted(info);
87
+ }
88
+ if (preflight.securityHighlights.length) {
89
+ timeline.log(sectionTitle('security profile preview'));
90
+ for (const item of preflight.securityHighlights)
91
+ timeline.logMuted(item);
92
+ }
93
+ if (preflight.warnings.length) {
94
+ timeline.log(sectionTitle('preflight warnings'));
95
+ for (const warning of preflight.warnings)
96
+ timeline.logMuted(warning);
97
+ }
98
+ if (!preflight.ok) {
99
+ timeline.log(sectionTitle('preflight errors'));
100
+ for (const error of preflight.errors)
101
+ timeline.logMuted(error);
102
+ stats.push({
103
+ agent: 'preflight checks',
104
+ provider: 'system',
105
+ model: '—',
106
+ securityProfile: '—',
107
+ permissionMode: '—',
108
+ status: 'failed',
109
+ seconds: preflightSeconds,
110
+ turns: 0,
111
+ cost: 0,
112
+ });
113
+ failStep(timeline, 0, `${preflight.errors.length} error${preflight.errors.length > 1 ? 's' : ''}`, `Preflight checks failed:\n- ${preflight.errors.join('\n- ')}`);
114
+ }
115
+ timeline.setDone(0, `${preflightSeconds.toFixed(1)}s ${G.bullet} ${preflight.providerCount} provider${preflight.providerCount > 1 ? 's' : ''}`);
116
+ timeline.logSuccess(`${G.success} preflight checks - ${preflight.providerCount} provider${preflight.providerCount > 1 ? 's' : ''}`);
117
+ stats.push({
118
+ agent: 'preflight checks',
119
+ provider: 'system',
120
+ model: '—',
121
+ securityProfile: '—',
122
+ permissionMode: '—',
123
+ status: 'done',
124
+ seconds: preflightSeconds,
125
+ turns: 0,
126
+ cost: 0,
127
+ });
128
+ for (let i = 0; i < pipeline.steps.length; i++) {
129
+ const step = pipeline.steps[i];
130
+ const timelineIndex = i + 1;
131
+ if (!step.agent_file) {
132
+ failStep(timeline, timelineIndex, 'no agent_file', `Step "${step.agent}" is missing agent_file.`);
133
+ }
134
+ const agentFilePath = path.isAbsolute(step.agent_file)
135
+ ? step.agent_file
136
+ : path.resolve(cwd, step.agent_file);
137
+ const raw = await fs.readFile(agentFilePath, 'utf8');
138
+ const { agent, error } = parseAgentFileDetailed(raw, agentFilePath);
139
+ if (!agent) {
140
+ failStep(timeline, timelineIndex, `failed to parse ${step.agent_file}`, `Failed to parse agent file: ${step.agent_file}${error ? ` (${error})` : ''}`);
141
+ }
142
+ const outputNames = Object.keys(step.outputs || {});
143
+ if (outputNames.length === 0) {
144
+ const provider = resolveProvider(step, agent);
145
+ const model = resolveModel(step, agent);
146
+ const runnerAgent = resolveRunnerAgent(step, agent);
147
+ timeline.setDone(timelineIndex, 'skipped (no outputs)');
148
+ stats.push({
149
+ agent: step.agent,
150
+ provider,
151
+ model: model || '—',
152
+ runnerAgent: runnerAgent || '—',
153
+ securityProfile: resolveSecurityPolicyWithConfig(step, agent, securityConfig).profile,
154
+ permissionMode: step.permission_mode || agent.permission_mode || '—',
155
+ status: 'skipped',
156
+ seconds: 0,
157
+ turns: 0,
158
+ cost: 0,
159
+ });
160
+ continue;
161
+ }
162
+ if (dryRun) {
163
+ const provider = resolveProvider(step, agent);
164
+ const model = resolveModel(step, agent);
165
+ const runnerAgent = resolveRunnerAgent(step, agent);
166
+ const permissionMode = resolvePermissionMode(step, agent);
167
+ const securityPolicy = resolveSecurityPolicyWithConfig(step, agent, securityConfig);
168
+ timeline.setDone(timelineIndex, `dry-run ${G.bullet} ${outputNames.join(', ')}`);
169
+ for (const name of outputNames)
170
+ registry[`${step.agent}.${name}`] = `(dry-run:${step.agent}.${name})`;
171
+ stats.push({
172
+ agent: step.agent,
173
+ provider,
174
+ model: model || '—',
175
+ runnerAgent: runnerAgent || '—',
176
+ securityProfile: securityPolicy.profile,
177
+ permissionMode: permissionMode || '—',
178
+ status: 'dry-run',
179
+ seconds: 0,
180
+ turns: 0,
181
+ cost: 0,
182
+ });
183
+ continue;
184
+ }
185
+ const stepIndex = String(i + 1).padStart(2, '0');
186
+ const stepDir = runDir ? path.join(runDir, `${stepIndex}-${step.agent}`) : null;
187
+ if (stepDir)
188
+ await fs.mkdir(stepDir, { recursive: true });
189
+ let resolvedInputs = {};
190
+ const runtimeInputValues = debug
191
+ ? { ...inputValues, ...debugInputOverrides }
192
+ : inputValues;
193
+ for (const [name, spec] of Object.entries(step.inputs || {})) {
194
+ resolvedInputs[name] = await resolveInput(spec, { registry, cwd, inputValues: runtimeInputValues, inputDefs });
195
+ }
196
+ const provider = resolveProvider(step, agent);
197
+ const model = resolveModel(step, agent);
198
+ const runnerAgent = resolveRunnerAgent(step, agent);
199
+ const permissionMode = resolvePermissionMode(step, agent);
200
+ const securityPolicy = resolveSecurityPolicyWithConfig(step, agent, securityConfig);
201
+ const systemPrompt = agent.prompt || agent.description;
202
+ const workspaceInfoForAttempt = (attemptNumber) => {
203
+ if (!stepDir)
204
+ return null;
205
+ const attemptDir = debug && attemptNumber > 1 ? path.join(stepDir, `attempt-${attemptNumber}`) : stepDir;
206
+ return { projectRoot: cwd, stepDirRel: path.relative(cwd, attemptDir) };
207
+ };
208
+ const workspaceInfo = workspaceInfoForAttempt(1);
209
+ if (debug) {
210
+ timeline.setPaused(timelineIndex, 'debug review');
211
+ const decision = await promptDebugStepDecision({
212
+ step,
213
+ stepNumber: i + 1,
214
+ totalSteps: pipeline.steps.length,
215
+ provider,
216
+ model,
217
+ runnerAgent,
218
+ permissionMode,
219
+ securityPolicy,
220
+ resolvedInputs,
221
+ outputNames,
222
+ systemPrompt,
223
+ workspaceInfo,
224
+ timeline,
225
+ shell,
226
+ quiet,
227
+ decisionFn: opts.debugDecision,
228
+ debugEvents,
229
+ });
230
+ if (decision.inputs) {
231
+ const decisionInputs = decision.inputs;
232
+ const overrides = resolveDebugInputOverridesFromEdit(step, resolvedInputs, decisionInputs, inputDefs);
233
+ for (const [id, value] of Object.entries(overrides)) {
234
+ debugInputOverrides[id] = value;
235
+ }
236
+ if (Object.keys(overrides).length) {
237
+ pushDebugEvent(debugEvents, {
238
+ step: step.agent,
239
+ phase: 'pre-step',
240
+ action: 'set-runtime-input-overrides',
241
+ inputIds: Object.keys(overrides),
242
+ });
243
+ }
244
+ resolvedInputs = decisionInputs;
245
+ }
246
+ if (decision.action === 'skip') {
247
+ for (const name of outputNames) {
248
+ registry[`${step.agent}.${name}`] = `(debug-skipped:${step.agent}.${name})`;
249
+ }
250
+ timeline.setDone(timelineIndex, 'skipped by debug');
251
+ timeline.log(`${G.skipped} ${step.agent} - skipped by debug`);
252
+ stats.push({
253
+ agent: step.agent,
254
+ provider,
255
+ model: model || '—',
256
+ runnerAgent: runnerAgent || '—',
257
+ securityProfile: securityPolicy.profile,
258
+ permissionMode: permissionMode || '—',
259
+ status: 'skipped',
260
+ seconds: 0,
261
+ turns: 0,
262
+ cost: 0,
263
+ });
264
+ continue;
265
+ }
266
+ if (decision.action === 'abort') {
267
+ stats.push({
268
+ agent: step.agent,
269
+ provider,
270
+ model: model || '—',
271
+ runnerAgent: runnerAgent || '—',
272
+ securityProfile: securityPolicy.profile,
273
+ permissionMode: permissionMode || '—',
274
+ status: 'failed',
275
+ seconds: 0,
276
+ turns: 0,
277
+ cost: 0,
278
+ });
279
+ failStep(timeline, timelineIndex, 'aborted by debug', `Pipeline aborted before step "${step.agent}".`);
280
+ }
281
+ }
282
+ const runner = getRunner(provider);
283
+ let attempt = 1;
284
+ let finalAttempt = null;
285
+ let shouldReplay = false;
286
+ let replayInputs = resolvedInputs;
287
+ let replayInputOverride = null;
288
+ let totalAttemptSeconds = 0;
289
+ let totalAttemptTurns = 0;
290
+ let totalAttemptCost = 0;
291
+ const stepRegistrySnapshot = new Map(outputNames.map((name) => {
292
+ const key = `${step.agent}.${name}`;
293
+ return [key, Object.prototype.hasOwnProperty.call(registry, key) ? registry[key] : undefined];
294
+ }));
295
+ const stepSnapshotDir = debug && stepDir ? path.join(stepDir, '.snapshot') : null;
296
+ const activeSnapshotManager = snapshotManager;
297
+ const stepSnapshot = stepSnapshotDir
298
+ ? await activeSnapshotManager.createRestoreSnapshot({ snapshotDir: stepSnapshotDir })
299
+ : null;
300
+ logSnapshotCoverage({ snapshot: stepSnapshot, timeline });
301
+ const stepOriginalPaths = currentSnapshot ? new Set(currentSnapshot.keys()) : new Set();
302
+ do {
303
+ if (shouldReplay) {
304
+ const replayState = await prepareReplayAttempt({
305
+ attempt,
306
+ finalAttempt,
307
+ stepSnapshot,
308
+ snapshotManager: activeSnapshotManager,
309
+ stepOriginalPaths,
310
+ stepRegistrySnapshot,
311
+ registry,
312
+ replayInputs,
313
+ replayInputOverride,
314
+ step,
315
+ debugEvents,
316
+ inputDefs,
317
+ timeline,
318
+ shell,
319
+ outputNames,
320
+ workspaceInfoForAttempt,
321
+ systemPrompt,
322
+ securityPolicy,
323
+ debugInputOverrides,
324
+ currentSnapshot,
325
+ stats,
326
+ provider,
327
+ model,
328
+ runnerAgent,
329
+ permissionMode,
330
+ totalAttemptSeconds,
331
+ totalAttemptTurns,
332
+ totalAttemptCost,
333
+ timelineIndex,
334
+ failStep,
335
+ });
336
+ attempt = replayState.attempt;
337
+ replayInputs = replayState.replayInputs;
338
+ replayInputOverride = replayState.replayInputOverride;
339
+ currentSnapshot = replayState.currentSnapshot;
340
+ }
341
+ const attemptWorkspaceInfo = workspaceInfoForAttempt(attempt);
342
+ const attemptResult = await runStepAttempt({
343
+ attempt,
344
+ debug,
345
+ stepDir,
346
+ cwd,
347
+ step,
348
+ outputNames,
349
+ inputs: replayInputs,
350
+ securityPolicy,
351
+ systemPrompt,
352
+ workspaceInfo: attemptWorkspaceInfo,
353
+ timeline,
354
+ timelineIndex,
355
+ verbose,
356
+ runner,
357
+ provider,
358
+ model,
359
+ runnerAgent,
360
+ permissionMode,
361
+ inputValues,
362
+ registry,
363
+ fileWrites,
364
+ snapshotManager: activeSnapshotManager,
365
+ currentSnapshot,
366
+ shell,
367
+ handlePostRunViolations,
368
+ failStep,
369
+ });
370
+ if (attemptResult.failed) {
371
+ const attemptError = attemptResult.error instanceof Error
372
+ ? attemptResult.error
373
+ : new Error(String(attemptResult.error));
374
+ stats.push({
375
+ agent: step.agent,
376
+ provider,
377
+ model: model || '—',
378
+ runnerAgent: runnerAgent || '—',
379
+ securityProfile: securityPolicy.profile,
380
+ permissionMode: permissionMode || '—',
381
+ status: 'failed',
382
+ seconds: totalAttemptSeconds + attemptResult.elapsedSeconds,
383
+ turns: 0,
384
+ cost: totalAttemptCost,
385
+ attempts: attempt,
386
+ });
387
+ failStep(timeline, timelineIndex, attemptError.message, `Step "${step.agent}" failed: ${attemptError.message}`);
388
+ }
389
+ totalAttemptSeconds += attemptResult.elapsedSeconds;
390
+ totalAttemptTurns += attemptResult.attemptTurns;
391
+ totalAttemptCost += attemptResult.attemptCost;
392
+ currentSnapshot = attemptResult.stepAfterSnapshot || currentSnapshot;
393
+ const { stepWritesStart, attemptWrites, stepChanges, outputWarnings, parsedOutputSummary, rawOutputPath, parsed, text, } = attemptResult;
394
+ if (step.require_changes && stepChanges.length === 0) {
395
+ stats.push({
396
+ agent: step.agent,
397
+ provider,
398
+ model: model || '—',
399
+ runnerAgent: runnerAgent || '—',
400
+ securityProfile: securityPolicy.profile,
401
+ permissionMode: permissionMode || '—',
402
+ status: 'failed',
403
+ seconds: totalAttemptSeconds,
404
+ turns: totalAttemptTurns,
405
+ cost: totalAttemptCost,
406
+ attempts: attempt,
407
+ outputWarnings,
408
+ parsedOutputs: parsedOutputSummary,
409
+ rawOutputPath: rawOutputPath ? path.relative(cwd, rawOutputPath) : null,
410
+ });
411
+ failStep(timeline, timelineIndex, 'no project changes', `Step "${step.agent}" requires project file changes but did not modify any tracked project file.`);
412
+ }
413
+ if (debug) {
414
+ timeline.setPaused(timelineIndex, 'output review');
415
+ const postDecision = await promptDebugPostStepDecision({
416
+ step,
417
+ stepNumber: i + 1,
418
+ totalSteps: pipeline.steps.length,
419
+ parsed,
420
+ outputNames,
421
+ stepWrites: attemptWrites,
422
+ stepChanges,
423
+ outputWarnings,
424
+ rawText: text,
425
+ rawOutputPath: rawOutputPath ? path.relative(cwd, rawOutputPath) : null,
426
+ attempt,
427
+ maxDebugReplays,
428
+ cwd,
429
+ timeline,
430
+ shell,
431
+ quiet,
432
+ decisionFn: opts.debugPostDecision,
433
+ debugEvents,
434
+ getDiffPreview: getViolationDiffPreview,
435
+ });
436
+ const postAction = typeof postDecision === 'object' && postDecision
437
+ ? postDecision.action
438
+ : postDecision;
439
+ if (postAction === 'abort') {
440
+ stats.push({
441
+ agent: step.agent,
442
+ provider,
443
+ model: model || '—',
444
+ runnerAgent: runnerAgent || '—',
445
+ securityProfile: securityPolicy.profile,
446
+ permissionMode: permissionMode || '—',
447
+ status: 'failed',
448
+ seconds: totalAttemptSeconds,
449
+ turns: totalAttemptTurns,
450
+ cost: totalAttemptCost,
451
+ attempts: attempt,
452
+ outputWarnings,
453
+ parsedOutputs: parsedOutputSummary,
454
+ rawOutputPath: rawOutputPath ? path.relative(cwd, rawOutputPath) : null,
455
+ });
456
+ failStep(timeline, timelineIndex, 'aborted after output review', `Pipeline aborted after step "${step.agent}" output review.`);
457
+ }
458
+ if (postAction === 'replay') {
459
+ if (attempt - 1 >= maxDebugReplays) {
460
+ stats.push({
461
+ agent: step.agent,
462
+ provider,
463
+ model: model || '—',
464
+ runnerAgent: runnerAgent || '—',
465
+ securityProfile: securityPolicy.profile,
466
+ permissionMode: permissionMode || '—',
467
+ status: 'failed',
468
+ seconds: totalAttemptSeconds,
469
+ turns: totalAttemptTurns,
470
+ cost: totalAttemptCost,
471
+ attempts: attempt,
472
+ outputWarnings,
473
+ parsedOutputs: parsedOutputSummary,
474
+ rawOutputPath: rawOutputPath ? path.relative(cwd, rawOutputPath) : null,
475
+ });
476
+ failStep(timeline, timelineIndex, 'replay limit reached', `Replay limit reached for step "${step.agent}" (${maxDebugReplays} per step).`);
477
+ }
478
+ const movedAttempt = await moveAttemptArtifactsToAttemptDir({
479
+ cwd,
480
+ stepDir,
481
+ attempt,
482
+ writes: attemptWrites,
483
+ rawOutputPath: rawOutputPath ? path.relative(cwd, rawOutputPath) : null,
484
+ });
485
+ finalAttempt = { stepChanges, stepWrites: movedAttempt.writes };
486
+ fileWrites.splice(stepWritesStart);
487
+ replayInputOverride = typeof postDecision === 'object' && postDecision?.inputs
488
+ ? postDecision.inputs
489
+ : null;
490
+ shouldReplay = true;
491
+ continue;
492
+ }
493
+ }
494
+ const totalElapsed = totalAttemptSeconds.toFixed(1);
495
+ const costInfo = totalAttemptCost ? ` ${G.bullet} $${totalAttemptCost.toFixed(4)}` : '';
496
+ const turnInfo = totalAttemptTurns ? ` ${G.bullet} ${totalAttemptTurns}t` : '';
497
+ const attemptInfo = attempt > 1 ? ` ${G.bullet} ${attempt} attempts` : '';
498
+ timeline.setDone(timelineIndex, `${totalElapsed}s${attemptInfo}${turnInfo}${costInfo}`);
499
+ timeline.logSuccess(`${G.success} ${step.agent} - ${totalElapsed}s${attemptInfo}${turnInfo}${costInfo}`);
500
+ stats.push({
501
+ agent: step.agent,
502
+ provider,
503
+ model: model || '—',
504
+ runnerAgent: runnerAgent || '—',
505
+ securityProfile: securityPolicy.profile,
506
+ permissionMode: permissionMode || '—',
507
+ status: 'done',
508
+ seconds: totalAttemptSeconds,
509
+ turns: totalAttemptTurns,
510
+ cost: totalAttemptCost,
511
+ attempts: attempt,
512
+ outputWarnings,
513
+ parsedOutputs: parsedOutputSummary,
514
+ rawOutputPath: rawOutputPath ? path.relative(cwd, rawOutputPath) : null,
515
+ });
516
+ shouldReplay = false;
517
+ } while (shouldReplay);
518
+ }
519
+ }
520
+ catch (err) {
521
+ runError = err instanceof Error ? err : new Error(String(err));
522
+ }
523
+ finally {
524
+ timeline.end();
525
+ if (shell)
526
+ shell.exitPipelineMode();
527
+ }
528
+ const finalSnapshot = snapshotManager ? await snapshotManager.captureState() : null;
529
+ const detectedDeliverables = snapshotManager && beforeSnapshot && finalSnapshot
530
+ ? snapshotManager.detectChanges(beforeSnapshot, finalSnapshot)
531
+ : [];
532
+ currentSnapshot = finalSnapshot || currentSnapshot;
533
+ const runStatus = runError ? 'failed' : (dryRun ? 'dry-run' : 'done');
534
+ if (runDir) {
535
+ await writeRunManifest({
536
+ runDir,
537
+ runId,
538
+ pipeline,
539
+ cwd,
540
+ stats,
541
+ fileWrites,
542
+ detectedDeliverables,
543
+ status: runStatus,
544
+ error: runError,
545
+ debugEvents,
546
+ });
547
+ await writeLatestRunPointer({ cwd, runId });
548
+ }
549
+ const combinedWrites = [];
550
+ const seenWrites = new Set();
551
+ for (const entry of [...fileWrites, ...detectedDeliverables]) {
552
+ if (seenWrites.has(entry.absPath))
553
+ continue;
554
+ seenWrites.add(entry.absPath);
555
+ combinedWrites.push(entry);
556
+ }
557
+ const out = quiet
558
+ ? () => { }
559
+ : shell
560
+ ? (t) => shell.log(t)
561
+ : (t) => console.log(stripBlessedTags(t));
562
+ for (const line of renderRunSummary({
563
+ stats,
564
+ fileWrites: combinedWrites.map((f) => f.relPath),
565
+ dryRun,
566
+ runDir,
567
+ cwd,
568
+ runStatus,
569
+ }))
570
+ out(line);
571
+ if (runError) {
572
+ shell?.setMode?.('error');
573
+ // One-line outcome banner: bold red marker + reason inline (instead of two stacked × lines).
574
+ const reason = String(runError.message || 'unknown error').split('\n')[0];
575
+ out(`{${S.error}-fg}{bold}${G.error} Pipeline failed{/}{${S.muted}-fg} - {/}{${S.error}-fg}${reason}{/}`);
576
+ out('');
577
+ throw runError;
578
+ }
579
+ shell?.setMode?.(null);
580
+ out(dryRun
581
+ ? `{${S.success}-fg}{bold}${G.success} Dry-run complete{/}`
582
+ : `{${S.success}-fg}{bold}${G.success} Pipeline complete{/}`);
583
+ out('');
584
+ }