worktreeにタスク指示書をコピー
This commit is contained in:
parent
c42799739e
commit
77cd485c22
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 } : {}),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user