feat: Phase 1 - Complete TDD Workflow Automation System (#1289)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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'
|
||||
},
|
||||
|
||||
119
apps/cli/src/commands/autopilot/abort.command.ts
Normal file
119
apps/cli/src/commands/autopilot/abort.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
169
apps/cli/src/commands/autopilot/commit.command.ts
Normal file
169
apps/cli/src/commands/autopilot/commit.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
apps/cli/src/commands/autopilot/complete.command.ts
Normal file
172
apps/cli/src/commands/autopilot/complete.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
apps/cli/src/commands/autopilot/index.ts
Normal file
82
apps/cli/src/commands/autopilot/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
164
apps/cli/src/commands/autopilot/next.command.ts
Normal file
164
apps/cli/src/commands/autopilot/next.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
apps/cli/src/commands/autopilot/resume.command.ts
Normal file
111
apps/cli/src/commands/autopilot/resume.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
262
apps/cli/src/commands/autopilot/shared.ts
Normal file
262
apps/cli/src/commands/autopilot/shared.ts
Normal 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
|
||||
}));
|
||||
}
|
||||
168
apps/cli/src/commands/autopilot/start.command.ts
Normal file
168
apps/cli/src/commands/autopilot/start.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
apps/cli/src/commands/autopilot/status.command.ts
Normal file
114
apps/cli/src/commands/autopilot/status.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
540
apps/cli/tests/integration/commands/autopilot/workflow.test.ts
Normal file
540
apps/cli/tests/integration/commands/autopilot/workflow.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
202
apps/cli/tests/unit/commands/autopilot/shared.test.ts
Normal file
202
apps/cli/tests/unit/commands/autopilot/shared.test.ts
Normal 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
25
apps/cli/vitest.config.ts
Normal 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'
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
944
apps/docs/tdd-workflow/ai-agent-integration.mdx
Normal file
944
apps/docs/tdd-workflow/ai-agent-integration.mdx
Normal 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
|
||||
315
apps/docs/tdd-workflow/quickstart.mdx
Normal file
315
apps/docs/tdd-workflow/quickstart.mdx
Normal 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!
|
||||
388
apps/docs/tdd-workflow/templates/claude-template.mdx
Normal file
388
apps/docs/tdd-workflow/templates/claude-template.mdx
Normal 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)
|
||||
478
apps/docs/tdd-workflow/templates/example-prompts.mdx
Normal file
478
apps/docs/tdd-workflow/templates/example-prompts.mdx
Normal 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
54
apps/mcp/package.json
Normal 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
8
apps/mcp/src/index.ts
Normal 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';
|
||||
36
apps/mcp/src/shared/types.ts
Normal file
36
apps/mcp/src/shared/types.ts
Normal 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;
|
||||
}
|
||||
257
apps/mcp/src/shared/utils.ts
Normal file
257
apps/mcp/src/shared/utils.ts
Normal 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}`
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
99
apps/mcp/src/tools/autopilot/abort.tool.ts
Normal file
99
apps/mcp/src/tools/autopilot/abort.tool.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
240
apps/mcp/src/tools/autopilot/commit.tool.ts
Normal file
240
apps/mcp/src/tools/autopilot/commit.tool.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
152
apps/mcp/src/tools/autopilot/complete.tool.ts
Normal file
152
apps/mcp/src/tools/autopilot/complete.tool.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
114
apps/mcp/src/tools/autopilot/finalize.tool.ts
Normal file
114
apps/mcp/src/tools/autopilot/finalize.tool.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
13
apps/mcp/src/tools/autopilot/index.ts
Normal file
13
apps/mcp/src/tools/autopilot/index.ts
Normal 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';
|
||||
99
apps/mcp/src/tools/autopilot/next.tool.ts
Normal file
99
apps/mcp/src/tools/autopilot/next.tool.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
95
apps/mcp/src/tools/autopilot/resume.tool.ts
Normal file
95
apps/mcp/src/tools/autopilot/resume.tool.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
197
apps/mcp/src/tools/autopilot/start.tool.ts
Normal file
197
apps/mcp/src/tools/autopilot/start.tool.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
93
apps/mcp/src/tools/autopilot/status.tool.ts
Normal file
93
apps/mcp/src/tools/autopilot/status.tool.ts
Normal 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
36
apps/mcp/tsconfig.json
Normal 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
23
apps/mcp/vitest.config.ts
Normal 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'
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user