@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +127 -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
@@ -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
- return {
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
- fs.mkdirSync(runrDir, { recursive: true });
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
356
- // Create example tasks
357
- createExampleTasks(runrDir);
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
+ }