feat: Phase 1 - Complete TDD Workflow Automation System (#1289)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
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