@weldr/runr 0.4.0 → 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 +127 -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
package/dist/commands/init.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import fsPromises from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { getWorkflowProfileDefaults } from '../config/schema.js';
|
|
5
|
+
import { loadPackByName } from '../packs/loader.js';
|
|
6
|
+
import { executeActions } from '../packs/actions.js';
|
|
7
|
+
import { formatVerificationCommands } from '../packs/renderer.js';
|
|
3
8
|
/**
|
|
4
9
|
* Detect Python project verification commands
|
|
5
10
|
*/
|
|
@@ -191,7 +196,7 @@ function generateDefaultConfig(repoPath) {
|
|
|
191
196
|
/**
|
|
192
197
|
* Build config object from detection results
|
|
193
198
|
*/
|
|
194
|
-
function buildConfig(repoPath, detection) {
|
|
199
|
+
function buildConfig(repoPath, detection, workflowProfile) {
|
|
195
200
|
const hasSrc = fs.existsSync(path.join(repoPath, 'src'));
|
|
196
201
|
const hasTests = fs.existsSync(path.join(repoPath, 'tests')) ||
|
|
197
202
|
fs.existsSync(path.join(repoPath, 'test'));
|
|
@@ -207,7 +212,7 @@ function buildConfig(repoPath, detection) {
|
|
|
207
212
|
// Default: allow everything except common excludes
|
|
208
213
|
allowlist.push('**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx');
|
|
209
214
|
}
|
|
210
|
-
|
|
215
|
+
const config = {
|
|
211
216
|
agent: {
|
|
212
217
|
name: path.basename(repoPath),
|
|
213
218
|
version: '1'
|
|
@@ -259,8 +264,18 @@ function buildConfig(repoPath, detection) {
|
|
|
259
264
|
auto_resume_delays_ms: [30000, 120000, 300000],
|
|
260
265
|
max_worker_call_minutes: 45,
|
|
261
266
|
max_review_rounds: 2
|
|
267
|
+
},
|
|
268
|
+
receipts: {
|
|
269
|
+
redact: true,
|
|
270
|
+
capture_cmd_output: 'truncated',
|
|
271
|
+
max_output_bytes: 10240
|
|
262
272
|
}
|
|
263
273
|
};
|
|
274
|
+
// Add workflow config if profile specified
|
|
275
|
+
if (workflowProfile) {
|
|
276
|
+
config.workflow = getWorkflowProfileDefaults(workflowProfile);
|
|
277
|
+
}
|
|
278
|
+
return config;
|
|
264
279
|
}
|
|
265
280
|
/**
|
|
266
281
|
* Create example task files
|
|
@@ -317,10 +332,207 @@ Update documentation for [topic/module]
|
|
|
317
332
|
fs.writeFileSync(path.join(tasksDir, 'example-feature.md'), exampleFeature);
|
|
318
333
|
fs.writeFileSync(path.join(tasksDir, 'example-docs.md'), exampleDocs);
|
|
319
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Ensure .gitignore contains the specified entry.
|
|
337
|
+
* Returns true if entry was added, false if already present.
|
|
338
|
+
*/
|
|
339
|
+
async function ensureGitignoreEntry(repoPath, entry) {
|
|
340
|
+
const gitignorePath = path.join(repoPath, '.gitignore');
|
|
341
|
+
let content = '';
|
|
342
|
+
try {
|
|
343
|
+
content = await fsPromises.readFile(gitignorePath, 'utf-8');
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// No .gitignore exists, will create one
|
|
347
|
+
}
|
|
348
|
+
// Check if entry already exists
|
|
349
|
+
const lines = content.split('\n');
|
|
350
|
+
const hasEntry = lines.some(line => line.trim() === entry.trim());
|
|
351
|
+
if (!hasEntry) {
|
|
352
|
+
const newContent = content.endsWith('\n') || content === ''
|
|
353
|
+
? `${content}${entry}\n`
|
|
354
|
+
: `${content}\n${entry}\n`;
|
|
355
|
+
await fsPromises.writeFile(gitignorePath, newContent);
|
|
356
|
+
return true; // Added
|
|
357
|
+
}
|
|
358
|
+
return false; // Already present
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Generate the demo project
|
|
362
|
+
*/
|
|
363
|
+
async function generateDemoProject(demoDir) {
|
|
364
|
+
fs.mkdirSync(demoDir, { recursive: true });
|
|
365
|
+
fs.mkdirSync(path.join(demoDir, 'src'), { recursive: true });
|
|
366
|
+
fs.mkdirSync(path.join(demoDir, 'tests'), { recursive: true });
|
|
367
|
+
fs.mkdirSync(path.join(demoDir, '.runr', 'tasks'), { recursive: true });
|
|
368
|
+
const packageJson = {
|
|
369
|
+
name: 'runr-demo',
|
|
370
|
+
version: '1.0.0',
|
|
371
|
+
type: 'module',
|
|
372
|
+
scripts: { test: 'vitest run', typecheck: 'tsc --noEmit' },
|
|
373
|
+
devDependencies: { typescript: '^5.0.0', vitest: '^1.0.0' }
|
|
374
|
+
};
|
|
375
|
+
fs.writeFileSync(path.join(demoDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
376
|
+
const tsconfig = {
|
|
377
|
+
compilerOptions: {
|
|
378
|
+
target: 'ES2022', module: 'ESNext', moduleResolution: 'node',
|
|
379
|
+
strict: true, esModuleInterop: true, skipLibCheck: true, outDir: 'dist'
|
|
380
|
+
},
|
|
381
|
+
include: ['src/**/*', 'tests/**/*']
|
|
382
|
+
};
|
|
383
|
+
fs.writeFileSync(path.join(demoDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n');
|
|
384
|
+
fs.writeFileSync(path.join(demoDir, 'src', 'math.ts'), `/**
|
|
385
|
+
* Simple math functions for demo
|
|
386
|
+
*/
|
|
387
|
+
export function add(a: number, b: number): number { return a + b; }
|
|
388
|
+
export function subtract(a: number, b: number): number { return a - b; }
|
|
389
|
+
// TODO: implement multiply
|
|
390
|
+
`);
|
|
391
|
+
fs.writeFileSync(path.join(demoDir, 'tests', 'math.test.ts'), `import { describe, it, expect } from 'vitest';
|
|
392
|
+
import { add, subtract } from '../src/math.js';
|
|
393
|
+
|
|
394
|
+
describe('math', () => {
|
|
395
|
+
it('adds two numbers', () => { expect(add(2, 3)).toBe(5); });
|
|
396
|
+
it('subtracts two numbers', () => { expect(subtract(5, 3)).toBe(2); });
|
|
397
|
+
});
|
|
398
|
+
`);
|
|
399
|
+
const runrConfig = {
|
|
400
|
+
agent: { name: 'runr-demo', version: '1' },
|
|
401
|
+
scope: {
|
|
402
|
+
allowlist: ['src/**', 'tests/**', '.runr/**'],
|
|
403
|
+
denylist: ['node_modules/**', 'README.md'],
|
|
404
|
+
lockfiles: ['package-lock.json'],
|
|
405
|
+
presets: ['typescript', 'vitest']
|
|
406
|
+
},
|
|
407
|
+
verification: { tier0: ['npm run typecheck'], tier1: ['npm test'] },
|
|
408
|
+
workers: { claude: { bin: 'claude', args: ['-p', '--output-format', 'json', '--dangerously-skip-permissions'], output: 'json' } },
|
|
409
|
+
phases: { plan: 'claude', implement: 'claude', review: 'claude' },
|
|
410
|
+
workflow: { profile: 'solo', mode: 'flow', integration_branch: 'main', require_verification: true, require_clean_tree: true }
|
|
411
|
+
};
|
|
412
|
+
fs.writeFileSync(path.join(demoDir, '.runr', 'runr.config.json'), JSON.stringify(runrConfig, null, 2) + '\n');
|
|
413
|
+
fs.writeFileSync(path.join(demoDir, '.runr', 'tasks', '00-success.md'), `# Implement multiply function
|
|
414
|
+
|
|
415
|
+
## Goal
|
|
416
|
+
Add a multiply function to src/math.ts and add a test for it.
|
|
417
|
+
|
|
418
|
+
## Requirements
|
|
419
|
+
- Add \`multiply(a: number, b: number): number\` to src/math.ts
|
|
420
|
+
- Add a test in tests/math.test.ts
|
|
421
|
+
|
|
422
|
+
## Success Criteria
|
|
423
|
+
- npm run typecheck passes
|
|
424
|
+
- npm test passes
|
|
425
|
+
`);
|
|
426
|
+
fs.writeFileSync(path.join(demoDir, '.runr', 'tasks', '01-intentional-fail.md'), `# Add divide function with edge case
|
|
427
|
+
|
|
428
|
+
## Goal
|
|
429
|
+
Add a divide function with tests including edge cases.
|
|
430
|
+
|
|
431
|
+
## Requirements
|
|
432
|
+
- Add \`divide(a: number, b: number): number\` to src/math.ts
|
|
433
|
+
- Add tests including divide(10, 2) === 5 and divide(10, 0) === Infinity
|
|
434
|
+
|
|
435
|
+
## Success Criteria
|
|
436
|
+
- npm run typecheck passes
|
|
437
|
+
- npm test passes
|
|
438
|
+
`);
|
|
439
|
+
fs.writeFileSync(path.join(demoDir, '.runr', 'tasks', '02-scope-violation.md'), `# Update README with project description
|
|
440
|
+
|
|
441
|
+
## Goal
|
|
442
|
+
Update README.md to describe this math library.
|
|
443
|
+
|
|
444
|
+
## Requirements
|
|
445
|
+
- Add a description of the available functions
|
|
446
|
+
- Add usage examples
|
|
447
|
+
|
|
448
|
+
## Success Criteria
|
|
449
|
+
- README.md contains function documentation
|
|
450
|
+
|
|
451
|
+
## Note
|
|
452
|
+
This task will trigger a scope violation because README.md is in the denylist.
|
|
453
|
+
`);
|
|
454
|
+
fs.writeFileSync(path.join(demoDir, 'README.md'), `# Runr Demo
|
|
455
|
+
|
|
456
|
+
Try Runr in 2 minutes.
|
|
457
|
+
|
|
458
|
+
## Step 1: Install
|
|
459
|
+
|
|
460
|
+
\`\`\`bash
|
|
461
|
+
npm install
|
|
462
|
+
\`\`\`
|
|
463
|
+
|
|
464
|
+
## Step 2: Run the tasks
|
|
465
|
+
|
|
466
|
+
### Task 00: Success (quick win)
|
|
467
|
+
|
|
468
|
+
\`\`\`bash
|
|
469
|
+
runr run --task .runr/tasks/00-success.md
|
|
470
|
+
\`\`\`
|
|
471
|
+
|
|
472
|
+
**Expected:** Completes cleanly. The agent adds a multiply function and test.
|
|
473
|
+
|
|
474
|
+
\`\`\`bash
|
|
475
|
+
runr report latest # see what happened
|
|
476
|
+
\`\`\`
|
|
477
|
+
|
|
478
|
+
### Task 01: Failure + Recovery
|
|
479
|
+
|
|
480
|
+
\`\`\`bash
|
|
481
|
+
runr run --task .runr/tasks/01-intentional-fail.md
|
|
482
|
+
\`\`\`
|
|
483
|
+
|
|
484
|
+
**Expected:** May stop (verification failed or review loop). This is intentional.
|
|
485
|
+
|
|
486
|
+
\`\`\`bash
|
|
487
|
+
runr # shows STOPPED + 3 next actions
|
|
488
|
+
runr continue # attempt auto-fix
|
|
489
|
+
runr report latest
|
|
490
|
+
\`\`\`
|
|
491
|
+
|
|
492
|
+
### Task 02: Scope Guard
|
|
493
|
+
|
|
494
|
+
\`\`\`bash
|
|
495
|
+
runr run --task .runr/tasks/02-scope-violation.md
|
|
496
|
+
\`\`\`
|
|
497
|
+
|
|
498
|
+
**Expected:** STOPPED (scope guard). README.md is in the denylist.
|
|
499
|
+
|
|
500
|
+
This demonstrates the safety guardrails.
|
|
501
|
+
|
|
502
|
+
## The point
|
|
503
|
+
|
|
504
|
+
Runr stops with receipts and 3 next actions you can trust:
|
|
505
|
+
- **continue** — auto-fix what's safe, then resume
|
|
506
|
+
- **report** — open the run receipt: diffs + logs + timeline
|
|
507
|
+
- **intervene** — record manual fixes
|
|
508
|
+
`);
|
|
509
|
+
fs.writeFileSync(path.join(demoDir, '.gitignore'), 'node_modules/\ndist/\n.runr/runs/\n');
|
|
510
|
+
}
|
|
320
511
|
/**
|
|
321
512
|
* Initialize Runr configuration for a repository
|
|
322
513
|
*/
|
|
323
514
|
export async function initCommand(options) {
|
|
515
|
+
// Handle --demo flag
|
|
516
|
+
if (options.demo) {
|
|
517
|
+
const demoDir = path.resolve(options.demoDir || 'runr-demo');
|
|
518
|
+
if (fs.existsSync(demoDir)) {
|
|
519
|
+
if (!options.force) {
|
|
520
|
+
console.error(`Error: ${demoDir} already exists. Use --force to overwrite.`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
fs.rmSync(demoDir, { recursive: true, force: true });
|
|
524
|
+
}
|
|
525
|
+
console.log('Creating demo project...\n');
|
|
526
|
+
await generateDemoProject(demoDir);
|
|
527
|
+
console.log(`✅ Demo created at ${demoDir}\n`);
|
|
528
|
+
console.log('Next steps:');
|
|
529
|
+
console.log(` cd ${path.basename(demoDir)}`);
|
|
530
|
+
console.log(' npm install');
|
|
531
|
+
console.log(' runr run --task .runr/tasks/00-success.md');
|
|
532
|
+
console.log('');
|
|
533
|
+
console.log('See README.md in the demo for the full walkthrough.');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
324
536
|
const repoPath = path.resolve(options.repo);
|
|
325
537
|
const runrDir = path.join(repoPath, '.runr');
|
|
326
538
|
const configPath = path.join(runrDir, 'runr.config.json');
|
|
@@ -333,32 +545,180 @@ export async function initCommand(options) {
|
|
|
333
545
|
process.exit(0);
|
|
334
546
|
}
|
|
335
547
|
// Check if config already exists
|
|
336
|
-
if (fs.existsSync(configPath) && !options.force) {
|
|
548
|
+
if (fs.existsSync(configPath) && !options.force && !options.dryRun) {
|
|
337
549
|
console.error('Error: .runr/runr.config.json already exists');
|
|
338
550
|
console.error('Use --force to overwrite');
|
|
339
551
|
process.exit(1);
|
|
340
552
|
}
|
|
553
|
+
// Load pack if specified
|
|
554
|
+
let pack = null;
|
|
555
|
+
if (options.pack) {
|
|
556
|
+
pack = loadPackByName(options.pack);
|
|
557
|
+
if (!pack) {
|
|
558
|
+
console.error(`Error: Pack "${options.pack}" not found`);
|
|
559
|
+
console.error('Run "runr tools packs" to see available packs');
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
if (!pack.validation.valid) {
|
|
563
|
+
console.error(`Error: Pack "${options.pack}" is invalid:`);
|
|
564
|
+
for (const error of pack.validation.errors) {
|
|
565
|
+
console.error(` - ${error}`);
|
|
566
|
+
}
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
341
570
|
// Detect verification commands - try Python first, then package.json, then default
|
|
342
571
|
const detection = detectPythonVerification(repoPath) ||
|
|
343
572
|
detectFromPackageJson(repoPath) ||
|
|
344
573
|
generateDefaultConfig(repoPath);
|
|
574
|
+
// Determine workflow profile (pack defaults override --workflow flag)
|
|
575
|
+
let workflowProfile = options.workflow;
|
|
576
|
+
if (pack?.manifest.defaults?.profile) {
|
|
577
|
+
workflowProfile = pack.manifest.defaults.profile;
|
|
578
|
+
}
|
|
345
579
|
// Build config
|
|
346
|
-
const config = buildConfig(repoPath, detection);
|
|
580
|
+
const config = buildConfig(repoPath, detection, workflowProfile);
|
|
581
|
+
// Apply pack defaults to config if pack is loaded
|
|
582
|
+
if (pack?.manifest.defaults) {
|
|
583
|
+
const packDefaults = pack.manifest.defaults;
|
|
584
|
+
// Ensure workflow config exists
|
|
585
|
+
if (!config.workflow) {
|
|
586
|
+
config.workflow = {
|
|
587
|
+
profile: packDefaults.profile || 'solo',
|
|
588
|
+
mode: packDefaults.mode || 'flow',
|
|
589
|
+
integration_branch: packDefaults.integration_branch || 'main',
|
|
590
|
+
submit_strategy: 'cherry-pick',
|
|
591
|
+
require_clean_tree: packDefaults.require_clean_tree ?? true,
|
|
592
|
+
require_verification: packDefaults.require_verification ?? true
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Apply pack defaults to existing workflow config
|
|
597
|
+
if (packDefaults.profile)
|
|
598
|
+
config.workflow.profile = packDefaults.profile;
|
|
599
|
+
if (packDefaults.integration_branch)
|
|
600
|
+
config.workflow.integration_branch = packDefaults.integration_branch;
|
|
601
|
+
if (packDefaults.submit_strategy)
|
|
602
|
+
config.workflow.submit_strategy = packDefaults.submit_strategy;
|
|
603
|
+
if (packDefaults.require_clean_tree !== undefined)
|
|
604
|
+
config.workflow.require_clean_tree = packDefaults.require_clean_tree;
|
|
605
|
+
if (packDefaults.require_verification !== undefined)
|
|
606
|
+
config.workflow.require_verification = packDefaults.require_verification;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
347
609
|
// If --print mode, just output and exit
|
|
348
610
|
if (options.print) {
|
|
349
611
|
console.log(JSON.stringify(config, null, 2));
|
|
350
612
|
return;
|
|
351
613
|
}
|
|
614
|
+
// Handle --dry-run mode (pack actions only)
|
|
615
|
+
if (options.dryRun && pack) {
|
|
616
|
+
console.log('[DRY RUN] Pack-based initialization plan:\n');
|
|
617
|
+
console.log(`Pack: ${pack.manifest.display_name}`);
|
|
618
|
+
console.log(`Description: ${pack.manifest.description}\n`);
|
|
619
|
+
console.log('Config changes:');
|
|
620
|
+
console.log(` - Workflow profile: ${config.workflow?.profile || 'none'}`);
|
|
621
|
+
console.log(` - Integration branch: ${config.workflow?.integration_branch || 'none'}`);
|
|
622
|
+
console.log(` - Require verification: ${config.workflow?.require_verification}`);
|
|
623
|
+
console.log(` - Require clean tree: ${config.workflow?.require_clean_tree}\n`);
|
|
624
|
+
if (pack.manifest.init_actions && pack.manifest.init_actions.length > 0) {
|
|
625
|
+
console.log('Init actions:');
|
|
626
|
+
const templateContext = {
|
|
627
|
+
project_name: options.about || path.basename(repoPath),
|
|
628
|
+
project_about: options.about || `Project: ${path.basename(repoPath)}`,
|
|
629
|
+
verification_commands: formatVerificationCommands(config.verification),
|
|
630
|
+
integration_branch: config.workflow?.integration_branch || 'main',
|
|
631
|
+
release_branch: pack.manifest.defaults?.release_branch || 'main',
|
|
632
|
+
pack_name: pack.manifest.name
|
|
633
|
+
};
|
|
634
|
+
const actionContext = {
|
|
635
|
+
repoPath,
|
|
636
|
+
packDir: pack.packDir,
|
|
637
|
+
templates: pack.manifest.templates || {},
|
|
638
|
+
templateContext,
|
|
639
|
+
flags: {
|
|
640
|
+
with_claude: options.withClaude || false
|
|
641
|
+
},
|
|
642
|
+
dryRun: true
|
|
643
|
+
};
|
|
644
|
+
const results = await executeActions(pack.manifest.init_actions, actionContext);
|
|
645
|
+
for (const result of results) {
|
|
646
|
+
console.log(` ${result.message}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
352
651
|
// Create .runr directory
|
|
353
|
-
|
|
652
|
+
if (!options.dryRun) {
|
|
653
|
+
fs.mkdirSync(runrDir, { recursive: true });
|
|
654
|
+
}
|
|
655
|
+
// Execute pack actions if pack is loaded
|
|
656
|
+
if (pack?.manifest.init_actions) {
|
|
657
|
+
const templateContext = {
|
|
658
|
+
project_name: options.about || path.basename(repoPath),
|
|
659
|
+
project_about: options.about || `Project: ${path.basename(repoPath)}`,
|
|
660
|
+
verification_commands: formatVerificationCommands(config.verification),
|
|
661
|
+
integration_branch: config.workflow?.integration_branch || 'main',
|
|
662
|
+
release_branch: pack.manifest.defaults?.release_branch || 'main',
|
|
663
|
+
pack_name: pack.manifest.name
|
|
664
|
+
};
|
|
665
|
+
const actionContext = {
|
|
666
|
+
repoPath,
|
|
667
|
+
packDir: pack.packDir,
|
|
668
|
+
templates: pack.manifest.templates || {},
|
|
669
|
+
templateContext,
|
|
670
|
+
flags: {
|
|
671
|
+
with_claude: options.withClaude || false
|
|
672
|
+
},
|
|
673
|
+
dryRun: false
|
|
674
|
+
};
|
|
675
|
+
const results = await executeActions(pack.manifest.init_actions, actionContext);
|
|
676
|
+
for (const result of results) {
|
|
677
|
+
if (result.error) {
|
|
678
|
+
console.log(`❌ ${result.message}: ${result.error}`);
|
|
679
|
+
}
|
|
680
|
+
else if (result.executed) {
|
|
681
|
+
console.log(`✅ ${result.message}`);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
console.log(`✓ ${result.message}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
// Legacy path: ensure .runr/ is gitignored if no pack actions
|
|
690
|
+
const added = await ensureGitignoreEntry(repoPath, '.runr/');
|
|
691
|
+
if (added) {
|
|
692
|
+
console.log('✅ Added .runr/ to .gitignore');
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
console.log('✓ .runr/ already in .gitignore');
|
|
696
|
+
}
|
|
697
|
+
console.log('');
|
|
698
|
+
console.log('💡 Tip: runr init --pack solo --dry-run to preview workflow scaffolding');
|
|
699
|
+
console.log('');
|
|
700
|
+
}
|
|
354
701
|
// Write config
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
702
|
+
if (!options.dryRun) {
|
|
703
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
704
|
+
}
|
|
705
|
+
// Create example tasks (only if no pack is loaded)
|
|
706
|
+
if (!pack && !options.dryRun) {
|
|
707
|
+
createExampleTasks(runrDir);
|
|
708
|
+
}
|
|
358
709
|
// Report results
|
|
359
710
|
console.log('✅ Runr initialized successfully!\n');
|
|
360
711
|
console.log(`Config written to: ${configPath}`);
|
|
361
712
|
console.log(`Example tasks created in: ${path.join(runrDir, 'tasks')}/\n`);
|
|
713
|
+
// Report workflow config if set
|
|
714
|
+
if (options.workflow && config.workflow) {
|
|
715
|
+
console.log('Workflow configuration:');
|
|
716
|
+
console.log(` profile: ${config.workflow.profile}`);
|
|
717
|
+
console.log(` integration_branch: ${config.workflow.integration_branch}`);
|
|
718
|
+
console.log(` require_verification: ${config.workflow.require_verification}`);
|
|
719
|
+
console.log(` require_clean_tree: ${config.workflow.require_clean_tree}`);
|
|
720
|
+
console.log('');
|
|
721
|
+
}
|
|
362
722
|
if (detection.source === 'package.json') {
|
|
363
723
|
console.log('Detected from package.json:');
|
|
364
724
|
if (detection.verification.tier0.length > 0) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runr intervene - Record manual work done outside Runr's normal flow.
|
|
3
|
+
*
|
|
4
|
+
* When the meta-agent (or you) routes around friction, this command
|
|
5
|
+
* creates a structured intervention receipt so the audit trail stays intact.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* runr intervene <run_id> --reason review_loop --note "Fixed TS errors" --cmd "npm test"
|
|
9
|
+
* runr intervene --latest --reason manual_fix --note "Added missing import"
|
|
10
|
+
*/
|
|
11
|
+
import { RunStore } from '../store/run-store.js';
|
|
12
|
+
import { resolveRunId } from '../store/run-utils.js';
|
|
13
|
+
import { writeIntervention, printInterventionReceipt } from '../receipt/intervention.js';
|
|
14
|
+
import { getCurrentMode, checkModeRestriction } from './mode.js';
|
|
15
|
+
import { checkAmendAllowed } from '../guards/checkpoint.js';
|
|
16
|
+
const VALID_REASONS = [
|
|
17
|
+
'review_loop',
|
|
18
|
+
'stalled_timeout',
|
|
19
|
+
'verification_failed',
|
|
20
|
+
'scope_violation',
|
|
21
|
+
'manual_fix',
|
|
22
|
+
'other'
|
|
23
|
+
];
|
|
24
|
+
export async function interveneCommand(options) {
|
|
25
|
+
const { repo, reason, note, commands, json, cmdOutput, noRedact, since, commit, amendLast, stageOnly, force } = options;
|
|
26
|
+
// Get workflow mode for error messages
|
|
27
|
+
const workflowMode = getCurrentMode(repo);
|
|
28
|
+
const isLedgerMode = workflowMode === 'ledger';
|
|
29
|
+
// Check if HEAD is a checkpoint commit (blocks amend even in Flow mode)
|
|
30
|
+
if (amendLast) {
|
|
31
|
+
const checkpointCheck = checkAmendAllowed(repo, force, isLedgerMode);
|
|
32
|
+
if (!checkpointCheck.allowed) {
|
|
33
|
+
console.error(checkpointCheck.error);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Print warning if force was used on checkpoint
|
|
38
|
+
if (checkpointCheck.error && force) {
|
|
39
|
+
console.error(checkpointCheck.error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Check mode restrictions for --amend-last (Ledger mode blocks all amends)
|
|
43
|
+
if (amendLast) {
|
|
44
|
+
const check = checkModeRestriction(repo, 'amend_last', force);
|
|
45
|
+
if (!check.allowed) {
|
|
46
|
+
console.error(check.error);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Validate reason
|
|
52
|
+
if (!VALID_REASONS.includes(reason)) {
|
|
53
|
+
console.error(`Error: Invalid reason '${reason}'`);
|
|
54
|
+
console.error(`Valid reasons: ${VALID_REASONS.join(', ')}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Resolve run ID (supports 'latest')
|
|
59
|
+
let runId;
|
|
60
|
+
try {
|
|
61
|
+
runId = resolveRunId(options.runId, repo);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(err.message);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Get run store
|
|
69
|
+
const runStore = RunStore.init(runId, repo);
|
|
70
|
+
// Write intervention receipt
|
|
71
|
+
try {
|
|
72
|
+
const result = await writeIntervention({
|
|
73
|
+
runStorePath: runStore.path,
|
|
74
|
+
repoPath: repo,
|
|
75
|
+
runId,
|
|
76
|
+
reason,
|
|
77
|
+
note,
|
|
78
|
+
commands,
|
|
79
|
+
captureMode: cmdOutput,
|
|
80
|
+
redactSecrets: !noRedact,
|
|
81
|
+
sinceSha: since,
|
|
82
|
+
commitMessage: commit,
|
|
83
|
+
amendLast,
|
|
84
|
+
stageOnly,
|
|
85
|
+
workflowMode,
|
|
86
|
+
forceAmend: force
|
|
87
|
+
});
|
|
88
|
+
if (json) {
|
|
89
|
+
// JSON output for programmatic use
|
|
90
|
+
console.log(JSON.stringify({
|
|
91
|
+
success: true,
|
|
92
|
+
run_id: runId,
|
|
93
|
+
receipt_path: result.receiptPath,
|
|
94
|
+
trailers: result.trailers,
|
|
95
|
+
commands_run: result.receipt.commands.length,
|
|
96
|
+
all_passed: result.receipt.commands.every(c => c.exit_code === 0),
|
|
97
|
+
files_changed: result.receipt.files_changed.length
|
|
98
|
+
}, null, 2));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Human-readable output
|
|
102
|
+
printInterventionReceipt(result);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`Error writing intervention: ${err.message}`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
}
|