@weldr/runr 0.3.1 → 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 +150 -1
- package/README.md +124 -111
- package/dist/audit/classifier.js +331 -0
- package/dist/cli.js +593 -282
- 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/journal.js +167 -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 +497 -0
- package/dist/journal/redactor.js +68 -0
- package/dist/journal/renderer.js +220 -0
- package/dist/journal/types.js +7 -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 +161 -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 +5 -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/commands/__tests__/report.test.js +0 -202
- package/dist/config/__tests__/presets.test.js +0 -104
- package/dist/context/__tests__/artifact.test.js +0 -130
- package/dist/context/__tests__/pack.test.js +0 -191
- package/dist/env/__tests__/fingerprint.test.js +0 -116
- package/dist/orchestrator/__tests__/policy.test.js +0 -185
- package/dist/orchestrator/__tests__/schema-version.test.js +0 -65
- package/dist/supervisor/__tests__/evidence-gate.test.js +0 -111
- package/dist/supervisor/__tests__/ownership.test.js +0 -103
- package/dist/supervisor/__tests__/state-machine.test.js +0 -290
- package/dist/workers/__tests__/claude.test.js +0 -88
- package/dist/workers/__tests__/codex.test.js +0 -81
|
@@ -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
|
}
|
|
@@ -16,7 +16,11 @@ import { checkLockfiles, checkScope, partitionChangedFiles } from './scope-guard
|
|
|
16
16
|
import { commandsForTier, selectTiersWithReasons } from './verification-policy.js';
|
|
17
17
|
import { runVerification } from '../verification/engine.js';
|
|
18
18
|
import { stopRun, updatePhase, prepareForResume } from './state-machine.js';
|
|
19
|
+
import { buildJournal } from '../journal/builder.js';
|
|
20
|
+
import { renderJournal } from '../journal/renderer.js';
|
|
21
|
+
import { writeReceipt, extractBaseSha, deriveTerminalState, printRunReceipt } from '../receipt/writer.js';
|
|
19
22
|
import { getActiveRuns, checkFileCollisions, formatFileCollisionError } from './collision.js';
|
|
23
|
+
import { parseReviewFeedback } from '../review/check-parser.js';
|
|
20
24
|
import { validateNoChangesEvidence, formatEvidenceErrors } from './evidence-gate.js';
|
|
21
25
|
import { normalizeOwnsPatterns, toPosixPath } from '../ownership/normalize.js';
|
|
22
26
|
export function checkOwnership(changedFiles, ownedPaths, envAllowlist) {
|
|
@@ -266,6 +270,24 @@ function buildStructuredStopMemo(params) {
|
|
|
266
270
|
lines.push('', '## Next Action', '```bash', nextAction, '```', '', '## Tips', tipsByReason[reason] ?? '- Review the timeline.jsonl for detailed event history.');
|
|
267
271
|
return lines.join('\n');
|
|
268
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Auto-write journal.md when run completes
|
|
275
|
+
*/
|
|
276
|
+
async function writeJournalOnRunComplete(runId, repoPath) {
|
|
277
|
+
try {
|
|
278
|
+
const journal = await buildJournal(runId, repoPath);
|
|
279
|
+
const markdown = renderJournal(journal);
|
|
280
|
+
// Get runs root and construct journal path
|
|
281
|
+
const { getRunsRoot } = await import('../store/runs-root.js');
|
|
282
|
+
const runDir = path.join(getRunsRoot(repoPath), runId);
|
|
283
|
+
const journalPath = path.join(runDir, 'journal.md');
|
|
284
|
+
fs.writeFileSync(journalPath, markdown, 'utf-8');
|
|
285
|
+
console.log(`\n✓ Case file generated: runs/${runId}/journal.md`);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
throw new Error(`Failed to generate journal: ${err.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
269
291
|
/**
|
|
270
292
|
* Main supervisor entry point with auto-resume support.
|
|
271
293
|
*
|
|
@@ -576,6 +598,60 @@ async function runSupervisorOnce(options) {
|
|
|
576
598
|
}
|
|
577
599
|
finally {
|
|
578
600
|
clearInterval(watchdog);
|
|
601
|
+
// Auto-write journal.md when run reaches terminal state
|
|
602
|
+
try {
|
|
603
|
+
const finalState = options.runStore.readState();
|
|
604
|
+
if (finalState.phase === 'STOPPED') {
|
|
605
|
+
await writeJournalOnRunComplete(finalState.run_id, options.repoPath);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
// Never crash on journal generation failure
|
|
610
|
+
console.warn(`Warning: Failed to generate journal: ${err.message}`);
|
|
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
|
+
}
|
|
579
655
|
}
|
|
580
656
|
}
|
|
581
657
|
/**
|
|
@@ -871,6 +947,16 @@ async function handleImplement(state, options) {
|
|
|
871
947
|
return stopWithError(state, options, 'implement_blocked', implementer.handoff_memo);
|
|
872
948
|
}
|
|
873
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
|
+
}
|
|
874
960
|
const scopeCheck = checkScope(changedFiles, state.scope_lock.allowlist, state.scope_lock.denylist);
|
|
875
961
|
const lockfileCheck = checkLockfiles(changedFiles, options.config.scope.lockfiles, options.allowDeps);
|
|
876
962
|
if (!scopeCheck.ok || !lockfileCheck.ok) {
|
|
@@ -1190,6 +1276,10 @@ async function handleReview(state, options) {
|
|
|
1190
1276
|
const exceededRounds = currentRounds > maxRounds;
|
|
1191
1277
|
if (sameFingerprint || exceededRounds) {
|
|
1192
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;
|
|
1193
1283
|
options.runStore.appendEvent({
|
|
1194
1284
|
type: 'review_loop_detected',
|
|
1195
1285
|
source: 'supervisor',
|
|
@@ -1198,24 +1288,55 @@ async function handleReview(state, options) {
|
|
|
1198
1288
|
review_rounds: currentRounds,
|
|
1199
1289
|
max_review_rounds: maxRounds,
|
|
1200
1290
|
same_fingerprint: sameFingerprint,
|
|
1201
|
-
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
|
|
1202
1295
|
}
|
|
1203
1296
|
});
|
|
1204
|
-
// Write review digest for debugging
|
|
1297
|
+
// Write enhanced review digest for debugging
|
|
1205
1298
|
const digestLines = [
|
|
1206
1299
|
'# Review Digest',
|
|
1207
1300
|
'',
|
|
1208
1301
|
`**Milestone:** ${state.milestone_index + 1} of ${state.milestones.length}`,
|
|
1209
|
-
`**Review Rounds:** ${currentRounds}`,
|
|
1302
|
+
`**Review Rounds:** ${currentRounds} (max: ${maxRounds})`,
|
|
1210
1303
|
`**Stop Reason:** ${reason}`,
|
|
1211
1304
|
'',
|
|
1212
|
-
'##
|
|
1305
|
+
'## Reviewer Requested Changes',
|
|
1213
1306
|
'',
|
|
1214
1307
|
...review.changes.map((change, i) => `${i + 1}. ${change}`),
|
|
1215
|
-
''
|
|
1216
|
-
'## Status',
|
|
1217
|
-
`- **Verdict:** ${review.status}`
|
|
1308
|
+
''
|
|
1218
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}`);
|
|
1219
1340
|
options.runStore.writeMemo('review_digest.md', digestLines.join('\n'));
|
|
1220
1341
|
const errorMsg = sameFingerprint
|
|
1221
1342
|
? `Identical review feedback detected after ${currentRounds} rounds. Manual intervention required.`
|
|
@@ -1247,14 +1368,43 @@ async function handleCheckpoint(state, options) {
|
|
|
1247
1368
|
const status = await git(['status', '--porcelain'], options.repoPath);
|
|
1248
1369
|
if (status.stdout.trim().length > 0) {
|
|
1249
1370
|
await git(['add', '-A'], options.repoPath);
|
|
1250
|
-
const message = `chore(
|
|
1371
|
+
const message = `chore(runr): checkpoint ${state.run_id} milestone ${state.milestone_index}`;
|
|
1251
1372
|
await git(['commit', '-m', message], options.repoPath);
|
|
1252
1373
|
}
|
|
1253
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
|
+
}
|
|
1254
1404
|
const nextIndex = state.milestone_index + 1;
|
|
1255
1405
|
const updated = {
|
|
1256
1406
|
...state,
|
|
1257
|
-
checkpoint_commit_sha:
|
|
1407
|
+
checkpoint_commit_sha: sha,
|
|
1258
1408
|
milestone_index: nextIndex,
|
|
1259
1409
|
milestone_retries: 0,
|
|
1260
1410
|
last_verify_failure: undefined,
|
|
@@ -1266,7 +1416,8 @@ async function handleCheckpoint(state, options) {
|
|
|
1266
1416
|
source: 'supervisor',
|
|
1267
1417
|
payload: {
|
|
1268
1418
|
commit: updated.checkpoint_commit_sha,
|
|
1269
|
-
milestone_index: state.milestone_index
|
|
1419
|
+
milestone_index: state.milestone_index,
|
|
1420
|
+
sidecar_written: sidecarWritten
|
|
1270
1421
|
}
|
|
1271
1422
|
});
|
|
1272
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
|
}
|