@weldr/runr 0.3.0
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 +216 -0
- package/LICENSE +190 -0
- package/NOTICE +4 -0
- package/README.md +200 -0
- package/dist/cli.js +464 -0
- package/dist/commands/__tests__/report.test.js +202 -0
- package/dist/commands/compare.js +168 -0
- package/dist/commands/doctor.js +124 -0
- package/dist/commands/follow.js +251 -0
- package/dist/commands/gc.js +161 -0
- package/dist/commands/guards-only.js +89 -0
- package/dist/commands/metrics.js +441 -0
- package/dist/commands/orchestrate.js +800 -0
- package/dist/commands/paths.js +31 -0
- package/dist/commands/preflight.js +152 -0
- package/dist/commands/report.js +478 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run.js +538 -0
- package/dist/commands/status.js +189 -0
- package/dist/commands/summarize.js +220 -0
- package/dist/commands/version.js +82 -0
- package/dist/commands/wait.js +170 -0
- package/dist/config/__tests__/presets.test.js +104 -0
- package/dist/config/load.js +66 -0
- package/dist/config/schema.js +160 -0
- package/dist/context/__tests__/artifact.test.js +130 -0
- package/dist/context/__tests__/pack.test.js +191 -0
- package/dist/context/artifact.js +67 -0
- package/dist/context/index.js +2 -0
- package/dist/context/pack.js +273 -0
- package/dist/diagnosis/analyzer.js +678 -0
- package/dist/diagnosis/formatter.js +136 -0
- package/dist/diagnosis/index.js +6 -0
- package/dist/diagnosis/types.js +7 -0
- package/dist/env/__tests__/fingerprint.test.js +116 -0
- package/dist/env/fingerprint.js +111 -0
- package/dist/orchestrator/__tests__/policy.test.js +185 -0
- package/dist/orchestrator/__tests__/schema-version.test.js +65 -0
- package/dist/orchestrator/artifacts.js +405 -0
- package/dist/orchestrator/state-machine.js +646 -0
- package/dist/orchestrator/types.js +88 -0
- package/dist/ownership/normalize.js +45 -0
- package/dist/repo/context.js +90 -0
- package/dist/repo/git.js +13 -0
- package/dist/repo/worktree.js +239 -0
- package/dist/store/run-store.js +107 -0
- package/dist/store/run-utils.js +69 -0
- package/dist/store/runs-root.js +126 -0
- package/dist/supervisor/__tests__/evidence-gate.test.js +111 -0
- package/dist/supervisor/__tests__/ownership.test.js +103 -0
- package/dist/supervisor/__tests__/state-machine.test.js +290 -0
- package/dist/supervisor/collision.js +240 -0
- package/dist/supervisor/evidence-gate.js +98 -0
- package/dist/supervisor/planner.js +18 -0
- package/dist/supervisor/runner.js +1562 -0
- package/dist/supervisor/scope-guard.js +55 -0
- package/dist/supervisor/state-machine.js +121 -0
- package/dist/supervisor/verification-policy.js +64 -0
- package/dist/tasks/task-metadata.js +72 -0
- package/dist/types/schemas.js +1 -0
- package/dist/verification/engine.js +49 -0
- package/dist/workers/__tests__/claude.test.js +88 -0
- package/dist/workers/__tests__/codex.test.js +81 -0
- package/dist/workers/claude.js +119 -0
- package/dist/workers/codex.js +162 -0
- package/dist/workers/json.js +22 -0
- package/dist/workers/mock.js +193 -0
- package/dist/workers/prompts.js +98 -0
- package/dist/workers/schemas.js +39 -0
- package/package.json +47 -0
- package/templates/prompts/implementer.md +70 -0
- package/templates/prompts/planner.md +62 -0
- package/templates/prompts/reviewer.md +77 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File collision detection for parallel runs.
|
|
3
|
+
*
|
|
4
|
+
* Two-stage collision prevention:
|
|
5
|
+
* 1. Pre-PLAN: Coarse check on allowlist patterns (warn-only)
|
|
6
|
+
* 2. Post-PLAN: Precise check on files_expected (STOP by default)
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import picomatch from 'picomatch';
|
|
11
|
+
import { getRunsRoot } from '../store/runs-root.js';
|
|
12
|
+
/**
|
|
13
|
+
* Get all active (non-stopped) runs from the runs directory.
|
|
14
|
+
*/
|
|
15
|
+
export function getActiveRuns(repoPath, excludeRunId) {
|
|
16
|
+
const runsRoot = getRunsRoot(repoPath);
|
|
17
|
+
if (!fs.existsSync(runsRoot)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const runDirs = fs.readdirSync(runsRoot, { withFileTypes: true })
|
|
21
|
+
.filter(d => d.isDirectory())
|
|
22
|
+
.map(d => d.name);
|
|
23
|
+
const activeRuns = [];
|
|
24
|
+
for (const runId of runDirs) {
|
|
25
|
+
if (runId === excludeRunId)
|
|
26
|
+
continue;
|
|
27
|
+
const statePath = path.join(runsRoot, runId, 'state.json');
|
|
28
|
+
if (!fs.existsSync(statePath))
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
const stateRaw = fs.readFileSync(statePath, 'utf-8');
|
|
32
|
+
const state = JSON.parse(stateRaw);
|
|
33
|
+
// Only include running runs (not STOPPED)
|
|
34
|
+
if (state.phase === 'STOPPED')
|
|
35
|
+
continue;
|
|
36
|
+
// Extract predicted touch files from milestones
|
|
37
|
+
const predictedTouchFiles = extractPredictedTouchFiles(state);
|
|
38
|
+
activeRuns.push({
|
|
39
|
+
runId,
|
|
40
|
+
phase: state.phase,
|
|
41
|
+
allowlist: state.scope_lock?.allowlist ?? [],
|
|
42
|
+
predictedTouchFiles,
|
|
43
|
+
updatedAt: state.updated_at ?? ''
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Skip runs with invalid state
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return activeRuns;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract union of all files_expected from milestones.
|
|
54
|
+
*/
|
|
55
|
+
function extractPredictedTouchFiles(state) {
|
|
56
|
+
const files = new Set();
|
|
57
|
+
for (const milestone of state.milestones) {
|
|
58
|
+
for (const file of milestone.files_expected ?? []) {
|
|
59
|
+
files.add(file);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(files);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if two allowlist patterns could overlap.
|
|
66
|
+
* Uses picomatch to test pattern intersection.
|
|
67
|
+
*/
|
|
68
|
+
export function patternsOverlap(pattern1, pattern2) {
|
|
69
|
+
// If patterns are identical, they definitely overlap
|
|
70
|
+
if (pattern1 === pattern2)
|
|
71
|
+
return true;
|
|
72
|
+
// Extract base paths (before glob characters)
|
|
73
|
+
const base1 = getPatternBase(pattern1);
|
|
74
|
+
const base2 = getPatternBase(pattern2);
|
|
75
|
+
// If one base is a prefix of the other, they could overlap
|
|
76
|
+
if (base1.startsWith(base2) || base2.startsWith(base1)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Check if pattern1 could match pattern2's base or vice versa
|
|
80
|
+
const matcher1 = picomatch(pattern1);
|
|
81
|
+
const matcher2 = picomatch(pattern2);
|
|
82
|
+
// Test representative paths
|
|
83
|
+
if (matcher1(base2) || matcher2(base1)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the non-glob prefix of a pattern.
|
|
90
|
+
*/
|
|
91
|
+
function getPatternBase(pattern) {
|
|
92
|
+
const globIndex = pattern.search(/[*?[\]]/);
|
|
93
|
+
if (globIndex === -1)
|
|
94
|
+
return pattern;
|
|
95
|
+
const base = pattern.slice(0, globIndex);
|
|
96
|
+
// Remove trailing slash if present
|
|
97
|
+
return base.replace(/\/$/, '');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Stage 1: Check for allowlist pattern overlaps (coarse, warn-only).
|
|
101
|
+
*/
|
|
102
|
+
export function checkAllowlistOverlaps(newAllowlist, activeRuns) {
|
|
103
|
+
const overlaps = [];
|
|
104
|
+
for (const run of activeRuns) {
|
|
105
|
+
const overlappingPatterns = [];
|
|
106
|
+
for (const newPattern of newAllowlist) {
|
|
107
|
+
for (const existingPattern of run.allowlist) {
|
|
108
|
+
if (patternsOverlap(newPattern, existingPattern)) {
|
|
109
|
+
overlappingPatterns.push(`${newPattern} ∩ ${existingPattern}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (overlappingPatterns.length > 0) {
|
|
114
|
+
overlaps.push({
|
|
115
|
+
runId: run.runId,
|
|
116
|
+
overlappingPatterns: [...new Set(overlappingPatterns)]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return overlaps;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Stage 2: Check for exact file collisions (precise, STOP by default).
|
|
124
|
+
*/
|
|
125
|
+
export function checkFileCollisions(newTouchFiles, activeRuns) {
|
|
126
|
+
const collisions = [];
|
|
127
|
+
const newFilesSet = new Set(newTouchFiles);
|
|
128
|
+
for (const run of activeRuns) {
|
|
129
|
+
const collidingFiles = [];
|
|
130
|
+
for (const file of run.predictedTouchFiles) {
|
|
131
|
+
if (newFilesSet.has(file)) {
|
|
132
|
+
collidingFiles.push(file);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (collidingFiles.length > 0) {
|
|
136
|
+
collisions.push({
|
|
137
|
+
runId: run.runId,
|
|
138
|
+
collidingFiles,
|
|
139
|
+
phase: run.phase,
|
|
140
|
+
updatedAt: run.updatedAt
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return collisions;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Full collision check (both stages).
|
|
148
|
+
*/
|
|
149
|
+
export function checkCollisions(newAllowlist, newTouchFiles, activeRuns) {
|
|
150
|
+
const allowlistOverlaps = checkAllowlistOverlaps(newAllowlist, activeRuns);
|
|
151
|
+
const fileCollisions = checkFileCollisions(newTouchFiles, activeRuns);
|
|
152
|
+
return {
|
|
153
|
+
hasCollision: fileCollisions.length > 0,
|
|
154
|
+
allowlistOverlaps,
|
|
155
|
+
fileCollisions
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Format collision warning for console output.
|
|
160
|
+
*/
|
|
161
|
+
export function formatAllowlistWarning(overlaps) {
|
|
162
|
+
if (overlaps.length === 0)
|
|
163
|
+
return '';
|
|
164
|
+
const lines = [
|
|
165
|
+
'WARNING: Allowlist overlap detected with active runs:',
|
|
166
|
+
''
|
|
167
|
+
];
|
|
168
|
+
for (const overlap of overlaps) {
|
|
169
|
+
lines.push(` Run ${overlap.runId}:`);
|
|
170
|
+
for (const pattern of overlap.overlappingPatterns.slice(0, 5)) {
|
|
171
|
+
lines.push(` - ${pattern}`);
|
|
172
|
+
}
|
|
173
|
+
if (overlap.overlappingPatterns.length > 5) {
|
|
174
|
+
lines.push(` ... and ${overlap.overlappingPatterns.length - 5} more`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
lines.push('');
|
|
178
|
+
lines.push('Consider waiting for active runs or use --force-parallel to proceed.');
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Format age from ISO timestamp to human-readable string.
|
|
183
|
+
*/
|
|
184
|
+
function formatAge(isoTimestamp) {
|
|
185
|
+
if (!isoTimestamp)
|
|
186
|
+
return '';
|
|
187
|
+
const diffMs = Date.now() - new Date(isoTimestamp).getTime();
|
|
188
|
+
if (diffMs < 0)
|
|
189
|
+
return '';
|
|
190
|
+
const minutes = Math.floor(diffMs / 60000);
|
|
191
|
+
if (minutes < 60)
|
|
192
|
+
return `${minutes}m`;
|
|
193
|
+
const hours = Math.floor(minutes / 60);
|
|
194
|
+
if (hours < 24)
|
|
195
|
+
return `${hours}h`;
|
|
196
|
+
const days = Math.floor(hours / 24);
|
|
197
|
+
return `${days}d`;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Format collision error for console output.
|
|
201
|
+
*/
|
|
202
|
+
export function formatFileCollisionError(collisions) {
|
|
203
|
+
if (collisions.length === 0)
|
|
204
|
+
return '';
|
|
205
|
+
const lines = [
|
|
206
|
+
'ERROR: This run will stop to avoid merge conflicts.',
|
|
207
|
+
''
|
|
208
|
+
];
|
|
209
|
+
for (const collision of collisions) {
|
|
210
|
+
const ageStr = formatAge(collision.updatedAt);
|
|
211
|
+
const phaseStr = collision.phase ? `, ${collision.phase}` : '';
|
|
212
|
+
const contextStr = ageStr || phaseStr ? ` (${[collision.phase, ageStr].filter(Boolean).join(', ')})` : '';
|
|
213
|
+
const fileCount = collision.collidingFiles.length;
|
|
214
|
+
const showCount = Math.min(3, fileCount);
|
|
215
|
+
lines.push(`Conflicts with: ${collision.runId}${contextStr}`);
|
|
216
|
+
lines.push(` ${fileCount} file${fileCount > 1 ? 's' : ''} overlap${showCount < fileCount ? ` (showing ${showCount})` : ''}:`);
|
|
217
|
+
for (const file of collision.collidingFiles.slice(0, showCount)) {
|
|
218
|
+
lines.push(` - ${file}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push('Options:');
|
|
223
|
+
lines.push(` 1. Wait for run ${collisions[0].runId} to complete (recommended)`);
|
|
224
|
+
lines.push(' 2. Re-run with --force-parallel (may require manual merge resolution)');
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get collision summary for status display.
|
|
229
|
+
* Returns diagnostic labels: 'none', 'allowlist' (pattern overlap), 'collision' (file conflict).
|
|
230
|
+
*/
|
|
231
|
+
export function getCollisionRisk(runAllowlist, runTouchFiles, activeRuns) {
|
|
232
|
+
const result = checkCollisions(runAllowlist, runTouchFiles, activeRuns);
|
|
233
|
+
if (result.fileCollisions.length > 0) {
|
|
234
|
+
return 'collision';
|
|
235
|
+
}
|
|
236
|
+
if (result.allowlistOverlaps.length > 0) {
|
|
237
|
+
return 'allowlist';
|
|
238
|
+
}
|
|
239
|
+
return 'none';
|
|
240
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence gating for counter-evidence claims.
|
|
3
|
+
*
|
|
4
|
+
* When an implementer claims "no_changes_needed", it must provide
|
|
5
|
+
* machine-checkable evidence to prove the claim. This prevents
|
|
6
|
+
* false certainty and "it said it was done but wasn't" failures.
|
|
7
|
+
*/
|
|
8
|
+
import picomatch from 'picomatch';
|
|
9
|
+
/**
|
|
10
|
+
* Validate that no_changes_needed claims have sufficient evidence.
|
|
11
|
+
*
|
|
12
|
+
* Requirements (at least one must be satisfied):
|
|
13
|
+
* - files_checked: at least 1 file, all within scope allowlist
|
|
14
|
+
* - grep_output: non-empty string (max 8KB enforced by schema)
|
|
15
|
+
* - commands_run: at least 1 command with exit_code === 0
|
|
16
|
+
*
|
|
17
|
+
* @param evidence - The evidence object from implementer output
|
|
18
|
+
* @param allowlist - Scope allowlist patterns for file validation
|
|
19
|
+
* @returns Validation result with ok flag and any errors
|
|
20
|
+
*/
|
|
21
|
+
export function validateNoChangesEvidence(evidence, allowlist) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!evidence) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
errors: ['No evidence provided for no_changes_needed claim']
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Check files_checked
|
|
30
|
+
const filesChecked = evidence.files_checked ?? [];
|
|
31
|
+
if (filesChecked.length > 0) {
|
|
32
|
+
// Validate all files are within scope
|
|
33
|
+
const matchers = allowlist.map(pattern => picomatch(pattern));
|
|
34
|
+
const outOfScope = filesChecked.filter(file => !matchers.some(match => match(file)));
|
|
35
|
+
if (outOfScope.length > 0) {
|
|
36
|
+
errors.push(`files_checked contains paths outside scope: ${outOfScope.slice(0, 3).join(', ')}${outOfScope.length > 3 ? ` (+${outOfScope.length - 3} more)` : ''}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
errors: [],
|
|
42
|
+
satisfied_by: 'files_checked'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Check grep_output
|
|
47
|
+
const grepOutput = evidence.grep_output ?? '';
|
|
48
|
+
if (grepOutput.trim().length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
errors: [],
|
|
52
|
+
satisfied_by: 'grep_output'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Check commands_run
|
|
56
|
+
const commandsRun = evidence.commands_run ?? [];
|
|
57
|
+
if (commandsRun.length > 0) {
|
|
58
|
+
const allPassed = commandsRun.every(cmd => cmd.exit_code === 0);
|
|
59
|
+
if (allPassed) {
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
errors: [],
|
|
63
|
+
satisfied_by: 'commands_run'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const failedCommands = commandsRun.filter(cmd => cmd.exit_code !== 0);
|
|
68
|
+
errors.push(`commands_run contains failed commands: ${failedCommands.map(c => `${c.command} (exit ${c.exit_code})`).slice(0, 2).join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// None of the evidence types were sufficient
|
|
72
|
+
if (errors.length === 0) {
|
|
73
|
+
errors.push('Evidence must include at least one of: files_checked (non-empty), grep_output (non-empty), or commands_run (with exit_code 0)');
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
errors
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Format evidence validation errors for stop memo.
|
|
82
|
+
*/
|
|
83
|
+
export function formatEvidenceErrors(result) {
|
|
84
|
+
if (result.ok)
|
|
85
|
+
return '';
|
|
86
|
+
const lines = [
|
|
87
|
+
'Insufficient evidence for "no_changes_needed" claim.',
|
|
88
|
+
'',
|
|
89
|
+
'Errors:',
|
|
90
|
+
...result.errors.map(e => ` - ${e}`),
|
|
91
|
+
'',
|
|
92
|
+
'Required: At least one of:',
|
|
93
|
+
' - files_checked: array of file paths that were inspected (must be within scope)',
|
|
94
|
+
' - grep_output: output from grep/search showing the feature already exists',
|
|
95
|
+
' - commands_run: commands executed with exit_code 0 proving no changes needed'
|
|
96
|
+
];
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
export function loadTaskFile(taskPath) {
|
|
3
|
+
return fs.readFileSync(taskPath, 'utf-8');
|
|
4
|
+
}
|
|
5
|
+
export function buildMilestonesFromTask(taskText) {
|
|
6
|
+
const firstLine = taskText
|
|
7
|
+
.split('\n')
|
|
8
|
+
.map((line) => line.trim())
|
|
9
|
+
.find((line) => line.length > 0);
|
|
10
|
+
const goal = firstLine ? firstLine : 'Implement task requirements';
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
goal,
|
|
14
|
+
done_checks: ['Core changes implemented', 'Tier 0 checks pass'],
|
|
15
|
+
risk_level: 'medium'
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
}
|