/** * TAKT タスク実行モード * * .takt/tasks/ ディレクトリ内のタスクファイルを読み込み、 * 順番に実行してレポートを生成する。 * * Supports both .md (plain text) and .yaml/.yml (structured) task files. * * 使用方法: * /task # タスク一覧を表示 * /task run # 次のタスクを実行 * /task run # 指定したタスクを実行 * /task list # タスク一覧を表示 */ import * as fs from 'node:fs'; import * as path from 'node:path'; import { parseTaskFiles, parseTaskFile, type ParsedTask } from './parser.js'; import type { TaskFileData } from './schema.js'; /** タスク情報 */ export interface TaskInfo { filePath: string; name: string; content: string; createdAt: string; /** Structured data from YAML files (null for .md files) */ data: TaskFileData | null; } /** タスク実行結果 */ export interface TaskResult { task: TaskInfo; success: boolean; response: string; executionLog: string[]; startedAt: string; completedAt: string; } /** * タスク実行管理クラス */ export class TaskRunner { private projectDir: string; private tasksDir: string; private completedDir: string; private failedDir: string; constructor(projectDir: string) { this.projectDir = projectDir; this.tasksDir = path.join(projectDir, '.takt', 'tasks'); this.completedDir = path.join(projectDir, '.takt', 'completed'); this.failedDir = path.join(projectDir, '.takt', 'failed'); } /** ディレクトリ構造を作成 */ ensureDirs(): void { fs.mkdirSync(this.tasksDir, { recursive: true }); fs.mkdirSync(this.completedDir, { recursive: true }); fs.mkdirSync(this.failedDir, { recursive: true }); } /** タスクディレクトリのパスを取得 */ getTasksDir(): string { return this.tasksDir; } /** * タスク一覧を取得 * @returns タスク情報のリスト(ファイル名順) */ listTasks(): TaskInfo[] { this.ensureDirs(); try { const parsed = parseTaskFiles(this.tasksDir); return parsed.map(toTaskInfo); } catch (err) { const nodeErr = err as NodeJS.ErrnoException; if (nodeErr.code !== 'ENOENT') { throw err; // 予期しないエラーは再スロー } // ENOENT は許容(ディレクトリ未作成) return []; } } /** * 指定した名前のタスクを取得 * Searches for .yaml, .yml, and .md files in that order. */ getTask(name: string): TaskInfo | null { this.ensureDirs(); const extensions = ['.yaml', '.yml', '.md']; for (const ext of extensions) { const filePath = path.join(this.tasksDir, `${name}${ext}`); if (!fs.existsSync(filePath)) { continue; } try { const parsed = parseTaskFile(filePath); return toTaskInfo(parsed); } catch { // Parse error: skip this extension } } return null; } /** * 次に実行すべきタスクを取得(最初のタスク) */ getNextTask(): TaskInfo | null { const tasks = this.listTasks(); return tasks[0] ?? null; } /** * タスクを完了としてマーク * * タスクファイルを .takt/completed に移動し、 * レポートファイルを作成する。 * * @returns レポートファイルのパス */ completeTask(result: TaskResult): string { if (!result.success) { throw new Error('Cannot complete a failed task. Use failTask() instead.'); } return this.moveTask(result, this.completedDir); } /** * タスクを失敗としてマーク * * タスクファイルを .takt/failed に移動し、 * レポートファイルを作成する。 * * @returns レポートファイルのパス */ failTask(result: TaskResult): string { return this.moveTask(result, this.failedDir); } /** * タスクファイルを指定ディレクトリに移動し、レポート・ログを生成する */ private moveTask(result: TaskResult, targetDir: string): string { this.ensureDirs(); // タイムスタンプを生成 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); // ターゲットディレクトリにサブディレクトリを作成 const taskTargetDir = path.join( targetDir, `${timestamp}_${result.task.name}` ); fs.mkdirSync(taskTargetDir, { recursive: true }); // 元のタスクファイルを移動(元の拡張子を保持) const originalExt = path.extname(result.task.filePath); const movedTaskFile = path.join(taskTargetDir, `${result.task.name}${originalExt}`); fs.renameSync(result.task.filePath, movedTaskFile); // レポートを生成 const reportFile = path.join(taskTargetDir, 'report.md'); const reportContent = this.generateReport(result); fs.writeFileSync(reportFile, reportContent, 'utf-8'); // ログを保存 const logFile = path.join(taskTargetDir, 'log.json'); const logData = { taskName: result.task.name, success: result.success, startedAt: result.startedAt, completedAt: result.completedAt, executionLog: result.executionLog, response: result.response, }; fs.writeFileSync(logFile, JSON.stringify(logData, null, 2), 'utf-8'); return reportFile; } /** * レポートを生成 */ private generateReport(result: TaskResult): string { const status = result.success ? '成功' : '失敗'; return `# タスク実行レポート ## 基本情報 - タスク名: ${result.task.name} - ステータス: ${status} - 開始時刻: ${result.startedAt} - 完了時刻: ${result.completedAt} ## 元のタスク \`\`\`markdown ${result.task.content} \`\`\` ## 実行結果 ${result.response} --- *Generated by TAKT Task Runner* `; } } /** Convert ParsedTask to TaskInfo */ function toTaskInfo(parsed: ParsedTask): TaskInfo { return { filePath: parsed.filePath, name: parsed.name, content: parsed.content, createdAt: parsed.createdAt, data: parsed.data, }; }