Compare commits
2 Commits
extension@
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d11093732 | ||
|
|
7c1d05958f |
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
38
apps/cli/src/commands/index.ts
Normal file
38
apps/cli/src/commands/index.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
58
apps/cli/src/commands/workflow.command.ts
Normal file
58
apps/cli/src/commands/workflow.command.ts
Normal 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;
|
||||||
9
apps/cli/src/commands/workflow/index.ts
Normal file
9
apps/cli/src/commands/workflow/index.ts
Normal 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';
|
||||||
253
apps/cli/src/commands/workflow/workflow-list.command.ts
Normal file
253
apps/cli/src/commands/workflow/workflow-list.command.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
239
apps/cli/src/commands/workflow/workflow-start.command.ts
Normal file
239
apps/cli/src/commands/workflow/workflow-start.command.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
339
apps/cli/src/commands/workflow/workflow-status.command.ts
Normal file
339
apps/cli/src/commands/workflow/workflow-status.command.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
260
apps/cli/src/commands/workflow/workflow-stop.command.ts
Normal file
260
apps/cli/src/commands/workflow/workflow-stop.command.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
221
apps/docs/capabilities/workflows.mdx
Normal file
221
apps/docs/capabilities/workflows.mdx
Normal 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.
|
||||||
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
40
output.txt
Normal file
File diff suppressed because one or more lines are too long
45
package-lock.json
generated
45
package-lock.json
generated
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
packages/tm-core/src/workflow/index.ts
Normal file
17
packages/tm-core/src/workflow/index.ts
Normal 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';
|
||||||
218
packages/tm-core/src/workflow/workflow-service.ts
Normal file
218
packages/tm-core/src/workflow/workflow-service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
371
packages/workflow-engine/README.md
Normal file
371
packages/workflow-engine/README.md
Normal 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
|
||||||
56
packages/workflow-engine/package.json
Normal file
56
packages/workflow-engine/package.json
Normal 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"
|
||||||
|
}
|
||||||
6
packages/workflow-engine/src/errors/index.ts
Normal file
6
packages/workflow-engine/src/errors/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Workflow Engine Errors
|
||||||
|
* Public error exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './workflow.errors.js';
|
||||||
59
packages/workflow-engine/src/errors/workflow.errors.ts
Normal file
59
packages/workflow-engine/src/errors/workflow.errors.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/workflow-engine/src/index.ts
Normal file
19
packages/workflow-engine/src/index.ts
Normal 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';
|
||||||
6
packages/workflow-engine/src/process/index.ts
Normal file
6
packages/workflow-engine/src/process/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Process Management
|
||||||
|
* Public exports for process operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './process-sandbox.js';
|
||||||
378
packages/workflow-engine/src/process/process-sandbox.ts
Normal file
378
packages/workflow-engine/src/process/process-sandbox.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/workflow-engine/src/state/index.ts
Normal file
6
packages/workflow-engine/src/state/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview State Management
|
||||||
|
* Public exports for workflow state operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './workflow-state-manager.js';
|
||||||
320
packages/workflow-engine/src/state/workflow-state-manager.ts
Normal file
320
packages/workflow-engine/src/state/workflow-state-manager.ts
Normal 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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/workflow-engine/src/task-execution/index.ts
Normal file
6
packages/workflow-engine/src/task-execution/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Task Execution Management
|
||||||
|
* Public exports for task execution operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './task-execution-manager.js';
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/workflow-engine/src/types/index.ts
Normal file
6
packages/workflow-engine/src/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Workflow Engine Types
|
||||||
|
* Public type exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './workflow.types.js';
|
||||||
119
packages/workflow-engine/src/types/workflow.types.ts
Normal file
119
packages/workflow-engine/src/types/workflow.types.ts
Normal 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;
|
||||||
|
}
|
||||||
6
packages/workflow-engine/src/worktree/index.ts
Normal file
6
packages/workflow-engine/src/worktree/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Worktree Management
|
||||||
|
* Public exports for worktree operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './worktree-manager.js';
|
||||||
351
packages/workflow-engine/src/worktree/worktree-manager.ts
Normal file
351
packages/workflow-engine/src/worktree/worktree-manager.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/workflow-engine/tsconfig.json
Normal file
19
packages/workflow-engine/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
17
packages/workflow-engine/tsup.config.ts
Normal file
17
packages/workflow-engine/tsup.config.ts
Normal 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
|
||||||
|
});
|
||||||
19
packages/workflow-engine/vitest.config.ts
Normal file
19
packages/workflow-engine/vitest.config.ts
Normal 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'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user