リトライ時でタスクにつめるようにする

This commit is contained in:
nrslib 2026-03-04 16:20:37 +09:00
parent 8ffe0592ef
commit 9fc8ab73fd
2 changed files with 38 additions and 12 deletions

View File

@ -2,7 +2,7 @@
* Tests for /retry slash command in the conversation loop. * Tests for /retry slash command in the conversation loop.
* *
* Verifies: * Verifies:
* - /retry with previousOrderContent returns execute action with order content * - /retry with previousOrderContent uses post-summary action selection
* - /retry without previousOrderContent shows error and continues loop * - /retry without previousOrderContent shows error and continues loop
* - /retry in retry mode with order.md context in system prompt * - /retry in retry mode with order.md context in system prompt
*/ */
@ -18,6 +18,7 @@ import {
createMockProvider, createMockProvider,
type MockProviderCapture, type MockProviderCapture,
} from './helpers/stdinSimulator.js'; } from './helpers/stdinSimulator.js';
import { selectOption } from '../shared/prompt/index.js';
// --- Mocks (infrastructure only) --- // --- Mocks (infrastructure only) ---
@ -147,7 +148,8 @@ describe('/retry slash command', () => {
rmSync(tmpDir, { recursive: true, force: true }); rmSync(tmpDir, { recursive: true, force: true });
}); });
it('should execute with previous order content when /retry is used', async () => { it('should route previous order content through action selection when /retry is used', async () => {
vi.mocked(selectOption).mockResolvedValueOnce('save_task');
const orderContent = '# Task Order\n\nImplement feature X with tests.'; const orderContent = '# Task Order\n\nImplement feature X with tests.';
setupRawStdin(toRawInputs(['/retry'])); setupRawStdin(toRawInputs(['/retry']));
setupProvider([]); setupProvider([]);
@ -155,7 +157,7 @@ describe('/retry slash command', () => {
const retryContext = buildRetryContext({ previousOrderContent: orderContent }); const retryContext = buildRetryContext({ previousOrderContent: orderContent });
const result = await runRetryMode(tmpDir, retryContext, orderContent); const result = await runRetryMode(tmpDir, retryContext, orderContent);
expect(result.action).toBe('execute'); expect(result.action).toBe('save_task');
expect(result.task).toBe(orderContent); expect(result.task).toBe(orderContent);
}); });
@ -170,6 +172,18 @@ describe('/retry slash command', () => {
expect(result.action).toBe('cancel'); expect(result.action).toBe('cancel');
}); });
it('should continue when /retry is selected with continue action', async () => {
vi.mocked(selectOption).mockResolvedValueOnce('continue');
setupRawStdin(toRawInputs(['/retry', '/cancel']));
setupProvider([]);
const orderContent = '# Task Order\n\nImplement feature X with tests.';
const retryContext = buildRetryContext({ previousOrderContent: orderContent });
const result = await runRetryMode(tmpDir, retryContext, orderContent);
expect(result.action).toBe('cancel');
});
it('should inject order.md content into retry system prompt', async () => { it('should inject order.md content into retry system prompt', async () => {
const orderContent = '# Build login page\n\nWith OAuth2 support.'; const orderContent = '# Build login page\n\nWith OAuth2 support.';
setupRawStdin(toRawInputs(['check the order', '/cancel'])); setupRawStdin(toRawInputs(['check the order', '/cancel']));

View File

@ -144,6 +144,18 @@ export async function runConversationLoop(
} }
} }
async function handleSummaryAction(task: string): Promise<InteractiveModeResult | null> {
const selectedAction = strategy.selectAction
? await strategy.selectAction(task, ctx.lang)
: await selectPostSummaryAction(task, ui.proposed, ui);
if (selectedAction === 'continue' || selectedAction === null) {
info(ui.continuePrompt);
return null;
}
log.info('Conversation action selected', { action: selectedAction, messageCount: history.length });
return { action: selectedAction, task };
}
while (true) { while (true) {
const input = await readMultilineInput(chalk.green('> ')); const input = await readMultilineInput(chalk.green('> '));
@ -203,8 +215,12 @@ export async function runConversationLoop(
info(ui.retryNoOrder); info(ui.retryNoOrder);
continue; continue;
} }
log.info('Retry command — resubmitting previous order.md'); log.info('Retry command — using previous order.md');
return { action: 'execute', task: strategy.previousOrderContent }; const selectedAction = await handleSummaryAction(strategy.previousOrderContent);
if (selectedAction === null) {
continue;
}
return selectedAction;
} }
case SlashCommand.Go: { case SlashCommand.Go: {
@ -234,15 +250,11 @@ export async function runConversationLoop(
return { action: 'cancel', task: '' }; return { action: 'cancel', task: '' };
} }
const task = summaryResult.content.trim(); const task = summaryResult.content.trim();
const selectedAction = strategy.selectAction const selectedAction = await handleSummaryAction(task);
? await strategy.selectAction(task, ctx.lang) if (selectedAction === null) {
: await selectPostSummaryAction(task, ui.proposed, ui);
if (selectedAction === 'continue' || selectedAction === null) {
info(ui.continuePrompt);
continue; continue;
} }
log.info('Conversation action selected', { action: selectedAction, messageCount: history.length }); return selectedAction;
return { action: selectedAction, task };
} }
case SlashCommand.Replay: { case SlashCommand.Replay: {