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

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.
@@ -0,0 +1,587 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import { S } from '../shell.js';
3
+ import { buildUserMessage } from './inputs.js';
4
+ import { summarizeParsedOutputs } from './outputs.js';
5
+ import { formatSnapshotCoverage } from './snapshot-manager.js';
6
+
7
+ export const DEFAULT_MAX_DEBUG_REPLAYS = 3;
8
+
9
+ function previewValue(value, max = 140) {
10
+ const compact = String(value || '').replace(/\s+/g, ' ').trim();
11
+ if (compact.length <= max) return compact || '—';
12
+ return `${compact.slice(0, max - 1)}…`;
13
+ }
14
+
15
+ // Semantic tokens for debug field formatting.
16
+ // key — labels ("inputs:", "system prompt:")
17
+ // identity — positive resolved values ("found", chars/lines counts)
18
+ // text — neutral readable values
19
+ // path — file paths and URIs
20
+ // policy — attention values ("missing", security notes)
21
+ // muted — fallback or secondary info
22
+ export const debugToken = {
23
+ key: (value) => `{${S.accent}-fg}{bold}${value}:{/}`,
24
+ identity: (value) => `{${S.success}-fg}${value || '—'}{/}`,
25
+ text: (value) => `{${S.text}-fg}${value || '—'}{/}`,
26
+ path: (value) => `{${S.string}-fg}${value || '—'}{/}`,
27
+ policy: (value) => `{${S.warning}-fg}${value || '—'}{/}`,
28
+ muted: (value) => `{${S.muted}-fg}${value || '—'}{/}`,
29
+ };
30
+
31
+ // Action semantics for debug toolbars:
32
+ // continue → success | inspect/output → keyword (observe)
33
+ // edit/diff → warning (modifies) | skip/replay → accent (neutral action)
34
+ // abort → error
35
+ const DEBUG_ACTION_PROMPT = [
36
+ `{${S.text}-fg}{bold}Debug action{/}`,
37
+ `{${S.success}-fg}▶ continue{/}{${S.subtle}-fg}(c){/}`,
38
+ `{${S.keyword}-fg}? inspect{/}{${S.subtle}-fg}(i){/}`,
39
+ `{${S.warning}-fg}✎ edit{/}{${S.subtle}-fg}(e){/}`,
40
+ `{${S.accent}-fg}→ skip{/}{${S.subtle}-fg}(s){/}`,
41
+ `{${S.error}-fg}■ abort{/}{${S.subtle}-fg}(a){/}`,
42
+ ].join(` {${S.subtle}-fg}·{/} `);
43
+
44
+ const DEBUG_ACTION_HELP = [
45
+ `{${S.text}-fg}Choose{/}`,
46
+ `{${S.success}-fg}▶ continue{/}`,
47
+ `{${S.keyword}-fg}? inspect{/}`,
48
+ `{${S.warning}-fg}✎ edit{/}`,
49
+ `{${S.accent}-fg}→ skip{/}`,
50
+ `{${S.error}-fg}■ abort{/}`,
51
+ ].join(` {${S.subtle}-fg}·{/} `);
52
+
53
+ const DEBUG_POST_ACTION_PROMPT = [
54
+ `{${S.text}-fg}{bold}Debug output{/}`,
55
+ `{${S.success}-fg}▶ continue{/}{${S.subtle}-fg}(c){/}`,
56
+ `{${S.keyword}-fg}? output{/}{${S.subtle}-fg}(o){/}`,
57
+ `{${S.keyword}-fg}raw output{/}{${S.subtle}-fg}(r){/}`,
58
+ `{${S.warning}-fg}± diff{/}{${S.subtle}-fg}(d){/}`,
59
+ `{${S.accent}-fg}↻ replay{/}{${S.subtle}-fg}(p){/}`,
60
+ `{${S.error}-fg}■ abort{/}{${S.subtle}-fg}(a){/}`,
61
+ ].join(` {${S.subtle}-fg}·{/} `);
62
+
63
+ const DEBUG_POST_ACTION_HELP = [
64
+ `{${S.text}-fg}Choose{/}`,
65
+ `{${S.success}-fg}▶ continue{/}`,
66
+ `{${S.keyword}-fg}? output{/}`,
67
+ `{${S.keyword}-fg}raw output{/}`,
68
+ `{${S.warning}-fg}± diff{/}`,
69
+ `{${S.accent}-fg}↻ replay{/}`,
70
+ `{${S.error}-fg}■ abort{/}`,
71
+ ].join(` {${S.subtle}-fg}·{/} `);
72
+
73
+ // Compact one-line summary of the action the user picked, with the action's icon and color.
74
+ // Replaces the noisy toolbar that used to get re-logged after each prompt.
75
+ // e.g. {muted}[Step 1/2 input]{/} {success}▶ continue{/}
76
+ const ACTION_LABEL = {
77
+ continue: { icon: '▶', name: 'continue', color: S.success },
78
+ inspect: { icon: '?', name: 'inspect', color: S.keyword },
79
+ 'inspect-output': { icon: '?', name: 'output', color: S.keyword },
80
+ 'inspect-raw': { icon: '?', name: 'raw output', color: S.keyword },
81
+ 'inspect-diff': { icon: '±', name: 'diff', color: S.warning },
82
+ edit: { icon: '✎', name: 'edit', color: S.warning },
83
+ skip: { icon: '→', name: 'skip', color: S.accent },
84
+ replay: { icon: '↻', name: 'replay', color: S.accent },
85
+ abort: { icon: '■', name: 'abort', color: S.error },
86
+ };
87
+
88
+ export function logDebugChoice(timeline, { stepNumber, totalSteps, phase, action, suffix = '' }) {
89
+ const a = ACTION_LABEL[action] || { icon: '·', name: action, color: S.muted };
90
+ const tag = `{${S.muted}-fg}[Step ${stepNumber}/${totalSteps} ${phase}]{/}`;
91
+ const head = `{${a.color}-fg}${a.icon} ${a.name}{/}`;
92
+ const tail = suffix ? ` {${S.muted}-fg}${suffix}{/}` : '';
93
+ timeline.log(`${tag} ${head}${tail}`);
94
+ }
95
+
96
+ function logDebugSection(title, timeline) {
97
+ const width = 72;
98
+ const text = ` ${title} `;
99
+ const left = Math.max(0, Math.floor((width - text.length) / 2));
100
+ const right = Math.max(0, width - text.length - left);
101
+ timeline.logMuted(' ');
102
+ timeline.logMuted(' ');
103
+ timeline.log(`{${S.subtle}-fg}${'─'.repeat(left)}{/}{${S.keyword}-fg}{bold}${text}{/}{${S.subtle}-fg}${'─'.repeat(right)}{/}`);
104
+ timeline.logMuted(' ');
105
+ timeline.logMuted(' ');
106
+ }
107
+
108
+ export function formatDebugList(values, fallback = 'none') {
109
+ if (!values.length) return debugToken.muted(fallback);
110
+ return values.map((value) => debugToken.identity(value)).join(` ${debugToken.muted('·')} `);
111
+ }
112
+
113
+ export function pushDebugEvent(events, event) {
114
+ if (!Array.isArray(events)) return;
115
+ const { systemPrompt: _systemPrompt, workspaceInfo: _workspaceInfo, ...safeEvent } = event || {};
116
+ events.push({
117
+ timestamp: new Date().toISOString(),
118
+ ...safeEvent,
119
+ });
120
+ }
121
+
122
+ function looksLikePath(value) {
123
+ const text = String(value || '').trim();
124
+ return /^\.{0,2}\//.test(text) || /^[\w.-]+\/[\w./-]+$/.test(text) || /\.[a-z0-9]{1,8}$/i.test(text);
125
+ }
126
+
127
+ function debugValue(value, kind = 'text') {
128
+ if (!value || value === '—') return debugToken.muted('—');
129
+ if (kind === 'identity') return debugToken.identity(value);
130
+ if (kind === 'path') return debugToken.path(value);
131
+ if (kind === 'policy') return debugToken.policy(value);
132
+ return debugToken.text(value);
133
+ }
134
+
135
+ function debugLine(label, value, kind = 'text') {
136
+ return `${debugToken.key(label)} ${debugValue(value, kind)}`;
137
+ }
138
+
139
+ function isPromptCancelled(value) {
140
+ return value === '__SINGLETON_ESC__';
141
+ }
142
+
143
+ function logDebugInputs(resolvedInputs, timeline) {
144
+ const entries = Object.entries(resolvedInputs);
145
+ if (entries.length) {
146
+ timeline.logMuted(debugToken.key('inputs'));
147
+ for (const [name, value] of entries) {
148
+ const preview = previewValue(value);
149
+ timeline.logMuted(
150
+ ` ${debugToken.muted('·')} ${debugToken.key(name)} ` +
151
+ `${debugValue(preview, looksLikePath(preview) ? 'path' : 'text')}`
152
+ );
153
+ }
154
+ } else {
155
+ timeline.logMuted(`${debugToken.key('inputs')} ${debugToken.muted('none')}`);
156
+ }
157
+ }
158
+
159
+ function markEditedInputTags(line, editedInputs = new Set()) {
160
+ let text = String(line || ' ');
161
+ for (const name of editedInputs) {
162
+ const open = new RegExp(`<${name}>`, 'g');
163
+ text = text.replace(open, `<${name} debug-edited="true">`);
164
+ }
165
+ return text;
166
+ }
167
+
168
+ function debugPromptTextLine(line, { editedInputs = new Set() } = {}) {
169
+ const text = markEditedInputTags(line || ' ', editedInputs);
170
+ const tagPattern = /(<\/?[A-Za-z_][\w.-]*(?:\s+[^>]*)?>)/g;
171
+ const parts = text.split(tagPattern);
172
+ return parts.map((part) => {
173
+ if (!part) return '';
174
+ const isTag = tagPattern.test(part);
175
+ tagPattern.lastIndex = 0;
176
+ if (isTag) {
177
+ const isVariableTag = /^<\/?(workspace|security_policy|file)\b/i.test(part) === false;
178
+ // Variable tags = interpolated user data (warning-toned, watch what gets in).
179
+ // System tags = structural (workspace, security_policy, file) keyword-toned.
180
+ const color = isVariableTag ? S.warning : S.keyword;
181
+ tagPattern.lastIndex = 0;
182
+ return `{${color}-fg}{bold}${part}{/}`;
183
+ }
184
+ return `{${S.text}-fg}${part}{/}`;
185
+ }).join('');
186
+ }
187
+
188
+ export function logDebugPromptPreview({ systemPrompt, userMessage, timeline, editedInputs = new Set() }) {
189
+ logDebugSection('Debug prompt preview', timeline);
190
+ if (editedInputs.size) {
191
+ timeline.logMuted(`${debugToken.key('edited inputs')} ${formatDebugList([...editedInputs])}`);
192
+ timeline.logMuted(`${debugToken.policy('Editing one input may not override other inputs or the agent prompt. Inspect the final prompt before continuing.')}`);
193
+ timeline.logMuted(' ');
194
+ }
195
+ timeline.logMuted(debugToken.key('system prompt'));
196
+ for (const line of systemPrompt.split('\n')) {
197
+ timeline.logMuted(` ${debugPromptTextLine(line, { editedInputs })}`);
198
+ }
199
+ timeline.logMuted(' ');
200
+ timeline.logMuted(' ');
201
+ timeline.logMuted(debugToken.key('user message'));
202
+ for (const line of userMessage.split('\n')) {
203
+ timeline.logMuted(` ${debugPromptTextLine(line, { editedInputs })}`);
204
+ }
205
+ }
206
+
207
+ function logDebugOutputs(parsed, outputNames, timeline) {
208
+ logDebugSection('Debug parsed outputs', timeline);
209
+ const summaries = summarizeParsedOutputs(parsed, outputNames);
210
+ for (const summary of summaries) {
211
+ const status = summary.found ? debugToken.identity('found') : debugToken.policy('missing');
212
+ timeline.logMuted(
213
+ `${debugToken.key(summary.name)} ${status} ` +
214
+ `${debugToken.muted('·')} ${debugValue(`${summary.chars} chars`, 'identity')} ` +
215
+ `${debugToken.muted('·')} ${debugValue(`${summary.lines} lines`, 'identity')}`
216
+ );
217
+ const name = summary.name;
218
+ const lines = String(parsed[name] || '').split('\n');
219
+ for (const line of lines) {
220
+ timeline.logMuted(` ${debugPromptTextLine(line)}`);
221
+ }
222
+ timeline.logMuted(' ');
223
+ }
224
+ }
225
+
226
+ function uniqueDebugPaths(entries) {
227
+ const out = [];
228
+ const seen = new Set();
229
+ for (const entry of entries) {
230
+ if (!entry?.relPath || seen.has(entry.relPath)) continue;
231
+ seen.add(entry.relPath);
232
+ out.push(entry);
233
+ }
234
+ return out;
235
+ }
236
+
237
+ async function logDebugDiffs({ changes, writes = [], cwd, timeline, getDiffPreview }) {
238
+ logDebugSection('Debug step diff', timeline);
239
+ const entries = uniqueDebugPaths([...changes, ...writes]);
240
+ if (!entries.length) {
241
+ timeline.logMuted(`${debugToken.key('changes')} ${debugToken.muted('none')}`);
242
+ return;
243
+ }
244
+
245
+ for (const change of entries.slice(0, 8)) {
246
+ timeline.log(`${debugToken.key(change.relPath)}`);
247
+ const preview = await getDiffPreview(cwd, change.relPath);
248
+ for (const line of preview) timeline.logDiffLine(` ${line}`);
249
+ }
250
+ if (entries.length > 8) {
251
+ timeline.logMuted(`${debugToken.muted(`... ${entries.length - 8} more changed file(s)`)}`);
252
+ }
253
+ }
254
+
255
+ export function logSnapshotCoverage({ snapshot, timeline }) {
256
+ if (!snapshot) return;
257
+ const details = formatSnapshotCoverage(snapshot);
258
+ if (!details.length) return;
259
+ timeline.logMuted(`${debugToken.key('replay snapshot coverage')} ${formatDebugList(details, 'none')}`);
260
+ if (snapshot.skippedLarge.length || snapshot.skippedBinary.length || snapshot.skippedIgnored.length) {
261
+ timeline.logMuted(debugToken.policy('Replay rollback is not fully guaranteed if a skipped file is modified.'));
262
+ }
263
+ }
264
+
265
+ export async function promptDebugPostStepDecision({
266
+ step,
267
+ stepNumber,
268
+ totalSteps,
269
+ parsed,
270
+ outputNames,
271
+ stepWrites,
272
+ stepChanges,
273
+ outputWarnings,
274
+ rawText,
275
+ rawOutputPath,
276
+ attempt,
277
+ maxDebugReplays,
278
+ cwd,
279
+ timeline,
280
+ shell,
281
+ quiet,
282
+ decisionFn,
283
+ debugEvents,
284
+ getDiffPreview,
285
+ }) {
286
+ const choose = (action, suffix = '') => logDebugChoice(timeline, { stepNumber, totalSteps, phase: 'output', action, suffix });
287
+ const summary = {
288
+ agent: step.agent,
289
+ outputs: outputNames,
290
+ parsedOutputs: summarizeParsedOutputs(parsed, outputNames),
291
+ outputWarnings,
292
+ rawOutputPath: rawOutputPath || null,
293
+ writtenFiles: stepWrites.map((entry) => entry.relPath),
294
+ changedFiles: stepChanges.map((entry) => entry.relPath),
295
+ };
296
+
297
+ if (decisionFn) {
298
+ const decision = await decisionFn(summary);
299
+ if (typeof decision === 'object' && decision) {
300
+ const action = String(decision.action || 'continue').trim().toLowerCase() || 'continue';
301
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action, ...summary });
302
+ return {
303
+ action,
304
+ inputs: decision.inputs && typeof decision.inputs === 'object' ? decision.inputs : null,
305
+ };
306
+ }
307
+ const action = String(decision || 'continue').trim().toLowerCase() || 'continue';
308
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action, ...summary });
309
+ return action;
310
+ }
311
+ if (quiet) {
312
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action: 'continue', ...summary });
313
+ return 'continue';
314
+ }
315
+
316
+ logDebugSection(`Debug output review · Step ${stepNumber}/${totalSteps}`, timeline);
317
+ timeline.logMuted(debugLine('agent', step.agent, 'identity'));
318
+ timeline.logMuted(`${debugToken.key('outputs')} ${formatDebugList(outputNames)}`);
319
+ timeline.logMuted(`${debugToken.key('written files')} ${formatDebugList(stepWrites.map((entry) => entry.relPath))}`);
320
+ timeline.logMuted(`${debugToken.key('changed files')} ${formatDebugList(stepChanges.map((entry) => entry.relPath))}`);
321
+ if (outputWarnings.length) {
322
+ timeline.logMuted(`${debugToken.key('warnings')} ${debugToken.policy(outputWarnings.join(' · '))}`);
323
+ } else {
324
+ timeline.logMuted(`${debugToken.key('warnings')} ${debugToken.muted('none')}`);
325
+ }
326
+
327
+ while (true) {
328
+ const raw = shell
329
+ ? await shell.prompt(DEBUG_POST_ACTION_PROMPT, { silent: true })
330
+ : await input({ message: 'Debug output: continue, output, raw output, diff, replay, or abort? (c/o/r/d/p/a)', default: 'c' });
331
+ if (isPromptCancelled(raw)) {
332
+ timeline.logMuted(`${debugToken.muted('Cancelled. Back to debug output menu.')}`);
333
+ continue;
334
+ }
335
+ const answer = String(raw || '').trim().toLowerCase();
336
+ if (!answer || answer === 'c' || answer === 'continue') {
337
+ choose('continue');
338
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action: 'continue', ...summary });
339
+ return 'continue';
340
+ }
341
+ if (answer === 'a' || answer === 'abort' || answer === 'stop') {
342
+ choose('abort');
343
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action: 'abort', ...summary });
344
+ return 'abort';
345
+ }
346
+ if (answer === 'o' || answer === 'output' || answer === 'inspect') {
347
+ choose('inspect-output');
348
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action: 'inspect-output', ...summary });
349
+ logDebugOutputs(parsed, outputNames, timeline);
350
+ continue;
351
+ }
352
+ if (answer === 'r' || answer === 'raw' || answer === 'raw output') {
353
+ choose('inspect-raw');
354
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action: 'inspect-raw-output', ...summary });
355
+ logDebugSection('Debug raw output', timeline);
356
+ if (rawOutputPath) timeline.logMuted(`${debugToken.key('saved')} ${debugValue(rawOutputPath, 'path')}`);
357
+ const lines = String(rawText || '').split('\n');
358
+ for (const line of lines.slice(0, 120)) timeline.logMuted(` ${debugPromptTextLine(line)}`);
359
+ if (lines.length > 120) timeline.logMuted(`${debugToken.muted(`... ${lines.length - 120} more line(s)`)}`);
360
+ continue;
361
+ }
362
+ if (answer === 'd' || answer === 'diff') {
363
+ choose('inspect-diff');
364
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action: 'inspect-diff', ...summary });
365
+ await logDebugDiffs({ changes: stepChanges, writes: stepWrites, cwd, timeline, getDiffPreview });
366
+ continue;
367
+ }
368
+ if (answer === 'p' || answer === 'replay' || answer === 'retry') {
369
+ const usedReplays = Math.max(0, Number(attempt || 1) - 1);
370
+ if (usedReplays >= maxDebugReplays) {
371
+ timeline.logMuted(`${debugToken.policy(`Replay limit reached (${maxDebugReplays} per step).`)}`);
372
+ continue;
373
+ }
374
+ if (stepWrites.length || stepChanges.length) {
375
+ logDebugSection('Replay soft warning', timeline);
376
+ timeline.logMuted(`${debugToken.policy('Replay restores detected project file changes only.')}`);
377
+ const written = stepWrites.map((entry) => entry.relPath);
378
+ const changed = stepChanges.map((entry) => entry.relPath);
379
+ timeline.logMuted(`${debugToken.key('already written')} ${formatDebugList(written)}`);
380
+ timeline.logMuted(`${debugToken.key('already changed')} ${formatDebugList(changed)}`);
381
+ timeline.logMuted(`${debugToken.muted('Previous run artifacts stay in their attempt folder for traceability.')}`);
382
+ timeline.logMuted(`${debugToken.muted('Skipped folders such as .git, node_modules, dist, build, and .next are not restored.')}`);
383
+ timeline.logMuted(`${debugToken.muted('External side effects such as commits, pushes, PRs, shell state, or network calls are not rolled back.')}`);
384
+ }
385
+ choose('replay');
386
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'post-step', action: 'replay', ...summary });
387
+ return 'replay';
388
+ }
389
+ timeline.logMuted(DEBUG_POST_ACTION_HELP);
390
+ }
391
+ }
392
+
393
+ export async function editDebugInputs({ resolvedInputs, shell, timeline, step, debugEvents, editedInputs }) {
394
+ const names = Object.keys(resolvedInputs);
395
+ if (names.length === 0) {
396
+ timeline.logMuted('No inputs to edit.');
397
+ return resolvedInputs;
398
+ }
399
+
400
+ logDebugSection('Debug edit inputs', timeline);
401
+ timeline.logMuted(`Type an input name to override it. Empty input name returns to debug review.`);
402
+ timeline.logMuted(`${debugToken.policy('Warning: editing one input may not override other inputs or the agent prompt.')}`);
403
+ timeline.logMuted(`${debugToken.policy('Use inspect after editing to verify the final prompt.')}`);
404
+ timeline.logMuted(' ');
405
+ const nextInputs = { ...resolvedInputs };
406
+
407
+ while (true) {
408
+ const rawName = shell
409
+ ? await shell.prompt(`Input to edit (${names.join(', ')})`)
410
+ : await input({ message: `Input to edit (${names.join(', ')})` });
411
+ if (isPromptCancelled(rawName)) {
412
+ timeline.logMuted(`${debugToken.muted('Edit cancelled. Back to debug menu.')}`);
413
+ return nextInputs;
414
+ }
415
+ const name = String(rawName || '').trim();
416
+ if (!name) return nextInputs;
417
+ if (!Object.hasOwn(nextInputs, name)) {
418
+ timeline.logMuted(`Unknown input "${name}".`);
419
+ continue;
420
+ }
421
+
422
+ const current = previewValue(nextInputs[name], 220);
423
+ timeline.logMuted(`${debugToken.key(name)} current: ${debugValue(current, looksLikePath(current) ? 'path' : 'text')}`);
424
+ const value = shell
425
+ ? await shell.prompt(`New value for ${name}`)
426
+ : await input({ message: `New value for ${name}`, default: String(nextInputs[name] || '') });
427
+ if (isPromptCancelled(value)) {
428
+ timeline.logMuted(`${debugToken.muted('Value edit cancelled. Back to input selection.')}`);
429
+ continue;
430
+ }
431
+ nextInputs[name] = String(value || '');
432
+ if (editedInputs) editedInputs.add(name);
433
+ pushDebugEvent(debugEvents, {
434
+ step: step?.agent,
435
+ phase: 'pre-step',
436
+ action: 'edit-input',
437
+ input: name,
438
+ previousPreview: previewValue(current, 120),
439
+ nextPreview: previewValue(nextInputs[name], 120),
440
+ });
441
+ timeline.logMuted(`${debugToken.key(name)} updated.`);
442
+ }
443
+ }
444
+
445
+ export async function promptDebugStepDecision({
446
+ step,
447
+ stepNumber,
448
+ totalSteps,
449
+ provider,
450
+ model,
451
+ runnerAgent,
452
+ permissionMode,
453
+ securityPolicy,
454
+ resolvedInputs,
455
+ outputNames,
456
+ systemPrompt,
457
+ workspaceInfo,
458
+ timeline,
459
+ shell,
460
+ quiet,
461
+ decisionFn,
462
+ debugEvents,
463
+ }) {
464
+ const summary = {
465
+ agent: step.agent,
466
+ stepNumber,
467
+ totalSteps,
468
+ provider,
469
+ model,
470
+ runnerAgent,
471
+ permissionMode,
472
+ securityProfile: securityPolicy.profile,
473
+ inputs: Object.keys(resolvedInputs),
474
+ outputs: outputNames,
475
+ systemPrompt,
476
+ workspaceInfo,
477
+ };
478
+
479
+ if (decisionFn) {
480
+ const decision = await decisionFn(summary);
481
+ if (typeof decision === 'object' && decision) {
482
+ const action = String(decision.action || 'continue').trim().toLowerCase();
483
+ const nextInputs = decision.inputs && typeof decision.inputs === 'object' ? decision.inputs : resolvedInputs;
484
+ const editedInputs = Object.keys(nextInputs).filter((name) => nextInputs[name] !== resolvedInputs[name]);
485
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action, editedInputs, ...summary });
486
+ return {
487
+ action,
488
+ inputs: nextInputs,
489
+ };
490
+ }
491
+ const action = String(decision || 'continue').trim().toLowerCase() || 'continue';
492
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action, ...summary });
493
+ return {
494
+ action,
495
+ inputs: resolvedInputs,
496
+ };
497
+ }
498
+ if (quiet) {
499
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action: 'continue', ...summary });
500
+ return { action: 'continue', inputs: resolvedInputs };
501
+ }
502
+
503
+ let currentInputs = { ...resolvedInputs };
504
+ const editedInputs = new Set();
505
+
506
+ logDebugSection(`Debug step review · Step ${stepNumber}/${totalSteps}`, timeline);
507
+ timeline.logMuted(debugLine('agent', step.agent, 'identity'));
508
+ timeline.logMuted(debugLine('provider', provider, 'identity'));
509
+ timeline.logMuted(debugLine('model', model || '—', 'identity'));
510
+ if (runnerAgent) timeline.logMuted(debugLine('runner_agent', runnerAgent, 'identity'));
511
+ timeline.logMuted(debugLine('security', securityPolicy.profile, 'policy'));
512
+ timeline.logMuted(debugLine('permission', permissionMode || '—', 'policy'));
513
+ timeline.logMuted(
514
+ `${debugToken.key('outputs')} ${outputNames.length ? outputNames.map((name) => debugToken.identity(name)).join(` ${debugToken.muted('·')} `) : debugToken.muted('none')}`
515
+ );
516
+ logDebugInputs(currentInputs, timeline);
517
+
518
+ const choose = (action, suffix = '') => logDebugChoice(timeline, { stepNumber, totalSteps, phase: 'input', action, suffix });
519
+
520
+ while (true) {
521
+ const raw = shell
522
+ ? await shell.prompt(DEBUG_ACTION_PROMPT, { silent: true })
523
+ : await input({ message: 'Debug: continue, inspect, edit, skip, or abort? (c/i/e/s/a)', default: 'c' });
524
+ if (isPromptCancelled(raw)) {
525
+ timeline.logMuted(`${debugToken.muted('Cancelled. Back to debug action menu.')}`);
526
+ continue;
527
+ }
528
+ const answer = String(raw || '').trim().toLowerCase();
529
+ if (!answer || answer === 'c' || answer === 'continue') {
530
+ choose('continue');
531
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action: 'continue', ...summary });
532
+ return { action: 'continue', inputs: currentInputs };
533
+ }
534
+ if (answer === 's' || answer === 'skip') {
535
+ choose('skip');
536
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action: 'skip', ...summary });
537
+ return { action: 'skip', inputs: currentInputs };
538
+ }
539
+ if (answer === 'a' || answer === 'abort' || answer === 'stop') {
540
+ choose('abort');
541
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action: 'abort', ...summary });
542
+ return { action: 'abort', inputs: currentInputs };
543
+ }
544
+ if (answer === 'i' || answer === 'inspect') {
545
+ choose('inspect');
546
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action: 'inspect-prompt', ...summary });
547
+ logDebugPromptPreview({
548
+ systemPrompt: summary.systemPrompt,
549
+ userMessage: buildUserMessage(currentInputs, outputNames, summary.workspaceInfo, securityPolicy),
550
+ timeline,
551
+ editedInputs,
552
+ });
553
+ continue;
554
+ }
555
+ if (answer === 'e' || answer === 'edit') {
556
+ const beforeEdit = new Set(editedInputs);
557
+ choose('edit');
558
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action: 'open-edit-inputs', ...summary });
559
+ currentInputs = await editDebugInputs({ resolvedInputs: currentInputs, shell, timeline, step, debugEvents, editedInputs });
560
+ const newlyEdited = [...editedInputs].filter((name) => !beforeEdit.has(name));
561
+ if (newlyEdited.length) {
562
+ choose('edit', `(${newlyEdited.join(', ')} edited)`);
563
+ }
564
+ logDebugInputs(currentInputs, timeline);
565
+ if (editedInputs.size) {
566
+ const inspectAnswer = shell
567
+ ? await shell.prompt(`{${S.text}-fg}Inspect final prompt now?{/} {${S.success}-fg}yes{/}{${S.subtle}-fg}(y){/} {${S.subtle}-fg}or{/} {${S.subtle}-fg}no(n){/}`)
568
+ : await input({ message: 'Inspect final prompt now? (y/N)', default: 'n' });
569
+ if (isPromptCancelled(inspectAnswer)) {
570
+ timeline.logMuted(`${debugToken.muted('Inspect prompt cancelled.')}`);
571
+ continue;
572
+ }
573
+ if (['y', 'yes'].includes(String(inspectAnswer || '').trim().toLowerCase())) {
574
+ pushDebugEvent(debugEvents, { step: step.agent, phase: 'pre-step', action: 'inspect-prompt-after-edit', editedInputs: [...editedInputs], ...summary });
575
+ logDebugPromptPreview({
576
+ systemPrompt: summary.systemPrompt,
577
+ userMessage: buildUserMessage(currentInputs, outputNames, summary.workspaceInfo, securityPolicy),
578
+ timeline,
579
+ editedInputs,
580
+ });
581
+ }
582
+ }
583
+ continue;
584
+ }
585
+ timeline.logMuted(DEBUG_ACTION_HELP);
586
+ }
587
+ }