takt/src/task/runner.ts

233 lines
6.1 KiB
TypeScript

/**
* TAKT タスク実行モード
*
* .takt/tasks/ ディレクトリ内のタスクファイルを読み込み、
* 順番に実行してレポートを生成する。
*
* Supports both .md (plain text) and .yaml/.yml (structured) task files.
*
* 使用方法:
* /task # タスク一覧を表示
* /task run # 次のタスクを実行
* /task run <filename> # 指定したタスクを実行
* /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,
};
}