@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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runr audit - View project history classified by provenance
|
|
3
|
+
*
|
|
4
|
+
* Shows the timeline of commits classified as:
|
|
5
|
+
* - CHECKPOINT: Runr checkpoint commits with receipts
|
|
6
|
+
* - INTERVENTION: Manual work recorded via runr intervene
|
|
7
|
+
* - ATTRIBUTED: Has Runr trailers but no receipt
|
|
8
|
+
* - GAP: No Runr attribution (audit gap)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* runr audit # Last 50 commits on current branch
|
|
12
|
+
* runr audit --range main~80..main # Custom range
|
|
13
|
+
* runr audit --run <run_id> # Commits for specific run
|
|
14
|
+
* runr audit --json # JSON output
|
|
15
|
+
*/
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
import { parseGitLog, classifyCommits, generateSummary, formatClassification, getClassificationIcon } from '../audit/classifier.js';
|
|
18
|
+
/**
|
|
19
|
+
* Get the default branch for range.
|
|
20
|
+
*/
|
|
21
|
+
function getDefaultBranch(repoPath) {
|
|
22
|
+
try {
|
|
23
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
24
|
+
cwd: repoPath,
|
|
25
|
+
encoding: 'utf-8'
|
|
26
|
+
}).trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return 'main';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the git range string.
|
|
34
|
+
*/
|
|
35
|
+
function buildRange(options) {
|
|
36
|
+
if (options.range) {
|
|
37
|
+
return options.range;
|
|
38
|
+
}
|
|
39
|
+
const branch = getDefaultBranch(options.repo);
|
|
40
|
+
const limit = options.limit || 50;
|
|
41
|
+
return `${branch}~${limit}..${branch}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Print table of classified commits.
|
|
45
|
+
*/
|
|
46
|
+
function printTable(commits) {
|
|
47
|
+
// Column headers
|
|
48
|
+
const headers = ['', 'SHA', 'TYPE', 'RUN ID', 'SUBJECT'];
|
|
49
|
+
// Calculate column widths (fixed for readability)
|
|
50
|
+
const widths = [2, 7, 12, 14, 50];
|
|
51
|
+
// Print header
|
|
52
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(' ');
|
|
53
|
+
console.log(headerLine);
|
|
54
|
+
console.log('-'.repeat(headerLine.length));
|
|
55
|
+
// Print rows (most recent first)
|
|
56
|
+
for (const commit of commits) {
|
|
57
|
+
const icon = getClassificationIcon(commit.classification);
|
|
58
|
+
const type = formatClassification(commit.classification);
|
|
59
|
+
const runId = commit.runId || '-';
|
|
60
|
+
const subject = commit.subject.length > widths[4]
|
|
61
|
+
? commit.subject.slice(0, widths[4] - 3) + '...'
|
|
62
|
+
: commit.subject;
|
|
63
|
+
const row = [
|
|
64
|
+
icon.padEnd(widths[0]),
|
|
65
|
+
commit.shortSha.padEnd(widths[1]),
|
|
66
|
+
type.padEnd(widths[2]),
|
|
67
|
+
runId.padEnd(widths[3]),
|
|
68
|
+
subject.padEnd(widths[4])
|
|
69
|
+
];
|
|
70
|
+
console.log(row.join(' '));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Print summary section.
|
|
75
|
+
*/
|
|
76
|
+
function printSummary(summary, strict = false) {
|
|
77
|
+
const { counts, gaps, runsReferenced, explicitCoverage, inferredCoverage, fullCoverage } = summary;
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log('Summary');
|
|
80
|
+
console.log('-------');
|
|
81
|
+
console.log(`Total commits: ${counts.total}`);
|
|
82
|
+
console.log(` ✓ Checkpoints: ${counts.runr_checkpoint}`);
|
|
83
|
+
console.log(` ⚡ Interventions: ${counts.runr_intervention}`);
|
|
84
|
+
console.log(` ~ Inferred: ${counts.runr_inferred}`);
|
|
85
|
+
console.log(` ○ Attributed: ${counts.manual_attributed}`);
|
|
86
|
+
console.log(` ? Gaps: ${counts.gap}`);
|
|
87
|
+
if (runsReferenced.length > 0) {
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(`Runs referenced: ${runsReferenced.length}`);
|
|
90
|
+
const displayRuns = runsReferenced.slice(0, 5);
|
|
91
|
+
for (const runId of displayRuns) {
|
|
92
|
+
console.log(` ${runId}`);
|
|
93
|
+
}
|
|
94
|
+
if (runsReferenced.length > 5) {
|
|
95
|
+
console.log(` ...${runsReferenced.length - 5} more`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Audit health indicator with coverage numbers
|
|
99
|
+
console.log('');
|
|
100
|
+
// Show coverage differently based on strict mode
|
|
101
|
+
const coverageDisplay = strict
|
|
102
|
+
? `${explicitCoverage}% coverage (strict mode)`
|
|
103
|
+
: `${explicitCoverage}% (explicit) / ${inferredCoverage}% (with inferred)`;
|
|
104
|
+
if (gaps.length === 0) {
|
|
105
|
+
console.log(`Audit status: ✓ CLEAN (${coverageDisplay})`);
|
|
106
|
+
}
|
|
107
|
+
else if (gaps.length <= 3) {
|
|
108
|
+
console.log(`Audit status: ⚠ ${gaps.length} gap${gaps.length === 1 ? '' : 's'} (${coverageDisplay})`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(`Audit status: ✗ ${gaps.length} gaps (${coverageDisplay})`);
|
|
112
|
+
}
|
|
113
|
+
// Show top gaps if any
|
|
114
|
+
if (gaps.length > 0) {
|
|
115
|
+
console.log('');
|
|
116
|
+
const gapLabel = strict ? 'Top gaps (unattributed commits + inferred):' : 'Top gaps (unattributed commits):';
|
|
117
|
+
console.log(gapLabel);
|
|
118
|
+
const displayGaps = gaps.slice(0, 5);
|
|
119
|
+
for (const gap of displayGaps) {
|
|
120
|
+
console.log(` ${gap.shortSha} ${gap.subject.slice(0, 50)}`);
|
|
121
|
+
}
|
|
122
|
+
if (gaps.length > 5) {
|
|
123
|
+
console.log(` ...${gaps.length - 5} more`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Print coverage report in human-readable format.
|
|
129
|
+
*/
|
|
130
|
+
function printCoverageReport(summary, options) {
|
|
131
|
+
const { counts, explicitCoverage, inferredCoverage, fullCoverage } = summary;
|
|
132
|
+
const explicitCount = counts.runr_checkpoint + counts.runr_intervention;
|
|
133
|
+
const inferredCount = explicitCount + counts.runr_inferred;
|
|
134
|
+
const fullCount = inferredCount + counts.manual_attributed;
|
|
135
|
+
console.log('Coverage Report');
|
|
136
|
+
console.log('---------------');
|
|
137
|
+
console.log(`Explicit coverage: ${explicitCoverage}% (${explicitCount}/${counts.total})`);
|
|
138
|
+
console.log(`With inferred: ${inferredCoverage}% (${inferredCount}/${counts.total})`);
|
|
139
|
+
console.log(`Full (with attributed): ${fullCoverage}% (${fullCount}/${counts.total})`);
|
|
140
|
+
// Show threshold status if specified
|
|
141
|
+
if (options.failUnder !== undefined || options.failUnderWithInferred !== undefined) {
|
|
142
|
+
console.log('');
|
|
143
|
+
if (options.failUnder !== undefined) {
|
|
144
|
+
const status = explicitCoverage >= options.failUnder ? 'PASS' : 'FAIL';
|
|
145
|
+
console.log(`Threshold: ${options.failUnder}% (explicit)`);
|
|
146
|
+
console.log(`Status: ${status} (explicit coverage ${explicitCoverage}% ${status === 'PASS' ? '>=' : '<'} ${options.failUnder}%)`);
|
|
147
|
+
}
|
|
148
|
+
if (options.failUnderWithInferred !== undefined) {
|
|
149
|
+
const status = inferredCoverage >= options.failUnderWithInferred ? 'PASS' : 'FAIL';
|
|
150
|
+
console.log(`Threshold: ${options.failUnderWithInferred}% (with inferred)`);
|
|
151
|
+
console.log(`Status: ${status} (inferred coverage ${inferredCoverage}% ${status === 'PASS' ? '>=' : '<'} ${options.failUnderWithInferred}%)`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate coverage JSON report.
|
|
157
|
+
*/
|
|
158
|
+
function generateCoverageJson(summary) {
|
|
159
|
+
const { counts, gaps, runsReferenced, explicitCoverage, inferredCoverage, fullCoverage } = summary;
|
|
160
|
+
return {
|
|
161
|
+
range: summary.range,
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
total_commits: counts.total,
|
|
164
|
+
classifications: {
|
|
165
|
+
runr_checkpoint: counts.runr_checkpoint,
|
|
166
|
+
runr_intervention: counts.runr_intervention,
|
|
167
|
+
runr_inferred: counts.runr_inferred,
|
|
168
|
+
manual_attributed: counts.manual_attributed,
|
|
169
|
+
gap: counts.gap
|
|
170
|
+
},
|
|
171
|
+
coverage: {
|
|
172
|
+
explicit: explicitCoverage / 100,
|
|
173
|
+
with_inferred: inferredCoverage / 100,
|
|
174
|
+
with_attributed: fullCoverage / 100
|
|
175
|
+
},
|
|
176
|
+
gaps: gaps.map(g => ({ sha: g.sha, subject: g.subject })),
|
|
177
|
+
runs_referenced: runsReferenced
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Filter commits for a specific run.
|
|
182
|
+
*/
|
|
183
|
+
function filterByRun(commits, runId) {
|
|
184
|
+
return commits.filter(c => c.runId === runId);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Audit command: View project history by provenance.
|
|
188
|
+
*/
|
|
189
|
+
export async function auditCommand(options) {
|
|
190
|
+
const range = buildRange(options);
|
|
191
|
+
// Parse git log
|
|
192
|
+
let commits = parseGitLog(options.repo, range);
|
|
193
|
+
if (commits.length === 0) {
|
|
194
|
+
if (options.json) {
|
|
195
|
+
console.log(JSON.stringify({ error: 'no_commits', range }, null, 2));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(`No commits found in range: ${range}`);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Classify commits
|
|
203
|
+
commits = classifyCommits(commits, options.repo);
|
|
204
|
+
// Filter by run if specified
|
|
205
|
+
if (options.runId) {
|
|
206
|
+
commits = filterByRun(commits, options.runId);
|
|
207
|
+
if (commits.length === 0) {
|
|
208
|
+
if (options.json) {
|
|
209
|
+
console.log(JSON.stringify({ error: 'no_commits_for_run', runId: options.runId }, null, 2));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
console.log(`No commits found for run: ${options.runId}`);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Generate summary (strict mode treats inferred as gaps)
|
|
218
|
+
const summary = generateSummary(commits, range, options.strict);
|
|
219
|
+
// Output
|
|
220
|
+
if (options.coverage) {
|
|
221
|
+
// Coverage report mode
|
|
222
|
+
if (options.json) {
|
|
223
|
+
console.log(JSON.stringify(generateCoverageJson(summary), null, 2));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.log(`Audit: ${range}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
printCoverageReport(summary, options);
|
|
229
|
+
console.log('');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (options.json) {
|
|
233
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log(`Audit: ${range}`);
|
|
237
|
+
if (options.runId) {
|
|
238
|
+
console.log(`Filtered by run: ${options.runId}`);
|
|
239
|
+
}
|
|
240
|
+
if (options.strict) {
|
|
241
|
+
console.log('(strict mode: inferred attribution treated as gaps)');
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
printTable(commits);
|
|
245
|
+
printSummary(summary, options.strict);
|
|
246
|
+
console.log('');
|
|
247
|
+
}
|
|
248
|
+
// Check threshold and set exit code if specified
|
|
249
|
+
if (options.failUnder !== undefined) {
|
|
250
|
+
if (summary.explicitCoverage < options.failUnder) {
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (options.failUnderWithInferred !== undefined) {
|
|
255
|
+
if (summary.inferredCoverage < options.failUnderWithInferred) {
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { RunStore } from '../store/run-store.js';
|
|
5
|
+
/**
|
|
6
|
+
* Get verification status display from run state.
|
|
7
|
+
*/
|
|
8
|
+
function getVerificationStatus(state) {
|
|
9
|
+
const evidence = state.last_verification_evidence;
|
|
10
|
+
if (!evidence) {
|
|
11
|
+
return {
|
|
12
|
+
status: 'UNVERIFIED',
|
|
13
|
+
tier: 'none',
|
|
14
|
+
commands: 'none',
|
|
15
|
+
resultLine: '⚠ UNVERIFIED'
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const tier = evidence.tiers_run?.[0] || 'none';
|
|
19
|
+
const commands = evidence.commands_run?.map(c => c.command).join(', ') || 'none';
|
|
20
|
+
// If evidence exists, assume PASSED (adjust if you track explicit pass/fail)
|
|
21
|
+
return {
|
|
22
|
+
status: 'PASSED',
|
|
23
|
+
tier,
|
|
24
|
+
commands,
|
|
25
|
+
resultLine: '✓ PASSED'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Render milestone checklist.
|
|
30
|
+
*/
|
|
31
|
+
function renderMilestones(state) {
|
|
32
|
+
const milestones = state.milestones || [];
|
|
33
|
+
const currentIndex = state.milestone_index ?? -1;
|
|
34
|
+
const completed = currentIndex >= 0 ? Math.min(currentIndex + 1, milestones.length) : 0;
|
|
35
|
+
const total = milestones.length;
|
|
36
|
+
const lines = [];
|
|
37
|
+
lines.push(`## Milestones (${completed}/${total})`);
|
|
38
|
+
if (milestones.length === 0) {
|
|
39
|
+
lines.push('- (none)');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (let i = 0; i < milestones.length; i++) {
|
|
43
|
+
const goal = milestones[i]?.goal || '';
|
|
44
|
+
const checked = i <= currentIndex ? 'x' : ' ';
|
|
45
|
+
lines.push(`- [${checked}] M${i}: ${goal}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get git diffstat for checkpoint.
|
|
52
|
+
*/
|
|
53
|
+
async function getCheckpointDiffstat(repoPath, sha) {
|
|
54
|
+
try {
|
|
55
|
+
const result = await execa('git', ['show', '--stat', '--oneline', '--no-color', sha], {
|
|
56
|
+
cwd: repoPath
|
|
57
|
+
});
|
|
58
|
+
return result.stdout.trim();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return `Error: unable to compute git stat for checkpoint ${sha}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get timeline event summary (all event types, sorted alphabetically).
|
|
66
|
+
*/
|
|
67
|
+
async function getTimelineSummary(runDir) {
|
|
68
|
+
const timelinePath = path.join(runDir, 'timeline.jsonl');
|
|
69
|
+
try {
|
|
70
|
+
const content = await fs.readFile(timelinePath, 'utf-8');
|
|
71
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
72
|
+
const counts = new Map();
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
try {
|
|
75
|
+
const event = JSON.parse(line);
|
|
76
|
+
if (event.type) {
|
|
77
|
+
counts.set(event.type, (counts.get(event.type) ?? 0) + 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Skip malformed lines
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Deterministic: all event types, sorted alphabetically
|
|
85
|
+
return [...counts.entries()]
|
|
86
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
87
|
+
.map(([type, count]) => `- ${type}: ${count}`);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return ['- (none)'];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if artifact file exists.
|
|
95
|
+
*/
|
|
96
|
+
async function fileExists(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
await fs.access(filePath);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Bundle command: Generate deterministic markdown evidence packet.
|
|
107
|
+
*/
|
|
108
|
+
export async function bundleCommand(options) {
|
|
109
|
+
const runStore = RunStore.init(options.runId, options.repo);
|
|
110
|
+
let state;
|
|
111
|
+
try {
|
|
112
|
+
state = runStore.readState();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
console.error(`Error: run state not found for ${options.runId}`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const checkpoint = state.checkpoint_commit_sha || 'none';
|
|
120
|
+
const created = state.started_at || '';
|
|
121
|
+
const phase = state.phase || '';
|
|
122
|
+
const stopReason = state.stop_reason || '';
|
|
123
|
+
const verification = getVerificationStatus(state);
|
|
124
|
+
const milestoneLines = renderMilestones(state);
|
|
125
|
+
// Get checkpoint diffstat
|
|
126
|
+
let diffstat = 'none';
|
|
127
|
+
if (checkpoint !== 'none') {
|
|
128
|
+
diffstat = await getCheckpointDiffstat(options.repo, checkpoint);
|
|
129
|
+
}
|
|
130
|
+
// Get timeline summary
|
|
131
|
+
const timelineLines = await getTimelineSummary(runStore.path);
|
|
132
|
+
// Artifact paths (relative, no absolute paths)
|
|
133
|
+
const artifacts = [
|
|
134
|
+
`- Timeline: .runr/runs/${options.runId}/timeline.jsonl`,
|
|
135
|
+
`- Journal: .runr/runs/${options.runId}/journal.md`,
|
|
136
|
+
`- State: .runr/runs/${options.runId}/state.json`
|
|
137
|
+
];
|
|
138
|
+
// Check if review digest exists
|
|
139
|
+
const reviewPath = path.join(runStore.path, 'review_digest.md');
|
|
140
|
+
if (await fileExists(reviewPath)) {
|
|
141
|
+
artifacts.push(`- Review: .runr/runs/${options.runId}/review_digest.md`);
|
|
142
|
+
}
|
|
143
|
+
// Build output (deterministic markdown)
|
|
144
|
+
const lines = [
|
|
145
|
+
`# Run ${options.runId}`,
|
|
146
|
+
'',
|
|
147
|
+
`**Created:** ${created}`,
|
|
148
|
+
`**Checkpoint:** ${checkpoint}`,
|
|
149
|
+
`**Status:** ${phase}${stopReason ? ` (${stopReason})` : ''}`,
|
|
150
|
+
'',
|
|
151
|
+
...milestoneLines,
|
|
152
|
+
'',
|
|
153
|
+
'## Verification Evidence',
|
|
154
|
+
`**Status:** ${verification.status}`,
|
|
155
|
+
`**Tier:** ${verification.tier}`,
|
|
156
|
+
`**Commands:** ${verification.commands}`,
|
|
157
|
+
`**Result:** ${verification.resultLine}`,
|
|
158
|
+
'',
|
|
159
|
+
'## Checkpoint Diffstat',
|
|
160
|
+
diffstat,
|
|
161
|
+
'',
|
|
162
|
+
'## Timeline Event Summary',
|
|
163
|
+
...timelineLines,
|
|
164
|
+
'',
|
|
165
|
+
'## Artifacts',
|
|
166
|
+
...artifacts,
|
|
167
|
+
'',
|
|
168
|
+
'---',
|
|
169
|
+
'🤖 Generated with Runr'
|
|
170
|
+
];
|
|
171
|
+
const output = lines.join('\n');
|
|
172
|
+
// Write to file or stdout
|
|
173
|
+
if (options.output) {
|
|
174
|
+
await fs.writeFile(options.output, output, 'utf-8');
|
|
175
|
+
console.log(`Wrote bundle to ${options.output}`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log(output);
|
|
179
|
+
}
|
|
180
|
+
}
|