takt/src/features/tasks/execute/resolveTask.ts
nrs 4f02c20c1d
Merge pull request #465 from nrslib/takt/420/remove-default-piece-switch
feat: デフォルトピースの概念と takt switch コマンドを削除
2026-03-04 18:02:28 +09:00

186 lines
6.1 KiB
TypeScript

import * as fs from 'node:fs';
import * as path from 'node:path';
import { resolvePieceConfigValue } from '../../../infra/config/index.js';
import { type TaskInfo, buildTaskInstruction, createSharedClone, resolveBaseBranch, summarizeTaskName } from '../../../infra/task/index.js';
import { getGitProvider, type Issue } from '../../../infra/git/index.js';
import { withProgress } from '../../../shared/ui/index.js';
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
const log = createLogger('task');
function resolveTaskDataBaseBranch(taskData: TaskInfo['data']): string | undefined {
return taskData?.base_branch;
}
function resolveTaskBaseBranch(projectDir: string, taskData: TaskInfo['data']): string {
const preferredBaseBranch = resolveTaskDataBaseBranch(taskData);
return resolveBaseBranch(projectDir, preferredBaseBranch).branch;
}
export interface ResolvedTaskExecution {
execCwd: string;
execPiece: string;
isWorktree: boolean;
taskPrompt?: string;
reportDirName?: string;
branch?: string;
worktreePath?: string;
baseBranch?: string;
startMovement?: string;
retryNote?: string;
autoPr: boolean;
draftPr: boolean;
issueNumber?: number;
maxMovementsOverride?: number;
initialIterationOverride?: number;
}
function stageTaskSpecForExecution(
projectCwd: string,
execCwd: string,
taskDir: string,
reportDirName: string,
): string {
const sourceOrderPath = path.join(projectCwd, taskDir, 'order.md');
if (!fs.existsSync(sourceOrderPath)) {
throw new Error(`Task spec file is missing: ${sourceOrderPath}`);
}
const targetTaskDir = path.join(execCwd, '.takt', 'runs', reportDirName, 'context', 'task');
const targetOrderPath = path.join(targetTaskDir, 'order.md');
fs.mkdirSync(targetTaskDir, { recursive: true });
fs.copyFileSync(sourceOrderPath, targetOrderPath);
const runTaskDir = `.takt/runs/${reportDirName}/context/task`;
const orderFile = `${runTaskDir}/order.md`;
return buildTaskInstruction(runTaskDir, orderFile);
}
function throwIfAborted(signal?: AbortSignal): void {
if (signal?.aborted) {
throw new Error('Task execution aborted');
}
}
export function resolveTaskIssue(issueNumber: number | undefined): Issue[] | undefined {
if (issueNumber === undefined) {
return undefined;
}
const gitProvider = getGitProvider();
const cliStatus = gitProvider.checkCliStatus();
if (!cliStatus.available) {
log.info('gh CLI unavailable, skipping issue resolution for PR body', { issueNumber });
return undefined;
}
try {
const issue = gitProvider.fetchIssue(issueNumber);
return [issue];
} catch (e) {
log.info('Failed to fetch issue for PR body, continuing without issue info', { issueNumber, error: getErrorMessage(e) });
return undefined;
}
}
export async function resolveTaskExecution(
task: TaskInfo,
defaultCwd: string,
abortSignal?: AbortSignal,
): Promise<ResolvedTaskExecution> {
throwIfAborted(abortSignal);
const data = task.data;
if (!data) {
throw new Error(`Task "${task.name}" is missing required data, including piece.`);
}
if (!data.piece || typeof data.piece !== 'string' || data.piece.trim() === '') {
throw new Error(`Task "${task.name}" is missing required piece.`);
}
let execCwd = defaultCwd;
let isWorktree = false;
let reportDirName: string | undefined;
let taskPrompt: string | undefined;
let branch: string | undefined;
let worktreePath: string | undefined;
let baseBranch: string | undefined;
const preferredBaseBranch = resolveTaskDataBaseBranch(data);
if (task.taskDir) {
const taskSlug = getTaskSlugFromTaskDir(task.taskDir);
if (!taskSlug) {
throw new Error(`Invalid task_dir format: ${task.taskDir}`);
}
reportDirName = taskSlug;
}
if (data.worktree) {
throwIfAborted(abortSignal);
baseBranch = resolveTaskBaseBranch(defaultCwd, data);
if (task.worktreePath && fs.existsSync(task.worktreePath)) {
execCwd = task.worktreePath;
branch = data.branch;
worktreePath = task.worktreePath;
isWorktree = true;
} else {
const taskSlug = task.slug ?? await withProgress(
'Generating branch name...',
(slug) => `Branch name generated: ${slug}`,
() => summarizeTaskName(task.content, { cwd: defaultCwd }),
);
throwIfAborted(abortSignal);
const result = await withProgress(
'Creating clone...',
(cloneResult) => `Clone created: ${cloneResult.path} (branch: ${cloneResult.branch})`,
async () => createSharedClone(defaultCwd, {
worktree: data.worktree!,
branch: data.branch,
...(preferredBaseBranch ? { baseBranch: preferredBaseBranch } : {}),
taskSlug,
issueNumber: data.issue,
}),
);
throwIfAborted(abortSignal);
execCwd = result.path;
branch = result.branch;
worktreePath = result.path;
isWorktree = true;
}
}
if (task.taskDir && reportDirName) {
taskPrompt = stageTaskSpecForExecution(defaultCwd, execCwd, task.taskDir, reportDirName);
}
const execPiece = data.piece;
const startMovement = data.start_movement;
const retryNote = data.retry_note;
const maxMovementsOverride = data.exceeded_max_movements;
const initialIterationOverride = data.exceeded_current_iteration;
const autoPr = data.auto_pr ?? resolvePieceConfigValue(defaultCwd, 'autoPr') ?? false;
const draftPr = data.draft_pr ?? resolvePieceConfigValue(defaultCwd, 'draftPr') ?? false;
return {
execCwd,
execPiece,
isWorktree,
autoPr,
draftPr,
...(taskPrompt ? { taskPrompt } : {}),
...(reportDirName ? { reportDirName } : {}),
...(branch ? { branch } : {}),
...(worktreePath ? { worktreePath } : {}),
...(baseBranch ? { baseBranch } : {}),
...(startMovement ? { startMovement } : {}),
...(retryNote ? { retryNote } : {}),
...(data.issue !== undefined ? { issueNumber: data.issue } : {}),
...(maxMovementsOverride !== undefined ? { maxMovementsOverride } : {}),
...(initialIterationOverride !== undefined ? { initialIterationOverride } : {}),
};
}