@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,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
|
+
}
|