Co-authored-by: Max Tuzzolino <maxtuzz@Maxs-MacBook-Pro.local>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Max Tuzzolino <max.tuzsmith@gmail.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
Eyal Toledano
2025-09-19 18:08:20 -04:00
committed by GitHub
parent 4e126430a0
commit fce841490a
55 changed files with 3559 additions and 693 deletions

View File

@@ -0,0 +1,80 @@
/**
* Base executor class providing common functionality for all executors
*/
import type { Task } from '../types/index.js';
import type { ITaskExecutor, ExecutorType, ExecutionResult } from './types.js';
import { getLogger } from '../logger/index.js';
export abstract class BaseExecutor implements ITaskExecutor {
protected readonly logger = getLogger('BaseExecutor');
protected readonly projectRoot: string;
protected readonly config: Record<string, any>;
constructor(projectRoot: string, config: Record<string, any> = {}) {
this.projectRoot = projectRoot;
this.config = config;
}
abstract execute(task: Task): Promise<ExecutionResult>;
abstract getType(): ExecutorType;
abstract isAvailable(): Promise<boolean>;
/**
* Format task details into a readable prompt
*/
protected formatTaskPrompt(task: Task): string {
const sections: string[] = [];
sections.push(`Task ID: ${task.id}`);
sections.push(`Title: ${task.title}`);
if (task.description) {
sections.push(`\nDescription:\n${task.description}`);
}
if (task.details) {
sections.push(`\nImplementation Details:\n${task.details}`);
}
if (task.testStrategy) {
sections.push(`\nTest Strategy:\n${task.testStrategy}`);
}
if (task.dependencies && task.dependencies.length > 0) {
sections.push(`\nDependencies: ${task.dependencies.join(', ')}`);
}
if (task.subtasks && task.subtasks.length > 0) {
const subtaskList = task.subtasks
.map((st) => ` - [${st.status}] ${st.id}: ${st.title}`)
.join('\n');
sections.push(`\nSubtasks:\n${subtaskList}`);
}
sections.push(`\nStatus: ${task.status}`);
sections.push(`Priority: ${task.priority}`);
return sections.join('\n');
}
/**
* Create base execution result
*/
protected createResult(
taskId: string,
success: boolean,
output?: string,
error?: string
): ExecutionResult {
return {
success,
taskId,
executorType: this.getType(),
output,
error,
startTime: new Date().toISOString(),
endTime: new Date().toISOString()
};
}
}

View File

@@ -0,0 +1,147 @@
/**
* Claude executor implementation for Task Master
*/
import { spawn } from 'child_process';
import type { Task } from '../types/index.js';
import type {
ExecutorType,
ExecutionResult,
ClaudeExecutorConfig
} from './types.js';
import { BaseExecutor } from './base-executor.js';
export class ClaudeExecutor extends BaseExecutor {
private claudeConfig: ClaudeExecutorConfig;
private currentProcess: any = null;
constructor(projectRoot: string, config: ClaudeExecutorConfig = {}) {
super(projectRoot, config);
this.claudeConfig = {
command: config.command || 'claude',
systemPrompt:
config.systemPrompt ||
'You are a helpful AI assistant helping to complete a software development task.',
additionalFlags: config.additionalFlags || []
};
}
getType(): ExecutorType {
return 'claude';
}
async isAvailable(): Promise<boolean> {
return new Promise((resolve) => {
const checkProcess = spawn('which', [this.claudeConfig.command!], {
shell: true
});
checkProcess.on('close', (code) => {
resolve(code === 0);
});
checkProcess.on('error', () => {
resolve(false);
});
});
}
async execute(task: Task): Promise<ExecutionResult> {
const startTime = new Date().toISOString();
try {
// Check if Claude is available
const isAvailable = await this.isAvailable();
if (!isAvailable) {
return this.createResult(
task.id,
false,
undefined,
`Claude CLI not found. Please ensure 'claude' command is available in PATH.`
);
}
// Format the task into a prompt
const taskPrompt = this.formatTaskPrompt(task);
const fullPrompt = `${this.claudeConfig.systemPrompt}\n\nHere is the task to complete:\n\n${taskPrompt}`;
// Execute Claude with the task details
const result = await this.runClaude(fullPrompt, task.id);
return {
...result,
startTime,
endTime: new Date().toISOString()
};
} catch (error: any) {
this.logger.error(`Failed to execute task ${task.id}:`, error);
return this.createResult(
task.id,
false,
undefined,
error.message || 'Unknown error occurred'
);
}
}
private runClaude(prompt: string, taskId: string): Promise<ExecutionResult> {
return new Promise((resolve) => {
const args = [prompt, ...this.claudeConfig.additionalFlags!];
this.logger.info(`Executing Claude for task ${taskId}`);
this.logger.debug(
`Command: ${this.claudeConfig.command} ${args.join(' ')}`
);
this.currentProcess = spawn(this.claudeConfig.command!, args, {
cwd: this.projectRoot,
shell: false,
stdio: 'inherit' // Let Claude handle its own I/O
});
this.currentProcess.on('close', (code: number) => {
this.currentProcess = null;
if (code === 0) {
resolve(
this.createResult(
taskId,
true,
'Claude session completed successfully'
)
);
} else {
resolve(
this.createResult(
taskId,
false,
undefined,
`Claude exited with code ${code}`
)
);
}
});
this.currentProcess.on('error', (error: any) => {
this.currentProcess = null;
this.logger.error(`Claude process error:`, error);
resolve(
this.createResult(
taskId,
false,
undefined,
`Failed to spawn Claude: ${error.message}`
)
);
});
});
}
async stop(): Promise<void> {
if (this.currentProcess) {
this.logger.info('Stopping Claude process...');
this.currentProcess.kill('SIGTERM');
this.currentProcess = null;
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* Factory for creating task executors
*/
import type { ITaskExecutor, ExecutorOptions, ExecutorType } from './types.js';
import { ClaudeExecutor } from './claude-executor.js';
import { getLogger } from '../logger/index.js';
export class ExecutorFactory {
private static logger = getLogger('ExecutorFactory');
/**
* Create an executor based on the provided options
*/
static create(options: ExecutorOptions): ITaskExecutor {
this.logger.debug(`Creating executor of type: ${options.type}`);
switch (options.type) {
case 'claude':
return new ClaudeExecutor(options.projectRoot, options.config);
case 'shell':
// Placeholder for shell executor
throw new Error('Shell executor not yet implemented');
case 'custom':
// Placeholder for custom executor
throw new Error('Custom executor not yet implemented');
default:
throw new Error(`Unknown executor type: ${options.type}`);
}
}
/**
* Get the default executor type based on available tools
*/
static async getDefaultExecutor(
projectRoot: string
): Promise<ExecutorType | null> {
// Check for Claude first
const claudeExecutor = new ClaudeExecutor(projectRoot);
if (await claudeExecutor.isAvailable()) {
this.logger.info('Claude CLI detected as default executor');
return 'claude';
}
// Could check for other executors here
this.logger.warn('No default executor available');
return null;
}
/**
* Get list of available executor types
*/
static getAvailableTypes(): ExecutorType[] {
return ['claude', 'shell', 'custom'];
}
}

View File

@@ -0,0 +1,105 @@
/**
* Service for managing task execution
*/
import type { Task } from '../types/index.js';
import type {
ITaskExecutor,
ExecutorOptions,
ExecutionResult,
ExecutorType
} from './types.js';
import { ExecutorFactory } from './executor-factory.js';
import { getLogger } from '../logger/index.js';
export interface ExecutorServiceOptions {
projectRoot: string;
defaultExecutor?: ExecutorType;
executorConfig?: Record<string, any>;
}
export class ExecutorService {
private logger = getLogger('ExecutorService');
private projectRoot: string;
private defaultExecutor?: ExecutorType;
private executorConfig: Record<string, any>;
private currentExecutor?: ITaskExecutor;
constructor(options: ExecutorServiceOptions) {
this.projectRoot = options.projectRoot;
this.defaultExecutor = options.defaultExecutor;
this.executorConfig = options.executorConfig || {};
}
/**
* Execute a task
*/
async executeTask(
task: Task,
executorType?: ExecutorType
): Promise<ExecutionResult> {
try {
// Determine executor type
const type =
executorType ||
this.defaultExecutor ||
(await ExecutorFactory.getDefaultExecutor(this.projectRoot));
if (!type) {
return {
success: false,
taskId: task.id,
executorType: 'claude',
error:
'No executor available. Please install Claude CLI or specify an executor type.',
startTime: new Date().toISOString()
};
}
// Create executor
const executorOptions: ExecutorOptions = {
type,
projectRoot: this.projectRoot,
config: this.executorConfig
};
this.currentExecutor = ExecutorFactory.create(executorOptions);
// Check if executor is available
const isAvailable = await this.currentExecutor.isAvailable();
if (!isAvailable) {
return {
success: false,
taskId: task.id,
executorType: type,
error: `Executor ${type} is not available or not configured properly`,
startTime: new Date().toISOString()
};
}
// Execute the task
this.logger.info(`Starting task ${task.id} with ${type} executor`);
const result = await this.currentExecutor.execute(task);
return result;
} catch (error: any) {
this.logger.error(`Failed to execute task ${task.id}:`, error);
return {
success: false,
taskId: task.id,
executorType: executorType || 'claude',
error: error.message || 'Unknown error occurred',
startTime: new Date().toISOString()
};
}
}
/**
* Stop the current task execution
*/
async stopCurrentTask(): Promise<void> {
if (this.currentExecutor && this.currentExecutor.stop) {
await this.currentExecutor.stop();
this.currentExecutor = undefined;
}
}
}

View File

@@ -0,0 +1,12 @@
/**
* Public API for the executors module
*/
export * from './types.js';
export { BaseExecutor } from './base-executor.js';
export { ClaudeExecutor } from './claude-executor.js';
export { ExecutorFactory } from './executor-factory.js';
export {
ExecutorService,
type ExecutorServiceOptions
} from './executor-service.js';

View File

@@ -0,0 +1,76 @@
/**
* Executor types and interfaces for Task Master
*/
import type { Task } from '../types/index.js';
/**
* Supported executor types
*/
export type ExecutorType = 'claude' | 'shell' | 'custom';
/**
* Options for executor creation
*/
export interface ExecutorOptions {
type: ExecutorType;
projectRoot: string;
config?: Record<string, any>;
}
/**
* Result from task execution
*/
export interface ExecutionResult {
success: boolean;
taskId: string;
executorType: ExecutorType;
output?: string;
error?: string;
startTime: string;
endTime?: string;
processId?: number;
}
/**
* Base interface for all task executors
*/
export interface ITaskExecutor {
/**
* Execute a task
*/
execute(task: Task): Promise<ExecutionResult>;
/**
* Stop a running task execution
*/
stop?(): Promise<void>;
/**
* Get executor type
*/
getType(): ExecutorType;
/**
* Check if executor is available/configured
*/
isAvailable(): Promise<boolean>;
}
/**
* Configuration for Claude executor
*/
export interface ClaudeExecutorConfig {
command?: string; // Default: 'claude'
systemPrompt?: string;
additionalFlags?: string[];
}
/**
* Configuration for Shell executor
*/
export interface ShellExecutorConfig {
shell?: string; // Default: '/bin/bash'
env?: Record<string, string>;
cwd?: string;
}