Compare commits

...

2 Commits

Author SHA1 Message Date
github-actions[bot]
3d11093732 docs: auto-update documentation based on changes in next branch
This PR was automatically generated to update documentation based on recent changes.

  Original commit: feat: implement workflow (wip)\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-09-11 18:11:46 +00:00
Ralph Khreish
7c1d05958f feat: implement workflow (wip) 2025-09-11 11:03:31 -07:00
44 changed files with 4174 additions and 64 deletions

View File

@@ -28,6 +28,7 @@
}, },
"dependencies": { "dependencies": {
"@tm/core": "*", "@tm/core": "*",
"@tm/workflow-engine": "*",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"chalk": "5.6.2", "chalk": "5.6.2",
"cli-table3": "^0.6.5", "cli-table3": "^0.6.5",

View File

@@ -494,17 +494,6 @@ export class AuthCommand extends Command {
/** /**
* Static method to register this command on an existing program * Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const authCommand = new AuthCommand();
program.addCommand(authCommand);
return authCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/ */
static register(program: Command, name?: string): AuthCommand { static register(program: Command, name?: string): AuthCommand {
const authCommand = new AuthCommand(name); const authCommand = new AuthCommand(name);

View File

@@ -0,0 +1,38 @@
/**
* Command registry - exports all CLI commands for central registration
*/
import type { Command } from 'commander';
import { ListTasksCommand } from './list.command.js';
import { AuthCommand } from './auth.command.js';
import WorkflowCommand from './workflow.command.js';
// Define interface for command classes that can register themselves
export interface CommandRegistrar {
register(program: Command, name?: string): any;
}
// Future commands can be added here as they're created
// The pattern is: each command exports a class with a static register(program: Command, name?: string) method
/**
* Auto-register all exported commands that implement the CommandRegistrar interface
*/
export function registerAllCommands(program: Command): void {
// Get all exports from this module
const commands = [
ListTasksCommand,
AuthCommand,
WorkflowCommand
// Add new commands here as they're imported above
];
commands.forEach((CommandClass) => {
if (
'register' in CommandClass &&
typeof CommandClass.register === 'function'
) {
CommandClass.register(program);
}
});
}

View File

@@ -315,17 +315,6 @@ export class ListTasksCommand extends Command {
/** /**
* Static method to register this command on an existing program * Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const listCommand = new ListTasksCommand();
program.addCommand(listCommand);
return listCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/ */
static register(program: Command, name?: string): ListTasksCommand { static register(program: Command, name?: string): ListTasksCommand {
const listCommand = new ListTasksCommand(name); const listCommand = new ListTasksCommand(name);

View File

@@ -0,0 +1,58 @@
/**
* @fileoverview Workflow Command
* Main workflow command with subcommands
*/
import { Command } from 'commander';
import {
WorkflowStartCommand,
WorkflowListCommand,
WorkflowStopCommand,
WorkflowStatusCommand
} from './workflow/index.js';
/**
* WorkflowCommand - Main workflow command with subcommands
*/
export class WorkflowCommand extends Command {
constructor(name?: string) {
super(name || 'workflow');
this.description('Manage task execution workflows with git worktrees and Claude Code')
.alias('wf');
// Register subcommands
this.addSubcommands();
}
private addSubcommands(): void {
// Start workflow
WorkflowStartCommand.register(this);
// List workflows
WorkflowListCommand.register(this);
// Stop workflow
WorkflowStopCommand.register(this);
// Show workflow status
WorkflowStatusCommand.register(this);
// Alias commands for convenience
this.addCommand(new WorkflowStartCommand('run')); // tm workflow run <task-id>
this.addCommand(new WorkflowStopCommand('kill')); // tm workflow kill <workflow-id>
this.addCommand(new WorkflowStatusCommand('info')); // tm workflow info <workflow-id>
}
/**
* Static method to register this command on an existing program
*/
static register(program: Command, name?: string): WorkflowCommand {
const workflowCommand = new WorkflowCommand(name);
program.addCommand(workflowCommand);
return workflowCommand;
}
}
export default WorkflowCommand;

View File

@@ -0,0 +1,9 @@
/**
* @fileoverview Workflow Commands
* Exports for all workflow-related CLI commands
*/
export * from './workflow-start.command.js';
export * from './workflow-list.command.js';
export * from './workflow-stop.command.js';
export * from './workflow-status.command.js';

View File

@@ -0,0 +1,253 @@
/**
* @fileoverview Workflow List Command
* List active and recent workflow executions
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig,
type WorkflowExecutionContext
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowListOptions {
project?: string;
status?: string;
format?: 'text' | 'json' | 'compact';
worktreeBase?: string;
claude?: string;
all?: boolean;
}
/**
* WorkflowListCommand - List workflow executions
*/
export class WorkflowListCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'list');
this.description('List active and recent workflow executions')
.alias('ls')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('-s, --status <status>', 'Filter by status (running, completed, failed, etc.)')
.option('-f, --format <format>', 'Output format (text, json, compact)', 'text')
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('--all', 'Show all workflows including completed ones')
.action(async (options: WorkflowListOptions) => {
await this.executeCommand(options);
});
}
private async executeCommand(options: WorkflowListOptions): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
// Get workflows
let workflows = this.workflowManager!.listWorkflows();
// Apply status filter
if (options.status) {
workflows = workflows.filter(w => w.status === options.status);
}
// Apply active filter (default behavior)
if (!options.all) {
workflows = workflows.filter(w =>
['pending', 'initializing', 'running', 'paused'].includes(w.status)
);
}
// Display results
this.displayResults(workflows, options);
} catch (error: any) {
ui.displayError(error.message || 'Failed to list workflows');
process.exit(1);
}
}
private async initializeWorkflowManager(options: WorkflowListOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private displayResults(workflows: WorkflowExecutionContext[], options: WorkflowListOptions): void {
switch (options.format) {
case 'json':
this.displayJson(workflows);
break;
case 'compact':
this.displayCompact(workflows);
break;
case 'text':
default:
this.displayText(workflows);
break;
}
}
private displayJson(workflows: WorkflowExecutionContext[]): void {
console.log(JSON.stringify({
workflows: workflows.map(w => ({
workflowId: `workflow-${w.taskId}`,
taskId: w.taskId,
taskTitle: w.taskTitle,
status: w.status,
worktreePath: w.worktreePath,
branchName: w.branchName,
processId: w.processId,
startedAt: w.startedAt,
lastActivity: w.lastActivity,
metadata: w.metadata
})),
total: workflows.length,
timestamp: new Date().toISOString()
}, null, 2));
}
private displayCompact(workflows: WorkflowExecutionContext[]): void {
if (workflows.length === 0) {
console.log(chalk.gray('No workflows found'));
return;
}
workflows.forEach(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
const statusDisplay = this.getStatusDisplay(workflow.status);
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
console.log(
`${chalk.cyan(workflowId)} ${statusDisplay} ${workflow.taskTitle} ${chalk.gray(`(${duration})`)}`
);
});
}
private displayText(workflows: WorkflowExecutionContext[]): void {
ui.displayBanner('Active Workflows');
if (workflows.length === 0) {
ui.displayWarning('No workflows found');
console.log();
console.log(chalk.blue('💡 Start a new workflow with:'));
console.log(` ${chalk.cyan('tm workflow start <task-id>')}`);
return;
}
// Statistics
console.log(chalk.blue.bold('\n📊 Statistics:\n'));
const statusCounts = this.getStatusCounts(workflows);
Object.entries(statusCounts).forEach(([status, count]) => {
console.log(` ${this.getStatusDisplay(status)}: ${chalk.cyan(count)}`);
});
// Workflows table
console.log(chalk.blue.bold(`\n🔄 Workflows (${workflows.length}):\n`));
const tableData = workflows.map(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
return [
chalk.cyan(workflowId),
chalk.yellow(workflow.taskId),
workflow.taskTitle.substring(0, 30) + (workflow.taskTitle.length > 30 ? '...' : ''),
this.getStatusDisplay(workflow.status),
workflow.processId ? chalk.green(workflow.processId.toString()) : chalk.gray('N/A'),
chalk.gray(duration),
chalk.gray(path.basename(workflow.worktreePath))
];
});
console.log(ui.createTable(
['Workflow ID', 'Task ID', 'Task Title', 'Status', 'PID', 'Duration', 'Worktree'],
tableData
));
// Running workflows actions
const runningWorkflows = workflows.filter(w => w.status === 'running');
if (runningWorkflows.length > 0) {
console.log(chalk.blue.bold('\n🚀 Quick Actions:\n'));
runningWorkflows.slice(0, 3).forEach(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
console.log(` • Attach to ${chalk.cyan(workflowId)}: ${chalk.gray(`tm workflow attach ${workflowId}`)}`);
});
if (runningWorkflows.length > 3) {
console.log(` ${chalk.gray(`... and ${runningWorkflows.length - 3} more`)}`);
}
}
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.orange },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || { icon: '❓', color: chalk.white };
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
private getStatusCounts(workflows: WorkflowExecutionContext[]): Record<string, number> {
const counts: Record<string, number> = {};
workflows.forEach(workflow => {
counts[workflow.status] = (counts[workflow.status] || 0) + 1;
});
return counts;
}
private formatDuration(start: Date, end: Date): string {
const diff = end.getTime() - start.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
return `${minutes}m`;
} else {
return '<1m';
}
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowListCommand {
const command = new WorkflowListCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -0,0 +1,239 @@
/**
* @fileoverview Workflow Start Command
* Start task execution in isolated worktree with Claude Code process
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
createTaskMasterCore,
type TaskMasterCore
} from '@tm/core';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStartOptions {
project?: string;
branch?: string;
timeout?: number;
worktreeBase?: string;
claude?: string;
debug?: boolean;
env?: string;
}
/**
* WorkflowStartCommand - Start task execution workflow
*/
export class WorkflowStartCommand extends Command {
private tmCore?: TaskMasterCore;
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'start');
this.description('Start task execution in isolated worktree')
.argument('<task-id>', 'Task ID to execute')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('-b, --branch <name>', 'Custom branch name for worktree')
.option('-t, --timeout <minutes>', 'Execution timeout in minutes', '60')
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('--debug', 'Enable debug logging')
.option('--env <vars>', 'Environment variables (KEY=VALUE,KEY2=VALUE2)')
.action(async (taskId: string, options: WorkflowStartOptions) => {
await this.executeCommand(taskId, options);
});
}
private async executeCommand(taskId: string, options: WorkflowStartOptions): Promise<void> {
try {
// Initialize components
await this.initializeCore(options.project || process.cwd());
await this.initializeWorkflowManager(options);
// Get task details
const task = await this.getTask(taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
// Check if task already has active workflow
const existingWorkflow = this.workflowManager!.getWorkflowByTaskId(taskId);
if (existingWorkflow) {
ui.displayWarning(`Task ${taskId} already has an active workflow`);
console.log(`Workflow ID: ${chalk.cyan('workflow-' + taskId)}`);
console.log(`Status: ${this.getStatusDisplay(existingWorkflow.status)}`);
console.log(`Worktree: ${chalk.gray(existingWorkflow.worktreePath)}`);
return;
}
// Parse environment variables
const env = this.parseEnvironmentVariables(options.env);
// Display task info
ui.displayBanner(`Starting Workflow for Task ${taskId}`);
console.log(`${chalk.blue('Task:')} ${task.title}`);
console.log(`${chalk.blue('Description:')} ${task.description}`);
if (task.dependencies?.length) {
console.log(`${chalk.blue('Dependencies:')} ${task.dependencies.join(', ')}`);
}
console.log(`${chalk.blue('Priority:')} ${task.priority || 'normal'}`);
console.log();
// Start workflow
ui.displaySpinner('Creating worktree and starting Claude Code process...');
const workflowId = await this.workflowManager!.startTaskExecution(task, {
branchName: options.branch,
timeout: parseInt(options.timeout || '60'),
env
});
const workflow = this.workflowManager!.getWorkflowStatus(workflowId);
ui.displaySuccess('Workflow started successfully!');
console.log();
console.log(`${chalk.green('✓')} Workflow ID: ${chalk.cyan(workflowId)}`);
console.log(`${chalk.green('✓')} Worktree: ${chalk.gray(workflow?.worktreePath)}`);
console.log(`${chalk.green('✓')} Branch: ${chalk.gray(workflow?.branchName)}`);
console.log(`${chalk.green('✓')} Process ID: ${chalk.gray(workflow?.processId)}`);
console.log();
// Display next steps
console.log(chalk.blue.bold('📋 Next Steps:'));
console.log(` • Monitor: ${chalk.cyan(`tm workflow status ${workflowId}`)}`);
console.log(` • Attach: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
console.log();
// Setup event listeners for real-time updates
this.setupEventListeners();
} catch (error: any) {
ui.displayError(error.message || 'Failed to start workflow');
if (options.debug && error.stack) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) {
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
}
}
private async initializeWorkflowManager(options: WorkflowStartOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: parseInt(options.timeout || '60'),
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: options.debug || false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async getTask(taskId: string) {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
const result = await this.tmCore.getTaskList({});
return result.tasks.find(task => task.id === taskId);
}
private parseEnvironmentVariables(envString?: string): Record<string, string> | undefined {
if (!envString) return undefined;
const env: Record<string, string> = {};
for (const pair of envString.split(',')) {
const [key, ...valueParts] = pair.trim().split('=');
if (key && valueParts.length > 0) {
env[key] = valueParts.join('=');
}
}
return Object.keys(env).length > 0 ? env : undefined;
}
private getStatusDisplay(status: string): string {
const colors = {
pending: chalk.yellow,
initializing: chalk.blue,
running: chalk.green,
paused: chalk.orange,
completed: chalk.green,
failed: chalk.red,
cancelled: chalk.gray,
timeout: chalk.red
};
const color = colors[status as keyof typeof colors] || chalk.white;
return color(status);
}
private setupEventListeners(): void {
if (!this.workflowManager) return;
this.workflowManager.on('workflow.started', (event) => {
console.log(`${chalk.green('🚀')} Workflow started: ${event.workflowId}`);
});
this.workflowManager.on('process.output', (event) => {
if (event.data?.stream === 'stdout') {
console.log(`${chalk.blue('[OUT]')} ${event.data.data.trim()}`);
} else if (event.data?.stream === 'stderr') {
console.log(`${chalk.red('[ERR]')} ${event.data.data.trim()}`);
}
});
this.workflowManager.on('workflow.completed', (event) => {
console.log(`${chalk.green('✅')} Workflow completed: ${event.workflowId}`);
});
this.workflowManager.on('workflow.failed', (event) => {
console.log(`${chalk.red('❌')} Workflow failed: ${event.workflowId}`);
if (event.error) {
console.log(`${chalk.red('Error:')} ${event.error.message}`);
}
});
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
// Don't cleanup workflows, just disconnect
this.workflowManager.removeAllListeners();
}
if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined;
}
}
static register(program: Command, name?: string): WorkflowStartCommand {
const command = new WorkflowStartCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -0,0 +1,339 @@
/**
* @fileoverview Workflow Status Command
* Show detailed status of a specific workflow
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStatusOptions {
project?: string;
worktreeBase?: string;
claude?: string;
watch?: boolean;
format?: 'text' | 'json';
}
/**
* WorkflowStatusCommand - Show workflow execution status
*/
export class WorkflowStatusCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'status');
this.description('Show detailed status of a workflow execution')
.argument('<workflow-id>', 'Workflow ID or task ID to check')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('-w, --watch', 'Watch for status changes (refresh every 2 seconds)')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.action(async (workflowId: string, options: WorkflowStatusOptions) => {
await this.executeCommand(workflowId, options);
});
}
private async executeCommand(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
if (options.watch) {
await this.watchWorkflowStatus(workflowId, options);
} else {
await this.showWorkflowStatus(workflowId, options);
}
} catch (error: any) {
ui.displayError(error.message || 'Failed to get workflow status');
process.exit(1);
}
}
private async initializeWorkflowManager(options: WorkflowStatusOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async showWorkflowStatus(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
// Try to find workflow by ID or task ID
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
// Try as task ID
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}
if (options.format === 'json') {
this.displayJsonStatus(workflow);
} else {
this.displayTextStatus(workflow);
}
}
private async watchWorkflowStatus(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
console.log(chalk.blue.bold('👀 Watching workflow status (Press Ctrl+C to exit)\n'));
let lastStatus = '';
let updateCount = 0;
const updateStatus = async () => {
try {
// Clear screen and move cursor to top
if (updateCount > 0) {
process.stdout.write('\x1b[2J\x1b[0f');
}
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
console.log(chalk.red(`Workflow not found: ${workflowId}`));
return;
}
// Display header with timestamp
console.log(chalk.blue.bold('👀 Watching Workflow Status'));
console.log(chalk.gray(`Last updated: ${new Date().toLocaleTimeString()}\n`));
this.displayTextStatus(workflow);
// Check if workflow has ended
if (['completed', 'failed', 'cancelled', 'timeout'].includes(workflow.status)) {
console.log(chalk.yellow('\n⚠ Workflow has ended. Stopping watch mode.'));
return;
}
updateCount++;
} catch (error) {
console.error(chalk.red('Error updating status:'), error);
}
};
// Initial display
await updateStatus();
// Setup interval for updates
const interval = setInterval(updateStatus, 2000);
// Handle Ctrl+C
process.on('SIGINT', () => {
clearInterval(interval);
console.log(chalk.yellow('\n👋 Stopped watching workflow status'));
process.exit(0);
});
// Keep the process alive
await new Promise(() => {});
}
private displayJsonStatus(workflow: any): void {
const status = {
workflowId: `workflow-${workflow.taskId}`,
taskId: workflow.taskId,
taskTitle: workflow.taskTitle,
taskDescription: workflow.taskDescription,
status: workflow.status,
worktreePath: workflow.worktreePath,
branchName: workflow.branchName,
processId: workflow.processId,
startedAt: workflow.startedAt,
lastActivity: workflow.lastActivity,
duration: this.calculateDuration(workflow.startedAt, workflow.lastActivity),
metadata: workflow.metadata
};
console.log(JSON.stringify(status, null, 2));
}
private displayTextStatus(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
ui.displayBanner(`Workflow Status: ${workflowId}`);
// Basic information
console.log(chalk.blue.bold('\n📋 Basic Information:\n'));
console.log(` Workflow ID: ${chalk.cyan(workflowId)}`);
console.log(` Task ID: ${chalk.cyan(workflow.taskId)}`);
console.log(` Task Title: ${workflow.taskTitle}`);
console.log(` Status: ${this.getStatusDisplay(workflow.status)}`);
console.log(` Duration: ${chalk.gray(duration)}`);
// Task details
if (workflow.taskDescription) {
console.log(chalk.blue.bold('\n📝 Task Details:\n'));
console.log(` ${workflow.taskDescription}`);
}
// Process information
console.log(chalk.blue.bold('\n⚙ Process Information:\n'));
console.log(` Process ID: ${workflow.processId ? chalk.green(workflow.processId) : chalk.gray('N/A')}`);
console.log(` Worktree: ${chalk.gray(workflow.worktreePath)}`);
console.log(` Branch: ${chalk.gray(workflow.branchName)}`);
// Timing information
console.log(chalk.blue.bold('\n⏰ Timing:\n'));
console.log(` Started: ${chalk.gray(workflow.startedAt.toLocaleString())}`);
console.log(` Last Activity: ${chalk.gray(workflow.lastActivity.toLocaleString())}`);
// Metadata
if (workflow.metadata && Object.keys(workflow.metadata).length > 0) {
console.log(chalk.blue.bold('\n🔖 Metadata:\n'));
Object.entries(workflow.metadata).forEach(([key, value]) => {
console.log(` ${key}: ${chalk.gray(String(value))}`);
});
}
// Status-specific information
this.displayStatusSpecificInfo(workflow);
// Actions
this.displayAvailableActions(workflow);
}
private displayStatusSpecificInfo(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
switch (workflow.status) {
case 'running':
console.log(chalk.blue.bold('\n🚀 Running Status:\n'));
console.log(` ${chalk.green('●')} Process is actively executing`);
console.log(` ${chalk.blue('')} Monitor output with: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
break;
case 'paused':
console.log(chalk.blue.bold('\n⏸ Paused Status:\n'));
console.log(` ${chalk.yellow('●')} Workflow is paused`);
console.log(` ${chalk.blue('')} Resume with: ${chalk.cyan(`tm workflow resume ${workflowId}`)}`);
break;
case 'completed':
console.log(chalk.blue.bold('\n✅ Completed Status:\n'));
console.log(` ${chalk.green('●')} Workflow completed successfully`);
console.log(` ${chalk.blue('')} Resources have been cleaned up`);
break;
case 'failed':
console.log(chalk.blue.bold('\n❌ Failed Status:\n'));
console.log(` ${chalk.red('●')} Workflow execution failed`);
console.log(` ${chalk.blue('')} Check logs for error details`);
break;
case 'initializing':
console.log(chalk.blue.bold('\n🔄 Initializing Status:\n'));
console.log(` ${chalk.blue('●')} Setting up worktree and process`);
console.log(` ${chalk.blue('')} This should complete shortly`);
break;
}
}
private displayAvailableActions(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
console.log(chalk.blue.bold('\n🎯 Available Actions:\n'));
switch (workflow.status) {
case 'running':
console.log(` • Attach: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
console.log(` • Pause: ${chalk.cyan(`tm workflow pause ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'paused':
console.log(` • Resume: ${chalk.cyan(`tm workflow resume ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'pending':
case 'initializing':
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'completed':
case 'failed':
case 'cancelled':
console.log(` • View logs: ${chalk.cyan(`tm workflow logs ${workflowId}`)}`);
console.log(` • Start new: ${chalk.cyan(`tm workflow start ${workflow.taskId}`)}`);
break;
}
console.log(` • List all: ${chalk.cyan('tm workflow list')}`);
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.orange },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || { icon: '❓', color: chalk.white };
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
private formatDuration(start: Date, end: Date): string {
const diff = end.getTime() - start.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(minutes / 60);
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds}s`;
} else {
return `${seconds}s`;
}
}
private calculateDuration(start: Date, end: Date): number {
return Math.floor((end.getTime() - start.getTime()) / 1000);
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowStatusCommand {
const command = new WorkflowStatusCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -0,0 +1,260 @@
/**
* @fileoverview Workflow Stop Command
* Stop and clean up workflow execution
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStopOptions {
project?: string;
worktreeBase?: string;
claude?: string;
force?: boolean;
all?: boolean;
}
/**
* WorkflowStopCommand - Stop workflow execution
*/
export class WorkflowStopCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'stop');
this.description('Stop workflow execution and clean up resources')
.argument('[workflow-id]', 'Workflow ID to stop (or task ID)')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option(
'--worktree-base <path>',
'Base directory for worktrees',
'../task-worktrees'
)
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('-f, --force', 'Force stop (kill process immediately)')
.option('--all', 'Stop all running workflows')
.action(
async (
workflowId: string | undefined,
options: WorkflowStopOptions
) => {
await this.executeCommand(workflowId, options);
}
);
}
private async executeCommand(
workflowId: string | undefined,
options: WorkflowStopOptions
): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
if (options.all) {
await this.stopAllWorkflows(options);
} else if (workflowId) {
await this.stopSingleWorkflow(workflowId, options);
} else {
ui.displayError('Please specify a workflow ID or use --all flag');
process.exit(1);
}
} catch (error: any) {
ui.displayError(error.message || 'Failed to stop workflow');
process.exit(1);
}
}
private async initializeWorkflowManager(
options: WorkflowStopOptions
): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(
projectRoot,
options.worktreeBase || '../task-worktrees'
);
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async stopSingleWorkflow(
workflowId: string,
options: WorkflowStopOptions
): Promise<void> {
// Try to find workflow by ID or task ID
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
// Try as task ID
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}
const actualWorkflowId = `workflow-${workflow.taskId}`;
// Display workflow info
console.log(chalk.blue.bold(`🛑 Stopping Workflow: ${actualWorkflowId}`));
console.log(`${chalk.blue('Task:')} ${workflow.taskTitle}`);
console.log(
`${chalk.blue('Status:')} ${this.getStatusDisplay(workflow.status)}`
);
console.log(
`${chalk.blue('Worktree:')} ${chalk.gray(workflow.worktreePath)}`
);
if (workflow.processId) {
console.log(
`${chalk.blue('Process ID:')} ${chalk.gray(workflow.processId)}`
);
}
console.log();
// Confirm if not forced
if (!options.force && ['running', 'paused'].includes(workflow.status)) {
const shouldProceed = await ui.confirm(
`Are you sure you want to stop this ${workflow.status} workflow?`
);
if (!shouldProceed) {
console.log(chalk.gray('Operation cancelled'));
return;
}
}
// Stop the workflow
ui.displaySpinner('Stopping workflow and cleaning up resources...');
await this.workflowManager!.stopTaskExecution(
actualWorkflowId,
options.force
);
ui.displaySuccess('Workflow stopped successfully!');
console.log();
console.log(`${chalk.green('✓')} Process terminated`);
console.log(`${chalk.green('✓')} Worktree cleaned up`);
console.log(`${chalk.green('✓')} State updated`);
}
private async stopAllWorkflows(options: WorkflowStopOptions): Promise<void> {
const workflows = this.workflowManager!.listWorkflows();
const activeWorkflows = workflows.filter((w) =>
['pending', 'initializing', 'running', 'paused'].includes(w.status)
);
if (activeWorkflows.length === 0) {
ui.displayWarning('No active workflows to stop');
return;
}
console.log(
chalk.blue.bold(`🛑 Stopping ${activeWorkflows.length} Active Workflows`)
);
console.log();
// List workflows to be stopped
activeWorkflows.forEach((workflow) => {
console.log(
`${chalk.cyan(`workflow-${workflow.taskId}`)} - ${workflow.taskTitle} ${this.getStatusDisplay(workflow.status)}`
);
});
console.log();
// Confirm if not forced
if (!options.force) {
const shouldProceed = await ui.confirm(
`Are you sure you want to stop all ${activeWorkflows.length} active workflows?`
);
if (!shouldProceed) {
console.log(chalk.gray('Operation cancelled'));
return;
}
}
// Stop all workflows
ui.displaySpinner('Stopping all workflows...');
let stopped = 0;
let failed = 0;
for (const workflow of activeWorkflows) {
try {
const workflowId = `workflow-${workflow.taskId}`;
await this.workflowManager!.stopTaskExecution(
workflowId,
options.force
);
stopped++;
} catch (error) {
console.error(
`${chalk.red('✗')} Failed to stop workflow ${workflow.taskId}: ${error}`
);
failed++;
}
}
console.log();
if (stopped > 0) {
ui.displaySuccess(`Successfully stopped ${stopped} workflows`);
}
if (failed > 0) {
ui.displayWarning(`Failed to stop ${failed} workflows`);
}
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.hex('#FFA500') },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || {
icon: '❓',
color: chalk.white
};
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowStopCommand {
const command = new WorkflowStopCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -6,8 +6,12 @@
// Commands // Commands
export { ListTasksCommand } from './commands/list.command.js'; export { ListTasksCommand } from './commands/list.command.js';
export { AuthCommand } from './commands/auth.command.js'; export { AuthCommand } from './commands/auth.command.js';
export { WorkflowCommand } from './commands/workflow.command.js';
export { ContextCommand } from './commands/context.command.js'; export { ContextCommand } from './commands/context.command.js';
// Command registry
export { registerAllCommands } from './commands/index.js';
// UI utilities (for other commands to use) // UI utilities (for other commands to use)
export * as ui from './utils/ui.js'; export * as ui from './utils/ui.js';

View File

@@ -324,3 +324,61 @@ export function createTaskTable(
return table.toString(); return table.toString();
} }
/**
* Display a spinner with message (mock implementation)
*/
export function displaySpinner(message: string): void {
console.log(chalk.blue('◐'), chalk.gray(message));
}
/**
* Simple confirmation prompt
*/
export async function confirm(message: string): Promise<boolean> {
// For now, return true. In a real implementation, use inquirer
console.log(chalk.yellow('?'), chalk.white(message), chalk.gray('(y/n)'));
// Mock implementation - in production this would use inquirer
return new Promise((resolve) => {
process.stdin.once('data', (data) => {
const answer = data.toString().trim().toLowerCase();
resolve(answer === 'y' || answer === 'yes');
});
process.stdin.resume();
});
}
/**
* Create a generic table
*/
export function createTable(headers: string[], rows: string[][]): string {
const table = new Table({
head: headers.map(h => chalk.blue.bold(h)),
style: {
head: [],
border: ['gray']
},
chars: {
'top': '─',
'top-mid': '┬',
'top-left': '┌',
'top-right': '┐',
'bottom': '─',
'bottom-mid': '┴',
'bottom-left': '└',
'bottom-right': '┘',
'left': '│',
'left-mid': '├',
'mid': '─',
'mid-mid': '┼',
'right': '│',
'right-mid': '┤',
'middle': '│'
}
});
rows.forEach(row => table.push(row));
return table.toString();
}

View File

@@ -200,6 +200,34 @@ sidebarTitle: "CLI Commands"
``` ```
</Accordion> </Accordion>
<Accordion title="Workflow Management">
```bash
# Start workflow execution for a task
task-master workflow start <task-id>
# or use alias
task-master workflow run <task-id>
# List all active workflows
task-master workflow list
# Check status of a specific workflow
task-master workflow status <workflow-id>
# or use alias
task-master workflow info <workflow-id>
# Stop a running workflow
task-master workflow stop <workflow-id>
# or use alias
task-master workflow kill <workflow-id>
```
The workflow system executes tasks in isolated git worktrees with dedicated Claude Code processes, providing:
- **Isolated Execution**: Each task runs in its own git worktree
- **Process Management**: Spawns dedicated Claude Code processes
- **Real-time Monitoring**: Track progress and output
- **Parallel Execution**: Run multiple tasks concurrently
</Accordion>
<Accordion title="Initialize a Project"> <Accordion title="Initialize a Project">
```bash ```bash
# Initialize a new project with Task Master structure # Initialize a new project with Task Master structure

View File

@@ -0,0 +1,221 @@
---
title: "Workflow Engine"
sidebarTitle: "Workflows"
---
The Task Master Workflow Engine provides advanced task execution capabilities with git worktree isolation and Claude Code process management.
## Overview
The workflow system extends Task Master with powerful execution features:
- **Git Worktree Isolation**: Each task runs in its own isolated git worktree
- **Process Sandboxing**: Spawns dedicated Claude Code processes for task execution
- **Real-time Monitoring**: Track workflow progress and process output
- **State Management**: Persistent workflow state across sessions
- **Parallel Execution**: Run multiple tasks concurrently with resource limits
## Quick Start
### Starting a Workflow
```bash
# Start workflow for a specific task
task-master workflow start 1.2
# Using the alias
task-master workflow run 1.2
```
### Monitoring Workflows
```bash
# List all active workflows
task-master workflow list
# Check specific workflow status
task-master workflow status workflow-1.2-1234567890-abc123
# Using the alias
task-master workflow info workflow-1.2-1234567890-abc123
```
### Stopping Workflows
```bash
# Stop a running workflow
task-master workflow stop workflow-1.2-1234567890-abc123
# Force stop using alias
task-master workflow kill workflow-1.2-1234567890-abc123
```
## Workflow States
| State | Description |
|-------|-------------|
| `pending` | Created but not started |
| `initializing` | Setting up worktree and process |
| `running` | Active execution in progress |
| `paused` | Temporarily stopped |
| `completed` | Successfully finished |
| `failed` | Error occurred during execution |
| `cancelled` | User cancelled the workflow |
| `timeout` | Exceeded time limit |
## Environment Configuration
### Environment Variables
Set these environment variables to customize workflow behavior:
- `TASKMASTER_WORKFLOW_DEBUG`: Enable debug logging
- `TASKMASTER_CLAUDE_PATH`: Custom Claude Code executable path
- `TASKMASTER_WORKTREE_BASE`: Base directory for worktrees
- `TASKMASTER_MAX_CONCURRENT`: Maximum concurrent workflows
### Example Configuration
```bash
# Enable debug mode
export TASKMASTER_WORKFLOW_DEBUG=true
# Set custom Claude path
export TASKMASTER_CLAUDE_PATH=/usr/local/bin/claude
# Set worktree base directory
export TASKMASTER_WORKTREE_BASE=./worktrees
# Limit concurrent workflows
export TASKMASTER_MAX_CONCURRENT=3
```
## Git Worktree Integration
### How It Works
When you start a workflow:
1. **Worktree Creation**: A new git worktree is created for the task
2. **Process Spawn**: A dedicated Claude Code process is launched in the worktree
3. **Task Execution**: The task runs in complete isolation
4. **State Tracking**: Progress is monitored and persisted
5. **Cleanup**: Worktree is removed when workflow completes
### Worktree Structure
```
project/
├── .git/ # Main repository
├── src/ # Main working directory
└── worktrees/ # Workflow worktrees
├── task-1.2/ # Worktree for task 1.2
├── task-2.1/ # Worktree for task 2.1
└── task-3.4/ # Worktree for task 3.4
```
## Best Practices
### When to Use Workflows
Use workflows for tasks that:
- Require isolated development environments
- Need dedicated Claude Code attention
- Benefit from parallel execution
- Require process monitoring and state tracking
### Workflow Management
- **Start workflows for complex tasks** that need focused execution
- **Monitor progress** using `workflow status` command
- **Clean up completed workflows** to free resources
- **Use meaningful task descriptions** for better workflow tracking
### Resource Management
- **Limit concurrent workflows** based on system resources
- **Monitor workflow output** for debugging and progress tracking
- **Stop unnecessary workflows** to free up resources
## Troubleshooting
### Common Issues
**Worktree Creation Fails**
```bash
# Check git version (requires 2.5+)
git --version
# Verify project is a git repository
git status
```
**Claude Code Not Found**
```bash
# Check Claude installation
which claude
# Set custom path
export TASKMASTER_CLAUDE_PATH=/path/to/claude
```
**Permission Errors**
```bash
# Check worktree directory permissions
chmod -R 755 ./worktrees
```
### Debug Mode
Enable debug logging for troubleshooting:
```bash
export TASKMASTER_WORKFLOW_DEBUG=true
task-master workflow start 1.2
```
## Integration Examples
### With VS Code Extension
The workflow engine integrates with the Task Master VS Code extension to provide:
- **Workflow Tree View**: Visual workflow management
- **Process Monitoring**: Real-time output streaming
- **Worktree Navigation**: Quick access to isolated workspaces
- **Status Indicators**: Visual workflow state tracking
### With Task Management
```bash
# Typical workflow
task-master next # Find next task
task-master workflow start 1.2 # Start workflow
task-master workflow status <id> # Monitor progress
task-master set-status --id=1.2 --status=done # Mark complete
```
## Advanced Features
### Parallel Execution
Run multiple workflows simultaneously:
```bash
# Start multiple workflows
task-master workflow start 1.2
task-master workflow start 2.1
task-master workflow start 3.4
# Monitor all active workflows
task-master workflow list
```
### Process Monitoring
Each workflow provides real-time output monitoring and process management through the workflow engine's event system.
### State Persistence
Workflow state is automatically persisted across sessions, allowing you to resume monitoring workflows after restarting the CLI.

View File

@@ -49,6 +49,7 @@
"pages": [ "pages": [
"capabilities/mcp", "capabilities/mcp",
"capabilities/cli-root-commands", "capabilities/cli-root-commands",
"capabilities/workflows",
"capabilities/task-structure" "capabilities/task-structure"
] ]
} }

View File

@@ -3,4 +3,38 @@ title: "What's New"
sidebarTitle: "What's New" sidebarTitle: "What's New"
--- ---
## New Workflow Engine (Latest)
Task Master now includes a powerful workflow engine that revolutionizes how tasks are executed:
### 🚀 Key Features
- **Git Worktree Isolation**: Each task runs in its own isolated git worktree
- **Claude Code Integration**: Spawns dedicated Claude Code processes for task execution
- **Real-time Monitoring**: Track workflow progress and process output
- **Parallel Execution**: Run multiple tasks concurrently with resource management
- **State Persistence**: Workflow state is maintained across sessions
### 🔧 New CLI Commands
```bash
# Start workflow execution
task-master workflow start <task-id>
# Monitor active workflows
task-master workflow list
# Check workflow status
task-master workflow status <workflow-id>
# Stop running workflow
task-master workflow stop <workflow-id>
```
### 📖 Learn More
Check out the new [Workflow Documentation](/capabilities/workflows) for comprehensive usage guides and best practices.
---
An easy way to see the latest releases An easy way to see the latest releases

40
output.txt Normal file

File diff suppressed because one or more lines are too long

45
package-lock.json generated
View File

@@ -96,6 +96,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tm/core": "*", "@tm/core": "*",
"@tm/workflow-engine": "*",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"chalk": "5.6.2", "chalk": "5.6.2",
"cli-table3": "^0.6.5", "cli-table3": "^0.6.5",
@@ -10311,6 +10312,10 @@
"resolved": "packages/tm-core", "resolved": "packages/tm-core",
"link": true "link": true
}, },
"node_modules/@tm/workflow-engine": {
"resolved": "packages/workflow-engine",
"link": true
},
"node_modules/@tokenizer/inflate": { "node_modules/@tokenizer/inflate": {
"version": "0.2.7", "version": "0.2.7",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
@@ -31082,7 +31087,9 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.57.4", "@supabase/supabase-js": "^2.57.0",
"@tm/workflow-engine": "*",
"chalk": "^5.3.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
@@ -31108,6 +31115,42 @@
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
},
"packages/tm-core/node_modules/undici-types": {
"version": "6.21.0",
"dev": true,
"license": "MIT"
},
"packages/workflow-engine": {
"name": "@tm/workflow-engine",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@tm/core": "*"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.0.0",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
}
},
"packages/workflow-engine/node_modules/@types/node": {
"version": "22.18.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz",
"integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"packages/workflow-engine/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
} }
} }
} }

View File

@@ -13,12 +13,13 @@
"scripts": { "scripts": {
"build": "npm run build:packages && tsup", "build": "npm run build:packages && tsup",
"dev": "npm run build:packages && npm link && (npm run dev:packages & tsup --watch --onSuccess 'echo Build complete && npm link')", "dev": "npm run build:packages && npm link && (npm run dev:packages & tsup --watch --onSuccess 'echo Build complete && npm link')",
"dev:packages": "(cd packages/tm-core && npm run dev) & (cd apps/cli && npm run dev) & wait", "dev:packages": "(cd packages/tm-core && npm run dev) & (cd packages/workflow-engine && npm run dev) & (cd apps/cli && npm run dev) & wait",
"dev:core": "cd packages/tm-core && npm run dev", "dev:core": "cd packages/tm-core && npm run dev",
"dev:workflow": "cd packages/workflow-engine && npm run dev",
"dev:cli": "cd apps/cli && npm run dev", "dev:cli": "cd apps/cli && npm run dev",
"build:packages": "npm run build:build-config && npm run build:core && npm run build:cli", "build:packages": "npm run build:core && npm run build:workflow && npm run build:cli",
"build:build-config": "cd packages/build-config && npm run build",
"build:core": "cd packages/tm-core && npm run build", "build:core": "cd packages/tm-core && npm run build",
"build:workflow": "cd packages/workflow-engine && npm run build",
"build:cli": "cd apps/cli && npm run build", "build:cli": "cd apps/cli && npm run build",
"typecheck": "npm run typecheck:core && npm run typecheck:cli", "typecheck": "npm run typecheck:core && npm run typecheck:cli",
"typecheck:core": "cd packages/tm-core && npm run typecheck", "typecheck:core": "cd packages/tm-core && npm run typecheck",

View File

@@ -49,8 +49,15 @@
}, },
"./utils": { "./utils": {
"types": "./src/utils/index.ts", "types": "./src/utils/index.ts",
"import": "./dist/utils/index.js" "import": "./dist/utils/index.js",
} "require": "./dist/utils/index.js"
},
"./workflow": {
"types": "./src/workflow/index.ts",
"import": "./dist/workflow/index.js",
"require": "./dist/workflow/index.js"
},
"./package.json": "./package.json"
}, },
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
@@ -66,7 +73,9 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.57.4", "@supabase/supabase-js": "^2.57.0",
"@tm/workflow-engine": "*",
"chalk": "^5.3.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -55,3 +55,7 @@ export {
// Re-export logger // Re-export logger
export { getLogger, createLogger, setGlobalLogger } from './logger/index.js'; export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
// Re-export workflow
export { WorkflowService, type WorkflowServiceConfig } from './workflow/index.js';
export type * from './workflow/index.js';

View File

@@ -16,6 +16,10 @@ import type {
TaskFilter, TaskFilter,
StorageType StorageType
} from './types/index.js'; } from './types/index.js';
import {
WorkflowService,
type WorkflowServiceConfig
} from './workflow/index.js';
/** /**
* Options for creating TaskMasterCore instance * Options for creating TaskMasterCore instance
@@ -23,6 +27,7 @@ import type {
export interface TaskMasterCoreOptions { export interface TaskMasterCoreOptions {
projectPath: string; projectPath: string;
configuration?: Partial<IConfiguration>; configuration?: Partial<IConfiguration>;
workflow?: Partial<WorkflowServiceConfig>;
} }
/** /**
@@ -38,6 +43,7 @@ export type { GetTaskListOptions } from './services/task-service.js';
export class TaskMasterCore { export class TaskMasterCore {
private configManager: ConfigManager; private configManager: ConfigManager;
private taskService: TaskService; private taskService: TaskService;
private workflowService: WorkflowService;
/** /**
* Create and initialize a new TaskMasterCore instance * Create and initialize a new TaskMasterCore instance
@@ -60,6 +66,7 @@ export class TaskMasterCore {
// Services will be initialized in the initialize() method // Services will be initialized in the initialize() method
this.configManager = null as any; this.configManager = null as any;
this.taskService = null as any; this.taskService = null as any;
this.workflowService = null as any;
} }
/** /**
@@ -86,6 +93,28 @@ export class TaskMasterCore {
// Create task service // Create task service
this.taskService = new TaskService(this.configManager); this.taskService = new TaskService(this.configManager);
await this.taskService.initialize(); await this.taskService.initialize();
// Create workflow service
const workflowConfig: WorkflowServiceConfig = {
projectRoot: options.projectPath,
...options.workflow
};
// Pass task retrieval function to workflow service
this.workflowService = new WorkflowService(
workflowConfig,
async (taskId: string) => {
const task = await this.getTask(taskId);
if (!task) {
throw new TaskMasterError(
`Task ${taskId} not found`,
ERROR_CODES.TASK_NOT_FOUND
);
}
return task;
}
);
await this.workflowService.initialize();
} catch (error) { } catch (error) {
throw new TaskMasterError( throw new TaskMasterError(
'Failed to initialize TaskMasterCore', 'Failed to initialize TaskMasterCore',
@@ -175,11 +204,21 @@ export class TaskMasterCore {
await this.configManager.setActiveTag(tag); await this.configManager.setActiveTag(tag);
} }
/**
* Get workflow service for workflow operations
*/
get workflow(): WorkflowService {
return this.workflowService;
}
/** /**
* Close and cleanup resources * Close and cleanup resources
*/ */
async close(): Promise<void> { async close(): Promise<void> {
// TaskService handles storage cleanup internally // TaskService handles storage cleanup internally
if (this.workflowService) {
await this.workflowService.dispose();
}
} }
} }

View File

@@ -0,0 +1,17 @@
/**
* @fileoverview Workflow Module
* Public exports for workflow functionality
*/
export { WorkflowService, type WorkflowServiceConfig } from './workflow-service.js';
// Re-export workflow engine types for convenience
export type {
WorkflowExecutionContext,
WorkflowStatus,
WorkflowEvent,
WorkflowEventType,
WorkflowProcess,
ProcessStatus,
WorktreeInfo
} from '@tm/workflow-engine';

View File

@@ -0,0 +1,218 @@
/**
* @fileoverview Workflow Service
* Integrates workflow engine into Task Master Core
*/
import {
TaskExecutionManager,
type TaskExecutionManagerConfig,
type WorkflowExecutionContext
} from '@tm/workflow-engine';
import type { Task } from '../types/index.js';
import { TaskMasterError } from '../errors/index.js';
export interface WorkflowServiceConfig {
/** Project root directory */
projectRoot: string;
/** Maximum number of concurrent workflows */
maxConcurrent?: number;
/** Default timeout for workflow execution (minutes) */
defaultTimeout?: number;
/** Base directory for worktrees */
worktreeBase?: string;
/** Claude Code executable path */
claudeExecutable?: string;
/** Enable debug logging */
debug?: boolean;
}
/**
* WorkflowService provides Task Master workflow capabilities through core
*/
export class WorkflowService {
private workflowEngine: TaskExecutionManager;
constructor(
config: WorkflowServiceConfig,
private getTask: (taskId: string) => Promise<Task>
) {
const engineConfig: TaskExecutionManagerConfig = {
projectRoot: config.projectRoot,
maxConcurrent: config.maxConcurrent || 5,
defaultTimeout: config.defaultTimeout || 60,
worktreeBase:
config.worktreeBase ||
require('path').join(config.projectRoot, '..', 'task-worktrees'),
claudeExecutable: config.claudeExecutable || 'claude',
debug: config.debug || false
};
this.workflowEngine = new TaskExecutionManager(engineConfig);
}
/**
* Initialize the workflow service
*/
async initialize(): Promise<void> {
await this.workflowEngine.initialize();
}
/**
* Start a workflow for a task
*/
async start(
taskId: string,
options?: {
branchName?: string;
timeout?: number;
env?: Record<string, string>;
}
): Promise<string> {
try {
// Get task from core
const task = await this.getTask(taskId);
// Start workflow using engine
return await this.workflowEngine.startTaskExecution(task, options);
} catch (error) {
throw new TaskMasterError(
`Failed to start workflow for task ${taskId}`,
'WORKFLOW_START_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Stop a workflow
*/
async stop(workflowId: string, force = false): Promise<void> {
try {
await this.workflowEngine.stopTaskExecution(workflowId, force);
} catch (error) {
throw new TaskMasterError(
`Failed to stop workflow ${workflowId}`,
'WORKFLOW_STOP_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Pause a workflow
*/
async pause(workflowId: string): Promise<void> {
try {
await this.workflowEngine.pauseTaskExecution(workflowId);
} catch (error) {
throw new TaskMasterError(
`Failed to pause workflow ${workflowId}`,
'WORKFLOW_PAUSE_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Resume a paused workflow
*/
async resume(workflowId: string): Promise<void> {
try {
await this.workflowEngine.resumeTaskExecution(workflowId);
} catch (error) {
throw new TaskMasterError(
`Failed to resume workflow ${workflowId}`,
'WORKFLOW_RESUME_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Get workflow status
*/
getStatus(workflowId: string): WorkflowExecutionContext | undefined {
return this.workflowEngine.getWorkflowStatus(workflowId);
}
/**
* Get workflow by task ID
*/
getByTaskId(taskId: string): WorkflowExecutionContext | undefined {
return this.workflowEngine.getWorkflowByTaskId(taskId);
}
/**
* List all workflows
*/
list(): WorkflowExecutionContext[] {
return this.workflowEngine.listWorkflows();
}
/**
* List active workflows
*/
listActive(): WorkflowExecutionContext[] {
return this.workflowEngine.listActiveWorkflows();
}
/**
* Send input to a running workflow
*/
async sendInput(workflowId: string, input: string): Promise<void> {
try {
await this.workflowEngine.sendInputToWorkflow(workflowId, input);
} catch (error) {
throw new TaskMasterError(
`Failed to send input to workflow ${workflowId}`,
'WORKFLOW_INPUT_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Clean up all workflows
*/
async cleanup(force = false): Promise<void> {
try {
await this.workflowEngine.cleanup(force);
} catch (error) {
throw new TaskMasterError(
'Failed to cleanup workflows',
'WORKFLOW_CLEANUP_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Subscribe to workflow events
*/
on(event: string, listener: (...args: any[]) => void): void {
this.workflowEngine.on(event, listener);
}
/**
* Unsubscribe from workflow events
*/
off(event: string, listener: (...args: any[]) => void): void {
this.workflowEngine.off(event, listener);
}
/**
* Get workflow engine instance (for advanced usage)
*/
getEngine(): TaskExecutionManager {
return this.workflowEngine;
}
/**
* Dispose of the workflow service
*/
async dispose(): Promise<void> {
await this.cleanup(true);
this.workflowEngine.removeAllListeners();
}
}

View File

@@ -1,24 +1,53 @@
import { defineConfig } from 'tsup'; import { defineConfig } from 'tsup';
import { libraryConfig, mergeConfig } from '@tm/build-config'; import { dotenvLoad } from 'dotenv-mono';
dotenvLoad();
export default defineConfig( // Get all TM_PUBLIC_* env variables for build-time injection
mergeConfig(libraryConfig, { const getBuildTimeEnvs = () => {
const envs: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('TM_PUBLIC_')) {
// Return the actual value, not JSON.stringify'd
envs[key] = value || '';
}
}
return envs;
};
export default defineConfig({
entry: { entry: {
index: 'src/index.ts', index: 'src/index.ts',
'auth/index': 'src/auth/index.ts', 'auth/index': 'src/auth/index.ts',
'config/index': 'src/config/index.ts', 'config/index': 'src/config/index.ts',
'services/index': 'src/services/index.ts', 'errors/index': 'src/errors/index.ts',
'logger/index': 'src/logger/index.ts',
'interfaces/index': 'src/interfaces/index.ts', 'interfaces/index': 'src/interfaces/index.ts',
'types/index': 'src/types/index.ts', 'logger/index': 'src/logger/index.ts',
'providers/index': 'src/providers/index.ts',
'storage/index': 'src/storage/index.ts',
'parser/index': 'src/parser/index.ts', 'parser/index': 'src/parser/index.ts',
'providers/index': 'src/providers/index.ts',
'services/index': 'src/services/index.ts',
'storage/index': 'src/storage/index.ts',
'types/index': 'src/types/index.ts',
'utils/index': 'src/utils/index.ts', 'utils/index': 'src/utils/index.ts',
'errors/index': 'src/errors/index.ts' 'workflow/index': 'src/workflow/index.ts'
}, },
format: ['cjs', 'esm'],
dts: true,
sourcemap: true,
clean: true,
splitting: false,
treeshake: true,
minify: false,
target: 'es2022',
tsconfig: './tsconfig.json', tsconfig: './tsconfig.json',
outDir: 'dist', outDir: 'dist',
external: ['zod', '@supabase/supabase-js'] // Replace process.env.TM_PUBLIC_* with actual values at build time
}) env: getBuildTimeEnvs(),
); // Auto-external all dependencies from package.json
external: [
// External all node_modules - everything not starting with . or /
/^[^./]/
],
esbuildOptions(options) {
options.conditions = ['module'];
}
});

View File

@@ -0,0 +1,371 @@
# @tm/workflow-engine
Enhanced Task Master workflow execution engine with git worktree isolation and Claude Code process management.
## Overview
The Workflow Engine extends Task Master with advanced execution capabilities:
- **Git Worktree Isolation**: Each task runs in its own isolated worktree
- **Process Sandboxing**: Spawns dedicated Claude Code processes for task execution
- **Real-time Monitoring**: Track workflow progress and process output
- **State Management**: Persistent workflow state across sessions
- **Parallel Execution**: Run multiple tasks concurrently with resource limits
## Architecture
```
TaskExecutionManager
├── WorktreeManager # Git worktree lifecycle
├── ProcessSandbox # Claude Code process management
└── WorkflowStateManager # Persistent state tracking
```
## Quick Start
```typescript
import { TaskExecutionManager } from '@tm/workflow-engine';
const manager = new TaskExecutionManager({
projectRoot: '/path/to/project',
worktreeBase: '/path/to/worktrees',
claudeExecutable: 'claude',
maxConcurrent: 3,
defaultTimeout: 60,
debug: true
});
await manager.initialize();
// Start task execution
const workflowId = await manager.startTaskExecution({
id: '1.2',
title: 'Implement authentication',
description: 'Add JWT-based auth system',
status: 'pending',
priority: 'high'
});
// Monitor workflow
const workflow = manager.getWorkflowStatus(workflowId);
console.log(`Status: ${workflow.status}`);
// Stop when complete
await manager.stopTaskExecution(workflowId);
```
## CLI Integration
```bash
# Start workflow
tm workflow start 1.2
# List active workflows
tm workflow list
# Check status
tm workflow status workflow-1.2-1234567890-abc123
# Stop workflow
tm workflow stop workflow-1.2-1234567890-abc123
```
## VS Code Extension
The workflow engine integrates with the Task Master VS Code extension to provide:
- **Workflow Tree View**: Visual workflow management
- **Process Monitoring**: Real-time output streaming
- **Worktree Navigation**: Quick access to isolated workspaces
- **Status Indicators**: Visual workflow state tracking
## Core Components
### TaskExecutionManager
Orchestrates complete workflow lifecycle:
```typescript
// Event-driven workflow management
manager.on('workflow.started', (event) => {
console.log(`Started: ${event.workflowId}`);
});
manager.on('process.output', (event) => {
console.log(`[${event.data.stream}]: ${event.data.data}`);
});
```
### WorktreeManager
Manages git worktree operations:
```typescript
import { WorktreeManager } from '@tm/workflow-engine';
const manager = new WorktreeManager({
worktreeBase: './worktrees',
projectRoot: process.cwd(),
autoCleanup: true
});
// Create isolated workspace
const worktree = await manager.createWorktree('task-1.2');
console.log(`Created: ${worktree.path}`);
// List all worktrees
const worktrees = await manager.listWorktrees();
// Cleanup
await manager.removeWorktree('task-1.2');
```
### ProcessSandbox
Spawns and manages Claude Code processes:
```typescript
import { ProcessSandbox } from '@tm/workflow-engine';
const sandbox = new ProcessSandbox({
claudeExecutable: 'claude',
defaultTimeout: 30,
debug: true
});
// Start isolated process
const process = await sandbox.startProcess(
'workflow-123',
'task-1.2',
'Implement user authentication with JWT tokens',
{ cwd: '/path/to/worktree' }
);
// Send input
await sandbox.sendInput('workflow-123', 'npm test');
// Monitor output
sandbox.on('process.output', (event) => {
console.log(event.data.data);
});
```
### WorkflowStateManager
Persistent workflow state management:
```typescript
import { WorkflowStateManager } from '@tm/workflow-engine';
const stateManager = new WorkflowStateManager({
projectRoot: process.cwd()
});
await stateManager.loadState();
// Register workflow
const workflowId = await stateManager.registerWorkflow({
taskId: '1.2',
taskTitle: 'Authentication',
// ... other context
});
// Update status
await stateManager.updateWorkflowStatus(workflowId, 'running');
// Query workflows
const running = stateManager.listWorkflowsByStatus('running');
```
## Configuration
### Environment Variables
- `TASKMASTER_WORKFLOW_DEBUG`: Enable debug logging
- `TASKMASTER_CLAUDE_PATH`: Custom Claude Code executable path
- `TASKMASTER_WORKTREE_BASE`: Base directory for worktrees
- `TASKMASTER_MAX_CONCURRENT`: Maximum concurrent workflows
### Config Object
```typescript
interface TaskExecutionManagerConfig {
projectRoot: string; // Project root directory
worktreeBase: string; // Worktree base path
claudeExecutable: string; // Claude executable
maxConcurrent: number; // Concurrent limit
defaultTimeout: number; // Timeout (minutes)
debug: boolean; // Debug logging
}
```
## Workflow States
| State | Description |
|-------|-------------|
| `pending` | Created but not started |
| `initializing` | Setting up worktree/process |
| `running` | Active execution |
| `paused` | Temporarily stopped |
| `completed` | Successfully finished |
| `failed` | Error occurred |
| `cancelled` | User cancelled |
| `timeout` | Exceeded time limit |
## Events
The workflow engine emits events for real-time monitoring:
```typescript
// Workflow lifecycle
manager.on('workflow.started', (event) => {});
manager.on('workflow.completed', (event) => {});
manager.on('workflow.failed', (event) => {});
// Process events
manager.on('process.started', (event) => {});
manager.on('process.output', (event) => {});
manager.on('process.stopped', (event) => {});
// Worktree events
manager.on('worktree.created', (event) => {});
manager.on('worktree.deleted', (event) => {});
```
## Error Handling
The workflow engine provides specialized error types:
```typescript
import {
WorkflowError,
WorktreeError,
ProcessError,
MaxConcurrentWorkflowsError
} from '@tm/workflow-engine';
try {
await manager.startTaskExecution(task);
} catch (error) {
if (error instanceof MaxConcurrentWorkflowsError) {
console.log('Too many concurrent workflows');
} else if (error instanceof WorktreeError) {
console.log('Worktree operation failed');
}
}
```
## Development
```bash
# Install dependencies
npm install
# Build package
npm run build
# Run tests
npm test
# Development mode
npm run dev
```
## Integration Examples
### With Task Master Core
```typescript
import { createTaskMasterCore } from '@tm/core';
import { TaskExecutionManager } from '@tm/workflow-engine';
const core = await createTaskMasterCore({ projectPath: '.' });
const workflows = new TaskExecutionManager({ /*...*/ });
// Get task from core
const tasks = await core.getTaskList({});
const task = tasks.tasks.find(t => t.id === '1.2');
// Execute with workflow engine
if (task) {
const workflowId = await workflows.startTaskExecution(task);
}
```
### With VS Code Extension
```typescript
import { WorkflowProvider } from './workflow-provider';
// Register tree view
const provider = new WorkflowProvider(context);
vscode.window.createTreeView('taskmaster.workflows', {
treeDataProvider: provider
});
// Register commands
vscode.commands.registerCommand('taskmaster.workflow.start',
async (taskId) => {
await provider.startWorkflow(taskId);
}
);
```
## Troubleshooting
### Common Issues
1. **Worktree Creation Fails**
```bash
# Check git version (requires 2.5+)
git --version
# Verify project is git repository
git status
```
2. **Claude Code Not Found**
```bash
# Check Claude installation
which claude
# Set custom path
export TASKMASTER_CLAUDE_PATH=/path/to/claude
```
3. **Permission Errors**
```bash
# Check worktree directory permissions
chmod -R 755 ./worktrees
```
### Debug Mode
Enable debug logging for troubleshooting:
```typescript
const manager = new TaskExecutionManager({
// ... other config
debug: true
});
```
Or via environment:
```bash
export TASKMASTER_WORKFLOW_DEBUG=true
tm workflow start 1.2
```
## Roadmap
- [ ] Process resource monitoring (CPU, memory)
- [ ] Workflow templates and presets
- [ ] Integration with CI/CD pipelines
- [ ] Workflow scheduling and queueing
- [ ] Multi-machine workflow distribution
- [ ] Advanced debugging and profiling tools
## License
MIT WITH Commons-Clause

View File

@@ -0,0 +1,56 @@
{
"name": "@tm/workflow-engine",
"version": "0.1.0",
"description": "Task Master workflow execution engine with git worktree and process management",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./task-execution": {
"import": "./dist/task-execution/index.js",
"types": "./dist/task-execution/index.d.ts"
},
"./worktree": {
"import": "./dist/worktree/index.js",
"types": "./dist/worktree/index.d.ts"
},
"./process": {
"import": "./dist/process/index.js",
"types": "./dist/process/index.d.ts"
},
"./state": {
"import": "./dist/state/index.js",
"types": "./dist/state/index.d.ts"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",
"test:watch": "vitest --watch",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@tm/core": "*"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.0.0",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
},
"files": ["dist"],
"keywords": [
"task-master",
"workflow",
"git-worktree",
"process-management",
"claude-code"
],
"author": "Task Master AI Team",
"license": "MIT"
}

View File

@@ -0,0 +1,6 @@
/**
* @fileoverview Workflow Engine Errors
* Public error exports
*/
export * from './workflow.errors.js';

View File

@@ -0,0 +1,59 @@
/**
* @fileoverview Workflow Engine Errors
* Custom error classes for workflow operations
*/
export class WorkflowError extends Error {
constructor(
message: string,
public code: string,
public workflowId?: string,
public taskId?: string,
public cause?: Error
) {
super(message);
this.name = 'WorkflowError';
}
}
export class WorktreeError extends WorkflowError {
constructor(message: string, public path?: string, cause?: Error) {
super(message, 'WORKTREE_ERROR', undefined, undefined, cause);
this.name = 'WorktreeError';
}
}
export class ProcessError extends WorkflowError {
constructor(message: string, public pid?: number, cause?: Error) {
super(message, 'PROCESS_ERROR', undefined, undefined, cause);
this.name = 'ProcessError';
}
}
export class WorkflowTimeoutError extends WorkflowError {
constructor(workflowId: string, timeoutMinutes: number) {
super(
`Workflow ${workflowId} timed out after ${timeoutMinutes} minutes`,
'WORKFLOW_TIMEOUT',
workflowId
);
this.name = 'WorkflowTimeoutError';
}
}
export class WorkflowNotFoundError extends WorkflowError {
constructor(workflowId: string) {
super(`Workflow ${workflowId} not found`, 'WORKFLOW_NOT_FOUND', workflowId);
this.name = 'WorkflowNotFoundError';
}
}
export class MaxConcurrentWorkflowsError extends WorkflowError {
constructor(maxConcurrent: number) {
super(
`Maximum concurrent workflows (${maxConcurrent}) reached`,
'MAX_CONCURRENT_WORKFLOWS'
);
this.name = 'MaxConcurrentWorkflowsError';
}
}

View File

@@ -0,0 +1,19 @@
/**
* @fileoverview Workflow Engine
* Main entry point for the Task Master workflow execution engine
*/
// Core task execution
export * from './task-execution/index.js';
// Component managers
export * from './worktree/index.js';
export * from './process/index.js';
export * from './state/index.js';
// Types and errors
export * from './types/index.js';
export * from './errors/index.js';
// Convenience exports
export { TaskExecutionManager as WorkflowEngine } from './task-execution/index.js';

View File

@@ -0,0 +1,6 @@
/**
* @fileoverview Process Management
* Public exports for process operations
*/
export * from './process-sandbox.js';

View File

@@ -0,0 +1,378 @@
/**
* @fileoverview Process Sandbox
* Manages Claude Code process execution in isolated environments
*/
import { spawn, ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import type {
WorkflowProcess,
WorkflowEvent,
WorkflowEventType
} from '../types/workflow.types.js';
import { ProcessError } from '../errors/workflow.errors.js';
export interface ProcessSandboxConfig {
/** Claude Code executable path */
claudeExecutable: string;
/** Default timeout for processes (minutes) */
defaultTimeout: number;
/** Environment variables to pass to processes */
environment?: Record<string, string>;
/** Enable debug output */
debug: boolean;
}
export interface ProcessOptions {
/** Working directory for the process */
cwd: string;
/** Environment variables (merged with config) */
env?: Record<string, string>;
/** Timeout in minutes (overrides default) */
timeout?: number;
/** Additional Claude Code arguments */
args?: string[];
}
/**
* ProcessSandbox manages Claude Code process lifecycle
* Single responsibility: Process spawning, monitoring, and cleanup
*/
export class ProcessSandbox extends EventEmitter {
private config: ProcessSandboxConfig;
private activeProcesses = new Map<string, WorkflowProcess>();
private childProcesses = new Map<string, ChildProcess>();
private timeouts = new Map<string, NodeJS.Timeout>();
constructor(config: ProcessSandboxConfig) {
super();
this.config = config;
this.setupCleanupHandlers();
}
/**
* Start a Claude Code process for task execution
*/
async startProcess(
workflowId: string,
taskId: string,
taskPrompt: string,
options: ProcessOptions
): Promise<WorkflowProcess> {
if (this.activeProcesses.has(workflowId)) {
throw new ProcessError(
`Process already running for workflow ${workflowId}`
);
}
// Prepare command and arguments
const args = [
'-p', // Print mode for non-interactive execution
taskPrompt,
...(options.args || [])
];
// Prepare environment
const env = {
...process.env,
...this.config.environment,
...options.env,
// Ensure task context is available
TASKMASTER_WORKFLOW_ID: workflowId,
TASKMASTER_TASK_ID: taskId
};
try {
// Spawn Claude Code process
const childProcess = spawn(this.config.claudeExecutable, args, {
cwd: options.cwd,
env,
stdio: ['pipe', 'pipe', 'pipe']
});
const workflowProcess: WorkflowProcess = {
pid: childProcess.pid!,
command: this.config.claudeExecutable,
args,
cwd: options.cwd,
env,
startedAt: new Date(),
status: 'starting'
};
// Store process references
this.activeProcesses.set(workflowId, workflowProcess);
this.childProcesses.set(workflowId, childProcess);
// Setup process event handlers
this.setupProcessHandlers(workflowId, taskId, childProcess);
// Setup timeout if specified
const timeoutMinutes = options.timeout || this.config.defaultTimeout;
if (timeoutMinutes > 0) {
this.setupProcessTimeout(workflowId, timeoutMinutes);
}
// Emit process started event
this.emitEvent('process.started', workflowId, taskId, {
pid: workflowProcess.pid,
command: workflowProcess.command
});
workflowProcess.status = 'running';
return workflowProcess;
} catch (error) {
throw new ProcessError(
`Failed to start process for workflow ${workflowId}`,
undefined,
error as Error
);
}
}
/**
* Stop a running process
*/
async stopProcess(workflowId: string, force = false): Promise<void> {
const process = this.activeProcesses.get(workflowId);
const childProcess = this.childProcesses.get(workflowId);
if (!process || !childProcess) {
throw new ProcessError(
`No running process found for workflow ${workflowId}`
);
}
try {
// Clear timeout
const timeout = this.timeouts.get(workflowId);
if (timeout) {
clearTimeout(timeout);
this.timeouts.delete(workflowId);
}
// Kill the process
if (force) {
childProcess.kill('SIGKILL');
} else {
childProcess.kill('SIGTERM');
// Give it 5 seconds to gracefully exit, then force kill
setTimeout(() => {
if (!childProcess.killed) {
childProcess.kill('SIGKILL');
}
}, 5000);
}
process.status = 'stopped';
// Emit process stopped event
this.emitEvent('process.stopped', workflowId, process.pid.toString(), {
pid: process.pid,
forced: force
});
} catch (error) {
throw new ProcessError(
`Failed to stop process for workflow ${workflowId}`,
process.pid,
error as Error
);
}
}
/**
* Send input to a running process
*/
async sendInput(workflowId: string, input: string): Promise<void> {
const childProcess = this.childProcesses.get(workflowId);
if (!childProcess) {
throw new ProcessError(
`No running process found for workflow ${workflowId}`
);
}
try {
childProcess.stdin?.write(input);
childProcess.stdin?.write('\n');
} catch (error) {
throw new ProcessError(
`Failed to send input to process for workflow ${workflowId}`,
childProcess.pid,
error as Error
);
}
}
/**
* Get process information
*/
getProcess(workflowId: string): WorkflowProcess | undefined {
return this.activeProcesses.get(workflowId);
}
/**
* List all active processes
*/
listProcesses(): WorkflowProcess[] {
return Array.from(this.activeProcesses.values());
}
/**
* Check if a process is running
*/
isProcessRunning(workflowId: string): boolean {
const process = this.activeProcesses.get(workflowId);
return process?.status === 'running' || process?.status === 'starting';
}
/**
* Clean up all processes
*/
async cleanupAll(force = false): Promise<void> {
const workflowIds = Array.from(this.activeProcesses.keys());
await Promise.all(
workflowIds.map(async (workflowId) => {
try {
await this.stopProcess(workflowId, force);
} catch (error) {
console.error(
`Failed to cleanup process for workflow ${workflowId}:`,
error
);
}
})
);
}
/**
* Setup process event handlers
*/
private setupProcessHandlers(
workflowId: string,
taskId: string,
childProcess: ChildProcess
): void {
const process = this.activeProcesses.get(workflowId);
if (!process) return;
// Handle stdout
childProcess.stdout?.on('data', (data) => {
const output = data.toString();
if (this.config.debug) {
console.log(`[${workflowId}] STDOUT:`, output);
}
this.emitEvent('process.output', workflowId, taskId, {
stream: 'stdout',
data: output
});
});
// Handle stderr
childProcess.stderr?.on('data', (data) => {
const output = data.toString();
if (this.config.debug) {
console.error(`[${workflowId}] STDERR:`, output);
}
this.emitEvent('process.output', workflowId, taskId, {
stream: 'stderr',
data: output
});
});
// Handle process exit
childProcess.on('exit', (code, signal) => {
process.status = code === 0 ? 'stopped' : 'crashed';
this.emitEvent('process.stopped', workflowId, taskId, {
pid: process.pid,
exitCode: code,
signal
});
// Cleanup
this.activeProcesses.delete(workflowId);
this.childProcesses.delete(workflowId);
const timeout = this.timeouts.get(workflowId);
if (timeout) {
clearTimeout(timeout);
this.timeouts.delete(workflowId);
}
});
// Handle process errors
childProcess.on('error', (error) => {
process.status = 'crashed';
this.emitEvent('process.error', workflowId, taskId, undefined, error);
// Cleanup
this.activeProcesses.delete(workflowId);
this.childProcesses.delete(workflowId);
});
}
/**
* Setup process timeout
*/
private setupProcessTimeout(
workflowId: string,
timeoutMinutes: number
): void {
const timeout = setTimeout(
async () => {
console.warn(`Process timeout reached for workflow ${workflowId}`);
try {
await this.stopProcess(workflowId, true);
} catch (error) {
console.error('Failed to stop timed out process:', error);
}
},
timeoutMinutes * 60 * 1000
);
this.timeouts.set(workflowId, timeout);
}
/**
* Emit workflow event
*/
private emitEvent(
type: WorkflowEventType,
workflowId: string,
taskId: string,
data?: any,
error?: Error
): void {
const event: WorkflowEvent = {
type,
workflowId,
taskId,
timestamp: new Date(),
data,
error
};
this.emit('event', event);
this.emit(type, event);
}
/**
* Setup cleanup handlers for graceful shutdown
*/
private setupCleanupHandlers(): void {
const cleanup = () => {
console.log('Cleaning up processes...');
this.cleanupAll(true).catch(console.error);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);
}
}

View File

@@ -0,0 +1,6 @@
/**
* @fileoverview State Management
* Public exports for workflow state operations
*/
export * from './workflow-state-manager.js';

View File

@@ -0,0 +1,320 @@
/**
* @fileoverview Workflow State Manager
* Extends tm-core RuntimeStateManager with workflow tracking capabilities
*/
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type {
WorkflowExecutionContext,
WorkflowStatus,
WorkflowEvent
} from '../types/workflow.types.js';
import { WorkflowError } from '../errors/workflow.errors.js';
export interface WorkflowStateConfig {
/** Project root directory */
projectRoot: string;
/** Custom state directory (defaults to .taskmaster) */
stateDir?: string;
}
export interface WorkflowRegistryEntry {
/** Workflow ID */
workflowId: string;
/** Task ID being executed */
taskId: string;
/** Workflow status */
status: WorkflowStatus;
/** Worktree path */
worktreePath: string;
/** Process ID if running */
processId?: number;
/** Start timestamp */
startedAt: string;
/** Last activity timestamp */
lastActivity: string;
/** Branch name */
branchName: string;
/** Additional metadata */
metadata?: Record<string, any>;
}
/**
* WorkflowStateManager manages workflow execution state
* Extends the concept of RuntimeStateManager to track active workflows globally
*/
export class WorkflowStateManager {
private config: WorkflowStateConfig;
private stateFilePath: string;
private activeWorkflows = new Map<string, WorkflowExecutionContext>();
constructor(config: WorkflowStateConfig) {
this.config = config;
const stateDir = config.stateDir || '.taskmaster';
this.stateFilePath = path.join(config.projectRoot, stateDir, 'workflows.json');
}
/**
* Load workflow state from disk
*/
async loadState(): Promise<void> {
try {
const stateData = await fs.readFile(this.stateFilePath, 'utf-8');
const registry = JSON.parse(stateData) as Record<string, WorkflowRegistryEntry>;
// Convert registry entries to WorkflowExecutionContext
for (const [workflowId, entry] of Object.entries(registry)) {
const context: WorkflowExecutionContext = {
taskId: entry.taskId,
taskTitle: `Task ${entry.taskId}`, // Will be updated when task details are loaded
taskDescription: '',
projectRoot: this.config.projectRoot,
worktreePath: entry.worktreePath,
branchName: entry.branchName,
processId: entry.processId,
startedAt: new Date(entry.startedAt),
status: entry.status,
lastActivity: new Date(entry.lastActivity),
metadata: entry.metadata
};
this.activeWorkflows.set(workflowId, context);
}
} catch (error: any) {
if (error.code === 'ENOENT') {
// Workflows file doesn't exist, start with empty state
console.debug('No workflows.json found, starting with empty state');
return;
}
console.warn('Failed to load workflow state:', error.message);
}
}
/**
* Save workflow state to disk
*/
async saveState(): Promise<void> {
const stateDir = path.dirname(this.stateFilePath);
try {
await fs.mkdir(stateDir, { recursive: true });
// Convert contexts to registry entries
const registry: Record<string, WorkflowRegistryEntry> = {};
for (const [workflowId, context] of this.activeWorkflows.entries()) {
registry[workflowId] = {
workflowId,
taskId: context.taskId,
status: context.status,
worktreePath: context.worktreePath,
processId: context.processId,
startedAt: context.startedAt.toISOString(),
lastActivity: context.lastActivity.toISOString(),
branchName: context.branchName,
metadata: context.metadata
};
}
await fs.writeFile(
this.stateFilePath,
JSON.stringify(registry, null, 2),
'utf-8'
);
} catch (error) {
throw new WorkflowError(
'Failed to save workflow state',
'WORKFLOW_STATE_SAVE_ERROR',
undefined,
undefined,
error as Error
);
}
}
/**
* Register a new workflow
*/
async registerWorkflow(context: WorkflowExecutionContext): Promise<string> {
const workflowId = this.generateWorkflowId(context.taskId);
this.activeWorkflows.set(workflowId, {
...context,
lastActivity: new Date()
});
await this.saveState();
return workflowId;
}
/**
* Update workflow context
*/
async updateWorkflow(
workflowId: string,
updates: Partial<WorkflowExecutionContext>
): Promise<void> {
const existing = this.activeWorkflows.get(workflowId);
if (!existing) {
throw new WorkflowError(
`Workflow ${workflowId} not found`,
'WORKFLOW_NOT_FOUND',
workflowId
);
}
const updated = {
...existing,
...updates,
lastActivity: new Date()
};
this.activeWorkflows.set(workflowId, updated);
await this.saveState();
}
/**
* Update workflow status
*/
async updateWorkflowStatus(workflowId: string, status: WorkflowStatus): Promise<void> {
await this.updateWorkflow(workflowId, { status });
}
/**
* Unregister a workflow (remove from state)
*/
async unregisterWorkflow(workflowId: string): Promise<void> {
if (!this.activeWorkflows.has(workflowId)) {
throw new WorkflowError(
`Workflow ${workflowId} not found`,
'WORKFLOW_NOT_FOUND',
workflowId
);
}
this.activeWorkflows.delete(workflowId);
await this.saveState();
}
/**
* Get workflow context by ID
*/
getWorkflow(workflowId: string): WorkflowExecutionContext | undefined {
return this.activeWorkflows.get(workflowId);
}
/**
* Get workflow by task ID
*/
getWorkflowByTaskId(taskId: string): WorkflowExecutionContext | undefined {
for (const context of this.activeWorkflows.values()) {
if (context.taskId === taskId) {
return context;
}
}
return undefined;
}
/**
* List all active workflows
*/
listWorkflows(): WorkflowExecutionContext[] {
return Array.from(this.activeWorkflows.values());
}
/**
* List workflows by status
*/
listWorkflowsByStatus(status: WorkflowStatus): WorkflowExecutionContext[] {
return this.listWorkflows().filter(w => w.status === status);
}
/**
* Get running workflows count
*/
getRunningCount(): number {
return this.listWorkflowsByStatus('running').length;
}
/**
* Check if a task has an active workflow
*/
hasActiveWorkflow(taskId: string): boolean {
return this.getWorkflowByTaskId(taskId) !== undefined;
}
/**
* Clean up completed/failed workflows older than specified time
*/
async cleanupOldWorkflows(olderThanHours = 24): Promise<number> {
const cutoffTime = new Date(Date.now() - (olderThanHours * 60 * 60 * 1000));
let cleaned = 0;
for (const [workflowId, context] of this.activeWorkflows.entries()) {
const isOld = context.lastActivity < cutoffTime;
const isFinished = ['completed', 'failed', 'cancelled', 'timeout'].includes(context.status);
if (isOld && isFinished) {
this.activeWorkflows.delete(workflowId);
cleaned++;
}
}
if (cleaned > 0) {
await this.saveState();
}
return cleaned;
}
/**
* Clear all workflow state
*/
async clearState(): Promise<void> {
try {
await fs.unlink(this.stateFilePath);
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
this.activeWorkflows.clear();
}
/**
* Record workflow event (for audit trail)
*/
async recordEvent(event: WorkflowEvent): Promise<void> {
// Update workflow last activity
const workflow = this.activeWorkflows.get(event.workflowId);
if (workflow) {
workflow.lastActivity = event.timestamp;
await this.saveState();
}
// Optional: Could extend to maintain event log file
if (process.env.TASKMASTER_DEBUG) {
console.log('Workflow Event:', {
type: event.type,
workflowId: event.workflowId,
taskId: event.taskId,
timestamp: event.timestamp.toISOString(),
data: event.data
});
}
}
/**
* Generate unique workflow ID
*/
private generateWorkflowId(taskId: string): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `workflow-${taskId}-${timestamp}-${random}`;
}
}

View File

@@ -0,0 +1,6 @@
/**
* @fileoverview Task Execution Management
* Public exports for task execution operations
*/
export * from './task-execution-manager.js';

View File

@@ -0,0 +1,433 @@
/**
* @fileoverview Task Execution Manager
* Orchestrates the complete task execution workflow using worktrees and processes
*/
import { EventEmitter } from 'node:events';
import path from 'node:path';
import type { Task } from '@tm/core';
import {
WorktreeManager,
type WorktreeManagerConfig
} from '../worktree/worktree-manager.js';
import {
ProcessSandbox,
type ProcessSandboxConfig
} from '../process/process-sandbox.js';
import {
WorkflowStateManager,
type WorkflowStateConfig
} from '../state/workflow-state-manager.js';
import type {
WorkflowConfig,
WorkflowExecutionContext,
WorkflowStatus,
WorkflowEvent
} from '../types/workflow.types.js';
import {
WorkflowError,
WorkflowNotFoundError,
MaxConcurrentWorkflowsError,
WorkflowTimeoutError
} from '../errors/workflow.errors.js';
export interface TaskExecutionManagerConfig extends WorkflowConfig {
/** Project root directory */
projectRoot: string;
}
/**
* TaskExecutionManager orchestrates the complete task execution workflow
* Coordinates worktree creation, process spawning, and state management
*/
export class TaskExecutionManager extends EventEmitter {
private config: TaskExecutionManagerConfig;
private worktreeManager: WorktreeManager;
private processSandbox: ProcessSandbox;
private stateManager: WorkflowStateManager;
private initialized = false;
constructor(config: TaskExecutionManagerConfig) {
super();
this.config = config;
// Initialize component managers
const worktreeConfig: WorktreeManagerConfig = {
worktreeBase: config.worktreeBase,
projectRoot: config.projectRoot,
autoCleanup: true
};
const processConfig: ProcessSandboxConfig = {
claudeExecutable: config.claudeExecutable,
defaultTimeout: config.defaultTimeout,
debug: config.debug
};
const stateConfig: WorkflowStateConfig = {
projectRoot: config.projectRoot
};
this.worktreeManager = new WorktreeManager(worktreeConfig);
this.processSandbox = new ProcessSandbox(processConfig);
this.stateManager = new WorkflowStateManager(stateConfig);
// Forward events from components
this.processSandbox.on('event', (event: WorkflowEvent) => {
this.stateManager.recordEvent(event);
this.emit('event', event);
});
}
/**
* Initialize the task execution manager
*/
async initialize(): Promise<void> {
if (this.initialized) return;
await this.stateManager.loadState();
// Clean up any stale workflows
await this.cleanupStaleWorkflows();
this.initialized = true;
}
/**
* Start task execution workflow
*/
async startTaskExecution(
task: Task,
options?: {
branchName?: string;
timeout?: number;
env?: Record<string, string>;
}
): Promise<string> {
if (!this.initialized) {
await this.initialize();
}
// Check concurrent workflow limit
const runningCount = this.stateManager.getRunningCount();
if (runningCount >= this.config.maxConcurrent) {
throw new MaxConcurrentWorkflowsError(this.config.maxConcurrent);
}
// Check if task already has an active workflow
if (this.stateManager.hasActiveWorkflow(task.id)) {
throw new WorkflowError(
`Task ${task.id} already has an active workflow`,
'TASK_ALREADY_EXECUTING',
undefined,
task.id
);
}
try {
// Create worktree
const worktreeInfo = await this.worktreeManager.createWorktree(
task.id,
options?.branchName
);
// Prepare task context
const context: WorkflowExecutionContext = {
taskId: task.id,
taskTitle: task.title,
taskDescription: task.description,
taskDetails: task.details,
projectRoot: this.config.projectRoot,
worktreePath: worktreeInfo.path,
branchName: worktreeInfo.branch,
startedAt: new Date(),
status: 'initializing',
lastActivity: new Date(),
metadata: {
priority: task.priority,
dependencies: task.dependencies
}
};
// Register workflow
const workflowId = await this.stateManager.registerWorkflow(context);
try {
// Prepare task prompt for Claude Code
const taskPrompt = this.generateTaskPrompt(task);
// Start Claude Code process
const process = await this.processSandbox.startProcess(
workflowId,
task.id,
taskPrompt,
{
cwd: worktreeInfo.path,
timeout: options?.timeout,
env: options?.env
}
);
// Update workflow with process information
await this.stateManager.updateWorkflow(workflowId, {
processId: process.pid,
status: 'running'
});
// Emit workflow started event
this.emitEvent('workflow.started', workflowId, task.id, {
worktreePath: worktreeInfo.path,
processId: process.pid
});
return workflowId;
} catch (error) {
// Clean up worktree if process failed to start
await this.worktreeManager.removeWorktree(task.id, true);
await this.stateManager.unregisterWorkflow(workflowId);
throw error;
}
} catch (error) {
throw new WorkflowError(
`Failed to start task execution for ${task.id}`,
'TASK_EXECUTION_START_ERROR',
undefined,
task.id,
error as Error
);
}
}
/**
* Stop task execution workflow
*/
async stopTaskExecution(workflowId: string, force = false): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
try {
// Stop the process if running
if (this.processSandbox.isProcessRunning(workflowId)) {
await this.processSandbox.stopProcess(workflowId, force);
}
// Update workflow status
const status: WorkflowStatus = force ? 'cancelled' : 'completed';
await this.stateManager.updateWorkflowStatus(workflowId, status);
// Clean up worktree
await this.worktreeManager.removeWorktree(workflow.taskId, force);
// Emit workflow stopped event
this.emitEvent('workflow.completed', workflowId, workflow.taskId, {
status,
forced: force
});
// Unregister workflow
await this.stateManager.unregisterWorkflow(workflowId);
} catch (error) {
throw new WorkflowError(
`Failed to stop workflow ${workflowId}`,
'WORKFLOW_STOP_ERROR',
workflowId,
workflow.taskId,
error as Error
);
}
}
/**
* Pause task execution
*/
async pauseTaskExecution(workflowId: string): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
if (workflow.status !== 'running') {
throw new WorkflowError(
`Cannot pause workflow ${workflowId} - not currently running`,
'WORKFLOW_NOT_RUNNING',
workflowId,
workflow.taskId
);
}
// For now, we'll just mark as paused - in the future could implement
// process suspension or other pause mechanisms
await this.stateManager.updateWorkflowStatus(workflowId, 'paused');
this.emitEvent('workflow.paused', workflowId, workflow.taskId);
}
/**
* Resume paused task execution
*/
async resumeTaskExecution(workflowId: string): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
if (workflow.status !== 'paused') {
throw new WorkflowError(
`Cannot resume workflow ${workflowId} - not currently paused`,
'WORKFLOW_NOT_PAUSED',
workflowId,
workflow.taskId
);
}
await this.stateManager.updateWorkflowStatus(workflowId, 'running');
this.emitEvent('workflow.resumed', workflowId, workflow.taskId);
}
/**
* Get workflow status
*/
getWorkflowStatus(workflowId: string): WorkflowExecutionContext | undefined {
return this.stateManager.getWorkflow(workflowId);
}
/**
* Get workflow by task ID
*/
getWorkflowByTaskId(taskId: string): WorkflowExecutionContext | undefined {
return this.stateManager.getWorkflowByTaskId(taskId);
}
/**
* List all workflows
*/
listWorkflows(): WorkflowExecutionContext[] {
return this.stateManager.listWorkflows();
}
/**
* List active workflows
*/
listActiveWorkflows(): WorkflowExecutionContext[] {
return this.stateManager.listWorkflowsByStatus('running');
}
/**
* Send input to a running workflow
*/
async sendInputToWorkflow(workflowId: string, input: string): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
if (!this.processSandbox.isProcessRunning(workflowId)) {
throw new WorkflowError(
`Cannot send input to workflow ${workflowId} - process not running`,
'PROCESS_NOT_RUNNING',
workflowId,
workflow.taskId
);
}
await this.processSandbox.sendInput(workflowId, input);
}
/**
* Clean up all workflows
*/
async cleanup(force = false): Promise<void> {
// Stop all processes
await this.processSandbox.cleanupAll(force);
// Clean up all worktrees
await this.worktreeManager.cleanupAll(force);
// Clear workflow state
await this.stateManager.clearState();
}
/**
* Generate task prompt for Claude Code
*/
private generateTaskPrompt(task: Task): string {
const prompt = [
`Work on Task ${task.id}: ${task.title}`,
'',
`Description: ${task.description}`
];
if (task.details) {
prompt.push('', `Details: ${task.details}`);
}
if (task.testStrategy) {
prompt.push('', `Test Strategy: ${task.testStrategy}`);
}
if (task.dependencies?.length) {
prompt.push('', `Dependencies: ${task.dependencies.join(', ')}`);
}
prompt.push(
'',
'Please implement this task following the project conventions and best practices.',
'When complete, update the task status appropriately using the available Task Master commands.'
);
return prompt.join('\n');
}
/**
* Clean up stale workflows from previous sessions
*/
private async cleanupStaleWorkflows(): Promise<void> {
const workflows = this.stateManager.listWorkflows();
for (const workflow of workflows) {
const isStale =
workflow.status === 'running' &&
!this.processSandbox.isProcessRunning(`workflow-${workflow.taskId}`);
if (isStale) {
console.log(`Cleaning up stale workflow for task ${workflow.taskId}`);
try {
await this.stateManager.updateWorkflowStatus(
`workflow-${workflow.taskId}`,
'failed'
);
// Try to clean up worktree
await this.worktreeManager.removeWorktree(workflow.taskId, true);
} catch (error) {
console.error(`Failed to cleanup stale workflow:`, error);
}
}
}
}
/**
* Emit workflow event
*/
private emitEvent(
type: string,
workflowId: string,
taskId: string,
data?: any
): void {
const event: WorkflowEvent = {
type: type as any,
workflowId,
taskId,
timestamp: new Date(),
data
};
this.emit('event', event);
this.emit(type, event);
}
}

View File

@@ -0,0 +1,6 @@
/**
* @fileoverview Workflow Engine Types
* Public type exports
*/
export * from './workflow.types.js';

View File

@@ -0,0 +1,119 @@
/**
* @fileoverview Workflow Engine Types
* Core types for workflow execution system
*/
export interface WorkflowConfig {
/** Maximum number of concurrent workflows */
maxConcurrent: number;
/** Default timeout for workflow execution (minutes) */
defaultTimeout: number;
/** Base directory for worktrees */
worktreeBase: string;
/** Claude Code executable path */
claudeExecutable: string;
/** Enable debug logging */
debug: boolean;
}
export interface WorkflowExecutionContext {
/** Task ID being executed */
taskId: string;
/** Task title for display */
taskTitle: string;
/** Full task description */
taskDescription: string;
/** Task implementation details */
taskDetails?: string;
/** Project root path */
projectRoot: string;
/** Worktree path */
worktreePath: string;
/** Branch name for this workflow */
branchName: string;
/** Process ID of running Claude Code */
processId?: number;
/** Workflow start time */
startedAt: Date;
/** Workflow status */
status: WorkflowStatus;
/** Last activity timestamp */
lastActivity: Date;
/** Execution metadata */
metadata?: Record<string, any>;
}
export type WorkflowStatus =
| 'pending' // Created but not started
| 'initializing' // Setting up worktree/process
| 'running' // Active execution
| 'paused' // Temporarily stopped
| 'completed' // Successfully finished
| 'failed' // Error occurred
| 'cancelled' // User cancelled
| 'timeout'; // Exceeded time limit
export interface WorkflowEvent {
type: WorkflowEventType;
workflowId: string;
taskId: string;
timestamp: Date;
data?: any;
error?: Error;
}
export type WorkflowEventType =
| 'workflow.created'
| 'workflow.started'
| 'workflow.paused'
| 'workflow.resumed'
| 'workflow.completed'
| 'workflow.failed'
| 'workflow.cancelled'
| 'worktree.created'
| 'worktree.deleted'
| 'process.started'
| 'process.stopped'
| 'process.output'
| 'process.error';
export interface WorkflowProcess {
/** Process ID */
pid: number;
/** Command that was executed */
command: string;
/** Command arguments */
args: string[];
/** Working directory */
cwd: string;
/** Environment variables */
env?: Record<string, string>;
/** Process start time */
startedAt: Date;
/** Process status */
status: ProcessStatus;
}
export type ProcessStatus =
| 'starting'
| 'running'
| 'stopped'
| 'crashed'
| 'killed';
export interface WorktreeInfo {
/** Worktree path */
path: string;
/** Branch name */
branch: string;
/** Creation timestamp */
createdAt: Date;
/** Associated task ID */
taskId: string;
/** Git commit hash */
commit?: string;
/** Worktree lock status */
locked: boolean;
/** Lock reason if applicable */
lockReason?: string;
}

View File

@@ -0,0 +1,6 @@
/**
* @fileoverview Worktree Management
* Public exports for worktree operations
*/
export * from './worktree-manager.js';

View File

@@ -0,0 +1,351 @@
/**
* @fileoverview Worktree Manager
* Manages git worktree lifecycle for task execution
*/
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { WorktreeInfo } from '../types/workflow.types.js';
import { WorktreeError } from '../errors/workflow.errors.js';
export interface WorktreeManagerConfig {
/** Base directory for all worktrees */
worktreeBase: string;
/** Project root directory */
projectRoot: string;
/** Auto-cleanup on process exit */
autoCleanup: boolean;
}
/**
* WorktreeManager handles git worktree operations
* Single responsibility: Git worktree lifecycle management
*/
export class WorktreeManager {
private config: WorktreeManagerConfig;
private activeWorktrees = new Map<string, WorktreeInfo>();
constructor(config: WorktreeManagerConfig) {
this.config = config;
if (config.autoCleanup) {
this.setupCleanupHandlers();
}
}
/**
* Create a new worktree for task execution
*/
async createWorktree(taskId: string, branchName?: string): Promise<WorktreeInfo> {
const sanitizedTaskId = this.sanitizeTaskId(taskId);
const worktreePath = path.join(this.config.worktreeBase, `task-${sanitizedTaskId}`);
// Ensure base directory exists
await fs.mkdir(this.config.worktreeBase, { recursive: true });
// Generate unique branch name if not provided
const branch = branchName || `task/${sanitizedTaskId}-${Date.now()}`;
try {
// Check if worktree path already exists
if (await this.pathExists(worktreePath)) {
throw new WorktreeError(`Worktree path already exists: ${worktreePath}`);
}
// Create the worktree
await this.executeGitCommand(['worktree', 'add', '-b', branch, worktreePath], {
cwd: this.config.projectRoot
});
const worktreeInfo: WorktreeInfo = {
path: worktreePath,
branch,
createdAt: new Date(),
taskId,
locked: false
};
// Get commit hash
try {
const commit = await this.executeGitCommand(['rev-parse', 'HEAD'], {
cwd: worktreePath
});
worktreeInfo.commit = commit.trim();
} catch (error) {
console.warn('Failed to get commit hash for worktree:', error);
}
this.activeWorktrees.set(taskId, worktreeInfo);
return worktreeInfo;
} catch (error) {
throw new WorktreeError(
`Failed to create worktree for task ${taskId}`,
worktreePath,
error as Error
);
}
}
/**
* Remove a worktree and clean up
*/
async removeWorktree(taskId: string, force = false): Promise<void> {
const worktreeInfo = this.activeWorktrees.get(taskId);
if (!worktreeInfo) {
throw new WorktreeError(`No active worktree found for task ${taskId}`);
}
try {
// Remove the worktree
const args = ['worktree', 'remove', worktreeInfo.path];
if (force) {
args.push('--force');
}
await this.executeGitCommand(args, {
cwd: this.config.projectRoot
});
// Remove branch if it's a task-specific branch
if (worktreeInfo.branch.startsWith('task/')) {
try {
await this.executeGitCommand(['branch', '-D', worktreeInfo.branch], {
cwd: this.config.projectRoot
});
} catch (error) {
console.warn(`Failed to delete branch ${worktreeInfo.branch}:`, error);
}
}
this.activeWorktrees.delete(taskId);
} catch (error) {
throw new WorktreeError(
`Failed to remove worktree for task ${taskId}`,
worktreeInfo.path,
error as Error
);
}
}
/**
* List all active worktrees for this project
*/
async listWorktrees(): Promise<WorktreeInfo[]> {
try {
const output = await this.executeGitCommand(['worktree', 'list', '--porcelain'], {
cwd: this.config.projectRoot
});
const worktrees: WorktreeInfo[] = [];
const lines = output.trim().split('\n');
let currentWorktree: Partial<WorktreeInfo> = {};
for (const line of lines) {
if (line.startsWith('worktree ')) {
if (currentWorktree.path) {
// Complete previous worktree
worktrees.push(this.completeWorktreeInfo(currentWorktree));
}
currentWorktree = { path: line.substring(9) };
} else if (line.startsWith('HEAD ')) {
currentWorktree.commit = line.substring(5);
} else if (line.startsWith('branch ')) {
currentWorktree.branch = line.substring(7).replace('refs/heads/', '');
} else if (line === 'locked') {
currentWorktree.locked = true;
} else if (line.startsWith('locked ')) {
currentWorktree.locked = true;
currentWorktree.lockReason = line.substring(7);
}
}
// Add the last worktree
if (currentWorktree.path) {
worktrees.push(this.completeWorktreeInfo(currentWorktree));
}
// Filter to only our task worktrees
return worktrees.filter(wt =>
wt.path.startsWith(this.config.worktreeBase) &&
wt.branch?.startsWith('task/')
);
} catch (error) {
throw new WorktreeError('Failed to list worktrees', undefined, error as Error);
}
}
/**
* Get worktree info for a specific task
*/
getWorktreeInfo(taskId: string): WorktreeInfo | undefined {
return this.activeWorktrees.get(taskId);
}
/**
* Lock a worktree to prevent cleanup
*/
async lockWorktree(taskId: string, reason?: string): Promise<void> {
const worktreeInfo = this.activeWorktrees.get(taskId);
if (!worktreeInfo) {
throw new WorktreeError(`No active worktree found for task ${taskId}`);
}
try {
const args = ['worktree', 'lock', worktreeInfo.path];
if (reason) {
args.push('--reason', reason);
}
await this.executeGitCommand(args, {
cwd: this.config.projectRoot
});
worktreeInfo.locked = true;
worktreeInfo.lockReason = reason;
} catch (error) {
throw new WorktreeError(
`Failed to lock worktree for task ${taskId}`,
worktreeInfo.path,
error as Error
);
}
}
/**
* Unlock a worktree
*/
async unlockWorktree(taskId: string): Promise<void> {
const worktreeInfo = this.activeWorktrees.get(taskId);
if (!worktreeInfo) {
throw new WorktreeError(`No active worktree found for task ${taskId}`);
}
try {
await this.executeGitCommand(['worktree', 'unlock', worktreeInfo.path], {
cwd: this.config.projectRoot
});
worktreeInfo.locked = false;
delete worktreeInfo.lockReason;
} catch (error) {
throw new WorktreeError(
`Failed to unlock worktree for task ${taskId}`,
worktreeInfo.path,
error as Error
);
}
}
/**
* Clean up all task-related worktrees
*/
async cleanupAll(force = false): Promise<void> {
const worktrees = await this.listWorktrees();
for (const worktree of worktrees) {
if (worktree.taskId) {
try {
await this.removeWorktree(worktree.taskId, force);
} catch (error) {
console.error(`Failed to cleanup worktree for task ${worktree.taskId}:`, error);
}
}
}
}
/**
* Execute git command and return output
*/
private async executeGitCommand(
args: string[],
options: { cwd: string }
): Promise<string> {
return new Promise((resolve, reject) => {
const git = spawn('git', args, {
cwd: options.cwd,
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
git.stdout.on('data', (data) => {
stdout += data.toString();
});
git.stderr.on('data', (data) => {
stderr += data.toString();
});
git.on('close', (code) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`Git command failed (${code}): ${stderr || stdout}`));
}
});
git.on('error', (error) => {
reject(error);
});
});
}
/**
* Sanitize task ID for use in filesystem paths
*/
private sanitizeTaskId(taskId: string): string {
return taskId.replace(/[^a-zA-Z0-9.-]/g, '-');
}
/**
* Check if path exists
*/
private async pathExists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
/**
* Complete worktree info with defaults
*/
private completeWorktreeInfo(partial: Partial<WorktreeInfo>): WorktreeInfo {
const branch = partial.branch || 'unknown';
const taskIdMatch = branch.match(/^task\/(.+?)-/);
return {
path: partial.path || '',
branch,
createdAt: partial.createdAt || new Date(),
taskId: taskIdMatch?.[1] || partial.taskId || 'unknown',
commit: partial.commit,
locked: partial.locked || false,
lockReason: partial.lockReason
};
}
/**
* Setup cleanup handlers for graceful shutdown
*/
private setupCleanupHandlers(): void {
const cleanup = () => {
console.log('Cleaning up worktrees...');
this.cleanupAll(true).catch(console.error);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);
}
}

View File

@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"dist",
"node_modules",
"**/*.test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: [
'src/index.ts',
'src/task-execution/index.ts',
'src/worktree/index.ts',
'src/process/index.ts',
'src/state/index.ts'
],
format: ['esm'],
dts: true,
sourcemap: true,
clean: true,
splitting: false,
treeshake: true
});

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.d.ts',
'**/*.test.ts',
'**/*.spec.ts'
]
}
}
});

View File

@@ -15,8 +15,8 @@ import search from '@inquirer/search';
import ora from 'ora'; // Import ora import ora from 'ora'; // Import ora
import { log, readJSON } from './utils.js'; import { log, readJSON } from './utils.js';
// Import new commands from @tm/cli // Import command registry from @tm/cli
import { ListTasksCommand, AuthCommand, ContextCommand } from '@tm/cli'; import { registerAllCommands } from '@tm/cli';
import { import {
parsePRD, parsePRD,
@@ -1737,17 +1737,9 @@ function registerCommands(programInstance) {
}); });
}); });
// NEW: Register the new list command from @tm/cli // Register all commands from @tm/cli using the command registry
// This command handles all its own configuration and logic // This automatically registers ListTasksCommand, AuthCommand, and any future commands
ListTasksCommand.registerOn(programInstance); registerAllCommands(programInstance);
// Register the auth command from @tm/cli
// Handles authentication with tryhamster.com
AuthCommand.registerOn(programInstance);
// Register the context command from @tm/cli
// Manages workspace context (org/brief selection)
ContextCommand.registerOn(programInstance);
// expand command // expand command
programInstance programInstance