@weldr/runr 0.4.0 → 0.7.3
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 +166 -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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Check Parser - Extracts actionable requests from review feedback.
|
|
3
|
+
*
|
|
4
|
+
* Parses review responses to identify:
|
|
5
|
+
* - Bullet points and numbered lists
|
|
6
|
+
* - "Please fix/add/update" patterns
|
|
7
|
+
* - Done check names marked as incomplete
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Command mapping patterns - keywords to verification commands.
|
|
11
|
+
*/
|
|
12
|
+
const COMMAND_MAPPINGS = [
|
|
13
|
+
{
|
|
14
|
+
keywords: ['type error', 'typescript error', 'ts error', 'typecheck', 'tsc'],
|
|
15
|
+
command: 'npm run typecheck',
|
|
16
|
+
category: 'type_errors'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
keywords: ['test', 'tests', 'unit test', 'testing', 'spec'],
|
|
20
|
+
command: 'npm test',
|
|
21
|
+
category: 'tests'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
keywords: ['lint', 'linting', 'eslint', 'style'],
|
|
25
|
+
command: 'npm run lint',
|
|
26
|
+
category: 'lint'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
keywords: ['build', 'compile', 'bundle'],
|
|
30
|
+
command: 'npm run build',
|
|
31
|
+
category: 'build'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
keywords: ['coverage', 'test coverage', 'code coverage'],
|
|
35
|
+
command: 'npm test -- --coverage',
|
|
36
|
+
category: 'coverage'
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Request extraction patterns.
|
|
41
|
+
*/
|
|
42
|
+
const REQUEST_PATTERNS = [
|
|
43
|
+
// Numbered lists: "1. Fix the issue"
|
|
44
|
+
/^\s*\d+[.)]\s*(.+)/gm,
|
|
45
|
+
// Bullet points: "- Fix the issue", "* Fix the issue"
|
|
46
|
+
/^\s*[-*•]\s*(.+)/gm,
|
|
47
|
+
// "Please" patterns: "Please fix...", "Please add..."
|
|
48
|
+
/please\s+(fix|add|update|remove|change|include|ensure|verify|check|run)\s+(.+?)(?:\.|$)/gi,
|
|
49
|
+
// "Need to" patterns: "Need to fix...", "Need to add..."
|
|
50
|
+
/(?:need|needs|should|must|have)\s+to\s+(fix|add|update|remove|change|include|run)\s+(.+?)(?:\.|$)/gi,
|
|
51
|
+
// "Fix the X" patterns
|
|
52
|
+
/fix\s+(?:the\s+)?(.+?)\s+(?:error|issue|problem|bug)/gi,
|
|
53
|
+
// "Add X" patterns
|
|
54
|
+
/add\s+(?:the\s+)?(.+?)\s+(?:test|output|evidence|check)/gi,
|
|
55
|
+
// "Missing X" patterns
|
|
56
|
+
/missing\s+(.+?)(?:\.|,|$)/gi
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Extract bullet points and numbered lists from text.
|
|
60
|
+
*/
|
|
61
|
+
export function extractListItems(text) {
|
|
62
|
+
const items = [];
|
|
63
|
+
// Numbered lists
|
|
64
|
+
const numberedMatches = text.matchAll(/^\s*\d+[.)]\s*(.+)/gm);
|
|
65
|
+
for (const match of numberedMatches) {
|
|
66
|
+
items.push(match[1].trim());
|
|
67
|
+
}
|
|
68
|
+
// Bullet points
|
|
69
|
+
const bulletMatches = text.matchAll(/^\s*[-*•]\s*(.+)/gm);
|
|
70
|
+
for (const match of bulletMatches) {
|
|
71
|
+
items.push(match[1].trim());
|
|
72
|
+
}
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract "please fix/add" style requests.
|
|
77
|
+
*/
|
|
78
|
+
export function extractActionRequests(text) {
|
|
79
|
+
const requests = [];
|
|
80
|
+
// "Please" patterns
|
|
81
|
+
const pleaseMatches = text.matchAll(/please\s+(fix|add|update|remove|change|include|ensure|verify|check|run)\s+(.+?)(?:\.|,|$)/gi);
|
|
82
|
+
for (const match of pleaseMatches) {
|
|
83
|
+
requests.push(`${match[1]} ${match[2]}`.trim());
|
|
84
|
+
}
|
|
85
|
+
// "Need to" patterns
|
|
86
|
+
const needMatches = text.matchAll(/(?:need|needs|should|must|have)\s+to\s+(fix|add|update|remove|change|include|run)\s+(.+?)(?:\.|,|$)/gi);
|
|
87
|
+
for (const match of needMatches) {
|
|
88
|
+
requests.push(`${match[1]} ${match[2]}`.trim());
|
|
89
|
+
}
|
|
90
|
+
return requests;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Map a request to a suggested verification command.
|
|
94
|
+
*/
|
|
95
|
+
export function mapToCommand(request) {
|
|
96
|
+
const lower = request.toLowerCase();
|
|
97
|
+
for (const mapping of COMMAND_MAPPINGS) {
|
|
98
|
+
if (mapping.keywords.some(kw => lower.includes(kw))) {
|
|
99
|
+
return { command: mapping.command, category: mapping.category };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse review feedback and extract actionable requests.
|
|
106
|
+
*/
|
|
107
|
+
export function parseReviewFeedback(reviewText) {
|
|
108
|
+
const requests = [];
|
|
109
|
+
const commandsSet = new Set();
|
|
110
|
+
// Check if approved (no further action needed)
|
|
111
|
+
const lowerText = reviewText.toLowerCase();
|
|
112
|
+
const isApproved = lowerText.includes('approved') ||
|
|
113
|
+
lowerText.includes('lgtm') ||
|
|
114
|
+
(lowerText.includes('ready') && lowerText.includes('merge'));
|
|
115
|
+
if (isApproved) {
|
|
116
|
+
return { requests: [], commandsToSatisfy: [], isApproved: true };
|
|
117
|
+
}
|
|
118
|
+
// Extract list items
|
|
119
|
+
const listItems = extractListItems(reviewText);
|
|
120
|
+
for (const item of listItems) {
|
|
121
|
+
const { command, category } = mapToCommand(item);
|
|
122
|
+
requests.push({ text: item, suggestedCommand: command, category });
|
|
123
|
+
if (command)
|
|
124
|
+
commandsSet.add(command);
|
|
125
|
+
}
|
|
126
|
+
// Extract action requests if no list items found
|
|
127
|
+
if (requests.length === 0) {
|
|
128
|
+
const actionRequests = extractActionRequests(reviewText);
|
|
129
|
+
for (const req of actionRequests) {
|
|
130
|
+
const { command, category } = mapToCommand(req);
|
|
131
|
+
requests.push({ text: req, suggestedCommand: command, category });
|
|
132
|
+
if (command)
|
|
133
|
+
commandsSet.add(command);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Look for specific patterns not in lists
|
|
137
|
+
const errorPatterns = [
|
|
138
|
+
{ pattern: /(\d+)\s+(?:type\s+)?error/i, category: 'type_errors' },
|
|
139
|
+
{ pattern: /(\d+)\s+(?:test|spec)s?\s+fail/i, category: 'tests' },
|
|
140
|
+
{ pattern: /coverage\s+(?:is\s+)?(?:only\s+)?(\d+)%/i, category: 'coverage' },
|
|
141
|
+
{ pattern: /(\d+)%\s+coverage/i, category: 'coverage' },
|
|
142
|
+
{ pattern: /lint\s+(?:error|fail)/i, category: 'lint' },
|
|
143
|
+
{ pattern: /build\s+(?:error|fail)/i, category: 'build' }
|
|
144
|
+
];
|
|
145
|
+
for (const { pattern, category } of errorPatterns) {
|
|
146
|
+
const match = reviewText.match(pattern);
|
|
147
|
+
if (match) {
|
|
148
|
+
const mapping = COMMAND_MAPPINGS.find(m => m.category === category);
|
|
149
|
+
if (mapping && !commandsSet.has(mapping.command)) {
|
|
150
|
+
// Only add if not already in requests
|
|
151
|
+
const hasCategory = requests.some(r => r.category === category);
|
|
152
|
+
if (!hasCategory) {
|
|
153
|
+
requests.push({
|
|
154
|
+
text: match[0],
|
|
155
|
+
suggestedCommand: mapping.command,
|
|
156
|
+
category
|
|
157
|
+
});
|
|
158
|
+
commandsSet.add(mapping.command);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
requests,
|
|
165
|
+
commandsToSatisfy: Array.from(commandsSet),
|
|
166
|
+
isApproved: false
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Format parsed review for display.
|
|
171
|
+
*/
|
|
172
|
+
export function formatReviewRequests(parsed, runId) {
|
|
173
|
+
const lines = [];
|
|
174
|
+
if (parsed.isApproved) {
|
|
175
|
+
lines.push('Review: Approved');
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
if (parsed.requests.length > 0) {
|
|
179
|
+
lines.push('Reviewer requested:');
|
|
180
|
+
parsed.requests.slice(0, 3).forEach((req, i) => {
|
|
181
|
+
lines.push(` ${i + 1}. ${req.text}`);
|
|
182
|
+
});
|
|
183
|
+
if (parsed.requests.length > 3) {
|
|
184
|
+
lines.push(` ... and ${parsed.requests.length - 3} more`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (parsed.commandsToSatisfy.length > 0) {
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push('Commands to satisfy:');
|
|
190
|
+
for (const cmd of parsed.commandsToSatisfy) {
|
|
191
|
+
lines.push(` ${cmd}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Build suggested intervention command
|
|
195
|
+
if (parsed.commandsToSatisfy.length > 0) {
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('Suggested intervention:');
|
|
198
|
+
const cmdArgs = parsed.commandsToSatisfy.map(c => `--cmd "${c}"`).join(' ');
|
|
199
|
+
lines.push(` runr intervene ${runId} --reason review_loop \\`);
|
|
200
|
+
lines.push(` --note "Fixed review requests" ${cmdArgs}`);
|
|
201
|
+
}
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Extract review feedback from a done_check list.
|
|
206
|
+
*/
|
|
207
|
+
export function extractUnmetDoneChecks(doneChecks) {
|
|
208
|
+
return doneChecks
|
|
209
|
+
.filter(check => !check.passed)
|
|
210
|
+
.map(check => check.message || check.name);
|
|
211
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Write checkpoint metadata sidecar file.
|
|
5
|
+
* Best-effort: does not throw if write fails.
|
|
6
|
+
*/
|
|
7
|
+
export async function writeCheckpointMetadata(options) {
|
|
8
|
+
const { repoPath, sha, runId, milestoneIndex, milestone, tier, verificationCommands } = options;
|
|
9
|
+
const checkpointsDir = path.join(repoPath, '.runr', 'checkpoints');
|
|
10
|
+
await fs.mkdir(checkpointsDir, { recursive: true });
|
|
11
|
+
const metadata = {
|
|
12
|
+
schema_version: 1,
|
|
13
|
+
sha,
|
|
14
|
+
run_id: runId,
|
|
15
|
+
milestone_index: milestoneIndex,
|
|
16
|
+
milestone_title: milestone.goal,
|
|
17
|
+
created_at: new Date().toISOString()
|
|
18
|
+
};
|
|
19
|
+
// Add optional fields only if available
|
|
20
|
+
if (tier !== undefined) {
|
|
21
|
+
metadata.tier = tier;
|
|
22
|
+
}
|
|
23
|
+
if (verificationCommands !== undefined && verificationCommands.length > 0) {
|
|
24
|
+
metadata.verification_commands = verificationCommands;
|
|
25
|
+
}
|
|
26
|
+
const metadataPath = path.join(checkpointsDir, `${sha}.json`);
|
|
27
|
+
const tempPath = `${metadataPath}.tmp`;
|
|
28
|
+
// Atomic write with Windows safety
|
|
29
|
+
await fs.writeFile(tempPath, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
30
|
+
// Windows-safe rename: unlink destination if exists
|
|
31
|
+
try {
|
|
32
|
+
await fs.unlink(metadataPath);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore if doesn't exist
|
|
36
|
+
}
|
|
37
|
+
await fs.rename(tempPath, metadataPath);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Find the latest checkpoint for a run by scanning sidecar files.
|
|
41
|
+
* Returns null if no valid sidecars found.
|
|
42
|
+
* Selection: highest milestone_index, then latest created_at, then latest mtime.
|
|
43
|
+
*/
|
|
44
|
+
export async function findLatestCheckpointBySidecar(repoPath, runId) {
|
|
45
|
+
const checkpointsDir = path.join(repoPath, '.runr', 'checkpoints');
|
|
46
|
+
try {
|
|
47
|
+
await fs.access(checkpointsDir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null; // No sidecars yet
|
|
51
|
+
}
|
|
52
|
+
const files = await fs.readdir(checkpointsDir);
|
|
53
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') && f !== 'index.json');
|
|
54
|
+
let latestCheckpoint = null;
|
|
55
|
+
for (const file of jsonFiles) {
|
|
56
|
+
try {
|
|
57
|
+
const filePath = path.join(checkpointsDir, file);
|
|
58
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
59
|
+
const metadata = JSON.parse(content);
|
|
60
|
+
// Sanity checks
|
|
61
|
+
if (metadata.schema_version !== 1) {
|
|
62
|
+
continue; // Ignore unknown schema versions
|
|
63
|
+
}
|
|
64
|
+
// Required fields type checks
|
|
65
|
+
if (typeof metadata.sha !== 'string' ||
|
|
66
|
+
typeof metadata.run_id !== 'string' ||
|
|
67
|
+
typeof metadata.milestone_title !== 'string' ||
|
|
68
|
+
typeof metadata.created_at !== 'string') {
|
|
69
|
+
continue; // Missing/invalid required fields
|
|
70
|
+
}
|
|
71
|
+
const expectedSha = file.replace('.json', '');
|
|
72
|
+
if (metadata.sha !== expectedSha) {
|
|
73
|
+
continue; // SHA mismatch = corruption
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isFinite(metadata.milestone_index) || metadata.milestone_index < 0) {
|
|
76
|
+
continue; // Invalid milestone index
|
|
77
|
+
}
|
|
78
|
+
if (metadata.run_id !== runId) {
|
|
79
|
+
continue; // Wrong run
|
|
80
|
+
}
|
|
81
|
+
// Get file mtime as fallback for tie-breaking
|
|
82
|
+
const stats = await fs.stat(filePath);
|
|
83
|
+
const mtime = stats.mtimeMs;
|
|
84
|
+
// Selection: higher milestone_index, then later created_at, then later mtime
|
|
85
|
+
const shouldReplace = latestCheckpoint === null ||
|
|
86
|
+
metadata.milestone_index > latestCheckpoint.milestoneIndex ||
|
|
87
|
+
(metadata.milestone_index === latestCheckpoint.milestoneIndex && (
|
|
88
|
+
// created_at comparison (handles missing/empty)
|
|
89
|
+
(metadata.created_at || '') > (latestCheckpoint.created_at || '') ||
|
|
90
|
+
// If created_at equal/both missing, use mtime fallback
|
|
91
|
+
((metadata.created_at || '') === (latestCheckpoint.created_at || '') &&
|
|
92
|
+
mtime > latestCheckpoint.mtime)));
|
|
93
|
+
if (shouldReplace) {
|
|
94
|
+
latestCheckpoint = {
|
|
95
|
+
sha: metadata.sha,
|
|
96
|
+
milestoneIndex: metadata.milestone_index,
|
|
97
|
+
created_at: metadata.created_at || '',
|
|
98
|
+
mtime
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue; // Malformed JSON or other read error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return latestCheckpoint ? {
|
|
107
|
+
sha: latestCheckpoint.sha,
|
|
108
|
+
milestoneIndex: latestCheckpoint.milestoneIndex,
|
|
109
|
+
created_at: latestCheckpoint.created_at
|
|
110
|
+
} : null;
|
|
111
|
+
}
|
package/dist/store/run-store.js
CHANGED
|
@@ -86,6 +86,27 @@ export class RunStore {
|
|
|
86
86
|
getLastEvent() {
|
|
87
87
|
return this.lastEvent;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Read all events from the timeline.
|
|
91
|
+
*/
|
|
92
|
+
readTimeline() {
|
|
93
|
+
if (!fs.existsSync(this.timelinePath)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const content = fs.readFileSync(this.timelinePath, 'utf-8');
|
|
97
|
+
const events = [];
|
|
98
|
+
for (const line of content.split('\n')) {
|
|
99
|
+
if (line.trim()) {
|
|
100
|
+
try {
|
|
101
|
+
events.push(JSON.parse(line));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Skip malformed lines
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return events;
|
|
109
|
+
}
|
|
89
110
|
recordWorkerCall(info) {
|
|
90
111
|
this.lastWorkerCall = info;
|
|
91
112
|
}
|
|
@@ -18,7 +18,9 @@ import { runVerification } from '../verification/engine.js';
|
|
|
18
18
|
import { stopRun, updatePhase, prepareForResume } from './state-machine.js';
|
|
19
19
|
import { buildJournal } from '../journal/builder.js';
|
|
20
20
|
import { renderJournal } from '../journal/renderer.js';
|
|
21
|
+
import { writeReceipt, extractBaseSha, deriveTerminalState, printRunReceipt } from '../receipt/writer.js';
|
|
21
22
|
import { getActiveRuns, checkFileCollisions, formatFileCollisionError } from './collision.js';
|
|
23
|
+
import { parseReviewFeedback } from '../review/check-parser.js';
|
|
22
24
|
import { validateNoChangesEvidence, formatEvidenceErrors } from './evidence-gate.js';
|
|
23
25
|
import { normalizeOwnsPatterns, toPosixPath } from '../ownership/normalize.js';
|
|
24
26
|
export function checkOwnership(changedFiles, ownedPaths, envAllowlist) {
|
|
@@ -607,6 +609,49 @@ async function runSupervisorOnce(options) {
|
|
|
607
609
|
// Never crash on journal generation failure
|
|
608
610
|
console.warn(`Warning: Failed to generate journal: ${err.message}`);
|
|
609
611
|
}
|
|
612
|
+
// Auto-write receipt artifacts at terminal state and print Run Receipt
|
|
613
|
+
try {
|
|
614
|
+
const finalState = options.runStore.readState();
|
|
615
|
+
if (finalState.phase === 'STOPPED') {
|
|
616
|
+
const baseSha = extractBaseSha(options.runStore.path);
|
|
617
|
+
const terminalState = deriveTerminalState(finalState.stop_reason);
|
|
618
|
+
const verificationTier = finalState.last_verification_evidence?.tiers_run?.[0] ?? null;
|
|
619
|
+
const result = await writeReceipt({
|
|
620
|
+
runStore: options.runStore,
|
|
621
|
+
repoPath: options.repoPath,
|
|
622
|
+
baseSha,
|
|
623
|
+
checkpointSha: finalState.checkpoint_commit_sha ?? null,
|
|
624
|
+
verificationTier,
|
|
625
|
+
terminalState,
|
|
626
|
+
stopReason: finalState.stop_reason,
|
|
627
|
+
runId: finalState.run_id
|
|
628
|
+
});
|
|
629
|
+
// Print Run Receipt to console
|
|
630
|
+
if (result) {
|
|
631
|
+
// Read diffstat for console output
|
|
632
|
+
const diffstatPath = path.join(options.runStore.path, 'diffstat.txt');
|
|
633
|
+
const diffstat = fs.existsSync(diffstatPath)
|
|
634
|
+
? fs.readFileSync(diffstatPath, 'utf-8')
|
|
635
|
+
: '';
|
|
636
|
+
// Get integration branch from config
|
|
637
|
+
const integrationBranch = options.config?.workflow?.integration_branch ?? 'main';
|
|
638
|
+
printRunReceipt({
|
|
639
|
+
runId: finalState.run_id,
|
|
640
|
+
terminalState,
|
|
641
|
+
stopReason: finalState.stop_reason,
|
|
642
|
+
receipt: result.receipt,
|
|
643
|
+
patchPath: result.patchPath,
|
|
644
|
+
compressed: result.compressed,
|
|
645
|
+
diffstat,
|
|
646
|
+
integrationBranch
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
// Never crash on receipt generation failure
|
|
653
|
+
console.warn(`Warning: Failed to generate receipt: ${err.message}`);
|
|
654
|
+
}
|
|
610
655
|
}
|
|
611
656
|
}
|
|
612
657
|
/**
|
|
@@ -902,6 +947,16 @@ async function handleImplement(state, options) {
|
|
|
902
947
|
return stopWithError(state, options, 'implement_blocked', implementer.handoff_memo);
|
|
903
948
|
}
|
|
904
949
|
const changedFiles = await listChangedFiles(options.repoPath);
|
|
950
|
+
// Record ignored files for forensics (journal)
|
|
951
|
+
const { getIgnoredChangesSummary } = await import('../repo/context.js');
|
|
952
|
+
const ignoredSummary = await getIgnoredChangesSummary(options.repoPath);
|
|
953
|
+
if (ignoredSummary.ignored_count > 0 || ignoredSummary.ignore_check_status === 'failed') {
|
|
954
|
+
options.runStore.appendEvent({
|
|
955
|
+
type: 'ignored_changes',
|
|
956
|
+
source: 'supervisor',
|
|
957
|
+
payload: ignoredSummary
|
|
958
|
+
});
|
|
959
|
+
}
|
|
905
960
|
const scopeCheck = checkScope(changedFiles, state.scope_lock.allowlist, state.scope_lock.denylist);
|
|
906
961
|
const lockfileCheck = checkLockfiles(changedFiles, options.config.scope.lockfiles, options.allowDeps);
|
|
907
962
|
if (!scopeCheck.ok || !lockfileCheck.ok) {
|
|
@@ -1221,6 +1276,10 @@ async function handleReview(state, options) {
|
|
|
1221
1276
|
const exceededRounds = currentRounds > maxRounds;
|
|
1222
1277
|
if (sameFingerprint || exceededRounds) {
|
|
1223
1278
|
const reason = sameFingerprint ? 'identical_review_feedback' : 'max_review_rounds_exceeded';
|
|
1279
|
+
// Parse review changes to extract actionable commands
|
|
1280
|
+
const parsedReview = parseReviewFeedback(changesText);
|
|
1281
|
+
const reviewerRequests = review.changes.slice(0, 5);
|
|
1282
|
+
const commandsToSatisfy = parsedReview.commandsToSatisfy;
|
|
1224
1283
|
options.runStore.appendEvent({
|
|
1225
1284
|
type: 'review_loop_detected',
|
|
1226
1285
|
source: 'supervisor',
|
|
@@ -1229,24 +1288,55 @@ async function handleReview(state, options) {
|
|
|
1229
1288
|
review_rounds: currentRounds,
|
|
1230
1289
|
max_review_rounds: maxRounds,
|
|
1231
1290
|
same_fingerprint: sameFingerprint,
|
|
1232
|
-
last_changes: review.changes.slice(0, 2) // First 2 items for context
|
|
1291
|
+
last_changes: review.changes.slice(0, 2), // First 2 items for context
|
|
1292
|
+
// Enhanced fields for diagnostics
|
|
1293
|
+
reviewer_requests: reviewerRequests,
|
|
1294
|
+
commands_to_satisfy: commandsToSatisfy
|
|
1233
1295
|
}
|
|
1234
1296
|
});
|
|
1235
|
-
// Write review digest for debugging
|
|
1297
|
+
// Write enhanced review digest for debugging
|
|
1236
1298
|
const digestLines = [
|
|
1237
1299
|
'# Review Digest',
|
|
1238
1300
|
'',
|
|
1239
1301
|
`**Milestone:** ${state.milestone_index + 1} of ${state.milestones.length}`,
|
|
1240
|
-
`**Review Rounds:** ${currentRounds}`,
|
|
1302
|
+
`**Review Rounds:** ${currentRounds} (max: ${maxRounds})`,
|
|
1241
1303
|
`**Stop Reason:** ${reason}`,
|
|
1242
1304
|
'',
|
|
1243
|
-
'##
|
|
1305
|
+
'## Reviewer Requested Changes',
|
|
1244
1306
|
'',
|
|
1245
1307
|
...review.changes.map((change, i) => `${i + 1}. ${change}`),
|
|
1246
|
-
''
|
|
1247
|
-
'## Status',
|
|
1248
|
-
`- **Verdict:** ${review.status}`
|
|
1308
|
+
''
|
|
1249
1309
|
];
|
|
1310
|
+
// Add commands to satisfy section if we found any
|
|
1311
|
+
if (commandsToSatisfy.length > 0) {
|
|
1312
|
+
digestLines.push('## Commands to Satisfy');
|
|
1313
|
+
digestLines.push('');
|
|
1314
|
+
digestLines.push('Run these commands to address the requested changes:');
|
|
1315
|
+
digestLines.push('');
|
|
1316
|
+
digestLines.push('```bash');
|
|
1317
|
+
commandsToSatisfy.forEach(cmd => digestLines.push(cmd));
|
|
1318
|
+
digestLines.push('```');
|
|
1319
|
+
digestLines.push('');
|
|
1320
|
+
}
|
|
1321
|
+
// Add suggested intervention
|
|
1322
|
+
digestLines.push('## Suggested Intervention');
|
|
1323
|
+
digestLines.push('');
|
|
1324
|
+
if (commandsToSatisfy.length > 0) {
|
|
1325
|
+
const cmdArgs = commandsToSatisfy.map(c => `--cmd "${c}"`).join(' ');
|
|
1326
|
+
digestLines.push('```bash');
|
|
1327
|
+
digestLines.push(`runr intervene ${state.run_id} --reason review_loop \\`);
|
|
1328
|
+
digestLines.push(` --note "Fixed review requests" ${cmdArgs}`);
|
|
1329
|
+
digestLines.push('```');
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
digestLines.push('```bash');
|
|
1333
|
+
digestLines.push(`runr intervene ${state.run_id} --reason review_loop \\`);
|
|
1334
|
+
digestLines.push(` --note "Fixed review requests" --cmd "npm run build"`);
|
|
1335
|
+
digestLines.push('```');
|
|
1336
|
+
}
|
|
1337
|
+
digestLines.push('');
|
|
1338
|
+
digestLines.push('## Status');
|
|
1339
|
+
digestLines.push(`- **Verdict:** ${review.status}`);
|
|
1250
1340
|
options.runStore.writeMemo('review_digest.md', digestLines.join('\n'));
|
|
1251
1341
|
const errorMsg = sameFingerprint
|
|
1252
1342
|
? `Identical review feedback detected after ${currentRounds} rounds. Manual intervention required.`
|
|
@@ -1278,14 +1368,43 @@ async function handleCheckpoint(state, options) {
|
|
|
1278
1368
|
const status = await git(['status', '--porcelain'], options.repoPath);
|
|
1279
1369
|
if (status.stdout.trim().length > 0) {
|
|
1280
1370
|
await git(['add', '-A'], options.repoPath);
|
|
1281
|
-
const message = `chore(
|
|
1371
|
+
const message = `chore(runr): checkpoint ${state.run_id} milestone ${state.milestone_index}`;
|
|
1282
1372
|
await git(['commit', '-m', message], options.repoPath);
|
|
1283
1373
|
}
|
|
1284
1374
|
const shaResult = await git(['rev-parse', 'HEAD'], options.repoPath);
|
|
1375
|
+
const sha = shaResult.stdout.trim();
|
|
1376
|
+
// Write checkpoint metadata sidecar (best-effort)
|
|
1377
|
+
let sidecarWritten = false;
|
|
1378
|
+
try {
|
|
1379
|
+
const { writeCheckpointMetadata } = await import('../store/checkpoint-metadata.js');
|
|
1380
|
+
await writeCheckpointMetadata({
|
|
1381
|
+
repoPath: options.repoPath,
|
|
1382
|
+
sha,
|
|
1383
|
+
runId: state.run_id,
|
|
1384
|
+
milestoneIndex: state.milestone_index,
|
|
1385
|
+
milestone: state.milestones[state.milestone_index],
|
|
1386
|
+
// Optional fields from last_verification_evidence (safe access)
|
|
1387
|
+
tier: state.last_verification_evidence?.tiers_run?.[0],
|
|
1388
|
+
verificationCommands: state.last_verification_evidence?.commands_run?.map(c => c.command) ?? undefined
|
|
1389
|
+
});
|
|
1390
|
+
sidecarWritten = true;
|
|
1391
|
+
}
|
|
1392
|
+
catch (error) {
|
|
1393
|
+
// Best-effort: don't fail run if sidecar write fails
|
|
1394
|
+
options.runStore.appendEvent({
|
|
1395
|
+
type: 'checkpoint_sidecar_write_failed',
|
|
1396
|
+
source: 'supervisor',
|
|
1397
|
+
payload: {
|
|
1398
|
+
sha,
|
|
1399
|
+
path: path.join(options.repoPath, '.runr', 'checkpoints', `${sha}.json`),
|
|
1400
|
+
error: String(error)
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1285
1404
|
const nextIndex = state.milestone_index + 1;
|
|
1286
1405
|
const updated = {
|
|
1287
1406
|
...state,
|
|
1288
|
-
checkpoint_commit_sha:
|
|
1407
|
+
checkpoint_commit_sha: sha,
|
|
1289
1408
|
milestone_index: nextIndex,
|
|
1290
1409
|
milestone_retries: 0,
|
|
1291
1410
|
last_verify_failure: undefined,
|
|
@@ -1297,7 +1416,8 @@ async function handleCheckpoint(state, options) {
|
|
|
1297
1416
|
source: 'supervisor',
|
|
1298
1417
|
payload: {
|
|
1299
1418
|
commit: updated.checkpoint_commit_sha,
|
|
1300
|
-
milestone_index: state.milestone_index
|
|
1419
|
+
milestone_index: state.milestone_index,
|
|
1420
|
+
sidecar_written: sidecarWritten
|
|
1301
1421
|
}
|
|
1302
1422
|
});
|
|
1303
1423
|
if (nextIndex >= updated.milestones.length) {
|
|
@@ -40,6 +40,75 @@ function coerceOwns(value, taskPath) {
|
|
|
40
40
|
}
|
|
41
41
|
throw new Error(`Invalid owns entry in ${taskPath}: must be string or string[]`);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse allowlist_add from frontmatter or body.
|
|
45
|
+
* Accepts both frontmatter field and markdown section.
|
|
46
|
+
*/
|
|
47
|
+
function parseAllowlistAdd(frontmatter, body) {
|
|
48
|
+
// Check frontmatter first
|
|
49
|
+
if (frontmatter?.allowlist_add) {
|
|
50
|
+
const value = frontmatter.allowlist_add;
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
return value.filter((item) => typeof item === 'string');
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'string') {
|
|
55
|
+
return [value];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Check for Scope section in markdown body (YAML-like format)
|
|
59
|
+
// Format:
|
|
60
|
+
// ## Scope
|
|
61
|
+
// allowlist_add:
|
|
62
|
+
// - pattern1
|
|
63
|
+
// - pattern2
|
|
64
|
+
const scopeMatch = body.match(/##\s*Scope\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
65
|
+
if (scopeMatch) {
|
|
66
|
+
const scopeContent = scopeMatch[1];
|
|
67
|
+
const allowlistMatch = scopeContent.match(/allowlist_add:\s*\n((?:\s*-\s*.+\n?)+)/);
|
|
68
|
+
if (allowlistMatch) {
|
|
69
|
+
const items = allowlistMatch[1].match(/-\s*(.+)/g);
|
|
70
|
+
if (items) {
|
|
71
|
+
return items.map(item => item.replace(/^-\s*/, '').trim());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse verification tier from frontmatter or body.
|
|
79
|
+
* Enforces minimum tier0.
|
|
80
|
+
*/
|
|
81
|
+
function parseVerificationTier(frontmatter, body) {
|
|
82
|
+
let tier = null;
|
|
83
|
+
// Check frontmatter first
|
|
84
|
+
if (frontmatter?.verification && typeof frontmatter.verification === 'object') {
|
|
85
|
+
const verification = frontmatter.verification;
|
|
86
|
+
if (verification.tier) {
|
|
87
|
+
tier = String(verification.tier);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (frontmatter?.tier) {
|
|
91
|
+
tier = String(frontmatter.tier);
|
|
92
|
+
}
|
|
93
|
+
// Check for Verification section in markdown body
|
|
94
|
+
if (!tier) {
|
|
95
|
+
const verifyMatch = body.match(/##\s*Verification\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
96
|
+
if (verifyMatch) {
|
|
97
|
+
const tierMatch = verifyMatch[1].match(/tier:\s*(tier[012])/i);
|
|
98
|
+
if (tierMatch) {
|
|
99
|
+
tier = tierMatch[1].toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Normalize and validate
|
|
104
|
+
if (tier) {
|
|
105
|
+
const normalized = tier.toLowerCase();
|
|
106
|
+
if (normalized === 'tier0' || normalized === 'tier1' || normalized === 'tier2') {
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null; // Use config default
|
|
111
|
+
}
|
|
43
112
|
export function loadTaskMetadata(taskPath) {
|
|
44
113
|
const raw = fs.readFileSync(taskPath, 'utf-8');
|
|
45
114
|
const { frontmatterText, body } = splitFrontmatter(raw);
|
|
@@ -62,11 +131,15 @@ export function loadTaskMetadata(taskPath) {
|
|
|
62
131
|
ownsRaw = coerceOwns(frontmatter.owns, taskPath);
|
|
63
132
|
}
|
|
64
133
|
const ownsNormalized = normalizeOwnsPatterns(ownsRaw);
|
|
134
|
+
const allowlistAdd = parseAllowlistAdd(frontmatter, body);
|
|
135
|
+
const verificationTier = parseVerificationTier(frontmatter, body);
|
|
65
136
|
return {
|
|
66
137
|
raw,
|
|
67
138
|
body,
|
|
68
139
|
owns_raw: ownsRaw,
|
|
69
140
|
owns_normalized: ownsNormalized,
|
|
70
|
-
frontmatter
|
|
141
|
+
frontmatter,
|
|
142
|
+
allowlist_add: allowlistAdd,
|
|
143
|
+
verification_tier: verificationTier
|
|
71
144
|
};
|
|
72
145
|
}
|