@weldr/runr 0.4.0 → 0.7.2
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/CHANGELOG.md +127 -1
- package/README.md +124 -165
- package/dist/audit/classifier.js +331 -0
- package/dist/cli.js +570 -300
- package/dist/commands/audit.js +259 -0
- package/dist/commands/bundle.js +180 -0
- package/dist/commands/continue.js +276 -0
- package/dist/commands/doctor.js +430 -45
- package/dist/commands/hooks.js +352 -0
- package/dist/commands/init.js +368 -8
- package/dist/commands/intervene.js +109 -0
- package/dist/commands/meta.js +245 -0
- package/dist/commands/mode.js +157 -0
- package/dist/commands/orchestrate.js +29 -0
- package/dist/commands/packs.js +47 -0
- package/dist/commands/preflight.js +8 -5
- package/dist/commands/resume.js +421 -3
- package/dist/commands/run.js +63 -4
- package/dist/commands/status.js +47 -0
- package/dist/commands/submit.js +374 -0
- package/dist/config/schema.js +61 -1
- package/dist/diagnosis/analyzer.js +86 -1
- package/dist/diagnosis/formatter.js +3 -0
- package/dist/diagnosis/index.js +1 -0
- package/dist/diagnosis/stop-explainer.js +267 -0
- package/dist/diagnostics/stop-explainer.js +267 -0
- package/dist/guards/checkpoint.js +119 -0
- package/dist/journal/builder.js +36 -3
- package/dist/journal/renderer.js +19 -0
- package/dist/orchestrator/artifacts.js +17 -2
- package/dist/orchestrator/receipt.js +304 -0
- package/dist/output/stop-footer.js +185 -0
- package/dist/packs/actions.js +176 -0
- package/dist/packs/loader.js +200 -0
- package/dist/packs/renderer.js +46 -0
- package/dist/receipt/intervention.js +465 -0
- package/dist/receipt/writer.js +296 -0
- package/dist/redaction/redactor.js +95 -0
- package/dist/repo/context.js +147 -20
- package/dist/review/check-parser.js +211 -0
- package/dist/store/checkpoint-metadata.js +111 -0
- package/dist/store/run-store.js +21 -0
- package/dist/supervisor/runner.js +130 -10
- package/dist/tasks/task-metadata.js +74 -1
- package/dist/ux/brain.js +528 -0
- package/dist/ux/render.js +123 -0
- package/dist/ux/safe-commands.js +133 -0
- package/dist/ux/state.js +193 -0
- package/dist/ux/telemetry.js +110 -0
- package/package.json +3 -1
- package/packs/pr/pack.json +50 -0
- package/packs/pr/templates/AGENTS.md.tmpl +120 -0
- package/packs/pr/templates/CLAUDE.md.tmpl +101 -0
- package/packs/pr/templates/bundle.md.tmpl +27 -0
- package/packs/solo/pack.json +82 -0
- package/packs/solo/templates/AGENTS.md.tmpl +80 -0
- package/packs/solo/templates/CLAUDE.md.tmpl +126 -0
- package/packs/solo/templates/bundle.md.tmpl +27 -0
- package/packs/solo/templates/claude-cmd-bundle.md.tmpl +40 -0
- package/packs/solo/templates/claude-cmd-resume.md.tmpl +43 -0
- package/packs/solo/templates/claude-cmd-submit.md.tmpl +51 -0
- package/packs/solo/templates/claude-skill.md.tmpl +96 -0
- package/packs/trunk/pack.json +50 -0
- package/packs/trunk/templates/AGENTS.md.tmpl +87 -0
- package/packs/trunk/templates/CLAUDE.md.tmpl +126 -0
- package/packs/trunk/templates/bundle.md.tmpl +27 -0
package/dist/journal/builder.js
CHANGED
|
@@ -337,7 +337,8 @@ async function extractChanges(runDir, base_sha, head_sha, warnings) {
|
|
|
337
337
|
insertions: null,
|
|
338
338
|
deletions: null,
|
|
339
339
|
top_files: null,
|
|
340
|
-
diff_stat: null
|
|
340
|
+
diff_stat: null,
|
|
341
|
+
ignored_changes: null
|
|
341
342
|
};
|
|
342
343
|
}
|
|
343
344
|
try {
|
|
@@ -373,6 +374,8 @@ async function extractChanges(runDir, base_sha, head_sha, warnings) {
|
|
|
373
374
|
cwd: repoPath,
|
|
374
375
|
encoding: 'utf-8'
|
|
375
376
|
});
|
|
377
|
+
// Extract ignored changes from timeline
|
|
378
|
+
const ignoredChanges = await extractIgnoredChanges(runDir, warnings);
|
|
376
379
|
return {
|
|
377
380
|
base_sha,
|
|
378
381
|
head_sha,
|
|
@@ -380,7 +383,8 @@ async function extractChanges(runDir, base_sha, head_sha, warnings) {
|
|
|
380
383
|
insertions: totalInsertions,
|
|
381
384
|
deletions: totalDeletions,
|
|
382
385
|
top_files: topFiles.length > 0 ? topFiles : null,
|
|
383
|
-
diff_stat: diffStat.trim()
|
|
386
|
+
diff_stat: diffStat.trim(),
|
|
387
|
+
ignored_changes: ignoredChanges
|
|
384
388
|
};
|
|
385
389
|
}
|
|
386
390
|
catch (err) {
|
|
@@ -392,10 +396,39 @@ async function extractChanges(runDir, base_sha, head_sha, warnings) {
|
|
|
392
396
|
insertions: null,
|
|
393
397
|
deletions: null,
|
|
394
398
|
top_files: null,
|
|
395
|
-
diff_stat: null
|
|
399
|
+
diff_stat: null,
|
|
400
|
+
ignored_changes: null
|
|
396
401
|
};
|
|
397
402
|
}
|
|
398
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Extract ignored changes summary from timeline events
|
|
406
|
+
*/
|
|
407
|
+
async function extractIgnoredChanges(runDir, warnings) {
|
|
408
|
+
try {
|
|
409
|
+
const timelinePath = path.join(runDir, 'timeline.jsonl');
|
|
410
|
+
if (!fs.existsSync(timelinePath)) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const events = await readTimelineEvents(timelinePath);
|
|
414
|
+
const ignoredEvents = events.filter(e => e.type === 'ignored_changes');
|
|
415
|
+
if (ignoredEvents.length === 0) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
// Take the last ignored_changes event (most recent)
|
|
419
|
+
const lastEvent = ignoredEvents[ignoredEvents.length - 1];
|
|
420
|
+
const payload = lastEvent.payload;
|
|
421
|
+
return {
|
|
422
|
+
count: payload.ignored_count,
|
|
423
|
+
sample: payload.ignored_sample,
|
|
424
|
+
ignore_check_status: payload.ignore_check_status
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
warnings.push(`Failed to extract ignored changes: ${err.message}`);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
399
432
|
/**
|
|
400
433
|
* Extract next action from stop.json or derive
|
|
401
434
|
*/
|
package/dist/journal/renderer.js
CHANGED
|
@@ -163,6 +163,25 @@ function renderChanges(journal) {
|
|
|
163
163
|
lines.push(journal.changes.diff_stat);
|
|
164
164
|
lines.push('```');
|
|
165
165
|
}
|
|
166
|
+
// Render ignored changes (tool pollution)
|
|
167
|
+
if (journal.changes.ignored_changes) {
|
|
168
|
+
const { count, sample, ignore_check_status } = journal.changes.ignored_changes;
|
|
169
|
+
if (ignore_check_status === 'failed') {
|
|
170
|
+
lines.push('\n> **Warning:** Git ignore-check failed; guard ran in strict mode.');
|
|
171
|
+
}
|
|
172
|
+
if (count > 0) {
|
|
173
|
+
lines.push(`\n**Ignored Tool Noise:** ${count} file${count === 1 ? '' : 's'} (filtered from guard checks)`);
|
|
174
|
+
if (sample.length > 0) {
|
|
175
|
+
lines.push('\nSample (first 20):');
|
|
176
|
+
for (const file of sample.slice(0, 20)) {
|
|
177
|
+
lines.push(`- \`${file}\``);
|
|
178
|
+
}
|
|
179
|
+
if (count > sample.length) {
|
|
180
|
+
lines.push(`\n_...and ${count - sample.length} more_`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
166
185
|
return lines.join('\n');
|
|
167
186
|
}
|
|
168
187
|
function renderNextAction(nextAction) {
|
|
@@ -353,10 +353,12 @@ export function generateOrchestrationMarkdown(state, summary) {
|
|
|
353
353
|
* Order is critical:
|
|
354
354
|
* 1. summary.json
|
|
355
355
|
* 2. orchestration.md
|
|
356
|
-
* 3.
|
|
356
|
+
* 3. receipt.json and receipt.md
|
|
357
|
+
* 4. complete.json OR stop.json (LAST - signals terminal)
|
|
357
358
|
*/
|
|
358
359
|
export function writeTerminalArtifacts(state, repoPath) {
|
|
359
360
|
const handoffsDir = getHandoffsDir(repoPath, state.orchestrator_id);
|
|
361
|
+
const orchDir = getOrchestrationDir(repoPath, state.orchestrator_id);
|
|
360
362
|
fs.mkdirSync(handoffsDir, { recursive: true });
|
|
361
363
|
const isComplete = state.status === 'complete';
|
|
362
364
|
// 1. Write summary.json
|
|
@@ -365,7 +367,20 @@ export function writeTerminalArtifacts(state, repoPath) {
|
|
|
365
367
|
// 2. Write orchestration.md
|
|
366
368
|
const markdown = generateOrchestrationMarkdown(state, summary);
|
|
367
369
|
fs.writeFileSync(path.join(handoffsDir, 'orchestration.md'), markdown);
|
|
368
|
-
// 3. Write
|
|
370
|
+
// 3. Write receipt artifacts (manager dashboard)
|
|
371
|
+
try {
|
|
372
|
+
// Dynamic import to avoid circular dependency
|
|
373
|
+
import('./receipt.js').then(({ buildReceipt, writeReceipt }) => {
|
|
374
|
+
const receipt = buildReceipt(state, repoPath);
|
|
375
|
+
writeReceipt(receipt, repoPath);
|
|
376
|
+
}).catch(() => {
|
|
377
|
+
// Ignore receipt generation errors - not critical
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Ignore receipt generation errors - not critical
|
|
382
|
+
}
|
|
383
|
+
// 4. Write complete.json OR stop.json (LAST)
|
|
369
384
|
if (isComplete) {
|
|
370
385
|
const completeArtifact = buildWaitResult(state, repoPath);
|
|
371
386
|
fs.writeFileSync(path.join(handoffsDir, 'complete.json'), JSON.stringify(completeArtifact, null, 2));
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration Receipt Generator.
|
|
3
|
+
*
|
|
4
|
+
* Produces "manager dashboard" artifacts that summarize an orchestration:
|
|
5
|
+
* - receipt.json: Machine-readable summary with task outcomes, interventions, and issues
|
|
6
|
+
* - receipt.md: Human-readable markdown summary
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { getOrchestrationDir, findOrchestrationDir } from './artifacts.js';
|
|
11
|
+
import { loadOrchestratorState, findLatestOrchestrationId } from './state-machine.js';
|
|
12
|
+
import { getRunsRoot } from '../store/runs-root.js';
|
|
13
|
+
export const RECEIPT_SCHEMA_VERSION = '1';
|
|
14
|
+
/**
|
|
15
|
+
* Suggested fixes for common stop reasons.
|
|
16
|
+
*/
|
|
17
|
+
const STOP_REASON_FIXES = {
|
|
18
|
+
'review_loop_detected': 'Check reviewer expectations match verifier output',
|
|
19
|
+
'max_ticks_reached': 'Consider increasing --max-ticks or breaking into smaller tasks',
|
|
20
|
+
'time_budget_exceeded': 'Consider increasing --time or simplifying the task',
|
|
21
|
+
'verification_failed': 'Review verification commands and fix failing tests',
|
|
22
|
+
'user_stop': 'Task was stopped by user request',
|
|
23
|
+
'collision_detected': 'Ensure tasks have non-overlapping ownership declarations'
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Get suggested fix for a stop reason.
|
|
27
|
+
*/
|
|
28
|
+
function getSuggestedFix(reason) {
|
|
29
|
+
return STOP_REASON_FIXES[reason] ?? 'Review run logs for details';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Find intervention receipt for a run.
|
|
33
|
+
*/
|
|
34
|
+
function findIntervention(repoPath, runId) {
|
|
35
|
+
const runsRoot = getRunsRoot(repoPath);
|
|
36
|
+
const runDir = path.join(runsRoot, runId);
|
|
37
|
+
const interventionsDir = path.join(runDir, 'interventions');
|
|
38
|
+
if (!fs.existsSync(interventionsDir)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
// Find any intervention receipt
|
|
42
|
+
try {
|
|
43
|
+
const entries = fs.readdirSync(interventionsDir, { withFileTypes: true });
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
const receiptPath = path.join(interventionsDir, entry.name, 'intervention-receipt.json');
|
|
47
|
+
if (fs.existsSync(receiptPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf-8'));
|
|
50
|
+
return {
|
|
51
|
+
receipt_path: path.relative(repoPath, receiptPath),
|
|
52
|
+
reason: receipt.reason ?? 'unknown'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {
|
|
57
|
+
receipt_path: path.relative(repoPath, receiptPath),
|
|
58
|
+
reason: 'unknown'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Ignore errors
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read run state to get checkpoint SHA and milestones.
|
|
72
|
+
*/
|
|
73
|
+
function getRunDetails(repoPath, runId) {
|
|
74
|
+
const runsRoot = getRunsRoot(repoPath);
|
|
75
|
+
const stateFile = path.join(runsRoot, runId, 'state.json');
|
|
76
|
+
if (!fs.existsSync(stateFile)) {
|
|
77
|
+
return { milestones: 0 };
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
81
|
+
return {
|
|
82
|
+
checkpointSha: state.checkpoint_commit_sha,
|
|
83
|
+
milestones: state.completed_milestones ?? 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { milestones: 0 };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build receipt task entry from track step.
|
|
92
|
+
*/
|
|
93
|
+
function buildReceiptTask(repoPath, step, track) {
|
|
94
|
+
const runId = step.run_id;
|
|
95
|
+
const result = step.result;
|
|
96
|
+
// Determine status
|
|
97
|
+
let status = 'pending';
|
|
98
|
+
if (result) {
|
|
99
|
+
status = result.status === 'complete' ? 'finished' : 'stopped';
|
|
100
|
+
}
|
|
101
|
+
// Get run details
|
|
102
|
+
const details = runId ? getRunDetails(repoPath, runId) : { milestones: 0 };
|
|
103
|
+
// Find intervention
|
|
104
|
+
const intervention = runId ? findIntervention(repoPath, runId) : undefined;
|
|
105
|
+
return {
|
|
106
|
+
task_path: step.task_path,
|
|
107
|
+
run_id: runId,
|
|
108
|
+
status,
|
|
109
|
+
stop_reason: result?.stop_reason,
|
|
110
|
+
milestones_completed: details.milestones,
|
|
111
|
+
checkpoint_sha: details.checkpointSha,
|
|
112
|
+
duration_ms: result?.elapsed_ms ?? 0,
|
|
113
|
+
intervention
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Aggregate stop reasons across all tasks.
|
|
118
|
+
*/
|
|
119
|
+
function aggregateStopReasons(tasks) {
|
|
120
|
+
const counts = new Map();
|
|
121
|
+
for (const task of tasks) {
|
|
122
|
+
if (task.stop_reason) {
|
|
123
|
+
counts.set(task.stop_reason, (counts.get(task.stop_reason) ?? 0) + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Sort by count descending
|
|
127
|
+
return [...counts.entries()]
|
|
128
|
+
.sort((a, b) => b[1] - a[1])
|
|
129
|
+
.map(([reason, count]) => ({
|
|
130
|
+
reason,
|
|
131
|
+
count,
|
|
132
|
+
suggested_fix: getSuggestedFix(reason)
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build receipt from orchestrator state.
|
|
137
|
+
*/
|
|
138
|
+
export function buildReceipt(state, repoPath) {
|
|
139
|
+
const startTime = new Date(state.started_at).getTime();
|
|
140
|
+
const endTime = state.ended_at ? new Date(state.ended_at).getTime() : Date.now();
|
|
141
|
+
// Build task list
|
|
142
|
+
const tasks = [];
|
|
143
|
+
for (const track of state.tracks) {
|
|
144
|
+
for (const step of track.steps) {
|
|
145
|
+
tasks.push(buildReceiptTask(repoPath, step, track));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Count outcomes
|
|
149
|
+
const tasksCompleted = tasks.filter(t => t.status === 'finished').length;
|
|
150
|
+
const tasksStopped = tasks.filter(t => t.status === 'stopped').length;
|
|
151
|
+
const tasksPending = tasks.filter(t => t.status === 'pending').length;
|
|
152
|
+
const interventionsCount = tasks.filter(t => t.intervention).length;
|
|
153
|
+
const totalCheckpoints = tasks.filter(t => t.checkpoint_sha).length;
|
|
154
|
+
// Aggregate stop reasons
|
|
155
|
+
const topStopReasons = aggregateStopReasons(tasks);
|
|
156
|
+
return {
|
|
157
|
+
schema_version: RECEIPT_SCHEMA_VERSION,
|
|
158
|
+
orchestration_id: state.orchestrator_id,
|
|
159
|
+
started_at: state.started_at,
|
|
160
|
+
completed_at: state.ended_at,
|
|
161
|
+
duration_ms: endTime - startTime,
|
|
162
|
+
summary: {
|
|
163
|
+
tasks_total: tasks.length,
|
|
164
|
+
tasks_completed: tasksCompleted,
|
|
165
|
+
tasks_stopped: tasksStopped,
|
|
166
|
+
tasks_pending: tasksPending,
|
|
167
|
+
interventions_count: interventionsCount,
|
|
168
|
+
total_checkpoints: totalCheckpoints
|
|
169
|
+
},
|
|
170
|
+
tasks,
|
|
171
|
+
top_stop_reasons: topStopReasons
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Format duration in human-readable form.
|
|
176
|
+
*/
|
|
177
|
+
function formatDuration(ms) {
|
|
178
|
+
const seconds = Math.floor(ms / 1000);
|
|
179
|
+
const minutes = Math.floor(seconds / 60);
|
|
180
|
+
const hours = Math.floor(minutes / 60);
|
|
181
|
+
if (hours > 0) {
|
|
182
|
+
return `${hours}h ${minutes % 60}m`;
|
|
183
|
+
}
|
|
184
|
+
if (minutes > 0) {
|
|
185
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
186
|
+
}
|
|
187
|
+
return `${seconds}s`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Generate markdown receipt from JSON receipt.
|
|
191
|
+
*/
|
|
192
|
+
export function generateReceiptMarkdown(receipt) {
|
|
193
|
+
const lines = [];
|
|
194
|
+
// Header
|
|
195
|
+
lines.push(`# Orchestration Receipt: ${receipt.orchestration_id}`);
|
|
196
|
+
lines.push('');
|
|
197
|
+
// Summary table
|
|
198
|
+
lines.push('## Summary');
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push('| Metric | Value |');
|
|
201
|
+
lines.push('|--------|-------|');
|
|
202
|
+
lines.push(`| Duration | ${formatDuration(receipt.duration_ms)} |`);
|
|
203
|
+
lines.push(`| Tasks | ${receipt.summary.tasks_completed}/${receipt.summary.tasks_total} completed |`);
|
|
204
|
+
lines.push(`| Checkpoints | ${receipt.summary.total_checkpoints} |`);
|
|
205
|
+
lines.push(`| Interventions | ${receipt.summary.interventions_count} |`);
|
|
206
|
+
lines.push('');
|
|
207
|
+
// Tasks section
|
|
208
|
+
lines.push('## Tasks');
|
|
209
|
+
lines.push('');
|
|
210
|
+
for (const task of receipt.tasks) {
|
|
211
|
+
const statusIcon = task.status === 'finished' ? '✓' : task.status === 'stopped' ? '⚠' : '○';
|
|
212
|
+
const taskName = path.basename(task.task_path);
|
|
213
|
+
lines.push(`### ${statusIcon} ${taskName}`);
|
|
214
|
+
if (task.run_id) {
|
|
215
|
+
lines.push(`- Run: ${task.run_id}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push(`- Status: ${task.status}${task.stop_reason ? ` (${task.stop_reason})` : ''}`);
|
|
218
|
+
if (task.checkpoint_sha) {
|
|
219
|
+
lines.push(`- Checkpoint: ${task.checkpoint_sha.slice(0, 7)}`);
|
|
220
|
+
}
|
|
221
|
+
if (task.intervention) {
|
|
222
|
+
lines.push(`- Intervention: ${task.intervention.reason}`);
|
|
223
|
+
}
|
|
224
|
+
lines.push('');
|
|
225
|
+
}
|
|
226
|
+
// Top issues
|
|
227
|
+
if (receipt.top_stop_reasons.length > 0) {
|
|
228
|
+
lines.push('## Top Issues');
|
|
229
|
+
lines.push('');
|
|
230
|
+
for (let i = 0; i < receipt.top_stop_reasons.length; i++) {
|
|
231
|
+
const entry = receipt.top_stop_reasons[i];
|
|
232
|
+
lines.push(`${i + 1}. **${entry.reason}** (${entry.count} occurrence${entry.count > 1 ? 's' : ''})`);
|
|
233
|
+
lines.push(` - Suggested: ${entry.suggested_fix}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push('');
|
|
236
|
+
}
|
|
237
|
+
// Next steps
|
|
238
|
+
if (receipt.summary.tasks_stopped > 0) {
|
|
239
|
+
lines.push('## Next Steps');
|
|
240
|
+
lines.push('');
|
|
241
|
+
const stoppedTasks = receipt.tasks.filter(t => t.status === 'stopped');
|
|
242
|
+
for (const task of stoppedTasks) {
|
|
243
|
+
lines.push(`- Review stopped task: ${path.basename(task.task_path)}`);
|
|
244
|
+
}
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Write receipt artifacts to orchestration directory.
|
|
251
|
+
*/
|
|
252
|
+
export function writeReceipt(receipt, repoPath) {
|
|
253
|
+
const orchDir = getOrchestrationDir(repoPath, receipt.orchestration_id);
|
|
254
|
+
fs.mkdirSync(orchDir, { recursive: true });
|
|
255
|
+
const jsonPath = path.join(orchDir, 'receipt.json');
|
|
256
|
+
const mdPath = path.join(orchDir, 'receipt.md');
|
|
257
|
+
fs.writeFileSync(jsonPath, JSON.stringify(receipt, null, 2));
|
|
258
|
+
fs.writeFileSync(mdPath, generateReceiptMarkdown(receipt));
|
|
259
|
+
return { json: jsonPath, md: mdPath };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Load existing receipt if available.
|
|
263
|
+
*/
|
|
264
|
+
export function loadReceipt(repoPath, orchestratorId) {
|
|
265
|
+
const orchDir = findOrchestrationDir(repoPath, orchestratorId);
|
|
266
|
+
if (!orchDir) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const receiptPath = path.join(orchDir, 'receipt.json');
|
|
270
|
+
if (!fs.existsSync(receiptPath)) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(fs.readFileSync(receiptPath, 'utf-8'));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Generate or load receipt for an orchestration.
|
|
282
|
+
*/
|
|
283
|
+
export function getReceipt(repoPath, orchestratorId) {
|
|
284
|
+
// Resolve "latest"
|
|
285
|
+
let resolvedId = orchestratorId;
|
|
286
|
+
if (orchestratorId === 'latest') {
|
|
287
|
+
const latest = findLatestOrchestrationId(repoPath);
|
|
288
|
+
if (!latest) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
resolvedId = latest;
|
|
292
|
+
}
|
|
293
|
+
// Try to load existing receipt
|
|
294
|
+
const existing = loadReceipt(repoPath, resolvedId);
|
|
295
|
+
if (existing) {
|
|
296
|
+
return existing;
|
|
297
|
+
}
|
|
298
|
+
// Generate from state
|
|
299
|
+
const state = loadOrchestratorState(resolvedId, repoPath);
|
|
300
|
+
if (!state) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return buildReceipt(state, repoPath);
|
|
304
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop Footer - Consistent "Next Steps" block for stopped runs.
|
|
3
|
+
*
|
|
4
|
+
* Shows exactly 3 actions derived from the brain module for consistency
|
|
5
|
+
* across front door, continue command, and stop footer.
|
|
6
|
+
*
|
|
7
|
+
* For review_loop_detected, also shows:
|
|
8
|
+
* - Reviewer requested items
|
|
9
|
+
* - Commands to satisfy
|
|
10
|
+
* - Suggested intervention command
|
|
11
|
+
*/
|
|
12
|
+
const SEPARATOR = '─'.repeat(50);
|
|
13
|
+
/**
|
|
14
|
+
* Get context line based on stop reason.
|
|
15
|
+
*/
|
|
16
|
+
function getContextLine(ctx) {
|
|
17
|
+
switch (ctx.stopReason) {
|
|
18
|
+
case 'review_loop_detected':
|
|
19
|
+
if (ctx.lastError) {
|
|
20
|
+
// Extract first 2 items from error message if it contains a list
|
|
21
|
+
const match = ctx.lastError.match(/(?:Unmet|Failed|Missing):\s*(.+)/i);
|
|
22
|
+
if (match) {
|
|
23
|
+
const items = match[1].split(/[,;]/).slice(0, 2).map(s => s.trim());
|
|
24
|
+
return `Unmet: ${items.join(', ')}`;
|
|
25
|
+
}
|
|
26
|
+
return `Unmet: ${ctx.lastError.slice(0, 60)}...`;
|
|
27
|
+
}
|
|
28
|
+
return 'Unmet: review requirements not satisfied';
|
|
29
|
+
case 'verification_failed':
|
|
30
|
+
if (ctx.lastError) {
|
|
31
|
+
const cmdMatch = ctx.lastError.match(/command.*failed|failed.*command/i);
|
|
32
|
+
if (cmdMatch) {
|
|
33
|
+
return `Failed: ${ctx.lastError.slice(0, 60)}`;
|
|
34
|
+
}
|
|
35
|
+
return `Failed: verification check`;
|
|
36
|
+
}
|
|
37
|
+
return 'Failed: verification check';
|
|
38
|
+
case 'scope_violation':
|
|
39
|
+
if (ctx.lastError) {
|
|
40
|
+
// Extract file paths from error
|
|
41
|
+
const files = ctx.lastError.match(/[\w./\-_]+\.\w+/g);
|
|
42
|
+
if (files && files.length > 0) {
|
|
43
|
+
return `Files: ${files.slice(0, 2).join(', ')}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return 'Files: scope boundary exceeded';
|
|
47
|
+
case 'stalled_timeout':
|
|
48
|
+
case 'worker_call_timeout':
|
|
49
|
+
return `Stalled at: ${ctx.phase || 'unknown phase'}`;
|
|
50
|
+
case 'guard_fail':
|
|
51
|
+
case 'preflight_failed':
|
|
52
|
+
if (ctx.lastError) {
|
|
53
|
+
const guardMatch = ctx.lastError.match(/guard.*failed|failed.*guard/i);
|
|
54
|
+
if (guardMatch) {
|
|
55
|
+
return `Guard: ${ctx.lastError.slice(0, 50)}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return 'Guard: preflight check failed';
|
|
59
|
+
default:
|
|
60
|
+
// No context line for other reasons
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build next steps commands.
|
|
66
|
+
*/
|
|
67
|
+
export function buildNextSteps(runId, stopReason) {
|
|
68
|
+
return {
|
|
69
|
+
resume: `runr resume ${runId}`,
|
|
70
|
+
intervene: `runr intervene ${runId} --reason ${stopReason || 'manual'} --note "..."`,
|
|
71
|
+
audit: `runr runs audit --run ${runId}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format stop footer for console output.
|
|
76
|
+
* If brainActions are provided, uses them for consistent UX across all entry points.
|
|
77
|
+
* Otherwise falls back to default 3 commands (resume, intervene, audit).
|
|
78
|
+
*/
|
|
79
|
+
export function formatStopFooter(ctx, brainActions) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push(SEPARATOR);
|
|
82
|
+
// Header with optional round info
|
|
83
|
+
if (ctx.stopReason === 'review_loop_detected' && ctx.reviewRound && ctx.maxReviewRounds) {
|
|
84
|
+
lines.push(`STOPPED: ${ctx.stopReason} (round ${ctx.reviewRound}/${ctx.maxReviewRounds})`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
lines.push(`STOPPED: ${ctx.stopReason}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push('');
|
|
90
|
+
// Last checkpoint line
|
|
91
|
+
if (ctx.checkpointSha) {
|
|
92
|
+
lines.push(`Last checkpoint: ${ctx.checkpointSha.slice(0, 7)} (milestone ${ctx.milestoneIndex + 1}/${ctx.milestonesTotal})`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
lines.push(`No checkpoint (milestone ${ctx.milestoneIndex + 1}/${ctx.milestonesTotal})`);
|
|
96
|
+
}
|
|
97
|
+
// Enhanced output for review_loop_detected
|
|
98
|
+
if (ctx.stopReason === 'review_loop_detected') {
|
|
99
|
+
// Show reviewer requests if available
|
|
100
|
+
if (ctx.reviewerRequests && ctx.reviewerRequests.length > 0) {
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push('Reviewer requested:');
|
|
103
|
+
ctx.reviewerRequests.slice(0, 3).forEach((req, i) => {
|
|
104
|
+
lines.push(` ${i + 1}. ${req}`);
|
|
105
|
+
});
|
|
106
|
+
if (ctx.reviewerRequests.length > 3) {
|
|
107
|
+
lines.push(` ... and ${ctx.reviewerRequests.length - 3} more`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Show commands to satisfy if available
|
|
111
|
+
if (ctx.commandsToSatisfy && ctx.commandsToSatisfy.length > 0) {
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push('Commands to satisfy:');
|
|
114
|
+
for (const cmd of ctx.commandsToSatisfy) {
|
|
115
|
+
lines.push(` ${cmd}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Show suggested intervention
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('Suggested intervention:');
|
|
121
|
+
if (ctx.commandsToSatisfy && ctx.commandsToSatisfy.length > 0) {
|
|
122
|
+
const cmdArgs = ctx.commandsToSatisfy.map(c => `--cmd "${c}"`).join(' ');
|
|
123
|
+
lines.push(` runr intervene ${ctx.runId} --reason review_loop \\`);
|
|
124
|
+
lines.push(` --note "Fixed review requests" ${cmdArgs}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
lines.push(` runr intervene ${ctx.runId} --reason review_loop \\`);
|
|
128
|
+
lines.push(` --note "Fixed review requests" --cmd "npm run build""`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Context line based on stop reason (for non-review_loop cases)
|
|
133
|
+
const contextLine = getContextLine(ctx);
|
|
134
|
+
if (contextLine) {
|
|
135
|
+
lines.push(contextLine);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
lines.push('');
|
|
139
|
+
lines.push('Next steps:');
|
|
140
|
+
if (brainActions && brainActions.length >= 3) {
|
|
141
|
+
// Use brain-computed actions for consistency across UX
|
|
142
|
+
for (const action of brainActions.slice(0, 3)) {
|
|
143
|
+
lines.push(` ${action.command}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Fallback to default commands
|
|
148
|
+
const steps = buildNextSteps(ctx.runId, ctx.stopReason);
|
|
149
|
+
lines.push(` ${steps.resume}`);
|
|
150
|
+
lines.push(` ${steps.intervene}`);
|
|
151
|
+
lines.push(` ${steps.audit}`);
|
|
152
|
+
}
|
|
153
|
+
lines.push(SEPARATOR);
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build stop context from run state.
|
|
158
|
+
* Extended version accepts optional review loop data.
|
|
159
|
+
*/
|
|
160
|
+
export function buildStopContext(state, reviewLoopData) {
|
|
161
|
+
return {
|
|
162
|
+
runId: state.run_id,
|
|
163
|
+
stopReason: state.stop_reason || 'unknown',
|
|
164
|
+
checkpointSha: state.checkpoint_commit_sha,
|
|
165
|
+
milestoneIndex: state.milestone_index,
|
|
166
|
+
milestonesTotal: state.milestones.length,
|
|
167
|
+
lastError: state.last_error,
|
|
168
|
+
phase: state.phase,
|
|
169
|
+
// Extended review loop fields
|
|
170
|
+
reviewRound: reviewLoopData?.reviewRound ?? state.review_rounds,
|
|
171
|
+
maxReviewRounds: reviewLoopData?.maxReviewRounds,
|
|
172
|
+
reviewerRequests: reviewLoopData?.reviewerRequests,
|
|
173
|
+
commandsToSatisfy: reviewLoopData?.commandsToSatisfy
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Print stop footer to console.
|
|
178
|
+
* If reviewLoopData is provided, includes enhanced diagnostics.
|
|
179
|
+
* If brainActions is provided, uses them for consistent UX (from brain module).
|
|
180
|
+
*/
|
|
181
|
+
export function printStopFooter(state, reviewLoopData, brainActions) {
|
|
182
|
+
const ctx = buildStopContext(state, reviewLoopData);
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(formatStopFooter(ctx, brainActions));
|
|
185
|
+
}
|