/** * Task execution logic */ import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../config/index.js'; import { TaskRunner, type TaskInfo } from '../task/index.js'; import { createSharedClone } from '../task/clone.js'; import { autoCommitAndPush } from '../task/autoCommit.js'; import { summarizeTaskName } from '../task/summarize.js'; import { header, info, error, success, status, } from '../utils/ui.js'; import { createLogger } from '../utils/debug.js'; import { getErrorMessage } from '../utils/error.js'; import { executeWorkflow } from './workflowExecution.js'; import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; import type { ProviderType } from '../providers/index.js'; const log = createLogger('task'); export interface TaskExecutionOptions { provider?: ProviderType; model?: string; } /** * Execute a single task with workflow. * * @param task - Task content * @param cwd - Working directory (may be a clone path) * @param workflowIdentifier - Workflow name or path (auto-detected by isWorkflowPath) * @param projectCwd - Project root (where .takt/ lives). Defaults to cwd. */ export async function executeTask( task: string, cwd: string, workflowIdentifier: string = DEFAULT_WORKFLOW_NAME, projectCwd?: string, options?: TaskExecutionOptions ): Promise { const effectiveProjectCwd = projectCwd || cwd; const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, effectiveProjectCwd); if (!workflowConfig) { if (isWorkflowPath(workflowIdentifier)) { error(`Workflow file not found: ${workflowIdentifier}`); } else { error(`Workflow "${workflowIdentifier}" not found.`); info('Available workflows are in ~/.takt/workflows/ or .takt/workflows/'); info('Use "takt switch" to select a workflow.'); } return false; } log.debug('Running workflow', { name: workflowConfig.name, steps: workflowConfig.steps.map((s: { name: string }) => s.name), }); const globalConfig = loadGlobalConfig(); const result = await executeWorkflow(workflowConfig, task, cwd, { projectCwd, language: globalConfig.language, provider: options?.provider, model: options?.model, }); return result.success; } /** * Execute a task: resolve clone → run workflow → auto-commit+push → remove clone → record completion. * * Shared by runAllTasks() and watchTasks() to avoid duplicated * resolve → execute → autoCommit → complete logic. * * @returns true if the task succeeded */ export async function executeAndCompleteTask( task: TaskInfo, taskRunner: TaskRunner, cwd: string, workflowName: string, options?: TaskExecutionOptions, ): Promise { const startedAt = new Date().toISOString(); const executionLog: string[] = []; try { const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName); // cwd is always the project root; pass it as projectCwd so reports/sessions go there const taskSuccess = await executeTask(task.content, execCwd, execWorkflow, cwd, options); const completedAt = new Date().toISOString(); if (taskSuccess && isWorktree) { const commitResult = autoCommitAndPush(execCwd, task.name, cwd); if (commitResult.success && commitResult.commitHash) { info(`Auto-committed & pushed: ${commitResult.commitHash}`); } else if (!commitResult.success) { error(`Auto-commit failed: ${commitResult.message}`); } } const taskResult = { task, success: taskSuccess, response: taskSuccess ? 'Task completed successfully' : 'Task failed', executionLog, startedAt, completedAt, }; if (taskSuccess) { taskRunner.completeTask(taskResult); success(`Task "${task.name}" completed`); } else { taskRunner.failTask(taskResult); error(`Task "${task.name}" failed`); } return taskSuccess; } catch (err) { const completedAt = new Date().toISOString(); taskRunner.failTask({ task, success: false, response: getErrorMessage(err), executionLog, startedAt, completedAt, }); error(`Task "${task.name}" error: ${getErrorMessage(err)}`); return false; } } /** * Run all pending tasks from .takt/tasks/ * * タスクを動的に取得する。各タスク実行前に次のタスクを取得するため、 * 実行中にタスクファイルが追加・削除されても反映される。 */ export async function runAllTasks( cwd: string, workflowName: string = DEFAULT_WORKFLOW_NAME, options?: TaskExecutionOptions, ): Promise { const taskRunner = new TaskRunner(cwd); // 最初のタスクを取得 let task = taskRunner.getNextTask(); if (!task) { info('No pending tasks in .takt/tasks/'); info('Create task files as .takt/tasks/*.yaml or use takt add'); return; } header('Running tasks'); let successCount = 0; let failCount = 0; while (task) { console.log(); info(`=== Task: ${task.name} ===`); console.log(); const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options); if (taskSuccess) { successCount++; } else { failCount++; } // 次のタスクを動的に取得(新しく追加されたタスクも含む) task = taskRunner.getNextTask(); } const totalCount = successCount + failCount; console.log(); header('Tasks Summary'); status('Total', String(totalCount)); status('Success', String(successCount), successCount === totalCount ? 'green' : undefined); if (failCount > 0) { status('Failed', String(failCount), 'red'); } } /** * Resolve execution directory and workflow from task data. * If the task has worktree settings, create a shared clone and use it as cwd. * Task name is summarized to English by AI for use in branch/clone names. */ export async function resolveTaskExecution( task: TaskInfo, defaultCwd: string, defaultWorkflow: string ): Promise<{ execCwd: string; execWorkflow: string; isWorktree: boolean; branch?: string }> { const data = task.data; // No structured data: use defaults if (!data) { return { execCwd: defaultCwd, execWorkflow: defaultWorkflow, isWorktree: false }; } let execCwd = defaultCwd; let isWorktree = false; let branch: string | undefined; // Handle worktree (now creates a shared clone) if (data.worktree) { // Summarize task content to English slug using AI info('Generating branch name...'); const taskSlug = await summarizeTaskName(task.content, { cwd: defaultCwd }); const result = createSharedClone(defaultCwd, { worktree: data.worktree, branch: data.branch, taskSlug, }); execCwd = result.path; branch = result.branch; isWorktree = true; info(`Clone created: ${result.path} (branch: ${result.branch})`); } // Handle workflow override const execWorkflow = data.workflow || defaultWorkflow; return { execCwd, execWorkflow, isWorktree, branch }; }