worktreeにタスク指示書をコピー
This commit is contained in:
parent
c42799739e
commit
77cd485c22
@ -2,6 +2,9 @@
|
|||||||
* Tests for resolveTaskExecution
|
* 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';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
// Mock dependencies before importing the module under test
|
// Mock dependencies before importing the module under test
|
||||||
@ -522,7 +525,13 @@ describe('resolveTaskExecution', () => {
|
|||||||
expect(mockCreateSharedClone).not.toHaveBeenCalled();
|
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 = {
|
const task: TaskInfo = {
|
||||||
name: 'task-with-dir',
|
name: 'task-with-dir',
|
||||||
content: 'Task content',
|
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.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 () => {
|
it('should throw when taskDir format is invalid', async () => {
|
||||||
@ -569,4 +584,41 @@ describe('resolveTaskExecution', () => {
|
|||||||
'Invalid task_dir format: .takt/tasks/..',
|
'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)
|
// Normal parsing for all other cases (including '#' prefixed inputs)
|
||||||
await program.parseAsync();
|
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];
|
const rootArg = process.argv.slice(2)[0];
|
||||||
if (rootArg !== 'watch') {
|
if (rootArg !== 'watch') {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
* Resolve execution directory and piece from task data.
|
* 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 { loadGlobalConfig } from '../../../infra/config/index.js';
|
||||||
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
|
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
|
||||||
import { info, withProgress } from '../../../shared/ui/index.js';
|
import { info, withProgress } from '../../../shared/ui/index.js';
|
||||||
@ -11,6 +13,7 @@ export interface ResolvedTaskExecution {
|
|||||||
execCwd: string;
|
execCwd: string;
|
||||||
execPiece: string;
|
execPiece: string;
|
||||||
isWorktree: boolean;
|
isWorktree: boolean;
|
||||||
|
taskPrompt?: string;
|
||||||
reportDirName?: string;
|
reportDirName?: string;
|
||||||
branch?: string;
|
branch?: string;
|
||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
@ -20,6 +23,36 @@ export interface ResolvedTaskExecution {
|
|||||||
issueNumber?: number;
|
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 {
|
function throwIfAborted(signal?: AbortSignal): void {
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
throw new Error('Task execution aborted');
|
throw new Error('Task execution aborted');
|
||||||
@ -47,6 +80,7 @@ export async function resolveTaskExecution(
|
|||||||
let execCwd = defaultCwd;
|
let execCwd = defaultCwd;
|
||||||
let isWorktree = false;
|
let isWorktree = false;
|
||||||
let reportDirName: string | undefined;
|
let reportDirName: string | undefined;
|
||||||
|
let taskPrompt: string | undefined;
|
||||||
let branch: string | undefined;
|
let branch: string | undefined;
|
||||||
let baseBranch: string | undefined;
|
let baseBranch: string | undefined;
|
||||||
if (task.taskDir) {
|
if (task.taskDir) {
|
||||||
@ -81,6 +115,11 @@ export async function resolveTaskExecution(
|
|||||||
execCwd = result.path;
|
execCwd = result.path;
|
||||||
branch = result.branch;
|
branch = result.branch;
|
||||||
isWorktree = true;
|
isWorktree = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.taskDir && reportDirName) {
|
||||||
|
taskPrompt = stageTaskSpecForExecution(defaultCwd, execCwd, task.taskDir, reportDirName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const execPiece = data.piece || defaultPiece;
|
const execPiece = data.piece || defaultPiece;
|
||||||
@ -99,6 +138,7 @@ export async function resolveTaskExecution(
|
|||||||
execCwd,
|
execCwd,
|
||||||
execPiece,
|
execPiece,
|
||||||
isWorktree,
|
isWorktree,
|
||||||
|
...(taskPrompt ? { taskPrompt } : {}),
|
||||||
...(reportDirName ? { reportDirName } : {}),
|
...(reportDirName ? { reportDirName } : {}),
|
||||||
...(branch ? { branch } : {}),
|
...(branch ? { branch } : {}),
|
||||||
...(baseBranch ? { baseBranch } : {}),
|
...(baseBranch ? { baseBranch } : {}),
|
||||||
|
|||||||
@ -130,11 +130,23 @@ export async function executeAndCompleteTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
// cwd is always the project root; pass it as projectCwd so reports/sessions go there
|
||||||
const taskRunResult = await executeTaskWithResult({
|
const taskRunResult = await executeTaskWithResult({
|
||||||
task: task.content,
|
task: taskPrompt ?? task.content,
|
||||||
cwd: execCwd,
|
cwd: execCwd,
|
||||||
pieceIdentifier: execPiece,
|
pieceIdentifier: execPiece,
|
||||||
projectCwd: cwd,
|
projectCwd: cwd,
|
||||||
|
|||||||
@ -14,9 +14,7 @@ function pauseStdinSafely(): void {
|
|||||||
if (process.stdin.readable && !process.stdin.destroyed) {
|
if (process.stdin.readable && !process.stdin.destroyed) {
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Ignore stdin state errors during prompt cleanup.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user