@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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Classifier - Classifies commits by provenance
|
|
3
|
+
*
|
|
4
|
+
* Classifications:
|
|
5
|
+
* - runr_checkpoint: Has checkpoint receipt or checkpoint commit
|
|
6
|
+
* - runr_intervention: Has Runr-Intervention trailer or intervention receipt
|
|
7
|
+
* - manual_attributed: Has Runr-Run-Id trailer but not checkpoint/intervention
|
|
8
|
+
* - gap: No Runr attribution (audit gap)
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import { getRunsRoot } from '../store/runs-root.js';
|
|
14
|
+
/**
|
|
15
|
+
* Parse git log output into commit objects.
|
|
16
|
+
*/
|
|
17
|
+
export function parseGitLog(repoPath, range) {
|
|
18
|
+
const commits = [];
|
|
19
|
+
try {
|
|
20
|
+
// Format: sha|short|subject|author|date|trailers
|
|
21
|
+
// Use %x00 as delimiter to handle subjects with |
|
|
22
|
+
const format = '%H%x00%h%x00%s%x00%an%x00%ai%x00%(trailers:key=Runr-Run-Id,valueonly)%x00%(trailers:key=Runr-Intervention,valueonly)%x00%(trailers:key=Runr-Reason,valueonly)%x00%(trailers:key=Runr-Checkpoint,valueonly)';
|
|
23
|
+
const output = execSync(`git log --format="${format}" ${range}`, {
|
|
24
|
+
cwd: repoPath,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
maxBuffer: 10 * 1024 * 1024
|
|
27
|
+
});
|
|
28
|
+
for (const line of output.trim().split('\n')) {
|
|
29
|
+
if (!line.trim())
|
|
30
|
+
continue;
|
|
31
|
+
const parts = line.split('\x00');
|
|
32
|
+
if (parts.length < 5)
|
|
33
|
+
continue;
|
|
34
|
+
const [sha, shortSha, subject, author, date, runId, intervention, reason, checkpoint] = parts;
|
|
35
|
+
const trailers = {};
|
|
36
|
+
if (runId?.trim())
|
|
37
|
+
trailers.runrRunId = runId.trim();
|
|
38
|
+
if (intervention?.trim().toLowerCase() === 'true')
|
|
39
|
+
trailers.runrIntervention = true;
|
|
40
|
+
if (reason?.trim())
|
|
41
|
+
trailers.runrReason = reason.trim();
|
|
42
|
+
if (checkpoint?.trim())
|
|
43
|
+
trailers.runrCheckpoint = checkpoint.trim();
|
|
44
|
+
commits.push({
|
|
45
|
+
sha,
|
|
46
|
+
shortSha,
|
|
47
|
+
subject,
|
|
48
|
+
author,
|
|
49
|
+
date,
|
|
50
|
+
classification: 'gap', // Will be classified later
|
|
51
|
+
trailers
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Git log failed - return empty array
|
|
57
|
+
console.error(`Warning: git log failed: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
return commits;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if a run has a checkpoint commit matching the given SHA.
|
|
63
|
+
*/
|
|
64
|
+
function hasCheckpointForSha(runsRoot, sha) {
|
|
65
|
+
if (!fs.existsSync(runsRoot))
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const runDirs = fs.readdirSync(runsRoot, { withFileTypes: true })
|
|
69
|
+
.filter(d => d.isDirectory() && /^\d{14}$/.test(d.name));
|
|
70
|
+
for (const runDir of runDirs) {
|
|
71
|
+
// Check state.json for checkpoint_commit_sha
|
|
72
|
+
const statePath = path.join(runsRoot, runDir.name, 'state.json');
|
|
73
|
+
if (fs.existsSync(statePath)) {
|
|
74
|
+
try {
|
|
75
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
76
|
+
if (state.checkpoint_commit_sha === sha) {
|
|
77
|
+
return {
|
|
78
|
+
runId: runDir.name,
|
|
79
|
+
receiptPath: path.join(runsRoot, runDir.name, 'receipt.json')
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
}
|
|
85
|
+
// Check receipt.json for checkpoint_sha
|
|
86
|
+
const receiptPath = path.join(runsRoot, runDir.name, 'receipt.json');
|
|
87
|
+
if (fs.existsSync(receiptPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf-8'));
|
|
90
|
+
if (receipt.checkpoint_sha === sha) {
|
|
91
|
+
return {
|
|
92
|
+
runId: runDir.name,
|
|
93
|
+
receiptPath
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch { /* ignore */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch { /* ignore */ }
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if a run has an intervention receipt for a given run ID.
|
|
106
|
+
*/
|
|
107
|
+
function hasInterventionReceipt(runsRoot, runId) {
|
|
108
|
+
const interventionsDir = path.join(runsRoot, runId, 'interventions');
|
|
109
|
+
if (!fs.existsSync(interventionsDir))
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const files = fs.readdirSync(interventionsDir).filter(f => f.endsWith('.json'));
|
|
113
|
+
if (files.length > 0) {
|
|
114
|
+
return path.join(interventionsDir, files[0]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch { /* ignore */ }
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Load all intervention ranges from run stores.
|
|
122
|
+
*/
|
|
123
|
+
function loadInterventionRanges(runsRoot) {
|
|
124
|
+
const ranges = [];
|
|
125
|
+
if (!fs.existsSync(runsRoot))
|
|
126
|
+
return ranges;
|
|
127
|
+
try {
|
|
128
|
+
const runDirs = fs.readdirSync(runsRoot, { withFileTypes: true })
|
|
129
|
+
.filter(d => d.isDirectory() && /^\d{14}$/.test(d.name));
|
|
130
|
+
for (const runDir of runDirs) {
|
|
131
|
+
const interventionsDir = path.join(runsRoot, runDir.name, 'interventions');
|
|
132
|
+
if (!fs.existsSync(interventionsDir))
|
|
133
|
+
continue;
|
|
134
|
+
try {
|
|
135
|
+
const files = fs.readdirSync(interventionsDir).filter(f => f.endsWith('.json'));
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const receiptPath = path.join(interventionsDir, file);
|
|
138
|
+
try {
|
|
139
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf-8'));
|
|
140
|
+
// Only include receipts with both base_sha and head_sha
|
|
141
|
+
if (receipt.base_sha && receipt.head_sha && receipt.base_sha !== receipt.head_sha) {
|
|
142
|
+
ranges.push({
|
|
143
|
+
runId: runDir.name,
|
|
144
|
+
baseSha: receipt.base_sha,
|
|
145
|
+
headSha: receipt.head_sha,
|
|
146
|
+
receiptPath
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch { /* skip invalid receipts */ }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* skip inaccessible dirs */ }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch { /* ignore */ }
|
|
157
|
+
return ranges;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if a commit SHA is within any intervention range.
|
|
161
|
+
* Uses git merge-base --is-ancestor to check ancestry.
|
|
162
|
+
*/
|
|
163
|
+
function findInterventionRangeForCommit(repoPath, sha, ranges) {
|
|
164
|
+
for (const range of ranges) {
|
|
165
|
+
try {
|
|
166
|
+
// Check if sha is a descendant of base_sha
|
|
167
|
+
execSync(`git merge-base --is-ancestor ${range.baseSha} ${sha}`, {
|
|
168
|
+
cwd: repoPath,
|
|
169
|
+
encoding: 'utf-8',
|
|
170
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
171
|
+
});
|
|
172
|
+
// Check if sha is an ancestor of (or equal to) head_sha
|
|
173
|
+
execSync(`git merge-base --is-ancestor ${sha} ${range.headSha}`, {
|
|
174
|
+
cwd: repoPath,
|
|
175
|
+
encoding: 'utf-8',
|
|
176
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
177
|
+
});
|
|
178
|
+
// Both checks passed - commit is within range
|
|
179
|
+
return range;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Not in this range, continue checking
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Classify commits based on trailers and receipt artifacts.
|
|
189
|
+
*/
|
|
190
|
+
export function classifyCommits(commits, repoPath) {
|
|
191
|
+
const runsRoot = getRunsRoot(repoPath);
|
|
192
|
+
// Load intervention ranges for SHA-based inference
|
|
193
|
+
const interventionRanges = loadInterventionRanges(runsRoot);
|
|
194
|
+
for (const commit of commits) {
|
|
195
|
+
// Priority 1: Check if this is a checkpoint commit (by SHA match)
|
|
196
|
+
const checkpointInfo = hasCheckpointForSha(runsRoot, commit.sha);
|
|
197
|
+
if (checkpointInfo) {
|
|
198
|
+
commit.classification = 'runr_checkpoint';
|
|
199
|
+
commit.runId = checkpointInfo.runId;
|
|
200
|
+
commit.receiptPath = checkpointInfo.receiptPath;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// Priority 2: Has Runr-Checkpoint trailer
|
|
204
|
+
if (commit.trailers.runrCheckpoint) {
|
|
205
|
+
commit.classification = 'runr_checkpoint';
|
|
206
|
+
commit.runId = commit.trailers.runrRunId;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// Priority 3: Has Runr-Intervention trailer
|
|
210
|
+
if (commit.trailers.runrIntervention) {
|
|
211
|
+
commit.classification = 'runr_intervention';
|
|
212
|
+
commit.runId = commit.trailers.runrRunId;
|
|
213
|
+
if (commit.runId) {
|
|
214
|
+
const receiptPath = hasInterventionReceipt(runsRoot, commit.runId);
|
|
215
|
+
if (receiptPath)
|
|
216
|
+
commit.receiptPath = receiptPath;
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Priority 4: Has Runr-Run-Id but not checkpoint/intervention
|
|
221
|
+
if (commit.trailers.runrRunId) {
|
|
222
|
+
// Check if there's an intervention receipt for this run
|
|
223
|
+
const receiptPath = hasInterventionReceipt(runsRoot, commit.trailers.runrRunId);
|
|
224
|
+
if (receiptPath) {
|
|
225
|
+
commit.classification = 'runr_intervention';
|
|
226
|
+
commit.runId = commit.trailers.runrRunId;
|
|
227
|
+
commit.receiptPath = receiptPath;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
commit.classification = 'manual_attributed';
|
|
231
|
+
commit.runId = commit.trailers.runrRunId;
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Priority 5: Check if commit falls within intervention SHA range (inferred)
|
|
236
|
+
const matchedRange = findInterventionRangeForCommit(repoPath, commit.sha, interventionRanges);
|
|
237
|
+
if (matchedRange) {
|
|
238
|
+
commit.classification = 'runr_inferred';
|
|
239
|
+
commit.runId = matchedRange.runId;
|
|
240
|
+
commit.receiptPath = matchedRange.receiptPath;
|
|
241
|
+
commit.inferenceSource = 'intervention_range';
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// Priority 6: Check commit message for Task patterns (legacy)
|
|
245
|
+
if (/Task \d+|task-\d+/i.test(commit.subject)) {
|
|
246
|
+
// Try to find a matching run
|
|
247
|
+
const runMatch = commit.subject.match(/(\d{14})/);
|
|
248
|
+
if (runMatch) {
|
|
249
|
+
commit.classification = 'manual_attributed';
|
|
250
|
+
commit.runId = runMatch[1];
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Default: gap (no attribution)
|
|
255
|
+
commit.classification = 'gap';
|
|
256
|
+
}
|
|
257
|
+
return commits;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Generate audit summary from classified commits.
|
|
261
|
+
* @param strict If true, treat runr_inferred as gaps
|
|
262
|
+
*/
|
|
263
|
+
export function generateSummary(commits, range, strict = false) {
|
|
264
|
+
const counts = {
|
|
265
|
+
total: commits.length,
|
|
266
|
+
runr_checkpoint: 0,
|
|
267
|
+
runr_intervention: 0,
|
|
268
|
+
runr_inferred: 0,
|
|
269
|
+
manual_attributed: 0,
|
|
270
|
+
gap: 0
|
|
271
|
+
};
|
|
272
|
+
const gaps = [];
|
|
273
|
+
const runsReferenced = new Set();
|
|
274
|
+
for (const commit of commits) {
|
|
275
|
+
counts[commit.classification]++;
|
|
276
|
+
// In strict mode, treat inferred as gaps
|
|
277
|
+
if (commit.classification === 'gap' || (strict && commit.classification === 'runr_inferred')) {
|
|
278
|
+
gaps.push(commit);
|
|
279
|
+
}
|
|
280
|
+
if (commit.runId) {
|
|
281
|
+
runsReferenced.add(commit.runId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Calculate coverage percentages
|
|
285
|
+
const explicitCount = counts.runr_checkpoint + counts.runr_intervention;
|
|
286
|
+
const withInferredCount = explicitCount + counts.runr_inferred;
|
|
287
|
+
const fullCount = withInferredCount + counts.manual_attributed;
|
|
288
|
+
const explicitCoverage = counts.total > 0
|
|
289
|
+
? Math.round((explicitCount / counts.total) * 100)
|
|
290
|
+
: 0;
|
|
291
|
+
const inferredCoverage = counts.total > 0
|
|
292
|
+
? Math.round((withInferredCount / counts.total) * 100)
|
|
293
|
+
: 0;
|
|
294
|
+
const fullCoverage = counts.total > 0
|
|
295
|
+
? Math.round((fullCount / counts.total) * 100)
|
|
296
|
+
: 0;
|
|
297
|
+
return {
|
|
298
|
+
range,
|
|
299
|
+
commits,
|
|
300
|
+
counts,
|
|
301
|
+
gaps,
|
|
302
|
+
runsReferenced: Array.from(runsReferenced).sort(),
|
|
303
|
+
explicitCoverage,
|
|
304
|
+
inferredCoverage,
|
|
305
|
+
fullCoverage
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Format classification for display.
|
|
310
|
+
*/
|
|
311
|
+
export function formatClassification(classification) {
|
|
312
|
+
switch (classification) {
|
|
313
|
+
case 'runr_checkpoint': return 'CHECKPOINT';
|
|
314
|
+
case 'runr_intervention': return 'INTERVENTION';
|
|
315
|
+
case 'runr_inferred': return 'INFERRED';
|
|
316
|
+
case 'manual_attributed': return 'ATTRIBUTED';
|
|
317
|
+
case 'gap': return 'GAP';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get icon for classification.
|
|
322
|
+
*/
|
|
323
|
+
export function getClassificationIcon(classification) {
|
|
324
|
+
switch (classification) {
|
|
325
|
+
case 'runr_checkpoint': return '✓';
|
|
326
|
+
case 'runr_intervention': return '⚡';
|
|
327
|
+
case 'runr_inferred': return '~';
|
|
328
|
+
case 'manual_attributed': return '○';
|
|
329
|
+
case 'gap': return '?';
|
|
330
|
+
}
|
|
331
|
+
}
|