interactive.tsからsummary/runSelector/runSessionReader/selectorUtilsを分離し、 run session参照をrouting層からinstructMode層に移動。instructBranchで新タスク 作成の代わりに既存タスクのrequeueを使用する方式に変更。worktree確認プロンプトを 廃止し常時有効化。
143 lines
4.5 KiB
TypeScript
143 lines
4.5 KiB
TypeScript
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { TaskFileSchema, type TaskFileData, type TaskRecord } from './schema.js';
|
|
import type { TaskInfo, TaskListItem } from './types.js';
|
|
|
|
function firstLine(content: string): string {
|
|
return content.trim().split('\n')[0]?.slice(0, 80) ?? '';
|
|
}
|
|
|
|
function toDisplayPath(projectDir: string, targetPath: string): string {
|
|
const relativePath = path.relative(projectDir, targetPath);
|
|
if (!relativePath || relativePath.startsWith('..')) {
|
|
return targetPath;
|
|
}
|
|
return relativePath;
|
|
}
|
|
|
|
function buildTaskDirInstruction(projectDir: string, taskDirPath: string, orderFilePath: string): string {
|
|
const displayTaskDir = toDisplayPath(projectDir, taskDirPath);
|
|
const displayOrderFile = toDisplayPath(projectDir, orderFilePath);
|
|
return [
|
|
`Implement using only the files in \`${displayTaskDir}\`.`,
|
|
`Primary spec: \`${displayOrderFile}\`.`,
|
|
'Use report files in Report Directory as primary execution history.',
|
|
'Do not rely on previous response or conversation summary.',
|
|
].join('\n');
|
|
}
|
|
|
|
export function resolveTaskContent(projectDir: string, task: TaskRecord): string {
|
|
if (task.content) {
|
|
return task.content;
|
|
}
|
|
if (task.task_dir) {
|
|
const taskDirPath = path.join(projectDir, task.task_dir);
|
|
const orderFilePath = path.join(taskDirPath, 'order.md');
|
|
if (!fs.existsSync(orderFilePath)) {
|
|
throw new Error(`Task spec file is missing: ${orderFilePath}`);
|
|
}
|
|
return buildTaskDirInstruction(projectDir, taskDirPath, orderFilePath);
|
|
}
|
|
if (!task.content_file) {
|
|
throw new Error(`Task content is missing: ${task.name}`);
|
|
}
|
|
|
|
const contentPath = path.isAbsolute(task.content_file)
|
|
? task.content_file
|
|
: path.join(projectDir, task.content_file);
|
|
return fs.readFileSync(contentPath, 'utf-8');
|
|
}
|
|
|
|
export function toTaskData(projectDir: string, task: TaskRecord): TaskFileData {
|
|
return TaskFileSchema.parse({
|
|
task: resolveTaskContent(projectDir, task),
|
|
worktree: task.worktree,
|
|
branch: task.branch,
|
|
piece: task.piece,
|
|
issue: task.issue,
|
|
start_movement: task.start_movement,
|
|
retry_note: task.retry_note,
|
|
auto_pr: task.auto_pr,
|
|
});
|
|
}
|
|
|
|
export function toTaskInfo(projectDir: string, tasksFile: string, task: TaskRecord): TaskInfo {
|
|
const content = resolveTaskContent(projectDir, task);
|
|
return {
|
|
filePath: tasksFile,
|
|
name: task.name,
|
|
content,
|
|
taskDir: task.task_dir,
|
|
createdAt: task.created_at,
|
|
status: task.status,
|
|
worktreePath: task.worktree_path,
|
|
data: TaskFileSchema.parse({
|
|
task: content,
|
|
worktree: task.worktree,
|
|
branch: task.branch,
|
|
piece: task.piece,
|
|
issue: task.issue,
|
|
start_movement: task.start_movement,
|
|
retry_note: task.retry_note,
|
|
auto_pr: task.auto_pr,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function toPendingTaskItem(projectDir: string, tasksFile: string, task: TaskRecord): TaskListItem {
|
|
return {
|
|
kind: 'pending',
|
|
...toBaseTaskListItem(projectDir, tasksFile, task),
|
|
};
|
|
}
|
|
|
|
export function toFailedTaskItem(projectDir: string, tasksFile: string, task: TaskRecord): TaskListItem {
|
|
return {
|
|
kind: 'failed',
|
|
...toBaseTaskListItem(projectDir, tasksFile, task),
|
|
failure: task.failure,
|
|
};
|
|
}
|
|
|
|
function toRunningTaskItem(projectDir: string, tasksFile: string, task: TaskRecord): TaskListItem {
|
|
return {
|
|
kind: 'running',
|
|
...toBaseTaskListItem(projectDir, tasksFile, task),
|
|
};
|
|
}
|
|
|
|
function toCompletedTaskItem(projectDir: string, tasksFile: string, task: TaskRecord): TaskListItem {
|
|
return {
|
|
kind: 'completed',
|
|
...toBaseTaskListItem(projectDir, tasksFile, task),
|
|
};
|
|
}
|
|
|
|
function toBaseTaskListItem(projectDir: string, tasksFile: string, task: TaskRecord): Omit<TaskListItem, 'kind' | 'failure'> {
|
|
return {
|
|
name: task.name,
|
|
createdAt: task.created_at,
|
|
filePath: tasksFile,
|
|
content: firstLine(resolveTaskContent(projectDir, task)),
|
|
branch: task.branch,
|
|
worktreePath: task.worktree_path,
|
|
startedAt: task.started_at ?? undefined,
|
|
completedAt: task.completed_at ?? undefined,
|
|
ownerPid: task.owner_pid ?? undefined,
|
|
data: toTaskData(projectDir, task),
|
|
};
|
|
}
|
|
|
|
export function toTaskListItem(projectDir: string, tasksFile: string, task: TaskRecord): TaskListItem {
|
|
switch (task.status) {
|
|
case 'pending':
|
|
return toPendingTaskItem(projectDir, tasksFile, task);
|
|
case 'running':
|
|
return toRunningTaskItem(projectDir, tasksFile, task);
|
|
case 'completed':
|
|
return toCompletedTaskItem(projectDir, tasksFile, task);
|
|
case 'failed':
|
|
return toFailedTaskItem(projectDir, tasksFile, task);
|
|
}
|
|
}
|