From 54001b51222ad74f93aff5c46b349590b6649cc2 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:16:47 +0900 Subject: [PATCH 1/2] takt: instruct --- src/__tests__/instructMode.test.ts | 64 ++++++++++- src/__tests__/interactive-summary.test.ts | 53 +++++++++ src/__tests__/it-retry-mode.test.ts | 15 ++- src/__tests__/orderReader.test.ts | 104 ++++++++++++++++++ src/__tests__/retryMode.test.ts | 27 +++++ src/__tests__/taskInstructionActions.test.ts | 4 + src/__tests__/taskRetryActions.test.ts | 2 + src/features/interactive/conversationLoop.ts | 12 ++ src/features/interactive/index.ts | 1 + .../interactive/interactive-summary.ts | 53 ++++++++- src/features/interactive/orderReader.ts | 57 ++++++++++ src/features/interactive/retryMode.ts | 35 ++---- src/features/tasks/list/instructMode.ts | 32 ++---- .../tasks/list/taskInstructionActions.ts | 6 +- src/features/tasks/list/taskRetryActions.ts | 4 +- src/shared/i18n/labels_en.yaml | 1 + src/shared/i18n/labels_ja.yaml | 1 + .../en/score_instruct_system_prompt.md | 10 +- .../prompts/en/score_retry_system_prompt.md | 10 +- .../ja/score_instruct_system_prompt.md | 10 +- .../prompts/ja/score_retry_system_prompt.md | 10 +- 21 files changed, 452 insertions(+), 59 deletions(-) create mode 100644 src/__tests__/orderReader.test.ts create mode 100644 src/features/interactive/orderReader.ts diff --git a/src/__tests__/instructMode.test.ts b/src/__tests__/instructMode.test.ts index 5ebb1a3..bcd200d 100644 --- a/src/__tests__/instructMode.test.ts +++ b/src/__tests__/instructMode.test.ts @@ -149,9 +149,10 @@ describe('runInstructMode', () => { expect(result.action).toBe('cancel'); }); - it('should use custom action selector without create_issue option', async () => { + it('should exclude execute from action selector options', async () => { setupRawStdin(toRawInputs(['task', '/go'])); setupMockProvider(['response', 'Task summary.']); + mockSelectOption.mockResolvedValue('save_task'); await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', ''); @@ -161,7 +162,7 @@ describe('runInstructMode', () => { expect(selectCall).toBeDefined(); const options = selectCall![1] as Array<{ value: string }>; const values = options.map((o) => o.value); - expect(values).toContain('execute'); + expect(values).not.toContain('execute'); expect(values).toContain('save_task'); expect(values).toContain('continue'); expect(values).not.toContain('create_issue'); @@ -215,4 +216,63 @@ describe('runInstructMode', () => { }), ); }); + + it('should inject previousOrderContent into template variables when provided', async () => { + setupRawStdin(toRawInputs(['/cancel'])); + setupMockProvider([]); + + await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', undefined, undefined, '# Previous Order\nDo the thing'); + + expect(mockLoadTemplate).toHaveBeenCalledWith( + 'score_instruct_system_prompt', + 'en', + expect.objectContaining({ + hasOrderContent: true, + orderContent: '# Previous Order\nDo the thing', + }), + ); + }); + + it('should set hasOrderContent=false when previousOrderContent is null', async () => { + setupRawStdin(toRawInputs(['/cancel'])); + setupMockProvider([]); + + await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', undefined, undefined, null); + + expect(mockLoadTemplate).toHaveBeenCalledWith( + 'score_instruct_system_prompt', + 'en', + expect.objectContaining({ + hasOrderContent: false, + orderContent: '', + }), + ); + }); + + it('should return execute with previous order content on /replay when previousOrderContent is set', async () => { + setupRawStdin(toRawInputs(['/replay'])); + setupMockProvider([]); + + const previousOrder = '# Previous Order\nDo the thing'; + const result = await runInstructMode( + '/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', + undefined, undefined, previousOrder, + ); + + expect(result.action).toBe('execute'); + expect(result.task).toBe(previousOrder); + }); + + it('should show error and continue when /replay is used without previousOrderContent', async () => { + setupRawStdin(toRawInputs(['/replay', '/cancel'])); + setupMockProvider([]); + + const result = await runInstructMode( + '/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', + undefined, undefined, null, + ); + + expect(result.action).toBe('cancel'); + expect(mockInfo).toHaveBeenCalledWith('Mock label'); + }); }); diff --git a/src/__tests__/interactive-summary.test.ts b/src/__tests__/interactive-summary.test.ts index b999491..310ea09 100644 --- a/src/__tests__/interactive-summary.test.ts +++ b/src/__tests__/interactive-summary.test.ts @@ -6,8 +6,10 @@ import { describe, expect, it } from 'vitest'; import { buildSummaryPrompt, + buildSummaryActionOptions, formatTaskHistorySummary, type PieceContext, + type SummaryActionLabels, type TaskHistorySummaryItem, } from '../features/interactive/interactive.js'; @@ -100,3 +102,54 @@ describe('buildSummaryPrompt', () => { expect(summary).toContain('User: Improve parser'); }); }); + +describe('buildSummaryActionOptions', () => { + const labels: SummaryActionLabels = { + execute: 'Execute now', + saveTask: 'Save as Task', + continue: 'Continue editing', + }; + + it('should include all base actions when no exclude is given', () => { + const options = buildSummaryActionOptions(labels); + const values = options.map((o) => o.value); + + expect(values).toEqual(['execute', 'save_task', 'continue']); + }); + + it('should exclude specified actions', () => { + const options = buildSummaryActionOptions(labels, [], ['execute']); + const values = options.map((o) => o.value); + + expect(values).toEqual(['save_task', 'continue']); + expect(values).not.toContain('execute'); + }); + + it('should exclude multiple actions', () => { + const options = buildSummaryActionOptions(labels, [], ['execute', 'continue']); + const values = options.map((o) => o.value); + + expect(values).toEqual(['save_task']); + }); + + it('should handle append and exclude together', () => { + const labelsWithIssue: SummaryActionLabels = { + ...labels, + createIssue: 'Create Issue', + }; + const options = buildSummaryActionOptions(labelsWithIssue, ['create_issue'], ['execute']); + const values = options.map((o) => o.value); + + expect(values).toEqual(['save_task', 'continue', 'create_issue']); + expect(values).not.toContain('execute'); + }); + + it('should return empty exclude by default (backward compatible)', () => { + const options = buildSummaryActionOptions(labels, []); + const values = options.map((o) => o.value); + + expect(values).toContain('execute'); + expect(values).toContain('save_task'); + expect(values).toContain('continue'); + }); +}); diff --git a/src/__tests__/it-retry-mode.test.ts b/src/__tests__/it-retry-mode.test.ts index bd4cac7..df5847b 100644 --- a/src/__tests__/it-retry-mode.test.ts +++ b/src/__tests__/it-retry-mode.test.ts @@ -191,6 +191,7 @@ describe('E2E: Retry mode with failure context injection', () => { const retryContext: RetryContext = { failure: { taskName: 'implement-auth', + taskContent: 'Implement authentication feature', createdAt: '2026-02-15T10:00:00Z', failedMovement: 'review', error: 'Timeout after 300s', @@ -207,7 +208,7 @@ describe('E2E: Retry mode with failure context injection', () => { run: null, }; - const result = await runRetryMode(tmpDir, retryContext); + const result = await runRetryMode(tmpDir, retryContext, null); // Verify: system prompt contains failure information expect(capture.systemPrompts.length).toBeGreaterThan(0); @@ -252,6 +253,7 @@ describe('E2E: Retry mode with failure context injection', () => { const retryContext: RetryContext = { failure: { taskName: 'build-login', + taskContent: 'Build login page with OAuth2', createdAt: '2026-02-15T14:00:00Z', failedMovement: 'implement', error: 'CSS compilation failed', @@ -276,7 +278,7 @@ describe('E2E: Retry mode with failure context injection', () => { }, }; - const result = await runRetryMode(tmpDir, retryContext); + const result = await runRetryMode(tmpDir, retryContext, null); // Verify: system prompt contains BOTH failure info and run session data const systemPrompt = capture.systemPrompts[0]!; @@ -314,6 +316,7 @@ describe('E2E: Retry mode with failure context injection', () => { const retryContext: RetryContext = { failure: { taskName: 'fix-tests', + taskContent: 'Fix failing test suite', createdAt: '2026-02-15T16:00:00Z', failedMovement: '', error: 'Test suite failed', @@ -330,7 +333,7 @@ describe('E2E: Retry mode with failure context injection', () => { run: null, }; - await runRetryMode(tmpDir, retryContext); + await runRetryMode(tmpDir, retryContext, null); const systemPrompt = capture.systemPrompts[0]!; expect(systemPrompt).toContain('Existing Retry Note'); @@ -348,6 +351,7 @@ describe('E2E: Retry mode with failure context injection', () => { const retryContext: RetryContext = { failure: { taskName: 'some-task', + taskContent: 'Complete some task', createdAt: '2026-02-15T12:00:00Z', failedMovement: 'plan', error: 'Unknown error', @@ -364,7 +368,7 @@ describe('E2E: Retry mode with failure context injection', () => { run: null, }; - const result = await runRetryMode(tmpDir, retryContext); + const result = await runRetryMode(tmpDir, retryContext, null); expect(result.action).toBe('cancel'); expect(result.task).toBe(''); @@ -385,6 +389,7 @@ describe('E2E: Retry mode with failure context injection', () => { const retryContext: RetryContext = { failure: { taskName: 'optimize-review', + taskContent: 'Optimize the review step', createdAt: '2026-02-15T18:00:00Z', failedMovement: 'review', error: 'Timeout', @@ -401,7 +406,7 @@ describe('E2E: Retry mode with failure context injection', () => { run: null, }; - const result = await runRetryMode(tmpDir, retryContext); + const result = await runRetryMode(tmpDir, retryContext, null); expect(result.action).toBe('execute'); expect(result.task).toBe('Increase review timeout to 600s and add retry logic.'); diff --git a/src/__tests__/orderReader.test.ts b/src/__tests__/orderReader.test.ts new file mode 100644 index 0000000..8001021 --- /dev/null +++ b/src/__tests__/orderReader.test.ts @@ -0,0 +1,104 @@ +/** + * Unit tests for orderReader: findPreviousOrderContent + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { findPreviousOrderContent } from '../features/interactive/orderReader.js'; + +const TEST_DIR = join(process.cwd(), 'tmp-test-order-reader'); + +function createRunWithOrder(slug: string, content: string): void { + const orderDir = join(TEST_DIR, '.takt', 'runs', slug, 'context', 'task'); + mkdirSync(orderDir, { recursive: true }); + writeFileSync(join(orderDir, 'order.md'), content, 'utf-8'); +} + +function createRunWithoutOrder(slug: string): void { + const runDir = join(TEST_DIR, '.takt', 'runs', slug); + mkdirSync(runDir, { recursive: true }); +} + +beforeEach(() => { + mkdirSync(TEST_DIR, { recursive: true }); +}); + +afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }); +}); + +describe('findPreviousOrderContent', () => { + it('should return order content when slug is specified and order.md exists', () => { + createRunWithOrder('20260218-run1', '# Task Order\nDo something'); + + const result = findPreviousOrderContent(TEST_DIR, '20260218-run1'); + + expect(result).toBe('# Task Order\nDo something'); + }); + + it('should return null when slug is specified but order.md does not exist', () => { + createRunWithoutOrder('20260218-run1'); + + const result = findPreviousOrderContent(TEST_DIR, '20260218-run1'); + + expect(result).toBeNull(); + }); + + it('should return null when slug is specified but run directory does not exist', () => { + mkdirSync(join(TEST_DIR, '.takt', 'runs'), { recursive: true }); + + const result = findPreviousOrderContent(TEST_DIR, 'nonexistent-slug'); + + expect(result).toBeNull(); + }); + + it('should return null for empty order.md content', () => { + createRunWithOrder('20260218-run1', ''); + + const result = findPreviousOrderContent(TEST_DIR, '20260218-run1'); + + expect(result).toBeNull(); + }); + + it('should return null for whitespace-only order.md content', () => { + createRunWithOrder('20260218-run1', ' \n '); + + const result = findPreviousOrderContent(TEST_DIR, '20260218-run1'); + + expect(result).toBeNull(); + }); + + it('should find order from latest run when slug is null', () => { + createRunWithOrder('20260218-run-a', 'First order'); + createRunWithOrder('20260219-run-b', 'Second order'); + + const result = findPreviousOrderContent(TEST_DIR, null); + + expect(result).toBe('Second order'); + }); + + it('should skip runs without order.md when searching latest', () => { + createRunWithOrder('20260218-run-a', 'First order'); + createRunWithoutOrder('20260219-run-b'); + + const result = findPreviousOrderContent(TEST_DIR, null); + + expect(result).toBe('First order'); + }); + + it('should return null when no runs have order.md', () => { + createRunWithoutOrder('20260218-run-a'); + createRunWithoutOrder('20260219-run-b'); + + const result = findPreviousOrderContent(TEST_DIR, null); + + expect(result).toBeNull(); + }); + + it('should return null when .takt/runs directory does not exist', () => { + const result = findPreviousOrderContent(TEST_DIR, null); + + expect(result).toBeNull(); + }); +}); diff --git a/src/__tests__/retryMode.test.ts b/src/__tests__/retryMode.test.ts index 4ef3dbe..cb3261b 100644 --- a/src/__tests__/retryMode.test.ts +++ b/src/__tests__/retryMode.test.ts @@ -9,6 +9,7 @@ function createRetryContext(overrides?: Partial): RetryContext { return { failure: { taskName: 'my-task', + taskContent: 'Do something', createdAt: '2026-02-15T10:00:00Z', failedMovement: 'review', error: 'Timeout', @@ -44,6 +45,7 @@ describe('buildRetryTemplateVars', () => { const ctx = createRetryContext({ failure: { taskName: 'task', + taskContent: 'Do something', createdAt: '2026-01-01T00:00:00Z', failedMovement: '', error: 'Error', @@ -133,6 +135,7 @@ describe('buildRetryTemplateVars', () => { const ctx = createRetryContext({ failure: { taskName: 'task', + taskContent: 'Do something', createdAt: '2026-01-01T00:00:00Z', failedMovement: '', error: 'Error', @@ -144,4 +147,28 @@ describe('buildRetryTemplateVars', () => { expect(vars.retryNote).toBe('Added more specific error handling'); }); + + it('should set hasOrderContent=false when previousOrderContent is null', () => { + const ctx = createRetryContext(); + const vars = buildRetryTemplateVars(ctx, 'en', null); + + expect(vars.hasOrderContent).toBe(false); + expect(vars.orderContent).toBe(''); + }); + + it('should set hasOrderContent=true and populate orderContent when provided', () => { + const ctx = createRetryContext(); + const vars = buildRetryTemplateVars(ctx, 'en', '# Previous Order\nDo the thing'); + + expect(vars.hasOrderContent).toBe(true); + expect(vars.orderContent).toBe('# Previous Order\nDo the thing'); + }); + + it('should default hasOrderContent to false when previousOrderContent is omitted', () => { + const ctx = createRetryContext(); + const vars = buildRetryTemplateVars(ctx, 'en'); + + expect(vars.hasOrderContent).toBe(false); + expect(vars.orderContent).toBe(''); + }); }); diff --git a/src/__tests__/taskInstructionActions.test.ts b/src/__tests__/taskInstructionActions.test.ts index 83d12e6..1ac715b 100644 --- a/src/__tests__/taskInstructionActions.test.ts +++ b/src/__tests__/taskInstructionActions.test.ts @@ -82,6 +82,8 @@ vi.mock('../features/interactive/index.js', () => ({ listRecentRuns: (...args: unknown[]) => mockListRecentRuns(...args), selectRun: (...args: unknown[]) => mockSelectRun(...args), loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args), + findRunForTask: vi.fn(() => null), + findPreviousOrderContent: vi.fn(() => null), })); vi.mock('../features/tasks/execute/taskExecution.js', () => ({ @@ -191,6 +193,7 @@ describe('instructBranch direct execution flow', () => { '', expect.anything(), undefined, + null, ); }); @@ -227,6 +230,7 @@ describe('instructBranch direct execution flow', () => { '', expect.anything(), runContext, + null, ); }); diff --git a/src/__tests__/taskRetryActions.test.ts b/src/__tests__/taskRetryActions.test.ts index b65f54d..a07200c 100644 --- a/src/__tests__/taskRetryActions.test.ts +++ b/src/__tests__/taskRetryActions.test.ts @@ -73,6 +73,7 @@ vi.mock('../features/interactive/index.js', () => ({ runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '', })), runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args), + findPreviousOrderContent: vi.fn(() => null), })); vi.mock('../infra/task/index.js', () => ({ @@ -151,6 +152,7 @@ describe('retryFailedTask', () => { expect.objectContaining({ failure: expect.objectContaining({ taskName: 'my-task', taskContent: 'Do something' }), }), + null, ); expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A'); expect(mockExecuteAndCompleteTask).toHaveBeenCalled(); diff --git a/src/features/interactive/conversationLoop.ts b/src/features/interactive/conversationLoop.ts index fb988ef..0d25d61 100644 --- a/src/features/interactive/conversationLoop.ts +++ b/src/features/interactive/conversationLoop.ts @@ -186,6 +186,8 @@ export interface ConversationStrategy { introMessage: string; /** Custom action selector (optional). If not provided, uses default selectPostSummaryAction. */ selectAction?: (task: string, lang: 'en' | 'ja') => Promise; + /** Previous order.md content for /replay command (retry/instruct only) */ + previousOrderContent?: string; } /** @@ -300,6 +302,16 @@ export async function runConversationLoop( return { action: selectedAction, task }; } + if (trimmed === '/replay') { + if (!strategy.previousOrderContent) { + const replayNoOrder = getLabel('instruct.ui.replayNoOrder', ctx.lang); + info(replayNoOrder); + continue; + } + log.info('Replay command'); + return { action: 'execute', task: strategy.previousOrderContent }; + } + if (trimmed === '/cancel') { info(ui.cancelled); return { action: 'cancel', task: '' }; diff --git a/src/features/interactive/index.ts b/src/features/interactive/index.ts index 36fb96b..0f4b101 100644 --- a/src/features/interactive/index.ts +++ b/src/features/interactive/index.ts @@ -25,3 +25,4 @@ export { selectRun } from './runSelector.js'; export { listRecentRuns, findRunForTask, loadRunSessionContext, formatRunSessionForPrompt, getRunPaths, type RunSessionContext, type RunPaths } from './runSessionReader.js'; export { runRetryMode, buildRetryTemplateVars, type RetryContext, type RetryFailureInfo, type RetryRunInfo } from './retryMode.js'; export { dispatchConversationAction, type ConversationActionResult } from './actionDispatcher.js'; +export { findPreviousOrderContent } from './orderReader.js'; diff --git a/src/features/interactive/interactive-summary.ts b/src/features/interactive/interactive-summary.ts index 13c64ae..68bf5e1 100644 --- a/src/features/interactive/interactive-summary.ts +++ b/src/features/interactive/interactive-summary.ts @@ -197,13 +197,15 @@ export interface InteractiveSummaryUIText { export function buildSummaryActionOptions( labels: SummaryActionLabels, append: readonly SummaryActionValue[] = [], + exclude: readonly SummaryActionValue[] = [], ): SummaryActionOption[] { const order = [...BASE_SUMMARY_ACTIONS, ...append]; + const excluded = new Set(exclude); const seen = new Set(); const options: SummaryActionOption[] = []; for (const action of order) { - if (seen.has(action)) { + if (seen.has(action) || excluded.has(action)) { continue; } seen.add(action); @@ -261,3 +263,52 @@ export function selectPostSummaryAction( ), ); } + +/** + * Build the /replay command hint for intro messages. + * + * Returns a hint string when previous order content is available, empty string otherwise. + */ +export function buildReplayHint(lang: 'en' | 'ja', hasPreviousOrder: boolean): string { + if (!hasPreviousOrder) return ''; + return lang === 'ja' + ? ', /replay(前回の指示書を再投入)' + : ', /replay (resubmit previous order)'; +} + +/** UI labels required by createSelectActionWithoutExecute */ +export interface ActionWithoutExecuteUIText { + proposed: string; + actionPrompt: string; + actions: { + execute: string; + saveTask: string; + continue: string; + }; +} + +/** + * Create an action selector that excludes the 'execute' option. + * + * Used by retry and instruct modes where worktree execution is assumed. + */ +export function createSelectActionWithoutExecute( + ui: ActionWithoutExecuteUIText, +): (task: string, lang: 'en' | 'ja') => Promise { + return async (task: string, _lang: 'en' | 'ja'): Promise => { + return selectSummaryAction( + task, + ui.proposed, + ui.actionPrompt, + buildSummaryActionOptions( + { + execute: ui.actions.execute, + saveTask: ui.actions.saveTask, + continue: ui.actions.continue, + }, + [], + ['execute'], + ), + ); + }; +} diff --git a/src/features/interactive/orderReader.ts b/src/features/interactive/orderReader.ts new file mode 100644 index 0000000..784389c --- /dev/null +++ b/src/features/interactive/orderReader.ts @@ -0,0 +1,57 @@ +/** + * Order reader for retry/instruct modes. + * + * Reads the previous order.md from a run's context directory + * to inject into conversation system prompts. + */ + +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * Find and read the previous order.md content from a run directory. + * + * When runSlug is provided, reads directly from that run's context. + * When runSlug is null, scans .takt/runs/ directories in reverse order + * and returns the first order.md found. + * + * @returns The order.md content, or null if not found. + */ +export function findPreviousOrderContent(worktreeCwd: string, runSlug: string | null): string | null { + if (runSlug) { + return readOrderFromRun(worktreeCwd, runSlug); + } + + return findOrderFromLatestRun(worktreeCwd); +} + +function readOrderFromRun(worktreeCwd: string, slug: string): string | null { + const orderPath = join(worktreeCwd, '.takt', 'runs', slug, 'context', 'task', 'order.md'); + if (!existsSync(orderPath)) { + return null; + } + const content = readFileSync(orderPath, 'utf-8').trim(); + return content || null; +} + +function findOrderFromLatestRun(worktreeCwd: string): string | null { + const runsDir = join(worktreeCwd, '.takt', 'runs'); + if (!existsSync(runsDir)) { + return null; + } + + const entries = readdirSync(runsDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort() + .reverse(); + + for (const slug of entries) { + const content = readOrderFromRun(worktreeCwd, slug); + if (content) { + return content; + } + } + + return null; +} diff --git a/src/features/interactive/retryMode.ts b/src/features/interactive/retryMode.ts index 4b2bbbe..882a2b0 100644 --- a/src/features/interactive/retryMode.ts +++ b/src/features/interactive/retryMode.ts @@ -11,11 +11,10 @@ import { runConversationLoop, type SessionContext, type ConversationStrategy, - type PostSummaryAction, } from './conversationLoop.js'; import { - buildSummaryActionOptions, - selectSummaryAction, + createSelectActionWithoutExecute, + buildReplayHint, formatMovementPreviews, type PieceContext, } from './interactive-summary.js'; @@ -60,7 +59,7 @@ const RETRY_TOOLS = ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch']; /** * Convert RetryContext into template variable map. */ -export function buildRetryTemplateVars(ctx: RetryContext, lang: 'en' | 'ja'): Record { +export function buildRetryTemplateVars(ctx: RetryContext, lang: 'en' | 'ja', previousOrderContent: string | null = null): Record { const hasPiecePreview = !!ctx.pieceContext.movementPreviews?.length; const movementDetails = hasPiecePreview ? formatMovementPreviews(ctx.pieceContext.movementPreviews!, lang) @@ -88,21 +87,8 @@ export function buildRetryTemplateVars(ctx: RetryContext, lang: 'en' | 'ja'): Re runStatus: hasRun ? ctx.run!.status : '', runMovementLogs: hasRun ? ctx.run!.movementLogs : '', runReports: hasRun ? ctx.run!.reports : '', - }; -} - -function createSelectRetryAction(ui: InstructUIText): (task: string, lang: 'en' | 'ja') => Promise { - return async (task: string, _lang: 'en' | 'ja'): Promise => { - return selectSummaryAction( - task, - ui.proposed, - ui.actionPrompt, - buildSummaryActionOptions({ - execute: ui.actions.execute, - saveTask: ui.actions.saveTask, - continue: ui.actions.continue, - }), - ); + hasOrderContent: previousOrderContent !== null, + orderContent: previousOrderContent ?? '', }; } @@ -115,6 +101,7 @@ function createSelectRetryAction(ui: InstructUIText): (task: string, lang: 'en' export async function runRetryMode( cwd: string, retryContext: RetryContext, + previousOrderContent: string | null, ): Promise { const globalConfig = loadGlobalConfig(); const lang = resolveLanguage(globalConfig.language); @@ -130,12 +117,13 @@ export async function runRetryMode( const ui = getLabelObject('instruct.ui', ctx.lang); - const templateVars = buildRetryTemplateVars(retryContext, lang); + const templateVars = buildRetryTemplateVars(retryContext, lang, previousOrderContent); const systemPrompt = loadTemplate('score_retry_system_prompt', ctx.lang, templateVars); + const replayHint = buildReplayHint(ctx.lang, previousOrderContent !== null); const introLabel = ctx.lang === 'ja' - ? `## リトライ: ${retryContext.failure.taskName}\n\nブランチ: ${retryContext.branchName}\n\n${ui.intro}` - : `## Retry: ${retryContext.failure.taskName}\n\nBranch: ${retryContext.branchName}\n\n${ui.intro}`; + ? `## リトライ: ${retryContext.failure.taskName}\n\nブランチ: ${retryContext.branchName}\n\n${ui.intro}${replayHint}` + : `## Retry: ${retryContext.failure.taskName}\n\nBranch: ${retryContext.branchName}\n\n${ui.intro}${replayHint}`; const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {}); @@ -154,7 +142,8 @@ export async function runRetryMode( allowedTools: RETRY_TOOLS, transformPrompt: injectPolicy, introMessage: introLabel, - selectAction: createSelectRetryAction(ui), + selectAction: createSelectActionWithoutExecute(ui), + previousOrderContent: previousOrderContent ?? undefined, }; const result = await runConversationLoop(cwd, ctx, strategy, retryContext.pieceContext, undefined); diff --git a/src/features/tasks/list/instructMode.ts b/src/features/tasks/list/instructMode.ts index 6cc56b8..5730893 100644 --- a/src/features/tasks/list/instructMode.ts +++ b/src/features/tasks/list/instructMode.ts @@ -11,15 +11,13 @@ import { runConversationLoop, type SessionContext, type ConversationStrategy, - type PostSummaryAction, } from '../../interactive/conversationLoop.js'; import { resolveLanguage, - buildSummaryActionOptions, - selectSummaryAction, formatMovementPreviews, type PieceContext, } from '../../interactive/interactive.js'; +import { createSelectActionWithoutExecute, buildReplayHint } from '../../interactive/interactive-summary.js'; import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js'; import { loadTemplate } from '../../../shared/prompts/index.js'; import { getLabelObject } from '../../../shared/i18n/index.js'; @@ -50,21 +48,6 @@ export interface InstructUIText { const INSTRUCT_TOOLS = ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch']; -function createSelectInstructAction(ui: InstructUIText): (task: string, lang: 'en' | 'ja') => Promise { - return async (task: string, _lang: 'en' | 'ja'): Promise => { - return selectSummaryAction( - task, - ui.proposed, - ui.actionPrompt, - buildSummaryActionOptions({ - execute: ui.actions.execute, - saveTask: ui.actions.saveTask, - continue: ui.actions.continue, - }), - ); - }; -} - function buildInstructTemplateVars( branchContext: string, branchName: string, @@ -74,6 +57,7 @@ function buildInstructTemplateVars( lang: 'en' | 'ja', pieceContext?: PieceContext, runSessionContext?: RunSessionContext, + previousOrderContent?: string | null, ): Record { const hasPiecePreview = !!pieceContext?.movementPreviews?.length; const movementDetails = hasPiecePreview @@ -96,6 +80,8 @@ function buildInstructTemplateVars( movementDetails, hasRunSession, ...runPromptVars, + hasOrderContent: !!previousOrderContent, + orderContent: previousOrderContent ?? '', }; } @@ -108,6 +94,7 @@ export async function runInstructMode( retryNote: string, pieceContext?: PieceContext, runSessionContext?: RunSessionContext, + previousOrderContent?: string | null, ): Promise { const globalConfig = loadGlobalConfig(); const lang = resolveLanguage(globalConfig.language); @@ -125,10 +112,12 @@ export async function runInstructMode( const templateVars = buildInstructTemplateVars( branchContext, branchName, taskName, taskContent, retryNote, lang, - pieceContext, runSessionContext, + pieceContext, runSessionContext, previousOrderContent, ); const systemPrompt = loadTemplate('score_instruct_system_prompt', ctx.lang, templateVars); + const replayHint = buildReplayHint(ctx.lang, !!previousOrderContent); + const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {}); function injectPolicy(userMessage: string): string { @@ -145,8 +134,9 @@ export async function runInstructMode( systemPrompt, allowedTools: INSTRUCT_TOOLS, transformPrompt: injectPolicy, - introMessage: ui.intro, - selectAction: createSelectInstructAction(ui), + introMessage: `${ui.intro}${replayHint}`, + selectAction: createSelectActionWithoutExecute(ui), + previousOrderContent: previousOrderContent ?? undefined, }; const result = await runConversationLoop(cwd, ctx, strategy, pieceContext, undefined); diff --git a/src/features/tasks/list/taskInstructionActions.ts b/src/features/tasks/list/taskInstructionActions.ts index 578962a..5c72927 100644 --- a/src/features/tasks/list/taskInstructionActions.ts +++ b/src/features/tasks/list/taskInstructionActions.ts @@ -18,7 +18,7 @@ import { runInstructMode } from './instructMode.js'; import { selectPiece } from '../../pieceSelection/index.js'; import { dispatchConversationAction } from '../../interactive/actionDispatcher.js'; import type { PieceContext } from '../../interactive/interactive.js'; -import { resolveLanguage } from '../../interactive/index.js'; +import { resolveLanguage, findRunForTask, findPreviousOrderContent } from '../../interactive/index.js'; import { type BranchActionTarget, resolveTargetBranch } from './taskActionTarget.js'; import { appendRetryNote, selectRunSessionContext } from './requeueHelpers.js'; import { executeAndCompleteTask } from '../execute/taskExecution.js'; @@ -105,13 +105,15 @@ export async function instructBranch( const lang = resolveLanguage(globalConfig.language); // Runs data lives in the worktree (written during previous execution) const runSessionContext = await selectRunSessionContext(worktreePath, lang); + const matchedSlug = findRunForTask(worktreePath, target.content); + const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug); const branchContext = getBranchContext(projectDir, branch); const result = await runInstructMode( worktreePath, branchContext, branch, target.name, target.content, target.data?.retry_note ?? '', - pieceContext, runSessionContext, + pieceContext, runSessionContext, previousOrderContent, ); const executeWithInstruction = async (instruction: string): Promise => { diff --git a/src/features/tasks/list/taskRetryActions.ts b/src/features/tasks/list/taskRetryActions.ts index 4a4bfce..e1baf9a 100644 --- a/src/features/tasks/list/taskRetryActions.ts +++ b/src/features/tasks/list/taskRetryActions.ts @@ -20,6 +20,7 @@ import { getRunPaths, formatRunSessionForPrompt, runRetryMode, + findPreviousOrderContent, type RetryContext, type RetryFailureInfo, type RetryRunInfo, @@ -156,6 +157,7 @@ export async function retryFailedTask( // Runs data lives in the worktree (written during previous execution) const matchedSlug = findRunForTask(worktreePath, task.content); const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null; + const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug); blankLine(); const branchName = task.branch ?? task.name; @@ -166,7 +168,7 @@ export async function retryFailedTask( run: runInfo, }; - const retryResult = await runRetryMode(worktreePath, retryContext); + const retryResult = await runRetryMode(worktreePath, retryContext, previousOrderContent); if (retryResult.action === 'cancel') { return false; } diff --git a/src/shared/i18n/labels_en.yaml b/src/shared/i18n/labels_en.yaml index 8e36598..c1ead6a 100644 --- a/src/shared/i18n/labels_en.yaml +++ b/src/shared/i18n/labels_en.yaml @@ -87,6 +87,7 @@ instruct: saveTask: "Save as Task" continue: "Continue editing" cancelled: "Cancelled" + replayNoOrder: "Previous order (order.md) not found" run: notifyComplete: "Run complete ({total} tasks)" diff --git a/src/shared/i18n/labels_ja.yaml b/src/shared/i18n/labels_ja.yaml index 6f1d93b..5a35605 100644 --- a/src/shared/i18n/labels_ja.yaml +++ b/src/shared/i18n/labels_ja.yaml @@ -87,6 +87,7 @@ instruct: saveTask: "タスクにつむ" continue: "会話を続ける" cancelled: "キャンセルしました" + replayNoOrder: "前回の指示書(order.md)が見つかりません" run: notifyComplete: "run完了 ({total} tasks)" diff --git a/src/shared/prompts/en/score_instruct_system_prompt.md b/src/shared/prompts/en/score_instruct_system_prompt.md index 881c1d2..a690ee5 100644 --- a/src/shared/prompts/en/score_instruct_system_prompt.md +++ b/src/shared/prompts/en/score_instruct_system_prompt.md @@ -1,7 +1,7 @@ # Additional Instruction Assistant @@ -85,3 +85,11 @@ The user has selected a previous run for reference. Use this information to help - Help the user identify what went wrong or what needs additional work - Suggest concrete follow-up instructions based on the run results {{/if}} +{{#if hasOrderContent}} + +## Previous Order (order.md) + +The instruction document used in the previous execution. Use it as a reference for re-execution. + +{{orderContent}} +{{/if}} diff --git a/src/shared/prompts/en/score_retry_system_prompt.md b/src/shared/prompts/en/score_retry_system_prompt.md index ca89064..1943f4c 100644 --- a/src/shared/prompts/en/score_retry_system_prompt.md +++ b/src/shared/prompts/en/score_retry_system_prompt.md @@ -1,7 +1,7 @@ # Retry Assistant @@ -95,3 +95,11 @@ Logs and reports from the previous execution are available for reference. Use th - Cross-reference the plans and implementation recorded in reports with the actual failure point - If the user wants more details, files in the directories above can be read using the Read tool {{/if}} +{{#if hasOrderContent}} + +## Previous Order (order.md) + +The instruction document used in the previous execution. Use it as a reference for re-execution. + +{{orderContent}} +{{/if}} diff --git a/src/shared/prompts/ja/score_instruct_system_prompt.md b/src/shared/prompts/ja/score_instruct_system_prompt.md index 74f12f8..e5b0367 100644 --- a/src/shared/prompts/ja/score_instruct_system_prompt.md +++ b/src/shared/prompts/ja/score_instruct_system_prompt.md @@ -1,7 +1,7 @@ # 追加指示アシスタント @@ -85,3 +85,11 @@ - 何がうまくいかなかったか、追加作業が必要な箇所をユーザーが特定できるよう支援してください - 実行結果に基づいて、具体的なフォローアップ指示を提案してください {{/if}} +{{#if hasOrderContent}} + +## 前回の指示書(order.md) + +前回の実行時に使用された指示書です。再実行の参考にしてください。 + +{{orderContent}} +{{/if}} diff --git a/src/shared/prompts/ja/score_retry_system_prompt.md b/src/shared/prompts/ja/score_retry_system_prompt.md index 85d3fce..a303ac1 100644 --- a/src/shared/prompts/ja/score_retry_system_prompt.md +++ b/src/shared/prompts/ja/score_retry_system_prompt.md @@ -1,7 +1,7 @@ # リトライアシスタント @@ -95,3 +95,11 @@ - レポートに記録された計画や実装内容と、実際の失敗箇所を照合してください - ユーザーが詳細を知りたい場合は、上記ディレクトリのファイルを Read ツールで参照できます {{/if}} +{{#if hasOrderContent}} + +## 前回の指示書(order.md) + +前回の実行時に使用された指示書です。再実行の参考にしてください。 + +{{orderContent}} +{{/if}}