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.
- package/dist/packages/cli/src/assets/singleton-logo.txt +10 -0
- package/dist/packages/cli/src/commands/new.js +763 -0
- package/dist/packages/cli/src/commands/repl.js +557 -0
- package/dist/packages/cli/src/commands/usage.js +49 -0
- package/dist/packages/cli/src/executor/debug-loop.js +525 -0
- package/dist/packages/cli/src/executor/inputs.js +226 -0
- package/dist/packages/cli/src/executor/outputs.js +134 -0
- package/dist/packages/cli/src/executor/preflight.js +605 -0
- package/dist/packages/cli/src/executor/replay-loop.js +120 -0
- package/dist/packages/cli/src/executor/run-report.js +209 -0
- package/dist/packages/cli/src/executor/run-setup.js +114 -0
- package/dist/packages/cli/src/executor/security-review.js +97 -0
- package/dist/packages/cli/src/executor/snapshot-manager.js +349 -0
- package/dist/packages/cli/src/executor/step-runner.js +241 -0
- package/dist/packages/cli/src/executor.js +584 -0
- package/dist/packages/cli/src/index.js +107 -0
- package/dist/packages/cli/src/parser.js +89 -0
- package/dist/packages/cli/src/runners/_shared.js +96 -0
- package/dist/packages/cli/src/runners/claude.js +103 -0
- package/dist/packages/cli/src/runners/codex-instructions.js +69 -0
- package/dist/packages/cli/src/runners/codex.js +141 -0
- package/dist/packages/cli/src/runners/copilot.js +209 -0
- package/dist/packages/cli/src/runners/index.js +18 -0
- package/dist/packages/cli/src/runners/opencode.js +240 -0
- package/dist/packages/cli/src/scanner.js +43 -0
- package/dist/packages/cli/src/security/policy.js +115 -0
- package/dist/packages/cli/src/sentinels.js +1 -0
- package/dist/packages/cli/src/shell.js +753 -0
- package/dist/packages/cli/src/theme.js +39 -0
- package/dist/packages/cli/src/timeline.js +238 -0
- package/dist/packages/cli/src/types.js +1 -0
- package/dist/packages/cli/src/usage/aggregator.js +44 -0
- package/dist/packages/cli/src/usage/reader.js +30 -0
- package/dist/packages/cli/src/usage/types.js +1 -0
- package/dist/packages/server/src/index.js +36 -0
- package/dist/packages/server/src/routes/agents.js +31 -0
- package/dist/packages/server/src/routes/files.js +45 -0
- package/dist/packages/server/src/routes/pipelines.js +74 -0
- package/docs/reference.md +28 -0
- package/package.json +15 -14
- package/packages/web/dist/assets/{index-CnKytBly.js → index-9S0goZlQ.js} +1 -1
- package/packages/web/dist/assets/{index-CCFWfCA2.css → index-iV4UtXoN.css} +1 -1
- package/packages/web/dist/assets/logo-COSyZmgk.png +0 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/cli/package.json +0 -18
- package/packages/cli/src/commands/new.js +0 -786
- package/packages/cli/src/commands/repl.js +0 -548
- package/packages/cli/src/executor/debug-loop.js +0 -587
- package/packages/cli/src/executor/inputs.js +0 -202
- package/packages/cli/src/executor/outputs.js +0 -140
- package/packages/cli/src/executor/preflight.js +0 -459
- package/packages/cli/src/executor/replay-loop.js +0 -172
- package/packages/cli/src/executor/run-report.js +0 -189
- package/packages/cli/src/executor/run-setup.js +0 -93
- package/packages/cli/src/executor/security-review.js +0 -108
- package/packages/cli/src/executor/snapshot-manager.js +0 -335
- package/packages/cli/src/executor/step-runner.js +0 -266
- package/packages/cli/src/executor.js +0 -652
- package/packages/cli/src/index.js +0 -107
- package/packages/cli/src/parser.js +0 -78
- package/packages/cli/src/runners/_shared.js +0 -83
- package/packages/cli/src/runners/claude.js +0 -122
- package/packages/cli/src/runners/codex-instructions.js +0 -75
- package/packages/cli/src/runners/codex.js +0 -165
- package/packages/cli/src/runners/copilot.js +0 -224
- package/packages/cli/src/runners/index.js +0 -20
- package/packages/cli/src/runners/opencode.js +0 -265
- package/packages/cli/src/scanner.js +0 -47
- package/packages/cli/src/security/policy.js +0 -126
- package/packages/cli/src/shell.js +0 -732
- package/packages/cli/src/theme.js +0 -46
- package/packages/cli/src/timeline.js +0 -180
- package/packages/server/package.json +0 -11
- package/packages/server/src/index.js +0 -43
- package/packages/server/src/routes/agents.js +0 -32
- package/packages/server/src/routes/files.js +0 -42
- 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
|
+
}
|