claude-flow-tbowman01
Version:
Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)
624 lines • 24.7 kB
JavaScript
import { getErrorMessage } from '../../utils/error-handler.js';
/**
* Workflow execution commands for Claude-Flow
*/
import { Command } from 'commander';
import { promises as fs } from 'node:fs';
import chalk from 'chalk';
import inquirer from 'inquirer';
import * as Table from 'cli-table3';
import { generateId } from '../../utils/helpers.js';
import { formatDuration, formatStatusIndicator, formatProgressBar } from '../formatter.js';
export const workflowCommand = new Command()
.name('workflow')
.description('Execute and manage workflows')
.action(() => {
workflowCommand.outputHelp();
})
.command('run')
.description('Execute a workflow from file')
.argument('<workflow-file>', 'Workflow file path')
.option('-d, --dry-run', 'Validate workflow without executing')
.option('-v, --variables <vars>', 'Override variables (JSON format)')
.option('-w, --watch', 'Watch workflow execution progress')
.option('--parallel', 'Allow parallel execution where possible')
.option('--fail-fast', 'Stop on first task failure')
.action(async (workflowFile, options) => {
await runWorkflow(workflowFile, options);
})
.command('validate')
.description('Validate a workflow file')
.argument('<workflow-file>', 'Workflow file path')
.option('--strict', 'Use strict validation mode')
.action(async (workflowFile, options) => {
await validateWorkflow(workflowFile, options);
})
.command('list')
.description('List running workflows')
.option('--all', 'Include completed workflows')
.option('--format <format>', 'Output format (table, json)', 'table')
.action(async (options) => {
await listWorkflows(options);
})
.command('status')
.description('Show workflow execution status')
.argument('<workflow-id>', 'Workflow ID')
.option('-w, --watch', 'Watch workflow progress')
.action(async (workflowId, options) => {
await showWorkflowStatus(workflowId, options);
})
.command('stop')
.description('Stop a running workflow')
.argument('<workflow-id>', 'Workflow ID')
.option('-f, --force', 'Force stop without cleanup')
.action(async (workflowId, options) => {
await stopWorkflow(workflowId, options);
})
.command('template')
.description('Generate workflow templates')
.argument('<template-type>', 'Template type')
.option('-o, --output <file>', 'Output file path')
.option('--format <format>', 'Template format (json, yaml)', 'json')
.action(async (templateType, options) => {
await generateTemplate(templateType, options);
});
async function runWorkflow(workflowFile, options) {
try {
// Load and validate workflow
const workflow = await loadWorkflow(workflowFile);
if (options.dryRun) {
await validateWorkflowDefinition(workflow, true);
console.log(chalk.green('✓ Workflow validation passed'));
return;
}
// Override variables if provided
if (options.variables) {
try {
const vars = JSON.parse(options.variables);
workflow.variables = { ...workflow.variables, ...vars };
}
catch (error) {
throw new Error(`Invalid variables JSON: ${error.message}`);
}
}
// Create execution plan
const execution = await createExecution(workflow);
console.log(chalk.cyan.bold('Starting workflow execution'));
console.log(`${chalk.white('Workflow:')} ${workflow.name}`);
console.log(`${chalk.white('ID:')} ${execution.id}`);
console.log(`${chalk.white('Tasks:')} ${execution.tasks.length}`);
console.log();
// Execute workflow
if (options.watch) {
await executeWorkflowWithWatch(execution, workflow, options);
}
else {
await executeWorkflow(execution, workflow, options);
}
}
catch (error) {
console.error(chalk.red('Workflow execution failed:'), error.message);
process.exit(1);
}
}
async function validateWorkflow(workflowFile, options) {
try {
const workflow = await loadWorkflow(workflowFile);
await validateWorkflowDefinition(workflow, options.strict);
console.log(chalk.green('✓ Workflow validation passed'));
console.log(`${chalk.white('Name:')} ${workflow.name}`);
console.log(`${chalk.white('Tasks:')} ${workflow.tasks.length}`);
console.log(`${chalk.white('Agents:')} ${workflow.agents?.length || 0}`);
if (workflow.dependencies) {
const depCount = Object.values(workflow.dependencies).flat().length;
console.log(`${chalk.white('Dependencies:')} ${depCount}`);
}
}
catch (error) {
console.error(chalk.red('✗ Workflow validation failed:'), error.message);
process.exit(1);
}
}
async function listWorkflows(options) {
try {
// Mock workflow list - in production, this would query the orchestrator
const workflows = await getRunningWorkflows(options.all);
if (options.format === 'json') {
console.log(JSON.stringify(workflows, null, 2));
return;
}
if (workflows.length === 0) {
console.log(chalk.gray('No workflows found'));
return;
}
console.log(chalk.cyan.bold(`Workflows (${workflows.length})`));
console.log('─'.repeat(60));
const table = new Table.default({
head: ['ID', 'Name', 'Status', 'Progress', 'Started', 'Duration'],
});
for (const workflow of workflows) {
const statusIcon = formatStatusIndicator(workflow.status);
const progress = `${workflow.progress.completed}/${workflow.progress.total}`;
const progressBar = formatProgressBar(workflow.progress.completed, workflow.progress.total, 10);
const duration = workflow.completedAt
? formatDuration(workflow.completedAt.getTime() - workflow.startedAt.getTime())
: formatDuration(Date.now() - workflow.startedAt.getTime());
table.push([
chalk.gray(workflow.id.substring(0, 8) + '...'),
chalk.white(workflow.workflowName),
`${statusIcon} ${workflow.status}`,
`${progressBar} ${progress}`,
workflow.startedAt.toLocaleTimeString(),
duration,
]);
}
console.log(table.toString());
}
catch (error) {
console.error(chalk.red('Failed to list workflows:'), error.message);
}
}
async function showWorkflowStatus(workflowId, options) {
try {
if (options.watch) {
await watchWorkflowStatus(workflowId);
}
else {
const execution = await getWorkflowExecution(workflowId);
displayWorkflowStatus(execution);
}
}
catch (error) {
console.error(chalk.red('Failed to get workflow status:'), error.message);
}
}
async function stopWorkflow(workflowId, options) {
try {
const execution = await getWorkflowExecution(workflowId);
if (execution.status !== 'running') {
console.log(chalk.yellow(`Workflow is not running (status: ${execution.status})`));
return;
}
if (!options.force) {
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: `Stop workflow "${execution.workflowName}"?`,
default: false,
},
]);
if (!confirmed) {
console.log(chalk.gray('Stop cancelled'));
return;
}
}
console.log(chalk.yellow('Stopping workflow...'));
// Mock stopping - in production, this would call the orchestrator
if (options.force) {
console.log(chalk.red('• Force stopping all tasks'));
}
else {
console.log(chalk.blue('• Gracefully stopping tasks'));
console.log(chalk.blue('• Cleaning up resources'));
}
console.log(chalk.green('✓ Workflow stopped'));
}
catch (error) {
console.error(chalk.red('Failed to stop workflow:'), error.message);
}
}
async function generateTemplate(templateType, options) {
const templates = {
research: {
name: 'Research Workflow',
description: 'Multi-stage research and analysis workflow',
variables: {
topic: 'quantum computing',
depth: 'comprehensive',
},
agents: [
{ id: 'researcher', type: 'researcher', name: 'Research Agent' },
{ id: 'analyst', type: 'analyst', name: 'Analysis Agent' },
],
tasks: [
{
id: 'research-task',
type: 'research',
description: 'Research the given topic',
assignTo: 'researcher',
input: { topic: '${topic}', depth: '${depth}' },
},
{
id: 'analyze-task',
type: 'analysis',
description: 'Analyze research findings',
assignTo: 'analyst',
depends: ['research-task'],
input: { data: '${research-task.output}' },
},
],
settings: {
maxConcurrency: 2,
timeout: 300000,
failurePolicy: 'fail-fast',
},
},
implementation: {
name: 'Implementation Workflow',
description: 'Code implementation and testing workflow',
agents: [
{ id: 'implementer', type: 'implementer', name: 'Implementation Agent' },
{ id: 'tester', type: 'implementer', name: 'Testing Agent' },
],
tasks: [
{
id: 'implement',
type: 'implementation',
description: 'Implement the solution',
assignTo: 'implementer',
},
{
id: 'test',
type: 'testing',
description: 'Test the implementation',
assignTo: 'tester',
depends: ['implement'],
},
],
},
coordination: {
name: 'Multi-Agent Coordination',
description: 'Complex multi-agent coordination workflow',
agents: [
{ id: 'coordinator', type: 'coordinator', name: 'Coordinator Agent' },
{ id: 'worker1', type: 'implementer', name: 'Worker Agent 1' },
{ id: 'worker2', type: 'implementer', name: 'Worker Agent 2' },
],
tasks: [
{
id: 'plan',
type: 'planning',
description: 'Create execution plan',
assignTo: 'coordinator',
},
{
id: 'work1',
type: 'implementation',
description: 'Execute part 1',
assignTo: 'worker1',
depends: ['plan'],
},
{
id: 'work2',
type: 'implementation',
description: 'Execute part 2',
assignTo: 'worker2',
depends: ['plan'],
},
{
id: 'integrate',
type: 'integration',
description: 'Integrate results',
assignTo: 'coordinator',
depends: ['work1', 'work2'],
},
],
settings: {
maxConcurrency: 3,
failurePolicy: 'continue',
},
},
};
const template = templates[templateType];
if (!template) {
console.error(chalk.red(`Unknown template type: ${templateType}`));
console.log(chalk.gray('Available templates:'), Object.keys(templates).join(', '));
return;
}
const outputFile = options.output || `${templateType}-workflow.${options.format}`;
let content;
if (options.format === 'yaml') {
// In production, use a proper YAML library
console.log(chalk.yellow('YAML format not implemented, using JSON'));
content = JSON.stringify(template, null, 2);
}
else {
content = JSON.stringify(template, null, 2);
}
await fs.writeFile(outputFile, content);
console.log(chalk.green('✓ Workflow template generated'));
console.log(`${chalk.white('Template:')} ${templateType}`);
console.log(`${chalk.white('File:')} ${outputFile}`);
console.log(`${chalk.white('Tasks:')} ${template.tasks.length}`);
console.log(`${chalk.white('Agents:')} ${template.agents?.length || 0}`);
}
async function loadWorkflow(workflowFile) {
try {
const content = await fs.readFile(workflowFile, 'utf-8');
if (workflowFile.endsWith('.yaml') || workflowFile.endsWith('.yml')) {
// In production, use a proper YAML parser
throw new Error('YAML workflows not yet supported');
}
return JSON.parse(content);
}
catch (error) {
throw new Error(`Failed to load workflow file: ${getErrorMessage(error)}`);
}
}
async function validateWorkflowDefinition(workflow, strict = false) {
const errors = [];
// Basic validation
if (!workflow.name)
errors.push('Workflow name is required');
if (!workflow.tasks || workflow.tasks.length === 0)
errors.push('At least one task is required');
// Task validation
const taskIds = new Set();
for (const task of workflow.tasks || []) {
if (!task.id)
errors.push('Task ID is required');
if (taskIds.has(task.id))
errors.push(`Duplicate task ID: ${task.id}`);
taskIds.add(task.id);
if (!task.type)
errors.push(`Task ${task.id}: type is required`);
if (!task.description)
errors.push(`Task ${task.id}: description is required`);
// Validate dependencies
if (task.depends) {
for (const dep of task.depends) {
if (!taskIds.has(dep)) {
// Check if dependency exists in previous tasks
const taskIndex = workflow.tasks.indexOf(task);
const depExists = workflow.tasks.slice(0, taskIndex).some((t) => t.id === dep);
if (!depExists) {
errors.push(`Task ${task.id}: unknown dependency ${dep}`);
}
}
}
}
}
// Agent validation
if (workflow.agents) {
const agentIds = new Set();
for (const agent of workflow.agents) {
if (!agent.id)
errors.push('Agent ID is required');
if (agentIds.has(agent.id))
errors.push(`Duplicate agent ID: ${agent.id}`);
agentIds.add(agent.id);
if (!agent.type)
errors.push(`Agent ${agent.id}: type is required`);
}
// Validate task assignments
for (const task of workflow.tasks) {
if (task.assignTo && !agentIds.has(task.assignTo)) {
errors.push(`Task ${task.id}: assigned to unknown agent ${task.assignTo}`);
}
}
}
// Strict validation
if (strict) {
// Check for circular dependencies
const graph = new Map();
for (const task of workflow.tasks) {
graph.set(task.id, task.depends || []);
}
if (hasCircularDependencies(graph)) {
errors.push('Circular dependencies detected');
}
}
if (errors.length > 0) {
throw new Error('Workflow validation failed:\n• ' + errors.join('\n• '));
}
}
async function createExecution(workflow) {
const tasks = workflow.tasks.map((task) => ({
id: generateId('task-exec'),
taskId: task.id,
status: 'pending',
}));
return {
id: generateId('workflow-exec'),
workflowName: workflow.name,
status: 'pending',
startedAt: new Date(),
progress: {
total: tasks.length,
completed: 0,
failed: 0,
},
tasks,
};
}
async function executeWorkflow(execution, workflow, options) {
execution.status = 'running';
console.log(chalk.blue('Executing workflow...'));
console.log();
// Mock execution - in production, this would use the orchestrator
for (let i = 0; i < execution.tasks.length; i++) {
const taskExec = execution.tasks[i];
const taskDef = workflow.tasks.find((t) => t.id === taskExec.taskId);
console.log(`${chalk.cyan('→')} Starting task: ${taskDef.description}`);
taskExec.status = 'running';
taskExec.startedAt = new Date();
// Simulate task execution
await new Promise((resolve) => setTimeout(resolve, 1000 + Math.random() * 2000));
// Random success/failure for demo
const success = Math.random() > 0.1; // 90% success rate
if (success) {
taskExec.status = 'completed';
taskExec.completedAt = new Date();
execution.progress.completed++;
console.log(`${chalk.green('✓')} Completed: ${taskDef.description}`);
}
else {
taskExec.status = 'failed';
taskExec.completedAt = new Date();
taskExec.error = 'Simulated task failure';
execution.progress.failed++;
console.log(`${chalk.red('✗')} Failed: ${taskDef.description}`);
if (options.failFast || workflow.settings?.failurePolicy === 'fail-fast') {
execution.status = 'failed';
console.log(chalk.red('\nWorkflow failed (fail-fast mode)'));
return;
}
}
console.log();
}
execution.status = execution.progress.failed > 0 ? 'failed' : 'completed';
execution.completedAt = new Date();
const duration = formatDuration(execution.completedAt.getTime() - execution.startedAt.getTime());
if (execution.status === 'completed') {
console.log(chalk.green.bold('✓ Workflow completed successfully'));
}
else {
console.log(chalk.red.bold('✗ Workflow completed with failures'));
}
console.log(`${chalk.white('Duration:')} ${duration}`);
console.log(`${chalk.white('Tasks:')} ${execution.progress.completed}/${execution.progress.total} completed`);
if (execution.progress.failed > 0) {
console.log(`${chalk.white('Failed:')} ${execution.progress.failed}`);
}
}
async function executeWorkflowWithWatch(execution, workflow, options) {
console.log(chalk.yellow('Starting workflow execution in watch mode...'));
console.log(chalk.gray('Press Ctrl+C to stop\n'));
// Start execution in background and watch progress
const executionPromise = executeWorkflow(execution, workflow, options);
// Watch loop
const watchInterval = setInterval(() => {
displayWorkflowProgress(execution);
}, 1000);
try {
await executionPromise;
}
finally {
clearInterval(watchInterval);
displayWorkflowProgress(execution);
}
}
async function watchWorkflowStatus(workflowId) {
console.log(chalk.cyan('Watching workflow status...'));
console.log(chalk.gray('Press Ctrl+C to stop\n'));
// eslint-disable-next-line no-constant-condition
while (true) {
try {
console.clear();
const execution = await getWorkflowExecution(workflowId);
displayWorkflowStatus(execution);
if (execution.status === 'completed' ||
execution.status === 'failed' ||
execution.status === 'stopped') {
console.log('\n' + chalk.gray('Workflow finished. Exiting watch mode.'));
break;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
catch (error) {
console.error(chalk.red('Error watching workflow:'), error.message);
break;
}
}
}
function displayWorkflowStatus(execution) {
console.log(chalk.cyan.bold('Workflow Status'));
console.log('─'.repeat(50));
const statusIcon = formatStatusIndicator(execution.status);
const duration = execution.completedAt
? formatDuration(execution.completedAt.getTime() - execution.startedAt.getTime())
: formatDuration(Date.now() - execution.startedAt.getTime());
console.log(`${chalk.white('Name:')} ${execution.workflowName}`);
console.log(`${chalk.white('ID:')} ${execution.id}`);
console.log(`${chalk.white('Status:')} ${statusIcon} ${execution.status}`);
console.log(`${chalk.white('Started:')} ${execution.startedAt.toLocaleString()}`);
console.log(`${chalk.white('Duration:')} ${duration}`);
const progressBar = formatProgressBar(execution.progress.completed, execution.progress.total, 40, 'Progress');
console.log(`${progressBar} ${execution.progress.completed}/${execution.progress.total}`);
if (execution.progress.failed > 0) {
console.log(`${chalk.white('Failed Tasks:')} ${chalk.red(execution.progress.failed.toString())}`);
}
console.log();
// Task details
console.log(chalk.cyan.bold('Tasks'));
console.log('─'.repeat(50));
const table = new Table.default({
head: ['Task', 'Status', 'Duration', 'Agent'],
});
for (const taskExec of execution.tasks) {
const statusIcon = formatStatusIndicator(taskExec.status);
const duration = taskExec.completedAt && taskExec.startedAt
? formatDuration(taskExec.completedAt.getTime() - taskExec.startedAt.getTime())
: taskExec.startedAt
? formatDuration(Date.now() - taskExec.startedAt.getTime())
: '-';
table.push([
chalk.white(taskExec.taskId),
`${statusIcon} ${taskExec.status}`,
duration,
taskExec.assignedAgent || '-',
]);
}
console.log(table.toString());
}
function displayWorkflowProgress(execution) {
const progress = `${execution.progress.completed}/${execution.progress.total}`;
const progressBar = formatProgressBar(execution.progress.completed, execution.progress.total, 30);
console.log(`\r${progressBar} ${progress} tasks completed`);
}
async function getRunningWorkflows(includeAll = false) {
// Mock workflow list - in production, this would query the orchestrator
return [
{
id: 'workflow-001',
workflowName: 'Research Workflow',
status: 'running',
startedAt: new Date(Date.now() - 120000), // 2 minutes ago
progress: { total: 5, completed: 3, failed: 0 },
tasks: [],
},
{
id: 'workflow-002',
workflowName: 'Implementation Workflow',
status: 'completed',
startedAt: new Date(Date.now() - 300000), // 5 minutes ago
completedAt: new Date(Date.now() - 60000), // 1 minute ago
progress: { total: 3, completed: 3, failed: 0 },
tasks: [],
},
].filter((w) => includeAll || w.status === 'running');
}
async function getWorkflowExecution(workflowId) {
const workflows = await getRunningWorkflows(true);
const workflow = workflows.find((w) => w.id === workflowId || w.id.startsWith(workflowId));
if (!workflow) {
throw new Error(`Workflow '${workflowId}' not found`);
}
return workflow;
}
function hasCircularDependencies(graph) {
const visited = new Set();
const recursionStack = new Set();
function hasCycle(node) {
if (recursionStack.has(node))
return true;
if (visited.has(node))
return false;
visited.add(node);
recursionStack.add(node);
const dependencies = graph.get(node) || [];
for (const dep of dependencies) {
if (hasCycle(dep))
return true;
}
recursionStack.delete(node);
return false;
}
for (const node of graph.keys()) {
if (hasCycle(node))
return true;
}
return false;
}
//# sourceMappingURL=workflow.js.map