feat: --pr インタラクティブモードで create_issue 除外・save_task 時の PR ブランチ自動設定
- --pr 指定時のインタラクティブモードで create_issue を選択肢から除外 - execute アクション時に PR ブランチを fetch + checkout してから実行 - save_task アクション時に worktree/branch/autoPr を自動設定しプロンプトをスキップ - saveTaskFromInteractive に presetSettings オプションを追加 - interactiveMode に InteractiveModeOptions(excludeActions)を追加 - checkoutBranch() を git.ts に追加し steps.ts の重複コードを DRY 化
This commit is contained in:
parent
e5f296a3e0
commit
c843858f2e
@ -231,6 +231,8 @@ describe('Issue resolution in routing', () => {
|
||||
'## GitHub Issue #131: Issue #131',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Then: selectAndExecuteTask should be called (issues are used only for initialInput, not selectOptions)
|
||||
@ -282,6 +284,8 @@ describe('Issue resolution in routing', () => {
|
||||
'## GitHub Issue #131: Issue #131',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Then: selectAndExecuteTask should be called
|
||||
@ -305,6 +309,8 @@ describe('Issue resolution in routing', () => {
|
||||
'refactor the code',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Then: no issue fetching should occur
|
||||
@ -324,6 +330,8 @@ describe('Issue resolution in routing', () => {
|
||||
undefined,
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Then: no issue fetching should occur
|
||||
@ -399,6 +407,8 @@ describe('Issue resolution in routing', () => {
|
||||
]),
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -433,6 +443,8 @@ describe('Issue resolution in routing', () => {
|
||||
]),
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -450,6 +462,8 @@ describe('Issue resolution in routing', () => {
|
||||
'fix issue',
|
||||
expect.objectContaining({ taskHistory: [] }),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -463,6 +477,8 @@ describe('Issue resolution in routing', () => {
|
||||
'verify history',
|
||||
expect.objectContaining({ taskHistory: [] }),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -533,6 +549,8 @@ describe('Issue resolution in routing', () => {
|
||||
undefined,
|
||||
expect.anything(),
|
||||
'saved-session-123',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -556,6 +574,8 @@ describe('Issue resolution in routing', () => {
|
||||
undefined,
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -572,6 +592,8 @@ describe('Issue resolution in routing', () => {
|
||||
undefined,
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -586,6 +608,8 @@ describe('Issue resolution in routing', () => {
|
||||
undefined,
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
info: vi.fn(),
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
withProgress: vi.fn(async (_start, _done, operation) => operation()),
|
||||
}));
|
||||
@ -76,11 +77,13 @@ vi.mock('../features/interactive/index.js', () => ({
|
||||
|
||||
const mockListAllTaskItems = vi.fn();
|
||||
const mockIsStaleRunningTask = vi.fn();
|
||||
const mockCheckoutBranch = vi.fn();
|
||||
vi.mock('../infra/task/index.js', () => ({
|
||||
TaskRunner: vi.fn(() => ({
|
||||
listAllTaskItems: mockListAllTaskItems,
|
||||
})),
|
||||
isStaleRunningTask: (...args: unknown[]) => mockIsStaleRunningTask(...args),
|
||||
checkoutBranch: (...args: unknown[]) => mockCheckoutBranch(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/index.js', () => ({
|
||||
@ -171,6 +174,8 @@ describe('PR resolution in routing', () => {
|
||||
expect.stringContaining('## PR #456 Review Comments:'),
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
{ excludeActions: ['create_issue'] },
|
||||
);
|
||||
});
|
||||
|
||||
@ -184,7 +189,7 @@ describe('PR resolution in routing', () => {
|
||||
// When
|
||||
await executeDefaultAction();
|
||||
|
||||
// Then: selectAndExecuteTask is called (branch is no longer passed via selectOptions)
|
||||
// Then: selectAndExecuteTask is called
|
||||
expect(mockSelectAndExecuteTask).toHaveBeenCalledWith(
|
||||
'/test/cwd',
|
||||
'summarized task',
|
||||
@ -193,6 +198,20 @@ describe('PR resolution in routing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should checkout PR branch before executing task', async () => {
|
||||
// Given
|
||||
mockOpts.pr = 456;
|
||||
const prReview = createMockPrReview({ headRefName: 'feat/my-pr-branch' });
|
||||
mockCheckCliStatus.mockReturnValue({ available: true });
|
||||
mockFetchPrReviewComments.mockReturnValue(prReview);
|
||||
|
||||
// When
|
||||
await executeDefaultAction();
|
||||
|
||||
// Then: checkoutBranch is called with the PR's head branch
|
||||
expect(mockCheckoutBranch).toHaveBeenCalledWith('/test/cwd', 'feat/my-pr-branch');
|
||||
});
|
||||
|
||||
it('should exit with error when gh CLI is unavailable', async () => {
|
||||
// Given
|
||||
mockOpts.pr = 456;
|
||||
@ -229,6 +248,8 @@ describe('PR resolution in routing', () => {
|
||||
expect.stringContaining('## PR #456 Review Comments:'),
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
{ excludeActions: ['create_issue'] },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -5,12 +5,13 @@
|
||||
* pipeline mode, or interactive mode.
|
||||
*/
|
||||
|
||||
import { info, error as logError, withProgress } from '../../shared/ui/index.js';
|
||||
import { info, success, error as logError, withProgress } from '../../shared/ui/index.js';
|
||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||
import { getLabel } from '../../shared/i18n/index.js';
|
||||
import { formatIssueAsTask, parseIssueNumbers, formatPrReviewAsTask } from '../../infra/github/index.js';
|
||||
import { getGitProvider } from '../../infra/git/index.js';
|
||||
import type { PrReviewData } from '../../infra/git/index.js';
|
||||
import { checkoutBranch } from '../../infra/task/index.js';
|
||||
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueAndSaveTask, promptLabelSelection, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||
import { executePipeline } from '../../features/pipeline/index.js';
|
||||
import {
|
||||
@ -85,7 +86,7 @@ async function resolveIssueInput(
|
||||
*/
|
||||
async function resolvePrInput(
|
||||
prNumber: number,
|
||||
): Promise<{ initialInput: string }> {
|
||||
): Promise<{ initialInput: string; prBranch: string }> {
|
||||
const ghStatus = getGitProvider().checkCliStatus();
|
||||
if (!ghStatus.available) {
|
||||
throw new Error(ghStatus.error);
|
||||
@ -97,7 +98,7 @@ async function resolvePrInput(
|
||||
async () => getGitProvider().fetchPrReviewComments(prNumber),
|
||||
);
|
||||
|
||||
return { initialInput: formatPrReviewAsTask(prReview) };
|
||||
return { initialInput: formatPrReviewAsTask(prReview), prBranch: prReview.headRefName };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -169,11 +170,13 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
|
||||
// Resolve PR review comments (--pr N) before interactive mode
|
||||
let initialInput: string | undefined = task;
|
||||
let prBranch: string | undefined;
|
||||
|
||||
if (prNumber) {
|
||||
try {
|
||||
const prResult = await resolvePrInput(prNumber);
|
||||
initialInput = prResult.initialInput;
|
||||
prBranch = prResult.prBranch;
|
||||
} catch (e) {
|
||||
logError(getErrorMessage(e));
|
||||
process.exit(1);
|
||||
@ -234,7 +237,8 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
info(getLabel('interactive.continueNoSession', lang));
|
||||
}
|
||||
}
|
||||
result = await interactiveMode(resolvedCwd, initialInput, pieceContext, selectedSessionId);
|
||||
const interactiveOpts = prBranch ? { excludeActions: ['create_issue'] as const } : undefined;
|
||||
result = await interactiveMode(resolvedCwd, initialInput, pieceContext, selectedSessionId, undefined, interactiveOpts);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -259,6 +263,11 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
|
||||
await dispatchConversationAction(result, {
|
||||
execute: async ({ task: confirmedTask }) => {
|
||||
if (prBranch) {
|
||||
info(`Fetching and checking out PR branch: ${prBranch}`);
|
||||
checkoutBranch(resolvedCwd, prBranch);
|
||||
success(`Checked out PR branch: ${prBranch}`);
|
||||
}
|
||||
selectOptions.interactiveUserInput = true;
|
||||
selectOptions.piece = pieceId;
|
||||
selectOptions.interactiveMetadata = { confirmed: true, task: confirmedTask };
|
||||
@ -273,7 +282,10 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
});
|
||||
},
|
||||
save_task: async ({ task: confirmedTask }) => {
|
||||
await saveTaskFromInteractive(resolvedCwd, confirmedTask, pieceId);
|
||||
const presetSettings = prBranch
|
||||
? { worktree: true as const, branch: prBranch, autoPr: false }
|
||||
: undefined;
|
||||
await saveTaskFromInteractive(resolvedCwd, confirmedTask, pieceId, { presetSettings });
|
||||
},
|
||||
cancel: () => undefined,
|
||||
});
|
||||
|
||||
@ -14,6 +14,7 @@ export {
|
||||
type TaskHistorySummaryItem,
|
||||
type InteractiveModeResult,
|
||||
type InteractiveModeAction,
|
||||
type InteractiveModeOptions,
|
||||
} from './interactive.js';
|
||||
|
||||
export { selectInteractiveMode } from './modeSelection.js';
|
||||
|
||||
@ -25,6 +25,10 @@ import {
|
||||
type PieceContext,
|
||||
formatMovementPreviews,
|
||||
type InteractiveModeAction,
|
||||
type SummaryActionValue,
|
||||
type PostSummaryAction,
|
||||
buildSummaryActionOptions,
|
||||
selectSummaryAction,
|
||||
} from './interactive-summary.js';
|
||||
import { type RunSessionContext, formatRunSessionForPrompt } from './runSessionReader.js';
|
||||
|
||||
@ -113,12 +117,18 @@ export {
|
||||
* /cancel → exits without executing
|
||||
* Ctrl+D → exits without executing
|
||||
*/
|
||||
export interface InteractiveModeOptions {
|
||||
/** Actions to exclude from the post-summary action selector. */
|
||||
excludeActions?: readonly SummaryActionValue[];
|
||||
}
|
||||
|
||||
export async function interactiveMode(
|
||||
cwd: string,
|
||||
initialInput?: string,
|
||||
pieceContext?: PieceContext,
|
||||
sessionId?: string,
|
||||
runSessionContext?: RunSessionContext,
|
||||
options?: InteractiveModeOptions,
|
||||
): Promise<InteractiveModeResult> {
|
||||
const baseCtx = initializeSession(cwd, 'interactive');
|
||||
const ctx = sessionId ? { ...baseCtx, sessionId } : baseCtx;
|
||||
@ -155,11 +165,32 @@ export async function interactiveMode(
|
||||
return `## Policy\n${policyIntro}\n\n${policyContent}\n\n---\n\n${userMessage}\n\n---\n**Policy Reminder:** ${reminderLabel}`;
|
||||
}
|
||||
|
||||
const excludeActions = options?.excludeActions;
|
||||
const selectAction = excludeActions?.length
|
||||
? (task: string): Promise<PostSummaryAction | null> =>
|
||||
selectSummaryAction(
|
||||
task,
|
||||
ui.proposed,
|
||||
ui.actionPrompt,
|
||||
buildSummaryActionOptions(
|
||||
{
|
||||
execute: ui.actions.execute,
|
||||
createIssue: ui.actions.createIssue,
|
||||
saveTask: ui.actions.saveTask,
|
||||
continue: ui.actions.continue,
|
||||
},
|
||||
['create_issue'],
|
||||
excludeActions,
|
||||
),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return runConversationLoop(cwd, ctx, {
|
||||
systemPrompt,
|
||||
allowedTools: DEFAULT_INTERACTIVE_TOOLS,
|
||||
transformPrompt: injectPolicy,
|
||||
introMessage: ui.intro,
|
||||
selectAction,
|
||||
}, pieceContext, initialInput);
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { formatIssueAsTask, buildPrBody, formatPrReviewAsTask } from '../../infra/github/index.js';
|
||||
import { getGitProvider, type Issue } from '../../infra/git/index.js';
|
||||
import { stageAndCommit, resolveBaseBranch, pushBranch } from '../../infra/task/index.js';
|
||||
import { stageAndCommit, resolveBaseBranch, pushBranch, checkoutBranch } from '../../infra/task/index.js';
|
||||
import { executeTask, confirmAndCreateWorktree, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
||||
import { info, error, success } from '../../shared/ui/index.js';
|
||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||
@ -150,8 +150,7 @@ export async function resolveExecutionContext(
|
||||
}
|
||||
if (prBranch) {
|
||||
info(`Fetching and checking out PR branch: ${prBranch}`);
|
||||
execFileSync('git', ['fetch', 'origin', prBranch], { cwd, stdio: 'pipe' });
|
||||
execFileSync('git', ['checkout', prBranch], { cwd, stdio: 'pipe' });
|
||||
checkoutBranch(cwd, prBranch);
|
||||
success(`Checked out PR branch: ${prBranch}`);
|
||||
return { execCwd: cwd, branch: prBranch, baseBranch: resolveBaseBranch(cwd).branch, isWorktree: false };
|
||||
}
|
||||
|
||||
@ -142,12 +142,13 @@ async function promptWorktreeSettings(): Promise<WorktreeSettings> {
|
||||
/**
|
||||
* Save a task from interactive mode result.
|
||||
* Prompts for worktree/branch/auto_pr settings before saving.
|
||||
* If presetSettings is provided, skips the prompt and uses those settings directly.
|
||||
*/
|
||||
export async function saveTaskFromInteractive(
|
||||
cwd: string,
|
||||
task: string,
|
||||
piece?: string,
|
||||
options?: { issue?: number; confirmAtEndMessage?: string },
|
||||
options?: { issue?: number; confirmAtEndMessage?: string; presetSettings?: WorktreeSettings },
|
||||
): Promise<void> {
|
||||
if (options?.confirmAtEndMessage) {
|
||||
const approved = await confirm(options.confirmAtEndMessage, true);
|
||||
@ -155,7 +156,7 @@ export async function saveTaskFromInteractive(
|
||||
return;
|
||||
}
|
||||
}
|
||||
const settings = await promptWorktreeSettings();
|
||||
const settings = options?.presetSettings ?? await promptWorktreeSettings();
|
||||
const created = await saveTaskFile(cwd, task, { piece, issue: options?.issue, ...settings });
|
||||
displayTaskCreationResult(created, settings, piece);
|
||||
}
|
||||
|
||||
@ -40,6 +40,15 @@ export function stageAndCommit(cwd: string, message: string): string | undefined
|
||||
}).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and checks out a branch from origin. Throws on failure.
|
||||
*/
|
||||
export function checkoutBranch(cwd: string, branch: string): void {
|
||||
log.info('Checking out branch from origin', { branch });
|
||||
execFileSync('git', ['fetch', 'origin', branch], { cwd, stdio: 'pipe' });
|
||||
execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws on failure.
|
||||
*/
|
||||
|
||||
@ -55,7 +55,7 @@ export {
|
||||
getOriginalInstruction,
|
||||
buildListItems,
|
||||
} from './branchList.js';
|
||||
export { stageAndCommit, getCurrentBranch, pushBranch } from './git.js';
|
||||
export { stageAndCommit, getCurrentBranch, pushBranch, checkoutBranch } from './git.js';
|
||||
export { autoCommitAndPush, type AutoCommitResult } from './autoCommit.js';
|
||||
export { summarizeTaskName } from './summarize.js';
|
||||
export { TaskWatcher, type TaskWatcherOptions } from './watcher.js';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user