diff --git a/src/__tests__/taskExecution.test.ts b/src/__tests__/taskExecution.test.ts index 60d49d0..382e00c 100644 --- a/src/__tests__/taskExecution.test.ts +++ b/src/__tests__/taskExecution.test.ts @@ -2,6 +2,9 @@ * Tests for resolveTaskExecution */ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock dependencies before importing the module under test @@ -522,7 +525,13 @@ describe('resolveTaskExecution', () => { expect(mockCreateSharedClone).not.toHaveBeenCalled(); }); - it('should return reportDirName from taskDir basename', async () => { + it('should stage task_dir spec into run context and return reportDirName', async () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-taskdir-normal-')); + const projectDir = path.join(tmpRoot, 'project'); + fs.mkdirSync(path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng'), { recursive: true }); + const sourceOrder = path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng', 'order.md'); + fs.writeFileSync(sourceOrder, '# normal task spec\n', 'utf-8'); + const task: TaskInfo = { name: 'task-with-dir', content: 'Task content', @@ -533,9 +542,15 @@ describe('resolveTaskExecution', () => { }, }; - const result = await resolveTaskExecution(task, '/project', 'default'); + const result = await resolveTaskExecution(task, projectDir, 'default'); expect(result.reportDirName).toBe('20260201-015714-foptng'); + expect(result.execCwd).toBe(projectDir); + const stagedOrder = path.join(projectDir, '.takt', 'runs', '20260201-015714-foptng', 'context', 'task', 'order.md'); + expect(fs.existsSync(stagedOrder)).toBe(true); + expect(fs.readFileSync(stagedOrder, 'utf-8')).toContain('normal task spec'); + expect(result.taskPrompt).toContain('Primary spec: `.takt/runs/20260201-015714-foptng/context/task/order.md`.'); + expect(result.taskPrompt).not.toContain(projectDir); }); it('should throw when taskDir format is invalid', async () => { @@ -569,4 +584,41 @@ describe('resolveTaskExecution', () => { 'Invalid task_dir format: .takt/tasks/..', ); }); + + it('should stage task_dir spec into worktree run context and return run-scoped task prompt', async () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-taskdir-')); + const projectDir = path.join(tmpRoot, 'project'); + const cloneDir = path.join(tmpRoot, 'clone'); + fs.mkdirSync(path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng'), { recursive: true }); + fs.mkdirSync(cloneDir, { recursive: true }); + const sourceOrder = path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng', 'order.md'); + fs.writeFileSync(sourceOrder, '# webhook task\n', 'utf-8'); + + const task: TaskInfo = { + name: 'task-with-taskdir-worktree', + content: 'Task content', + taskDir: '.takt/tasks/20260201-015714-foptng', + filePath: '/tasks/task.yaml', + data: { + task: 'Task content', + worktree: true, + }, + }; + + mockSummarizeTaskName.mockResolvedValue('webhook-task'); + mockCreateSharedClone.mockReturnValue({ + path: cloneDir, + branch: 'takt/webhook-task', + }); + + const result = await resolveTaskExecution(task, projectDir, 'default'); + + const stagedOrder = path.join(cloneDir, '.takt', 'runs', '20260201-015714-foptng', 'context', 'task', 'order.md'); + expect(fs.existsSync(stagedOrder)).toBe(true); + expect(fs.readFileSync(stagedOrder, 'utf-8')).toContain('webhook task'); + + expect(result.taskPrompt).toContain('Implement using only the files in `.takt/runs/20260201-015714-foptng/context/task`.'); + expect(result.taskPrompt).toContain('Primary spec: `.takt/runs/20260201-015714-foptng/context/task/order.md`.'); + expect(result.taskPrompt).not.toContain(projectDir); + }); }); diff --git a/src/app/cli/index.ts b/src/app/cli/index.ts index c93b140..76394a9 100644 --- a/src/app/cli/index.ts +++ b/src/app/cli/index.ts @@ -36,8 +36,6 @@ import { executeDefaultAction } from './routing.js'; // Normal parsing for all other cases (including '#' prefixed inputs) await program.parseAsync(); - // Some providers/SDKs may leave active handles even after command completion. - // Keep only watch mode as a long-running command; all others should exit explicitly. const rootArg = process.argv.slice(2)[0]; if (rootArg !== 'watch') { process.exit(0); diff --git a/src/features/tasks/execute/resolveTask.ts b/src/features/tasks/execute/resolveTask.ts index a63fd25..e860e65 100644 --- a/src/features/tasks/execute/resolveTask.ts +++ b/src/features/tasks/execute/resolveTask.ts @@ -2,6 +2,8 @@ * Resolve execution directory and piece from task data. */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { loadGlobalConfig } from '../../../infra/config/index.js'; import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js'; import { info, withProgress } from '../../../shared/ui/index.js'; @@ -11,6 +13,7 @@ export interface ResolvedTaskExecution { execCwd: string; execPiece: string; isWorktree: boolean; + taskPrompt?: string; reportDirName?: string; branch?: string; baseBranch?: string; @@ -20,6 +23,36 @@ export interface ResolvedTaskExecution { issueNumber?: number; } +function buildRunTaskDirInstruction(reportDirName: string): string { + const runTaskDir = `.takt/runs/${reportDirName}/context/task`; + const orderFile = `${runTaskDir}/order.md`; + return [ + `Implement using only the files in \`${runTaskDir}\`.`, + `Primary spec: \`${orderFile}\`.`, + 'Use report files in Report Directory as primary execution history.', + 'Do not rely on previous response or conversation summary.', + ].join('\n'); +} + +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); + + return buildRunTaskDirInstruction(reportDirName); +} + function throwIfAborted(signal?: AbortSignal): void { if (signal?.aborted) { throw new Error('Task execution aborted'); @@ -47,6 +80,7 @@ export async function resolveTaskExecution( let execCwd = defaultCwd; let isWorktree = false; let reportDirName: string | undefined; + let taskPrompt: string | undefined; let branch: string | undefined; let baseBranch: string | undefined; if (task.taskDir) { @@ -81,6 +115,11 @@ export async function resolveTaskExecution( execCwd = result.path; branch = result.branch; isWorktree = true; + + } + + if (task.taskDir && reportDirName) { + taskPrompt = stageTaskSpecForExecution(defaultCwd, execCwd, task.taskDir, reportDirName); } const execPiece = data.piece || defaultPiece; @@ -99,6 +138,7 @@ export async function resolveTaskExecution( execCwd, execPiece, isWorktree, + ...(taskPrompt ? { taskPrompt } : {}), ...(reportDirName ? { reportDirName } : {}), ...(branch ? { branch } : {}), ...(baseBranch ? { baseBranch } : {}), diff --git a/src/features/tasks/execute/taskExecution.ts b/src/features/tasks/execute/taskExecution.ts index 5c17e3a..bdc50cb 100644 --- a/src/features/tasks/execute/taskExecution.ts +++ b/src/features/tasks/execute/taskExecution.ts @@ -130,11 +130,23 @@ export async function executeAndCompleteTask( } try { - const { execCwd, execPiece, isWorktree, reportDirName, branch, baseBranch, startMovement, retryNote, autoPr, issueNumber } = await resolveTaskExecution(task, cwd, pieceName, taskAbortSignal); + const { + execCwd, + execPiece, + isWorktree, + taskPrompt, + reportDirName, + branch, + baseBranch, + startMovement, + retryNote, + autoPr, + issueNumber, + } = await resolveTaskExecution(task, cwd, pieceName, taskAbortSignal); // cwd is always the project root; pass it as projectCwd so reports/sessions go there const taskRunResult = await executeTaskWithResult({ - task: task.content, + task: taskPrompt ?? task.content, cwd: execCwd, pieceIdentifier: execPiece, projectCwd: cwd, diff --git a/src/shared/prompt/confirm.ts b/src/shared/prompt/confirm.ts index ac3bc4f..663cde8 100644 --- a/src/shared/prompt/confirm.ts +++ b/src/shared/prompt/confirm.ts @@ -14,9 +14,7 @@ function pauseStdinSafely(): void { if (process.stdin.readable && !process.stdin.destroyed) { process.stdin.pause(); } - } catch { - // Ignore stdin state errors during prompt cleanup. - } + } catch {} } /**