@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +150 -1
  2. package/README.md +124 -111
  3. package/dist/audit/classifier.js +331 -0
  4. package/dist/cli.js +593 -282
  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/journal.js +167 -0
  13. package/dist/commands/meta.js +245 -0
  14. package/dist/commands/mode.js +157 -0
  15. package/dist/commands/orchestrate.js +29 -0
  16. package/dist/commands/packs.js +47 -0
  17. package/dist/commands/preflight.js +8 -5
  18. package/dist/commands/resume.js +421 -3
  19. package/dist/commands/run.js +63 -4
  20. package/dist/commands/status.js +47 -0
  21. package/dist/commands/submit.js +374 -0
  22. package/dist/config/schema.js +61 -1
  23. package/dist/diagnosis/analyzer.js +86 -1
  24. package/dist/diagnosis/formatter.js +3 -0
  25. package/dist/diagnosis/index.js +1 -0
  26. package/dist/diagnosis/stop-explainer.js +267 -0
  27. package/dist/diagnostics/stop-explainer.js +267 -0
  28. package/dist/guards/checkpoint.js +119 -0
  29. package/dist/journal/builder.js +497 -0
  30. package/dist/journal/redactor.js +68 -0
  31. package/dist/journal/renderer.js +220 -0
  32. package/dist/journal/types.js +7 -0
  33. package/dist/orchestrator/artifacts.js +17 -2
  34. package/dist/orchestrator/receipt.js +304 -0
  35. package/dist/output/stop-footer.js +185 -0
  36. package/dist/packs/actions.js +176 -0
  37. package/dist/packs/loader.js +200 -0
  38. package/dist/packs/renderer.js +46 -0
  39. package/dist/receipt/intervention.js +465 -0
  40. package/dist/receipt/writer.js +296 -0
  41. package/dist/redaction/redactor.js +95 -0
  42. package/dist/repo/context.js +147 -20
  43. package/dist/review/check-parser.js +211 -0
  44. package/dist/store/checkpoint-metadata.js +111 -0
  45. package/dist/store/run-store.js +21 -0
  46. package/dist/supervisor/runner.js +161 -10
  47. package/dist/tasks/task-metadata.js +74 -1
  48. package/dist/ux/brain.js +528 -0
  49. package/dist/ux/render.js +123 -0
  50. package/dist/ux/safe-commands.js +133 -0
  51. package/dist/ux/state.js +193 -0
  52. package/dist/ux/telemetry.js +110 -0
  53. package/package.json +5 -1
  54. package/packs/pr/pack.json +50 -0
  55. package/packs/pr/templates/AGENTS.md.tmpl +120 -0
  56. package/packs/pr/templates/CLAUDE.md.tmpl +101 -0
  57. package/packs/pr/templates/bundle.md.tmpl +27 -0
  58. package/packs/solo/pack.json +82 -0
  59. package/packs/solo/templates/AGENTS.md.tmpl +80 -0
  60. package/packs/solo/templates/CLAUDE.md.tmpl +126 -0
  61. package/packs/solo/templates/bundle.md.tmpl +27 -0
  62. package/packs/solo/templates/claude-cmd-bundle.md.tmpl +40 -0
  63. package/packs/solo/templates/claude-cmd-resume.md.tmpl +43 -0
  64. package/packs/solo/templates/claude-cmd-submit.md.tmpl +51 -0
  65. package/packs/solo/templates/claude-skill.md.tmpl +96 -0
  66. package/packs/trunk/pack.json +50 -0
  67. package/packs/trunk/templates/AGENTS.md.tmpl +87 -0
  68. package/packs/trunk/templates/CLAUDE.md.tmpl +126 -0
  69. package/packs/trunk/templates/bundle.md.tmpl +27 -0
  70. package/dist/commands/__tests__/report.test.js +0 -202
  71. package/dist/config/__tests__/presets.test.js +0 -104
  72. package/dist/context/__tests__/artifact.test.js +0 -130
  73. package/dist/context/__tests__/pack.test.js +0 -191
  74. package/dist/env/__tests__/fingerprint.test.js +0 -116
  75. package/dist/orchestrator/__tests__/policy.test.js +0 -185
  76. package/dist/orchestrator/__tests__/schema-version.test.js +0 -65
  77. package/dist/supervisor/__tests__/evidence-gate.test.js +0 -111
  78. package/dist/supervisor/__tests__/ownership.test.js +0 -103
  79. package/dist/supervisor/__tests__/state-machine.test.js +0 -290
  80. package/dist/workers/__tests__/claude.test.js +0 -88
  81. package/dist/workers/__tests__/codex.test.js +0 -81
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Git hooks management for Runr provenance.
3
+ *
4
+ * Commands:
5
+ * - runr hooks install: Create hook scripts in .git/hooks/
6
+ * - runr hooks uninstall: Remove hooks and restore backups
7
+ * - runr hooks status: Show installed state
8
+ * - runr hooks check-commit: Check commit against run state (internal)
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ const HOOK_SCRIPTS = {
13
+ 'commit-msg': `#!/bin/bash
14
+ # Runr provenance hook - installed by 'runr hooks install'
15
+ if command -v runr &> /dev/null; then
16
+ runr hooks check-commit "$1"
17
+ fi
18
+ # Always allow commit if runr not available
19
+ exit 0
20
+ `
21
+ };
22
+ const BACKUP_SUFFIX = '.runr-backup';
23
+ function getGitHooksDir(repoPath) {
24
+ const gitDir = path.join(repoPath, '.git');
25
+ if (!fs.existsSync(gitDir)) {
26
+ return null;
27
+ }
28
+ return path.join(gitDir, 'hooks');
29
+ }
30
+ function getHooksConfigPath(repoPath) {
31
+ return path.join(repoPath, '.runr', 'hooks.json');
32
+ }
33
+ function getActiveStatePath(repoPath) {
34
+ return path.join(repoPath, '.runr', 'active.json');
35
+ }
36
+ function loadHooksConfig(repoPath) {
37
+ const configPath = getHooksConfigPath(repoPath);
38
+ if (!fs.existsSync(configPath)) {
39
+ return null;
40
+ }
41
+ try {
42
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function saveHooksConfig(repoPath, config) {
49
+ const configPath = getHooksConfigPath(repoPath);
50
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
51
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
52
+ }
53
+ /**
54
+ * Load active run state from sentinel file.
55
+ */
56
+ export function loadActiveState(repoPath) {
57
+ const statePath = getActiveStatePath(repoPath);
58
+ if (!fs.existsSync(statePath)) {
59
+ return {
60
+ run_id: null,
61
+ status: 'NONE',
62
+ updated_at: new Date().toISOString()
63
+ };
64
+ }
65
+ try {
66
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
67
+ }
68
+ catch {
69
+ return {
70
+ run_id: null,
71
+ status: 'NONE',
72
+ updated_at: new Date().toISOString()
73
+ };
74
+ }
75
+ }
76
+ /**
77
+ * Update active run state sentinel file.
78
+ */
79
+ export function updateActiveState(repoPath, state) {
80
+ const current = loadActiveState(repoPath);
81
+ const updated = {
82
+ ...current,
83
+ ...state,
84
+ updated_at: new Date().toISOString()
85
+ };
86
+ const statePath = getActiveStatePath(repoPath);
87
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
88
+ fs.writeFileSync(statePath, JSON.stringify(updated, null, 2));
89
+ }
90
+ /**
91
+ * Clear active run state (set to NONE).
92
+ */
93
+ export function clearActiveState(repoPath) {
94
+ updateActiveState(repoPath, {
95
+ run_id: null,
96
+ status: 'NONE',
97
+ stop_reason: undefined
98
+ });
99
+ }
100
+ /**
101
+ * Install Runr git hooks.
102
+ */
103
+ export async function installCommand(options) {
104
+ const repoPath = path.resolve(options.repo);
105
+ // Check for .git directory
106
+ const hooksDir = getGitHooksDir(repoPath);
107
+ if (!hooksDir) {
108
+ console.error('Error: Not a git repository (no .git directory found)');
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ // Ensure hooks directory exists
113
+ fs.mkdirSync(hooksDir, { recursive: true });
114
+ // Check if already installed
115
+ const existingConfig = loadHooksConfig(repoPath);
116
+ if (existingConfig) {
117
+ console.log('Runr hooks already installed. Use "runr hooks status" to check.');
118
+ return;
119
+ }
120
+ const installedHooks = [];
121
+ // Install each hook
122
+ for (const [hookName, hookScript] of Object.entries(HOOK_SCRIPTS)) {
123
+ const hookPath = path.join(hooksDir, hookName);
124
+ // Backup existing hook if present
125
+ if (fs.existsSync(hookPath)) {
126
+ const backupPath = hookPath + BACKUP_SUFFIX;
127
+ fs.copyFileSync(hookPath, backupPath);
128
+ console.log(` Backed up existing ${hookName} to ${hookName}${BACKUP_SUFFIX}`);
129
+ }
130
+ // Write new hook
131
+ fs.writeFileSync(hookPath, hookScript);
132
+ fs.chmodSync(hookPath, '755');
133
+ installedHooks.push(hookName);
134
+ console.log(` Installed ${hookName} hook`);
135
+ }
136
+ // Save config
137
+ const config = {
138
+ installed_at: new Date().toISOString(),
139
+ hooks: installedHooks,
140
+ backup_suffix: BACKUP_SUFFIX
141
+ };
142
+ saveHooksConfig(repoPath, config);
143
+ console.log('');
144
+ console.log('Runr hooks installed. Use "runr hooks status" to check.');
145
+ }
146
+ /**
147
+ * Uninstall Runr git hooks.
148
+ */
149
+ export async function uninstallCommand(options) {
150
+ const repoPath = path.resolve(options.repo);
151
+ const hooksDir = getGitHooksDir(repoPath);
152
+ if (!hooksDir) {
153
+ console.error('Error: Not a git repository');
154
+ process.exitCode = 1;
155
+ return;
156
+ }
157
+ const config = loadHooksConfig(repoPath);
158
+ if (!config) {
159
+ console.log('Runr hooks not installed.');
160
+ return;
161
+ }
162
+ // Remove each hook and restore backups
163
+ for (const hookName of config.hooks) {
164
+ const hookPath = path.join(hooksDir, hookName);
165
+ const backupPath = hookPath + config.backup_suffix;
166
+ // Remove Runr hook
167
+ if (fs.existsSync(hookPath)) {
168
+ fs.unlinkSync(hookPath);
169
+ console.log(` Removed ${hookName} hook`);
170
+ }
171
+ // Restore backup if exists
172
+ if (fs.existsSync(backupPath)) {
173
+ fs.renameSync(backupPath, hookPath);
174
+ console.log(` Restored ${hookName} from backup`);
175
+ }
176
+ }
177
+ // Remove config
178
+ const configPath = getHooksConfigPath(repoPath);
179
+ if (fs.existsSync(configPath)) {
180
+ fs.unlinkSync(configPath);
181
+ }
182
+ console.log('');
183
+ console.log('Runr hooks removed.');
184
+ }
185
+ /**
186
+ * Show git hooks status.
187
+ */
188
+ export async function statusCommand(options) {
189
+ const repoPath = path.resolve(options.repo);
190
+ const hooksDir = getGitHooksDir(repoPath);
191
+ if (!hooksDir) {
192
+ console.error('Error: Not a git repository');
193
+ process.exitCode = 1;
194
+ return;
195
+ }
196
+ const config = loadHooksConfig(repoPath);
197
+ console.log('Runr Git Hooks Status');
198
+ console.log('');
199
+ if (!config) {
200
+ console.log(' Status: NOT INSTALLED');
201
+ console.log('');
202
+ console.log(' Run "runr hooks install" to enable provenance tracking.');
203
+ return;
204
+ }
205
+ console.log(' Status: INSTALLED');
206
+ console.log(` Installed: ${config.installed_at}`);
207
+ console.log('');
208
+ console.log(' Hooks:');
209
+ for (const hookName of config.hooks) {
210
+ const hookPath = path.join(hooksDir, hookName);
211
+ const exists = fs.existsSync(hookPath);
212
+ const icon = exists ? '✓' : '✗';
213
+ console.log(` ${icon} ${hookName}`);
214
+ }
215
+ // Show active run state
216
+ const activeState = loadActiveState(repoPath);
217
+ console.log('');
218
+ console.log(' Active Run:');
219
+ if (activeState.status === 'NONE') {
220
+ console.log(' No active run');
221
+ }
222
+ else {
223
+ console.log(` Run ID: ${activeState.run_id}`);
224
+ console.log(` Status: ${activeState.status}`);
225
+ if (activeState.stop_reason) {
226
+ console.log(` Stop Reason: ${activeState.stop_reason}`);
227
+ }
228
+ }
229
+ }
230
+ /**
231
+ * Runr trailers that indicate proper attribution.
232
+ */
233
+ const RUNR_TRAILERS = [
234
+ /^Runr-Run-Id:\s*.+$/m,
235
+ /^Runr-Intervention:\s*true$/m,
236
+ /^Runr-Checkpoint:\s*true$/m
237
+ ];
238
+ /**
239
+ * Check if commit message has Runr trailers.
240
+ */
241
+ function hasRunrTrailers(commitMessage) {
242
+ return RUNR_TRAILERS.some(pattern => pattern.test(commitMessage));
243
+ }
244
+ /**
245
+ * Check if this is a merge commit (skip check).
246
+ */
247
+ function isMergeCommit(msgFile) {
248
+ // Git uses specific filenames for merge commits
249
+ const filename = path.basename(msgFile);
250
+ return filename === 'MERGE_MSG' || filename === 'SQUASH_MSG';
251
+ }
252
+ /**
253
+ * Get current workflow mode from config.
254
+ */
255
+ function getWorkflowMode(repoPath) {
256
+ const configPath = path.join(repoPath, '.runr', 'runr.config.json');
257
+ if (!fs.existsSync(configPath)) {
258
+ return 'flow'; // Default to flow mode
259
+ }
260
+ try {
261
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
262
+ return config.workflow?.mode === 'ledger' ? 'ledger' : 'flow';
263
+ }
264
+ catch {
265
+ return 'flow';
266
+ }
267
+ }
268
+ /**
269
+ * Check commit against run state (called by git hook).
270
+ *
271
+ * Mode-aware behavior:
272
+ * - Flow mode: warn but allow commits without attribution
273
+ * - Ledger mode: block commits without attribution
274
+ */
275
+ export async function checkCommitCommand(options) {
276
+ const repoPath = path.resolve(options.repo);
277
+ // Skip check for merge commits
278
+ if (isMergeCommit(options.msgFile)) {
279
+ process.exitCode = 0;
280
+ return;
281
+ }
282
+ // Check if .runr directory exists
283
+ const runrDir = path.join(repoPath, '.runr');
284
+ if (!fs.existsSync(runrDir)) {
285
+ process.exitCode = 0;
286
+ return;
287
+ }
288
+ // Load active state
289
+ const activeState = loadActiveState(repoPath);
290
+ // If no stopped run, allow
291
+ if (activeState.status !== 'STOPPED') {
292
+ process.exitCode = 0;
293
+ return;
294
+ }
295
+ // Read commit message
296
+ let commitMessage = '';
297
+ try {
298
+ commitMessage = fs.readFileSync(options.msgFile, 'utf-8');
299
+ }
300
+ catch {
301
+ // Can't read commit message, fail open
302
+ process.exitCode = 0;
303
+ return;
304
+ }
305
+ // Check for Runr trailers
306
+ if (hasRunrTrailers(commitMessage)) {
307
+ // Properly attributed commit, allow
308
+ process.exitCode = 0;
309
+ return;
310
+ }
311
+ // Check for override environment variable
312
+ const allowGap = process.env.RUNR_ALLOW_GAP === '1';
313
+ // Get workflow mode
314
+ const mode = getWorkflowMode(repoPath);
315
+ // Format the provenance warning/error message
316
+ const formatMessage = (isError) => {
317
+ const icon = isError ? '❌' : '⚠️';
318
+ const title = isError
319
+ ? 'Provenance required (Ledger mode)'
320
+ : 'Provenance gap detected';
321
+ console.error('');
322
+ console.error(`${icon} ${title}`);
323
+ console.error('');
324
+ console.error(`Run ${activeState.run_id} is STOPPED (${activeState.stop_reason || 'unknown'}).`);
325
+ console.error('This commit has no Runr attribution.');
326
+ console.error('');
327
+ console.error('To add attribution:');
328
+ console.error(` runr intervene ${activeState.run_id} --reason ${activeState.stop_reason || 'manual'} \\`);
329
+ console.error(' --note "description" --commit "your message"');
330
+ console.error('');
331
+ if (isError) {
332
+ console.error('To override (not recommended):');
333
+ console.error(' RUNR_ALLOW_GAP=1 git commit ...');
334
+ console.error(' # or: git commit --no-verify');
335
+ console.error('');
336
+ }
337
+ else {
338
+ console.error('Proceeding anyway (Flow mode).');
339
+ console.error('');
340
+ }
341
+ };
342
+ if (mode === 'ledger' && !allowGap) {
343
+ // Ledger mode: block commit
344
+ formatMessage(true);
345
+ process.exitCode = 1;
346
+ }
347
+ else {
348
+ // Flow mode or override: warn but allow
349
+ formatMessage(false);
350
+ process.exitCode = 0;
351
+ }
352
+ }