mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
fix: return actual subtask instead of parent task for subtask IDs (#1358)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Fixes #1355
This commit is contained in:
7
.changeset/forty-squids-sell.md
Normal file
7
.changeset/forty-squids-sell.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix subtask ID display to show full compound notation
|
||||
|
||||
When displaying a subtask via `tm show 104.1`, the header and properties table showed only the subtask's local ID (e.g., "1") instead of the full compound ID (e.g., "104.1"). The CLI now preserves and displays the original requested task ID throughout the display chain, ensuring subtasks are clearly identified with their parent context. Also improved TypeScript typing by using discriminated unions for Task/Subtask returns from `tasks.get()`, eliminating unsafe type coercions.
|
||||
@@ -1,507 +0,0 @@
|
||||
/**
|
||||
* @fileoverview AutopilotCommand using Commander's native class pattern
|
||||
* Extends Commander.Command for better integration with the framework
|
||||
* This is a thin presentation layer over @tm/core's autopilot functionality
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import ora, { type Ora } from 'ora';
|
||||
import { createTmCore, type TmCore, type Task, type Subtask } from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
* CLI-specific options interface for the autopilot command
|
||||
*/
|
||||
export interface AutopilotCommandOptions {
|
||||
format?: 'text' | 'json';
|
||||
project?: string;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preflight check result for a single check
|
||||
*/
|
||||
export interface PreflightCheckResult {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overall preflight check results
|
||||
*/
|
||||
export interface PreflightResult {
|
||||
success: boolean;
|
||||
testCommand: PreflightCheckResult;
|
||||
gitWorkingTree: PreflightCheckResult;
|
||||
requiredTools: PreflightCheckResult;
|
||||
defaultBranch: PreflightCheckResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-specific result type from autopilot command
|
||||
*/
|
||||
export interface AutopilotCommandResult {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
task?: Task;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AutopilotCommand extending Commander's Command class
|
||||
* This is a thin presentation layer over @tm/core's autopilot functionality
|
||||
*/
|
||||
export class AutopilotCommand extends Command {
|
||||
private tmCore?: TmCore;
|
||||
private lastResult?: AutopilotCommandResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'autopilot');
|
||||
|
||||
// Configure the command
|
||||
this.description(
|
||||
'Execute a task autonomously using TDD workflow with git integration'
|
||||
)
|
||||
.argument('<taskId>', 'Task ID to execute autonomously')
|
||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||
.option(
|
||||
'--dry-run',
|
||||
'Show what would be executed without performing actions'
|
||||
)
|
||||
.action(async (taskId: string, options: AutopilotCommandOptions) => {
|
||||
await this.executeCommand(taskId, options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the autopilot command
|
||||
*/
|
||||
private async executeCommand(
|
||||
taskId: string,
|
||||
options: AutopilotCommandOptions
|
||||
): Promise<void> {
|
||||
let spinner: Ora | null = null;
|
||||
|
||||
try {
|
||||
// Validate options
|
||||
if (!this.validateOptions(options)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate task ID format
|
||||
if (!this.validateTaskId(taskId)) {
|
||||
ui.displayError(`Invalid task ID format: ${taskId}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize tm-core with spinner
|
||||
spinner = ora('Initializing Task Master...').start();
|
||||
await this.initializeCore(options.project || process.cwd());
|
||||
spinner.succeed('Task Master initialized');
|
||||
|
||||
// Load and validate task existence
|
||||
spinner = ora(`Loading task ${taskId}...`).start();
|
||||
const task = await this.loadTask(taskId);
|
||||
|
||||
if (!task) {
|
||||
spinner.fail(`Task ${taskId} not found`);
|
||||
ui.displayError(`Task with ID ${taskId} does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner.succeed(`Task ${taskId} loaded`);
|
||||
|
||||
// Display task information
|
||||
this.displayTaskInfo(task, options.dryRun || false);
|
||||
|
||||
// Execute autopilot logic (placeholder for now)
|
||||
const result = await this.performAutopilot(taskId, task, options);
|
||||
|
||||
// Store result for programmatic access
|
||||
this.setLastResult(result);
|
||||
|
||||
// Display results
|
||||
this.displayResults(result, options);
|
||||
} catch (error: unknown) {
|
||||
if (spinner) {
|
||||
spinner.fail('Operation failed');
|
||||
}
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command options
|
||||
*/
|
||||
private validateOptions(options: AutopilotCommandOptions): boolean {
|
||||
// Validate format
|
||||
if (options.format && !['text', 'json'].includes(options.format)) {
|
||||
console.error(chalk.red(`Invalid format: ${options.format}`));
|
||||
console.error(chalk.gray(`Valid formats: text, json`));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate task ID format
|
||||
*/
|
||||
private validateTaskId(taskId: string): boolean {
|
||||
// Task ID should be a number or number.number format (e.g., "1" or "1.2")
|
||||
const taskIdPattern = /^\d+(\.\d+)*$/;
|
||||
return taskIdPattern.test(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TmCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = await createTmCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load task from tm-core
|
||||
*/
|
||||
private async loadTask(taskId: string): Promise<Task | null> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const { task } = await this.tmCore.tasks.get(taskId);
|
||||
return task;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display task information before execution
|
||||
*/
|
||||
private displayTaskInfo(task: Task, isDryRun: boolean): void {
|
||||
const prefix = isDryRun ? '[DRY RUN] ' : '';
|
||||
console.log();
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.cyan.bold(`${prefix}Autopilot Task Execution`) +
|
||||
'\n\n' +
|
||||
chalk.white(`Task ID: ${task.id}`) +
|
||||
'\n' +
|
||||
chalk.white(`Title: ${task.title}`) +
|
||||
'\n' +
|
||||
chalk.white(`Status: ${task.status}`) +
|
||||
(task.description ? '\n\n' + chalk.gray(task.description) : ''),
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'cyan',
|
||||
width: process.stdout.columns ? process.stdout.columns * 0.95 : 100
|
||||
}
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform autopilot execution using PreflightChecker and TaskLoader
|
||||
*/
|
||||
private async performAutopilot(
|
||||
taskId: string,
|
||||
task: Task,
|
||||
options: AutopilotCommandOptions
|
||||
): Promise<AutopilotCommandResult> {
|
||||
// Run preflight checks
|
||||
const preflightResult = await this.runPreflightChecks(options);
|
||||
if (!preflightResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
taskId,
|
||||
task,
|
||||
error: 'Preflight checks failed',
|
||||
message: 'Please resolve the issues above before running autopilot'
|
||||
};
|
||||
}
|
||||
|
||||
// Validate task structure and get execution order
|
||||
const validationResult = await this.validateTaskStructure(taskId, task);
|
||||
if (!validationResult.success) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
// Display execution plan
|
||||
this.displayExecutionPlan(
|
||||
validationResult.task!,
|
||||
validationResult.orderedSubtasks!,
|
||||
options
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId,
|
||||
task: validationResult.task,
|
||||
message: options.dryRun
|
||||
? 'Dry run completed successfully'
|
||||
: 'Autopilot execution ready (actual execution not yet implemented)'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run preflight checks and display results
|
||||
*/
|
||||
private async runPreflightChecks(
|
||||
options: AutopilotCommandOptions
|
||||
): Promise<PreflightResult> {
|
||||
const { PreflightChecker } = await import('@tm/core');
|
||||
|
||||
console.log();
|
||||
console.log(chalk.cyan.bold('Running preflight checks...'));
|
||||
|
||||
const preflightChecker = new PreflightChecker(
|
||||
options.project || process.cwd()
|
||||
);
|
||||
const result = await preflightChecker.runAllChecks();
|
||||
|
||||
this.displayPreflightResults(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate task structure and get execution order
|
||||
*/
|
||||
private async validateTaskStructure(
|
||||
taskId: string,
|
||||
task: Task
|
||||
): Promise<AutopilotCommandResult & { orderedSubtasks?: Subtask[] }> {
|
||||
if (!this.tmCore) {
|
||||
return {
|
||||
success: false,
|
||||
taskId,
|
||||
task,
|
||||
error: 'TmCore not initialized'
|
||||
};
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.cyan.bold('Validating task structure...'));
|
||||
|
||||
const validationResult = await this.tmCore.tasks.loadAndValidate(taskId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
taskId,
|
||||
task,
|
||||
error: validationResult.errorMessage,
|
||||
message: validationResult.suggestion
|
||||
};
|
||||
}
|
||||
|
||||
const orderedSubtasks = this.tmCore.tasks.getExecutionOrder(
|
||||
validationResult.task!
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId,
|
||||
task: validationResult.task,
|
||||
orderedSubtasks
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display execution plan with subtasks and TDD workflow
|
||||
*/
|
||||
private displayExecutionPlan(
|
||||
task: Task,
|
||||
orderedSubtasks: Subtask[],
|
||||
options: AutopilotCommandOptions
|
||||
): void {
|
||||
console.log();
|
||||
console.log(chalk.green.bold('✓ All checks passed!'));
|
||||
console.log();
|
||||
console.log(chalk.cyan.bold('Execution Plan:'));
|
||||
console.log(chalk.white(`Task: ${task.title}`));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`${orderedSubtasks.length} subtasks will be executed in dependency order`
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
|
||||
// Display subtasks
|
||||
orderedSubtasks.forEach((subtask: Subtask, index: number) => {
|
||||
console.log(
|
||||
chalk.yellow(`${index + 1}. ${task.id}.${subtask.id}: ${subtask.title}`)
|
||||
);
|
||||
if (subtask.dependencies && subtask.dependencies.length > 0) {
|
||||
console.log(
|
||||
chalk.gray(` Dependencies: ${subtask.dependencies.join(', ')}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.cyan('Autopilot would execute each subtask using TDD workflow:')
|
||||
);
|
||||
console.log(chalk.gray(' 1. RED phase: Write failing test'));
|
||||
console.log(chalk.gray(' 2. GREEN phase: Implement code to pass test'));
|
||||
console.log(chalk.gray(' 3. COMMIT phase: Commit changes'));
|
||||
console.log();
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(
|
||||
chalk.yellow('This was a dry run. Use without --dry-run to execute.')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display preflight check results
|
||||
*/
|
||||
private displayPreflightResults(result: PreflightResult): void {
|
||||
const checks = [
|
||||
{ name: 'Test command', result: result.testCommand },
|
||||
{ name: 'Git working tree', result: result.gitWorkingTree },
|
||||
{ name: 'Required tools', result: result.requiredTools },
|
||||
{ name: 'Default branch', result: result.defaultBranch }
|
||||
];
|
||||
|
||||
checks.forEach((check) => {
|
||||
const icon = check.result.success ? chalk.green('✓') : chalk.red('✗');
|
||||
const status = check.result.success
|
||||
? chalk.green('PASS')
|
||||
: chalk.red('FAIL');
|
||||
console.log(`${icon} ${chalk.white(check.name)}: ${status}`);
|
||||
if (check.result.message) {
|
||||
console.log(chalk.gray(` ${check.result.message}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results based on format
|
||||
*/
|
||||
private displayResults(
|
||||
result: AutopilotCommandResult,
|
||||
options: AutopilotCommandOptions
|
||||
): void {
|
||||
const format = options.format || 'text';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
this.displayJson(result);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
this.displayTextResult(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display in JSON format
|
||||
*/
|
||||
private displayJson(result: AutopilotCommandResult): void {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display result in text format
|
||||
*/
|
||||
private displayTextResult(result: AutopilotCommandResult): void {
|
||||
if (result.success) {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green.bold('✓ Autopilot Command Completed') +
|
||||
'\n\n' +
|
||||
chalk.white(result.message || 'Execution complete'),
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'green',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.red.bold('✗ Autopilot Command Failed') +
|
||||
'\n\n' +
|
||||
chalk.white(result.error || 'Unknown error'),
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'red',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle general errors
|
||||
*/
|
||||
private handleError(error: unknown): void {
|
||||
const errorObj = error as {
|
||||
getSanitizedDetails?: () => { message: string };
|
||||
message?: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
const msg = errorObj?.getSanitizedDetails?.() ?? {
|
||||
message: errorObj?.message ?? String(error)
|
||||
};
|
||||
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
||||
|
||||
// Show stack trace in development mode or when DEBUG is set
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
if ((isDevelopment || process.env.DEBUG) && errorObj.stack) {
|
||||
console.error(chalk.gray(errorObj.stack));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last result for programmatic access
|
||||
*/
|
||||
private setLastResult(result: AutopilotCommandResult): void {
|
||||
this.lastResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last result (for programmatic usage)
|
||||
*/
|
||||
getLastResult(): AutopilotCommandResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): AutopilotCommand {
|
||||
const autopilotCommand = new AutopilotCommand(name);
|
||||
program.addCommand(autopilotCommand);
|
||||
return autopilotCommand;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { createTmCore, type Task, type TmCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core';
|
||||
import type { StorageType, Subtask } from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
@@ -28,16 +28,17 @@ export interface ShowCommandOptions {
|
||||
* Result type from show command
|
||||
*/
|
||||
export interface ShowTaskResult {
|
||||
task: Task | null;
|
||||
task: Task | Subtask | null;
|
||||
found: boolean;
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
originalTaskId?: string; // The original task ID requested (for subtasks like "104.1")
|
||||
}
|
||||
|
||||
/**
|
||||
* Result type for multiple tasks
|
||||
*/
|
||||
export interface ShowMultipleTasksResult {
|
||||
tasks: Task[];
|
||||
tasks: (Task | Subtask)[];
|
||||
notFound: string[];
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
}
|
||||
@@ -161,7 +162,8 @@ export class ShowCommand extends Command {
|
||||
return {
|
||||
task: result.task,
|
||||
found: result.task !== null,
|
||||
storageType: storageType as Exclude<StorageType, 'auto'>
|
||||
storageType: storageType as Exclude<StorageType, 'auto'>,
|
||||
originalTaskId: result.isSubtask ? taskId : undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ export class ShowCommand extends Command {
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
const tasks: Task[] = [];
|
||||
const tasks: (Task | Subtask)[] = [];
|
||||
const notFound: string[] = [];
|
||||
|
||||
// Get each task individually
|
||||
@@ -262,9 +264,11 @@ export class ShowCommand extends Command {
|
||||
console.log(); // Add spacing
|
||||
|
||||
// Use the global task details display function
|
||||
// Pass the original requested ID if it's a subtask
|
||||
displayTaskDetails(result.task, {
|
||||
statusFilter: options.status,
|
||||
showSuggestedActions: true
|
||||
showSuggestedActions: true,
|
||||
originalTaskId: result.originalTaskId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export { ContextCommand } from './commands/context.command.js';
|
||||
export { StartCommand } from './commands/start.command.js';
|
||||
export { SetStatusCommand } from './commands/set-status.command.js';
|
||||
export { ExportCommand } from './commands/export.command.js';
|
||||
export { AutopilotCommand } from './commands/autopilot.command.js';
|
||||
|
||||
// Command Registry
|
||||
export {
|
||||
|
||||
@@ -8,7 +8,7 @@ import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { marked, MarkedExtension } from 'marked';
|
||||
import { markedTerminal } from 'marked-terminal';
|
||||
import type { Task } from '@tm/core';
|
||||
import type { Subtask, Task } from '@tm/core';
|
||||
import {
|
||||
getStatusWithColor,
|
||||
getPriorityWithColor,
|
||||
@@ -74,7 +74,10 @@ export function displayTaskHeader(
|
||||
/**
|
||||
* Display task properties in a table format
|
||||
*/
|
||||
export function displayTaskProperties(task: Task): void {
|
||||
export function displayTaskProperties(
|
||||
task: Task | Subtask,
|
||||
originalTaskId?: string
|
||||
): void {
|
||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||
// Create table for task properties - simple 2-column layout
|
||||
const table = new Table({
|
||||
@@ -95,6 +98,9 @@ export function displayTaskProperties(task: Task): void {
|
||||
? task.dependencies.map((d) => String(d)).join(', ')
|
||||
: 'None';
|
||||
|
||||
// Use originalTaskId if provided (for subtasks like "104.1")
|
||||
const displayId = originalTaskId || String(task.id);
|
||||
|
||||
// Build the left column (labels) and right column (values)
|
||||
const labels = [
|
||||
chalk.cyan('ID:'),
|
||||
@@ -107,7 +113,7 @@ export function displayTaskProperties(task: Task): void {
|
||||
].join('\n');
|
||||
|
||||
const values = [
|
||||
String(task.id),
|
||||
displayId,
|
||||
task.title,
|
||||
getStatusWithColor(task.status),
|
||||
getPriorityWithColor(task.priority),
|
||||
@@ -272,19 +278,21 @@ export function displaySuggestedActions(taskId: string | number): void {
|
||||
* Display complete task details - used by both show and start commands
|
||||
*/
|
||||
export function displayTaskDetails(
|
||||
task: Task,
|
||||
task: Task | Subtask,
|
||||
options?: {
|
||||
statusFilter?: string;
|
||||
showSuggestedActions?: boolean;
|
||||
customHeader?: string;
|
||||
headerColor?: string;
|
||||
originalTaskId?: string;
|
||||
}
|
||||
): void {
|
||||
const {
|
||||
statusFilter,
|
||||
showSuggestedActions = false,
|
||||
customHeader,
|
||||
headerColor = 'blue'
|
||||
headerColor = 'blue',
|
||||
originalTaskId
|
||||
} = options || {};
|
||||
|
||||
// Display header - either custom or default
|
||||
@@ -298,11 +306,13 @@ export function displayTaskDetails(
|
||||
})
|
||||
);
|
||||
} else {
|
||||
displayTaskHeader(task.id, task.title);
|
||||
// Use originalTaskId if provided (for subtasks like "104.1")
|
||||
const displayId = originalTaskId || task.id;
|
||||
displayTaskHeader(displayId, task.title);
|
||||
}
|
||||
|
||||
// Display task properties in table format
|
||||
displayTaskProperties(task);
|
||||
displayTaskProperties(task, originalTaskId);
|
||||
|
||||
// Display implementation details if available
|
||||
if (task.details) {
|
||||
@@ -335,6 +345,7 @@ export function displayTaskDetails(
|
||||
// Display suggested actions if requested
|
||||
if (showSuggestedActions) {
|
||||
console.log(); // Empty line for spacing
|
||||
displaySuggestedActions(task.id);
|
||||
const actionTaskId = originalTaskId || task.id;
|
||||
displaySuggestedActions(actionTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import type { Task, TaskStatus, TaskPriority } from '@tm/core';
|
||||
import type { Task, TaskStatus, TaskPriority, Subtask } from '@tm/core';
|
||||
|
||||
/**
|
||||
* Get colored status display with ASCII icons (matches scripts/modules/ui.js style)
|
||||
@@ -294,7 +294,7 @@ export function formatDependenciesWithStatus(
|
||||
* Create a task table for display
|
||||
*/
|
||||
export function createTaskTable(
|
||||
tasks: Task[],
|
||||
tasks: (Task | Subtask)[],
|
||||
options?: {
|
||||
showSubtasks?: boolean;
|
||||
showComplexity?: boolean;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore, type Task } from '@tm/core';
|
||||
import { createTmCore, Subtask, type Task } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GetTaskSchema = z.object({
|
||||
@@ -66,7 +66,7 @@ export function registerGetTaskTool(server: FastMCP) {
|
||||
taskIds.map((taskId) => tmCore.tasks.get(taskId, tag))
|
||||
);
|
||||
|
||||
const tasks: Task[] = [];
|
||||
const tasks: (Task | Subtask)[] = [];
|
||||
for (const result of results) {
|
||||
if (!result.task) continue;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TaskExecutionService } from './services/task-execution-service.js';
|
||||
import { TaskLoaderService } from './services/task-loader.service.js';
|
||||
import { PreflightChecker } from './services/preflight-checker.service.js';
|
||||
|
||||
import type { Task, TaskStatus } from '../../common/types/index.js';
|
||||
import type { Subtask, Task, TaskStatus } from '../../common/types/index.js';
|
||||
import type {
|
||||
TaskListResult,
|
||||
GetTaskListOptions
|
||||
@@ -58,12 +58,16 @@ export class TasksDomain {
|
||||
* - Simple task IDs (e.g., "1", "HAM-123")
|
||||
* - Subtask IDs with dot notation (e.g., "1.2", "HAM-123.2")
|
||||
*
|
||||
* @returns Task and whether the ID represents a subtask
|
||||
* @returns Discriminated union indicating task/subtask with proper typing
|
||||
*/
|
||||
async get(
|
||||
taskId: string,
|
||||
tag?: string
|
||||
): Promise<{ task: Task | null; isSubtask: boolean }> {
|
||||
): Promise<
|
||||
| { task: Task; isSubtask: false }
|
||||
| { task: Subtask; isSubtask: true }
|
||||
| { task: null; isSubtask: boolean }
|
||||
> {
|
||||
// Parse ID - check for dot notation (subtask)
|
||||
const parts = taskId.split('.');
|
||||
const parentId = parts[0];
|
||||
@@ -75,13 +79,17 @@ export class TasksDomain {
|
||||
return { task: null, isSubtask: false };
|
||||
}
|
||||
|
||||
// Handle subtask notation (1.2, HAM-123.2)
|
||||
// Handle subtask notation (1.2)
|
||||
if (subtaskIdPart && task.subtasks) {
|
||||
const subtask = task.subtasks.find(
|
||||
(st) => String(st.id) === subtaskIdPart
|
||||
);
|
||||
// Return parent task with isSubtask flag
|
||||
return { task, isSubtask: !!subtask };
|
||||
if (subtask) {
|
||||
// Return the actual subtask with properly typed result
|
||||
return { task: subtask, isSubtask: true };
|
||||
}
|
||||
// Subtask ID provided but not found
|
||||
return { task: null, isSubtask: true };
|
||||
}
|
||||
|
||||
// It's a regular task
|
||||
|
||||
Reference in New Issue
Block a user