@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,245 @@
1
+ import { execa } from 'execa';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { getRunrPaths } from '../store/runs-root.js';
5
+ /**
6
+ * Detect which meta-agent tool is available
7
+ */
8
+ async function detectTool() {
9
+ // Try Claude Code first
10
+ try {
11
+ const result = await execa('claude', ['--version'], {
12
+ timeout: 5000,
13
+ reject: false
14
+ });
15
+ if (result.exitCode === 0) {
16
+ return 'claude';
17
+ }
18
+ }
19
+ catch {
20
+ // Claude not found, continue
21
+ }
22
+ // Try Codex CLI
23
+ try {
24
+ const result = await execa('codex', ['--version'], {
25
+ timeout: 5000,
26
+ reject: false
27
+ });
28
+ if (result.exitCode === 0) {
29
+ return 'codex';
30
+ }
31
+ }
32
+ catch {
33
+ // Codex not found
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Check if working tree is clean
39
+ */
40
+ async function checkWorkingTree(repoPath) {
41
+ try {
42
+ const result = await execa('git', ['status', '--porcelain'], {
43
+ cwd: repoPath,
44
+ reject: false
45
+ });
46
+ if (result.exitCode !== 0) {
47
+ throw new Error('git status failed');
48
+ }
49
+ const lines = result.stdout.trim().split('\n').filter(line => line.length > 0);
50
+ return {
51
+ clean: lines.length === 0,
52
+ uncommittedCount: lines.length
53
+ };
54
+ }
55
+ catch (error) {
56
+ throw new Error(`Failed to check working tree: ${error.message}`);
57
+ }
58
+ }
59
+ /**
60
+ * Check if repository is properly set up for Runr
61
+ */
62
+ function checkRepoSetup(repoPath) {
63
+ const paths = getRunrPaths(repoPath);
64
+ const configPath = path.join(paths.runr_root, 'runr.config.json');
65
+ const gitignorePath = path.join(repoPath, '.gitignore');
66
+ const agentsMdPath = path.join(repoPath, 'AGENTS.md');
67
+ const configExists = fs.existsSync(configPath);
68
+ const agentsMdExists = fs.existsSync(agentsMdPath);
69
+ // Check gitignore
70
+ let gitignoreOk = false;
71
+ try {
72
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
73
+ const lines = content.split('\n').map(l => l.trim());
74
+ gitignoreOk = lines.some(line => line.startsWith('.runr/') || line === '.runr' || line === '.runr/');
75
+ }
76
+ catch {
77
+ // .gitignore doesn't exist
78
+ }
79
+ const missingSetup = [];
80
+ if (!configExists)
81
+ missingSetup.push('.runr/runr.config.json');
82
+ if (!gitignoreOk)
83
+ missingSetup.push('.gitignore entries');
84
+ if (!agentsMdExists)
85
+ missingSetup.push('AGENTS.md');
86
+ return {
87
+ configExists,
88
+ gitignoreOk,
89
+ agentsMdExists,
90
+ missingSetup
91
+ };
92
+ }
93
+ /**
94
+ * Check Claude Code integration status
95
+ */
96
+ function checkClaudeIntegration(repoPath) {
97
+ const skillPath = path.join(repoPath, '.claude/skills/runr-workflow/SKILL.md');
98
+ const commandPaths = [
99
+ path.join(repoPath, '.claude/commands/runr-bundle.md'),
100
+ path.join(repoPath, '.claude/commands/runr-submit.md'),
101
+ path.join(repoPath, '.claude/commands/runr-resume.md')
102
+ ];
103
+ const skillsPresent = fs.existsSync(skillPath);
104
+ const commandsPresent = commandPaths.every(p => fs.existsSync(p));
105
+ const missingFiles = [];
106
+ if (!skillsPresent)
107
+ missingFiles.push('.claude/skills/runr-workflow/SKILL.md');
108
+ commandPaths.forEach(p => {
109
+ if (!fs.existsSync(p)) {
110
+ missingFiles.push(path.relative(repoPath, p));
111
+ }
112
+ });
113
+ return {
114
+ skillsPresent,
115
+ commandsPresent,
116
+ missingFiles
117
+ };
118
+ }
119
+ /**
120
+ * Launch the meta-agent tool
121
+ */
122
+ async function launchTool(tool, repoPath, interactive) {
123
+ console.log(`\nLaunching ${tool === 'claude' ? 'Claude Code' : 'Codex CLI'} with Runr workflow...\n`);
124
+ if (tool === 'claude') {
125
+ console.log('Agent will follow rules from:');
126
+ console.log('- AGENTS.md (workflow guide)');
127
+ const claudeCheck = checkClaudeIntegration(repoPath);
128
+ if (claudeCheck.skillsPresent) {
129
+ console.log('- .claude/skills/runr-workflow (safety playbook)');
130
+ }
131
+ if (claudeCheck.commandsPresent) {
132
+ console.log('- .claude/commands/ (runr shortcuts)');
133
+ }
134
+ }
135
+ else {
136
+ console.log('Agent will follow rules from:');
137
+ console.log('- AGENTS.md (workflow guide, read by Codex)');
138
+ }
139
+ if (!interactive && tool === 'claude') {
140
+ console.log('\nPermission mode: skip all permissions (allows Bash, Edit, etc.)');
141
+ }
142
+ console.log('Exit with Ctrl+C\n');
143
+ console.log('─'.repeat(60));
144
+ console.log();
145
+ // Launch the tool in interactive mode
146
+ try {
147
+ // Build args based on tool
148
+ const args = [];
149
+ if (tool === 'claude' && !interactive) {
150
+ // Use dangerously-skip-permissions to bypass all confirmation dialogs
151
+ // This matches what Runr uses when running Claude as a worker
152
+ // Allows the agent to use Bash (for runr commands), Edit, and other tools
153
+ args.push('--dangerously-skip-permissions');
154
+ }
155
+ await execa(tool, args, {
156
+ cwd: repoPath,
157
+ stdio: 'inherit'
158
+ });
159
+ }
160
+ catch (error) {
161
+ // User likely pressed Ctrl+C
162
+ console.log('\nMeta-agent session ended.');
163
+ }
164
+ }
165
+ /**
166
+ * Main meta command implementation
167
+ */
168
+ export async function metaCommand(options) {
169
+ const repoPath = path.resolve(options.repo || '.');
170
+ // Step 1: Detect tool
171
+ let tool;
172
+ if (options.tool === 'auto' || !options.tool) {
173
+ const detected = await detectTool();
174
+ if (!detected) {
175
+ console.error('❌ No meta-agent tool found\n');
176
+ console.error('Install one of:');
177
+ console.error(' • Claude Code: https://code.claude.com');
178
+ console.error(' • Codex CLI: https://github.com/openai/codex-cli');
179
+ process.exit(2);
180
+ }
181
+ tool = detected;
182
+ }
183
+ else {
184
+ tool = options.tool;
185
+ // Verify the requested tool is available
186
+ const detected = await detectTool();
187
+ if (detected !== tool) {
188
+ console.error(`❌ Requested tool "${tool}" not found`);
189
+ console.error('Run with --tool auto to detect available tools');
190
+ process.exit(2);
191
+ }
192
+ }
193
+ // Step 2: Check working tree
194
+ try {
195
+ const treeCheck = await checkWorkingTree(repoPath);
196
+ if (!treeCheck.clean) {
197
+ if (options.allowDirty) {
198
+ console.log('⚠️ WARNING: Working tree has uncommitted changes\n');
199
+ console.log('Running agents on uncommitted work risks data loss.');
200
+ console.log('You used --allow-dirty to override this safety check.\n');
201
+ }
202
+ else {
203
+ console.error('⛔ BLOCKED: Working tree has uncommitted changes\n');
204
+ console.error('Running agents on uncommitted work risks data loss.\n');
205
+ console.error('Fix with:');
206
+ console.error(' git commit -am "WIP: save before agent"');
207
+ console.error(' # OR');
208
+ console.error(' git stash\n');
209
+ console.error('To override (not recommended):');
210
+ console.error(' runr meta --allow-dirty');
211
+ process.exit(1);
212
+ }
213
+ }
214
+ }
215
+ catch (error) {
216
+ console.error(`❌ Failed to check working tree: ${error.message}`);
217
+ process.exit(1);
218
+ }
219
+ // Step 3: Check repo setup
220
+ const setup = checkRepoSetup(repoPath);
221
+ if (setup.missingSetup.length > 0) {
222
+ console.error('⚠️ Incomplete Runr setup\n');
223
+ console.error('Missing:');
224
+ setup.missingSetup.forEach(item => console.error(` • ${item}`));
225
+ console.error('\nFix with:');
226
+ console.error(' runr init --pack solo');
227
+ console.error();
228
+ process.exit(1);
229
+ }
230
+ // Step 4: Claude-specific checks
231
+ if (tool === 'claude') {
232
+ const claudeCheck = checkClaudeIntegration(repoPath);
233
+ if (!claudeCheck.skillsPresent || !claudeCheck.commandsPresent) {
234
+ console.log('💡 Tip: Claude Code integration incomplete\n');
235
+ console.log('Missing:');
236
+ claudeCheck.missingFiles.forEach(file => console.log(` • ${file}`));
237
+ console.log('\nFor better integration, run:');
238
+ console.log(' runr init --pack solo --with-claude\n');
239
+ console.log('(This will add Claude Code skills and commands)\n');
240
+ // Don't block, just inform
241
+ }
242
+ }
243
+ // Step 5: Launch tool
244
+ await launchTool(tool, repoPath, options.interactive || false);
245
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * runr mode - View or set workflow mode
3
+ *
4
+ * Mode determines how strictly Runr enforces audit trail requirements:
5
+ * - flow: Productivity-first, interventions allowed freely
6
+ * - ledger: Audit-first, stricter controls on interventions
7
+ *
8
+ * Usage:
9
+ * runr mode # Show current mode
10
+ * runr mode flow # Set mode to flow
11
+ * runr mode ledger # Set mode to ledger
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { loadConfig, resolveConfigPath } from '../config/load.js';
16
+ const VALID_MODES = ['flow', 'ledger'];
17
+ /**
18
+ * Get current mode from config.
19
+ */
20
+ export function getCurrentMode(repoPath) {
21
+ try {
22
+ const configPath = resolveConfigPath(repoPath);
23
+ if (!fs.existsSync(configPath)) {
24
+ return 'flow';
25
+ }
26
+ const config = loadConfig(configPath);
27
+ return config.workflow?.mode || 'flow';
28
+ }
29
+ catch {
30
+ return 'flow';
31
+ }
32
+ }
33
+ /**
34
+ * Print mode banner (called at start of commands).
35
+ */
36
+ export function printModeBanner(repoPath) {
37
+ const mode = getCurrentMode(repoPath);
38
+ const version = '0.7.0'; // TODO: Read from package.json
39
+ console.log(`Runr v${version} | Mode: ${mode}`);
40
+ }
41
+ /**
42
+ * Check if a mode-restricted operation is allowed.
43
+ */
44
+ export function checkModeRestriction(repoPath, operation, forceOverride) {
45
+ const mode = getCurrentMode(repoPath);
46
+ if (forceOverride) {
47
+ return { allowed: true };
48
+ }
49
+ if (mode === 'ledger') {
50
+ switch (operation) {
51
+ case 'amend_last':
52
+ return {
53
+ allowed: false,
54
+ error: `Error: --amend-last is not allowed in Ledger mode.
55
+ In Ledger mode, use explicit commits:
56
+ runr intervene <run_id> --commit "message" --reason <reason>
57
+ Or switch to Flow mode with: runr config mode flow`
58
+ };
59
+ }
60
+ }
61
+ return { allowed: true };
62
+ }
63
+ /**
64
+ * Set mode in config file.
65
+ */
66
+ function setMode(repoPath, newMode) {
67
+ const configPath = resolveConfigPath(repoPath);
68
+ const configExists = fs.existsSync(configPath);
69
+ if (!configExists) {
70
+ // Create new config
71
+ const dir = path.dirname(configPath);
72
+ if (!fs.existsSync(dir)) {
73
+ fs.mkdirSync(dir, { recursive: true });
74
+ }
75
+ const newConfig = {
76
+ workflow: {
77
+ mode: newMode,
78
+ profile: 'solo',
79
+ integration_branch: 'dev',
80
+ release_branch: 'main'
81
+ }
82
+ };
83
+ fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
84
+ return;
85
+ }
86
+ // Update existing config
87
+ try {
88
+ const content = fs.readFileSync(configPath, 'utf-8');
89
+ const config = JSON.parse(content);
90
+ if (!config.workflow) {
91
+ config.workflow = {};
92
+ }
93
+ config.workflow.mode = newMode;
94
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
95
+ }
96
+ catch (err) {
97
+ throw new Error(`Failed to update config: ${err.message}`);
98
+ }
99
+ }
100
+ /**
101
+ * Mode command: View or set workflow mode.
102
+ */
103
+ export async function modeCommand(options) {
104
+ const { repo, newMode } = options;
105
+ if (newMode) {
106
+ // Validate mode
107
+ if (!VALID_MODES.includes(newMode)) {
108
+ console.error(`Error: Invalid mode '${newMode}'`);
109
+ console.error(`Valid modes: ${VALID_MODES.join(', ')}`);
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+ // Set mode
114
+ try {
115
+ setMode(repo, newMode);
116
+ console.log(`Mode set to: ${newMode}`);
117
+ if (newMode === 'ledger') {
118
+ console.log('');
119
+ console.log('Ledger mode restrictions:');
120
+ console.log(' - --amend-last is not allowed');
121
+ console.log(' - All merges should go through runr submit');
122
+ console.log(' - Higher audit coverage expectations');
123
+ }
124
+ else {
125
+ console.log('');
126
+ console.log('Flow mode enabled:');
127
+ console.log(' - Interventions allowed freely');
128
+ console.log(' - --amend-last allowed');
129
+ console.log(' - Flexible workflow for productivity');
130
+ }
131
+ }
132
+ catch (err) {
133
+ console.error(err.message);
134
+ process.exitCode = 1;
135
+ }
136
+ }
137
+ else {
138
+ // Show current mode
139
+ const mode = getCurrentMode(repo);
140
+ console.log(`Current mode: ${mode}`);
141
+ console.log('');
142
+ if (mode === 'flow') {
143
+ console.log('Flow mode (productivity-first):');
144
+ console.log(' - Interventions allowed freely');
145
+ console.log(' - --amend-last allowed');
146
+ console.log(' - Flexible workflow');
147
+ }
148
+ else {
149
+ console.log('Ledger mode (audit-first):');
150
+ console.log(' - --amend-last not allowed');
151
+ console.log(' - Strict audit trail');
152
+ console.log(' - All changes through runr submit');
153
+ }
154
+ console.log('');
155
+ console.log('To change mode: runr config mode <flow|ledger>');
156
+ }
157
+ }
@@ -798,3 +798,32 @@ function outputWaitResult(result, json, elapsedMs) {
798
798
  }
799
799
  }
800
800
  }
801
+ /**
802
+ * Generate and display orchestration receipt.
803
+ */
804
+ export async function receiptCommand(options) {
805
+ const { getReceipt, writeReceipt, generateReceiptMarkdown } = await import('../orchestrator/receipt.js');
806
+ const repoPath = path.resolve(options.repo);
807
+ // Get receipt (from cache or generate from state)
808
+ const receipt = getReceipt(repoPath, options.orchestratorId);
809
+ if (!receipt) {
810
+ console.error(`Orchestration not found: ${options.orchestratorId}`);
811
+ process.exitCode = 1;
812
+ return;
813
+ }
814
+ // Write artifacts if requested
815
+ if (options.write) {
816
+ const paths = writeReceipt(receipt, repoPath);
817
+ console.log(`Receipt written:`);
818
+ console.log(` JSON: ${paths.json}`);
819
+ console.log(` MD: ${paths.md}`);
820
+ console.log('');
821
+ }
822
+ // Output
823
+ if (options.json) {
824
+ console.log(JSON.stringify(receipt, null, 2));
825
+ }
826
+ else {
827
+ console.log(generateReceiptMarkdown(receipt));
828
+ }
829
+ }
@@ -0,0 +1,47 @@
1
+ import { loadAllPacks, getPacksDirectory } from '../packs/loader.js';
2
+ /**
3
+ * List available packs
4
+ */
5
+ export async function packsCommand(options = {}) {
6
+ const packs = loadAllPacks();
7
+ if (options.verbose) {
8
+ console.log(`Loading packs from: ${getPacksDirectory()}\n`);
9
+ }
10
+ if (packs.length === 0) {
11
+ console.log('No packs found.');
12
+ console.log('');
13
+ console.log('Packs are workflow presets that provide:');
14
+ console.log(' • Default configuration (branches, verification)');
15
+ console.log(' • Documentation templates (AGENTS.md, CLAUDE.md)');
16
+ console.log(' • Idempotent initialization actions');
17
+ return;
18
+ }
19
+ const validPacks = packs.filter(p => p.validation.valid);
20
+ const invalidPacks = packs.filter(p => !p.validation.valid);
21
+ console.log('Available workflow packs:\n');
22
+ // Display valid packs
23
+ for (const pack of validPacks) {
24
+ console.log(` \x1b[1m${pack.name}\x1b[0m`);
25
+ console.log(` ${pack.manifest.description}`);
26
+ console.log('');
27
+ }
28
+ if (validPacks.length > 0) {
29
+ console.log('Usage:');
30
+ console.log(` runr init --pack solo # Solo dev workflow (dev→main)`);
31
+ console.log(` runr init --pack pr # PR workflow (feature→main)`);
32
+ console.log(` runr init --pack trunk # Trunk-based (main only)`);
33
+ console.log(` runr init --pack solo --dry-run # Preview changes`);
34
+ console.log('');
35
+ }
36
+ // Display invalid packs
37
+ if (invalidPacks.length > 0) {
38
+ console.log('\x1b[33mInvalid packs:\x1b[0m\n');
39
+ for (const pack of invalidPacks) {
40
+ console.log(` ${pack.name}`);
41
+ for (const error of pack.validation.errors) {
42
+ console.log(` ❌ ${error}`);
43
+ }
44
+ console.log('');
45
+ }
46
+ }
47
+ }
@@ -72,12 +72,15 @@ export async function runPreflight(options) {
72
72
  }
73
73
  }
74
74
  // Check worker binaries exist (cheaper than ping, catches "command not found")
75
+ // Only check workers that are actually used by phases
75
76
  const workers = options.config.workers;
77
+ const phases = options.config.phases;
78
+ const usedWorkers = new Set([phases.plan, phases.implement, phases.review]);
76
79
  const binaryCheckPromises = [];
77
- if (workers.claude) {
80
+ if (workers.claude && usedWorkers.has('claude')) {
78
81
  binaryCheckPromises.push(checkWorkerBinary('claude', workers.claude));
79
82
  }
80
- if (workers.codex) {
83
+ if (workers.codex && usedWorkers.has('codex')) {
81
84
  binaryCheckPromises.push(checkWorkerBinary('codex', workers.codex));
82
85
  }
83
86
  const binaryResults = await Promise.all(binaryCheckPromises);
@@ -103,12 +106,12 @@ export async function runPreflight(options) {
103
106
  }
104
107
  else {
105
108
  const pingResults = [];
106
- // Ping all configured workers in parallel
109
+ // Ping all workers that are actually used by phases
107
110
  const pingPromises = [];
108
- if (workers.claude) {
111
+ if (workers.claude && usedWorkers.has('claude')) {
109
112
  pingPromises.push(pingClaude(workers.claude));
110
113
  }
111
- if (workers.codex) {
114
+ if (workers.codex && usedWorkers.has('codex')) {
112
115
  pingPromises.push(pingCodex(workers.codex));
113
116
  }
114
117
  const results = await Promise.all(pingPromises);