/** * @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 { 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 { 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 { 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 { 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 { // 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, tag?: string ): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { return this.executionService.getNextAvailableTask(); } /** * Check if a task can be started */ async canStart(taskId: string, force?: boolean): Promise { return this.executionService.canStartTask(taskId, force); } // ========== Task Loading & Validation ========== /** * Load and validate a task for execution */ async loadAndValidate(taskId: string): Promise { 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 { 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 { 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 { // 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 { await this.taskService.close(); } }