@fractary/faber-cli 1.5.31 → 1.5.34
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/dist/commands/workflow/index.d.ts +8 -0
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +462 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib/yaml-config.d.ts.map +1 -1
- package/dist/lib/yaml-config.js +7 -0
- package/package.json +3 -3
|
@@ -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,
|
|
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,CAsJ1C;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,CAgB/C"}
|
|
@@ -12,46 +12,138 @@ 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 <
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.log(chalk.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
phase: options.phase,
|
|
67
|
+
forceNew: options.forceNew,
|
|
68
|
+
});
|
|
69
|
+
results.push({ workId, status: result.status });
|
|
70
|
+
if (!options.json) {
|
|
71
|
+
console.log(chalk.green(`✓ Workflow ${i + 1}/${workIds.length} completed: #${workId}`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
results.push({ workId, status: 'failed', error: message });
|
|
77
|
+
if (!options.json) {
|
|
78
|
+
console.error(chalk.red(`✗ Workflow ${i + 1}/${workIds.length} FAILED: #${workId}`));
|
|
79
|
+
console.error(chalk.red(` Error: ${message}`));
|
|
80
|
+
console.error(chalk.yellow('\nBatch stopped due to error. Remaining workflows not executed.'));
|
|
81
|
+
}
|
|
82
|
+
break; // Stop batch on error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Batch summary
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify({ status: 'success', data: { batch: true, results } }, null, 2));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const completed = results.filter(r => r.status !== 'failed');
|
|
91
|
+
const failed = results.filter(r => r.status === 'failed');
|
|
92
|
+
console.log(chalk.blue.bold('\n═══════════════════════════════════════════════'));
|
|
93
|
+
console.log(chalk.blue.bold(' BATCH COMPLETE'));
|
|
94
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
95
|
+
console.log(`Total: ${workIds.length} | Completed: ${completed.length} | Failed: ${failed.length}`);
|
|
96
|
+
for (const r of results) {
|
|
97
|
+
if (r.status === 'failed') {
|
|
98
|
+
console.log(chalk.red(` ✗ #${r.workId} — ${r.error}`));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(chalk.green(` ✓ #${r.workId} — ${r.status}`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (results.some(r => r.status === 'failed')) {
|
|
106
|
+
process.exit(1);
|
|
41
107
|
}
|
|
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
108
|
}
|
|
50
109
|
else {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
110
|
+
// Single mode: original behavior
|
|
111
|
+
const workflow = new FaberWorkflow();
|
|
112
|
+
if (!options.json) {
|
|
113
|
+
console.log(chalk.blue(`Starting FABER workflow for work item #${workIds[0]}`));
|
|
114
|
+
console.log(chalk.gray(`Autonomy: ${options.autonomy}`));
|
|
115
|
+
}
|
|
116
|
+
workflow.addEventListener((event, data) => {
|
|
117
|
+
if (options.json)
|
|
118
|
+
return;
|
|
119
|
+
switch (event) {
|
|
120
|
+
case 'phase:start':
|
|
121
|
+
console.log(chalk.cyan(`\n→ Starting phase: ${String(data.phase || '').toUpperCase()}`));
|
|
122
|
+
break;
|
|
123
|
+
case 'phase:complete':
|
|
124
|
+
console.log(chalk.green(` ✓ Completed phase: ${data.phase}`));
|
|
125
|
+
break;
|
|
126
|
+
case 'workflow:fail':
|
|
127
|
+
case 'phase:fail':
|
|
128
|
+
console.error(chalk.red(` ✗ Error: ${data.error || 'Unknown error'}`));
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const result = await workflow.run({
|
|
133
|
+
workId: workIds[0],
|
|
134
|
+
autonomy: options.autonomy,
|
|
135
|
+
phase: options.phase,
|
|
136
|
+
forceNew: options.forceNew,
|
|
137
|
+
});
|
|
138
|
+
if (options.json) {
|
|
139
|
+
console.log(JSON.stringify({ status: 'success', data: result }, null, 2));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(chalk.green(`\n✓ Workflow ${result.status}`));
|
|
143
|
+
console.log(chalk.gray(` Workflow ID: ${result.workflow_id}`));
|
|
144
|
+
console.log(chalk.gray(` Duration: ${result.duration_ms}ms`));
|
|
145
|
+
console.log(chalk.gray(` Phases: ${result.phases.map((p) => p.phase).join(' → ')}`));
|
|
146
|
+
}
|
|
55
147
|
}
|
|
56
148
|
}
|
|
57
149
|
catch (error) {
|
|
@@ -437,6 +529,343 @@ export function createWorkflowDebugCommand() {
|
|
|
437
529
|
}
|
|
438
530
|
});
|
|
439
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Create the workflow-batch-plan command
|
|
534
|
+
*/
|
|
535
|
+
export function createBatchPlanCommand() {
|
|
536
|
+
return new Command('workflow-batch-plan')
|
|
537
|
+
.description('Initialize a batch of workflows for sequential planning and unattended execution')
|
|
538
|
+
.requiredOption('--work-id <ids>', 'Comma-separated work item IDs (e.g., "258,259,260")')
|
|
539
|
+
.option('--name <batch-id>', 'Custom batch name/ID (default: auto-generated timestamp)')
|
|
540
|
+
.option('--autonomous', 'Continue on plan failures without prompting')
|
|
541
|
+
.option('--json', 'Output as JSON')
|
|
542
|
+
.action(async (options) => {
|
|
543
|
+
try {
|
|
544
|
+
await executeBatchPlanCommand(options);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
handleWorkflowError(error, options);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Create the workflow-batch-run command
|
|
553
|
+
*/
|
|
554
|
+
export function createBatchRunCommand() {
|
|
555
|
+
return new Command('workflow-batch-run')
|
|
556
|
+
.description('Execute a planned FABER batch sequentially with state tracking')
|
|
557
|
+
.requiredOption('--batch <batch-id>', 'Batch ID to execute (from workflow-batch-plan)')
|
|
558
|
+
.option('--autonomous', 'Auto-skip failed items without prompting (for overnight unattended runs)')
|
|
559
|
+
.option('--resume', 'Skip already-completed items (safe to re-run after interruption)')
|
|
560
|
+
.option('--phase <phases>', 'Execute only specified phase(s) - comma-separated')
|
|
561
|
+
.option('--force-new', 'Force fresh start for each item, bypass auto-resume')
|
|
562
|
+
.option('--json', 'Output as JSON')
|
|
563
|
+
.action(async (options) => {
|
|
564
|
+
try {
|
|
565
|
+
await executeBatchRunCommand(options);
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
handleWorkflowError(error, options);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
// ─── Batch Implementation ───────────────────────────────────────────────────
|
|
573
|
+
import fs from 'fs/promises';
|
|
574
|
+
import path from 'path';
|
|
575
|
+
import { spawnSync } from 'child_process';
|
|
576
|
+
function getBatchDir(batchId) {
|
|
577
|
+
return path.join(process.cwd(), '.fractary', 'faber', 'batches', batchId);
|
|
578
|
+
}
|
|
579
|
+
function generateBatchId() {
|
|
580
|
+
const now = new Date();
|
|
581
|
+
const ts = now.toISOString().replace(/:/g, '-').replace(/\.\d{3}Z$/, 'Z');
|
|
582
|
+
return `batch-${ts}`;
|
|
583
|
+
}
|
|
584
|
+
async function readBatchState(batchId) {
|
|
585
|
+
const statePath = path.join(getBatchDir(batchId), 'state.json');
|
|
586
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
587
|
+
return JSON.parse(content);
|
|
588
|
+
}
|
|
589
|
+
async function writeBatchState(batchId, state) {
|
|
590
|
+
state.updated_at = new Date().toISOString();
|
|
591
|
+
const statePath = path.join(getBatchDir(batchId), 'state.json');
|
|
592
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2));
|
|
593
|
+
}
|
|
594
|
+
async function executeBatchPlanCommand(options) {
|
|
595
|
+
const workIds = options.workId.split(',').map((id) => id.trim()).filter(Boolean);
|
|
596
|
+
if (workIds.length === 0) {
|
|
597
|
+
throw new Error('--work-id must contain at least one work item ID');
|
|
598
|
+
}
|
|
599
|
+
const batchId = options.name || generateBatchId();
|
|
600
|
+
const batchDir = getBatchDir(batchId);
|
|
601
|
+
const now = new Date().toISOString();
|
|
602
|
+
if (!options.json) {
|
|
603
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
604
|
+
console.log(chalk.blue.bold(' FABER BATCH PLAN'));
|
|
605
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
606
|
+
console.log(chalk.gray(`Batch ID: ${batchId}`));
|
|
607
|
+
console.log(chalk.gray(`Work IDs: ${workIds.join(', ')}`));
|
|
608
|
+
console.log(chalk.gray(`Items: ${workIds.length}`));
|
|
609
|
+
console.log('');
|
|
610
|
+
}
|
|
611
|
+
// Create batch directory
|
|
612
|
+
await fs.mkdir(batchDir, { recursive: true });
|
|
613
|
+
// Write queue.txt
|
|
614
|
+
const queueLines = [
|
|
615
|
+
'# FABER Batch Queue',
|
|
616
|
+
`# Batch ID: ${batchId}`,
|
|
617
|
+
`# Created: ${now}`,
|
|
618
|
+
'# Format: <work-id> [--workflow custom-workflow] [--phase build,evaluate]',
|
|
619
|
+
'',
|
|
620
|
+
...workIds,
|
|
621
|
+
];
|
|
622
|
+
await fs.writeFile(path.join(batchDir, 'queue.txt'), queueLines.join('\n'));
|
|
623
|
+
// Initialize state.json
|
|
624
|
+
const state = {
|
|
625
|
+
batch_id: batchId,
|
|
626
|
+
status: 'planning',
|
|
627
|
+
autonomous: options.autonomous ?? false,
|
|
628
|
+
created_at: now,
|
|
629
|
+
updated_at: now,
|
|
630
|
+
items: workIds.map((id) => ({
|
|
631
|
+
work_id: id,
|
|
632
|
+
status: 'pending',
|
|
633
|
+
plan_id: null,
|
|
634
|
+
run_id: null,
|
|
635
|
+
started_at: null,
|
|
636
|
+
completed_at: null,
|
|
637
|
+
skipped: false,
|
|
638
|
+
error: null,
|
|
639
|
+
})),
|
|
640
|
+
};
|
|
641
|
+
await writeBatchState(batchId, state);
|
|
642
|
+
if (!options.json) {
|
|
643
|
+
console.log(chalk.green(`✓ Batch created: ${batchId}`));
|
|
644
|
+
console.log(chalk.gray(` Directory: ${batchDir}`));
|
|
645
|
+
console.log('');
|
|
646
|
+
console.log(chalk.cyan(`→ Planning ${workIds.length} item(s)...`));
|
|
647
|
+
console.log('');
|
|
648
|
+
}
|
|
649
|
+
// Plan each item
|
|
650
|
+
let planned = 0;
|
|
651
|
+
let failed = 0;
|
|
652
|
+
for (let i = 0; i < workIds.length; i++) {
|
|
653
|
+
const workId = workIds[i];
|
|
654
|
+
if (!options.json) {
|
|
655
|
+
console.log(chalk.blue(`[${i + 1}/${workIds.length}] Planning #${workId}...`));
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
// Invoke the existing workflow-plan CLI command as a subprocess
|
|
659
|
+
const faberBin = process.argv[1];
|
|
660
|
+
const result = spawnSync(process.execPath, [faberBin, 'workflow-plan', '--work-id', workId, '--skip-confirm'], { stdio: options.json ? 'pipe' : 'inherit', encoding: 'utf-8' });
|
|
661
|
+
if (result.status !== 0) {
|
|
662
|
+
throw new Error(result.stderr || `workflow-plan exited with code ${result.status}`);
|
|
663
|
+
}
|
|
664
|
+
// Mark as planned
|
|
665
|
+
state.items[i].status = 'planned';
|
|
666
|
+
state.items[i].completed_at = new Date().toISOString();
|
|
667
|
+
await writeBatchState(batchId, state);
|
|
668
|
+
planned++;
|
|
669
|
+
if (!options.json) {
|
|
670
|
+
console.log(chalk.green(` ✓ Planned: #${workId}`));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
675
|
+
state.items[i].status = 'plan_failed';
|
|
676
|
+
state.items[i].error = message;
|
|
677
|
+
state.items[i].completed_at = new Date().toISOString();
|
|
678
|
+
await writeBatchState(batchId, state);
|
|
679
|
+
failed++;
|
|
680
|
+
if (!options.json) {
|
|
681
|
+
console.error(chalk.red(` ✗ Plan failed for #${workId}: ${message}`));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Final state
|
|
686
|
+
state.status = failed === 0 ? 'planned' : 'planning_partial';
|
|
687
|
+
await writeBatchState(batchId, state);
|
|
688
|
+
if (options.json) {
|
|
689
|
+
console.log(JSON.stringify({
|
|
690
|
+
status: 'success',
|
|
691
|
+
data: { batch_id: batchId, total: workIds.length, planned, failed },
|
|
692
|
+
}, null, 2));
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
console.log('');
|
|
696
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
697
|
+
console.log(chalk.blue.bold(' BATCH PLANNING COMPLETE'));
|
|
698
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
699
|
+
console.log(`Total: ${workIds.length} | Planned: ${planned} | Failed: ${failed}`);
|
|
700
|
+
console.log('');
|
|
701
|
+
console.log(chalk.cyan('To run this batch (unattended):'));
|
|
702
|
+
console.log(chalk.white(` fractary-faber workflow-batch-run --batch ${batchId} --autonomous`));
|
|
703
|
+
console.log('');
|
|
704
|
+
console.log(chalk.cyan('Or in Claude Code:'));
|
|
705
|
+
console.log(chalk.white(` /fractary-faber:workflow-batch-run --batch ${batchId} --autonomous`));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async function executeBatchRunCommand(options) {
|
|
709
|
+
const batchId = options.batch;
|
|
710
|
+
const batchDir = getBatchDir(batchId);
|
|
711
|
+
// Verify batch exists
|
|
712
|
+
try {
|
|
713
|
+
await fs.access(batchDir);
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
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}`);
|
|
717
|
+
}
|
|
718
|
+
// Load state
|
|
719
|
+
let state = await readBatchState(batchId);
|
|
720
|
+
if (state.status === 'completed') {
|
|
721
|
+
if (options.json) {
|
|
722
|
+
console.log(JSON.stringify({ status: 'success', data: { batch_id: batchId, message: 'already completed', state } }, null, 2));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log(chalk.green(`✓ Batch '${batchId}' already completed. Nothing to do.`));
|
|
726
|
+
const done = state.items.filter(i => i.status === 'completed').length;
|
|
727
|
+
console.log(chalk.gray(` Completed: ${done}/${state.items.length}`));
|
|
728
|
+
console.log(chalk.gray('\nTo re-run, edit state.json and reset items to "pending".'));
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
// Filter items to process
|
|
733
|
+
const toProcess = options.resume
|
|
734
|
+
? state.items.filter(i => i.status !== 'completed' && i.status !== 'skipped')
|
|
735
|
+
: state.items.filter(i => i.status !== 'completed' && i.status !== 'skipped');
|
|
736
|
+
if (!options.json) {
|
|
737
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
738
|
+
console.log(chalk.blue.bold(' FABER BATCH RUN'));
|
|
739
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
740
|
+
console.log(chalk.gray(`Batch ID: ${batchId}`));
|
|
741
|
+
console.log(chalk.gray(`Mode: ${options.autonomous ? 'autonomous (unattended)' : 'interactive'}`));
|
|
742
|
+
if (options.resume) {
|
|
743
|
+
const completed = state.items.filter(i => i.status === 'completed').length;
|
|
744
|
+
console.log(chalk.gray(`Resume: ${completed}/${state.items.length} already completed`));
|
|
745
|
+
}
|
|
746
|
+
if (options.phase)
|
|
747
|
+
console.log(chalk.gray(`Phase filter: ${options.phase}`));
|
|
748
|
+
if (options.forceNew)
|
|
749
|
+
console.log(chalk.gray('Force new: yes'));
|
|
750
|
+
console.log(chalk.gray(`Remaining: ${toProcess.length} item(s)`));
|
|
751
|
+
console.log('');
|
|
752
|
+
}
|
|
753
|
+
const results = [];
|
|
754
|
+
for (let i = 0; i < toProcess.length; i++) {
|
|
755
|
+
const item = toProcess[i];
|
|
756
|
+
const workId = item.work_id;
|
|
757
|
+
const globalIndex = state.items.findIndex(s => s.work_id === workId);
|
|
758
|
+
if (!options.json) {
|
|
759
|
+
console.log(chalk.blue(`\n═══ Workflow ${i + 1}/${toProcess.length}: #${workId} ═══`));
|
|
760
|
+
}
|
|
761
|
+
// Mark in progress
|
|
762
|
+
state.items[globalIndex].status = 'in_progress';
|
|
763
|
+
state.items[globalIndex].started_at = new Date().toISOString();
|
|
764
|
+
await writeBatchState(batchId, state);
|
|
765
|
+
try {
|
|
766
|
+
const workflow = new FaberWorkflow();
|
|
767
|
+
workflow.addEventListener((event, data) => {
|
|
768
|
+
if (options.json)
|
|
769
|
+
return;
|
|
770
|
+
switch (event) {
|
|
771
|
+
case 'phase:start':
|
|
772
|
+
console.log(chalk.cyan(`\n → Phase: ${String(data.phase || '').toUpperCase()}`));
|
|
773
|
+
break;
|
|
774
|
+
case 'phase:complete':
|
|
775
|
+
console.log(chalk.green(` ✓ Phase: ${data.phase}`));
|
|
776
|
+
break;
|
|
777
|
+
case 'workflow:fail':
|
|
778
|
+
case 'phase:fail':
|
|
779
|
+
console.error(chalk.red(` ✗ Error: ${data.error || 'Unknown error'}`));
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
const result = await workflow.run({
|
|
784
|
+
workId,
|
|
785
|
+
autonomy: options.autonomous ? 'autonomous' : 'guarded',
|
|
786
|
+
phase: options.phase,
|
|
787
|
+
forceNew: options.forceNew,
|
|
788
|
+
});
|
|
789
|
+
state.items[globalIndex].status = 'completed';
|
|
790
|
+
state.items[globalIndex].run_id = result.run_id || null;
|
|
791
|
+
state.items[globalIndex].completed_at = new Date().toISOString();
|
|
792
|
+
await writeBatchState(batchId, state);
|
|
793
|
+
results.push({ workId, status: 'completed' });
|
|
794
|
+
if (!options.json) {
|
|
795
|
+
console.log(chalk.green(`✓ Completed: #${workId}`));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
800
|
+
state.items[globalIndex].status = 'failed';
|
|
801
|
+
state.items[globalIndex].skipped = true;
|
|
802
|
+
state.items[globalIndex].error = message;
|
|
803
|
+
state.items[globalIndex].completed_at = new Date().toISOString();
|
|
804
|
+
await writeBatchState(batchId, state);
|
|
805
|
+
results.push({ workId, status: 'failed', error: message });
|
|
806
|
+
if (!options.json) {
|
|
807
|
+
console.error(chalk.red(`✗ Failed: #${workId} — ${message}`));
|
|
808
|
+
}
|
|
809
|
+
if (!options.autonomous) {
|
|
810
|
+
// Non-autonomous: stop on first failure
|
|
811
|
+
if (!options.json) {
|
|
812
|
+
console.error(chalk.yellow('\nBatch stopped. Use --autonomous to auto-skip failures.'));
|
|
813
|
+
console.error(chalk.gray(`Resume with: fractary-faber workflow-batch-run --batch ${batchId} --autonomous --resume`));
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
// Autonomous: continue to next item
|
|
818
|
+
if (!options.json) {
|
|
819
|
+
console.log(chalk.yellow(' → Auto-skipping (autonomous mode). Continuing...'));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// Final state
|
|
824
|
+
const allCompleted = state.items.every(i => i.status === 'completed');
|
|
825
|
+
const anyFailed = state.items.some(i => i.status === 'failed');
|
|
826
|
+
state.status = allCompleted ? 'completed' : anyFailed ? 'completed_with_failures' : 'paused';
|
|
827
|
+
await writeBatchState(batchId, state);
|
|
828
|
+
if (options.json) {
|
|
829
|
+
const completed = state.items.filter(i => i.status === 'completed').length;
|
|
830
|
+
const failedCount = state.items.filter(i => i.status === 'failed').length;
|
|
831
|
+
console.log(JSON.stringify({
|
|
832
|
+
status: 'success',
|
|
833
|
+
data: { batch_id: batchId, total: state.items.length, completed, failed: failedCount, results },
|
|
834
|
+
}, null, 2));
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
const completed = state.items.filter(i => i.status === 'completed').length;
|
|
838
|
+
const failedCount = state.items.filter(i => i.status === 'failed').length;
|
|
839
|
+
console.log('');
|
|
840
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
841
|
+
console.log(chalk.blue.bold(' BATCH RUN COMPLETE'));
|
|
842
|
+
console.log(chalk.blue.bold('═══════════════════════════════════════════════'));
|
|
843
|
+
console.log(`Total: ${state.items.length} | Completed: ${completed} | Failed: ${failedCount}`);
|
|
844
|
+
console.log('');
|
|
845
|
+
for (const item of state.items) {
|
|
846
|
+
if (item.status === 'completed') {
|
|
847
|
+
console.log(chalk.green(` ✓ #${item.work_id} — completed`));
|
|
848
|
+
}
|
|
849
|
+
else if (item.status === 'failed') {
|
|
850
|
+
console.log(chalk.red(` ✗ #${item.work_id} — failed: ${item.error}`));
|
|
851
|
+
}
|
|
852
|
+
else if (item.status === 'skipped') {
|
|
853
|
+
console.log(chalk.yellow(` ○ #${item.work_id} — skipped`));
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
console.log(chalk.gray(` ○ #${item.work_id} — ${item.status}`));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (failedCount > 0) {
|
|
860
|
+
console.log('');
|
|
861
|
+
console.log(chalk.cyan('To retry failed items:'));
|
|
862
|
+
console.log(chalk.white(` fractary-faber workflow-batch-run --batch ${batchId} --autonomous --resume`));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (anyFailed && !options.autonomous) {
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
440
869
|
// Helper functions
|
|
441
870
|
function getStateColor(state) {
|
|
442
871
|
switch (state) {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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;
|
|
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"}
|
package/dist/lib/yaml-config.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.5.34",
|
|
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": "
|
|
41
|
-
"@fractary/faber": "
|
|
40
|
+
"@fractary/core": "*",
|
|
41
|
+
"@fractary/faber": "*",
|
|
42
42
|
"ajv": "^8.12.0",
|
|
43
43
|
"chalk": "^5.0.0",
|
|
44
44
|
"commander": "^12.0.0",
|