ポストエクスキューションの共通化とinstructモードの改善

- commit+push+PR作成ロジックをpostExecutionFlowに抽出し、interactive/run/watchの3ルートで共通化
- instructモードはexecuteでcommit+pushのみ(既存PRにpushで反映されるためPR作成不要)
- instructのsave_taskで元ブランチ名・worktree・auto_pr:falseを固定保存(プロンプト不要)
- instructの会話ループにpieceContextを渡し、/goのサマリー品質を改善
- resolveTaskExecutionのautoPrをboolean必須に変更(undefinedフォールバック廃止)
- cloneデフォルトパスを../から../takt-worktree/に変更
This commit is contained in:
nrslib 2026-02-14 01:02:23 +09:00
parent 8af8ff0943
commit 9cc6ac2ca7
9 changed files with 154 additions and 128 deletions

View File

@ -120,6 +120,7 @@ describe('resolveTaskExecution', () => {
execCwd: '/project',
execPiece: 'default',
isWorktree: false,
autoPr: false,
});
expect(mockSummarizeTaskName).not.toHaveBeenCalled();
expect(mockCreateSharedClone).not.toHaveBeenCalled();
@ -177,6 +178,7 @@ describe('resolveTaskExecution', () => {
execCwd: '/project/../20260128T0504-add-auth',
execPiece: 'default',
isWorktree: true,
autoPr: false,
branch: 'takt/20260128T0504-add-auth',
baseBranch: 'main',
});
@ -372,7 +374,7 @@ describe('resolveTaskExecution', () => {
expect(result.autoPr).toBe(true);
});
it('should return undefined autoPr when neither task nor config specifies', async () => {
it('should return false autoPr when neither task nor config specifies', async () => {
// Given: Neither task nor config has autoPr
mockLoadGlobalConfig.mockReturnValue({
language: 'en',
@ -393,7 +395,7 @@ describe('resolveTaskExecution', () => {
const result = await resolveTaskExecution(task, '/project', 'default');
// Then
expect(result.autoPr).toBeUndefined();
expect(result.autoPr).toBe(false);
});
it('should prioritize task YAML auto_pr over global config', async () => {

View File

@ -0,0 +1,81 @@
/**
* Shared post-execution logic: auto-commit, push, and PR creation.
*
* Used by both selectAndExecuteTask (interactive mode) and
* instructBranch (instruct mode from takt list).
*/
import { loadGlobalConfig } from '../../../infra/config/index.js';
import { confirm } from '../../../shared/prompt/index.js';
import { autoCommitAndPush } from '../../../infra/task/index.js';
import { info, error, success } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/index.js';
import { createPullRequest, buildPrBody, pushBranch } from '../../../infra/github/index.js';
import type { GitHubIssue } from '../../../infra/github/index.js';
const log = createLogger('postExecution');
/**
* Resolve auto-PR setting with priority: CLI option > config > prompt.
*/
export async function resolveAutoPr(optionAutoPr: boolean | undefined): Promise<boolean> {
if (typeof optionAutoPr === 'boolean') {
return optionAutoPr;
}
const globalConfig = loadGlobalConfig();
if (typeof globalConfig.autoPr === 'boolean') {
return globalConfig.autoPr;
}
return confirm('Create pull request?', true);
}
export interface PostExecutionOptions {
execCwd: string;
projectCwd: string;
task: string;
branch?: string;
baseBranch?: string;
shouldCreatePr: boolean;
pieceIdentifier?: string;
issues?: GitHubIssue[];
repo?: string;
}
/**
* Auto-commit, push, and optionally create a PR after successful task execution.
*/
export async function postExecutionFlow(options: PostExecutionOptions): Promise<void> {
const { execCwd, projectCwd, task, branch, baseBranch, shouldCreatePr, pieceIdentifier, issues, repo } = options;
const commitResult = autoCommitAndPush(execCwd, task, projectCwd);
if (commitResult.success && commitResult.commitHash) {
success(`Auto-committed & pushed: ${commitResult.commitHash}`);
} else if (!commitResult.success) {
error(`Auto-commit failed: ${commitResult.message}`);
}
if (commitResult.success && commitResult.commitHash && branch && shouldCreatePr) {
info('Creating pull request...');
try {
pushBranch(projectCwd, branch);
} catch (pushError) {
log.info('Branch push from project cwd failed (may already exist)', { error: pushError });
}
const report = pieceIdentifier ? `Piece \`${pieceIdentifier}\` completed successfully.` : 'Task completed successfully.';
const prBody = buildPrBody(issues, report);
const prResult = createPullRequest(projectCwd, {
branch,
title: task.length > 100 ? `${task.slice(0, 97)}...` : task,
body: prBody,
base: baseBranch,
repo,
});
if (prResult.success) {
success(`PR created: ${prResult.url}`);
} else {
error(`PR creation failed: ${prResult.error}`);
}
}
}

View File

@ -19,7 +19,7 @@ export interface ResolvedTaskExecution {
baseBranch?: string;
startMovement?: string;
retryNote?: string;
autoPr?: boolean;
autoPr: boolean;
issueNumber?: number;
}
@ -74,7 +74,7 @@ export async function resolveTaskExecution(
const data = task.data;
if (!data) {
return { execCwd: defaultCwd, execPiece: defaultPiece, isWorktree: false };
return { execCwd: defaultCwd, execPiece: defaultPiece, isWorktree: false, autoPr: false };
}
let execCwd = defaultCwd;
@ -115,7 +115,6 @@ export async function resolveTaskExecution(
execCwd = result.path;
branch = result.branch;
isWorktree = true;
}
if (task.taskDir && reportDirName) {
@ -126,25 +125,25 @@ export async function resolveTaskExecution(
const startMovement = data.start_movement;
const retryNote = data.retry_note;
let autoPr: boolean | undefined;
let autoPr: boolean;
if (data.auto_pr !== undefined) {
autoPr = data.auto_pr;
} else {
const globalConfig = loadGlobalConfig();
autoPr = globalConfig.autoPr;
autoPr = globalConfig.autoPr ?? false;
}
return {
execCwd,
execPiece,
isWorktree,
autoPr,
...(taskPrompt ? { taskPrompt } : {}),
...(reportDirName ? { reportDirName } : {}),
...(branch ? { branch } : {}),
...(baseBranch ? { baseBranch } : {}),
...(startMovement ? { startMovement } : {}),
...(retryNote ? { retryNote } : {}),
...(autoPr !== undefined ? { autoPr } : {}),
...(data.issue !== undefined ? { issueNumber: data.issue } : {}),
};
}

View File

@ -9,15 +9,14 @@
import {
listPieces,
isPiecePath,
loadGlobalConfig,
} from '../../../infra/config/index.js';
import { confirm } from '../../../shared/prompt/index.js';
import { createSharedClone, autoCommitAndPush, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
import { createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
import { info, error, success, withProgress } from '../../../shared/ui/index.js';
import { info, error, withProgress } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/index.js';
import { createPullRequest, buildPrBody, pushBranch } from '../../../infra/github/index.js';
import { executeTask } from './taskExecution.js';
import { resolveAutoPr, postExecutionFlow } from './postExecution.js';
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
import { selectPiece } from '../../pieceSelection/index.js';
@ -75,26 +74,6 @@ export async function confirmAndCreateWorktree(
return { execCwd: result.path, isWorktree: true, branch: result.branch, baseBranch };
}
/**
* Resolve auto-PR setting with priority: CLI option > config > prompt.
* Only applicable when worktree is enabled.
*/
async function resolveAutoPr(optionAutoPr: boolean | undefined): Promise<boolean> {
// CLI option takes precedence
if (typeof optionAutoPr === 'boolean') {
return optionAutoPr;
}
// Check global config
const globalConfig = loadGlobalConfig();
if (typeof globalConfig.autoPr === 'boolean') {
return globalConfig.autoPr;
}
// Fall back to interactive prompt
return confirm('Create pull request?', true);
}
/**
* Execute a task with piece selection, optional worktree, and auto-commit.
* Shared by direct task execution and interactive mode.
@ -136,36 +115,17 @@ export async function selectAndExecuteTask(
});
if (taskSuccess && isWorktree) {
const commitResult = autoCommitAndPush(execCwd, task, cwd);
if (commitResult.success && commitResult.commitHash) {
success(`Auto-committed & pushed: ${commitResult.commitHash}`);
} else if (!commitResult.success) {
error(`Auto-commit failed: ${commitResult.message}`);
}
if (commitResult.success && commitResult.commitHash && branch && shouldCreatePr) {
info('Creating pull request...');
// Push branch from project cwd to origin (clone's origin is removed after shared clone)
try {
pushBranch(cwd, branch);
} catch (pushError) {
// Branch may already be pushed by autoCommitAndPush, continue to PR creation
log.info('Branch push from project cwd failed (may already exist)', { error: pushError });
}
const prBody = buildPrBody(options?.issues, `Piece \`${pieceIdentifier}\` completed successfully.`);
const prResult = createPullRequest(cwd, {
await postExecutionFlow({
execCwd,
projectCwd: cwd,
task,
branch,
title: task.length > 100 ? `${task.slice(0, 97)}...` : task,
body: prBody,
base: baseBranch,
baseBranch,
shouldCreatePr,
pieceIdentifier,
issues: options?.issues,
repo: options?.repo,
});
if (prResult.success) {
success(`PR created: ${prResult.url}`);
} else {
error(`PR creation failed: ${prResult.error}`);
}
}
}
if (!taskSuccess) {

View File

@ -3,7 +3,7 @@
*/
import { loadPieceByIdentifier, isPiecePath, loadGlobalConfig } from '../../../infra/config/index.js';
import { TaskRunner, type TaskInfo, autoCommitAndPush } from '../../../infra/task/index.js';
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
import {
header,
info,
@ -17,9 +17,10 @@ import { getLabel } from '../../../shared/i18n/index.js';
import { executePiece } from './pieceExecution.js';
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
import type { TaskExecutionOptions, ExecuteTaskOptions, PieceExecutionResult } from './types.js';
import { createPullRequest, buildPrBody, pushBranch, fetchIssue, checkGhCli } from '../../../infra/github/index.js';
import { fetchIssue, checkGhCli } from '../../../infra/github/index.js';
import { runWithWorkerPool } from './parallelExecution.js';
import { resolveTaskExecution } from './resolveTask.js';
import { postExecutionFlow } from './postExecution.js';
export type { TaskExecutionOptions, ExecuteTaskOptions };
@ -167,37 +168,17 @@ export async function executeAndCompleteTask(
const completedAt = new Date().toISOString();
if (taskSuccess && isWorktree) {
const commitResult = autoCommitAndPush(execCwd, task.name, cwd);
if (commitResult.success && commitResult.commitHash) {
info(`Auto-committed & pushed: ${commitResult.commitHash}`);
} else if (!commitResult.success) {
error(`Auto-commit failed: ${commitResult.message}`);
}
// Create PR if autoPr is enabled and commit succeeded
if (commitResult.success && commitResult.commitHash && branch && autoPr) {
info('Creating pull request...');
// Push branch from project cwd to origin
try {
pushBranch(cwd, branch);
} catch (pushError) {
// Branch may already be pushed, continue to PR creation
log.info('Branch push from project cwd failed (may already exist)', { error: pushError });
}
const issues = resolveTaskIssue(issueNumber);
const prBody = buildPrBody(issues, `Piece \`${execPiece}\` completed successfully.`);
const prResult = createPullRequest(cwd, {
await postExecutionFlow({
execCwd,
projectCwd: cwd,
task: task.name,
branch,
title: task.name.length > 100 ? `${task.name.slice(0, 97)}...` : task.name,
body: prBody,
base: baseBranch,
baseBranch,
shouldCreatePr: autoPr,
pieceIdentifier: execPiece,
issues,
});
if (prResult.success) {
success(`PR created: ${prResult.url}`);
} else {
error(`PR creation failed: ${prResult.error}`);
}
}
}
const taskResult = {

View File

@ -14,6 +14,7 @@ export {
type SelectAndExecuteOptions,
type WorktreeConfirmationResult,
} from './execute/selectAndExecute.js';
export { resolveAutoPr, postExecutionFlow, type PostExecutionOptions } from './execute/postExecution.js';
export { addTask, saveTaskFile, saveTaskFromInteractive, createIssueFromTask, createIssueAndSaveTask } from './add/index.js';
export { watchTasks } from './watch/index.js';
export {

View File

@ -17,6 +17,7 @@ import {
resolveLanguage,
buildSummaryActionOptions,
selectSummaryAction,
type PieceContext,
} from '../../interactive/interactive.js';
import { loadTemplate } from '../../../shared/prompts/index.js';
import { getLabelObject } from '../../../shared/i18n/index.js';
@ -66,6 +67,7 @@ export async function runInstructMode(
cwd: string,
branchContext: string,
branchName: string,
pieceContext?: PieceContext,
): Promise<InstructModeResult> {
const globalConfig = loadGlobalConfig();
const lang = resolveLanguage(globalConfig.language);
@ -113,7 +115,7 @@ export async function runInstructMode(
selectAction: createSelectInstructAction(ui),
};
const result = await runConversationLoop(cwd, ctx, strategy, undefined, undefined);
const result = await runConversationLoop(cwd, ctx, strategy, pieceContext, undefined);
if (result.action === 'cancel') {
return { action: 'cancel', task: '' };

View File

@ -19,6 +19,7 @@ import {
autoCommitAndPush,
type BranchListItem,
} from '../../../infra/task/index.js';
import { loadGlobalConfig, getPieceDescription } from '../../../infra/config/index.js';
import { selectOption } from '../../../shared/prompt/index.js';
import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js';
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
@ -29,6 +30,7 @@ import { runInstructMode } from './instructMode.js';
import { saveTaskFile } from '../add/index.js';
import { selectPiece } from '../../pieceSelection/index.js';
import { dispatchConversationAction } from '../../interactive/actionDispatcher.js';
import type { PieceContext } from '../../interactive/interactive.js';
const log = createLogger('list-tasks');
@ -306,7 +308,7 @@ function getBranchContext(projectDir: string, branch: string): string {
/**
* Instruct branch: create a temp clone, give additional instructions via
* interactive conversation, then auto-commit+push or save as task file.
* interactive conversation, then auto-commit+push+PR or save as task file.
*/
export async function instructBranch(
projectDir: string,
@ -315,45 +317,43 @@ export async function instructBranch(
): Promise<boolean> {
const { branch } = item.info;
const branchContext = getBranchContext(projectDir, branch);
const result = await runInstructMode(projectDir, branchContext, branch);
let selectedPiece: string | null = null;
const ensurePieceSelected = async (): Promise<string | null> => {
if (selectedPiece) {
return selectedPiece;
}
selectedPiece = await selectPiece(projectDir);
const selectedPiece = await selectPiece(projectDir);
if (!selectedPiece) {
info('Cancelled');
return null;
return false;
}
return selectedPiece;
const globalConfig = loadGlobalConfig();
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
const pieceContext: PieceContext = {
name: pieceDesc.name,
description: pieceDesc.description,
pieceStructure: pieceDesc.pieceStructure,
movementPreviews: pieceDesc.movementPreviews,
};
const branchContext = getBranchContext(projectDir, branch);
const result = await runInstructMode(projectDir, branchContext, branch, pieceContext);
return dispatchConversationAction(result, {
cancel: () => {
info('Cancelled');
return false;
},
save_task: async ({ task }) => {
const piece = await ensurePieceSelected();
if (!piece) {
return false;
}
const created = await saveTaskFile(projectDir, task, { piece });
const created = await saveTaskFile(projectDir, task, {
piece: selectedPiece,
worktree: true,
branch,
autoPr: false,
});
success(`Task saved: ${created.taskName}`);
info(` File: ${created.tasksFile}`);
log.info('Task saved from instruct mode', { branch, piece });
info(` Branch: ${branch}`);
log.info('Task saved from instruct mode', { branch, piece: selectedPiece });
return true;
},
execute: async ({ task }) => {
const piece = await ensurePieceSelected();
if (!piece) {
return false;
}
log.info('Instructing branch via temp clone', { branch, piece });
log.info('Instructing branch via temp clone', { branch, piece: selectedPiece });
info(`Running instruction on ${branch}...`);
const clone = createTempCloneForBranch(projectDir, branch);
@ -366,17 +366,17 @@ export async function instructBranch(
const taskSuccess = await executeTask({
task: fullInstruction,
cwd: clone.path,
pieceIdentifier: piece,
pieceIdentifier: selectedPiece,
projectCwd: projectDir,
agentOverrides: options,
});
if (taskSuccess) {
const commitResult = autoCommitAndPush(clone.path, item.taskSlug, projectDir);
const commitResult = autoCommitAndPush(clone.path, task, projectDir);
if (commitResult.success && commitResult.commitHash) {
info(`Auto-committed & pushed: ${commitResult.commitHash}`);
success(`Auto-committed & pushed: ${commitResult.commitHash}`);
} else if (!commitResult.success) {
warn(`Auto-commit skipped: ${commitResult.message}`);
logError(`Auto-commit failed: ${commitResult.message}`);
}
success(`Instruction completed on ${branch}`);
log.info('Instruction completed', { branch });

View File

@ -42,7 +42,7 @@ export class CloneManager {
? globalConfig.worktreeDir
: path.resolve(projectDir, globalConfig.worktreeDir);
}
return path.join(projectDir, '..');
return path.join(projectDir, '..', 'takt-worktree');
}
/** Resolve the clone path based on options and global config */