Merge pull request #315 from nrslib/takt/308/improve-retry-instruct-interac

instruct
This commit is contained in:
nrs 2026-02-19 17:42:48 +09:00 committed by GitHub
commit e742897cac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 452 additions and 59 deletions

View File

@ -149,9 +149,10 @@ describe('runInstructMode', () => {
expect(result.action).toBe('cancel'); 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'])); setupRawStdin(toRawInputs(['task', '/go']));
setupMockProvider(['response', 'Task summary.']); setupMockProvider(['response', 'Task summary.']);
mockSelectOption.mockResolvedValue('save_task');
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', ''); await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
@ -161,7 +162,7 @@ describe('runInstructMode', () => {
expect(selectCall).toBeDefined(); expect(selectCall).toBeDefined();
const options = selectCall![1] as Array<{ value: string }>; const options = selectCall![1] as Array<{ value: string }>;
const values = options.map((o) => o.value); const values = options.map((o) => o.value);
expect(values).toContain('execute'); expect(values).not.toContain('execute');
expect(values).toContain('save_task'); expect(values).toContain('save_task');
expect(values).toContain('continue'); expect(values).toContain('continue');
expect(values).not.toContain('create_issue'); 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');
});
}); });

View File

@ -6,8 +6,10 @@ import { describe, expect, it } from 'vitest';
import { import {
buildSummaryPrompt, buildSummaryPrompt,
buildSummaryActionOptions,
formatTaskHistorySummary, formatTaskHistorySummary,
type PieceContext, type PieceContext,
type SummaryActionLabels,
type TaskHistorySummaryItem, type TaskHistorySummaryItem,
} from '../features/interactive/interactive.js'; } from '../features/interactive/interactive.js';
@ -100,3 +102,54 @@ describe('buildSummaryPrompt', () => {
expect(summary).toContain('User: Improve parser'); 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');
});
});

View File

@ -191,6 +191,7 @@ describe('E2E: Retry mode with failure context injection', () => {
const retryContext: RetryContext = { const retryContext: RetryContext = {
failure: { failure: {
taskName: 'implement-auth', taskName: 'implement-auth',
taskContent: 'Implement authentication feature',
createdAt: '2026-02-15T10:00:00Z', createdAt: '2026-02-15T10:00:00Z',
failedMovement: 'review', failedMovement: 'review',
error: 'Timeout after 300s', error: 'Timeout after 300s',
@ -207,7 +208,7 @@ describe('E2E: Retry mode with failure context injection', () => {
run: null, run: null,
}; };
const result = await runRetryMode(tmpDir, retryContext); const result = await runRetryMode(tmpDir, retryContext, null);
// Verify: system prompt contains failure information // Verify: system prompt contains failure information
expect(capture.systemPrompts.length).toBeGreaterThan(0); expect(capture.systemPrompts.length).toBeGreaterThan(0);
@ -252,6 +253,7 @@ describe('E2E: Retry mode with failure context injection', () => {
const retryContext: RetryContext = { const retryContext: RetryContext = {
failure: { failure: {
taskName: 'build-login', taskName: 'build-login',
taskContent: 'Build login page with OAuth2',
createdAt: '2026-02-15T14:00:00Z', createdAt: '2026-02-15T14:00:00Z',
failedMovement: 'implement', failedMovement: 'implement',
error: 'CSS compilation failed', 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 // Verify: system prompt contains BOTH failure info and run session data
const systemPrompt = capture.systemPrompts[0]!; const systemPrompt = capture.systemPrompts[0]!;
@ -314,6 +316,7 @@ describe('E2E: Retry mode with failure context injection', () => {
const retryContext: RetryContext = { const retryContext: RetryContext = {
failure: { failure: {
taskName: 'fix-tests', taskName: 'fix-tests',
taskContent: 'Fix failing test suite',
createdAt: '2026-02-15T16:00:00Z', createdAt: '2026-02-15T16:00:00Z',
failedMovement: '', failedMovement: '',
error: 'Test suite failed', error: 'Test suite failed',
@ -330,7 +333,7 @@ describe('E2E: Retry mode with failure context injection', () => {
run: null, run: null,
}; };
await runRetryMode(tmpDir, retryContext); await runRetryMode(tmpDir, retryContext, null);
const systemPrompt = capture.systemPrompts[0]!; const systemPrompt = capture.systemPrompts[0]!;
expect(systemPrompt).toContain('Existing Retry Note'); expect(systemPrompt).toContain('Existing Retry Note');
@ -348,6 +351,7 @@ describe('E2E: Retry mode with failure context injection', () => {
const retryContext: RetryContext = { const retryContext: RetryContext = {
failure: { failure: {
taskName: 'some-task', taskName: 'some-task',
taskContent: 'Complete some task',
createdAt: '2026-02-15T12:00:00Z', createdAt: '2026-02-15T12:00:00Z',
failedMovement: 'plan', failedMovement: 'plan',
error: 'Unknown error', error: 'Unknown error',
@ -364,7 +368,7 @@ describe('E2E: Retry mode with failure context injection', () => {
run: null, run: null,
}; };
const result = await runRetryMode(tmpDir, retryContext); const result = await runRetryMode(tmpDir, retryContext, null);
expect(result.action).toBe('cancel'); expect(result.action).toBe('cancel');
expect(result.task).toBe(''); expect(result.task).toBe('');
@ -385,6 +389,7 @@ describe('E2E: Retry mode with failure context injection', () => {
const retryContext: RetryContext = { const retryContext: RetryContext = {
failure: { failure: {
taskName: 'optimize-review', taskName: 'optimize-review',
taskContent: 'Optimize the review step',
createdAt: '2026-02-15T18:00:00Z', createdAt: '2026-02-15T18:00:00Z',
failedMovement: 'review', failedMovement: 'review',
error: 'Timeout', error: 'Timeout',
@ -401,7 +406,7 @@ describe('E2E: Retry mode with failure context injection', () => {
run: null, run: null,
}; };
const result = await runRetryMode(tmpDir, retryContext); const result = await runRetryMode(tmpDir, retryContext, null);
expect(result.action).toBe('execute'); expect(result.action).toBe('execute');
expect(result.task).toBe('Increase review timeout to 600s and add retry logic.'); expect(result.task).toBe('Increase review timeout to 600s and add retry logic.');

View File

@ -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();
});
});

View File

@ -9,6 +9,7 @@ function createRetryContext(overrides?: Partial<RetryContext>): RetryContext {
return { return {
failure: { failure: {
taskName: 'my-task', taskName: 'my-task',
taskContent: 'Do something',
createdAt: '2026-02-15T10:00:00Z', createdAt: '2026-02-15T10:00:00Z',
failedMovement: 'review', failedMovement: 'review',
error: 'Timeout', error: 'Timeout',
@ -44,6 +45,7 @@ describe('buildRetryTemplateVars', () => {
const ctx = createRetryContext({ const ctx = createRetryContext({
failure: { failure: {
taskName: 'task', taskName: 'task',
taskContent: 'Do something',
createdAt: '2026-01-01T00:00:00Z', createdAt: '2026-01-01T00:00:00Z',
failedMovement: '', failedMovement: '',
error: 'Error', error: 'Error',
@ -133,6 +135,7 @@ describe('buildRetryTemplateVars', () => {
const ctx = createRetryContext({ const ctx = createRetryContext({
failure: { failure: {
taskName: 'task', taskName: 'task',
taskContent: 'Do something',
createdAt: '2026-01-01T00:00:00Z', createdAt: '2026-01-01T00:00:00Z',
failedMovement: '', failedMovement: '',
error: 'Error', error: 'Error',
@ -144,4 +147,28 @@ describe('buildRetryTemplateVars', () => {
expect(vars.retryNote).toBe('Added more specific error handling'); 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('');
});
}); });

View File

@ -82,6 +82,8 @@ vi.mock('../features/interactive/index.js', () => ({
listRecentRuns: (...args: unknown[]) => mockListRecentRuns(...args), listRecentRuns: (...args: unknown[]) => mockListRecentRuns(...args),
selectRun: (...args: unknown[]) => mockSelectRun(...args), selectRun: (...args: unknown[]) => mockSelectRun(...args),
loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args), loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args),
findRunForTask: vi.fn(() => null),
findPreviousOrderContent: vi.fn(() => null),
})); }));
vi.mock('../features/tasks/execute/taskExecution.js', () => ({ vi.mock('../features/tasks/execute/taskExecution.js', () => ({
@ -191,6 +193,7 @@ describe('instructBranch direct execution flow', () => {
'', '',
expect.anything(), expect.anything(),
undefined, undefined,
null,
); );
}); });
@ -227,6 +230,7 @@ describe('instructBranch direct execution flow', () => {
'', '',
expect.anything(), expect.anything(),
runContext, runContext,
null,
); );
}); });

View File

@ -73,6 +73,7 @@ vi.mock('../features/interactive/index.js', () => ({
runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '', runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '',
})), })),
runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args), runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args),
findPreviousOrderContent: vi.fn(() => null),
})); }));
vi.mock('../infra/task/index.js', () => ({ vi.mock('../infra/task/index.js', () => ({
@ -151,6 +152,7 @@ describe('retryFailedTask', () => {
expect.objectContaining({ expect.objectContaining({
failure: expect.objectContaining({ taskName: 'my-task', taskContent: 'Do something' }), failure: expect.objectContaining({ taskName: 'my-task', taskContent: 'Do something' }),
}), }),
null,
); );
expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A'); expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A');
expect(mockExecuteAndCompleteTask).toHaveBeenCalled(); expect(mockExecuteAndCompleteTask).toHaveBeenCalled();

View File

@ -189,6 +189,8 @@ export interface ConversationStrategy {
introMessage: string; introMessage: string;
/** Custom action selector (optional). If not provided, uses default selectPostSummaryAction. */ /** Custom action selector (optional). If not provided, uses default selectPostSummaryAction. */
selectAction?: (task: string, lang: 'en' | 'ja') => Promise<PostSummaryAction | null>; selectAction?: (task: string, lang: 'en' | 'ja') => Promise<PostSummaryAction | null>;
/** Previous order.md content for /replay command (retry/instruct only) */
previousOrderContent?: string;
} }
/** /**
@ -303,6 +305,16 @@ export async function runConversationLoop(
return { action: selectedAction, task }; 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') { if (trimmed === '/cancel') {
info(ui.cancelled); info(ui.cancelled);
return { action: 'cancel', task: '' }; return { action: 'cancel', task: '' };

View File

@ -25,3 +25,4 @@ export { selectRun } from './runSelector.js';
export { listRecentRuns, findRunForTask, loadRunSessionContext, formatRunSessionForPrompt, getRunPaths, type RunSessionContext, type RunPaths } from './runSessionReader.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 { runRetryMode, buildRetryTemplateVars, type RetryContext, type RetryFailureInfo, type RetryRunInfo } from './retryMode.js';
export { dispatchConversationAction, type ConversationActionResult } from './actionDispatcher.js'; export { dispatchConversationAction, type ConversationActionResult } from './actionDispatcher.js';
export { findPreviousOrderContent } from './orderReader.js';

View File

@ -197,13 +197,15 @@ export interface InteractiveSummaryUIText {
export function buildSummaryActionOptions( export function buildSummaryActionOptions(
labels: SummaryActionLabels, labels: SummaryActionLabels,
append: readonly SummaryActionValue[] = [], append: readonly SummaryActionValue[] = [],
exclude: readonly SummaryActionValue[] = [],
): SummaryActionOption[] { ): SummaryActionOption[] {
const order = [...BASE_SUMMARY_ACTIONS, ...append]; const order = [...BASE_SUMMARY_ACTIONS, ...append];
const excluded = new Set(exclude);
const seen = new Set<SummaryActionValue>(); const seen = new Set<SummaryActionValue>();
const options: SummaryActionOption[] = []; const options: SummaryActionOption[] = [];
for (const action of order) { for (const action of order) {
if (seen.has(action)) { if (seen.has(action) || excluded.has(action)) {
continue; continue;
} }
seen.add(action); 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<PostSummaryAction | null> {
return async (task: string, _lang: 'en' | 'ja'): Promise<PostSummaryAction | null> => {
return selectSummaryAction(
task,
ui.proposed,
ui.actionPrompt,
buildSummaryActionOptions(
{
execute: ui.actions.execute,
saveTask: ui.actions.saveTask,
continue: ui.actions.continue,
},
[],
['execute'],
),
);
};
}

View File

@ -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;
}

View File

@ -11,11 +11,10 @@ import {
runConversationLoop, runConversationLoop,
type SessionContext, type SessionContext,
type ConversationStrategy, type ConversationStrategy,
type PostSummaryAction,
} from './conversationLoop.js'; } from './conversationLoop.js';
import { import {
buildSummaryActionOptions, createSelectActionWithoutExecute,
selectSummaryAction, buildReplayHint,
formatMovementPreviews, formatMovementPreviews,
type PieceContext, type PieceContext,
} from './interactive-summary.js'; } from './interactive-summary.js';
@ -60,7 +59,7 @@ const RETRY_TOOLS = ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
/** /**
* Convert RetryContext into template variable map. * Convert RetryContext into template variable map.
*/ */
export function buildRetryTemplateVars(ctx: RetryContext, lang: 'en' | 'ja'): Record<string, string | boolean> { export function buildRetryTemplateVars(ctx: RetryContext, lang: 'en' | 'ja', previousOrderContent: string | null = null): Record<string, string | boolean> {
const hasPiecePreview = !!ctx.pieceContext.movementPreviews?.length; const hasPiecePreview = !!ctx.pieceContext.movementPreviews?.length;
const movementDetails = hasPiecePreview const movementDetails = hasPiecePreview
? formatMovementPreviews(ctx.pieceContext.movementPreviews!, lang) ? formatMovementPreviews(ctx.pieceContext.movementPreviews!, lang)
@ -88,21 +87,8 @@ export function buildRetryTemplateVars(ctx: RetryContext, lang: 'en' | 'ja'): Re
runStatus: hasRun ? ctx.run!.status : '', runStatus: hasRun ? ctx.run!.status : '',
runMovementLogs: hasRun ? ctx.run!.movementLogs : '', runMovementLogs: hasRun ? ctx.run!.movementLogs : '',
runReports: hasRun ? ctx.run!.reports : '', runReports: hasRun ? ctx.run!.reports : '',
}; hasOrderContent: previousOrderContent !== null,
} orderContent: previousOrderContent ?? '',
function createSelectRetryAction(ui: InstructUIText): (task: string, lang: 'en' | 'ja') => Promise<PostSummaryAction | null> {
return async (task: string, _lang: 'en' | 'ja'): Promise<PostSummaryAction | null> => {
return selectSummaryAction(
task,
ui.proposed,
ui.actionPrompt,
buildSummaryActionOptions({
execute: ui.actions.execute,
saveTask: ui.actions.saveTask,
continue: ui.actions.continue,
}),
);
}; };
} }
@ -115,6 +101,7 @@ function createSelectRetryAction(ui: InstructUIText): (task: string, lang: 'en'
export async function runRetryMode( export async function runRetryMode(
cwd: string, cwd: string,
retryContext: RetryContext, retryContext: RetryContext,
previousOrderContent: string | null,
): Promise<InstructModeResult> { ): Promise<InstructModeResult> {
const globalConfig = resolveConfigValues(cwd, ['language', 'provider']); const globalConfig = resolveConfigValues(cwd, ['language', 'provider']);
const lang = resolveLanguage(globalConfig.language); const lang = resolveLanguage(globalConfig.language);
@ -130,12 +117,13 @@ export async function runRetryMode(
const ui = getLabelObject<InstructUIText>('instruct.ui', ctx.lang); const ui = getLabelObject<InstructUIText>('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 systemPrompt = loadTemplate('score_retry_system_prompt', ctx.lang, templateVars);
const replayHint = buildReplayHint(ctx.lang, previousOrderContent !== null);
const introLabel = ctx.lang === 'ja' const introLabel = ctx.lang === 'ja'
? `## リトライ: ${retryContext.failure.taskName}\n\nブランチ: ${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}`; : `## Retry: ${retryContext.failure.taskName}\n\nBranch: ${retryContext.branchName}\n\n${ui.intro}${replayHint}`;
const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {}); const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {});
@ -154,7 +142,8 @@ export async function runRetryMode(
allowedTools: RETRY_TOOLS, allowedTools: RETRY_TOOLS,
transformPrompt: injectPolicy, transformPrompt: injectPolicy,
introMessage: introLabel, introMessage: introLabel,
selectAction: createSelectRetryAction(ui), selectAction: createSelectActionWithoutExecute(ui),
previousOrderContent: previousOrderContent ?? undefined,
}; };
const result = await runConversationLoop(cwd, ctx, strategy, retryContext.pieceContext, undefined); const result = await runConversationLoop(cwd, ctx, strategy, retryContext.pieceContext, undefined);

View File

@ -11,15 +11,13 @@ import {
runConversationLoop, runConversationLoop,
type SessionContext, type SessionContext,
type ConversationStrategy, type ConversationStrategy,
type PostSummaryAction,
} from '../../interactive/conversationLoop.js'; } from '../../interactive/conversationLoop.js';
import { import {
resolveLanguage, resolveLanguage,
buildSummaryActionOptions,
selectSummaryAction,
formatMovementPreviews, formatMovementPreviews,
type PieceContext, type PieceContext,
} from '../../interactive/interactive.js'; } from '../../interactive/interactive.js';
import { createSelectActionWithoutExecute, buildReplayHint } from '../../interactive/interactive-summary.js';
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js'; import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
import { loadTemplate } from '../../../shared/prompts/index.js'; import { loadTemplate } from '../../../shared/prompts/index.js';
import { getLabelObject } from '../../../shared/i18n/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']; const INSTRUCT_TOOLS = ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
function createSelectInstructAction(ui: InstructUIText): (task: string, lang: 'en' | 'ja') => Promise<PostSummaryAction | null> {
return async (task: string, _lang: 'en' | 'ja'): Promise<PostSummaryAction | null> => {
return selectSummaryAction(
task,
ui.proposed,
ui.actionPrompt,
buildSummaryActionOptions({
execute: ui.actions.execute,
saveTask: ui.actions.saveTask,
continue: ui.actions.continue,
}),
);
};
}
function buildInstructTemplateVars( function buildInstructTemplateVars(
branchContext: string, branchContext: string,
branchName: string, branchName: string,
@ -74,6 +57,7 @@ function buildInstructTemplateVars(
lang: 'en' | 'ja', lang: 'en' | 'ja',
pieceContext?: PieceContext, pieceContext?: PieceContext,
runSessionContext?: RunSessionContext, runSessionContext?: RunSessionContext,
previousOrderContent?: string | null,
): Record<string, string | boolean> { ): Record<string, string | boolean> {
const hasPiecePreview = !!pieceContext?.movementPreviews?.length; const hasPiecePreview = !!pieceContext?.movementPreviews?.length;
const movementDetails = hasPiecePreview const movementDetails = hasPiecePreview
@ -96,6 +80,8 @@ function buildInstructTemplateVars(
movementDetails, movementDetails,
hasRunSession, hasRunSession,
...runPromptVars, ...runPromptVars,
hasOrderContent: !!previousOrderContent,
orderContent: previousOrderContent ?? '',
}; };
} }
@ -108,6 +94,7 @@ export async function runInstructMode(
retryNote: string, retryNote: string,
pieceContext?: PieceContext, pieceContext?: PieceContext,
runSessionContext?: RunSessionContext, runSessionContext?: RunSessionContext,
previousOrderContent?: string | null,
): Promise<InstructModeResult> { ): Promise<InstructModeResult> {
const globalConfig = resolvePieceConfigValues(cwd, ['language', 'provider']); const globalConfig = resolvePieceConfigValues(cwd, ['language', 'provider']);
const lang = resolveLanguage(globalConfig.language); const lang = resolveLanguage(globalConfig.language);
@ -125,10 +112,12 @@ export async function runInstructMode(
const templateVars = buildInstructTemplateVars( const templateVars = buildInstructTemplateVars(
branchContext, branchName, taskName, taskContent, retryNote, lang, branchContext, branchName, taskName, taskContent, retryNote, lang,
pieceContext, runSessionContext, pieceContext, runSessionContext, previousOrderContent,
); );
const systemPrompt = loadTemplate('score_instruct_system_prompt', ctx.lang, templateVars); const systemPrompt = loadTemplate('score_instruct_system_prompt', ctx.lang, templateVars);
const replayHint = buildReplayHint(ctx.lang, !!previousOrderContent);
const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {}); const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {});
function injectPolicy(userMessage: string): string { function injectPolicy(userMessage: string): string {
@ -145,8 +134,9 @@ export async function runInstructMode(
systemPrompt, systemPrompt,
allowedTools: INSTRUCT_TOOLS, allowedTools: INSTRUCT_TOOLS,
transformPrompt: injectPolicy, transformPrompt: injectPolicy,
introMessage: ui.intro, introMessage: `${ui.intro}${replayHint}`,
selectAction: createSelectInstructAction(ui), selectAction: createSelectActionWithoutExecute(ui),
previousOrderContent: previousOrderContent ?? undefined,
}; };
const result = await runConversationLoop(cwd, ctx, strategy, pieceContext, undefined); const result = await runConversationLoop(cwd, ctx, strategy, pieceContext, undefined);

View File

@ -18,7 +18,7 @@ import { runInstructMode } from './instructMode.js';
import { selectPiece } from '../../pieceSelection/index.js'; import { selectPiece } from '../../pieceSelection/index.js';
import { dispatchConversationAction } from '../../interactive/actionDispatcher.js'; import { dispatchConversationAction } from '../../interactive/actionDispatcher.js';
import type { PieceContext } from '../../interactive/interactive.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 { type BranchActionTarget, resolveTargetBranch } from './taskActionTarget.js';
import { appendRetryNote, selectRunSessionContext } from './requeueHelpers.js'; import { appendRetryNote, selectRunSessionContext } from './requeueHelpers.js';
import { executeAndCompleteTask } from '../execute/taskExecution.js'; import { executeAndCompleteTask } from '../execute/taskExecution.js';
@ -105,13 +105,15 @@ export async function instructBranch(
const lang = resolveLanguage(globalConfig.language); const lang = resolveLanguage(globalConfig.language);
// Runs data lives in the worktree (written during previous execution) // Runs data lives in the worktree (written during previous execution)
const runSessionContext = await selectRunSessionContext(worktreePath, lang); const runSessionContext = await selectRunSessionContext(worktreePath, lang);
const matchedSlug = findRunForTask(worktreePath, target.content);
const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug);
const branchContext = getBranchContext(projectDir, branch); const branchContext = getBranchContext(projectDir, branch);
const result = await runInstructMode( const result = await runInstructMode(
worktreePath, branchContext, branch, worktreePath, branchContext, branch,
target.name, target.content, target.data?.retry_note ?? '', target.name, target.content, target.data?.retry_note ?? '',
pieceContext, runSessionContext, pieceContext, runSessionContext, previousOrderContent,
); );
const executeWithInstruction = async (instruction: string): Promise<boolean> => { const executeWithInstruction = async (instruction: string): Promise<boolean> => {

View File

@ -20,6 +20,7 @@ import {
getRunPaths, getRunPaths,
formatRunSessionForPrompt, formatRunSessionForPrompt,
runRetryMode, runRetryMode,
findPreviousOrderContent,
type RetryContext, type RetryContext,
type RetryFailureInfo, type RetryFailureInfo,
type RetryRunInfo, type RetryRunInfo,
@ -156,6 +157,7 @@ export async function retryFailedTask(
// Runs data lives in the worktree (written during previous execution) // Runs data lives in the worktree (written during previous execution)
const matchedSlug = findRunForTask(worktreePath, task.content); const matchedSlug = findRunForTask(worktreePath, task.content);
const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null; const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null;
const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug);
blankLine(); blankLine();
const branchName = task.branch ?? task.name; const branchName = task.branch ?? task.name;
@ -166,7 +168,7 @@ export async function retryFailedTask(
run: runInfo, run: runInfo,
}; };
const retryResult = await runRetryMode(worktreePath, retryContext); const retryResult = await runRetryMode(worktreePath, retryContext, previousOrderContent);
if (retryResult.action === 'cancel') { if (retryResult.action === 'cancel') {
return false; return false;
} }

View File

@ -88,6 +88,7 @@ instruct:
saveTask: "Save as Task" saveTask: "Save as Task"
continue: "Continue editing" continue: "Continue editing"
cancelled: "Cancelled" cancelled: "Cancelled"
replayNoOrder: "Previous order (order.md) not found"
run: run:
notifyComplete: "Run complete ({total} tasks)" notifyComplete: "Run complete ({total} tasks)"

View File

@ -88,6 +88,7 @@ instruct:
saveTask: "タスクにつむ" saveTask: "タスクにつむ"
continue: "会話を続ける" continue: "会話を続ける"
cancelled: "キャンセルしました" cancelled: "キャンセルしました"
replayNoOrder: "前回の指示書order.mdが見つかりません"
run: run:
notifyComplete: "run完了 ({total} tasks)" notifyComplete: "run完了 ({total} tasks)"

View File

@ -1,7 +1,7 @@
<!-- <!--
template: score_instruct_system_prompt template: score_instruct_system_prompt
role: system prompt for instruct assistant mode (completed/failed tasks) role: system prompt for instruct assistant mode (completed/failed tasks)
vars: taskName, taskContent, branchName, branchContext, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRunSession, runTask, runPiece, runStatus, runMovementLogs, runReports vars: taskName, taskContent, branchName, branchContext, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRunSession, runTask, runPiece, runStatus, runMovementLogs, runReports, hasOrderContent, orderContent
caller: features/tasks/list/instructMode caller: features/tasks/list/instructMode
--> -->
# Additional Instruction Assistant # 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 - Help the user identify what went wrong or what needs additional work
- Suggest concrete follow-up instructions based on the run results - Suggest concrete follow-up instructions based on the run results
{{/if}} {{/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}}

View File

@ -1,7 +1,7 @@
<!-- <!--
template: score_retry_system_prompt template: score_retry_system_prompt
role: system prompt for retry assistant mode role: system prompt for retry assistant mode
vars: taskName, taskContent, branchName, createdAt, failedMovement, failureError, failureLastMessage, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRun, runLogsDir, runReportsDir, runTask, runPiece, runStatus, runMovementLogs, runReports vars: taskName, taskContent, branchName, createdAt, failedMovement, failureError, failureLastMessage, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRun, runLogsDir, runReportsDir, runTask, runPiece, runStatus, runMovementLogs, runReports, hasOrderContent, orderContent
caller: features/interactive/retryMode caller: features/interactive/retryMode
--> -->
# Retry Assistant # 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 - 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 the user wants more details, files in the directories above can be read using the Read tool
{{/if}} {{/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}}

View File

@ -1,7 +1,7 @@
<!-- <!--
template: score_instruct_system_prompt template: score_instruct_system_prompt
role: system prompt for instruct assistant mode (completed/failed tasks) role: system prompt for instruct assistant mode (completed/failed tasks)
vars: taskName, taskContent, branchName, branchContext, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRunSession, runTask, runPiece, runStatus, runMovementLogs, runReports vars: taskName, taskContent, branchName, branchContext, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRunSession, runTask, runPiece, runStatus, runMovementLogs, runReports, hasOrderContent, orderContent
caller: features/tasks/list/instructMode caller: features/tasks/list/instructMode
--> -->
# 追加指示アシスタント # 追加指示アシスタント
@ -85,3 +85,11 @@
- 何がうまくいかなかったか、追加作業が必要な箇所をユーザーが特定できるよう支援してください - 何がうまくいかなかったか、追加作業が必要な箇所をユーザーが特定できるよう支援してください
- 実行結果に基づいて、具体的なフォローアップ指示を提案してください - 実行結果に基づいて、具体的なフォローアップ指示を提案してください
{{/if}} {{/if}}
{{#if hasOrderContent}}
## 前回の指示書order.md
前回の実行時に使用された指示書です。再実行の参考にしてください。
{{orderContent}}
{{/if}}

View File

@ -1,7 +1,7 @@
<!-- <!--
template: score_retry_system_prompt template: score_retry_system_prompt
role: system prompt for retry assistant mode role: system prompt for retry assistant mode
vars: taskName, taskContent, branchName, createdAt, failedMovement, failureError, failureLastMessage, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRun, runLogsDir, runReportsDir, runTask, runPiece, runStatus, runMovementLogs, runReports vars: taskName, taskContent, branchName, createdAt, failedMovement, failureError, failureLastMessage, retryNote, hasPiecePreview, pieceStructure, movementDetails, hasRun, runLogsDir, runReportsDir, runTask, runPiece, runStatus, runMovementLogs, runReports, hasOrderContent, orderContent
caller: features/interactive/retryMode caller: features/interactive/retryMode
--> -->
# リトライアシスタント # リトライアシスタント
@ -95,3 +95,11 @@
- レポートに記録された計画や実装内容と、実際の失敗箇所を照合してください - レポートに記録された計画や実装内容と、実際の失敗箇所を照合してください
- ユーザーが詳細を知りたい場合は、上記ディレクトリのファイルを Read ツールで参照できます - ユーザーが詳細を知りたい場合は、上記ディレクトリのファイルを Read ツールで参照できます
{{/if}} {{/if}}
{{#if hasOrderContent}}
## 前回の指示書order.md
前回の実行時に使用された指示書です。再実行の参考にしてください。
{{orderContent}}
{{/if}}