186 lines
6.1 KiB
TypeScript
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 } : {}),
|
|
};
|
|
}
|