Tm start (#1200)
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:
80
packages/tm-core/src/executors/base-executor.ts
Normal file
80
packages/tm-core/src/executors/base-executor.ts
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
147
packages/tm-core/src/executors/claude-executor.ts
Normal file
147
packages/tm-core/src/executors/claude-executor.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
packages/tm-core/src/executors/executor-factory.ts
Normal file
59
packages/tm-core/src/executors/executor-factory.ts
Normal 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'];
|
||||
}
|
||||
}
|
||||
105
packages/tm-core/src/executors/executor-service.ts
Normal file
105
packages/tm-core/src/executors/executor-service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
packages/tm-core/src/executors/index.ts
Normal file
12
packages/tm-core/src/executors/index.ts
Normal 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';
|
||||
76
packages/tm-core/src/executors/types.ts
Normal file
76
packages/tm-core/src/executors/types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user