feat: Phase 1 - Complete TDD Workflow Automation System (#1289)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Ralph Khreish
2025-10-14 20:25:01 +02:00
parent c92cee72c7
commit 11ace9da1f
83 changed files with 17215 additions and 2342 deletions

View File

@@ -14,7 +14,7 @@ import { ContextCommand } from './commands/context.command.js';
import { StartCommand } from './commands/start.command.js';
import { SetStatusCommand } from './commands/set-status.command.js';
import { ExportCommand } from './commands/export.command.js';
import { AutopilotCommand } from './commands/autopilot.command.js';
import { AutopilotCommand } from './commands/autopilot/index.js';
/**
* Command metadata for registration
@@ -73,7 +73,8 @@ export class CommandRegistry {
},
{
name: 'autopilot',
description: 'Execute a task autonomously using TDD workflow',
description:
'AI agent orchestration for TDD workflow (start, resume, next, complete, commit, status, abort)',
commandClass: AutopilotCommand as any,
category: 'development'
},

View File

@@ -0,0 +1,119 @@
/**
* @fileoverview Abort Command - Safely terminate workflow
*/
import { Command } from 'commander';
import { WorkflowOrchestrator } from '@tm/core';
import {
AutopilotBaseOptions,
hasWorkflowState,
loadWorkflowState,
deleteWorkflowState,
OutputFormatter
} from './shared.js';
import inquirer from 'inquirer';
interface AbortOptions extends AutopilotBaseOptions {
force?: boolean;
}
/**
* Abort Command - Safely terminate workflow and clean up state
*/
export class AbortCommand extends Command {
constructor() {
super('abort');
this.description('Abort the current TDD workflow and clean up state')
.option('-f, --force', 'Force abort without confirmation')
.action(async (options: AbortOptions) => {
await this.execute(options);
});
}
private async execute(options: AbortOptions): Promise<void> {
// Inherit parent options
const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
const mergedOptions: AbortOptions = {
...parentOpts,
...options,
projectRoot:
options.projectRoot || parentOpts?.projectRoot || process.cwd()
};
const formatter = new OutputFormatter(mergedOptions.json || false);
try {
// Check for workflow state
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
if (!hasState) {
formatter.warning('No active workflow to abort');
return;
}
// Load state
const state = await loadWorkflowState(mergedOptions.projectRoot!);
if (!state) {
formatter.error('Failed to load workflow state');
process.exit(1);
}
// Restore orchestrator
const orchestrator = new WorkflowOrchestrator(state.context);
orchestrator.restoreState(state);
// Get progress before abort
const progress = orchestrator.getProgress();
const currentSubtask = orchestrator.getCurrentSubtask();
// Confirm abort if not forced or in JSON mode
if (!mergedOptions.force && !mergedOptions.json) {
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message:
`This will abort the workflow for task ${state.context.taskId}. ` +
`Progress: ${progress.completed}/${progress.total} subtasks completed. ` +
`Continue?`,
default: false
}
]);
if (!confirmed) {
formatter.info('Abort cancelled');
return;
}
}
// Trigger abort in orchestrator
orchestrator.transition({ type: 'ABORT' });
// Delete workflow state
await deleteWorkflowState(mergedOptions.projectRoot!);
// Output result
formatter.success('Workflow aborted', {
taskId: state.context.taskId,
branchName: state.context.branchName,
progress: {
completed: progress.completed,
total: progress.total
},
lastSubtask: currentSubtask
? {
id: currentSubtask.id,
title: currentSubtask.title
}
: null,
note: 'Branch and commits remain. Clean up manually if needed.'
});
} catch (error) {
formatter.error((error as Error).message);
if (mergedOptions.verbose) {
console.error((error as Error).stack);
}
process.exit(1);
}
}
}

View File

@@ -0,0 +1,169 @@
/**
* @fileoverview Commit Command - Create commit with enhanced message generation
*/
import { Command } from 'commander';
import { WorkflowOrchestrator } from '@tm/core';
import {
AutopilotBaseOptions,
hasWorkflowState,
loadWorkflowState,
createGitAdapter,
createCommitMessageGenerator,
OutputFormatter,
saveWorkflowState
} from './shared.js';
type CommitOptions = AutopilotBaseOptions;
/**
* Commit Command - Create commit using enhanced message generator
*/
export class CommitCommand extends Command {
constructor() {
super('commit');
this.description('Create a commit for the completed GREEN phase').action(
async (options: CommitOptions) => {
await this.execute(options);
}
);
}
private async execute(options: CommitOptions): Promise<void> {
// Inherit parent options
const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
const mergedOptions: CommitOptions = {
...parentOpts,
...options,
projectRoot:
options.projectRoot || parentOpts?.projectRoot || process.cwd()
};
const formatter = new OutputFormatter(mergedOptions.json || false);
try {
// Check for workflow state
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
if (!hasState) {
formatter.error('No active workflow', {
suggestion: 'Start a workflow with: autopilot start <taskId>'
});
process.exit(1);
}
// Load state
const state = await loadWorkflowState(mergedOptions.projectRoot!);
if (!state) {
formatter.error('Failed to load workflow state');
process.exit(1);
}
const orchestrator = new WorkflowOrchestrator(state.context);
orchestrator.restoreState(state);
orchestrator.enableAutoPersist(async (newState) => {
await saveWorkflowState(mergedOptions.projectRoot!, newState);
});
// Verify in COMMIT phase
const tddPhase = orchestrator.getCurrentTDDPhase();
if (tddPhase !== 'COMMIT') {
formatter.error('Not in COMMIT phase', {
currentPhase: tddPhase || orchestrator.getCurrentPhase(),
suggestion: 'Complete RED and GREEN phases first'
});
process.exit(1);
}
// Get current subtask
const currentSubtask = orchestrator.getCurrentSubtask();
if (!currentSubtask) {
formatter.error('No current subtask');
process.exit(1);
}
// Initialize git adapter
const gitAdapter = createGitAdapter(mergedOptions.projectRoot!);
await gitAdapter.ensureGitRepository();
// Check for staged changes
const hasStagedChanges = await gitAdapter.hasStagedChanges();
if (!hasStagedChanges) {
// Stage all changes
formatter.info('No staged changes, staging all changes...');
await gitAdapter.stageFiles(['.']);
}
// Get changed files for scope detection
const status = await gitAdapter.getStatus();
const changedFiles = [...status.staged, ...status.modified];
// Generate commit message
const messageGenerator = createCommitMessageGenerator();
const testResults = state.context.lastTestResults;
const commitMessage = messageGenerator.generateMessage({
type: 'feat',
description: currentSubtask.title,
changedFiles,
taskId: state.context.taskId,
phase: 'TDD',
tag: (state.context.metadata.tag as string) || undefined,
testsPassing: testResults?.passed,
testsFailing: testResults?.failed,
coveragePercent: undefined // Could be added if available
});
// Create commit with metadata
await gitAdapter.createCommit(commitMessage, {
metadata: {
taskId: state.context.taskId,
subtaskId: currentSubtask.id,
phase: 'COMMIT',
tddCycle: 'complete'
}
});
// Get commit info
const lastCommit = await gitAdapter.getLastCommit();
// Complete COMMIT phase (this marks subtask as completed)
orchestrator.transition({ type: 'COMMIT_COMPLETE' });
// Check if should advance to next subtask
const progress = orchestrator.getProgress();
if (progress.current < progress.total) {
orchestrator.transition({ type: 'SUBTASK_COMPLETE' });
} else {
// All subtasks complete
orchestrator.transition({ type: 'ALL_SUBTASKS_COMPLETE' });
}
// Output success
formatter.success('Commit created', {
commitHash: lastCommit.hash.substring(0, 7),
message: commitMessage.split('\n')[0], // First line only
subtask: {
id: currentSubtask.id,
title: currentSubtask.title,
status: currentSubtask.status
},
progress: {
completed: progress.completed,
total: progress.total,
percentage: progress.percentage
},
nextAction:
progress.completed < progress.total
? 'Start next subtask with RED phase'
: 'All subtasks complete. Run: autopilot status'
});
} catch (error) {
formatter.error((error as Error).message);
if (mergedOptions.verbose) {
console.error((error as Error).stack);
}
process.exit(1);
}
}
}

View File

@@ -0,0 +1,172 @@
/**
* @fileoverview Complete Command - Complete current TDD phase with validation
*/
import { Command } from 'commander';
import { WorkflowOrchestrator, TestResult } from '@tm/core';
import {
AutopilotBaseOptions,
hasWorkflowState,
loadWorkflowState,
OutputFormatter
} from './shared.js';
interface CompleteOptions extends AutopilotBaseOptions {
results?: string;
coverage?: string;
}
/**
* Complete Command - Mark current phase as complete with validation
*/
export class CompleteCommand extends Command {
constructor() {
super('complete');
this.description('Complete the current TDD phase with result validation')
.option(
'-r, --results <json>',
'Test results JSON (with total, passed, failed, skipped)'
)
.option('-c, --coverage <percent>', 'Coverage percentage')
.action(async (options: CompleteOptions) => {
await this.execute(options);
});
}
private async execute(options: CompleteOptions): Promise<void> {
// Inherit parent options
const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
const mergedOptions: CompleteOptions = {
...parentOpts,
...options,
projectRoot:
options.projectRoot || parentOpts?.projectRoot || process.cwd()
};
const formatter = new OutputFormatter(mergedOptions.json || false);
try {
// Check for workflow state
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
if (!hasState) {
formatter.error('No active workflow', {
suggestion: 'Start a workflow with: autopilot start <taskId>'
});
process.exit(1);
}
// Load state
const state = await loadWorkflowState(mergedOptions.projectRoot!);
if (!state) {
formatter.error('Failed to load workflow state');
process.exit(1);
}
// Restore orchestrator with persistence
const { saveWorkflowState } = await import('./shared.js');
const orchestrator = new WorkflowOrchestrator(state.context);
orchestrator.restoreState(state);
orchestrator.enableAutoPersist(async (newState) => {
await saveWorkflowState(mergedOptions.projectRoot!, newState);
});
// Get current phase
const tddPhase = orchestrator.getCurrentTDDPhase();
const currentSubtask = orchestrator.getCurrentSubtask();
if (!tddPhase) {
formatter.error('Not in a TDD phase', {
phase: orchestrator.getCurrentPhase()
});
process.exit(1);
}
// Validate based on phase
if (tddPhase === 'RED' || tddPhase === 'GREEN') {
if (!mergedOptions.results) {
formatter.error('Test results required for RED/GREEN phase', {
usage:
'--results \'{"total":10,"passed":9,"failed":1,"skipped":0}\''
});
process.exit(1);
}
// Parse test results
let testResults: TestResult;
try {
const parsed = JSON.parse(mergedOptions.results);
testResults = {
total: parsed.total || 0,
passed: parsed.passed || 0,
failed: parsed.failed || 0,
skipped: parsed.skipped || 0,
phase: tddPhase
};
} catch (error) {
formatter.error('Invalid test results JSON', {
error: (error as Error).message
});
process.exit(1);
}
// Validate RED phase requirements
if (tddPhase === 'RED' && testResults.failed === 0) {
formatter.error('RED phase validation failed', {
reason: 'At least one test must be failing',
actual: {
passed: testResults.passed,
failed: testResults.failed
}
});
process.exit(1);
}
// Validate GREEN phase requirements
if (tddPhase === 'GREEN' && testResults.failed !== 0) {
formatter.error('GREEN phase validation failed', {
reason: 'All tests must pass',
actual: {
passed: testResults.passed,
failed: testResults.failed
}
});
process.exit(1);
}
// Complete phase with test results
if (tddPhase === 'RED') {
orchestrator.transition({
type: 'RED_PHASE_COMPLETE',
testResults
});
formatter.success('RED phase completed', {
nextPhase: 'GREEN',
testResults,
subtask: currentSubtask?.title
});
} else {
orchestrator.transition({
type: 'GREEN_PHASE_COMPLETE',
testResults
});
formatter.success('GREEN phase completed', {
nextPhase: 'COMMIT',
testResults,
subtask: currentSubtask?.title,
suggestion: 'Run: autopilot commit'
});
}
} else if (tddPhase === 'COMMIT') {
formatter.error('Use "autopilot commit" to complete COMMIT phase');
process.exit(1);
}
} catch (error) {
formatter.error((error as Error).message);
if (mergedOptions.verbose) {
console.error((error as Error).stack);
}
process.exit(1);
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* @fileoverview Autopilot CLI Commands for AI Agent Orchestration
* Provides subcommands for starting, resuming, and advancing the TDD workflow
* with JSON output for machine parsing.
*/
import { Command } from 'commander';
import { StartCommand } from './start.command.js';
import { ResumeCommand } from './resume.command.js';
import { NextCommand } from './next.command.js';
import { CompleteCommand } from './complete.command.js';
import { CommitCommand } from './commit.command.js';
import { StatusCommand } from './status.command.js';
import { AbortCommand } from './abort.command.js';
/**
* Shared command options for all autopilot commands
*/
export interface AutopilotBaseOptions {
json?: boolean;
verbose?: boolean;
projectRoot?: string;
}
/**
* AutopilotCommand with subcommands for TDD workflow orchestration
*/
export class AutopilotCommand extends Command {
constructor() {
super('autopilot');
// Configure main command
this.description('AI agent orchestration for TDD workflow execution')
.alias('ap')
// Global options for all subcommands
.option('--json', 'Output in JSON format for machine parsing')
.option('-v, --verbose', 'Enable verbose output')
.option(
'-p, --project-root <path>',
'Project root directory',
process.cwd()
);
// Register subcommands
this.registerSubcommands();
}
/**
* Register all autopilot subcommands
*/
private registerSubcommands(): void {
// Start new TDD workflow
this.addCommand(new StartCommand());
// Resume existing workflow
this.addCommand(new ResumeCommand());
// Get next action
this.addCommand(new NextCommand());
// Complete current phase
this.addCommand(new CompleteCommand());
// Create commit
this.addCommand(new CommitCommand());
// Show status
this.addCommand(new StatusCommand());
// Abort workflow
this.addCommand(new AbortCommand());
}
/**
* Register this command on an existing program
*/
static register(program: Command): AutopilotCommand {
const autopilotCommand = new AutopilotCommand();
program.addCommand(autopilotCommand);
return autopilotCommand;
}
}

View File

@@ -0,0 +1,164 @@
/**
* @fileoverview Next Command - Get next action in TDD workflow
*/
import { Command } from 'commander';
import { WorkflowOrchestrator } from '@tm/core';
import {
AutopilotBaseOptions,
hasWorkflowState,
loadWorkflowState,
OutputFormatter
} from './shared.js';
type NextOptions = AutopilotBaseOptions;
/**
* Next Command - Get next action details
*/
export class NextCommand extends Command {
constructor() {
super('next');
this.description(
'Get the next action to perform in the TDD workflow'
).action(async (options: NextOptions) => {
await this.execute(options);
});
}
private async execute(options: NextOptions): Promise<void> {
// Inherit parent options
const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
const mergedOptions: NextOptions = {
...parentOpts,
...options,
projectRoot:
options.projectRoot || parentOpts?.projectRoot || process.cwd()
};
const formatter = new OutputFormatter(mergedOptions.json || false);
try {
// Check for workflow state
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
if (!hasState) {
formatter.error('No active workflow', {
suggestion: 'Start a workflow with: autopilot start <taskId>'
});
process.exit(1);
}
// Load state
const state = await loadWorkflowState(mergedOptions.projectRoot!);
if (!state) {
formatter.error('Failed to load workflow state');
process.exit(1);
}
// Restore orchestrator
const orchestrator = new WorkflowOrchestrator(state.context);
orchestrator.restoreState(state);
// Get current phase and subtask
const phase = orchestrator.getCurrentPhase();
const tddPhase = orchestrator.getCurrentTDDPhase();
const currentSubtask = orchestrator.getCurrentSubtask();
// Determine next action based on phase
let actionType: string;
let actionDescription: string;
let actionDetails: Record<string, unknown> = {};
if (phase === 'COMPLETE') {
formatter.success('Workflow complete', {
message: 'All subtasks have been completed',
taskId: state.context.taskId
});
return;
}
if (phase === 'SUBTASK_LOOP' && tddPhase) {
switch (tddPhase) {
case 'RED':
actionType = 'generate_test';
actionDescription = 'Write failing test for current subtask';
actionDetails = {
subtask: currentSubtask
? {
id: currentSubtask.id,
title: currentSubtask.title,
attempts: currentSubtask.attempts
}
: null,
testCommand: 'npm test', // Could be customized based on config
expectedOutcome: 'Test should fail'
};
break;
case 'GREEN':
actionType = 'implement_code';
actionDescription = 'Implement code to pass the failing test';
actionDetails = {
subtask: currentSubtask
? {
id: currentSubtask.id,
title: currentSubtask.title,
attempts: currentSubtask.attempts
}
: null,
testCommand: 'npm test',
expectedOutcome: 'All tests should pass',
lastTestResults: state.context.lastTestResults
};
break;
case 'COMMIT':
actionType = 'commit_changes';
actionDescription = 'Commit the changes';
actionDetails = {
subtask: currentSubtask
? {
id: currentSubtask.id,
title: currentSubtask.title,
attempts: currentSubtask.attempts
}
: null,
suggestion: 'Use: autopilot commit'
};
break;
default:
actionType = 'unknown';
actionDescription = 'Unknown TDD phase';
}
} else {
actionType = 'workflow_phase';
actionDescription = `Currently in ${phase} phase`;
}
// Output next action
const output = {
action: actionType,
description: actionDescription,
phase,
tddPhase,
taskId: state.context.taskId,
branchName: state.context.branchName,
...actionDetails
};
if (mergedOptions.json) {
formatter.output(output);
} else {
formatter.success('Next action', output);
}
} catch (error) {
formatter.error((error as Error).message);
if (mergedOptions.verbose) {
console.error((error as Error).stack);
}
process.exit(1);
}
}
}

View File

@@ -0,0 +1,111 @@
/**
* @fileoverview Resume Command - Restore and resume TDD workflow
*/
import { Command } from 'commander';
import { WorkflowOrchestrator } from '@tm/core';
import {
AutopilotBaseOptions,
hasWorkflowState,
loadWorkflowState,
OutputFormatter
} from './shared.js';
type ResumeOptions = AutopilotBaseOptions;
/**
* Resume Command - Restore workflow from saved state
*/
export class ResumeCommand extends Command {
constructor() {
super('resume');
this.description('Resume a previously started TDD workflow').action(
async (options: ResumeOptions) => {
await this.execute(options);
}
);
}
private async execute(options: ResumeOptions): Promise<void> {
// Inherit parent options (autopilot command)
const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
const mergedOptions: ResumeOptions = {
...parentOpts,
...options,
projectRoot:
options.projectRoot || parentOpts?.projectRoot || process.cwd()
};
const formatter = new OutputFormatter(mergedOptions.json || false);
try {
// Check for workflow state
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
if (!hasState) {
formatter.error('No workflow state found', {
suggestion: 'Start a new workflow with: autopilot start <taskId>'
});
process.exit(1);
}
// Load state
formatter.info('Loading workflow state...');
const state = await loadWorkflowState(mergedOptions.projectRoot!);
if (!state) {
formatter.error('Failed to load workflow state');
process.exit(1);
}
// Validate state can be resumed
const orchestrator = new WorkflowOrchestrator(state.context);
if (!orchestrator.canResumeFromState(state)) {
formatter.error('Invalid workflow state', {
suggestion:
'State file may be corrupted. Consider starting a new workflow.'
});
process.exit(1);
}
// Restore state
orchestrator.restoreState(state);
// Re-enable auto-persistence
const { saveWorkflowState } = await import('./shared.js');
orchestrator.enableAutoPersist(async (newState) => {
await saveWorkflowState(mergedOptions.projectRoot!, newState);
});
// Get progress
const progress = orchestrator.getProgress();
const currentSubtask = orchestrator.getCurrentSubtask();
// Output success
formatter.success('Workflow resumed', {
taskId: state.context.taskId,
phase: orchestrator.getCurrentPhase(),
tddPhase: orchestrator.getCurrentTDDPhase(),
branchName: state.context.branchName,
progress: {
completed: progress.completed,
total: progress.total,
percentage: progress.percentage
},
currentSubtask: currentSubtask
? {
id: currentSubtask.id,
title: currentSubtask.title,
attempts: currentSubtask.attempts
}
: null
});
} catch (error) {
formatter.error((error as Error).message);
if (mergedOptions.verbose) {
console.error((error as Error).stack);
}
process.exit(1);
}
}
}

View File

@@ -0,0 +1,262 @@
/**
* @fileoverview Shared utilities for autopilot commands
*/
import {
WorkflowOrchestrator,
WorkflowStateManager,
GitAdapter,
CommitMessageGenerator
} from '@tm/core';
import type { WorkflowState, WorkflowContext, SubtaskInfo } from '@tm/core';
import chalk from 'chalk';
/**
* Base options interface for all autopilot commands
*/
export interface AutopilotBaseOptions {
projectRoot?: string;
json?: boolean;
verbose?: boolean;
}
/**
* Load workflow state from disk using WorkflowStateManager
*/
export async function loadWorkflowState(
projectRoot: string
): Promise<WorkflowState | null> {
const stateManager = new WorkflowStateManager(projectRoot);
if (!(await stateManager.exists())) {
return null;
}
try {
return await stateManager.load();
} catch (error) {
throw new Error(
`Failed to load workflow state: ${(error as Error).message}`
);
}
}
/**
* Save workflow state to disk using WorkflowStateManager
*/
export async function saveWorkflowState(
projectRoot: string,
state: WorkflowState
): Promise<void> {
const stateManager = new WorkflowStateManager(projectRoot);
try {
await stateManager.save(state);
} catch (error) {
throw new Error(
`Failed to save workflow state: ${(error as Error).message}`
);
}
}
/**
* Delete workflow state from disk using WorkflowStateManager
*/
export async function deleteWorkflowState(projectRoot: string): Promise<void> {
const stateManager = new WorkflowStateManager(projectRoot);
await stateManager.delete();
}
/**
* Check if workflow state exists using WorkflowStateManager
*/
export async function hasWorkflowState(projectRoot: string): Promise<boolean> {
const stateManager = new WorkflowStateManager(projectRoot);
return await stateManager.exists();
}
/**
* Initialize WorkflowOrchestrator with persistence
*/
export function createOrchestrator(
context: WorkflowContext,
projectRoot: string
): WorkflowOrchestrator {
const orchestrator = new WorkflowOrchestrator(context);
const stateManager = new WorkflowStateManager(projectRoot);
// Enable auto-persistence
orchestrator.enableAutoPersist(async (state: WorkflowState) => {
await stateManager.save(state);
});
return orchestrator;
}
/**
* Initialize GitAdapter for project
*/
export function createGitAdapter(projectRoot: string): GitAdapter {
return new GitAdapter(projectRoot);
}
/**
* Initialize CommitMessageGenerator
*/
export function createCommitMessageGenerator(): CommitMessageGenerator {
return new CommitMessageGenerator();
}
/**
* Output formatter for JSON and text modes
*/
export class OutputFormatter {
constructor(private useJson: boolean) {}
/**
* Output data in appropriate format
*/
output(data: Record<string, unknown>): void {
if (this.useJson) {
console.log(JSON.stringify(data, null, 2));
} else {
this.outputText(data);
}
}
/**
* Output data in human-readable text format
*/
private outputText(data: Record<string, unknown>): void {
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'object' && value !== null) {
console.log(chalk.cyan(`${key}:`));
this.outputObject(value as Record<string, unknown>, ' ');
} else {
console.log(chalk.white(`${key}: ${value}`));
}
}
}
/**
* Output nested object with indentation
*/
private outputObject(obj: Record<string, unknown>, indent: string): void {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
console.log(chalk.cyan(`${indent}${key}:`));
this.outputObject(value as Record<string, unknown>, indent + ' ');
} else {
console.log(chalk.gray(`${indent}${key}: ${value}`));
}
}
}
/**
* Output error message
*/
error(message: string, details?: Record<string, unknown>): void {
if (this.useJson) {
console.error(
JSON.stringify(
{
error: message,
...details
},
null,
2
)
);
} else {
console.error(chalk.red(`Error: ${message}`));
if (details) {
for (const [key, value] of Object.entries(details)) {
console.error(chalk.gray(` ${key}: ${value}`));
}
}
}
}
/**
* Output success message
*/
success(message: string, data?: Record<string, unknown>): void {
if (this.useJson) {
console.log(
JSON.stringify(
{
success: true,
message,
...data
},
null,
2
)
);
} else {
console.log(chalk.green(`${message}`));
if (data) {
this.output(data);
}
}
}
/**
* Output warning message
*/
warning(message: string): void {
if (this.useJson) {
console.warn(
JSON.stringify(
{
warning: message
},
null,
2
)
);
} else {
console.warn(chalk.yellow(`${message}`));
}
}
/**
* Output info message
*/
info(message: string): void {
if (this.useJson) {
// Don't output info messages in JSON mode
return;
}
console.log(chalk.blue(` ${message}`));
}
}
/**
* Validate task ID format
*/
export function validateTaskId(taskId: string): boolean {
// Task ID should be in format: number or number.number (e.g., "1" or "1.2")
const pattern = /^\d+(\.\d+)*$/;
return pattern.test(taskId);
}
/**
* Parse subtasks from task data
*/
export function parseSubtasks(
task: any,
maxAttempts: number = 3
): SubtaskInfo[] {
if (!task.subtasks || !Array.isArray(task.subtasks)) {
return [];
}
return task.subtasks.map((subtask: any) => ({
id: subtask.id,
title: subtask.title,
status: subtask.status === 'done' ? 'completed' : 'pending',
attempts: 0,
maxAttempts
}));
}

View File

@@ -0,0 +1,168 @@
/**
* @fileoverview Start Command - Initialize and start TDD workflow
*/
import { Command } from 'commander';
import { createTaskMasterCore, type WorkflowContext } from '@tm/core';
import {
AutopilotBaseOptions,
hasWorkflowState,
createOrchestrator,
createGitAdapter,
OutputFormatter,
validateTaskId,
parseSubtasks
} from './shared.js';
interface StartOptions extends AutopilotBaseOptions {
force?: boolean;
maxAttempts?: string;
}
/**
* Start Command - Initialize new TDD workflow
*/
export class StartCommand extends Command {
constructor() {
super('start');
this.description('Initialize and start a new TDD workflow for a task')
.argument('<taskId>', 'Task ID to start workflow for')
.option('-f, --force', 'Force start even if workflow state exists')
.option('--max-attempts <number>', 'Maximum attempts per subtask', '3')
.action(async (taskId: string, options: StartOptions) => {
await this.execute(taskId, options);
});
}
private async execute(taskId: string, options: StartOptions): Promise<void> {
// Inherit parent options
const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
const mergedOptions: StartOptions = {
...parentOpts,
...options,
projectRoot:
options.projectRoot || parentOpts?.projectRoot || process.cwd()
};
const formatter = new OutputFormatter(mergedOptions.json || false);
try {
// Validate task ID
if (!validateTaskId(taskId)) {
formatter.error('Invalid task ID format', {
taskId,
expected: 'Format: number or number.number (e.g., "1" or "1.2")'
});
process.exit(1);
}
// Check for existing workflow state
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
if (hasState && !mergedOptions.force) {
formatter.error(
'Workflow state already exists. Use --force to overwrite or resume with "autopilot resume"'
);
process.exit(1);
}
// Initialize Task Master Core
const tmCore = await createTaskMasterCore({
projectPath: mergedOptions.projectRoot!
});
// Get current tag from ConfigManager
const currentTag = tmCore.getActiveTag();
// Load task
formatter.info(`Loading task ${taskId}...`);
const { task } = await tmCore.getTaskWithSubtask(taskId);
if (!task) {
formatter.error('Task not found', { taskId });
await tmCore.close();
process.exit(1);
}
// Validate task has subtasks
if (!task.subtasks || task.subtasks.length === 0) {
formatter.error('Task has no subtasks. Expand task first.', {
taskId,
suggestion: `Run: task-master expand --id=${taskId}`
});
await tmCore.close();
process.exit(1);
}
// Initialize Git adapter
const gitAdapter = createGitAdapter(mergedOptions.projectRoot!);
await gitAdapter.ensureGitRepository();
await gitAdapter.ensureCleanWorkingTree();
// Parse subtasks
const maxAttempts = parseInt(mergedOptions.maxAttempts || '3', 10);
const subtasks = parseSubtasks(task, maxAttempts);
// Create workflow context
const context: WorkflowContext = {
taskId: task.id,
subtasks,
currentSubtaskIndex: 0,
errors: [],
metadata: {
startedAt: new Date().toISOString(),
tags: task.tags || []
}
};
// Create orchestrator with persistence
const orchestrator = createOrchestrator(
context,
mergedOptions.projectRoot!
);
// Complete PREFLIGHT phase
orchestrator.transition({ type: 'PREFLIGHT_COMPLETE' });
// Generate descriptive branch name
const sanitizedTitle = task.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 50);
const formattedTaskId = taskId.replace(/\./g, '-');
const tagPrefix = currentTag ? `${currentTag}/` : '';
const branchName = `${tagPrefix}task-${formattedTaskId}-${sanitizedTitle}`;
// Create and checkout branch
formatter.info(`Creating branch: ${branchName}`);
await gitAdapter.createAndCheckoutBranch(branchName);
// Transition to SUBTASK_LOOP
orchestrator.transition({
type: 'BRANCH_CREATED',
branchName
});
// Output success
formatter.success('TDD workflow started', {
taskId: task.id,
title: task.title,
phase: orchestrator.getCurrentPhase(),
tddPhase: orchestrator.getCurrentTDDPhase(),
branchName,
subtasks: subtasks.length,
currentSubtask: subtasks[0]?.title
});
// Clean up
await tmCore.close();
} catch (error) {
formatter.error((error as Error).message);
if (mergedOptions.verbose) {
console.error((error as Error).stack);
}
process.exit(1);
}
}
}

View File

@@ -0,0 +1,114 @@
/**
* @fileoverview Status Command - Show workflow progress
*/
import { Command } from 'commander';
import { WorkflowOrchestrator } from '@tm/core';
import {
AutopilotBaseOptions,
hasWorkflowState,
loadWorkflowState,
OutputFormatter
} from './shared.js';
type StatusOptions = AutopilotBaseOptions;
/**
* Status Command - Show current workflow status
*/
export class StatusCommand extends Command {
constructor() {
super('status');
this.description('Show current TDD workflow status and progress').action(
async (options: StatusOptions) => {
await this.execute(options);
}
);
}
private async execute(options: StatusOptions): Promise<void> {
// Inherit parent options
const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
const mergedOptions: StatusOptions = {
...parentOpts,
...options,
projectRoot:
options.projectRoot || parentOpts?.projectRoot || process.cwd()
};
const formatter = new OutputFormatter(mergedOptions.json || false);
try {
// Check for workflow state
const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
if (!hasState) {
formatter.error('No active workflow', {
suggestion: 'Start a workflow with: autopilot start <taskId>'
});
process.exit(1);
}
// Load state
const state = await loadWorkflowState(mergedOptions.projectRoot!);
if (!state) {
formatter.error('Failed to load workflow state');
process.exit(1);
}
// Restore orchestrator
const orchestrator = new WorkflowOrchestrator(state.context);
orchestrator.restoreState(state);
// Get status information
const phase = orchestrator.getCurrentPhase();
const tddPhase = orchestrator.getCurrentTDDPhase();
const progress = orchestrator.getProgress();
const currentSubtask = orchestrator.getCurrentSubtask();
const errors = state.context.errors ?? [];
// Build status output
const status = {
taskId: state.context.taskId,
phase,
tddPhase,
branchName: state.context.branchName,
progress: {
completed: progress.completed,
total: progress.total,
current: progress.current,
percentage: progress.percentage
},
currentSubtask: currentSubtask
? {
id: currentSubtask.id,
title: currentSubtask.title,
status: currentSubtask.status,
attempts: currentSubtask.attempts,
maxAttempts: currentSubtask.maxAttempts
}
: null,
subtasks: state.context.subtasks.map((st) => ({
id: st.id,
title: st.title,
status: st.status,
attempts: st.attempts
})),
errors: errors.length > 0 ? errors : undefined,
metadata: state.context.metadata
};
if (mergedOptions.json) {
formatter.output(status);
} else {
formatter.success('Workflow status', status);
}
} catch (error) {
formatter.error((error as Error).message);
if (mergedOptions.verbose) {
console.error((error as Error).stack);
}
process.exit(1);
}
}
}

View File

@@ -0,0 +1,540 @@
/**
* @fileoverview Integration tests for autopilot workflow commands
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import type { WorkflowState } from '@tm/core';
// Track file system state in memory - must be in vi.hoisted() for mock access
const {
mockFileSystem,
pathExistsFn,
readJSONFn,
writeJSONFn,
ensureDirFn,
removeFn
} = vi.hoisted(() => {
const mockFileSystem = new Map<string, string>();
return {
mockFileSystem,
pathExistsFn: vi.fn((path: string) =>
Promise.resolve(mockFileSystem.has(path))
),
readJSONFn: vi.fn((path: string) => {
const data = mockFileSystem.get(path);
return data
? Promise.resolve(JSON.parse(data))
: Promise.reject(new Error('File not found'));
}),
writeJSONFn: vi.fn((path: string, data: any) => {
mockFileSystem.set(path, JSON.stringify(data));
return Promise.resolve();
}),
ensureDirFn: vi.fn(() => Promise.resolve()),
removeFn: vi.fn((path: string) => {
mockFileSystem.delete(path);
return Promise.resolve();
})
};
});
// Mock fs-extra before any imports
vi.mock('fs-extra', () => ({
default: {
pathExists: pathExistsFn,
readJSON: readJSONFn,
writeJSON: writeJSONFn,
ensureDir: ensureDirFn,
remove: removeFn
}
}));
vi.mock('@tm/core', () => ({
WorkflowOrchestrator: vi.fn().mockImplementation((context) => ({
getCurrentPhase: vi.fn().mockReturnValue('SUBTASK_LOOP'),
getCurrentTDDPhase: vi.fn().mockReturnValue('RED'),
getContext: vi.fn().mockReturnValue(context),
transition: vi.fn(),
restoreState: vi.fn(),
getState: vi.fn().mockReturnValue({ phase: 'SUBTASK_LOOP', context }),
enableAutoPersist: vi.fn(),
canResumeFromState: vi.fn().mockReturnValue(true),
getCurrentSubtask: vi.fn().mockReturnValue({
id: '1',
title: 'Test Subtask',
status: 'pending',
attempts: 0
}),
getProgress: vi.fn().mockReturnValue({
completed: 0,
total: 3,
current: 1,
percentage: 0
}),
canProceed: vi.fn().mockReturnValue(false)
})),
GitAdapter: vi.fn().mockImplementation(() => ({
ensureGitRepository: vi.fn().mockResolvedValue(undefined),
ensureCleanWorkingTree: vi.fn().mockResolvedValue(undefined),
createAndCheckoutBranch: vi.fn().mockResolvedValue(undefined),
hasStagedChanges: vi.fn().mockResolvedValue(true),
getStatus: vi.fn().mockResolvedValue({
staged: ['file1.ts'],
modified: ['file2.ts']
}),
createCommit: vi.fn().mockResolvedValue(undefined),
getLastCommit: vi.fn().mockResolvedValue({
hash: 'abc123def456',
message: 'test commit'
}),
stageFiles: vi.fn().mockResolvedValue(undefined)
})),
CommitMessageGenerator: vi.fn().mockImplementation(() => ({
generateMessage: vi.fn().mockReturnValue('feat: test commit message')
})),
createTaskMasterCore: vi.fn().mockResolvedValue({
getTaskWithSubtask: vi.fn().mockResolvedValue({
task: {
id: '1',
title: 'Test Task',
subtasks: [
{ id: '1', title: 'Subtask 1', status: 'pending' },
{ id: '2', title: 'Subtask 2', status: 'pending' },
{ id: '3', title: 'Subtask 3', status: 'pending' }
],
tag: 'test'
}
}),
close: vi.fn().mockResolvedValue(undefined)
})
}));
// Import after mocks are set up
import { Command } from 'commander';
import { AutopilotCommand } from '../../../../src/commands/autopilot/index.js';
describe('Autopilot Workflow Integration Tests', () => {
const projectRoot = '/test/project';
let program: Command;
beforeEach(() => {
mockFileSystem.clear();
// Clear mock call history
pathExistsFn.mockClear();
readJSONFn.mockClear();
writeJSONFn.mockClear();
ensureDirFn.mockClear();
removeFn.mockClear();
program = new Command();
AutopilotCommand.register(program);
// Use exitOverride to handle Commander exits in tests
program.exitOverride();
});
afterEach(() => {
mockFileSystem.clear();
vi.restoreAllMocks();
});
describe('start command', () => {
it('should initialize workflow and create branch', async () => {
const consoleLogSpy = vi
.spyOn(console, 'log')
.mockImplementation(() => {});
await program.parseAsync([
'node',
'test',
'autopilot',
'start',
'1',
'--project-root',
projectRoot,
'--json'
]);
// Verify writeJSON was called with state
expect(writeJSONFn).toHaveBeenCalledWith(
expect.stringContaining('workflow-state.json'),
expect.objectContaining({
phase: expect.any(String),
context: expect.any(Object)
}),
expect.any(Object)
);
consoleLogSpy.mockRestore();
});
it('should reject invalid task ID', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
await expect(
program.parseAsync([
'node',
'test',
'autopilot',
'start',
'invalid',
'--project-root',
projectRoot,
'--json'
])
).rejects.toMatchObject({ exitCode: 1 });
consoleErrorSpy.mockRestore();
});
it('should reject starting when workflow exists without force', async () => {
// Create existing state
const mockState: WorkflowState = {
phase: 'SUBTASK_LOOP',
context: {
taskId: '1',
subtasks: [],
currentSubtaskIndex: 0,
errors: [],
metadata: {}
}
};
mockFileSystem.set(
`${projectRoot}/.taskmaster/workflow-state.json`,
JSON.stringify(mockState)
);
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
await expect(
program.parseAsync([
'node',
'test',
'autopilot',
'start',
'1',
'--project-root',
projectRoot,
'--json'
])
).rejects.toMatchObject({ exitCode: 1 });
consoleErrorSpy.mockRestore();
});
});
describe('resume command', () => {
beforeEach(() => {
// Create saved state
const mockState: WorkflowState = {
phase: 'SUBTASK_LOOP',
context: {
taskId: '1',
subtasks: [
{
id: '1',
title: 'Test Subtask',
status: 'pending',
attempts: 0
}
],
currentSubtaskIndex: 0,
currentTDDPhase: 'RED',
branchName: 'task-1',
errors: [],
metadata: {}
}
};
mockFileSystem.set(
`${projectRoot}/.taskmaster/workflow-state.json`,
JSON.stringify(mockState)
);
});
it('should restore workflow from saved state', async () => {
const consoleLogSpy = vi
.spyOn(console, 'log')
.mockImplementation(() => {});
await program.parseAsync([
'node',
'test',
'autopilot',
'resume',
'--project-root',
projectRoot,
'--json'
]);
expect(consoleLogSpy).toHaveBeenCalled();
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
expect(output.success).toBe(true);
expect(output.taskId).toBe('1');
consoleLogSpy.mockRestore();
});
it('should error when no state exists', async () => {
mockFileSystem.clear();
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
await expect(
program.parseAsync([
'node',
'test',
'autopilot',
'resume',
'--project-root',
projectRoot,
'--json'
])
).rejects.toMatchObject({ exitCode: 1 });
consoleErrorSpy.mockRestore();
});
});
describe('next command', () => {
beforeEach(() => {
const mockState: WorkflowState = {
phase: 'SUBTASK_LOOP',
context: {
taskId: '1',
subtasks: [
{
id: '1',
title: 'Test Subtask',
status: 'pending',
attempts: 0
}
],
currentSubtaskIndex: 0,
currentTDDPhase: 'RED',
branchName: 'task-1',
errors: [],
metadata: {}
}
};
mockFileSystem.set(
`${projectRoot}/.taskmaster/workflow-state.json`,
JSON.stringify(mockState)
);
});
it('should return next action in JSON format', async () => {
const consoleLogSpy = vi
.spyOn(console, 'log')
.mockImplementation(() => {});
await program.parseAsync([
'node',
'test',
'autopilot',
'next',
'--project-root',
projectRoot,
'--json'
]);
expect(consoleLogSpy).toHaveBeenCalled();
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
expect(output.action).toBe('generate_test');
expect(output.phase).toBe('SUBTASK_LOOP');
expect(output.tddPhase).toBe('RED');
consoleLogSpy.mockRestore();
});
});
describe('status command', () => {
beforeEach(() => {
const mockState: WorkflowState = {
phase: 'SUBTASK_LOOP',
context: {
taskId: '1',
subtasks: [
{ id: '1', title: 'Subtask 1', status: 'completed', attempts: 1 },
{ id: '2', title: 'Subtask 2', status: 'pending', attempts: 0 },
{ id: '3', title: 'Subtask 3', status: 'pending', attempts: 0 }
],
currentSubtaskIndex: 1,
currentTDDPhase: 'RED',
branchName: 'task-1',
errors: [],
metadata: {}
}
};
mockFileSystem.set(
`${projectRoot}/.taskmaster/workflow-state.json`,
JSON.stringify(mockState)
);
});
it('should display workflow progress', async () => {
const consoleLogSpy = vi
.spyOn(console, 'log')
.mockImplementation(() => {});
await program.parseAsync([
'node',
'test',
'autopilot',
'status',
'--project-root',
projectRoot,
'--json'
]);
expect(consoleLogSpy).toHaveBeenCalled();
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
expect(output.taskId).toBe('1');
expect(output.phase).toBe('SUBTASK_LOOP');
expect(output.progress).toBeDefined();
expect(output.subtasks).toHaveLength(3);
consoleLogSpy.mockRestore();
});
});
describe('complete command', () => {
beforeEach(() => {
const mockState: WorkflowState = {
phase: 'SUBTASK_LOOP',
context: {
taskId: '1',
subtasks: [
{
id: '1',
title: 'Test Subtask',
status: 'in-progress',
attempts: 0
}
],
currentSubtaskIndex: 0,
currentTDDPhase: 'RED',
branchName: 'task-1',
errors: [],
metadata: {}
}
};
mockFileSystem.set(
`${projectRoot}/.taskmaster/workflow-state.json`,
JSON.stringify(mockState)
);
});
it('should validate RED phase has failures', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
await expect(
program.parseAsync([
'node',
'test',
'autopilot',
'complete',
'--project-root',
projectRoot,
'--results',
'{"total":10,"passed":10,"failed":0,"skipped":0}',
'--json'
])
).rejects.toMatchObject({ exitCode: 1 });
consoleErrorSpy.mockRestore();
});
it('should complete RED phase with failures', async () => {
const consoleLogSpy = vi
.spyOn(console, 'log')
.mockImplementation(() => {});
await program.parseAsync([
'node',
'test',
'autopilot',
'complete',
'--project-root',
projectRoot,
'--results',
'{"total":10,"passed":9,"failed":1,"skipped":0}',
'--json'
]);
expect(consoleLogSpy).toHaveBeenCalled();
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
expect(output.success).toBe(true);
expect(output.nextPhase).toBe('GREEN');
consoleLogSpy.mockRestore();
});
});
describe('abort command', () => {
beforeEach(() => {
const mockState: WorkflowState = {
phase: 'SUBTASK_LOOP',
context: {
taskId: '1',
subtasks: [
{
id: '1',
title: 'Test Subtask',
status: 'pending',
attempts: 0
}
],
currentSubtaskIndex: 0,
currentTDDPhase: 'RED',
branchName: 'task-1',
errors: [],
metadata: {}
}
};
mockFileSystem.set(
`${projectRoot}/.taskmaster/workflow-state.json`,
JSON.stringify(mockState)
);
});
it('should abort workflow and delete state', async () => {
const consoleLogSpy = vi
.spyOn(console, 'log')
.mockImplementation(() => {});
await program.parseAsync([
'node',
'test',
'autopilot',
'abort',
'--project-root',
projectRoot,
'--force',
'--json'
]);
// Verify remove was called
expect(removeFn).toHaveBeenCalledWith(
expect.stringContaining('workflow-state.json')
);
consoleLogSpy.mockRestore();
});
});
});

View File

@@ -0,0 +1,202 @@
/**
* @fileoverview Unit tests for autopilot shared utilities
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
validateTaskId,
parseSubtasks,
OutputFormatter
} from '../../../../src/commands/autopilot/shared.js';
// Mock fs-extra
vi.mock('fs-extra', () => ({
default: {
pathExists: vi.fn(),
readJSON: vi.fn(),
writeJSON: vi.fn(),
ensureDir: vi.fn(),
remove: vi.fn()
},
pathExists: vi.fn(),
readJSON: vi.fn(),
writeJSON: vi.fn(),
ensureDir: vi.fn(),
remove: vi.fn()
}));
describe('Autopilot Shared Utilities', () => {
const projectRoot = '/test/project';
const statePath = `${projectRoot}/.taskmaster/workflow-state.json`;
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('validateTaskId', () => {
it('should validate simple task IDs', () => {
expect(validateTaskId('1')).toBe(true);
expect(validateTaskId('10')).toBe(true);
expect(validateTaskId('999')).toBe(true);
});
it('should validate subtask IDs', () => {
expect(validateTaskId('1.1')).toBe(true);
expect(validateTaskId('1.2')).toBe(true);
expect(validateTaskId('10.5')).toBe(true);
});
it('should validate nested subtask IDs', () => {
expect(validateTaskId('1.1.1')).toBe(true);
expect(validateTaskId('1.2.3')).toBe(true);
});
it('should reject invalid formats', () => {
expect(validateTaskId('')).toBe(false);
expect(validateTaskId('abc')).toBe(false);
expect(validateTaskId('1.')).toBe(false);
expect(validateTaskId('.1')).toBe(false);
expect(validateTaskId('1..2')).toBe(false);
expect(validateTaskId('1.2.3.')).toBe(false);
});
});
describe('parseSubtasks', () => {
it('should parse subtasks from task data', () => {
const task = {
id: '1',
title: 'Test Task',
subtasks: [
{ id: '1', title: 'Subtask 1', status: 'pending' },
{ id: '2', title: 'Subtask 2', status: 'done' },
{ id: '3', title: 'Subtask 3', status: 'in-progress' }
]
};
const result = parseSubtasks(task, 5);
expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: '1',
title: 'Subtask 1',
status: 'pending',
attempts: 0,
maxAttempts: 5
});
expect(result[1]).toEqual({
id: '2',
title: 'Subtask 2',
status: 'completed',
attempts: 0,
maxAttempts: 5
});
});
it('should return empty array for missing subtasks', () => {
const task = { id: '1', title: 'Test Task' };
expect(parseSubtasks(task)).toEqual([]);
});
it('should use default maxAttempts', () => {
const task = {
subtasks: [{ id: '1', title: 'Subtask 1', status: 'pending' }]
};
const result = parseSubtasks(task);
expect(result[0].maxAttempts).toBe(3);
});
});
// State persistence tests omitted - covered in integration tests
describe('OutputFormatter', () => {
let consoleLogSpy: any;
let consoleErrorSpy: any;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
describe('JSON mode', () => {
it('should output JSON for success', () => {
const formatter = new OutputFormatter(true);
formatter.success('Test message', { key: 'value' });
expect(consoleLogSpy).toHaveBeenCalled();
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
expect(output.success).toBe(true);
expect(output.message).toBe('Test message');
expect(output.key).toBe('value');
});
it('should output JSON for error', () => {
const formatter = new OutputFormatter(true);
formatter.error('Error message', { code: 'ERR001' });
expect(consoleErrorSpy).toHaveBeenCalled();
const output = JSON.parse(consoleErrorSpy.mock.calls[0][0]);
expect(output.error).toBe('Error message');
expect(output.code).toBe('ERR001');
});
it('should output JSON for data', () => {
const formatter = new OutputFormatter(true);
formatter.output({ test: 'data' });
expect(consoleLogSpy).toHaveBeenCalled();
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
expect(output.test).toBe('data');
});
});
describe('Text mode', () => {
it('should output formatted text for success', () => {
const formatter = new OutputFormatter(false);
formatter.success('Test message');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('✓ Test message')
);
});
it('should output formatted text for error', () => {
const formatter = new OutputFormatter(false);
formatter.error('Error message');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Error: Error message')
);
});
it('should output formatted text for warning', () => {
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => {});
const formatter = new OutputFormatter(false);
formatter.warning('Warning message');
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('⚠ Warning message')
);
consoleWarnSpy.mockRestore();
});
it('should not output info in JSON mode', () => {
const formatter = new OutputFormatter(true);
formatter.info('Info message');
expect(consoleLogSpy).not.toHaveBeenCalled();
});
});
});
});

25
apps/cli/vitest.config.ts Normal file
View File

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

View File

@@ -52,6 +52,20 @@
"capabilities/cli-root-commands",
"capabilities/task-structure"
]
},
{
"group": "TDD Workflow (Autopilot)",
"pages": [
"tdd-workflow/quickstart",
"tdd-workflow/ai-agent-integration",
{
"group": "Templates & Examples",
"pages": [
"tdd-workflow/templates/claude-template",
"tdd-workflow/templates/example-prompts"
]
}
]
}
]
}

View File

@@ -0,0 +1,944 @@
---
title: "AI Agent Integration Guide"
description: "Complete guide for integrating AI agents with TaskMaster's autonomous TDD workflow system"
---
Complete guide for integrating AI agents with TaskMaster's autonomous TDD workflow system.
## Overview
TaskMaster provides a complete TDD workflow orchestration system that enables AI agents to autonomously implement features following strict Test-Driven Development practices. The system manages workflow state, git operations, test validation, and progress tracking.
### Key Features
- **TDD State Machine**: Enforces RED → GREEN → COMMIT cycle
- **Git Integration**: Automated branch creation, commits with metadata
- **Test Validation**: Ensures RED phase has failures, GREEN phase passes
- **Progress Tracking**: Subtask completion, attempt counting, error logging
- **State Persistence**: Automatic workflow state management
- **Dual Interface**: CLI commands and MCP tools for flexibility
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ AI Agent │
│ (Claude Code, Custom Agent, etc.) │
└─────────────┬───────────────────────────────────────┘
│ Uses CLI or MCP
┌─────────────▼───────────────────────────────────────┐
│ TaskMaster Interface │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ CLI Commands │ │ MCP Tools │ │
│ └──────┬───────┘ └──────┬───────┘ │
└─────────┼────────────────────────┼─────────────────┘
│ │
┌─────────▼────────────────────────▼─────────────────┐
│ WorkflowOrchestrator (Core) │
│ ┌─────────────────────────────────────────────┐ │
│ │ State Machine: RED → GREEN → COMMIT │ │
│ └─────────────────────────────────────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │GitAdapter│ │TestResult│ │CommitMessage │ │
│ │ │ │Validator │ │Generator │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└────────────────────────────────────────────────────┘
│ Persists to
┌─────────▼───────────────────────────────────────────┐
│ .taskmaster/workflow-state.json │
└──────────────────────────────────────────────────────┘
```
### Component Responsibilities
**WorkflowOrchestrator**
- Manages TDD state machine transitions
- Tracks current subtask and progress
- Enforces workflow rules and validations
- Emits events for state changes
**GitAdapter**
- Creates and manages workflow branches
- Stages files and creates commits
- Validates git repository state
- Provides safety checks (clean working tree, etc.)
**TestResultValidator**
- Validates RED phase has test failures
- Validates GREEN phase has all tests passing
- Parses test results from various formats
**CommitMessageGenerator**
- Generates conventional commit messages
- Embeds workflow metadata (subtask ID, phase, etc.)
- Follows project commit conventions
## Getting Started
### Prerequisites
1. TaskMaster initialized project with subtasks
2. Git repository with clean working tree
3. Test framework configured (vitest, jest, etc.)
### Quick Start
```bash
# 1. Initialize workflow for a task
tm autopilot start 7
# 2. Check what to do next
tm autopilot next
# 3. Write failing test (RED phase)
# ... create test file ...
# 4. Run tests and complete RED phase
tm autopilot complete --results '{"total":1,"passed":0,"failed":1,"skipped":0}'
# 5. Implement code to pass tests (GREEN phase)
# ... write implementation ...
# 6. Run tests and complete GREEN phase
tm autopilot complete --results '{"total":1,"passed":1,"failed":0,"skipped":0}'
# 7. Commit changes
tm autopilot commit
# 8. Repeat for next subtask (automatically advanced)
tm autopilot next
```
## CLI Commands
All commands support `--json` flag for machine-readable output.
### `tm autopilot start <taskId>`
Initialize a new TDD workflow for a task.
**Options:**
- `--max-attempts <number>`: Maximum attempts per subtask (default: 3)
- `--force`: Force start even if workflow exists
- `--project-root <path>`: Project root directory
- `--json`: Output JSON
**Example:**
```bash
tm autopilot start 7 --max-attempts 5 --json
```
**JSON Output:**
```json
{
"success": true,
"message": "Workflow started for task 7",
"taskId": "7",
"branchName": "task-7",
"phase": "SUBTASK_LOOP",
"tddPhase": "RED",
"progress": {
"completed": 0,
"total": 5,
"percentage": 0
},
"currentSubtask": {
"id": "1",
"title": "Implement start command",
"status": "in-progress",
"attempts": 0
},
"nextAction": "generate_test"
}
```
### `tm autopilot resume`
Resume a previously started workflow from saved state.
**Example:**
```bash
tm autopilot resume --json
```
### `tm autopilot next`
Get the next action to perform with detailed context.
**JSON Output:**
```json
{
"action": "generate_test",
"actionDescription": "Write a failing test for the current subtask",
"phase": "SUBTASK_LOOP",
"tddPhase": "RED",
"taskId": "7",
"branchName": "task-7",
"progress": {
"completed": 0,
"total": 5,
"current": 1,
"percentage": 0
},
"currentSubtask": {
"id": "1",
"title": "Implement start command",
"status": "in-progress",
"attempts": 0,
"maxAttempts": 3
},
"expectedFiles": ["test file"],
"context": {
"canProceed": false,
"errors": []
}
}
```
### `tm autopilot status`
Get comprehensive workflow progress and state information.
**Example:**
```bash
tm autopilot status --json
```
### `tm autopilot complete`
Complete the current TDD phase with test result validation.
**Options:**
- `--results <json>`: Test results JSON string
**Example:**
```bash
tm autopilot complete --results '{"total":10,"passed":9,"failed":1,"skipped":0}' --json
```
**Validation Rules:**
- **RED Phase**: Must have at least one failing test
- **GREEN Phase**: All tests must pass (failed === 0)
### `tm autopilot commit`
Create a git commit with enhanced message generation.
**Options:**
- `--message <text>`: Custom commit message (optional)
- `--files <paths...>`: Specific files to stage (optional)
**Example:**
```bash
tm autopilot commit --json
```
### `tm autopilot abort`
Abort the workflow and clean up state (preserves git branch and code).
**Example:**
```bash
tm autopilot abort --force --json
```
## MCP Tools
MCP tools provide the same functionality as CLI commands for programmatic integration.
### `autopilot_start`
**Parameters:**
```typescript
{
taskId: string; // Required: Task ID (e.g., "7", "2.3")
projectRoot: string; // Required: Absolute path to project
tag?: string; // Optional: Tag context
maxAttempts?: number; // Optional: Default 3
force?: boolean; // Optional: Default false
}
```
**Returns:**
```typescript
{
success: boolean;
message: string;
taskId: string;
branchName: string;
phase: WorkflowPhase;
tddPhase: TDDPhase;
progress: {
completed: number;
total: number;
percentage: number;
};
currentSubtask: SubtaskInfo | null;
nextAction: string;
}
```
### `autopilot_resume`
**Parameters:**
```typescript
{
projectRoot: string; // Required: Absolute path to project
}
```
### `autopilot_next`
**Parameters:**
```typescript
{
projectRoot: string; // Required: Absolute path to project
}
```
**Returns:**
```typescript
{
action: string; // 'generate_test' | 'implement_code' | 'commit_changes'
actionDescription: string;
phase: WorkflowPhase;
tddPhase: TDDPhase;
taskId: string;
branchName: string;
progress: ProgressInfo;
currentSubtask: SubtaskInfo | null;
expectedFiles: string[];
context: {
canProceed: boolean;
errors: string[];
};
}
```
### `autopilot_status`
**Parameters:**
```typescript
{
projectRoot: string; // Required: Absolute path to project
}
```
**Returns:**
```typescript
{
taskId: string;
branchName: string;
phase: WorkflowPhase;
tddPhase: TDDPhase;
progress: ProgressInfo;
currentSubtask: SubtaskInfo | null;
subtasks: SubtaskInfo[];
errors: string[];
metadata: Record<string, any>;
canProceed: boolean;
}
```
### `autopilot_complete_phase`
**Parameters:**
```typescript
{
projectRoot: string; // Required: Absolute path to project
testResults: {
total: number; // Required: Total tests
passed: number; // Required: Passing tests
failed: number; // Required: Failing tests
skipped?: number; // Optional: Skipped tests
};
}
```
### `autopilot_commit`
**Parameters:**
```typescript
{
projectRoot: string; // Required: Absolute path to project
files?: string[]; // Optional: Files to stage
customMessage?: string; // Optional: Custom commit message
}
```
### `autopilot_abort`
**Parameters:**
```typescript
{
projectRoot: string; // Required: Absolute path to project
}
```
## Workflow Phases
### Phase Diagram
```
PREFLIGHT → BRANCH_SETUP → SUBTASK_LOOP → FINALIZE → COMPLETE
RED → GREEN → COMMIT
↑ ↓
└──────────────┘
(Next Subtask)
```
### Phase Descriptions
**PREFLIGHT**
- Validate task has subtasks
- Check git repository state
- Verify preconditions
**BRANCH_SETUP**
- Create workflow branch: `task-{taskId}`
- Checkout new branch
- Initialize workflow context
**SUBTASK_LOOP**
- **RED Phase**: Write failing tests
- Action: `generate_test`
- Validation: At least one test must fail
- Files: Test files
- **GREEN Phase**: Implement code
- Action: `implement_code`
- Validation: All tests must pass
- Files: Implementation files
- **COMMIT Phase**: Create commit
- Action: `commit_changes`
- Auto-generates commit message
- Advances to next subtask
**FINALIZE**
- All subtasks complete
- Workflow ready for review/merge
**COMPLETE**
- Workflow finished
- State can be cleaned up
## Responsibility Matrix
Clear division of responsibilities between AI Agent and TaskMaster.
| Responsibility | AI Agent | TaskMaster |
|---------------|----------|------------|
| **Workflow Orchestration** | | ✓ |
| Start/resume workflow | Call CLI/MCP | Execute & validate |
| Track workflow state | Read state | Persist state |
| Manage TDD phases | Request transitions | Enforce transitions |
| Validate phase completion | | ✓ (RED must fail, GREEN must pass) |
| **Test Management** | | |
| Write test code | ✓ | |
| Run tests | ✓ | |
| Parse test output | ✓ | |
| Report test results | Provide JSON | Validate results |
| **Implementation** | | |
| Write implementation code | ✓ | |
| Ensure tests pass | ✓ | |
| Follow TDD cycle | ✓ (guided by TaskMaster) | Enforce rules |
| **Git Operations** | | |
| Create workflow branch | Request | ✓ Execute |
| Stage files | Request (optional) | ✓ Execute |
| Generate commit messages | | ✓ |
| Create commits | Request | ✓ Execute |
| **Progress Tracking** | | |
| Query progress | Call status | ✓ Provide data |
| Advance subtasks | | ✓ (automatic on commit) |
| Count attempts | | ✓ |
| Log activity | | ✓ |
### AI Agent Responsibilities
1. **Read and understand subtask requirements**
2. **Write test code** that validates the requirement
3. **Run test suite** using project's test command
4. **Parse test output** into JSON format
5. **Report results** to TaskMaster for validation
6. **Write implementation** to satisfy tests
7. **Request commits** when GREEN phase complete
8. **Handle errors** and retry within attempt limits
### TaskMaster Responsibilities
1. **Manage workflow state machine**
2. **Enforce TDD rules** (RED must fail, GREEN must pass)
3. **Track progress** (completed, current, attempts)
4. **Create git commits** with enhanced messages
5. **Manage git branches** and repository safety
6. **Validate transitions** between phases
7. **Persist state** for resumability
8. **Generate reports** and activity logs
## Examples
### Complete TDD Cycle Example
#### 1. Start Workflow
```bash
$ tm autopilot start 7 --json
```
```json
{
"success": true,
"taskId": "7",
"branchName": "task-7",
"tddPhase": "RED",
"currentSubtask": {
"id": "1",
"title": "Implement start command",
"status": "in-progress"
},
"nextAction": "generate_test"
}
```
#### 2. Get Next Action
```bash
$ tm autopilot next --json
```
```json
{
"action": "generate_test",
"actionDescription": "Write a failing test for the current subtask",
"tddPhase": "RED",
"currentSubtask": {
"id": "1",
"title": "Implement start command"
}
}
```
#### 3. Write Failing Test
AI Agent creates `tests/start.test.ts`:
```typescript
import { describe, it, expect } from 'vitest';
import { StartCommand } from '../src/commands/start.js';
describe('StartCommand', () => {
it('should initialize workflow and create branch', async () => {
const command = new StartCommand();
const result = await command.execute({ taskId: '7' });
expect(result.success).toBe(true);
expect(result.branchName).toBe('task-7');
});
});
```
#### 4. Run Tests (Should Fail)
```bash
$ npm test
# Output: 1 test failed (expected)
```
Parse output to JSON:
```json
{
"total": 1,
"passed": 0,
"failed": 1,
"skipped": 0
}
```
#### 5. Complete RED Phase
```bash
$ tm autopilot complete --results '{"total":1,"passed":0,"failed":1,"skipped":0}' --json
```
```json
{
"success": true,
"message": "Completed RED phase",
"previousPhase": "RED",
"currentPhase": "GREEN",
"nextAction": "implement_code"
}
```
#### 6. Implement Code
AI Agent creates `src/commands/start.ts`:
```typescript
export class StartCommand {
async execute(options: { taskId: string }) {
// Implementation that makes test pass
return {
success: true,
branchName: `task-${options.taskId}`
};
}
}
```
#### 7. Run Tests (Should Pass)
```bash
$ npm test
# Output: 1 test passed
```
Parse output:
```json
{
"total": 1,
"passed": 1,
"failed": 0,
"skipped": 0
}
```
#### 8. Complete GREEN Phase
```bash
$ tm autopilot complete --results '{"total":1,"passed":1,"failed":0,"skipped":0}' --json
```
```json
{
"success": true,
"previousPhase": "GREEN",
"currentPhase": "COMMIT",
"nextAction": "commit_changes"
}
```
#### 9. Commit Changes
```bash
$ tm autopilot commit --json
```
```json
{
"success": true,
"commit": {
"hash": "abc123",
"message": "feat(autopilot): implement start command (Task 7.1)\n\n..."
},
"subtaskCompleted": "1",
"currentSubtask": {
"id": "2",
"title": "Implement resume command"
},
"nextAction": "generate_test"
}
```
### MCP Integration Example
```typescript
// AI Agent using MCP tools
async function implementTask(taskId: string) {
// Start workflow
const start = await mcp.call('autopilot_start', {
taskId,
projectRoot: '/path/to/project'
});
console.log(`Started task ${start.taskId} on branch ${start.branchName}`);
while (true) {
// Get next action
const next = await mcp.call('autopilot_next', {
projectRoot: '/path/to/project'
});
console.log(`Next action: ${next.action}`);
if (next.action === 'generate_test') {
// AI generates test
const testCode = await generateTest(next.currentSubtask);
await writeFile(testCode);
// Run tests
const results = await runTests();
// Complete RED phase
await mcp.call('autopilot_complete_phase', {
projectRoot: '/path/to/project',
testResults: results
});
} else if (next.action === 'implement_code') {
// AI generates implementation
const implCode = await generateImplementation(next.currentSubtask);
await writeFile(implCode);
// Run tests
const results = await runTests();
// Complete GREEN phase
await mcp.call('autopilot_complete_phase', {
projectRoot: '/path/to/project',
testResults: results
});
} else if (next.action === 'commit_changes') {
// Commit
const commit = await mcp.call('autopilot_commit', {
projectRoot: '/path/to/project'
});
console.log(`Committed: ${commit.commit.hash}`);
if (commit.isComplete) {
console.log('Task complete!');
break;
}
}
}
}
```
## Error Handling
### Common Errors and Solutions
#### Workflow Already Exists
**Error:**
```json
{
"error": "Workflow already in progress",
"suggestion": "Use autopilot_resume to continue the existing workflow"
}
```
**Solution:**
```bash
# Resume existing workflow
tm autopilot resume
# OR force start new workflow
tm autopilot start 7 --force
```
#### RED Phase Validation Failed
**Error:**
```json
{
"error": "RED phase validation failed",
"reason": "At least one test must be failing in RED phase",
"actual": { "passed": 10, "failed": 0 },
"suggestion": "Ensure you have written a failing test before proceeding"
}
```
**Solution:** The test isn't actually testing the new feature. Write a test that validates the new behavior that doesn't exist yet.
#### GREEN Phase Validation Failed
**Error:**
```json
{
"error": "GREEN phase validation failed",
"reason": "All tests must pass in GREEN phase",
"actual": { "passed": 9, "failed": 1 },
"suggestion": "Fix the implementation to make all tests pass"
}
```
**Solution:** Implementation isn't complete. Debug failing test and fix implementation.
#### No Staged Changes
**Error:**
```json
{
"error": "No staged changes to commit",
"suggestion": "Make code changes before committing"
}
```
**Solution:** Ensure you've actually created/modified files before committing.
#### Git Working Tree Not Clean
**Error:**
```json
{
"error": "Git validation failed: working tree not clean",
"suggestion": "Commit or stash changes before starting workflow"
}
```
**Solution:**
```bash
git status
git add . && git commit -m "chore: save work"
# Then start workflow
```
### Error Recovery Patterns
#### Retry Pattern
```typescript
async function withRetry<T>(
fn: () => Promise<T>,
maxAttempts: number = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log(`Attempt ${attempt} failed, retrying...`);
await sleep(1000 * attempt); // Exponential backoff
}
}
throw new Error('Should not reach here');
}
// Usage
const results = await withRetry(async () => {
const output = await runTests();
return parseTestResults(output);
});
```
#### Graceful Degradation
```typescript
async function completePhase(projectRoot: string, results: TestResults) {
try {
return await mcp.call('autopilot_complete_phase', {
projectRoot,
testResults: results
});
} catch (error) {
console.error('Phase completion failed:', error);
// Log error for debugging
await logError(error);
// Attempt manual recovery
console.log('Attempting manual state recovery...');
const status = await mcp.call('autopilot_status', { projectRoot });
console.log('Current state:', status);
// Provide user guidance
console.log('Manual intervention required:');
console.log('1. Check test results are correct');
console.log('2. Verify current phase allows transition');
console.log('3. Run: tm autopilot status');
throw error;
}
}
```
## Troubleshooting
### Workflow State Issues
**Problem:** State file corrupted or inconsistent
**Solution:**
```bash
# Check state file
cat .taskmaster/workflow-state.json
# If corrupted, abort and restart
tm autopilot abort --force
tm autopilot start 7
```
### Test Results Parsing
**Problem:** Test output format not recognized
**Solution:**
Ensure test results JSON has required fields:
```json
{
"total": 10, // Required
"passed": 8, // Required
"failed": 2, // Required
"skipped": 0 // Optional
}
```
### Branch Conflicts
**Problem:** Workflow branch already exists
**Solution:**
```bash
# Check branches
git branch
# Delete old workflow branch if safe
git branch -D task-7
# Start workflow again
tm autopilot start 7
```
### Permission Issues
**Problem:** Cannot write to .taskmaster directory
**Solution:**
```bash
# Check directory permissions
ls -la .taskmaster/
# Fix permissions
chmod -R u+w .taskmaster/
```
### State Persistence Failures
**Problem:** State not saving between commands
**Solution:**
```bash
# Check file system permissions
ls -la .taskmaster/workflow-state.json
# Verify state is being written
tm autopilot status --json | jq .
# If all else fails, reinstall
rm -rf .taskmaster/
tm init
```
---
## Additional Resources
- [Command Reference](./command-reference.mdx) - Complete CLI command documentation
- [MCP Provider Guide](./mcp-provider-guide.mdx) - MCP integration details
- [Task Structure](./task-structure.mdx) - Understanding TaskMaster's task system
- [Configuration](./configuration.mdx) - Project configuration options
## Support
For issues, questions, or contributions:
- GitHub Issues: https://github.com/eyaltoledano/claude-task-master/issues
- Documentation: https://docs.task-master.dev

View File

@@ -0,0 +1,315 @@
---
title: "TDD Workflow Quick Start"
description: "Get started with TaskMaster's autonomous TDD workflow in 5 minutes"
---
Get started with TaskMaster's autonomous TDD workflow in 5 minutes.
## Prerequisites
- TaskMaster initialized project (`tm init`)
- Tasks with subtasks created (`tm parse-prd` or `tm expand`)
- Git repository with clean working tree
- Test framework installed (vitest, jest, mocha, etc.)
## 1. Start a Workflow
```bash
tm autopilot start <taskId>
```
Example:
```bash
$ tm autopilot start 7
✓ Workflow started for task 7
✓ Created branch: task-7
✓ Current phase: RED
✓ Subtask 1/5: Implement start command
→ Next action: Write a failing test
```
## 2. The TDD Cycle
### RED Phase: Write Failing Test
```bash
# Check what to do next
$ tm autopilot next --json
{
"action": "generate_test",
"currentSubtask": {
"id": "1",
"title": "Implement start command"
}
}
```
Write a test that fails:
```typescript
// tests/start.test.ts
import { describe, it, expect } from 'vitest';
import { StartCommand } from '../src/commands/start';
describe('StartCommand', () => {
it('should initialize workflow', async () => {
const command = new StartCommand();
const result = await command.execute({ taskId: '7' });
expect(result.success).toBe(true);
});
});
```
Run tests:
```bash
$ npm test
# ✗ 1 test failed
```
Complete RED phase:
```bash
$ tm autopilot complete --results '{"total":1,"passed":0,"failed":1,"skipped":0}'
✓ RED phase complete
✓ Current phase: GREEN
→ Next action: Implement code to pass tests
```
### GREEN Phase: Implement Feature
Write minimal code to pass:
```typescript
// src/commands/start.ts
export class StartCommand {
async execute(options: { taskId: string }) {
return { success: true };
}
}
```
Run tests:
```bash
$ npm test
# ✓ 1 test passed
```
Complete GREEN phase:
```bash
$ tm autopilot complete --results '{"total":1,"passed":1,"failed":0,"skipped":0}'
✓ GREEN phase complete
✓ Current phase: COMMIT
→ Next action: Commit changes
```
### COMMIT Phase: Save Progress
```bash
$ tm autopilot commit
✓ Created commit: abc123
✓ Message: feat(autopilot): implement start command (Task 7.1)
✓ Advanced to subtask 2/5
✓ Current phase: RED
→ Next action: Write a failing test
```
## 3. Continue for All Subtasks
Repeat the RED-GREEN-COMMIT cycle for each subtask until complete.
```bash
# Check progress anytime
$ tm autopilot status --json
{
"taskId": "7",
"progress": {
"completed": 1,
"total": 5,
"percentage": 20
},
"currentSubtask": {
"id": "2",
"title": "Implement resume command"
}
}
```
## 4. Complete the Workflow
When all subtasks are done:
```bash
$ tm autopilot status --json
{
"phase": "COMPLETE",
"progress": {
"completed": 5,
"total": 5,
"percentage": 100
}
}
```
Your branch `task-7` is ready for review/merge!
## Common Patterns
### Parse Test Output
Your test runner outputs human-readable format - convert to JSON:
**Vitest:**
```
Tests 2 failed | 8 passed | 10 total
```
→ `{"total":10,"passed":8,"failed":2,"skipped":0}`
**Jest:**
```
Tests: 2 failed, 8 passed, 10 total
```
→ `{"total":10,"passed":8,"failed":2,"skipped":0}`
### Handle Errors
**Problem:** RED phase won't complete - "no test failures"
**Solution:** Your test isn't testing new behavior. Make sure it fails:
```typescript
// Bad - test passes immediately
it('should exist', () => {
expect(StartCommand).toBeDefined(); // Always passes
});
// Good - test fails until feature exists
it('should initialize workflow', async () => {
const result = await new StartCommand().execute({ taskId: '7' });
expect(result.success).toBe(true); // Fails until execute() is implemented
});
```
**Problem:** GREEN phase won't complete - "tests still failing"
**Solution:** Fix your implementation until all tests pass:
```bash
# Run tests to see what's failing
$ npm test
# Fix the issue
$ vim src/commands/start.ts
# Verify tests pass
$ npm test
# Try again
$ tm autopilot complete --results '{"total":1,"passed":1,"failed":0,"skipped":0}'
```
### Resume Interrupted Work
```bash
# If you interrupted the workflow
$ tm autopilot resume
✓ Workflow resumed
✓ Task 7 - subtask 3/5
✓ Current phase: GREEN
→ Continue from where you left off
```
## JSON Output Mode
All commands support `--json` for programmatic use:
```bash
$ tm autopilot start 7 --json | jq .
{
"success": true,
"taskId": "7",
"branchName": "task-7",
"phase": "SUBTASK_LOOP",
"tddPhase": "RED",
"progress": { ... },
"currentSubtask": { ... },
"nextAction": "generate_test"
}
```
Perfect for:
- CI/CD integration
- Custom tooling
- Automated workflows
- Progress monitoring
## MCP Integration
For AI agents (Claude Code, etc.), use MCP tools:
```typescript
// Start workflow
await mcp.call('autopilot_start', {
taskId: '7',
projectRoot: '/path/to/project'
});
// Get next action
const next = await mcp.call('autopilot_next', {
projectRoot: '/path/to/project'
});
// Complete phase
await mcp.call('autopilot_complete_phase', {
projectRoot: '/path/to/project',
testResults: { total: 1, passed: 0, failed: 1, skipped: 0 }
});
// Commit
await mcp.call('autopilot_commit', {
projectRoot: '/path/to/project'
});
```
See [AI Agent Integration Guide](./ai-agent-integration.mdx) for details.
## Cheat Sheet
```bash
# Start
tm autopilot start <taskId> # Initialize workflow
# Workflow Control
tm autopilot next # What's next?
tm autopilot status # Current state
tm autopilot resume # Continue interrupted work
tm autopilot abort # Cancel and cleanup
# TDD Cycle
tm autopilot complete --results '{...}' # Advance phase
tm autopilot commit # Save progress
# Options
--json # Machine-readable output
--project-root <path> # Specify project location
--force # Override safety checks
```
## Next Steps
- Read [AI Agent Integration Guide](./ai-agent-integration.mdx) for complete documentation
- See [Example Prompts](./templates/example-prompts.mdx) for AI agent patterns
- Check [Command Reference](./command-reference.mdx) for all options
- Review [CLAUDE.md Template](./templates/CLAUDE.md.template) for AI integration
## Tips
1. **Always let tests fail first** - That's the RED phase
2. **Write minimal code** - Just enough to pass
3. **Commit frequently** - After each subtask
4. **Use --json** - Better for programmatic use
5. **Check status often** - Know where you are
6. **Trust the workflow** - It enforces TDD rules
---
**Ready to start?** Run `tm autopilot start <taskId>` and begin your TDD journey!

View File

@@ -0,0 +1,388 @@
---
title: "CLAUDE.md Template"
description: "Ready-to-use CLAUDE.md template for AI agent integration with TDD workflow"
---
This file provides integration instructions for AI agents (like Claude Code) to work with TaskMaster's autonomous TDD workflow system.
## Quick Reference
```bash
# Start workflow
tm autopilot start <taskId>
# Get next action
tm autopilot next --json
# Complete phase with test results
tm autopilot complete --results '{"total":N,"passed":N,"failed":N,"skipped":N}'
# Commit changes
tm autopilot commit
# Check status
tm autopilot status --json
# Abort workflow
tm autopilot abort
```
## Integration Pattern
### 1. Start Task
Before implementing a task:
```bash
tm autopilot start {TASK_ID}
```
This creates a workflow branch and initializes the TDD state machine.
### 2. Follow TDD Cycle
For each subtask, repeat this cycle:
#### RED Phase - Write Failing Test
1. Check next action:
```bash
tm autopilot next --json
```
2. Write a test that **fails** because the feature doesn't exist yet
3. Run tests and report results:
```bash
npm test # or appropriate test command
tm autopilot complete --results '{TEST_RESULTS_JSON}'
```
**Important:** RED phase MUST have at least one failing test.
#### GREEN Phase - Implement Feature
1. Check next action confirms GREEN phase
2. Write minimal implementation to make tests pass
3. Run tests and report results:
```bash
npm test
tm autopilot complete --results '{TEST_RESULTS_JSON}'
```
**Important:** GREEN phase MUST have all tests passing (failed === 0).
#### COMMIT Phase - Save Progress
1. Review changes:
```bash
git status
git diff
```
2. Commit (auto-generates message with metadata):
```bash
tm autopilot commit
```
3. Workflow automatically advances to next subtask
### 3. Monitor Progress
```bash
# Check overall progress
tm autopilot status --json
# See what's next
tm autopilot next --json
```
### 4. Handle Completion
When all subtasks are done:
- Workflow enters COMPLETE phase
- Branch remains for review/merge
- State can be cleaned up
## Example Session
```bash
# Start task with 3 subtasks
$ tm autopilot start 7
✓ Workflow started for task 7
✓ Created branch: task-7
✓ Phase: RED
✓ Next: generate_test for subtask 7.1
# Write failing test for subtask 7.1
$ cat > tests/feature.test.ts
# ... write test ...
$ npm test
# 1 test, 0 passed, 1 failed
$ tm autopilot complete --results '{"total":1,"passed":0,"failed":1,"skipped":0}'
✓ RED phase complete
✓ Phase: GREEN
✓ Next: implement_code
# Write implementation
$ cat > src/feature.ts
# ... write code ...
$ npm test
# 1 test, 1 passed, 0 failed
$ tm autopilot complete --results '{"total":1,"passed":1,"failed":0,"skipped":0}'
✓ GREEN phase complete
✓ Phase: COMMIT
✓ Next: commit_changes
$ tm autopilot commit
✓ Created commit: abc123
✓ Message: feat(feature): implement feature (Task 7.1)
✓ Advanced to subtask 7.2
✓ Phase: RED
✓ Next: generate_test
# Repeat for subtasks 7.2 and 7.3...
```
## Test Result Format
Always provide test results in this JSON format:
```json
{
"total": 10, // Total number of tests
"passed": 8, // Number of passing tests
"failed": 2, // Number of failing tests
"skipped": 0 // Number of skipped tests (optional)
}
```
### Parsing Test Output
Common test frameworks output that needs parsing:
**Vitest:**
```
Test Files 1 passed (1)
Tests 10 passed | 2 failed (12)
```
→ `{"total":12,"passed":10,"failed":2,"skipped":0}`
**Jest:**
```
Tests: 2 failed, 10 passed, 12 total
```
→ `{"total":12,"passed":10,"failed":2,"skipped":0}`
**Mocha:**
```
12 passing
2 failing
```
→ `{"total":14,"passed":12,"failed":2,"skipped":0}`
## Error Handling
### Common Issues
**1. RED Phase Won't Complete**
- Error: "RED phase validation failed: no test failures"
- Solution: Your test isn't actually testing new behavior. Write a test that fails.
**2. GREEN Phase Won't Complete**
- Error: "GREEN phase validation failed: tests still failing"
- Solution: Implementation incomplete. Debug and fix failing tests.
**3. Workflow Already Exists**
- Error: "Workflow already in progress"
- Solution: Run `tm autopilot resume` or `tm autopilot abort --force` then restart
**4. No Staged Changes**
- Error: "No staged changes to commit"
- Solution: Ensure you've actually created/modified files
### Recovery
If workflow gets stuck:
```bash
# Check current state
tm autopilot status --json
# If corrupted, abort and restart
tm autopilot abort --force
tm autopilot start {TASK_ID}
```
## Best Practices
### 1. One Feature Per Test Cycle
Each RED-GREEN-COMMIT cycle should implement exactly one small feature or behavior.
**Good:**
- RED: Test that `getUser()` returns user object
- GREEN: Implement `getUser()` to return user
- COMMIT: One commit for getUser feature
**Bad:**
- RED: Test multiple features at once
- GREEN: Implement entire module
- COMMIT: Massive commit with unrelated changes
### 2. Meaningful Test Names
Tests should clearly describe what they're validating:
```typescript
// Good
it('should return 404 when user not found', async () => {
const result = await getUser('nonexistent');
expect(result.status).toBe(404);
});
// Bad
it('test 1', () => {
// what does this test?
});
```
### 3. Minimal Implementation
In GREEN phase, write just enough code to pass the test:
```typescript
// Good - minimal implementation
function getUser(id: string) {
if (id === 'nonexistent') {
return { status: 404 };
}
return { status: 200, data: users[id] };
}
// Bad - over-engineering
function getUser(id: string) {
// Adds caching, validation, logging, etc. that isn't tested
}
```
### 4. Keep Tests Fast
Fast tests mean fast feedback:
- Avoid network calls (use mocks)
- Avoid file system operations (use in-memory)
- Avoid waiting/sleeping
### 5. Commit Message Quality
Let TaskMaster generate commit messages - they include:
- Conventional commit format (feat, fix, refactor, etc.)
- Subtask context and ID
- Workflow metadata
- Co-authorship attribution
## MCP Integration (Advanced)
For programmatic integration, use MCP tools instead of CLI:
```typescript
import { MCPClient } from '@modelcontextprotocol/sdk';
const client = new MCPClient();
// Start workflow
const start = await client.call('autopilot_start', {
taskId: '7',
projectRoot: '/path/to/project'
});
// Get next action
const next = await client.call('autopilot_next', {
projectRoot: '/path/to/project'
});
// Complete phase
const complete = await client.call('autopilot_complete_phase', {
projectRoot: '/path/to/project',
testResults: { total: 1, passed: 0, failed: 1, skipped: 0 }
});
// Commit
const commit = await client.call('autopilot_commit', {
projectRoot: '/path/to/project'
});
```
See [AI Agent Integration Guide](../ai-agent-integration.mdx) for complete MCP documentation.
## Workflow State Files
TaskMaster persists workflow state to `.taskmaster/workflow-state.json`:
```json
{
"phase": "SUBTASK_LOOP",
"context": {
"taskId": "7",
"subtasks": [...],
"currentSubtaskIndex": 0,
"currentTDDPhase": "RED",
"branchName": "task-7",
"errors": [],
"metadata": {
"startedAt": "2025-01-10T..."
}
}
}
```
**Important:** Never manually edit this file. Use CLI/MCP tools only.
## Project Structure
```
project/
├── .taskmaster/
│ ├── workflow-state.json # Current workflow state
│ ├── tasks/
│ │ └── tasks.json # Task definitions
│ └── docs/
│ └── prd.txt # Product requirements
├── src/ # Implementation files
├── tests/ # Test files
└── package.json
```
## Additional Resources
- [AI Agent Integration Guide](../ai-agent-integration.mdx) - Complete integration documentation
- [Command Reference](../command-reference.mdx) - All CLI commands
- [Task Structure](../task-structure.mdx) - Understanding tasks and subtasks
- [MCP Provider Guide](../mcp-provider-guide.mdx) - MCP integration details
## Troubleshooting
**Q: Workflow won't start**
A: Check that task has subtasks (`tm show <taskId>`) and git working tree is clean
**Q: Can't complete RED phase**
A: Verify at least one test is actually failing (not skipped, not passing)
**Q: Can't complete GREEN phase**
A: Verify ALL tests pass (zero failures)
**Q: Commit fails**
A: Check that you've made changes and they're staged (or stageable)
**Q: State seems wrong**
A: Check `.taskmaster/workflow-state.json` or run `tm autopilot status`
---
**For detailed documentation, see:** [AI Agent Integration Guide](../ai-agent-integration.mdx)

View File

@@ -0,0 +1,478 @@
---
title: "Example Prompts"
description: "Collection of effective prompts for AI agents working with TaskMaster's TDD workflow system"
---
Collection of effective prompts for AI agents working with TaskMaster's TDD workflow system.
## Getting Started Prompts
### Start a Task
```
I want to implement Task 7 using TDD workflow. Please:
1. Start the autopilot workflow
2. Show me the first subtask to implement
3. Begin the RED-GREEN-COMMIT cycle
```
### Resume Work
```
I have an in-progress workflow. Please:
1. Resume the autopilot workflow
2. Show current status and progress
3. Continue from where we left off
```
### Understanding Current State
```
What's the current state of the workflow? Please show:
- Which subtask we're on
- Current TDD phase (RED/GREEN/COMMIT)
- Progress percentage
- Next action required
```
## Test Generation Prompts
### Basic Test Generation
```
We're in RED phase for subtask "{SUBTASK_TITLE}". Please:
1. Read the subtask requirements
2. Write a comprehensive test that validates the behavior
3. The test MUST fail because the feature doesn't exist yet
4. Use the project's testing framework (vitest/jest/etc)
5. Follow the project's test file conventions
```
### Test for Specific Feature
```
For subtask: "Implement user authentication endpoint"
Write a failing test that:
1. Tests POST /api/auth/login
2. Validates request body (email, password)
3. Checks response format and status codes
4. Uses proper mocking for database calls
5. Follows security best practices
```
### Edge Case Testing
```
The basic happy path test is passing. Now write additional tests for:
1. Error cases (invalid input, missing fields)
2. Edge cases (empty strings, null values, etc.)
3. Security concerns (SQL injection, XSS)
4. Performance expectations (timeout, rate limits)
Each test should initially fail.
```
### Test Refactoring
```
Our tests are passing but could be improved. Please:
1. Review existing tests for duplication
2. Extract common setup into beforeEach/fixtures
3. Improve test descriptions for clarity
4. Add missing edge cases
5. Ensure all new tests fail first (RED phase)
```
## Implementation Prompts
### Basic Implementation
```
We're in GREEN phase. The test is failing with: {ERROR_MESSAGE}
Please:
1. Implement the minimal code to make this test pass
2. Don't over-engineer or add features not tested
3. Follow the project's code style and patterns
4. Ensure the implementation is clean and readable
```
### Implementation with Constraints
```
Implement the feature to pass the test, but:
- Use TypeScript with strict type checking
- Follow SOLID principles
- Keep functions under 20 lines
- Use dependency injection where appropriate
- Add JSDoc comments for public APIs
```
### Fix Failing Tests
```
GREEN phase validation failed - {N} tests still failing.
Please:
1. Review the failing test output
2. Identify what's not working
3. Fix the implementation to pass all tests
4. Don't modify tests to make them pass
5. Explain what was wrong
```
### Refactor Implementation
```
Tests are passing but code quality needs improvement:
1. Extract repeated logic into functions
2. Improve variable names
3. Add error handling
4. Optimize performance if needed
5. Ensure tests still pass after refactoring
```
## Debugging Prompts
### Test Output Parsing
```
Here's the test output:
{PASTE_TEST_OUTPUT}
Please parse this into the required JSON format:
{
"total": N,
"passed": N,
"failed": N,
"skipped": N
}
Then complete the current phase.
```
### Workflow Stuck
```
The workflow seems stuck. Please:
1. Check the current workflow status
2. Identify the issue
3. If corrupted, abort and restart
4. Explain what went wrong and how to prevent it
```
### Phase Validation Failing
```
I'm getting: "RED phase validation failed: no test failures"
Please:
1. Review the test I just wrote
2. Identify why it's not actually testing new behavior
3. Rewrite the test to properly fail
4. Explain what makes a good failing test
```
### Git Issues
```
Getting git errors when trying to start workflow:
{PASTE_ERROR}
Please:
1. Diagnose the git issue
2. Provide commands to fix it
3. Restart the workflow once fixed
```
## Advanced Patterns
### Parallel Test Generation
```
We have 3 subtasks to implement. For efficiency:
1. Read all 3 subtask descriptions
2. Plan the test structure for each
3. Identify shared test utilities needed
4. Generate tests for subtask 1 (they should fail)
5. Once we complete 1, move to 2, then 3
```
### Integration Test Strategy
```
This subtask requires integration testing. Please:
1. Set up test database/environment
2. Write integration tests that exercise the full stack
3. Use proper cleanup in afterEach
4. Mock external services (APIs, third-party)
5. Ensure tests are deterministic and fast
```
### Test-Driven Refactoring
```
We need to refactor {MODULE_NAME} but keep behavior unchanged:
1. First, write comprehensive tests for current behavior
2. Ensure all tests pass (document current state)
3. Refactor the implementation
4. Verify all tests still pass
5. Commit the refactoring
```
### Complex Feature Implementation
```
Subtask: "{COMPLEX_SUBTASK}"
This is complex. Let's break it down:
1. Identify the core functionality to test
2. Write tests for the simplest version
3. Implement minimal working code
4. Commit that cycle
5. Then iteratively add more tests for additional features
6. Each iteration is a RED-GREEN-COMMIT cycle
```
### Performance Testing
```
Write performance tests for {FEATURE}:
1. Measure baseline performance (current state)
2. Write test that fails if operation takes > {N}ms
3. Implement optimizations to pass the test
4. Document performance improvements
5. Consider edge cases (large inputs, concurrent requests)
```
### Security Testing
```
Write security-focused tests for {FEATURE}:
1. Test input validation (injection attacks)
2. Test authentication/authorization
3. Test data sanitization
4. Test rate limiting
5. Document security assumptions
Each test should initially fail and demonstrate the vulnerability.
```
## Workflow Automation Patterns
### Full Autonomous Mode
```
Implement Task {TASK_ID} completely autonomously:
1. Start the workflow
2. For each subtask:
a. Read requirements
b. Write failing tests
c. Implement to pass tests
d. Commit changes
3. Continue until all subtasks complete
4. Report final status
Rules:
- Never skip the RED phase
- Always verify tests fail first
- Implement minimal working code
- Commit after each subtask
- Handle errors gracefully with retries
```
### Supervised Mode
```
Work on Task {TASK_ID} with human oversight:
1. Start workflow and show plan
2. For each subtask:
a. Show test plan, wait for approval
b. Write and run tests, show results
c. Show implementation plan, wait for approval
d. Implement and verify
e. Show commit message, wait for approval
3. Request feedback between subtasks
```
### Review Mode
```
Review the current workflow state:
1. Show all completed subtasks and their commits
2. Identify remaining subtasks
3. Check test coverage
4. Verify git history is clean
5. Recommend next steps
```
## Error Recovery Patterns
### Retry with Learning
```
The {PHASE} phase failed {N} times. Please:
1. Review all previous attempts
2. Identify the pattern of failures
3. Propose a different approach
4. Explain why this approach should work
5. Implement with the new approach
```
### Escalate to Human
```
After {MAX_ATTEMPTS} attempts, unable to complete {SUBTASK}.
Please:
1. Document what was tried
2. Explain what's not working
3. Provide relevant code and test output
4. Suggest where human expertise is needed
5. Save current state for manual intervention
```
### Reset and Restart
```
Workflow is in an inconsistent state. Please:
1. Save any valuable work
2. Abort the current workflow
3. Explain what went wrong
4. Propose a better approach
5. Restart with improved strategy
```
## Example Complete Session
```
I need to implement Task 7 which has 5 subtasks. Please work autonomously with these preferences:
1. Testing Framework: vitest
2. Code Style: TypeScript strict mode, functional style preferred
3. Commit Style: Conventional commits with detailed messages
4. Review: Show me status after each subtask completion
Workflow:
1. Start autopilot for task 7
2. For each subtask (7.1 through 7.5):
- RED: Write comprehensive failing tests
- GREEN: Implement minimal code to pass
- COMMIT: Auto-generate commit and advance
3. Final: Show summary of all commits and changes
Error Handling:
- If phase validation fails, explain why and retry
- If tests are flaky, identify and fix
- If stuck after 3 attempts, ask for help
Let's begin!
```
## Tips for Effective Prompts
### 1. Be Specific About Context
**Good:**
```
For the UserAuthenticationService in src/services/auth.ts,
write tests for the login method using vitest.
```
**Bad:**
```
Write tests for authentication.
```
### 2. Specify Success Criteria
**Good:**
```
Tests should cover:
1. Successful login with valid credentials
2. Failed login with invalid password
3. Account lockout after 5 failures
4. Rate limiting (max 10 attempts/minute)
```
**Bad:**
```
Test login functionality.
```
### 3. Request Explanations
**Good:**
```
Implement the feature and explain:
1. Why this approach was chosen
2. What edge cases are handled
3. What assumptions were made
```
**Bad:**
```
Just implement it.
```
### 4. Include Project Context
**Good:**
```
Following the existing pattern in src/models/,
create a User model that:
- Extends BaseModel
- Uses Zod for validation
- Includes proper TypeScript types
```
**Bad:**
```
Create a user model.
```
## Troubleshooting Prompts
### When Tests Won't Fail
```
My test is passing when it should fail. Please:
1. Review the test code
2. Identify why it's passing
3. Check if implementation already exists
4. Rewrite test to actually test new behavior
5. Verify it fails this time
```
### When Implementation is Incomplete
```
Tests are still failing after implementation. Please:
1. Show me the failing test output
2. Review the implementation
3. Identify what's missing
4. Fix the implementation
5. Verify all tests pass
```
### When Workflow Won't Advance
```
Can't complete the phase. Getting error: {ERROR}
Please:
1. Check workflow status
2. Verify test results format is correct
3. Check if phase validation requirements are met
4. If needed, show me how to manually fix state
```
---
## Additional Resources
- [AI Agent Integration Guide](../ai-agent-integration.mdx)
- [CLAUDE.md Template](./CLAUDE.md.template)
- [Command Reference](../command-reference.mdx)
- [Testing Best Practices](./testing-best-practices.mdx)

54
apps/mcp/package.json Normal file
View File

@@ -0,0 +1,54 @@
{
"name": "@tm/mcp",
"description": "Task Master MCP Tools - TypeScript MCP server tools for AI agent integration",
"type": "module",
"private": true,
"version": "0.28.0-rc.2",
"main": "./dist/index.js",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./tools/autopilot": "./src/tools/autopilot/index.ts"
},
"files": ["dist", "README.md"],
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "biome check src",
"format": "biome format --write src",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:unit": "vitest run -t unit",
"test:integration": "vitest run -t integration",
"test:ci": "vitest run --coverage --reporter=dot"
},
"dependencies": {
"@tm/core": "*",
"zod": "^4.1.11",
"fastmcp": "^3.19.2"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^22.10.5",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"engines": {
"node": ">=18.0.0"
},
"keywords": [
"task-master",
"mcp",
"mcp-server",
"ai-agent",
"workflow",
"tdd"
],
"author": "",
"license": "MIT",
"typesVersions": {
"*": {
"*": ["src/*"]
}
}
}

8
apps/mcp/src/index.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* @fileoverview Main entry point for @tm/mcp package
* Exports all MCP tool registration functions
*/
export * from './tools/autopilot/index.js';
export * from './shared/utils.js';
export * from './shared/types.js';

View File

@@ -0,0 +1,36 @@
/**
* Shared types for MCP tools
*/
export interface MCPResponse<T = any> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
suggestion?: string;
details?: any;
};
version?: {
version: string;
name: string;
};
tag?: {
currentTag: string;
availableTags: string[];
};
}
export interface MCPContext {
log: {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
debug: (message: string) => void;
};
session: any;
}
export interface WithProjectRoot {
projectRoot: string;
}

View File

@@ -0,0 +1,257 @@
/**
* Shared utilities for MCP tools
*/
import type { ContentResult } from 'fastmcp';
import path from 'node:path';
import fs from 'node:fs';
import packageJson from '../../../../package.json' with { type: 'json' };
/**
* Get version information
*/
export function getVersionInfo() {
return {
version: packageJson.version || 'unknown',
name: packageJson.name || 'task-master-ai'
};
}
/**
* Get current tag for a project root
*/
export function getCurrentTag(projectRoot: string): string | null {
try {
// Try to read current tag from state.json
const stateJsonPath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(stateJsonPath)) {
const stateData = JSON.parse(fs.readFileSync(stateJsonPath, 'utf-8'));
return stateData.currentTag || 'master';
}
return 'master';
} catch {
return null;
}
}
/**
* Handle API result with standardized error handling and response formatting
* This provides a consistent response structure for all MCP tools
*/
export async function handleApiResult<T>(options: {
result: { success: boolean; data?: T; error?: { message: string } };
log?: any;
errorPrefix?: string;
projectRoot?: string;
}): Promise<ContentResult> {
const { result, log, errorPrefix = 'API error', projectRoot } = options;
// Get version info for every response
const versionInfo = getVersionInfo();
// Get current tag if project root is provided
const currentTag = projectRoot ? getCurrentTag(projectRoot) : null;
if (!result.success) {
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
log?.error?.(`${errorPrefix}: ${errorMsg}`);
let errorText = `Error: ${errorMsg}\nVersion: ${versionInfo.version}\nName: ${versionInfo.name}`;
if (currentTag) {
errorText += `\nCurrent Tag: ${currentTag}`;
}
return {
content: [
{
type: 'text',
text: errorText
}
],
isError: true
};
}
log?.info?.('Successfully completed operation');
// Create the response payload including version info and tag
const responsePayload: any = {
data: result.data,
version: versionInfo
};
// Add current tag if available
if (currentTag) {
responsePayload.tag = currentTag;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(responsePayload, null, 2)
}
]
};
}
/**
* Normalize project root path (handles URI encoding, file:// protocol, Windows paths)
*/
export function normalizeProjectRoot(rawPath: string): string {
if (!rawPath) return process.cwd();
try {
let pathString = rawPath;
// Decode URI encoding
try {
pathString = decodeURIComponent(pathString);
} catch {
// If decoding fails, use as-is
}
// Strip file:// prefix
if (pathString.startsWith('file:///')) {
pathString = pathString.slice(7);
} else if (pathString.startsWith('file://')) {
pathString = pathString.slice(7);
}
// Handle Windows drive letter after stripping prefix (e.g., /C:/...)
if (
pathString.startsWith('/') &&
/[A-Za-z]:/.test(pathString.substring(1, 3))
) {
pathString = pathString.substring(1);
}
// Normalize backslashes to forward slashes
pathString = pathString.replace(/\\/g, '/');
// Resolve to absolute path
return path.resolve(pathString);
} catch {
return path.resolve(rawPath);
}
}
/**
* Get project root from session object
*/
function getProjectRootFromSession(session: any): string | null {
try {
// Check primary location
if (session?.roots?.[0]?.uri) {
return normalizeProjectRoot(session.roots[0].uri);
}
// Check alternate location
else if (session?.roots?.roots?.[0]?.uri) {
return normalizeProjectRoot(session.roots.roots[0].uri);
}
return null;
} catch {
return null;
}
}
/**
* Wrapper to normalize project root in args with proper precedence order
*
* PRECEDENCE ORDER:
* 1. TASK_MASTER_PROJECT_ROOT environment variable (from process.env or session)
* 2. args.projectRoot (explicitly provided)
* 3. Session-based project root resolution
* 4. Current directory fallback
*/
export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
fn: (
args: T & { projectRoot: string },
context: any
) => Promise<ContentResult>
): (args: T, context: any) => Promise<ContentResult> {
return async (args: T, context: any): Promise<ContentResult> => {
const { log, session } = context;
let normalizedRoot: string | null = null;
let rootSource = 'unknown';
try {
// 1. Check for TASK_MASTER_PROJECT_ROOT environment variable first
if (process.env.TASK_MASTER_PROJECT_ROOT) {
const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
normalizedRoot = path.isAbsolute(envRoot)
? envRoot
: path.resolve(process.cwd(), envRoot);
rootSource = 'TASK_MASTER_PROJECT_ROOT environment variable';
log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
}
// Also check session environment variables for TASK_MASTER_PROJECT_ROOT
else if (session?.env?.TASK_MASTER_PROJECT_ROOT) {
const envRoot = session.env.TASK_MASTER_PROJECT_ROOT;
normalizedRoot = path.isAbsolute(envRoot)
? envRoot
: path.resolve(process.cwd(), envRoot);
rootSource = 'TASK_MASTER_PROJECT_ROOT session environment variable';
log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
}
// 2. If no environment variable, try args.projectRoot
else if (args.projectRoot) {
normalizedRoot = normalizeProjectRoot(args.projectRoot);
rootSource = 'args.projectRoot';
log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
}
// 3. If no args.projectRoot, try session-based resolution
else {
const sessionRoot = getProjectRootFromSession(session);
if (sessionRoot) {
normalizedRoot = sessionRoot;
rootSource = 'session';
log?.info?.(
`Using project root from ${rootSource}: ${normalizedRoot}`
);
}
}
if (!normalizedRoot) {
log?.error?.(
'Could not determine project root from environment, args, or session.'
);
return handleApiResult({
result: {
success: false,
error: {
message:
'Could not determine project root. Please provide projectRoot argument or ensure TASK_MASTER_PROJECT_ROOT environment variable is set.'
}
}
});
}
// Inject the normalized root back into args
const updatedArgs = { ...args, projectRoot: normalizedRoot } as T & {
projectRoot: string;
};
// Execute the original function with normalized root in args
return await fn(updatedArgs, context);
} catch (error: any) {
log?.error?.(
`Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
);
if (error.stack && log?.debug) {
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: {
message: `Operation failed: ${error.message}`
}
}
});
}
};
}

View File

@@ -0,0 +1,99 @@
/**
* @fileoverview autopilot-abort MCP tool
* Abort a running TDD workflow and clean up state
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const AbortSchema = z.object({
projectRoot: z
.string()
.describe('Absolute path to the project root directory')
});
type AbortArgs = z.infer<typeof AbortSchema>;
/**
* Register the autopilot_abort tool with the MCP server
*/
export function registerAutopilotAbortTool(server: FastMCP) {
server.addTool({
name: 'autopilot_abort',
description:
'Abort the current TDD workflow and clean up workflow state. This will remove the workflow state file but will NOT delete the git branch or any code changes.',
parameters: AbortSchema,
execute: withNormalizedProjectRoot(
async (args: AbortArgs, context: MCPContext) => {
const { projectRoot } = args;
try {
context.log.info(`Aborting autopilot workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
// Check if workflow exists
const hasWorkflow = await workflowService.hasWorkflow();
if (!hasWorkflow) {
context.log.warn('No active workflow to abort');
return handleApiResult({
result: {
success: true,
data: {
message: 'No active workflow to abort',
hadWorkflow: false
}
},
log: context.log,
projectRoot
});
}
// Get info before aborting
await workflowService.resumeWorkflow();
const status = workflowService.getStatus();
// Abort workflow
await workflowService.abortWorkflow();
context.log.info('Workflow state deleted');
return handleApiResult({
result: {
success: true,
data: {
message: 'Workflow aborted',
hadWorkflow: true,
taskId: status.taskId,
branchName: status.branchName,
note: 'Git branch and code changes were preserved. You can manually clean them up if needed.'
}
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-abort: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to abort workflow: ${error.message}` }
},
log: context.log,
projectRoot
});
}
}
)
});
}

View File

@@ -0,0 +1,240 @@
/**
* @fileoverview autopilot-commit MCP tool
* Create a git commit with automatic staging and message generation
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService, GitAdapter, CommitMessageGenerator } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const CommitSchema = z.object({
projectRoot: z
.string()
.describe('Absolute path to the project root directory'),
files: z
.array(z.string())
.optional()
.describe(
'Specific files to stage (relative to project root). If not provided, stages all changes.'
),
customMessage: z
.string()
.optional()
.describe('Custom commit message to use instead of auto-generated message')
});
type CommitArgs = z.infer<typeof CommitSchema>;
/**
* Register the autopilot_commit tool with the MCP server
*/
export function registerAutopilotCommitTool(server: FastMCP) {
server.addTool({
name: 'autopilot_commit',
description:
'Create a git commit with automatic staging, message generation, and metadata embedding. Generates appropriate commit messages based on subtask context and TDD phase.',
parameters: CommitSchema,
execute: withNormalizedProjectRoot(
async (args: CommitArgs, context: MCPContext) => {
const { projectRoot, files, customMessage } = args;
try {
context.log.info(`Creating commit for workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
// Check if workflow exists
if (!(await workflowService.hasWorkflow())) {
return handleApiResult({
result: {
success: false,
error: {
message:
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
projectRoot
});
}
// Resume workflow
await workflowService.resumeWorkflow();
const status = workflowService.getStatus();
const workflowContext = workflowService.getContext();
// Verify we're in COMMIT phase
if (status.tddPhase !== 'COMMIT') {
context.log.warn(
`Not in COMMIT phase (currently in ${status.tddPhase})`
);
return handleApiResult({
result: {
success: false,
error: {
message: `Cannot commit: currently in ${status.tddPhase} phase. Complete the ${status.tddPhase} phase first using autopilot_complete_phase`
}
},
log: context.log,
projectRoot
});
}
// Verify there's an active subtask
if (!status.currentSubtask) {
return handleApiResult({
result: {
success: false,
error: { message: 'No active subtask to commit' }
},
log: context.log,
projectRoot
});
}
// Initialize git adapter
const gitAdapter = new GitAdapter(projectRoot);
// Stage files
try {
if (files && files.length > 0) {
await gitAdapter.stageFiles(files);
context.log.info(`Staged ${files.length} files`);
} else {
await gitAdapter.stageFiles(['.']);
context.log.info('Staged all changes');
}
} catch (error: any) {
context.log.error(`Failed to stage files: ${error.message}`);
return handleApiResult({
result: {
success: false,
error: { message: `Failed to stage files: ${error.message}` }
},
log: context.log,
projectRoot
});
}
// Check if there are staged changes
const hasStagedChanges = await gitAdapter.hasStagedChanges();
if (!hasStagedChanges) {
context.log.warn('No staged changes to commit');
return handleApiResult({
result: {
success: false,
error: {
message:
'No staged changes to commit. Make code changes before committing'
}
},
log: context.log,
projectRoot
});
}
// Get git status for message generation
const gitStatus = await gitAdapter.getStatus();
// Generate commit message
let commitMessage: string;
if (customMessage) {
commitMessage = customMessage;
context.log.info('Using custom commit message');
} else {
const messageGenerator = new CommitMessageGenerator();
// Determine commit type based on phase and subtask
// RED phase = test files, GREEN phase = implementation
const type = status.tddPhase === 'COMMIT' ? 'feat' : 'test';
// Use subtask title as description
const description = status.currentSubtask.title;
// Construct proper CommitMessageOptions
const options = {
type,
description,
changedFiles: gitStatus.staged,
taskId: status.taskId,
phase: status.tddPhase,
testsPassing: workflowContext.lastTestResults?.passed,
testsFailing: workflowContext.lastTestResults?.failed
};
commitMessage = messageGenerator.generateMessage(options);
context.log.info('Generated commit message automatically');
}
// Create commit
try {
await gitAdapter.createCommit(commitMessage);
context.log.info('Commit created successfully');
} catch (error: any) {
context.log.error(`Failed to create commit: ${error.message}`);
return handleApiResult({
result: {
success: false,
error: { message: `Failed to create commit: ${error.message}` }
},
log: context.log,
projectRoot
});
}
// Get last commit info
const lastCommit = await gitAdapter.getLastCommit();
// Complete COMMIT phase and advance workflow
const newStatus = await workflowService.commit();
context.log.info(
`Commit completed. Current phase: ${newStatus.tddPhase || newStatus.phase}`
);
const isComplete = newStatus.phase === 'COMPLETE';
// Get next action with guidance
const nextAction = workflowService.getNextAction();
return handleApiResult({
result: {
success: true,
data: {
message: isComplete
? 'Workflow completed successfully'
: 'Commit created and workflow advanced',
commitSha: lastCommit.sha,
commitMessage,
...newStatus,
isComplete,
nextAction: nextAction.action,
nextSteps: nextAction.nextSteps
}
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-commit: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to commit: ${error.message}` }
},
log: context.log,
projectRoot
});
}
}
)
});
}

View File

@@ -0,0 +1,152 @@
/**
* @fileoverview autopilot-complete MCP tool
* Complete the current TDD phase with test result validation
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const CompletePhaseSchema = z.object({
projectRoot: z
.string()
.describe('Absolute path to the project root directory'),
testResults: z
.object({
total: z.number().describe('Total number of tests'),
passed: z.number().describe('Number of passing tests'),
failed: z.number().describe('Number of failing tests'),
skipped: z.number().optional().describe('Number of skipped tests')
})
.describe('Test results from running the test suite')
});
type CompletePhaseArgs = z.infer<typeof CompletePhaseSchema>;
/**
* Register the autopilot_complete_phase tool with the MCP server
*/
export function registerAutopilotCompleteTool(server: FastMCP) {
server.addTool({
name: 'autopilot_complete_phase',
description:
'Complete the current TDD phase (RED, GREEN, or COMMIT) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing.',
parameters: CompletePhaseSchema,
execute: withNormalizedProjectRoot(
async (args: CompletePhaseArgs, context: MCPContext) => {
const { projectRoot, testResults } = args;
try {
context.log.info(
`Completing current phase in workflow for ${projectRoot}`
);
const workflowService = new WorkflowService(projectRoot);
// Check if workflow exists
if (!(await workflowService.hasWorkflow())) {
return handleApiResult({
result: {
success: false,
error: {
message:
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
projectRoot
});
}
// Resume workflow to get current state
await workflowService.resumeWorkflow();
const currentStatus = workflowService.getStatus();
// Validate that we're in a TDD phase (RED or GREEN)
if (!currentStatus.tddPhase) {
return handleApiResult({
result: {
success: false,
error: {
message: `Cannot complete phase: not in a TDD phase (current phase: ${currentStatus.phase})`
}
},
log: context.log,
projectRoot
});
}
// COMMIT phase completion is handled by autopilot_commit tool
if (currentStatus.tddPhase === 'COMMIT') {
return handleApiResult({
result: {
success: false,
error: {
message:
'Cannot complete COMMIT phase with this tool. Use autopilot_commit instead'
}
},
log: context.log,
projectRoot
});
}
// Map TDD phase to TestResult phase (only RED or GREEN allowed)
const phase = currentStatus.tddPhase as 'RED' | 'GREEN';
// Construct full TestResult with phase
const fullTestResults = {
total: testResults.total,
passed: testResults.passed,
failed: testResults.failed,
skipped: testResults.skipped ?? 0,
phase
};
// Complete phase with test results
const status = await workflowService.completePhase(fullTestResults);
const nextAction = workflowService.getNextAction();
context.log.info(
`Phase completed. New phase: ${status.tddPhase || status.phase}`
);
return handleApiResult({
result: {
success: true,
data: {
message: `Phase completed. Transitioned to ${status.tddPhase || status.phase}`,
...status,
nextAction: nextAction.action,
actionDescription: nextAction.description,
nextSteps: nextAction.nextSteps
}
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-complete: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: {
message: `Failed to complete phase: ${error.message}`
}
},
log: context.log,
projectRoot
});
}
}
)
});
}

View File

@@ -0,0 +1,114 @@
/**
* @fileoverview autopilot-finalize MCP tool
* Finalize and complete the workflow with working tree validation
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const FinalizeSchema = z.object({
projectRoot: z
.string()
.describe('Absolute path to the project root directory')
});
type FinalizeArgs = z.infer<typeof FinalizeSchema>;
/**
* Register the autopilot_finalize tool with the MCP server
*/
export function registerAutopilotFinalizeTool(server: FastMCP) {
server.addTool({
name: 'autopilot_finalize',
description:
'Finalize and complete the workflow. Validates that all changes are committed and working tree is clean before marking workflow as complete.',
parameters: FinalizeSchema,
execute: withNormalizedProjectRoot(
async (args: FinalizeArgs, context: MCPContext) => {
const { projectRoot } = args;
try {
context.log.info(`Finalizing workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
// Check if workflow exists
if (!(await workflowService.hasWorkflow())) {
return handleApiResult({
result: {
success: false,
error: {
message:
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
projectRoot
});
}
// Resume workflow
await workflowService.resumeWorkflow();
const currentStatus = workflowService.getStatus();
// Verify we're in FINALIZE phase
if (currentStatus.phase !== 'FINALIZE') {
return handleApiResult({
result: {
success: false,
error: {
message: `Cannot finalize: workflow is in ${currentStatus.phase} phase. Complete all subtasks first.`
}
},
log: context.log,
projectRoot
});
}
// Finalize workflow (validates clean working tree)
const newStatus = await workflowService.finalizeWorkflow();
context.log.info('Workflow finalized successfully');
// Get next action
const nextAction = workflowService.getNextAction();
return handleApiResult({
result: {
success: true,
data: {
message: 'Workflow completed successfully',
...newStatus,
nextAction: nextAction.action,
nextSteps: nextAction.nextSteps
}
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-finalize: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: {
message: `Failed to finalize workflow: ${error.message}`
}
},
log: context.log,
projectRoot
});
}
}
)
});
}

View File

@@ -0,0 +1,13 @@
/**
* @fileoverview Autopilot MCP tools index
* Exports all autopilot tool registration functions
*/
export { registerAutopilotStartTool } from './start.tool.js';
export { registerAutopilotResumeTool } from './resume.tool.js';
export { registerAutopilotNextTool } from './next.tool.js';
export { registerAutopilotStatusTool } from './status.tool.js';
export { registerAutopilotCompleteTool } from './complete.tool.js';
export { registerAutopilotCommitTool } from './commit.tool.js';
export { registerAutopilotFinalizeTool } from './finalize.tool.js';
export { registerAutopilotAbortTool } from './abort.tool.js';

View File

@@ -0,0 +1,99 @@
/**
* @fileoverview autopilot-next MCP tool
* Get the next action to perform in the TDD workflow
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const NextActionSchema = z.object({
projectRoot: z
.string()
.describe('Absolute path to the project root directory')
});
type NextActionArgs = z.infer<typeof NextActionSchema>;
/**
* Register the autopilot_next tool with the MCP server
*/
export function registerAutopilotNextTool(server: FastMCP) {
server.addTool({
name: 'autopilot_next',
description:
'Get the next action to perform in the TDD workflow. Returns detailed context about what needs to be done next, including the current phase, subtask, and expected actions.',
parameters: NextActionSchema,
execute: withNormalizedProjectRoot(
async (args: NextActionArgs, context: MCPContext) => {
const { projectRoot } = args;
try {
context.log.info(
`Getting next action for workflow in ${projectRoot}`
);
const workflowService = new WorkflowService(projectRoot);
// Check if workflow exists
if (!(await workflowService.hasWorkflow())) {
return handleApiResult({
result: {
success: false,
error: {
message:
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
projectRoot
});
}
// Resume to load state
await workflowService.resumeWorkflow();
// Get next action
const nextAction = workflowService.getNextAction();
const status = workflowService.getStatus();
context.log.info(`Next action determined: ${nextAction.action}`);
return handleApiResult({
result: {
success: true,
data: {
action: nextAction.action,
actionDescription: nextAction.description,
...status,
nextSteps: nextAction.nextSteps
}
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-next: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: {
message: `Failed to get next action: ${error.message}`
}
},
log: context.log,
projectRoot
});
}
}
)
});
}

View File

@@ -0,0 +1,95 @@
/**
* @fileoverview autopilot-resume MCP tool
* Resume a previously started TDD workflow from saved state
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const ResumeWorkflowSchema = z.object({
projectRoot: z
.string()
.describe('Absolute path to the project root directory')
});
type ResumeWorkflowArgs = z.infer<typeof ResumeWorkflowSchema>;
/**
* Register the autopilot_resume tool with the MCP server
*/
export function registerAutopilotResumeTool(server: FastMCP) {
server.addTool({
name: 'autopilot_resume',
description:
'Resume a previously started TDD workflow from saved state. Restores the workflow state machine and continues from where it left off.',
parameters: ResumeWorkflowSchema,
execute: withNormalizedProjectRoot(
async (args: ResumeWorkflowArgs, context: MCPContext) => {
const { projectRoot } = args;
try {
context.log.info(`Resuming autopilot workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
// Check if workflow exists
if (!(await workflowService.hasWorkflow())) {
return handleApiResult({
result: {
success: false,
error: {
message:
'No workflow state found. Start a new workflow with autopilot_start'
}
},
log: context.log,
projectRoot
});
}
// Resume workflow
const status = await workflowService.resumeWorkflow();
const nextAction = workflowService.getNextAction();
context.log.info(
`Workflow resumed successfully for task ${status.taskId}`
);
return handleApiResult({
result: {
success: true,
data: {
message: 'Workflow resumed',
...status,
nextAction: nextAction.action,
actionDescription: nextAction.description,
nextSteps: nextAction.nextSteps
}
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-resume: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to resume workflow: ${error.message}` }
},
log: context.log,
projectRoot
});
}
}
)
});
}

View File

@@ -0,0 +1,197 @@
/**
* @fileoverview autopilot-start MCP tool
* Initialize and start a new TDD workflow for a task
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { createTaskMasterCore } from '@tm/core';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const StartWorkflowSchema = z.object({
taskId: z
.string()
.describe(
'Main task ID to start workflow for (e.g., "1", "2", "HAM-123"). Subtask IDs (e.g., "2.3", "1.1") are not allowed.'
),
projectRoot: z
.string()
.describe('Absolute path to the project root directory'),
maxAttempts: z
.number()
.optional()
.default(3)
.describe('Maximum attempts per subtask (default: 3)'),
force: z
.boolean()
.optional()
.default(false)
.describe('Force start even if workflow state exists')
});
type StartWorkflowArgs = z.infer<typeof StartWorkflowSchema>;
/**
* Check if a task ID is a main task (not a subtask)
* Main tasks: "1", "2", "HAM-123", "PROJ-456"
* Subtasks: "1.1", "2.3", "HAM-123.1"
*/
function isMainTaskId(taskId: string): boolean {
// A main task has no dots in the ID after the optional prefix
// Examples: "1" ✓, "HAM-123" ✓, "1.1" ✗, "HAM-123.1" ✗
const parts = taskId.split('.');
return parts.length === 1;
}
/**
* Register the autopilot_start tool with the MCP server
*/
export function registerAutopilotStartTool(server: FastMCP) {
server.addTool({
name: 'autopilot_start',
description:
'Initialize and start a new TDD workflow for a task. Creates a git branch and sets up the workflow state machine.',
parameters: StartWorkflowSchema,
execute: withNormalizedProjectRoot(
async (args: StartWorkflowArgs, context: MCPContext) => {
const { taskId, projectRoot, maxAttempts, force } = args;
try {
context.log.info(
`Starting autopilot workflow for task ${taskId} in ${projectRoot}`
);
// Validate that taskId is a main task (not a subtask)
if (!isMainTaskId(taskId)) {
return handleApiResult({
result: {
success: false,
error: {
message: `Task ID "${taskId}" is a subtask. Autopilot workflows can only be started for main tasks (e.g., "1", "2", "HAM-123"). Please provide the parent task ID instead.`
}
},
log: context.log,
projectRoot
});
}
// Load task data and get current tag
const core = await createTaskMasterCore({
projectPath: projectRoot
});
// Get current tag from ConfigManager
const currentTag = core.getActiveTag();
const taskResult = await core.getTaskWithSubtask(taskId);
if (!taskResult || !taskResult.task) {
await core.close();
return handleApiResult({
result: {
success: false,
error: { message: `Task ${taskId} not found` }
},
log: context.log,
projectRoot
});
}
const task = taskResult.task;
// Validate task has subtasks
if (!task.subtasks || task.subtasks.length === 0) {
await core.close();
return handleApiResult({
result: {
success: false,
error: {
message: `Task ${taskId} has no subtasks. Please use expand_task (with id="${taskId}") to create subtasks first. For improved results, consider running analyze_complexity before expanding the task.`
}
},
log: context.log,
projectRoot
});
}
// Initialize workflow service
const workflowService = new WorkflowService(projectRoot);
// Check for existing workflow
const hasWorkflow = await workflowService.hasWorkflow();
if (hasWorkflow && !force) {
context.log.warn('Workflow state already exists');
return handleApiResult({
result: {
success: false,
error: {
message:
'Workflow already in progress. Use force=true to override or resume the existing workflow. Suggestion: Use autopilot_resume to continue the existing workflow'
}
},
log: context.log,
projectRoot
});
}
// Start workflow
const status = await workflowService.startWorkflow({
taskId,
taskTitle: task.title,
subtasks: task.subtasks.map((st: any) => ({
id: st.id,
title: st.title,
status: st.status,
maxAttempts
})),
maxAttempts,
force,
tag: currentTag // Pass current tag for branch naming
});
context.log.info(`Workflow started successfully for task ${taskId}`);
// Get next action with guidance from WorkflowService
const nextAction = workflowService.getNextAction();
return handleApiResult({
result: {
success: true,
data: {
message: `Workflow started for task ${taskId}`,
taskId,
branchName: status.branchName,
phase: status.phase,
tddPhase: status.tddPhase,
progress: status.progress,
currentSubtask: status.currentSubtask,
nextAction: nextAction.action,
nextSteps: nextAction.nextSteps
}
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-start: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to start workflow: ${error.message}` }
},
log: context.log,
projectRoot
});
}
}
)
});
}

View File

@@ -0,0 +1,93 @@
/**
* @fileoverview autopilot-status MCP tool
* Get comprehensive workflow status and progress information
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const StatusSchema = z.object({
projectRoot: z
.string()
.describe('Absolute path to the project root directory')
});
type StatusArgs = z.infer<typeof StatusSchema>;
/**
* Register the autopilot_status tool with the MCP server
*/
export function registerAutopilotStatusTool(server: FastMCP) {
server.addTool({
name: 'autopilot_status',
description:
'Get comprehensive workflow status including current phase, progress, subtask details, and activity history.',
parameters: StatusSchema,
execute: withNormalizedProjectRoot(
async (args: StatusArgs, context: MCPContext) => {
const { projectRoot } = args;
try {
context.log.info(`Getting workflow status for ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
// Check if workflow exists
if (!(await workflowService.hasWorkflow())) {
return handleApiResult({
result: {
success: false,
error: {
message:
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
projectRoot
});
}
// Resume to load state
await workflowService.resumeWorkflow();
// Get status
const status = workflowService.getStatus();
context.log.info(
`Workflow status retrieved for task ${status.taskId}`
);
return handleApiResult({
result: {
success: true,
data: status
},
log: context.log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-status: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: {
message: `Failed to get workflow status: ${error.message}`
}
},
log: context.log,
projectRoot
});
}
}
)
});
}

36
apps/mcp/tsconfig.json Normal file
View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"baseUrl": ".",
"rootDir": "./src",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "NodeNext",
"moduleDetection": "force",
"types": ["node"],
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
}

23
apps/mcp/vitest.config.ts Normal file
View File

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