@fractary/faber-cli 1.5.30 → 1.5.32

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.
@@ -44,4 +44,12 @@ export declare function createWorkflowInspectCommand(): Command;
44
44
  * Create the workflow-debug command
45
45
  */
46
46
  export declare function createWorkflowDebugCommand(): Command;
47
+ /**
48
+ * Create the workflow-batch-plan command
49
+ */
50
+ export declare function createBatchPlanCommand(): Command;
51
+ /**
52
+ * Create the workflow-batch-run command
53
+ */
54
+ export declare function createBatchRunCommand(): Command;
47
55
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/workflow/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAkD1C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAiF7C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAyB7C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAoB5C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CA2B9C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CA8B9C;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAoCrD;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CA4BrD;AAED;;GAEG;AACH,wBAAgB,4BAA4B,IAAI,OAAO,CA0CtD;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAkDpD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/workflow/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAkJ1C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAiF7C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAyB7C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAoB5C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CA2B9C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CA8B9C;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAoCrD;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CA4BrD;AAED;;GAEG;AACH,wBAAgB,4BAA4B,IAAI,OAAO,CA0CtD;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAkDpD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAchD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAc/C"}
@@ -12,46 +12,134 @@ import { parsePositiveInteger } from '../../utils/validation.js';
12
12
  */
13
13
  export function createRunCommand() {
14
14
  return new Command('workflow-run')
15
- .description('Run FABER workflow')
16
- .requiredOption('--work-id <id>', 'Work item ID to process')
15
+ .description('Run FABER workflow (supports comma-separated work IDs for batch execution)')
16
+ .requiredOption('--work-id <ids>', 'Work item ID(s) to process (comma-separated for batch, e.g., "258,259,260")')
17
17
  .option('--autonomy <level>', 'Autonomy level: supervised|assisted|autonomous', 'supervised')
18
+ .option('--phase <phases>', 'Execute only specified phase(s) - comma-separated')
19
+ .option('--force-new', 'Force fresh start, bypass auto-resume')
20
+ .option('--resume-batch', 'Resume a previously interrupted batch')
18
21
  .option('--json', 'Output as JSON')
19
22
  .action(async (options) => {
20
23
  try {
21
- const workflow = new FaberWorkflow();
22
- if (!options.json) {
23
- console.log(chalk.blue(`Starting FABER workflow for work item #${options.workId}`));
24
- console.log(chalk.gray(`Autonomy: ${options.autonomy}`));
25
- }
26
- // Add event listener for progress updates
27
- workflow.addEventListener((event, data) => {
28
- if (options.json)
29
- return;
30
- switch (event) {
31
- case 'phase:start':
32
- console.log(chalk.cyan(`\n→ Starting phase: ${String(data.phase || '').toUpperCase()}`));
33
- break;
34
- case 'phase:complete':
35
- console.log(chalk.green(` ✓ Completed phase: ${data.phase}`));
36
- break;
37
- case 'workflow:fail':
38
- case 'phase:fail':
39
- console.error(chalk.red(` ✗ Error: ${data.error || 'Unknown error'}`));
40
- break;
24
+ const workIds = options.workId.split(',').map((id) => id.trim()).filter(Boolean);
25
+ const isBatch = workIds.length > 1;
26
+ if (isBatch) {
27
+ // Batch mode: sequential execution
28
+ if (!options.json) {
29
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
30
+ console.log(chalk.blue.bold(' BATCH MODE: Sequential Multi-Workflow Execution'));
31
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
32
+ console.log(chalk.gray(`Work IDs: ${workIds.join(', ')}`));
33
+ console.log(chalk.gray(`Autonomy: ${options.autonomy}`));
34
+ if (options.phase)
35
+ console.log(chalk.gray(`Phases: ${options.phase}`));
36
+ console.log(chalk.gray(`Total workflows: ${workIds.length}`));
37
+ console.log('');
38
+ }
39
+ const results = [];
40
+ for (let i = 0; i < workIds.length; i++) {
41
+ const workId = workIds[i];
42
+ if (!options.json) {
43
+ console.log(chalk.blue(`\n═══ Workflow ${i + 1}/${workIds.length}: #${workId} ═══`));
44
+ }
45
+ try {
46
+ const workflow = new FaberWorkflow();
47
+ workflow.addEventListener((event, data) => {
48
+ if (options.json)
49
+ return;
50
+ switch (event) {
51
+ case 'phase:start':
52
+ console.log(chalk.cyan(`\n→ Starting phase: ${String(data.phase || '').toUpperCase()}`));
53
+ break;
54
+ case 'phase:complete':
55
+ console.log(chalk.green(` ✓ Completed phase: ${data.phase}`));
56
+ break;
57
+ case 'workflow:fail':
58
+ case 'phase:fail':
59
+ console.error(chalk.red(` ✗ Error: ${data.error || 'Unknown error'}`));
60
+ break;
61
+ }
62
+ });
63
+ const result = await workflow.run({
64
+ workId,
65
+ autonomy: options.autonomy,
66
+ });
67
+ results.push({ workId, status: result.status });
68
+ if (!options.json) {
69
+ console.log(chalk.green(`✓ Workflow ${i + 1}/${workIds.length} completed: #${workId}`));
70
+ }
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ results.push({ workId, status: 'failed', error: message });
75
+ if (!options.json) {
76
+ console.error(chalk.red(`✗ Workflow ${i + 1}/${workIds.length} FAILED: #${workId}`));
77
+ console.error(chalk.red(` Error: ${message}`));
78
+ console.error(chalk.yellow('\nBatch stopped due to error. Remaining workflows not executed.'));
79
+ }
80
+ break; // Stop batch on error
81
+ }
82
+ }
83
+ // Batch summary
84
+ if (options.json) {
85
+ console.log(JSON.stringify({ status: 'success', data: { batch: true, results } }, null, 2));
86
+ }
87
+ else {
88
+ const completed = results.filter(r => r.status !== 'failed');
89
+ const failed = results.filter(r => r.status === 'failed');
90
+ console.log(chalk.blue.bold('\n═══════════════════════════════════════════════'));
91
+ console.log(chalk.blue.bold(' BATCH COMPLETE'));
92
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
93
+ console.log(`Total: ${workIds.length} | Completed: ${completed.length} | Failed: ${failed.length}`);
94
+ for (const r of results) {
95
+ if (r.status === 'failed') {
96
+ console.log(chalk.red(` ✗ #${r.workId} — ${r.error}`));
97
+ }
98
+ else {
99
+ console.log(chalk.green(` ✓ #${r.workId} — ${r.status}`));
100
+ }
101
+ }
102
+ }
103
+ if (results.some(r => r.status === 'failed')) {
104
+ process.exit(1);
41
105
  }
42
- });
43
- const result = await workflow.run({
44
- workId: options.workId,
45
- autonomy: options.autonomy,
46
- });
47
- if (options.json) {
48
- console.log(JSON.stringify({ status: 'success', data: result }, null, 2));
49
106
  }
50
107
  else {
51
- console.log(chalk.green(`\n✓ Workflow ${result.status}`));
52
- console.log(chalk.gray(` Workflow ID: ${result.workflow_id}`));
53
- console.log(chalk.gray(` Duration: ${result.duration_ms}ms`));
54
- console.log(chalk.gray(` Phases: ${result.phases.map((p) => p.phase).join(' ')}`));
108
+ // Single mode: original behavior
109
+ const workflow = new FaberWorkflow();
110
+ if (!options.json) {
111
+ console.log(chalk.blue(`Starting FABER workflow for work item #${workIds[0]}`));
112
+ console.log(chalk.gray(`Autonomy: ${options.autonomy}`));
113
+ }
114
+ workflow.addEventListener((event, data) => {
115
+ if (options.json)
116
+ return;
117
+ switch (event) {
118
+ case 'phase:start':
119
+ console.log(chalk.cyan(`\n→ Starting phase: ${String(data.phase || '').toUpperCase()}`));
120
+ break;
121
+ case 'phase:complete':
122
+ console.log(chalk.green(` ✓ Completed phase: ${data.phase}`));
123
+ break;
124
+ case 'workflow:fail':
125
+ case 'phase:fail':
126
+ console.error(chalk.red(` ✗ Error: ${data.error || 'Unknown error'}`));
127
+ break;
128
+ }
129
+ });
130
+ const result = await workflow.run({
131
+ workId: workIds[0],
132
+ autonomy: options.autonomy,
133
+ });
134
+ if (options.json) {
135
+ console.log(JSON.stringify({ status: 'success', data: result }, null, 2));
136
+ }
137
+ else {
138
+ console.log(chalk.green(`\n✓ Workflow ${result.status}`));
139
+ console.log(chalk.gray(` Workflow ID: ${result.workflow_id}`));
140
+ console.log(chalk.gray(` Duration: ${result.duration_ms}ms`));
141
+ console.log(chalk.gray(` Phases: ${result.phases.map((p) => p.phase).join(' → ')}`));
142
+ }
55
143
  }
56
144
  }
57
145
  catch (error) {
@@ -437,6 +525,332 @@ export function createWorkflowDebugCommand() {
437
525
  }
438
526
  });
439
527
  }
528
+ /**
529
+ * Create the workflow-batch-plan command
530
+ */
531
+ export function createBatchPlanCommand() {
532
+ return new Command('workflow-batch-plan')
533
+ .description('Initialize a batch of workflows for sequential planning and unattended execution')
534
+ .requiredOption('--work-id <ids>', 'Comma-separated work item IDs (e.g., "258,259,260")')
535
+ .option('--name <batch-id>', 'Custom batch name/ID (default: auto-generated timestamp)')
536
+ .option('--autonomous', 'Continue on plan failures without prompting')
537
+ .option('--json', 'Output as JSON')
538
+ .action(async (options) => {
539
+ try {
540
+ await executeBatchPlanCommand(options);
541
+ }
542
+ catch (error) {
543
+ handleWorkflowError(error, options);
544
+ }
545
+ });
546
+ }
547
+ /**
548
+ * Create the workflow-batch-run command
549
+ */
550
+ export function createBatchRunCommand() {
551
+ return new Command('workflow-batch-run')
552
+ .description('Execute a planned FABER batch sequentially with state tracking')
553
+ .requiredOption('--batch <batch-id>', 'Batch ID to execute (from workflow-batch-plan)')
554
+ .option('--autonomous', 'Auto-skip failed items without prompting (for overnight unattended runs)')
555
+ .option('--resume', 'Skip already-completed items (safe to re-run after interruption)')
556
+ .option('--json', 'Output as JSON')
557
+ .action(async (options) => {
558
+ try {
559
+ await executeBatchRunCommand(options);
560
+ }
561
+ catch (error) {
562
+ handleWorkflowError(error, options);
563
+ }
564
+ });
565
+ }
566
+ // ─── Batch Implementation ───────────────────────────────────────────────────
567
+ import fs from 'fs/promises';
568
+ import path from 'path';
569
+ import { spawnSync } from 'child_process';
570
+ function getBatchDir(batchId) {
571
+ return path.join(process.cwd(), '.fractary', 'faber', 'batches', batchId);
572
+ }
573
+ function generateBatchId() {
574
+ const now = new Date();
575
+ const ts = now.toISOString().replace(/:/g, '-').replace(/\.\d{3}Z$/, 'Z');
576
+ return `batch-${ts}`;
577
+ }
578
+ async function readBatchState(batchId) {
579
+ const statePath = path.join(getBatchDir(batchId), 'state.json');
580
+ const content = await fs.readFile(statePath, 'utf-8');
581
+ return JSON.parse(content);
582
+ }
583
+ async function writeBatchState(batchId, state) {
584
+ state.updated_at = new Date().toISOString();
585
+ const statePath = path.join(getBatchDir(batchId), 'state.json');
586
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2));
587
+ }
588
+ async function executeBatchPlanCommand(options) {
589
+ const workIds = options.workId.split(',').map((id) => id.trim()).filter(Boolean);
590
+ if (workIds.length === 0) {
591
+ throw new Error('--work-id must contain at least one work item ID');
592
+ }
593
+ const batchId = options.name || generateBatchId();
594
+ const batchDir = getBatchDir(batchId);
595
+ const now = new Date().toISOString();
596
+ if (!options.json) {
597
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
598
+ console.log(chalk.blue.bold(' FABER BATCH PLAN'));
599
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
600
+ console.log(chalk.gray(`Batch ID: ${batchId}`));
601
+ console.log(chalk.gray(`Work IDs: ${workIds.join(', ')}`));
602
+ console.log(chalk.gray(`Items: ${workIds.length}`));
603
+ console.log('');
604
+ }
605
+ // Create batch directory
606
+ await fs.mkdir(batchDir, { recursive: true });
607
+ // Write queue.txt
608
+ const queueLines = [
609
+ '# FABER Batch Queue',
610
+ `# Batch ID: ${batchId}`,
611
+ `# Created: ${now}`,
612
+ '# Format: <work-id> [--workflow custom-workflow] [--phase build,evaluate]',
613
+ '',
614
+ ...workIds,
615
+ ];
616
+ await fs.writeFile(path.join(batchDir, 'queue.txt'), queueLines.join('\n'));
617
+ // Initialize state.json
618
+ const state = {
619
+ batch_id: batchId,
620
+ status: 'planning',
621
+ autonomous: options.autonomous ?? false,
622
+ created_at: now,
623
+ updated_at: now,
624
+ items: workIds.map((id) => ({
625
+ work_id: id,
626
+ status: 'pending',
627
+ plan_id: null,
628
+ run_id: null,
629
+ started_at: null,
630
+ completed_at: null,
631
+ skipped: false,
632
+ error: null,
633
+ })),
634
+ };
635
+ await writeBatchState(batchId, state);
636
+ if (!options.json) {
637
+ console.log(chalk.green(`✓ Batch created: ${batchId}`));
638
+ console.log(chalk.gray(` Directory: ${batchDir}`));
639
+ console.log('');
640
+ console.log(chalk.cyan(`→ Planning ${workIds.length} item(s)...`));
641
+ console.log('');
642
+ }
643
+ // Plan each item
644
+ let planned = 0;
645
+ let failed = 0;
646
+ for (let i = 0; i < workIds.length; i++) {
647
+ const workId = workIds[i];
648
+ if (!options.json) {
649
+ console.log(chalk.blue(`[${i + 1}/${workIds.length}] Planning #${workId}...`));
650
+ }
651
+ try {
652
+ // Invoke the existing workflow-plan CLI command as a subprocess
653
+ const faberBin = process.argv[1];
654
+ const result = spawnSync(process.execPath, [faberBin, 'workflow-plan', '--work-id', workId, '--skip-confirm'], { stdio: options.json ? 'pipe' : 'inherit', encoding: 'utf-8' });
655
+ if (result.status !== 0) {
656
+ throw new Error(result.stderr || `workflow-plan exited with code ${result.status}`);
657
+ }
658
+ // Mark as planned
659
+ state.items[i].status = 'planned';
660
+ state.items[i].completed_at = new Date().toISOString();
661
+ await writeBatchState(batchId, state);
662
+ planned++;
663
+ if (!options.json) {
664
+ console.log(chalk.green(` ✓ Planned: #${workId}`));
665
+ }
666
+ }
667
+ catch (error) {
668
+ const message = error instanceof Error ? error.message : String(error);
669
+ state.items[i].status = 'plan_failed';
670
+ state.items[i].error = message;
671
+ state.items[i].completed_at = new Date().toISOString();
672
+ await writeBatchState(batchId, state);
673
+ failed++;
674
+ if (!options.json) {
675
+ console.error(chalk.red(` ✗ Plan failed for #${workId}: ${message}`));
676
+ }
677
+ }
678
+ }
679
+ // Final state
680
+ state.status = failed === 0 ? 'planned' : 'planning_partial';
681
+ await writeBatchState(batchId, state);
682
+ if (options.json) {
683
+ console.log(JSON.stringify({
684
+ status: 'success',
685
+ data: { batch_id: batchId, total: workIds.length, planned, failed },
686
+ }, null, 2));
687
+ }
688
+ else {
689
+ console.log('');
690
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
691
+ console.log(chalk.blue.bold(' BATCH PLANNING COMPLETE'));
692
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
693
+ console.log(`Total: ${workIds.length} | Planned: ${planned} | Failed: ${failed}`);
694
+ console.log('');
695
+ console.log(chalk.cyan('To run this batch (unattended):'));
696
+ console.log(chalk.white(` fractary-faber workflow-batch-run --batch ${batchId} --autonomous`));
697
+ console.log('');
698
+ console.log(chalk.cyan('Or in Claude Code:'));
699
+ console.log(chalk.white(` /fractary-faber:workflow-batch-run --batch ${batchId} --autonomous`));
700
+ }
701
+ }
702
+ async function executeBatchRunCommand(options) {
703
+ const batchId = options.batch;
704
+ const batchDir = getBatchDir(batchId);
705
+ // Verify batch exists
706
+ try {
707
+ await fs.access(batchDir);
708
+ }
709
+ catch {
710
+ throw new Error(`Batch '${batchId}' not found.\nExpected: ${batchDir}\n\nCreate a batch first:\n fractary-faber workflow-batch-plan --work-id <ids> --name ${batchId}`);
711
+ }
712
+ // Load state
713
+ let state = await readBatchState(batchId);
714
+ if (state.status === 'completed') {
715
+ if (options.json) {
716
+ console.log(JSON.stringify({ status: 'success', data: { batch_id: batchId, message: 'already completed', state } }, null, 2));
717
+ }
718
+ else {
719
+ console.log(chalk.green(`✓ Batch '${batchId}' already completed. Nothing to do.`));
720
+ const done = state.items.filter(i => i.status === 'completed').length;
721
+ console.log(chalk.gray(` Completed: ${done}/${state.items.length}`));
722
+ console.log(chalk.gray('\nTo re-run, edit state.json and reset items to "pending".'));
723
+ }
724
+ return;
725
+ }
726
+ // Filter items to process
727
+ const toProcess = options.resume
728
+ ? state.items.filter(i => i.status !== 'completed' && i.status !== 'skipped')
729
+ : state.items.filter(i => i.status !== 'completed' && i.status !== 'skipped');
730
+ if (!options.json) {
731
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
732
+ console.log(chalk.blue.bold(' FABER BATCH RUN'));
733
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
734
+ console.log(chalk.gray(`Batch ID: ${batchId}`));
735
+ console.log(chalk.gray(`Mode: ${options.autonomous ? 'autonomous (unattended)' : 'interactive'}`));
736
+ if (options.resume) {
737
+ const completed = state.items.filter(i => i.status === 'completed').length;
738
+ console.log(chalk.gray(`Resume: ${completed}/${state.items.length} already completed`));
739
+ }
740
+ console.log(chalk.gray(`Remaining: ${toProcess.length} item(s)`));
741
+ console.log('');
742
+ }
743
+ const results = [];
744
+ for (let i = 0; i < toProcess.length; i++) {
745
+ const item = toProcess[i];
746
+ const workId = item.work_id;
747
+ const globalIndex = state.items.findIndex(s => s.work_id === workId);
748
+ if (!options.json) {
749
+ console.log(chalk.blue(`\n═══ Workflow ${i + 1}/${toProcess.length}: #${workId} ═══`));
750
+ }
751
+ // Mark in progress
752
+ state.items[globalIndex].status = 'in_progress';
753
+ state.items[globalIndex].started_at = new Date().toISOString();
754
+ await writeBatchState(batchId, state);
755
+ try {
756
+ const workflow = new FaberWorkflow();
757
+ workflow.addEventListener((event, data) => {
758
+ if (options.json)
759
+ return;
760
+ switch (event) {
761
+ case 'phase:start':
762
+ console.log(chalk.cyan(`\n → Phase: ${String(data.phase || '').toUpperCase()}`));
763
+ break;
764
+ case 'phase:complete':
765
+ console.log(chalk.green(` ✓ Phase: ${data.phase}`));
766
+ break;
767
+ case 'workflow:fail':
768
+ case 'phase:fail':
769
+ console.error(chalk.red(` ✗ Error: ${data.error || 'Unknown error'}`));
770
+ break;
771
+ }
772
+ });
773
+ const result = await workflow.run({ workId, autonomy: options.autonomous ? 'autonomous' : 'guarded' });
774
+ state.items[globalIndex].status = 'completed';
775
+ state.items[globalIndex].run_id = result.run_id || null;
776
+ state.items[globalIndex].completed_at = new Date().toISOString();
777
+ await writeBatchState(batchId, state);
778
+ results.push({ workId, status: 'completed' });
779
+ if (!options.json) {
780
+ console.log(chalk.green(`✓ Completed: #${workId}`));
781
+ }
782
+ }
783
+ catch (error) {
784
+ const message = error instanceof Error ? error.message : String(error);
785
+ state.items[globalIndex].status = 'failed';
786
+ state.items[globalIndex].skipped = true;
787
+ state.items[globalIndex].error = message;
788
+ state.items[globalIndex].completed_at = new Date().toISOString();
789
+ await writeBatchState(batchId, state);
790
+ results.push({ workId, status: 'failed', error: message });
791
+ if (!options.json) {
792
+ console.error(chalk.red(`✗ Failed: #${workId} — ${message}`));
793
+ }
794
+ if (!options.autonomous) {
795
+ // Non-autonomous: stop on first failure
796
+ if (!options.json) {
797
+ console.error(chalk.yellow('\nBatch stopped. Use --autonomous to auto-skip failures.'));
798
+ console.error(chalk.gray(`Resume with: fractary-faber workflow-batch-run --batch ${batchId} --autonomous --resume`));
799
+ }
800
+ break;
801
+ }
802
+ // Autonomous: continue to next item
803
+ if (!options.json) {
804
+ console.log(chalk.yellow(' → Auto-skipping (autonomous mode). Continuing...'));
805
+ }
806
+ }
807
+ }
808
+ // Final state
809
+ const allCompleted = state.items.every(i => i.status === 'completed');
810
+ const anyFailed = state.items.some(i => i.status === 'failed');
811
+ state.status = allCompleted ? 'completed' : anyFailed ? 'completed_with_failures' : 'paused';
812
+ await writeBatchState(batchId, state);
813
+ if (options.json) {
814
+ const completed = state.items.filter(i => i.status === 'completed').length;
815
+ const failedCount = state.items.filter(i => i.status === 'failed').length;
816
+ console.log(JSON.stringify({
817
+ status: 'success',
818
+ data: { batch_id: batchId, total: state.items.length, completed, failed: failedCount, results },
819
+ }, null, 2));
820
+ }
821
+ else {
822
+ const completed = state.items.filter(i => i.status === 'completed').length;
823
+ const failedCount = state.items.filter(i => i.status === 'failed').length;
824
+ console.log('');
825
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
826
+ console.log(chalk.blue.bold(' BATCH RUN COMPLETE'));
827
+ console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
828
+ console.log(`Total: ${state.items.length} | Completed: ${completed} | Failed: ${failedCount}`);
829
+ console.log('');
830
+ for (const item of state.items) {
831
+ if (item.status === 'completed') {
832
+ console.log(chalk.green(` ✓ #${item.work_id} — completed`));
833
+ }
834
+ else if (item.status === 'failed') {
835
+ console.log(chalk.red(` ✗ #${item.work_id} — failed: ${item.error}`));
836
+ }
837
+ else if (item.status === 'skipped') {
838
+ console.log(chalk.yellow(` ○ #${item.work_id} — skipped`));
839
+ }
840
+ else {
841
+ console.log(chalk.gray(` ○ #${item.work_id} — ${item.status}`));
842
+ }
843
+ }
844
+ if (failedCount > 0) {
845
+ console.log('');
846
+ console.log(chalk.cyan('To retry failed items:'));
847
+ console.log(chalk.white(` fractary-faber workflow-batch-run --batch ${batchId} --autonomous --resume`));
848
+ }
849
+ }
850
+ if (anyFailed && !options.autonomous) {
851
+ process.exit(1);
852
+ }
853
+ }
440
854
  // Helper functions
441
855
  function getStateColor(state) {
442
856
  switch (state) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAMH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBpC;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAsDxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAMH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBpC;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAwDxC"}
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import dotenv from 'dotenv';
10
10
  dotenv.config();
11
11
  import { Command } from 'commander';
12
12
  import chalk from 'chalk';
13
- import { createRunCommand, createStatusCommand, createResumeCommand, createPauseCommand, createRecoverCommand, createCleanupCommand, createWorkflowCreateCommand, createWorkflowUpdateCommand, createWorkflowInspectCommand, createWorkflowDebugCommand } from './commands/workflow/index.js';
13
+ import { createRunCommand, createStatusCommand, createResumeCommand, createPauseCommand, createRecoverCommand, createCleanupCommand, createWorkflowCreateCommand, createWorkflowUpdateCommand, createWorkflowInspectCommand, createWorkflowDebugCommand, createBatchPlanCommand, createBatchRunCommand } from './commands/workflow/index.js';
14
14
  import { createSessionLoadCommand, createSessionSaveCommand } from './commands/session.js';
15
15
  import { createWorkCommand } from './commands/work/index.js';
16
16
  import { createRepoCommand } from './commands/repo/index.js';
@@ -53,6 +53,8 @@ export function createFaberCLI() {
53
53
  program.addCommand(createWorkflowUpdateCommand()); // workflow-update
54
54
  program.addCommand(createWorkflowInspectCommand()); // workflow-inspect
55
55
  program.addCommand(createWorkflowDebugCommand()); // workflow-debug
56
+ program.addCommand(createBatchPlanCommand()); // workflow-batch-plan
57
+ program.addCommand(createBatchRunCommand()); // workflow-batch-run
56
58
  program.addCommand(createSessionLoadCommand()); // session-load
57
59
  program.addCommand(createSessionSaveCommand()); // session-save
58
60
  // Subcommand trees
@@ -1 +1 @@
1
- {"version":3,"file":"yaml-config.d.ts","sourceRoot":"","sources":["../../src/lib/yaml-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,YAAY,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAgH3E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa,GAAG,IAAI,CAoDpF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAkBN;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,UAAO,GAAG,MAAM,CAgD7E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAoDxE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAI1D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAI/D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAG/D"}
1
+ {"version":3,"file":"yaml-config.d.ts","sourceRoot":"","sources":["../../src/lib/yaml-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,YAAY,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAuH3E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa,GAAG,IAAI,CAoDpF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAkBN;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,UAAO,GAAG,MAAM,CAgD7E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAoDxE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAI1D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAI/D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAG/D"}
@@ -100,6 +100,13 @@ const unifiedConfigSchema = {
100
100
  },
101
101
  additionalProperties: false,
102
102
  },
103
+ changelog: {
104
+ type: 'object',
105
+ properties: {
106
+ path: { type: 'string' },
107
+ },
108
+ additionalProperties: false,
109
+ },
103
110
  },
104
111
  additionalProperties: false,
105
112
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fractary/faber-cli",
3
- "version": "1.5.30",
3
+ "version": "1.5.32",
4
4
  "description": "FABER CLI - Command-line interface for FABER development toolkit",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -37,8 +37,8 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@fractary/core": "^0.7.20",
41
- "@fractary/faber": "^2.4.14",
40
+ "@fractary/core": "*",
41
+ "@fractary/faber": "*",
42
42
  "ajv": "^8.12.0",
43
43
  "chalk": "^5.0.0",
44
44
  "commander": "^12.0.0",