@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +166 -1
  2. package/README.md +124 -165
  3. package/dist/audit/classifier.js +331 -0
  4. package/dist/cli.js +570 -300
  5. package/dist/commands/audit.js +259 -0
  6. package/dist/commands/bundle.js +180 -0
  7. package/dist/commands/continue.js +276 -0
  8. package/dist/commands/doctor.js +430 -45
  9. package/dist/commands/hooks.js +352 -0
  10. package/dist/commands/init.js +368 -8
  11. package/dist/commands/intervene.js +109 -0
  12. package/dist/commands/meta.js +245 -0
  13. package/dist/commands/mode.js +157 -0
  14. package/dist/commands/orchestrate.js +29 -0
  15. package/dist/commands/packs.js +47 -0
  16. package/dist/commands/preflight.js +8 -5
  17. package/dist/commands/resume.js +421 -3
  18. package/dist/commands/run.js +63 -4
  19. package/dist/commands/status.js +47 -0
  20. package/dist/commands/submit.js +374 -0
  21. package/dist/config/schema.js +61 -1
  22. package/dist/diagnosis/analyzer.js +86 -1
  23. package/dist/diagnosis/formatter.js +3 -0
  24. package/dist/diagnosis/index.js +1 -0
  25. package/dist/diagnosis/stop-explainer.js +267 -0
  26. package/dist/diagnostics/stop-explainer.js +267 -0
  27. package/dist/guards/checkpoint.js +119 -0
  28. package/dist/journal/builder.js +36 -3
  29. package/dist/journal/renderer.js +19 -0
  30. package/dist/orchestrator/artifacts.js +17 -2
  31. package/dist/orchestrator/receipt.js +304 -0
  32. package/dist/output/stop-footer.js +185 -0
  33. package/dist/packs/actions.js +176 -0
  34. package/dist/packs/loader.js +200 -0
  35. package/dist/packs/renderer.js +46 -0
  36. package/dist/receipt/intervention.js +465 -0
  37. package/dist/receipt/writer.js +296 -0
  38. package/dist/redaction/redactor.js +95 -0
  39. package/dist/repo/context.js +147 -20
  40. package/dist/review/check-parser.js +211 -0
  41. package/dist/store/checkpoint-metadata.js +111 -0
  42. package/dist/store/run-store.js +21 -0
  43. package/dist/supervisor/runner.js +130 -10
  44. package/dist/tasks/task-metadata.js +74 -1
  45. package/dist/ux/brain.js +528 -0
  46. package/dist/ux/render.js +123 -0
  47. package/dist/ux/safe-commands.js +133 -0
  48. package/dist/ux/state.js +193 -0
  49. package/dist/ux/telemetry.js +110 -0
  50. package/package.json +3 -1
  51. package/packs/pr/pack.json +50 -0
  52. package/packs/pr/templates/AGENTS.md.tmpl +120 -0
  53. package/packs/pr/templates/CLAUDE.md.tmpl +101 -0
  54. package/packs/pr/templates/bundle.md.tmpl +27 -0
  55. package/packs/solo/pack.json +82 -0
  56. package/packs/solo/templates/AGENTS.md.tmpl +80 -0
  57. package/packs/solo/templates/CLAUDE.md.tmpl +126 -0
  58. package/packs/solo/templates/bundle.md.tmpl +27 -0
  59. package/packs/solo/templates/claude-cmd-bundle.md.tmpl +40 -0
  60. package/packs/solo/templates/claude-cmd-resume.md.tmpl +43 -0
  61. package/packs/solo/templates/claude-cmd-submit.md.tmpl +51 -0
  62. package/packs/solo/templates/claude-skill.md.tmpl +96 -0
  63. package/packs/trunk/pack.json +50 -0
  64. package/packs/trunk/templates/AGENTS.md.tmpl +87 -0
  65. package/packs/trunk/templates/CLAUDE.md.tmpl +126 -0
  66. 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
+ }