mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-29 22:02:04 +00:00
489 lines
14 KiB
TypeScript
489 lines
14 KiB
TypeScript
/**
|
|
* @fileoverview Tasks Domain Facade
|
|
* Public API for task-related operations
|
|
*/
|
|
|
|
import type { ConfigManager } from '../config/managers/config-manager.js';
|
|
import type { AuthDomain } from '../auth/auth-domain.js';
|
|
import { BriefsDomain } from '../briefs/briefs-domain.js';
|
|
import { TaskService } from './services/task-service.js';
|
|
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 { TagService } from './services/tag.service.js';
|
|
import {
|
|
TaskFileGeneratorService,
|
|
type GenerateTaskFilesOptions,
|
|
type GenerateTaskFilesResult
|
|
} from './services/task-file-generator.service.js';
|
|
import type {
|
|
CreateTagOptions,
|
|
DeleteTagOptions,
|
|
CopyTagOptions
|
|
} from './services/tag.service.js';
|
|
|
|
import type { Subtask, Task, TaskStatus } from '../../common/types/index.js';
|
|
import type {
|
|
TaskListResult,
|
|
GetTaskListOptions
|
|
} from './services/task-service.js';
|
|
import type {
|
|
StartTaskOptions,
|
|
StartTaskResult
|
|
} from './services/task-execution-service.js';
|
|
import type {
|
|
PreflightResult
|
|
} from './services/preflight-checker.service.js';
|
|
import type { TaskValidationResult } from './services/task-loader.service.js';
|
|
import type { ExpandTaskResult } from '../integration/services/task-expansion.service.js';
|
|
import type {
|
|
WatchEvent,
|
|
WatchOptions,
|
|
WatchSubscription
|
|
} from '../../common/interfaces/storage.interface.js';
|
|
|
|
/**
|
|
* Tasks Domain - Unified API for all task operations
|
|
*/
|
|
export class TasksDomain {
|
|
private taskService: TaskService;
|
|
private executionService: TaskExecutionService;
|
|
private loaderService: TaskLoaderService;
|
|
private preflightChecker: PreflightChecker;
|
|
private briefsDomain: BriefsDomain;
|
|
private tagService!: TagService;
|
|
private taskFileGenerator!: TaskFileGeneratorService;
|
|
private projectRoot: string;
|
|
private configManager: ConfigManager;
|
|
|
|
constructor(configManager: ConfigManager, _authDomain?: AuthDomain) {
|
|
this.projectRoot = configManager.getProjectRoot();
|
|
this.configManager = configManager;
|
|
this.taskService = new TaskService(configManager);
|
|
this.executionService = new TaskExecutionService(this.taskService);
|
|
this.loaderService = new TaskLoaderService(this.taskService);
|
|
this.preflightChecker = new PreflightChecker(this.projectRoot);
|
|
this.briefsDomain = new BriefsDomain();
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
await this.taskService.initialize();
|
|
|
|
// TagService needs storage - get it from TaskService AFTER initialization
|
|
this.tagService = new TagService(this.taskService.getStorage());
|
|
|
|
// TaskFileGeneratorService needs storage and config for active tag
|
|
this.taskFileGenerator = new TaskFileGeneratorService(
|
|
this.taskService.getStorage(),
|
|
this.projectRoot,
|
|
this.configManager
|
|
);
|
|
}
|
|
|
|
// ========== Task Retrieval ==========
|
|
|
|
/**
|
|
* Get list of tasks with filtering
|
|
*/
|
|
async list(options?: GetTaskListOptions): Promise<TaskListResult> {
|
|
return this.taskService.getTaskList(options);
|
|
}
|
|
|
|
/**
|
|
* Get a single task by ID
|
|
* Automatically handles all ID formats:
|
|
* - Simple task IDs (e.g., "1", "HAM-123")
|
|
* - Subtask IDs with dot notation (e.g., "1.2", "HAM-123.2")
|
|
*
|
|
* @returns Discriminated union indicating task/subtask with proper typing
|
|
*/
|
|
async get(
|
|
taskId: string,
|
|
tag?: string
|
|
): 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];
|
|
const subtaskIdPart = parts[1];
|
|
|
|
// Fetch the task
|
|
const task = await this.taskService.getTask(parentId, tag);
|
|
if (!task) {
|
|
return { task: null, isSubtask: false };
|
|
}
|
|
|
|
// Handle subtask notation (1.2)
|
|
if (subtaskIdPart && task.subtasks) {
|
|
const subtask = task.subtasks.find(
|
|
(st) => String(st.id) === subtaskIdPart
|
|
);
|
|
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
|
|
return { task, isSubtask: false };
|
|
}
|
|
|
|
/**
|
|
* Get tasks by status
|
|
*/
|
|
async getByStatus(status: TaskStatus, tag?: string): Promise<Task[]> {
|
|
return this.taskService.getTasksByStatus(status, tag);
|
|
}
|
|
|
|
/**
|
|
* Get task statistics
|
|
*/
|
|
async getStats(tag?: string) {
|
|
return this.taskService.getTaskStats(tag);
|
|
}
|
|
|
|
/**
|
|
* Get next available task to work on
|
|
*/
|
|
async getNext(tag?: string): Promise<Task | null> {
|
|
return this.taskService.getNextTask(tag);
|
|
}
|
|
|
|
/**
|
|
* Get count of tasks and their subtasks matching a status
|
|
* Useful for determining work remaining, progress tracking, or setting loop iterations
|
|
*
|
|
* @param status - Task status to count (e.g., 'pending', 'done', 'in-progress')
|
|
* @param tag - Optional tag to filter tasks
|
|
* @returns Total count of matching tasks + matching subtasks
|
|
*/
|
|
async getCount(status: TaskStatus, tag?: string): Promise<number> {
|
|
// Fetch ALL tasks to ensure we count subtasks across all parent tasks
|
|
// (a parent task may have different status than its subtasks)
|
|
const result = await this.list({ tag });
|
|
|
|
let count = 0;
|
|
for (const task of result.tasks) {
|
|
// Count the task if it matches the status
|
|
if (task.status === status) {
|
|
count++;
|
|
}
|
|
|
|
// Count subtasks with matching status
|
|
if (task.subtasks && task.subtasks.length > 0) {
|
|
for (const subtask of task.subtasks) {
|
|
// For pending, also count subtasks without status (default to pending)
|
|
if (subtask.status === status || (status === 'pending' && !subtask.status)) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// ========== Task Status Management ==========
|
|
|
|
/**
|
|
* Update task with new data (direct structural update)
|
|
* @param taskId - Task ID (supports numeric, alphanumeric like TAS-49, and subtask IDs like 1.2)
|
|
* @param updates - Partial task object with fields to update
|
|
* @param tag - Optional tag context
|
|
*/
|
|
async update(
|
|
taskId: string | number,
|
|
updates: Partial<Task>,
|
|
tag?: string
|
|
): Promise<void> {
|
|
return this.taskService.updateTask(taskId, updates, tag);
|
|
}
|
|
|
|
/**
|
|
* Update task using AI-powered prompt (natural language update)
|
|
* @param taskId - Task ID (supports numeric, alphanumeric like TAS-49, and subtask IDs like 1.2)
|
|
* @param prompt - Natural language prompt describing the update
|
|
* @param tag - Optional tag context
|
|
* @param options - Optional update options
|
|
* @param options.useResearch - Use research AI for file storage updates
|
|
* @param options.mode - Update mode for API storage: 'append', 'update', or 'rewrite'
|
|
*/
|
|
async updateWithPrompt(
|
|
taskId: string | number,
|
|
prompt: string,
|
|
tag?: string,
|
|
options?: { mode?: 'append' | 'update' | 'rewrite'; useResearch?: boolean }
|
|
): Promise<void> {
|
|
return this.taskService.updateTaskWithPrompt(taskId, prompt, tag, options);
|
|
}
|
|
|
|
/**
|
|
* Expand task into subtasks using AI
|
|
* @returns ExpandTaskResult when using API storage, void for file storage
|
|
*/
|
|
async expand(
|
|
taskId: string | number,
|
|
tag?: string,
|
|
options?: {
|
|
numSubtasks?: number;
|
|
useResearch?: boolean;
|
|
additionalContext?: string;
|
|
force?: boolean;
|
|
}
|
|
): Promise<ExpandTaskResult | void> {
|
|
return this.taskService.expandTaskWithPrompt(taskId, tag, options);
|
|
}
|
|
|
|
/**
|
|
* Update task status
|
|
*/
|
|
async updateStatus(taskId: string, status: TaskStatus, tag?: string) {
|
|
return this.taskService.updateTaskStatus(taskId, status, tag);
|
|
}
|
|
|
|
/**
|
|
* Set active tag
|
|
*/
|
|
async setActiveTag(tag: string): Promise<void> {
|
|
return this.taskService.setActiveTag(tag);
|
|
}
|
|
|
|
/**
|
|
* Resolve a brief by ID, name, or partial match without switching
|
|
* Returns the full brief object
|
|
*
|
|
* Supports:
|
|
* - Full UUID
|
|
* - Last 8 characters of UUID
|
|
* - Brief name (exact or partial match)
|
|
*
|
|
* Only works with API storage (briefs).
|
|
*
|
|
* @param briefIdOrName - Brief identifier
|
|
* @param orgId - Optional organization ID
|
|
* @returns The resolved brief object
|
|
*/
|
|
async resolveBrief(briefIdOrName: string, orgId?: string): Promise<any> {
|
|
return this.briefsDomain.resolveBrief(briefIdOrName, orgId);
|
|
}
|
|
|
|
/**
|
|
* Switch to a different tag/brief context
|
|
* For file storage: updates active tag in state
|
|
* For API storage: looks up brief by name and updates auth context
|
|
*/
|
|
async switchTag(tagName: string): Promise<void> {
|
|
const storageType = this.taskService.getStorageType();
|
|
|
|
if (storageType === 'file') {
|
|
await this.setActiveTag(tagName);
|
|
} else {
|
|
await this.briefsDomain.switchBrief(tagName);
|
|
}
|
|
}
|
|
|
|
// ========== Task Execution ==========
|
|
|
|
/**
|
|
* Start working on a task
|
|
*/
|
|
async start(taskId: string, options?: StartTaskOptions): Promise<StartTaskResult> {
|
|
return this.executionService.startTask(taskId, options);
|
|
}
|
|
|
|
/**
|
|
* Check for in-progress conflicts
|
|
*/
|
|
async checkInProgressConflicts(taskId: string) {
|
|
return this.executionService.checkInProgressConflicts(taskId);
|
|
}
|
|
|
|
/**
|
|
* Get next available task (from execution service)
|
|
*/
|
|
async getNextAvailable(): Promise<string | null> {
|
|
return this.executionService.getNextAvailableTask();
|
|
}
|
|
|
|
/**
|
|
* Check if a task can be started
|
|
*/
|
|
async canStart(taskId: string, force?: boolean): Promise<boolean> {
|
|
return this.executionService.canStartTask(taskId, force);
|
|
}
|
|
|
|
// ========== Task Loading & Validation ==========
|
|
|
|
/**
|
|
* Load and validate a task for execution
|
|
*/
|
|
async loadAndValidate(taskId: string): Promise<TaskValidationResult> {
|
|
return this.loaderService.loadAndValidateTask(taskId);
|
|
}
|
|
|
|
/**
|
|
* Get execution order for subtasks
|
|
*/
|
|
getExecutionOrder(task: Task) {
|
|
return this.loaderService.getExecutionOrder(task);
|
|
}
|
|
|
|
// ========== Preflight Checks ==========
|
|
|
|
/**
|
|
* Run all preflight checks
|
|
*/
|
|
async runPreflightChecks(): Promise<PreflightResult> {
|
|
return this.preflightChecker.runAllChecks();
|
|
}
|
|
|
|
/**
|
|
* Detect test command
|
|
*/
|
|
async detectTestCommand() {
|
|
return this.preflightChecker.detectTestCommand();
|
|
}
|
|
|
|
/**
|
|
* Check git working tree
|
|
*/
|
|
async checkGitWorkingTree() {
|
|
return this.preflightChecker.checkGitWorkingTree();
|
|
}
|
|
|
|
/**
|
|
* Validate required tools
|
|
*/
|
|
async validateRequiredTools() {
|
|
return this.preflightChecker.validateRequiredTools();
|
|
}
|
|
|
|
/**
|
|
* Detect default git branch
|
|
*/
|
|
async detectDefaultBranch() {
|
|
return this.preflightChecker.detectDefaultBranch();
|
|
}
|
|
|
|
// ========== Tag Management ==========
|
|
|
|
/**
|
|
* Create a new tag
|
|
* For file storage: creates tag locally with optional task copying
|
|
* For API storage: throws error (client should redirect to web UI)
|
|
*/
|
|
async createTag(name: string, options?: CreateTagOptions) {
|
|
return this.tagService.createTag(name, options);
|
|
}
|
|
|
|
/**
|
|
* Delete an existing tag
|
|
* Cannot delete master tag
|
|
* For file storage: deletes tag locally
|
|
* For API storage: throws error (client should redirect to web UI)
|
|
*/
|
|
async deleteTag(name: string, options?: DeleteTagOptions) {
|
|
return this.tagService.deleteTag(name, options);
|
|
}
|
|
|
|
/**
|
|
* Rename an existing tag
|
|
* Cannot rename master tag
|
|
* For file storage: renames tag locally
|
|
* For API storage: throws error (client should redirect to web UI)
|
|
*/
|
|
async renameTag(oldName: string, newName: string) {
|
|
return this.tagService.renameTag(oldName, newName);
|
|
}
|
|
|
|
/**
|
|
* Copy an existing tag to create a new tag with the same tasks
|
|
* For file storage: copies tag locally
|
|
* For API storage: throws error (client should show alternative)
|
|
*/
|
|
async copyTag(source: string, target: string, options?: CopyTagOptions) {
|
|
return this.tagService.copyTag(source, target, options);
|
|
}
|
|
|
|
/**
|
|
* Get all tags with detailed statistics including task counts
|
|
* For API storage, returns briefs with task counts
|
|
* For file storage, returns tags from tasks.json with counts
|
|
*/
|
|
async getTagsWithStats() {
|
|
return this.tagService.getTagsWithStats();
|
|
}
|
|
|
|
// ========== Storage Information ==========
|
|
|
|
/**
|
|
* Get the resolved storage type (actual type being used at runtime)
|
|
*/
|
|
getStorageType(): 'file' | 'api' {
|
|
return this.taskService.getStorageType();
|
|
}
|
|
|
|
// ========== Watch ==========
|
|
|
|
/**
|
|
* Watch for changes to tasks
|
|
* For file storage: uses fs.watch on tasks.json with debouncing
|
|
* For API storage: uses Supabase Realtime subscriptions
|
|
*
|
|
* @param callback - Function called when tasks change
|
|
* @param options - Watch options (debounce, tag)
|
|
* @returns Subscription handle with unsubscribe method
|
|
*/
|
|
async watch(
|
|
callback: (event: WatchEvent) => void,
|
|
options?: WatchOptions
|
|
): Promise<WatchSubscription> {
|
|
const storage = this.taskService.getStorage();
|
|
return storage.watch(callback, options);
|
|
}
|
|
|
|
// ========== Task File Generation ==========
|
|
|
|
/**
|
|
* Generate individual task markdown files from tasks.json
|
|
* This writes .md files for each task in the tasks directory.
|
|
*
|
|
* Note: Only applicable for file storage. API storage throws an error.
|
|
*
|
|
* @param options - Generation options (tag, outputDir)
|
|
* @returns Result with count of generated files and cleanup info
|
|
*/
|
|
async generateTaskFiles(
|
|
options?: GenerateTaskFilesOptions
|
|
): Promise<GenerateTaskFilesResult> {
|
|
// Only file storage supports task file generation
|
|
if (this.getStorageType() === 'api') {
|
|
return {
|
|
success: false,
|
|
count: 0,
|
|
directory: '',
|
|
orphanedFilesRemoved: 0,
|
|
error:
|
|
'Task file generation is only available for local file storage. API storage manages tasks in the cloud.'
|
|
};
|
|
}
|
|
|
|
return this.taskFileGenerator.generateTaskFiles(options);
|
|
}
|
|
|
|
// ========== Cleanup ==========
|
|
|
|
/**
|
|
* Close and cleanup resources
|
|
* Releases file locks and other storage resources
|
|
*/
|
|
async close(): Promise<void> {
|
|
await this.taskService.close();
|
|
}
|
|
}
|