claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
884 lines • 41.7 kB
JavaScript
/**
* Workflow MCP Tools for CLI
*
* Tool definitions for workflow automation and orchestration.
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { getProjectCwd } from './types.js';
import { validateIdentifier, validatePath, validateText } from './validate-input.js';
import { executeAgentTask } from './agent-execute-core.js';
// Storage paths
const STORAGE_DIR = '.claude-flow';
const WORKFLOW_DIR = 'workflows';
const WORKFLOW_FILE = 'store.json';
function getWorkflowDir() {
return join(getProjectCwd(), STORAGE_DIR, WORKFLOW_DIR);
}
function getWorkflowPath() {
return join(getWorkflowDir(), WORKFLOW_FILE);
}
function ensureWorkflowDir() {
const dir = getWorkflowDir();
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
function loadWorkflowStore() {
try {
const path = getWorkflowPath();
if (existsSync(path)) {
const data = readFileSync(path, 'utf-8');
return JSON.parse(data);
}
}
catch {
// Return default store on error
}
return { workflows: {}, templates: {}, version: '3.0.0' };
}
function saveWorkflowStore(store) {
ensureWorkflowDir();
writeFileSync(getWorkflowPath(), JSON.stringify(store, null, 2), 'utf-8');
}
export const workflowTools = [
{
name: 'workflow_run',
description: 'Run a workflow from a template or file Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
template: { type: 'string', description: 'Template name to run' },
file: { type: 'string', description: 'Workflow file path' },
task: { type: 'string', description: 'Task description' },
options: {
type: 'object',
description: 'Workflow options',
properties: {
parallel: { type: 'boolean', description: 'Run stages in parallel' },
maxAgents: { type: 'number', description: 'Maximum agents to use' },
timeout: { type: 'number', description: 'Timeout in seconds' },
dryRun: { type: 'boolean', description: 'Validate without executing' },
},
},
},
},
handler: async (input) => {
// Validate user-provided input (#1425)
if (input.template) {
const v = validateIdentifier(input.template, 'template');
if (!v.valid)
return { success: false, error: v.error };
}
if (input.file) {
const v = validatePath(input.file, 'file');
if (!v.valid)
return { success: false, error: v.error };
}
if (input.task) {
const v = validateText(input.task, 'task');
if (!v.valid)
return { success: false, error: v.error };
}
const store = loadWorkflowStore();
const template = input.template;
const task = input.task;
const options = input.options || {};
const dryRun = options.dryRun;
// Build workflow from template or inline
const workflowId = `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const stages = [];
// Generate stages based on template
const templateName = template || 'custom';
const stageNames = (() => {
switch (templateName) {
case 'feature':
return ['Research', 'Design', 'Implement', 'Test', 'Review'];
case 'bugfix':
return ['Investigate', 'Fix', 'Test', 'Review'];
case 'refactor':
return ['Analyze', 'Refactor', 'Test', 'Review'];
case 'security':
return ['Scan', 'Analyze', 'Report'];
default:
return ['Execute'];
}
})();
for (const name of stageNames) {
stages.push({
name,
status: dryRun ? 'validated' : 'pending',
agents: [],
});
}
if (!dryRun) {
// Create and save the workflow
const steps = stageNames.map((name, i) => ({
stepId: `step-${i + 1}`,
name,
type: 'task',
config: { task: task || name },
status: 'pending',
}));
const workflow = {
workflowId,
name: task || `${templateName} workflow`,
description: task,
steps,
status: 'running',
currentStep: 0,
variables: { template: templateName, ...options },
createdAt: new Date().toISOString(),
startedAt: new Date().toISOString(),
};
store.workflows[workflowId] = workflow;
saveWorkflowStore(store);
}
return {
workflowId,
template: templateName,
status: dryRun ? 'validated' : 'running',
stages,
metrics: {
totalStages: stages.length,
completedStages: 0,
agentsSpawned: 0,
estimatedDuration: `${stages.length * 30}s`,
},
};
},
},
{
name: 'workflow_create',
description: 'Create a new workflow Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Workflow name' },
description: { type: 'string', description: 'Workflow description' },
steps: {
type: 'array',
description: 'Workflow steps',
items: {
type: 'object',
properties: {
name: { type: 'string' },
type: { type: 'string', enum: ['task', 'condition', 'parallel', 'loop', 'wait'] },
config: { type: 'object' },
},
},
},
variables: { type: 'object', description: 'Initial variables' },
},
required: ['name'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
const vName = validateText(input.name, 'name', 256);
if (!vName.valid)
return { success: false, error: vName.error };
if (input.description) {
const v = validateText(input.description, 'description');
if (!v.valid)
return { success: false, error: v.error };
}
const store = loadWorkflowStore();
const workflowId = `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const steps = (input.steps || []).map((s, i) => ({
stepId: `step-${i + 1}`,
name: s.name || `Step ${i + 1}`,
type: s.type || 'task',
config: s.config || {},
status: 'pending',
}));
const workflow = {
workflowId,
name: input.name,
description: input.description,
steps,
status: steps.length > 0 ? 'ready' : 'draft',
currentStep: 0,
variables: input.variables || {},
createdAt: new Date().toISOString(),
};
store.workflows[workflowId] = workflow;
saveWorkflowStore(store);
return {
workflowId,
name: workflow.name,
status: workflow.status,
stepCount: steps.length,
createdAt: workflow.createdAt,
};
},
},
{
name: 'workflow_execute',
description: 'Execute a workflow Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
workflowId: { type: 'string', description: 'Workflow ID to execute' },
variables: { type: 'object', description: 'Runtime variables to inject' },
startFromStep: { type: 'number', description: 'Step to start from (0-indexed)' },
},
required: ['workflowId'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
const vId = validateIdentifier(input.workflowId, 'workflowId');
if (!vId.valid)
return { success: false, error: vId.error };
const store = loadWorkflowStore();
const workflowId = input.workflowId;
const workflow = store.workflows[workflowId];
if (!workflow) {
return { workflowId, error: 'Workflow not found' };
}
if (workflow.status === 'running') {
return { workflowId, error: 'Workflow already running' };
}
// Inject runtime variables
if (input.variables) {
workflow.variables = { ...workflow.variables, ...input.variables };
}
workflow.status = 'running';
workflow.startedAt = new Date().toISOString();
workflow.currentStep = input.startFromStep || 0;
saveWorkflowStore(store);
// ADR-095 G3: real workflow runtime. Walk the steps in order;
// dispatch each by type. Persist progress after each step so a
// crash or pause can resume cleanly. No mock — task steps make
// real LLM calls via agent_execute (G1's wire).
const stepResults = [];
// Variable substitution: {{name}} → workflow.variables[name] OR steps[stepId].output
const interp = (text) => {
return text.replace(/\{\{\s*([a-zA-Z_][\w.]*)\s*\}\}/g, (_, key) => {
// Direct variable
if (key in workflow.variables)
return String(workflow.variables[key]);
// Step-output reference: stepId.output
const dot = key.indexOf('.');
if (dot > 0) {
const stepId = key.slice(0, dot);
const field = key.slice(dot + 1);
const prior = stepResults.find(s => s.stepId === stepId);
if (prior && field === 'output' && typeof prior.output === 'string')
return prior.output;
}
return '';
});
};
const startedAt = Date.now();
let i = workflow.currentStep;
while (i < workflow.steps.length) {
// Honor pause/cancel signals between steps.
const live = loadWorkflowStore().workflows[workflowId];
if (!live || live.status === 'paused') {
workflow.status = 'paused';
workflow.currentStep = i;
saveWorkflowStore(store);
break;
}
if (live.status === 'failed') {
workflow.status = 'failed';
saveWorkflowStore(store);
break;
}
const step = workflow.steps[i];
step.status = 'running';
step.startedAt = new Date().toISOString();
const stepStart = Date.now();
saveWorkflowStore(store);
let stepEntry = { stepId: step.stepId, type: step.type, status: 'running' };
try {
if (step.type === 'task') {
const cfg = step.config;
const agentId = cfg.agentId || workflow.variables.defaultAgentId;
const promptTpl = cfg.prompt || step.name;
if (!agentId)
throw new Error(`task step ${step.stepId} requires config.agentId or workflow.variables.defaultAgentId`);
const prompt = interp(promptTpl);
const result = await executeAgentTask({
agentId,
prompt,
systemPrompt: cfg.systemPrompt ? interp(String(cfg.systemPrompt)) : undefined,
maxTokens: cfg.maxTokens,
temperature: cfg.temperature,
timeoutMs: cfg.timeoutMs,
});
if (!result.success)
throw new Error(result.error || 'agent_execute failed');
step.result = result;
workflow.variables[`${step.stepId}.output`] = result.output;
workflow.variables.lastStepOutput = result.output;
stepEntry = { stepId: step.stepId, type: 'task', status: 'completed', durationMs: result.durationMs, output: result.output };
}
else if (step.type === 'wait') {
const cfg = step.config;
const ms = Math.min(Math.max(0, cfg.ms || 0), 60000);
await new Promise(r => setTimeout(r, ms));
step.result = { waitedMs: ms };
stepEntry = { stepId: step.stepId, type: 'wait', status: 'completed', durationMs: ms };
}
else if (step.type === 'condition') {
// Simple condition: config.when is a JS expression evaluated against workflow.variables.
// For safety, we only support `var === 'value'` or `var === number`.
const cfg = step.config;
const expr = String(cfg.when || 'true').trim();
const m = expr.match(/^([a-zA-Z_][\w]*)\s*===?\s*(['\"])?([^'\"]*)\2?$/);
let truthy = false;
if (m) {
const v = workflow.variables[m[1]];
const expected = m[2] ? m[3] : Number(m[3]);
truthy = v === expected;
}
else if (expr === 'true')
truthy = true;
step.result = { conditionExpr: expr, truthy };
// condition can declare a target step index to jump to via cfg.thenStep / cfg.elseStep
if (typeof cfg.thenStep === 'number' && truthy)
i = cfg.thenStep - 1;
if (typeof cfg.elseStep === 'number' && !truthy)
i = cfg.elseStep - 1;
stepEntry = { stepId: step.stepId, type: 'condition', status: 'completed' };
}
else {
// parallel/loop are deferred — mark skipped honestly rather than mock-completing.
step.result = { _note: `step type '${step.type}' not yet implemented in runtime` };
stepEntry = { stepId: step.stepId, type: step.type, status: 'skipped' };
}
step.status = stepEntry.status === 'skipped' ? 'skipped' : 'completed';
step.completedAt = new Date().toISOString();
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
step.status = 'failed';
step.result = { error: msg };
step.completedAt = new Date().toISOString();
stepEntry = { stepId: step.stepId, type: step.type, status: 'failed', durationMs: Date.now() - stepStart, error: msg };
stepResults.push(stepEntry);
workflow.status = 'failed';
workflow.error = msg;
workflow.completedAt = new Date().toISOString();
saveWorkflowStore(store);
return {
workflowId,
status: 'failed',
error: msg,
failedStep: step.stepId,
stepsCompleted: stepResults.filter(s => s.status === 'completed').length,
results: stepResults,
durationMs: Date.now() - startedAt,
};
}
if (typeof stepEntry.durationMs !== 'number')
stepEntry.durationMs = Date.now() - stepStart;
stepResults.push(stepEntry);
workflow.currentStep = i + 1;
saveWorkflowStore(store);
i++;
}
if (workflow.status === 'running') {
workflow.status = 'completed';
workflow.completedAt = new Date().toISOString();
saveWorkflowStore(store);
}
return {
workflowId,
status: workflow.status,
totalSteps: workflow.steps.length,
stepsCompleted: stepResults.filter(s => s.status === 'completed').length,
stepsSkipped: stepResults.filter(s => s.status === 'skipped').length,
stepsFailed: stepResults.filter(s => s.status === 'failed').length,
results: stepResults,
startedAt: workflow.startedAt,
completedAt: workflow.completedAt,
durationMs: Date.now() - startedAt,
};
},
},
{
name: 'workflow_status',
description: 'Get workflow status Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
workflowId: { type: 'string', description: 'Workflow ID' },
verbose: { type: 'boolean', description: 'Include step details' },
},
required: ['workflowId'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
const vId = validateIdentifier(input.workflowId, 'workflowId');
if (!vId.valid)
return { success: false, error: vId.error };
const store = loadWorkflowStore();
const workflowId = input.workflowId;
const workflow = store.workflows[workflowId];
if (!workflow) {
return { workflowId, error: 'Workflow not found' };
}
const completedSteps = workflow.steps.filter(s => s.status === 'completed').length;
const progress = workflow.steps.length > 0 ? (completedSteps / workflow.steps.length) * 100 : 0;
const status = {
workflowId: workflow.workflowId,
name: workflow.name,
status: workflow.status,
progress,
currentStep: workflow.currentStep,
totalSteps: workflow.steps.length,
completedSteps,
createdAt: workflow.createdAt,
startedAt: workflow.startedAt,
completedAt: workflow.completedAt,
};
if (input.verbose) {
return {
...status,
description: workflow.description,
variables: workflow.variables,
steps: workflow.steps.map(s => ({
stepId: s.stepId,
name: s.name,
type: s.type,
status: s.status,
startedAt: s.startedAt,
completedAt: s.completedAt,
})),
error: workflow.error,
};
}
return status;
},
},
{
name: 'workflow_list',
description: 'List all workflows Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', description: 'Filter by status' },
limit: { type: 'number', description: 'Max workflows to return' },
},
},
handler: async (input) => {
// Validate user-provided input (#1425)
if (input.status) {
const v = validateIdentifier(input.status, 'status');
if (!v.valid)
return { success: false, error: v.error };
}
const store = loadWorkflowStore();
let workflows = Object.values(store.workflows);
// Apply filters
if (input.status) {
workflows = workflows.filter(w => w.status === input.status);
}
// Sort by creation date (newest first)
workflows.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
// Apply limit
const limit = input.limit || 20;
workflows = workflows.slice(0, limit);
return {
workflows: workflows.map(w => ({
workflowId: w.workflowId,
name: w.name,
status: w.status,
stepCount: w.steps.length,
createdAt: w.createdAt,
completedAt: w.completedAt,
})),
total: workflows.length,
filters: { status: input.status },
};
},
},
{
name: 'workflow_pause',
description: 'Pause a running workflow Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
workflowId: { type: 'string', description: 'Workflow ID' },
},
required: ['workflowId'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
const vId = validateIdentifier(input.workflowId, 'workflowId');
if (!vId.valid)
return { success: false, error: vId.error };
const store = loadWorkflowStore();
const workflowId = input.workflowId;
const workflow = store.workflows[workflowId];
if (!workflow) {
return { workflowId, error: 'Workflow not found' };
}
if (workflow.status !== 'running') {
return { workflowId, error: 'Workflow not running' };
}
workflow.status = 'paused';
saveWorkflowStore(store);
return {
workflowId,
status: workflow.status,
pausedAt: new Date().toISOString(),
currentStep: workflow.currentStep,
};
},
},
{
name: 'workflow_resume',
description: 'Resume a paused workflow Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
workflowId: { type: 'string', description: 'Workflow ID' },
},
required: ['workflowId'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
const vId = validateIdentifier(input.workflowId, 'workflowId');
if (!vId.valid)
return { success: false, error: vId.error };
const store = loadWorkflowStore();
const workflowId = input.workflowId;
const workflow = store.workflows[workflowId];
if (!workflow) {
return { workflowId, error: 'Workflow not found' };
}
if (workflow.status !== 'paused') {
return { workflowId, error: 'Workflow not paused' };
}
workflow.status = 'running';
saveWorkflowStore(store);
// Report current step states — do not auto-complete them
const stepStates = workflow.steps.map(step => ({
stepId: step.stepId,
name: step.name,
status: step.status,
}));
const remainingSteps = workflow.steps.length - workflow.currentStep;
return {
workflowId,
status: workflow.status,
resumed: true,
currentStep: workflow.currentStep,
remainingSteps,
steps: stepStates,
_note: 'Workflow resumed. Steps remain in their current state and must be executed via task tools.',
};
},
},
{
name: 'workflow_cancel',
description: 'Cancel a workflow Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
workflowId: { type: 'string', description: 'Workflow ID' },
reason: { type: 'string', description: 'Cancellation reason' },
},
required: ['workflowId'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
const vId = validateIdentifier(input.workflowId, 'workflowId');
if (!vId.valid)
return { success: false, error: vId.error };
if (input.reason) {
const v = validateText(input.reason, 'reason');
if (!v.valid)
return { success: false, error: v.error };
}
const store = loadWorkflowStore();
const workflowId = input.workflowId;
const workflow = store.workflows[workflowId];
if (!workflow) {
return { workflowId, error: 'Workflow not found' };
}
if (workflow.status === 'completed' || workflow.status === 'failed') {
return { workflowId, error: 'Workflow already finished' };
}
workflow.status = 'failed';
workflow.error = input.reason || 'Cancelled by user';
workflow.completedAt = new Date().toISOString();
// Mark remaining steps as skipped
for (let i = workflow.currentStep; i < workflow.steps.length; i++) {
workflow.steps[i].status = 'skipped';
}
saveWorkflowStore(store);
return {
workflowId,
status: workflow.status,
cancelledAt: workflow.completedAt,
reason: workflow.error,
skippedSteps: workflow.steps.length - workflow.currentStep,
};
},
},
{
name: 'workflow_delete',
description: 'Delete a workflow Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
workflowId: { type: 'string', description: 'Workflow ID' },
},
required: ['workflowId'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
const vId = validateIdentifier(input.workflowId, 'workflowId');
if (!vId.valid)
return { success: false, error: vId.error };
const store = loadWorkflowStore();
const workflowId = input.workflowId;
if (!store.workflows[workflowId]) {
return { workflowId, error: 'Workflow not found' };
}
const workflow = store.workflows[workflowId];
if (workflow.status === 'running') {
return { workflowId, error: 'Cannot delete running workflow' };
}
delete store.workflows[workflowId];
saveWorkflowStore(store);
return {
workflowId,
deleted: true,
deletedAt: new Date().toISOString(),
};
},
},
{
name: 'workflow_template',
description: 'Save workflow as template or create from template Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, retry policy, pause/resume, and step-output binding across LLM-driven steps. For a single linear todo list, native TodoWrite is fine.',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
action: { type: 'string', enum: ['save', 'create', 'list'], description: 'Template action' },
workflowId: { type: 'string', description: 'Workflow ID (for save)' },
templateId: { type: 'string', description: 'Template ID (for create)' },
templateName: { type: 'string', description: 'Template name (for save)' },
newName: { type: 'string', description: 'New workflow name (for create)' },
},
required: ['action'],
},
handler: async (input) => {
// Validate user-provided input (#1425)
if (input.workflowId) {
const v = validateIdentifier(input.workflowId, 'workflowId');
if (!v.valid)
return { success: false, error: v.error };
}
if (input.templateId) {
const v = validateIdentifier(input.templateId, 'templateId');
if (!v.valid)
return { success: false, error: v.error };
}
if (input.templateName) {
const v = validateText(input.templateName, 'templateName', 256);
if (!v.valid)
return { success: false, error: v.error };
}
if (input.newName) {
const v = validateText(input.newName, 'newName', 256);
if (!v.valid)
return { success: false, error: v.error };
}
const store = loadWorkflowStore();
const action = input.action;
if (action === 'save') {
const workflow = store.workflows[input.workflowId];
if (!workflow) {
return { action, error: 'Workflow not found' };
}
const templateId = `template-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const template = {
...workflow,
workflowId: templateId,
name: input.templateName || `${workflow.name} Template`,
status: 'draft',
currentStep: 0,
createdAt: new Date().toISOString(),
startedAt: undefined,
completedAt: undefined,
};
// Reset step statuses
template.steps = template.steps.map(s => ({
...s,
status: 'pending',
result: undefined,
startedAt: undefined,
completedAt: undefined,
}));
store.templates[templateId] = template;
saveWorkflowStore(store);
return {
action,
templateId,
name: template.name,
savedAt: new Date().toISOString(),
};
}
if (action === 'create') {
const template = store.templates[input.templateId];
if (!template) {
return { action, error: 'Template not found' };
}
const workflowId = `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const workflow = {
...template,
workflowId,
name: input.newName || template.name.replace(' Template', ''),
status: 'ready',
createdAt: new Date().toISOString(),
};
store.workflows[workflowId] = workflow;
saveWorkflowStore(store);
return {
action,
workflowId,
name: workflow.name,
fromTemplate: input.templateId,
createdAt: workflow.createdAt,
};
}
if (action === 'list') {
return {
action,
templates: Object.values(store.templates).map(t => ({
templateId: t.workflowId,
name: t.name,
stepCount: t.steps.length,
createdAt: t.createdAt,
})),
total: Object.keys(store.templates).length,
};
}
return { action, error: 'Unknown action' };
},
},
{
// #1916: `ruflo workflow stop <id>` referenced an unregistered
// `workflow_stop` tool. Equivalent to workflow_cancel but returns the
// shape the CLI expects (`{ workflowId, stopped, stoppedAt }`).
name: 'workflow_stop',
description: 'Stop a running/paused workflow and skip its remaining steps. Use when native TodoWrite + sequential Bash is wrong because the work has a real dependency graph that needs persistence, pause/resume, and step-output binding — and you need to halt it cleanly mid-run. For a single linear todo list, native TodoWrite is fine. (Same effect as workflow_cancel; this name is what the CLI `workflow stop` subcommand calls.)',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
workflowId: { type: 'string', description: 'Workflow ID' },
graceful: { type: 'boolean', description: 'Let the current step finish (advisory)' },
},
required: ['workflowId'],
},
handler: async (input) => {
const vId = validateIdentifier(input.workflowId, 'workflowId');
if (!vId.valid)
return { success: false, error: vId.error };
const store = loadWorkflowStore();
const workflowId = input.workflowId;
const workflow = store.workflows[workflowId];
if (!workflow)
return { workflowId, error: 'Workflow not found' };
if (workflow.status === 'completed' || workflow.status === 'failed') {
return { workflowId, error: 'Workflow already finished' };
}
workflow.status = 'failed';
workflow.error = 'Stopped by user';
workflow.completedAt = new Date().toISOString();
for (let i = workflow.currentStep; i < workflow.steps.length; i++) {
workflow.steps[i].status = 'skipped';
}
saveWorkflowStore(store);
return { workflowId, stopped: true, stoppedAt: workflow.completedAt };
},
},
{
// #1916: `ruflo workflow validate -f <file>` referenced an unregistered
// `workflow_validate` tool. Structural sanity check (JSON workflow files);
// a full schema validator is a follow-up.
name: 'workflow_validate',
description: 'Structurally validate a workflow definition file (JSON) — checks it has a steps/stages/tasks array and that each step names an agent. Use when native Read is wrong because you want a parsed, structured pass/fail with error/warning lists and step/agent counts rather than eyeballing the file. For just reading the file, native Read is fine. (Basic checks today — a full workflow-schema validator is a tracked follow-up.)',
category: 'workflow',
inputSchema: {
type: 'object',
properties: {
file: { type: 'string', description: 'Path to the workflow definition file' },
strict: { type: 'boolean', description: 'Treat warnings as errors' },
},
required: ['file'],
},
handler: async (input) => {
const file = String(input.file ?? '');
const errors = [];
const warnings = [];
let stages = 0;
let agents = 0;
try {
if (!file || !existsSync(file)) {
errors.push({ line: 0, message: `File not found: ${file || '(empty)'}`, severity: 'error' });
}
else {
const raw = readFileSync(file, 'utf-8');
let doc = null;
if (/\.ya?ml$/i.test(file)) {
warnings.push({ line: 0, message: 'YAML workflow files are not schema-validated yet — only JSON is fully checked (#1916 follow-up)' });
try {
doc = JSON.parse(raw);
}
catch { /* not JSON; leave doc null */ }
}
else {
doc = JSON.parse(raw);
}
const d = (doc ?? {});
const steps = (d.steps ?? d.stages ?? d.tasks);
if (!Array.isArray(steps)) {
errors.push({ line: 0, message: 'Workflow has no `steps` / `stages` / `tasks` array', severity: 'error' });
}
else {
stages = steps.length;
const agentSet = new Set();
steps.forEach((s, i) => {
const step = (s ?? {});
const a = (step.agent ?? step.agentType ?? step.agent_type);
if (a)
agentSet.add(String(a));
else
warnings.push({ line: i + 1, message: `step ${i + 1} ("${step.name ?? step.id ?? i + 1}") names no agent` });
});
agents = agentSet.size;
}
}
}
catch (e) {
errors.push({ line: 0, message: `Parse error: ${e.message}`, severity: 'error' });
}
const valid = errors.length === 0 && (!input.strict || warnings.length === 0);
return {
valid,
file,
errors,
warnings,
stats: { stages, agents, estimatedDuration: stages > 0 ? `~${stages * 30}s` : 'unknown' },
};
},
},
];
//# sourceMappingURL=workflow-tools.js.map