リトライモード新設と instruct/retry の直接再実行対応
失敗タスク専用のリトライモード(retryMode.ts)を追加し、失敗情報・実行ログ・ レポートをシステムプロンプトに注入する方式に変更。instruct モードもタスク情報を プロンプトに含める専用テンプレートへ移行。requeue のみだった再実行を startReExecution による即時実行に対応し、既存ワークツリーの再利用も実装。 不要になった DebugConfig を削除。
This commit is contained in:
parent
85c845057e
commit
16d7f9f979
@ -25,6 +25,7 @@
|
|||||||
"test:e2e:codex": "npm run test:e2e:provider:codex",
|
"test:e2e:codex": "npm run test:e2e:provider:codex",
|
||||||
"test:e2e:opencode": "npm run test:e2e:provider:opencode",
|
"test:e2e:opencode": "npm run test:e2e:provider:opencode",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
|
"check:release": "npm run build && npm run lint && npm run test && npm run test:e2e",
|
||||||
"prepublishOnly": "npm run lint && npm run build && npm run test"
|
"prepublishOnly": "npm run lint && npm run build && npm run test"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@ -62,7 +62,6 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
initGlobalDirs: vi.fn(),
|
initGlobalDirs: vi.fn(),
|
||||||
initProjectDirs: vi.fn(),
|
initProjectDirs: vi.fn(),
|
||||||
loadGlobalConfig: vi.fn(() => ({ logLevel: 'info' })),
|
loadGlobalConfig: vi.fn(() => ({ logLevel: 'info' })),
|
||||||
getEffectiveDebugConfig: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/paths.js', () => ({
|
vi.mock('../infra/config/paths.js', () => ({
|
||||||
|
|||||||
@ -103,29 +103,17 @@ describe('runInstructMode', () => {
|
|||||||
setupRawStdin(toRawInputs(['/cancel']));
|
setupRawStdin(toRawInputs(['/cancel']));
|
||||||
setupMockProvider([]);
|
setupMockProvider([]);
|
||||||
|
|
||||||
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
||||||
|
|
||||||
expect(result.action).toBe('cancel');
|
expect(result.action).toBe('cancel');
|
||||||
expect(result.task).toBe('');
|
expect(result.task).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include branch name in intro message', async () => {
|
|
||||||
setupRawStdin(toRawInputs(['/cancel']));
|
|
||||||
setupMockProvider([]);
|
|
||||||
|
|
||||||
await runInstructMode('/project', 'diff stats', 'my-feature-branch');
|
|
||||||
|
|
||||||
const introCall = mockInfo.mock.calls.find((call) =>
|
|
||||||
call[0]?.includes('my-feature-branch')
|
|
||||||
);
|
|
||||||
expect(introCall).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return action=execute with task on /go after conversation', async () => {
|
it('should return action=execute with task on /go after conversation', async () => {
|
||||||
setupRawStdin(toRawInputs(['add more tests', '/go']));
|
setupRawStdin(toRawInputs(['add more tests', '/go']));
|
||||||
setupMockProvider(['What kind of tests?', 'Add unit tests for the feature.']);
|
setupMockProvider(['What kind of tests?', 'Add unit tests for the feature.']);
|
||||||
|
|
||||||
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
||||||
|
|
||||||
expect(result.action).toBe('execute');
|
expect(result.action).toBe('execute');
|
||||||
expect(result.task).toBe('Add unit tests for the feature.');
|
expect(result.task).toBe('Add unit tests for the feature.');
|
||||||
@ -136,7 +124,7 @@ describe('runInstructMode', () => {
|
|||||||
setupMockProvider(['response', 'Summarized task.']);
|
setupMockProvider(['response', 'Summarized task.']);
|
||||||
mockSelectOption.mockResolvedValue('save_task');
|
mockSelectOption.mockResolvedValue('save_task');
|
||||||
|
|
||||||
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
||||||
|
|
||||||
expect(result.action).toBe('save_task');
|
expect(result.action).toBe('save_task');
|
||||||
expect(result.task).toBe('Summarized task.');
|
expect(result.task).toBe('Summarized task.');
|
||||||
@ -147,7 +135,7 @@ describe('runInstructMode', () => {
|
|||||||
setupMockProvider(['response', 'Summarized task.']);
|
setupMockProvider(['response', 'Summarized task.']);
|
||||||
mockSelectOption.mockResolvedValueOnce('continue');
|
mockSelectOption.mockResolvedValueOnce('continue');
|
||||||
|
|
||||||
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
||||||
|
|
||||||
expect(result.action).toBe('cancel');
|
expect(result.action).toBe('cancel');
|
||||||
});
|
});
|
||||||
@ -156,7 +144,7 @@ describe('runInstructMode', () => {
|
|||||||
setupRawStdin(toRawInputs(['/go', '/cancel']));
|
setupRawStdin(toRawInputs(['/go', '/cancel']));
|
||||||
setupMockProvider([]);
|
setupMockProvider([]);
|
||||||
|
|
||||||
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
||||||
|
|
||||||
expect(result.action).toBe('cancel');
|
expect(result.action).toBe('cancel');
|
||||||
});
|
});
|
||||||
@ -165,7 +153,7 @@ describe('runInstructMode', () => {
|
|||||||
setupRawStdin(toRawInputs(['task', '/go']));
|
setupRawStdin(toRawInputs(['task', '/go']));
|
||||||
setupMockProvider(['response', 'Task summary.']);
|
setupMockProvider(['response', 'Task summary.']);
|
||||||
|
|
||||||
await runInstructMode('/project', 'branch context', 'feature-branch');
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
||||||
|
|
||||||
const selectCall = mockSelectOption.mock.calls.find((call) =>
|
const selectCall = mockSelectOption.mock.calls.find((call) =>
|
||||||
Array.isArray(call[1])
|
Array.isArray(call[1])
|
||||||
@ -179,6 +167,25 @@ describe('runInstructMode', () => {
|
|||||||
expect(values).not.toContain('create_issue');
|
expect(values).not.toContain('create_issue');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use dedicated instruct system prompt with task context', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['/cancel']));
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', 'existing note');
|
||||||
|
|
||||||
|
expect(mockLoadTemplate).toHaveBeenCalledWith(
|
||||||
|
'score_instruct_system_prompt',
|
||||||
|
'en',
|
||||||
|
expect.objectContaining({
|
||||||
|
taskName: 'my-task',
|
||||||
|
taskContent: 'Do something',
|
||||||
|
branchName: 'feature-branch',
|
||||||
|
branchContext: 'branch context',
|
||||||
|
retryNote: 'existing note',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should inject selected run context into system prompt variables', async () => {
|
it('should inject selected run context into system prompt variables', async () => {
|
||||||
setupRawStdin(toRawInputs(['/cancel']));
|
setupRawStdin(toRawInputs(['/cancel']));
|
||||||
setupMockProvider([]);
|
setupMockProvider([]);
|
||||||
@ -195,10 +202,10 @@ describe('runInstructMode', () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await runInstructMode('/project', 'branch context', 'feature-branch', undefined, runSessionContext);
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', undefined, runSessionContext);
|
||||||
|
|
||||||
expect(mockLoadTemplate).toHaveBeenCalledWith(
|
expect(mockLoadTemplate).toHaveBeenCalledWith(
|
||||||
'score_interactive_system_prompt',
|
'score_instruct_system_prompt',
|
||||||
'en',
|
'en',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
hasRunSession: true,
|
hasRunSession: true,
|
||||||
|
|||||||
@ -104,7 +104,7 @@ function setupScenarioProvider(...scenarios: Parameters<typeof createScenarioPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runInstruct() {
|
async function runInstruct() {
|
||||||
return runInstructMode('/test', '', 'takt/test-branch');
|
return runInstructMode('/test', '', 'takt/test-branch', 'test-branch', '', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -394,7 +394,7 @@ describe('policy injection', () => {
|
|||||||
setupRawStdin(toRawInputs(['fix the bug', '/cancel']));
|
setupRawStdin(toRawInputs(['fix the bug', '/cancel']));
|
||||||
const capture = setupProvider(['OK.']);
|
const capture = setupProvider(['OK.']);
|
||||||
|
|
||||||
await runInstructMode('/test', '', 'takt/test');
|
await runInstructMode('/test', '', 'takt/test', 'test', '', '');
|
||||||
|
|
||||||
// The prompt sent to AI should contain Policy section
|
// The prompt sent to AI should contain Policy section
|
||||||
expect(capture.prompts[0]).toContain('Policy');
|
expect(capture.prompts[0]).toContain('Policy');
|
||||||
@ -407,21 +407,22 @@ describe('policy injection', () => {
|
|||||||
// System prompt: branch name appears in intro
|
// System prompt: branch name appears in intro
|
||||||
// =================================================================
|
// =================================================================
|
||||||
describe('branch context', () => {
|
describe('branch context', () => {
|
||||||
it('should include branch name and context in intro', async () => {
|
it('should include branch name and context in system prompt', async () => {
|
||||||
setupRawStdin(toRawInputs(['/cancel']));
|
setupRawStdin(toRawInputs(['check changes', '/cancel']));
|
||||||
setupProvider([]);
|
const capture = setupProvider(['Looks good.']);
|
||||||
|
|
||||||
const { info: mockInfo } = await import('../shared/ui/index.js');
|
|
||||||
|
|
||||||
await runInstructMode(
|
await runInstructMode(
|
||||||
'/test',
|
'/test',
|
||||||
'## Changes\n```\nsrc/auth.ts | 50 +++\n```',
|
'## Changes\n```\nsrc/auth.ts | 50 +++\n```',
|
||||||
'takt/feature-auth',
|
'takt/feature-auth',
|
||||||
|
'feature-auth',
|
||||||
|
'Do something',
|
||||||
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
const introCall = vi.mocked(mockInfo).mock.calls.find((call) =>
|
expect(capture.systemPrompts.length).toBeGreaterThan(0);
|
||||||
call[0]?.includes('takt/feature-auth'),
|
const systemPrompt = capture.systemPrompts[0]!;
|
||||||
);
|
expect(systemPrompt).toContain('takt/feature-auth');
|
||||||
expect(introCall).toBeDefined();
|
expect(systemPrompt).toContain('src/auth.ts | 50 +++');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
410
src/__tests__/it-retry-mode.test.ts
Normal file
410
src/__tests__/it-retry-mode.test.ts
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
/**
|
||||||
|
* E2E test: Retry mode with failure context and run session injection.
|
||||||
|
*
|
||||||
|
* Simulates the retry assistant flow:
|
||||||
|
* 1. Create .takt/runs/ fixtures (logs, reports)
|
||||||
|
* 2. Build RetryContext with failure info + run session
|
||||||
|
* 3. Run retry mode with stdin simulation (user types message → /go)
|
||||||
|
* 4. Mock provider captures the system prompt sent to AI
|
||||||
|
* 5. Verify failure info AND run session data appear in the system prompt
|
||||||
|
*
|
||||||
|
* Real: buildRetryTemplateVars, loadTemplate, runConversationLoop,
|
||||||
|
* loadRunSessionContext, formatRunSessionForPrompt, getRunPaths
|
||||||
|
* Mocked: provider (captures system prompt), config, UI, session persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import {
|
||||||
|
setupRawStdin,
|
||||||
|
restoreStdin,
|
||||||
|
toRawInputs,
|
||||||
|
createMockProvider,
|
||||||
|
type MockProviderCapture,
|
||||||
|
} from './helpers/stdinSimulator.js';
|
||||||
|
|
||||||
|
// --- Mocks (infrastructure only) ---
|
||||||
|
|
||||||
|
vi.mock('../infra/fs/session.js', () => ({
|
||||||
|
loadNdjsonLog: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||||
|
loadGlobalConfig: vi.fn(() => ({ provider: 'mock', language: 'en' })),
|
||||||
|
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/providers/index.js', () => ({
|
||||||
|
getProvider: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
createLogger: () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/context.js', () => ({
|
||||||
|
isQuietMode: vi.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/paths.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
loadPersonaSessions: vi.fn(() => ({})),
|
||||||
|
updatePersonaSession: vi.fn(),
|
||||||
|
getProjectConfigDir: vi.fn(() => '/tmp'),
|
||||||
|
loadSessionState: vi.fn(() => null),
|
||||||
|
clearSessionState: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
blankLine: vi.fn(),
|
||||||
|
StreamDisplay: vi.fn().mockImplementation(() => ({
|
||||||
|
createHandler: vi.fn(() => vi.fn()),
|
||||||
|
flush: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
|
selectOption: vi.fn().mockResolvedValue('execute'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/i18n/index.js', () => ({
|
||||||
|
getLabel: vi.fn((_key: string, _lang: string) => 'Mock label'),
|
||||||
|
getLabelObject: vi.fn(() => ({
|
||||||
|
intro: 'Retry intro',
|
||||||
|
resume: 'Resume',
|
||||||
|
noConversation: 'No conversation',
|
||||||
|
summarizeFailed: 'Summarize failed',
|
||||||
|
continuePrompt: 'Continue?',
|
||||||
|
proposed: 'Proposed:',
|
||||||
|
actionPrompt: 'What next?',
|
||||||
|
playNoTask: 'No task',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
actions: { execute: 'Execute', saveTask: 'Save', continue: 'Continue' },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
|
import { getProvider } from '../infra/providers/index.js';
|
||||||
|
import { loadNdjsonLog } from '../infra/fs/session.js';
|
||||||
|
import {
|
||||||
|
loadRunSessionContext,
|
||||||
|
formatRunSessionForPrompt,
|
||||||
|
getRunPaths,
|
||||||
|
} from '../features/interactive/runSessionReader.js';
|
||||||
|
import { runRetryMode, type RetryContext } from '../features/interactive/retryMode.js';
|
||||||
|
|
||||||
|
const mockGetProvider = vi.mocked(getProvider);
|
||||||
|
const mockLoadNdjsonLog = vi.mocked(loadNdjsonLog);
|
||||||
|
|
||||||
|
// --- Fixture helpers ---
|
||||||
|
|
||||||
|
function createTmpDir(): string {
|
||||||
|
const dir = join(tmpdir(), `takt-retry-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRunFixture(
|
||||||
|
cwd: string,
|
||||||
|
slug: string,
|
||||||
|
overrides?: {
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
reports?: Array<{ name: string; content: string }>;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
const runDir = join(cwd, '.takt', 'runs', slug);
|
||||||
|
mkdirSync(join(runDir, 'logs'), { recursive: true });
|
||||||
|
mkdirSync(join(runDir, 'reports'), { recursive: true });
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
task: `Task for ${slug}`,
|
||||||
|
piece: 'default',
|
||||||
|
status: 'completed',
|
||||||
|
startTime: '2026-02-01T00:00:00.000Z',
|
||||||
|
logsDirectory: `.takt/runs/${slug}/logs`,
|
||||||
|
reportDirectory: `.takt/runs/${slug}/reports`,
|
||||||
|
runSlug: slug,
|
||||||
|
...overrides?.meta,
|
||||||
|
};
|
||||||
|
writeFileSync(join(runDir, 'meta.json'), JSON.stringify(meta), 'utf-8');
|
||||||
|
writeFileSync(join(runDir, 'logs', 'session-001.jsonl'), '{}', 'utf-8');
|
||||||
|
|
||||||
|
for (const report of overrides?.reports ?? []) {
|
||||||
|
writeFileSync(join(runDir, 'reports', report.name), report.content, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMockNdjsonLog(history: Array<{ step: string; persona: string; status: string; content: string }>): void {
|
||||||
|
mockLoadNdjsonLog.mockReturnValue({
|
||||||
|
task: 'mock',
|
||||||
|
projectDir: '',
|
||||||
|
pieceName: 'default',
|
||||||
|
iterations: history.length,
|
||||||
|
startTime: '2026-02-01T00:00:00.000Z',
|
||||||
|
status: 'completed',
|
||||||
|
history: history.map((h) => ({
|
||||||
|
...h,
|
||||||
|
instruction: '',
|
||||||
|
timestamp: '2026-02-01T00:00:00.000Z',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupProvider(responses: string[]): MockProviderCapture {
|
||||||
|
const { provider, capture } = createMockProvider(responses);
|
||||||
|
mockGetProvider.mockReturnValue(provider);
|
||||||
|
return capture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
describe('E2E: Retry mode with failure context injection', () => {
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = createTmpDir();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restoreStdin();
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should inject failure info into system prompt', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['what went wrong?', '/go']));
|
||||||
|
const capture = setupProvider([
|
||||||
|
'The review step failed due to a timeout.',
|
||||||
|
'Fix review timeout by increasing the limit.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const retryContext: RetryContext = {
|
||||||
|
failure: {
|
||||||
|
taskName: 'implement-auth',
|
||||||
|
createdAt: '2026-02-15T10:00:00Z',
|
||||||
|
failedMovement: 'review',
|
||||||
|
error: 'Timeout after 300s',
|
||||||
|
lastMessage: 'Agent stopped responding',
|
||||||
|
retryNote: '',
|
||||||
|
},
|
||||||
|
branchName: 'takt/implement-auth',
|
||||||
|
pieceContext: {
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '',
|
||||||
|
movementPreviews: [],
|
||||||
|
},
|
||||||
|
run: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runRetryMode(tmpDir, retryContext);
|
||||||
|
|
||||||
|
// Verify: system prompt contains failure information
|
||||||
|
expect(capture.systemPrompts.length).toBeGreaterThan(0);
|
||||||
|
const systemPrompt = capture.systemPrompts[0]!;
|
||||||
|
expect(systemPrompt).toContain('Retry Assistant');
|
||||||
|
expect(systemPrompt).toContain('implement-auth');
|
||||||
|
expect(systemPrompt).toContain('takt/implement-auth');
|
||||||
|
expect(systemPrompt).toContain('review');
|
||||||
|
expect(systemPrompt).toContain('Timeout after 300s');
|
||||||
|
expect(systemPrompt).toContain('Agent stopped responding');
|
||||||
|
|
||||||
|
// Verify: flow completed
|
||||||
|
expect(result.action).toBe('execute');
|
||||||
|
expect(result.task).toBe('Fix review timeout by increasing the limit.');
|
||||||
|
expect(capture.callCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should inject failure info AND run session data into system prompt', async () => {
|
||||||
|
// Create run fixture with logs and reports
|
||||||
|
createRunFixture(tmpDir, 'run-failed', {
|
||||||
|
meta: { task: 'Build login page', status: 'failed' },
|
||||||
|
reports: [
|
||||||
|
{ name: '00-plan.md', content: '# Plan\n\nLogin form with OAuth2.' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
setupMockNdjsonLog([
|
||||||
|
{ step: 'plan', persona: 'architect', status: 'completed', content: 'Planned OAuth2 login flow' },
|
||||||
|
{ step: 'implement', persona: 'coder', status: 'failed', content: 'Failed at CSS compilation' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Load real run session data
|
||||||
|
const sessionContext = loadRunSessionContext(tmpDir, 'run-failed');
|
||||||
|
const formatted = formatRunSessionForPrompt(sessionContext);
|
||||||
|
const paths = getRunPaths(tmpDir, 'run-failed');
|
||||||
|
|
||||||
|
setupRawStdin(toRawInputs(['fix the CSS issue', '/go']));
|
||||||
|
const capture = setupProvider([
|
||||||
|
'The CSS compilation error is likely due to missing imports.',
|
||||||
|
'Fix CSS imports in login component.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const retryContext: RetryContext = {
|
||||||
|
failure: {
|
||||||
|
taskName: 'build-login',
|
||||||
|
createdAt: '2026-02-15T14:00:00Z',
|
||||||
|
failedMovement: 'implement',
|
||||||
|
error: 'CSS compilation failed',
|
||||||
|
lastMessage: 'PostCSS error: unknown property',
|
||||||
|
retryNote: '',
|
||||||
|
},
|
||||||
|
branchName: 'takt/build-login',
|
||||||
|
pieceContext: {
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '',
|
||||||
|
movementPreviews: [],
|
||||||
|
},
|
||||||
|
run: {
|
||||||
|
logsDir: paths.logsDir,
|
||||||
|
reportsDir: paths.reportsDir,
|
||||||
|
task: formatted.runTask,
|
||||||
|
piece: formatted.runPiece,
|
||||||
|
status: formatted.runStatus,
|
||||||
|
movementLogs: formatted.runMovementLogs,
|
||||||
|
reports: formatted.runReports,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runRetryMode(tmpDir, retryContext);
|
||||||
|
|
||||||
|
// Verify: system prompt contains BOTH failure info and run session data
|
||||||
|
const systemPrompt = capture.systemPrompts[0]!;
|
||||||
|
|
||||||
|
// Failure info
|
||||||
|
expect(systemPrompt).toContain('build-login');
|
||||||
|
expect(systemPrompt).toContain('CSS compilation failed');
|
||||||
|
expect(systemPrompt).toContain('PostCSS error: unknown property');
|
||||||
|
expect(systemPrompt).toContain('implement');
|
||||||
|
|
||||||
|
// Run session data
|
||||||
|
expect(systemPrompt).toContain('Previous Run Data');
|
||||||
|
expect(systemPrompt).toContain('Build login page');
|
||||||
|
expect(systemPrompt).toContain('Planned OAuth2 login flow');
|
||||||
|
expect(systemPrompt).toContain('Failed at CSS compilation');
|
||||||
|
expect(systemPrompt).toContain('00-plan.md');
|
||||||
|
expect(systemPrompt).toContain('Login form with OAuth2');
|
||||||
|
|
||||||
|
// Run paths (AI can use Read tool)
|
||||||
|
expect(systemPrompt).toContain(paths.logsDir);
|
||||||
|
expect(systemPrompt).toContain(paths.reportsDir);
|
||||||
|
|
||||||
|
// Flow completed
|
||||||
|
expect(result.action).toBe('execute');
|
||||||
|
expect(result.task).toBe('Fix CSS imports in login component.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include existing retry note in system prompt', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['what should I do?', '/go']));
|
||||||
|
const capture = setupProvider([
|
||||||
|
'Based on the previous attempt, the mocks are still incomplete.',
|
||||||
|
'Add complete mocks for all API endpoints.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const retryContext: RetryContext = {
|
||||||
|
failure: {
|
||||||
|
taskName: 'fix-tests',
|
||||||
|
createdAt: '2026-02-15T16:00:00Z',
|
||||||
|
failedMovement: '',
|
||||||
|
error: 'Test suite failed',
|
||||||
|
lastMessage: '',
|
||||||
|
retryNote: 'Previous attempt: added missing mocks but still failing',
|
||||||
|
},
|
||||||
|
branchName: 'takt/fix-tests',
|
||||||
|
pieceContext: {
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '',
|
||||||
|
movementPreviews: [],
|
||||||
|
},
|
||||||
|
run: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await runRetryMode(tmpDir, retryContext);
|
||||||
|
|
||||||
|
const systemPrompt = capture.systemPrompts[0]!;
|
||||||
|
expect(systemPrompt).toContain('Existing Retry Note');
|
||||||
|
expect(systemPrompt).toContain('Previous attempt: added missing mocks but still failing');
|
||||||
|
|
||||||
|
// absent fields should NOT appear as sections
|
||||||
|
expect(systemPrompt).not.toContain('Failed movement');
|
||||||
|
expect(systemPrompt).not.toContain('Last Message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cancel cleanly and not crash', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['/cancel']));
|
||||||
|
setupProvider([]);
|
||||||
|
|
||||||
|
const retryContext: RetryContext = {
|
||||||
|
failure: {
|
||||||
|
taskName: 'some-task',
|
||||||
|
createdAt: '2026-02-15T12:00:00Z',
|
||||||
|
failedMovement: 'plan',
|
||||||
|
error: 'Unknown error',
|
||||||
|
lastMessage: '',
|
||||||
|
retryNote: '',
|
||||||
|
},
|
||||||
|
branchName: 'takt/some-task',
|
||||||
|
pieceContext: {
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '',
|
||||||
|
movementPreviews: [],
|
||||||
|
},
|
||||||
|
run: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runRetryMode(tmpDir, retryContext);
|
||||||
|
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
expect(result.task).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conversation before /go with failure context', async () => {
|
||||||
|
setupRawStdin(toRawInputs([
|
||||||
|
'what was the error?',
|
||||||
|
'can you suggest a fix?',
|
||||||
|
'/go',
|
||||||
|
]));
|
||||||
|
const capture = setupProvider([
|
||||||
|
'The error was a timeout in the review step.',
|
||||||
|
'You could increase the timeout limit or optimize the review.',
|
||||||
|
'Increase review timeout to 600s and add retry logic.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const retryContext: RetryContext = {
|
||||||
|
failure: {
|
||||||
|
taskName: 'optimize-review',
|
||||||
|
createdAt: '2026-02-15T18:00:00Z',
|
||||||
|
failedMovement: 'review',
|
||||||
|
error: 'Timeout',
|
||||||
|
lastMessage: '',
|
||||||
|
retryNote: '',
|
||||||
|
},
|
||||||
|
branchName: 'takt/optimize-review',
|
||||||
|
pieceContext: {
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '',
|
||||||
|
movementPreviews: [],
|
||||||
|
},
|
||||||
|
run: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runRetryMode(tmpDir, retryContext);
|
||||||
|
|
||||||
|
expect(result.action).toBe('execute');
|
||||||
|
expect(result.task).toBe('Increase review timeout to 600s and add retry logic.');
|
||||||
|
expect(capture.callCount).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -213,6 +213,9 @@ describe('E2E: Run session → instruct mode with interactive flow', () => {
|
|||||||
tmpDir,
|
tmpDir,
|
||||||
'## Branch: takt/fix-auth\n',
|
'## Branch: takt/fix-auth\n',
|
||||||
'takt/fix-auth',
|
'takt/fix-auth',
|
||||||
|
'fix-auth',
|
||||||
|
'Implement JWT auth',
|
||||||
|
'',
|
||||||
{ name: 'default', description: '', pieceStructure: '', movementPreviews: [] },
|
{ name: 'default', description: '', pieceStructure: '', movementPreviews: [] },
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
@ -239,7 +242,7 @@ describe('E2E: Run session → instruct mode with interactive flow', () => {
|
|||||||
setupRawStdin(toRawInputs(['/cancel']));
|
setupRawStdin(toRawInputs(['/cancel']));
|
||||||
setupProvider([]);
|
setupProvider([]);
|
||||||
|
|
||||||
const result = await runInstructMode(tmpDir, '', 'takt/fix', undefined, undefined);
|
const result = await runInstructMode(tmpDir, '', 'takt/fix', 'fix', '', '', undefined, undefined);
|
||||||
|
|
||||||
expect(result.action).toBe('cancel');
|
expect(result.action).toBe('cancel');
|
||||||
});
|
});
|
||||||
@ -254,7 +257,7 @@ describe('E2E: Run session → instruct mode with interactive flow', () => {
|
|||||||
const capture = setupProvider(['I understand.']);
|
const capture = setupProvider(['I understand.']);
|
||||||
|
|
||||||
const result = await runInstructMode(
|
const result = await runInstructMode(
|
||||||
tmpDir, '', 'takt/branch', undefined, context,
|
tmpDir, '', 'takt/branch', 'branch', '', '', undefined, context,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.action).toBe('cancel');
|
expect(result.action).toBe('cancel');
|
||||||
|
|||||||
@ -35,6 +35,16 @@ describe('loadTemplate', () => {
|
|||||||
expect(result).toContain('対話モードポリシー');
|
expect(result).toContain('対話モードポリシー');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('loads an English retry system prompt template', () => {
|
||||||
|
const result = loadTemplate('score_retry_system_prompt', 'en');
|
||||||
|
expect(result).toContain('Retry Assistant');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads a Japanese retry system prompt template', () => {
|
||||||
|
const result = loadTemplate('score_retry_system_prompt', 'ja');
|
||||||
|
expect(result).toContain('リトライアシスタント');
|
||||||
|
});
|
||||||
|
|
||||||
it('loads score_slug_system_prompt with explicit lang', () => {
|
it('loads score_slug_system_prompt with explicit lang', () => {
|
||||||
const result = loadTemplate('score_slug_system_prompt', 'en');
|
const result = loadTemplate('score_slug_system_prompt', 'en');
|
||||||
expect(result).toContain('You are a slug generator');
|
expect(result).toContain('You are a slug generator');
|
||||||
|
|||||||
147
src/__tests__/retryMode.test.ts
Normal file
147
src/__tests__/retryMode.test.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for retryMode: buildRetryTemplateVars
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { buildRetryTemplateVars, type RetryContext } from '../features/interactive/retryMode.js';
|
||||||
|
|
||||||
|
function createRetryContext(overrides?: Partial<RetryContext>): RetryContext {
|
||||||
|
return {
|
||||||
|
failure: {
|
||||||
|
taskName: 'my-task',
|
||||||
|
createdAt: '2026-02-15T10:00:00Z',
|
||||||
|
failedMovement: 'review',
|
||||||
|
error: 'Timeout',
|
||||||
|
lastMessage: 'Agent stopped',
|
||||||
|
retryNote: '',
|
||||||
|
},
|
||||||
|
branchName: 'takt/my-task',
|
||||||
|
pieceContext: {
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '1. plan → 2. implement → 3. review',
|
||||||
|
movementPreviews: [],
|
||||||
|
},
|
||||||
|
run: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('buildRetryTemplateVars', () => {
|
||||||
|
it('should map failure info to template variables', () => {
|
||||||
|
const ctx = createRetryContext();
|
||||||
|
const vars = buildRetryTemplateVars(ctx, 'en');
|
||||||
|
|
||||||
|
expect(vars.taskName).toBe('my-task');
|
||||||
|
expect(vars.branchName).toBe('takt/my-task');
|
||||||
|
expect(vars.createdAt).toBe('2026-02-15T10:00:00Z');
|
||||||
|
expect(vars.failedMovement).toBe('review');
|
||||||
|
expect(vars.failureError).toBe('Timeout');
|
||||||
|
expect(vars.failureLastMessage).toBe('Agent stopped');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set empty string for absent optional fields', () => {
|
||||||
|
const ctx = createRetryContext({
|
||||||
|
failure: {
|
||||||
|
taskName: 'task',
|
||||||
|
createdAt: '2026-01-01T00:00:00Z',
|
||||||
|
failedMovement: '',
|
||||||
|
error: 'Error',
|
||||||
|
lastMessage: '',
|
||||||
|
retryNote: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const vars = buildRetryTemplateVars(ctx, 'en');
|
||||||
|
|
||||||
|
expect(vars.failedMovement).toBe('');
|
||||||
|
expect(vars.failureLastMessage).toBe('');
|
||||||
|
expect(vars.retryNote).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set hasRun=false and empty run vars when run is null', () => {
|
||||||
|
const ctx = createRetryContext({ run: null });
|
||||||
|
const vars = buildRetryTemplateVars(ctx, 'en');
|
||||||
|
|
||||||
|
expect(vars.hasRun).toBe(false);
|
||||||
|
expect(vars.runLogsDir).toBe('');
|
||||||
|
expect(vars.runReportsDir).toBe('');
|
||||||
|
expect(vars.runTask).toBe('');
|
||||||
|
expect(vars.runPiece).toBe('');
|
||||||
|
expect(vars.runStatus).toBe('');
|
||||||
|
expect(vars.runMovementLogs).toBe('');
|
||||||
|
expect(vars.runReports).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set hasRun=true and populate run vars when run is provided', () => {
|
||||||
|
const ctx = createRetryContext({
|
||||||
|
run: {
|
||||||
|
logsDir: '/project/.takt/runs/slug/logs',
|
||||||
|
reportsDir: '/project/.takt/runs/slug/reports',
|
||||||
|
task: 'Build feature',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'failed',
|
||||||
|
movementLogs: '### plan\nPlanned.',
|
||||||
|
reports: '### 00-plan.md\n# Plan',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const vars = buildRetryTemplateVars(ctx, 'en');
|
||||||
|
|
||||||
|
expect(vars.hasRun).toBe(true);
|
||||||
|
expect(vars.runLogsDir).toBe('/project/.takt/runs/slug/logs');
|
||||||
|
expect(vars.runReportsDir).toBe('/project/.takt/runs/slug/reports');
|
||||||
|
expect(vars.runTask).toBe('Build feature');
|
||||||
|
expect(vars.runPiece).toBe('default');
|
||||||
|
expect(vars.runStatus).toBe('failed');
|
||||||
|
expect(vars.runMovementLogs).toBe('### plan\nPlanned.');
|
||||||
|
expect(vars.runReports).toBe('### 00-plan.md\n# Plan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set hasPiecePreview=false when no movement previews', () => {
|
||||||
|
const ctx = createRetryContext();
|
||||||
|
const vars = buildRetryTemplateVars(ctx, 'en');
|
||||||
|
|
||||||
|
expect(vars.hasPiecePreview).toBe(false);
|
||||||
|
expect(vars.movementDetails).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set hasPiecePreview=true and format movement details when previews exist', () => {
|
||||||
|
const ctx = createRetryContext({
|
||||||
|
pieceContext: {
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '1. plan',
|
||||||
|
movementPreviews: [
|
||||||
|
{
|
||||||
|
name: 'plan',
|
||||||
|
personaDisplayName: 'Architect',
|
||||||
|
personaContent: 'You are an architect.',
|
||||||
|
instructionContent: 'Plan the feature.',
|
||||||
|
allowedTools: ['Read', 'Grep'],
|
||||||
|
canEdit: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const vars = buildRetryTemplateVars(ctx, 'en');
|
||||||
|
|
||||||
|
expect(vars.hasPiecePreview).toBe(true);
|
||||||
|
expect(vars.movementDetails).toContain('plan');
|
||||||
|
expect(vars.movementDetails).toContain('Architect');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include retryNote when present', () => {
|
||||||
|
const ctx = createRetryContext({
|
||||||
|
failure: {
|
||||||
|
taskName: 'task',
|
||||||
|
createdAt: '2026-01-01T00:00:00Z',
|
||||||
|
failedMovement: '',
|
||||||
|
error: 'Error',
|
||||||
|
lastMessage: '',
|
||||||
|
retryNote: 'Added more specific error handling',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const vars = buildRetryTemplateVars(ctx, 'en');
|
||||||
|
|
||||||
|
expect(vars.retryNote).toBe('Added more specific error handling');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -14,6 +14,7 @@ vi.mock('../infra/fs/session.js', () => ({
|
|||||||
import { loadNdjsonLog } from '../infra/fs/session.js';
|
import { loadNdjsonLog } from '../infra/fs/session.js';
|
||||||
import {
|
import {
|
||||||
listRecentRuns,
|
listRecentRuns,
|
||||||
|
findRunForTask,
|
||||||
loadRunSessionContext,
|
loadRunSessionContext,
|
||||||
formatRunSessionForPrompt,
|
formatRunSessionForPrompt,
|
||||||
type RunSessionContext,
|
type RunSessionContext,
|
||||||
@ -107,6 +108,78 @@ describe('listRecentRuns', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findRunForTask', () => {
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = createTmpDir();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no runs exist', () => {
|
||||||
|
const result = findRunForTask(tmpDir, 'Some task');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no runs match the task content', () => {
|
||||||
|
createRunDir(tmpDir, 'run-other', {
|
||||||
|
task: 'Different task',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'completed',
|
||||||
|
startTime: '2026-02-01T00:00:00.000Z',
|
||||||
|
logsDirectory: '.takt/runs/run-other/logs',
|
||||||
|
reportDirectory: '.takt/runs/run-other/reports',
|
||||||
|
runSlug: 'run-other',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findRunForTask(tmpDir, 'My specific task');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the matching run slug', () => {
|
||||||
|
createRunDir(tmpDir, 'run-match', {
|
||||||
|
task: 'Build login page',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'failed',
|
||||||
|
startTime: '2026-02-01T00:00:00.000Z',
|
||||||
|
logsDirectory: '.takt/runs/run-match/logs',
|
||||||
|
reportDirectory: '.takt/runs/run-match/reports',
|
||||||
|
runSlug: 'run-match',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findRunForTask(tmpDir, 'Build login page');
|
||||||
|
expect(result).toBe('run-match');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the most recent matching run when multiple exist', () => {
|
||||||
|
createRunDir(tmpDir, 'run-old', {
|
||||||
|
task: 'Build login page',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'failed',
|
||||||
|
startTime: '2026-01-01T00:00:00.000Z',
|
||||||
|
logsDirectory: '.takt/runs/run-old/logs',
|
||||||
|
reportDirectory: '.takt/runs/run-old/reports',
|
||||||
|
runSlug: 'run-old',
|
||||||
|
});
|
||||||
|
createRunDir(tmpDir, 'run-new', {
|
||||||
|
task: 'Build login page',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'failed',
|
||||||
|
startTime: '2026-02-01T00:00:00.000Z',
|
||||||
|
logsDirectory: '.takt/runs/run-new/logs',
|
||||||
|
reportDirectory: '.takt/runs/run-new/reports',
|
||||||
|
runSlug: 'run-new',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findRunForTask(tmpDir, 'Build login page');
|
||||||
|
expect(result).toBe('run-new');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('loadRunSessionContext', () => {
|
describe('loadRunSessionContext', () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
mockExistsSync,
|
||||||
|
mockStartReExecution,
|
||||||
mockRequeueTask,
|
mockRequeueTask,
|
||||||
|
mockExecuteAndCompleteTask,
|
||||||
mockRunInstructMode,
|
mockRunInstructMode,
|
||||||
mockDispatchConversationAction,
|
mockDispatchConversationAction,
|
||||||
mockSelectPiece,
|
mockSelectPiece,
|
||||||
@ -12,7 +15,10 @@ const {
|
|||||||
mockSelectRun,
|
mockSelectRun,
|
||||||
mockLoadRunSessionContext,
|
mockLoadRunSessionContext,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
|
mockExistsSync: vi.fn(() => true),
|
||||||
|
mockStartReExecution: vi.fn(),
|
||||||
mockRequeueTask: vi.fn(),
|
mockRequeueTask: vi.fn(),
|
||||||
|
mockExecuteAndCompleteTask: vi.fn(),
|
||||||
mockRunInstructMode: vi.fn(),
|
mockRunInstructMode: vi.fn(),
|
||||||
mockDispatchConversationAction: vi.fn(),
|
mockDispatchConversationAction: vi.fn(),
|
||||||
mockSelectPiece: vi.fn(),
|
mockSelectPiece: vi.fn(),
|
||||||
@ -24,9 +30,17 @@ const {
|
|||||||
mockLoadRunSessionContext: vi.fn(),
|
mockLoadRunSessionContext: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('node:fs', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
existsSync: (...args: unknown[]) => mockExistsSync(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/task/index.js', () => ({
|
vi.mock('../infra/task/index.js', () => ({
|
||||||
detectDefaultBranch: vi.fn(() => 'main'),
|
detectDefaultBranch: vi.fn(() => 'main'),
|
||||||
TaskRunner: class {
|
TaskRunner: class {
|
||||||
|
startReExecution(...args: unknown[]) {
|
||||||
|
return mockStartReExecution(...args);
|
||||||
|
}
|
||||||
requeueTask(...args: unknown[]) {
|
requeueTask(...args: unknown[]) {
|
||||||
return mockRequeueTask(...args);
|
return mockRequeueTask(...args);
|
||||||
}
|
}
|
||||||
@ -47,10 +61,6 @@ vi.mock('../features/tasks/list/instructMode.js', () => ({
|
|||||||
runInstructMode: (...args: unknown[]) => mockRunInstructMode(...args),
|
runInstructMode: (...args: unknown[]) => mockRunInstructMode(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/tasks/add/index.js', () => ({
|
|
||||||
saveTaskFile: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../features/pieceSelection/index.js', () => ({
|
vi.mock('../features/pieceSelection/index.js', () => ({
|
||||||
selectPiece: (...args: unknown[]) => mockSelectPiece(...args),
|
selectPiece: (...args: unknown[]) => mockSelectPiece(...args),
|
||||||
}));
|
}));
|
||||||
@ -74,9 +84,12 @@ vi.mock('../features/interactive/index.js', () => ({
|
|||||||
loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args),
|
loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
||||||
|
executeAndCompleteTask: (...args: unknown[]) => mockExecuteAndCompleteTask(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
success: vi.fn(),
|
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -90,10 +103,15 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { instructBranch } from '../features/tasks/list/taskActions.js';
|
import { instructBranch } from '../features/tasks/list/taskActions.js';
|
||||||
|
import { error as logError } from '../shared/ui/index.js';
|
||||||
|
|
||||||
describe('instructBranch requeue flow', () => {
|
const mockLogError = vi.mocked(logError);
|
||||||
|
|
||||||
|
describe('instructBranch direct execution flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
mockExistsSync.mockReturnValue(true);
|
||||||
|
|
||||||
mockSelectPiece.mockResolvedValue('default');
|
mockSelectPiece.mockResolvedValue('default');
|
||||||
mockRunInstructMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
mockRunInstructMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
||||||
mockDispatchConversationAction.mockImplementation(async (_result, handlers) => handlers.execute({ task: '追加指示A' }));
|
mockDispatchConversationAction.mockImplementation(async (_result, handlers) => handlers.execute({ task: '追加指示A' }));
|
||||||
@ -102,9 +120,15 @@ describe('instructBranch requeue flow', () => {
|
|||||||
mockResolveLanguage.mockReturnValue('en');
|
mockResolveLanguage.mockReturnValue('en');
|
||||||
mockListRecentRuns.mockReturnValue([]);
|
mockListRecentRuns.mockReturnValue([]);
|
||||||
mockSelectRun.mockResolvedValue(null);
|
mockSelectRun.mockResolvedValue(null);
|
||||||
|
mockStartReExecution.mockReturnValue({
|
||||||
|
name: 'done-task',
|
||||||
|
content: 'done',
|
||||||
|
data: { task: 'done' },
|
||||||
|
});
|
||||||
|
mockExecuteAndCompleteTask.mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should requeue the same completed task instead of creating another task', async () => {
|
it('should execute directly via startReExecution instead of requeuing', async () => {
|
||||||
const result = await instructBranch('/project', {
|
const result = await instructBranch('/project', {
|
||||||
kind: 'completed',
|
kind: 'completed',
|
||||||
name: 'done-task',
|
name: 'done-task',
|
||||||
@ -117,12 +141,13 @@ describe('instructBranch requeue flow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(mockRequeueTask).toHaveBeenCalledWith(
|
expect(mockStartReExecution).toHaveBeenCalledWith(
|
||||||
'done-task',
|
'done-task',
|
||||||
['completed', 'failed'],
|
['completed', 'failed'],
|
||||||
undefined,
|
undefined,
|
||||||
'既存ノート\n\n追加指示A',
|
'既存ノート\n\n追加指示A',
|
||||||
);
|
);
|
||||||
|
expect(mockExecuteAndCompleteTask).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set generated instruction as retry note when no existing note', async () => {
|
it('should set generated instruction as retry note when no existing note', async () => {
|
||||||
@ -137,7 +162,7 @@ describe('instructBranch requeue flow', () => {
|
|||||||
data: { task: 'done' },
|
data: { task: 'done' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockRequeueTask).toHaveBeenCalledWith(
|
expect(mockStartReExecution).toHaveBeenCalledWith(
|
||||||
'done-task',
|
'done-task',
|
||||||
['completed', 'failed'],
|
['completed', 'failed'],
|
||||||
undefined,
|
undefined,
|
||||||
@ -145,7 +170,31 @@ describe('instructBranch requeue flow', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load selected run context and pass it to instruct mode', async () => {
|
it('should run instruct mode in existing worktree', async () => {
|
||||||
|
await instructBranch('/project', {
|
||||||
|
kind: 'completed',
|
||||||
|
name: 'done-task',
|
||||||
|
createdAt: '2026-02-14T00:00:00.000Z',
|
||||||
|
filePath: '/project/.takt/tasks.yaml',
|
||||||
|
content: 'done',
|
||||||
|
branch: 'takt/done-task',
|
||||||
|
worktreePath: '/project/.takt/worktrees/done-task',
|
||||||
|
data: { task: 'done' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRunInstructMode).toHaveBeenCalledWith(
|
||||||
|
'/project/.takt/worktrees/done-task',
|
||||||
|
expect.any(String),
|
||||||
|
'takt/done-task',
|
||||||
|
'done-task',
|
||||||
|
'done',
|
||||||
|
'',
|
||||||
|
expect.anything(),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search runs in worktree for run session context', async () => {
|
||||||
mockListRecentRuns.mockReturnValue([
|
mockListRecentRuns.mockReturnValue([
|
||||||
{ slug: 'run-1', task: 'fix', piece: 'default', status: 'completed', startTime: '2026-02-18T00:00:00Z' },
|
{ slug: 'run-1', task: 'fix', piece: 'default', status: 'completed', startTime: '2026-02-18T00:00:00Z' },
|
||||||
]);
|
]);
|
||||||
@ -165,14 +214,76 @@ describe('instructBranch requeue flow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConfirm).toHaveBeenCalledWith("Reference a previous run's results?", false);
|
expect(mockConfirm).toHaveBeenCalledWith("Reference a previous run's results?", false);
|
||||||
expect(mockSelectRun).toHaveBeenCalledWith('/project', 'en');
|
// selectRunSessionContext uses worktreePath for run data
|
||||||
expect(mockLoadRunSessionContext).toHaveBeenCalledWith('/project', 'run-1');
|
expect(mockListRecentRuns).toHaveBeenCalledWith('/project/.takt/worktrees/done-task');
|
||||||
|
expect(mockSelectRun).toHaveBeenCalledWith('/project/.takt/worktrees/done-task', 'en');
|
||||||
|
expect(mockLoadRunSessionContext).toHaveBeenCalledWith('/project/.takt/worktrees/done-task', 'run-1');
|
||||||
expect(mockRunInstructMode).toHaveBeenCalledWith(
|
expect(mockRunInstructMode).toHaveBeenCalledWith(
|
||||||
'/project',
|
'/project/.takt/worktrees/done-task',
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
'takt/done-task',
|
'takt/done-task',
|
||||||
|
'done-task',
|
||||||
|
'done',
|
||||||
|
'',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
runContext,
|
runContext,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false when worktree does not exist', async () => {
|
||||||
|
mockExistsSync.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = await instructBranch('/project', {
|
||||||
|
kind: 'completed',
|
||||||
|
name: 'done-task',
|
||||||
|
createdAt: '2026-02-14T00:00:00.000Z',
|
||||||
|
filePath: '/project/.takt/tasks.yaml',
|
||||||
|
content: 'done',
|
||||||
|
branch: 'takt/done-task',
|
||||||
|
worktreePath: '/project/.takt/worktrees/done-task',
|
||||||
|
data: { task: 'done' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockLogError).toHaveBeenCalledWith('Worktree directory does not exist for task: done-task');
|
||||||
|
expect(mockStartReExecution).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should requeue task via requeueTask when save_task action', async () => {
|
||||||
|
mockDispatchConversationAction.mockImplementation(async (_result, handlers) => handlers.save_task({ task: '追加指示A' }));
|
||||||
|
|
||||||
|
const result = await instructBranch('/project', {
|
||||||
|
kind: 'completed',
|
||||||
|
name: 'done-task',
|
||||||
|
createdAt: '2026-02-14T00:00:00.000Z',
|
||||||
|
filePath: '/project/.takt/tasks.yaml',
|
||||||
|
content: 'done',
|
||||||
|
branch: 'takt/done-task',
|
||||||
|
worktreePath: '/project/.takt/worktrees/done-task',
|
||||||
|
data: { task: 'done' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockRequeueTask).toHaveBeenCalledWith('done-task', ['completed', 'failed'], undefined, '追加指示A');
|
||||||
|
expect(mockStartReExecution).not.toHaveBeenCalled();
|
||||||
|
expect(mockExecuteAndCompleteTask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should requeue task with existing retry note appended when save_task', async () => {
|
||||||
|
mockDispatchConversationAction.mockImplementation(async (_result, handlers) => handlers.save_task({ task: '追加指示A' }));
|
||||||
|
|
||||||
|
const result = await instructBranch('/project', {
|
||||||
|
kind: 'completed',
|
||||||
|
name: 'done-task',
|
||||||
|
createdAt: '2026-02-14T00:00:00.000Z',
|
||||||
|
filePath: '/project/.takt/tasks.yaml',
|
||||||
|
content: 'done',
|
||||||
|
branch: 'takt/done-task',
|
||||||
|
worktreePath: '/project/.takt/worktrees/done-task',
|
||||||
|
data: { task: 'done', retry_note: '既存ノート' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockRequeueTask).toHaveBeenCalledWith('done-task', ['completed', 'failed'], undefined, '既存ノート\n\n追加指示A');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,17 +1,50 @@
|
|||||||
import * as fs from 'node:fs';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import * as path from 'node:path';
|
|
||||||
import * as os from 'node:os';
|
const {
|
||||||
import { stringify as stringifyYaml } from 'yaml';
|
mockExistsSync,
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
mockSelectPiece,
|
||||||
|
mockSelectOption,
|
||||||
|
mockLoadGlobalConfig,
|
||||||
|
mockLoadPieceByIdentifier,
|
||||||
|
mockGetPieceDescription,
|
||||||
|
mockRunRetryMode,
|
||||||
|
mockFindRunForTask,
|
||||||
|
mockStartReExecution,
|
||||||
|
mockRequeueTask,
|
||||||
|
mockExecuteAndCompleteTask,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
mockExistsSync: vi.fn(() => true),
|
||||||
|
mockSelectPiece: vi.fn(),
|
||||||
|
mockSelectOption: vi.fn(),
|
||||||
|
mockLoadGlobalConfig: vi.fn(),
|
||||||
|
mockLoadPieceByIdentifier: vi.fn(),
|
||||||
|
mockGetPieceDescription: vi.fn(() => ({
|
||||||
|
name: 'default',
|
||||||
|
description: 'desc',
|
||||||
|
pieceStructure: '',
|
||||||
|
movementPreviews: [],
|
||||||
|
})),
|
||||||
|
mockRunRetryMode: vi.fn(),
|
||||||
|
mockFindRunForTask: vi.fn(() => null),
|
||||||
|
mockStartReExecution: vi.fn(),
|
||||||
|
mockRequeueTask: vi.fn(),
|
||||||
|
mockExecuteAndCompleteTask: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('node:fs', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
existsSync: (...args: unknown[]) => mockExistsSync(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/pieceSelection/index.js', () => ({
|
||||||
|
selectPiece: (...args: unknown[]) => mockSelectPiece(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
selectOption: vi.fn(),
|
selectOption: (...args: unknown[]) => mockSelectOption(...args),
|
||||||
confirm: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
success: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
header: vi.fn(),
|
header: vi.fn(),
|
||||||
blankLine: vi.fn(),
|
blankLine: vi.fn(),
|
||||||
@ -27,48 +60,39 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
loadGlobalConfig: vi.fn(),
|
loadGlobalConfig: (...args: unknown[]) => mockLoadGlobalConfig(...args),
|
||||||
loadPieceByIdentifier: vi.fn(),
|
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
||||||
getPieceDescription: vi.fn(() => ({
|
getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args),
|
||||||
name: 'default',
|
|
||||||
description: 'desc',
|
|
||||||
pieceStructure: '',
|
|
||||||
movementPreviews: [],
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../features/tasks/list/instructMode.js', () => ({
|
|
||||||
runInstructMode: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/interactive/index.js', () => ({
|
vi.mock('../features/interactive/index.js', () => ({
|
||||||
resolveLanguage: vi.fn(() => 'en'),
|
findRunForTask: (...args: unknown[]) => mockFindRunForTask(...args),
|
||||||
listRecentRuns: vi.fn(() => []),
|
|
||||||
selectRun: vi.fn(() => null),
|
|
||||||
loadRunSessionContext: vi.fn(),
|
loadRunSessionContext: vi.fn(),
|
||||||
|
getRunPaths: vi.fn(() => ({ logsDir: '/tmp/logs', reportsDir: '/tmp/reports' })),
|
||||||
|
formatRunSessionForPrompt: vi.fn(() => ({
|
||||||
|
runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '',
|
||||||
|
})),
|
||||||
|
runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/i18n/index.js', () => ({
|
vi.mock('../infra/task/index.js', () => ({
|
||||||
getLabel: vi.fn(() => "Reference a previous run's results?"),
|
TaskRunner: class {
|
||||||
|
startReExecution(...args: unknown[]) {
|
||||||
|
return mockStartReExecution(...args);
|
||||||
|
}
|
||||||
|
requeueTask(...args: unknown[]) {
|
||||||
|
return mockRequeueTask(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
||||||
|
executeAndCompleteTask: (...args: unknown[]) => mockExecuteAndCompleteTask(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { selectOption, confirm } from '../shared/prompt/index.js';
|
|
||||||
import { success, error as logError } from '../shared/ui/index.js';
|
|
||||||
import { loadGlobalConfig, loadPieceByIdentifier } from '../infra/config/index.js';
|
|
||||||
import { retryFailedTask } from '../features/tasks/list/taskRetryActions.js';
|
import { retryFailedTask } from '../features/tasks/list/taskRetryActions.js';
|
||||||
import type { TaskListItem } from '../infra/task/types.js';
|
import type { TaskListItem } from '../infra/task/types.js';
|
||||||
import type { PieceConfig } from '../core/models/index.js';
|
import type { PieceConfig } from '../core/models/index.js';
|
||||||
import { runInstructMode } from '../features/tasks/list/instructMode.js';
|
|
||||||
|
|
||||||
const mockSelectOption = vi.mocked(selectOption);
|
|
||||||
const mockConfirm = vi.mocked(confirm);
|
|
||||||
const mockSuccess = vi.mocked(success);
|
|
||||||
const mockLogError = vi.mocked(logError);
|
|
||||||
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
|
||||||
const mockLoadPieceByIdentifier = vi.mocked(loadPieceByIdentifier);
|
|
||||||
const mockRunInstructMode = vi.mocked(runInstructMode);
|
|
||||||
|
|
||||||
let tmpDir: string;
|
|
||||||
|
|
||||||
const defaultPieceConfig: PieceConfig = {
|
const defaultPieceConfig: PieceConfig = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -82,115 +106,142 @@ const defaultPieceConfig: PieceConfig = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
function writeFailedTask(projectDir: string, name: string): TaskListItem {
|
function makeFailedTask(overrides?: Partial<TaskListItem>): TaskListItem {
|
||||||
const tasksFile = path.join(projectDir, '.takt', 'tasks.yaml');
|
|
||||||
fs.mkdirSync(path.dirname(tasksFile), { recursive: true });
|
|
||||||
fs.writeFileSync(tasksFile, stringifyYaml({
|
|
||||||
tasks: [
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
status: 'failed',
|
|
||||||
content: 'Do something',
|
|
||||||
created_at: '2025-01-15T12:00:00.000Z',
|
|
||||||
started_at: '2025-01-15T12:01:00.000Z',
|
|
||||||
completed_at: '2025-01-15T12:02:00.000Z',
|
|
||||||
piece: 'default',
|
|
||||||
failure: {
|
|
||||||
movement: 'review',
|
|
||||||
error: 'Boom',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}), 'utf-8');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: 'failed',
|
kind: 'failed',
|
||||||
name,
|
name: 'my-task',
|
||||||
createdAt: '2025-01-15T12:02:00.000Z',
|
createdAt: '2025-01-15T12:02:00.000Z',
|
||||||
filePath: tasksFile,
|
filePath: '/project/.takt/tasks.yaml',
|
||||||
content: 'Do something',
|
content: 'Do something',
|
||||||
|
branch: 'takt/my-task',
|
||||||
|
worktreePath: '/project/.takt/worktrees/my-task',
|
||||||
data: { task: 'Do something', piece: 'default' },
|
data: { task: 'Do something', piece: 'default' },
|
||||||
failure: { movement: 'review', error: 'Boom' },
|
failure: { movement: 'review', error: 'Boom' },
|
||||||
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-test-retry-'));
|
mockExistsSync.mockReturnValue(true);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
mockSelectPiece.mockResolvedValue('default');
|
||||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
mockLoadGlobalConfig.mockReturnValue({ defaultPiece: 'default' });
|
||||||
|
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
||||||
|
mockSelectOption.mockResolvedValue('plan');
|
||||||
|
mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
||||||
|
mockStartReExecution.mockReturnValue({
|
||||||
|
name: 'my-task',
|
||||||
|
content: 'Do something',
|
||||||
|
data: { task: 'Do something', piece: 'default' },
|
||||||
|
});
|
||||||
|
mockExecuteAndCompleteTask.mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('retryFailedTask', () => {
|
describe('retryFailedTask', () => {
|
||||||
it('should requeue task with selected movement', async () => {
|
it('should run retry mode in existing worktree and execute directly', async () => {
|
||||||
const task = writeFailedTask(tmpDir, 'my-task');
|
const task = makeFailedTask();
|
||||||
|
|
||||||
mockLoadGlobalConfig.mockReturnValue({ defaultPiece: 'default' });
|
const result = await retryFailedTask(task, '/project');
|
||||||
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
|
expect(mockRunRetryMode).toHaveBeenCalledWith(
|
||||||
|
'/project/.takt/worktrees/my-task',
|
||||||
|
expect.objectContaining({
|
||||||
|
failure: expect.objectContaining({ taskName: 'my-task', taskContent: 'Do something' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A');
|
||||||
|
expect(mockExecuteAndCompleteTask).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass non-initial movement as startMovement', async () => {
|
||||||
|
const task = makeFailedTask();
|
||||||
mockSelectOption.mockResolvedValue('implement');
|
mockSelectOption.mockResolvedValue('implement');
|
||||||
mockConfirm.mockResolvedValue(false);
|
|
||||||
mockRunInstructMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
|
||||||
|
|
||||||
const result = await retryFailedTask(task, tmpDir);
|
await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], 'implement', '追加指示A');
|
||||||
expect(mockSuccess).toHaveBeenCalledWith('Task requeued: my-task');
|
|
||||||
|
|
||||||
const tasksYaml = fs.readFileSync(path.join(tmpDir, '.takt', 'tasks.yaml'), 'utf-8');
|
|
||||||
expect(tasksYaml).toContain('status: pending');
|
|
||||||
expect(tasksYaml).toContain('start_movement: implement');
|
|
||||||
expect(tasksYaml).toContain('retry_note: 追加指示A');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not add start_movement when initial movement is selected', async () => {
|
it('should not pass startMovement when initial movement is selected', async () => {
|
||||||
const task = writeFailedTask(tmpDir, 'my-task');
|
const task = makeFailedTask();
|
||||||
|
|
||||||
mockLoadGlobalConfig.mockReturnValue({ defaultPiece: 'default' });
|
await retryFailedTask(task, '/project');
|
||||||
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
|
||||||
mockSelectOption.mockResolvedValue('plan');
|
|
||||||
mockConfirm.mockResolvedValue(false);
|
|
||||||
mockRunInstructMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
|
||||||
|
|
||||||
const result = await retryFailedTask(task, tmpDir);
|
expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A');
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
const tasksYaml = fs.readFileSync(path.join(tmpDir, '.takt', 'tasks.yaml'), 'utf-8');
|
|
||||||
expect(tasksYaml).not.toContain('start_movement');
|
|
||||||
expect(tasksYaml).toContain('retry_note: 追加指示A');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should append generated instruction to existing retry note', async () => {
|
it('should append instruction to existing retry note', async () => {
|
||||||
const task = writeFailedTask(tmpDir, 'my-task');
|
const task = makeFailedTask({ data: { task: 'Do something', piece: 'default', retry_note: '既存ノート' } });
|
||||||
task.data = { task: 'Do something', piece: 'default', retry_note: '既存ノート' };
|
|
||||||
|
|
||||||
mockLoadGlobalConfig.mockReturnValue({ defaultPiece: 'default' });
|
await retryFailedTask(task, '/project');
|
||||||
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
|
||||||
mockSelectOption.mockResolvedValue('plan');
|
|
||||||
mockConfirm.mockResolvedValue(false);
|
|
||||||
mockRunInstructMode.mockResolvedValue({ action: 'execute', task: '追加指示B' });
|
|
||||||
|
|
||||||
const result = await retryFailedTask(task, tmpDir);
|
expect(mockStartReExecution).toHaveBeenCalledWith(
|
||||||
|
'my-task', ['failed'], undefined, '既存ノート\n\n追加指示A',
|
||||||
expect(result).toBe(true);
|
|
||||||
const tasksYaml = fs.readFileSync(path.join(tmpDir, '.takt', 'tasks.yaml'), 'utf-8');
|
|
||||||
expect(tasksYaml).toContain('retry_note: |');
|
|
||||||
expect(tasksYaml).toContain('既存ノート');
|
|
||||||
expect(tasksYaml).toContain('追加指示B');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false and show error when piece not found', async () => {
|
|
||||||
const task = writeFailedTask(tmpDir, 'my-task');
|
|
||||||
|
|
||||||
mockLoadGlobalConfig.mockReturnValue({ defaultPiece: 'default' });
|
|
||||||
mockLoadPieceByIdentifier.mockReturnValue(null);
|
|
||||||
|
|
||||||
const result = await retryFailedTask(task, tmpDir);
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
expect(mockLogError).toHaveBeenCalledWith(
|
|
||||||
'Piece "default" not found. Cannot determine available movements.',
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should search runs in worktree, not projectDir', async () => {
|
||||||
|
const task = makeFailedTask();
|
||||||
|
|
||||||
|
await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
|
expect(mockFindRunForTask).toHaveBeenCalledWith('/project/.takt/worktrees/my-task', 'Do something');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when worktree path is not set', async () => {
|
||||||
|
const task = makeFailedTask({ worktreePath: undefined });
|
||||||
|
|
||||||
|
await expect(retryFailedTask(task, '/project')).rejects.toThrow('Worktree path is not set');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when worktree directory does not exist', async () => {
|
||||||
|
mockExistsSync.mockReturnValue(false);
|
||||||
|
const task = makeFailedTask();
|
||||||
|
|
||||||
|
await expect(retryFailedTask(task, '/project')).rejects.toThrow('Worktree directory does not exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when piece selection is cancelled', async () => {
|
||||||
|
const task = makeFailedTask();
|
||||||
|
mockSelectPiece.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when retry mode is cancelled', async () => {
|
||||||
|
const task = makeFailedTask();
|
||||||
|
mockRunRetryMode.mockResolvedValue({ action: 'cancel', task: '' });
|
||||||
|
|
||||||
|
const result = await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockStartReExecution).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should requeue task via requeueTask when save_task action', async () => {
|
||||||
|
const task = makeFailedTask();
|
||||||
|
mockRunRetryMode.mockResolvedValue({ action: 'save_task', task: '追加指示A' });
|
||||||
|
|
||||||
|
const result = await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A');
|
||||||
|
expect(mockStartReExecution).not.toHaveBeenCalled();
|
||||||
|
expect(mockExecuteAndCompleteTask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should requeue task with existing retry note appended when save_task', async () => {
|
||||||
|
const task = makeFailedTask({ data: { task: 'Do something', piece: 'default', retry_note: '既存ノート' } });
|
||||||
|
mockRunRetryMode.mockResolvedValue({ action: 'save_task', task: '追加指示A' });
|
||||||
|
|
||||||
|
await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
|
expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import {
|
|||||||
initGlobalDirs,
|
initGlobalDirs,
|
||||||
initProjectDirs,
|
initProjectDirs,
|
||||||
loadGlobalConfig,
|
loadGlobalConfig,
|
||||||
getEffectiveDebugConfig,
|
|
||||||
isVerboseMode,
|
isVerboseMode,
|
||||||
} from '../../infra/config/index.js';
|
} from '../../infra/config/index.js';
|
||||||
import { setQuietMode } from '../../shared/context.js';
|
import { setQuietMode } from '../../shared/context.js';
|
||||||
@ -68,13 +67,7 @@ export async function runPreActionHook(): Promise<void> {
|
|||||||
initProjectDirs(resolvedCwd);
|
initProjectDirs(resolvedCwd);
|
||||||
|
|
||||||
const verbose = isVerboseMode(resolvedCwd);
|
const verbose = isVerboseMode(resolvedCwd);
|
||||||
let debugConfig = getEffectiveDebugConfig(resolvedCwd);
|
initDebugLogger(verbose ? { enabled: true } : undefined, resolvedCwd);
|
||||||
|
|
||||||
if (verbose && (!debugConfig || !debugConfig.enabled)) {
|
|
||||||
debugConfig = { enabled: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
initDebugLogger(debugConfig, resolvedCwd);
|
|
||||||
|
|
||||||
const config = loadGlobalConfig();
|
const config = loadGlobalConfig();
|
||||||
|
|
||||||
|
|||||||
@ -17,12 +17,6 @@ export interface CustomAgentConfig {
|
|||||||
model?: string;
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Debug configuration for takt */
|
|
||||||
export interface DebugConfig {
|
|
||||||
enabled: boolean;
|
|
||||||
logFile?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Observability configuration for runtime event logs */
|
/** Observability configuration for runtime event logs */
|
||||||
export interface ObservabilityConfig {
|
export interface ObservabilityConfig {
|
||||||
/** Enable provider stream event logging (default: false when undefined) */
|
/** Enable provider stream event logging (default: false when undefined) */
|
||||||
@ -63,7 +57,6 @@ export interface GlobalConfig {
|
|||||||
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
||||||
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
||||||
model?: string;
|
model?: string;
|
||||||
debug?: DebugConfig;
|
|
||||||
observability?: ObservabilityConfig;
|
observability?: ObservabilityConfig;
|
||||||
/** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */
|
/** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */
|
||||||
worktreeDir?: string;
|
worktreeDir?: string;
|
||||||
|
|||||||
@ -27,7 +27,6 @@ export type {
|
|||||||
PieceConfig,
|
PieceConfig,
|
||||||
PieceState,
|
PieceState,
|
||||||
CustomAgentConfig,
|
CustomAgentConfig,
|
||||||
DebugConfig,
|
|
||||||
ObservabilityConfig,
|
ObservabilityConfig,
|
||||||
Language,
|
Language,
|
||||||
PipelineConfig,
|
PipelineConfig,
|
||||||
|
|||||||
@ -374,12 +374,6 @@ export const CustomAgentConfigSchema = z.object({
|
|||||||
{ message: 'Agent must have prompt_file, prompt, claude_agent, or claude_skill' }
|
{ message: 'Agent must have prompt_file, prompt, claude_agent, or claude_skill' }
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Debug config schema */
|
|
||||||
export const DebugConfigSchema = z.object({
|
|
||||||
enabled: z.boolean().optional().default(false),
|
|
||||||
log_file: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ObservabilityConfigSchema = z.object({
|
export const ObservabilityConfigSchema = z.object({
|
||||||
provider_events: z.boolean().optional(),
|
provider_events: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
@ -415,7 +409,6 @@ export const GlobalConfigSchema = z.object({
|
|||||||
log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
|
log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
|
||||||
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional().default('claude'),
|
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional().default('claude'),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
debug: DebugConfigSchema.optional(),
|
|
||||||
observability: ObservabilityConfigSchema.optional(),
|
observability: ObservabilityConfigSchema.optional(),
|
||||||
/** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */
|
/** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */
|
||||||
worktree_dir: z.string().optional(),
|
worktree_dir: z.string().optional(),
|
||||||
|
|||||||
@ -62,7 +62,6 @@ export type {
|
|||||||
// Configuration types (global and project)
|
// Configuration types (global and project)
|
||||||
export type {
|
export type {
|
||||||
CustomAgentConfig,
|
CustomAgentConfig,
|
||||||
DebugConfig,
|
|
||||||
ObservabilityConfig,
|
ObservabilityConfig,
|
||||||
Language,
|
Language,
|
||||||
PipelineConfig,
|
PipelineConfig,
|
||||||
|
|||||||
@ -22,5 +22,6 @@ export { passthroughMode } from './passthroughMode.js';
|
|||||||
export { quietMode } from './quietMode.js';
|
export { quietMode } from './quietMode.js';
|
||||||
export { personaMode } from './personaMode.js';
|
export { personaMode } from './personaMode.js';
|
||||||
export { selectRun } from './runSelector.js';
|
export { selectRun } from './runSelector.js';
|
||||||
export { listRecentRuns, loadRunSessionContext, type RunSessionContext } 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 { dispatchConversationAction, type ConversationActionResult } from './actionDispatcher.js';
|
export { dispatchConversationAction, type ConversationActionResult } from './actionDispatcher.js';
|
||||||
|
|||||||
167
src/features/interactive/retryMode.ts
Normal file
167
src/features/interactive/retryMode.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Retry mode for failed tasks.
|
||||||
|
*
|
||||||
|
* Provides a dedicated conversation loop with failure context,
|
||||||
|
* run session data, and piece structure injected into the system prompt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
initializeSession,
|
||||||
|
displayAndClearSessionState,
|
||||||
|
runConversationLoop,
|
||||||
|
type SessionContext,
|
||||||
|
type ConversationStrategy,
|
||||||
|
type PostSummaryAction,
|
||||||
|
} from './conversationLoop.js';
|
||||||
|
import {
|
||||||
|
buildSummaryActionOptions,
|
||||||
|
selectSummaryAction,
|
||||||
|
formatMovementPreviews,
|
||||||
|
type PieceContext,
|
||||||
|
} from './interactive-summary.js';
|
||||||
|
import { resolveLanguage } from './interactive.js';
|
||||||
|
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||||
|
import { getLabelObject } from '../../shared/i18n/index.js';
|
||||||
|
import { loadGlobalConfig } from '../../infra/config/index.js';
|
||||||
|
import type { InstructModeResult, InstructUIText } from '../tasks/list/instructMode.js';
|
||||||
|
|
||||||
|
/** Failure information for a retry task */
|
||||||
|
export interface RetryFailureInfo {
|
||||||
|
readonly taskName: string;
|
||||||
|
readonly taskContent: string;
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly failedMovement: string;
|
||||||
|
readonly error: string;
|
||||||
|
readonly lastMessage: string;
|
||||||
|
readonly retryNote: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run session reference data for retry prompt */
|
||||||
|
export interface RetryRunInfo {
|
||||||
|
readonly logsDir: string;
|
||||||
|
readonly reportsDir: string;
|
||||||
|
readonly task: string;
|
||||||
|
readonly piece: string;
|
||||||
|
readonly status: string;
|
||||||
|
readonly movementLogs: string;
|
||||||
|
readonly reports: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full retry context assembled by the caller */
|
||||||
|
export interface RetryContext {
|
||||||
|
readonly failure: RetryFailureInfo;
|
||||||
|
readonly branchName: string;
|
||||||
|
readonly pieceContext: PieceContext;
|
||||||
|
readonly run: RetryRunInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RETRY_TOOLS = ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert RetryContext into template variable map.
|
||||||
|
*/
|
||||||
|
export function buildRetryTemplateVars(ctx: RetryContext, lang: 'en' | 'ja'): Record<string, string | boolean> {
|
||||||
|
const hasPiecePreview = !!ctx.pieceContext.movementPreviews?.length;
|
||||||
|
const movementDetails = hasPiecePreview
|
||||||
|
? formatMovementPreviews(ctx.pieceContext.movementPreviews!, lang)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const hasRun = ctx.run !== null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
taskName: ctx.failure.taskName,
|
||||||
|
taskContent: ctx.failure.taskContent,
|
||||||
|
branchName: ctx.branchName,
|
||||||
|
createdAt: ctx.failure.createdAt,
|
||||||
|
failedMovement: ctx.failure.failedMovement,
|
||||||
|
failureError: ctx.failure.error,
|
||||||
|
failureLastMessage: ctx.failure.lastMessage,
|
||||||
|
retryNote: ctx.failure.retryNote,
|
||||||
|
hasPiecePreview,
|
||||||
|
pieceStructure: ctx.pieceContext.pieceStructure,
|
||||||
|
movementDetails,
|
||||||
|
hasRun,
|
||||||
|
runLogsDir: hasRun ? ctx.run!.logsDir : '',
|
||||||
|
runReportsDir: hasRun ? ctx.run!.reportsDir : '',
|
||||||
|
runTask: hasRun ? ctx.run!.task : '',
|
||||||
|
runPiece: hasRun ? ctx.run!.piece : '',
|
||||||
|
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<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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run retry mode conversation loop.
|
||||||
|
*
|
||||||
|
* Uses a dedicated system prompt with failure context, run session data,
|
||||||
|
* and piece structure injected for the AI assistant.
|
||||||
|
*/
|
||||||
|
export async function runRetryMode(
|
||||||
|
cwd: string,
|
||||||
|
retryContext: RetryContext,
|
||||||
|
): Promise<InstructModeResult> {
|
||||||
|
const globalConfig = loadGlobalConfig();
|
||||||
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
|
||||||
|
if (!globalConfig.provider) {
|
||||||
|
throw new Error('Provider is not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseCtx = initializeSession(cwd, 'retry');
|
||||||
|
const ctx: SessionContext = { ...baseCtx, lang, personaName: 'retry' };
|
||||||
|
|
||||||
|
displayAndClearSessionState(cwd, ctx.lang);
|
||||||
|
|
||||||
|
const ui = getLabelObject<InstructUIText>('instruct.ui', ctx.lang);
|
||||||
|
|
||||||
|
const templateVars = buildRetryTemplateVars(retryContext, lang);
|
||||||
|
const systemPrompt = loadTemplate('score_retry_system_prompt', ctx.lang, templateVars);
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {});
|
||||||
|
|
||||||
|
function injectPolicy(userMessage: string): string {
|
||||||
|
const policyIntro = ctx.lang === 'ja'
|
||||||
|
? '以下のポリシーは行動規範です。必ず遵守してください。'
|
||||||
|
: 'The following policy defines behavioral guidelines. Please follow them.';
|
||||||
|
const reminderLabel = ctx.lang === 'ja'
|
||||||
|
? '上記の Policy セクションで定義されたポリシー規範を遵守してください。'
|
||||||
|
: 'Please follow the policy guidelines defined in the Policy section above.';
|
||||||
|
return `## Policy\n${policyIntro}\n\n${policyContent}\n\n---\n\n${userMessage}\n\n---\n**Policy Reminder:** ${reminderLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategy: ConversationStrategy = {
|
||||||
|
systemPrompt,
|
||||||
|
allowedTools: RETRY_TOOLS,
|
||||||
|
transformPrompt: injectPolicy,
|
||||||
|
introMessage: introLabel,
|
||||||
|
selectAction: createSelectRetryAction(ui),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runConversationLoop(cwd, ctx, strategy, retryContext.pieceContext, undefined);
|
||||||
|
|
||||||
|
if (result.action === 'cancel') {
|
||||||
|
return { action: 'cancel', task: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { action: result.action as InstructModeResult['action'], task: result.task };
|
||||||
|
}
|
||||||
@ -48,6 +48,12 @@ export interface RunSessionContext {
|
|||||||
readonly reports: readonly ReportEntry[];
|
readonly reports: readonly ReportEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Absolute paths to a run's logs and reports directories */
|
||||||
|
export interface RunPaths {
|
||||||
|
readonly logsDir: string;
|
||||||
|
readonly reportsDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface MetaJson {
|
interface MetaJson {
|
||||||
readonly task: string;
|
readonly task: string;
|
||||||
readonly piece: string;
|
readonly piece: string;
|
||||||
@ -150,6 +156,33 @@ export function listRecentRuns(cwd: string): RunSummary[] {
|
|||||||
return summaries.slice(0, MAX_RUNS);
|
return summaries.slice(0, MAX_RUNS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the most recent run matching the given task content.
|
||||||
|
*
|
||||||
|
* @returns The run slug if found, null otherwise.
|
||||||
|
*/
|
||||||
|
export function findRunForTask(cwd: string, taskContent: string): string | null {
|
||||||
|
const runs = listRecentRuns(cwd);
|
||||||
|
const match = runs.find((r) => r.task === taskContent);
|
||||||
|
return match?.slug ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get absolute paths to a run's logs and reports directories.
|
||||||
|
*/
|
||||||
|
export function getRunPaths(cwd: string, slug: string): RunPaths {
|
||||||
|
const metaPath = join(cwd, '.takt', 'runs', slug, 'meta.json');
|
||||||
|
const meta = parseMetaJson(metaPath);
|
||||||
|
if (!meta) {
|
||||||
|
throw new Error(`Run not found: ${slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
logsDir: join(cwd, meta.logsDirectory),
|
||||||
|
reportsDir: join(cwd, meta.reportDirectory),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load full run session context for prompt injection.
|
* Load full run session context for prompt injection.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -96,28 +96,37 @@ export async function resolveTaskExecution(
|
|||||||
if (data.worktree) {
|
if (data.worktree) {
|
||||||
throwIfAborted(abortSignal);
|
throwIfAborted(abortSignal);
|
||||||
baseBranch = getCurrentBranch(defaultCwd);
|
baseBranch = getCurrentBranch(defaultCwd);
|
||||||
const taskSlug = await withProgress(
|
|
||||||
'Generating branch name...',
|
|
||||||
(slug) => `Branch name generated: ${slug}`,
|
|
||||||
() => summarizeTaskName(task.content, { cwd: defaultCwd }),
|
|
||||||
);
|
|
||||||
|
|
||||||
throwIfAborted(abortSignal);
|
if (task.worktreePath && fs.existsSync(task.worktreePath)) {
|
||||||
const result = await withProgress(
|
// Reuse existing worktree (clone still on disk from previous execution)
|
||||||
'Creating clone...',
|
execCwd = task.worktreePath;
|
||||||
(cloneResult) => `Clone created: ${cloneResult.path} (branch: ${cloneResult.branch})`,
|
branch = data.branch;
|
||||||
async () => createSharedClone(defaultCwd, {
|
worktreePath = task.worktreePath;
|
||||||
worktree: data.worktree!,
|
isWorktree = true;
|
||||||
branch: data.branch,
|
} else {
|
||||||
taskSlug,
|
const taskSlug = await withProgress(
|
||||||
issueNumber: data.issue,
|
'Generating branch name...',
|
||||||
}),
|
(slug) => `Branch name generated: ${slug}`,
|
||||||
);
|
() => summarizeTaskName(task.content, { cwd: defaultCwd }),
|
||||||
throwIfAborted(abortSignal);
|
);
|
||||||
execCwd = result.path;
|
|
||||||
branch = result.branch;
|
throwIfAborted(abortSignal);
|
||||||
worktreePath = result.path;
|
const result = await withProgress(
|
||||||
isWorktree = true;
|
'Creating clone...',
|
||||||
|
(cloneResult) => `Clone created: ${cloneResult.path} (branch: ${cloneResult.branch})`,
|
||||||
|
async () => createSharedClone(defaultCwd, {
|
||||||
|
worktree: data.worktree!,
|
||||||
|
branch: data.branch,
|
||||||
|
taskSlug,
|
||||||
|
issueNumber: data.issue,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
throwIfAborted(abortSignal);
|
||||||
|
execCwd = result.path;
|
||||||
|
branch = result.branch;
|
||||||
|
worktreePath = result.path;
|
||||||
|
isWorktree = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task.taskDir && reportDirName) {
|
if (task.taskDir && reportDirName) {
|
||||||
|
|||||||
@ -181,7 +181,7 @@ export async function listTasks(
|
|||||||
showFullDiff(cwd, task.branch);
|
showFullDiff(cwd, task.branch);
|
||||||
break;
|
break;
|
||||||
case 'instruct':
|
case 'instruct':
|
||||||
await instructBranch(cwd, task, options);
|
await instructBranch(cwd, task);
|
||||||
break;
|
break;
|
||||||
case 'try':
|
case 'try':
|
||||||
tryMergeBranch(cwd, task);
|
tryMergeBranch(cwd, task);
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
resolveLanguage,
|
resolveLanguage,
|
||||||
buildSummaryActionOptions,
|
buildSummaryActionOptions,
|
||||||
selectSummaryAction,
|
selectSummaryAction,
|
||||||
|
formatMovementPreviews,
|
||||||
type PieceContext,
|
type PieceContext,
|
||||||
} from '../../interactive/interactive.js';
|
} from '../../interactive/interactive.js';
|
||||||
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
|
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
|
||||||
@ -64,10 +65,47 @@ function createSelectInstructAction(ui: InstructUIText): (task: string, lang: 'e
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildInstructTemplateVars(
|
||||||
|
branchContext: string,
|
||||||
|
branchName: string,
|
||||||
|
taskName: string,
|
||||||
|
taskContent: string,
|
||||||
|
retryNote: string,
|
||||||
|
lang: 'en' | 'ja',
|
||||||
|
pieceContext?: PieceContext,
|
||||||
|
runSessionContext?: RunSessionContext,
|
||||||
|
): Record<string, string | boolean> {
|
||||||
|
const hasPiecePreview = !!pieceContext?.movementPreviews?.length;
|
||||||
|
const movementDetails = hasPiecePreview
|
||||||
|
? formatMovementPreviews(pieceContext!.movementPreviews!, lang)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const hasRunSession = !!runSessionContext;
|
||||||
|
const runPromptVars = hasRunSession
|
||||||
|
? formatRunSessionForPrompt(runSessionContext)
|
||||||
|
: { runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '' };
|
||||||
|
|
||||||
|
return {
|
||||||
|
taskName,
|
||||||
|
taskContent,
|
||||||
|
branchName,
|
||||||
|
branchContext,
|
||||||
|
retryNote,
|
||||||
|
hasPiecePreview,
|
||||||
|
pieceStructure: pieceContext?.pieceStructure ?? '',
|
||||||
|
movementDetails,
|
||||||
|
hasRunSession,
|
||||||
|
...runPromptVars,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function runInstructMode(
|
export async function runInstructMode(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
branchContext: string,
|
branchContext: string,
|
||||||
branchName: string,
|
branchName: string,
|
||||||
|
taskName: string,
|
||||||
|
taskContent: string,
|
||||||
|
retryNote: string,
|
||||||
pieceContext?: PieceContext,
|
pieceContext?: PieceContext,
|
||||||
runSessionContext?: RunSessionContext,
|
runSessionContext?: RunSessionContext,
|
||||||
): Promise<InstructModeResult> {
|
): Promise<InstructModeResult> {
|
||||||
@ -85,24 +123,11 @@ export async function runInstructMode(
|
|||||||
|
|
||||||
const ui = getLabelObject<InstructUIText>('instruct.ui', ctx.lang);
|
const ui = getLabelObject<InstructUIText>('instruct.ui', ctx.lang);
|
||||||
|
|
||||||
const hasRunSession = !!runSessionContext;
|
const templateVars = buildInstructTemplateVars(
|
||||||
const runPromptVars = hasRunSession
|
branchContext, branchName, taskName, taskContent, retryNote, lang,
|
||||||
? formatRunSessionForPrompt(runSessionContext)
|
pieceContext, runSessionContext,
|
||||||
: { runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '' };
|
);
|
||||||
|
const systemPrompt = loadTemplate('score_instruct_system_prompt', ctx.lang, templateVars);
|
||||||
const systemPrompt = loadTemplate('score_interactive_system_prompt', ctx.lang, {
|
|
||||||
hasPiecePreview: false,
|
|
||||||
pieceStructure: '',
|
|
||||||
movementDetails: '',
|
|
||||||
hasRunSession,
|
|
||||||
...runPromptVars,
|
|
||||||
});
|
|
||||||
|
|
||||||
const branchIntro = ctx.lang === 'ja'
|
|
||||||
? `## ブランチ: ${branchName}\n\n${branchContext}`
|
|
||||||
: `## Branch: ${branchName}\n\n${branchContext}`;
|
|
||||||
|
|
||||||
const introMessage = `${branchIntro}\n\n${ui.intro}`;
|
|
||||||
|
|
||||||
const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {});
|
const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {});
|
||||||
|
|
||||||
@ -120,7 +145,7 @@ export async function runInstructMode(
|
|||||||
systemPrompt,
|
systemPrompt,
|
||||||
allowedTools: INSTRUCT_TOOLS,
|
allowedTools: INSTRUCT_TOOLS,
|
||||||
transformPrompt: injectPolicy,
|
transformPrompt: injectPolicy,
|
||||||
introMessage,
|
introMessage: ui.intro,
|
||||||
selectAction: createSelectInstructAction(ui),
|
selectAction: createSelectInstructAction(ui),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Instruction actions for completed/failed tasks.
|
||||||
|
*
|
||||||
|
* Uses the existing worktree (clone) for conversation and direct re-execution.
|
||||||
|
* The worktree is preserved after initial execution, so no clone creation is needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import {
|
import {
|
||||||
TaskRunner,
|
TaskRunner,
|
||||||
|
detectDefaultBranch,
|
||||||
} from '../../../infra/task/index.js';
|
} from '../../../infra/task/index.js';
|
||||||
import { loadGlobalConfig, getPieceDescription } from '../../../infra/config/index.js';
|
import { loadGlobalConfig, getPieceDescription } from '../../../infra/config/index.js';
|
||||||
import { info, success } from '../../../shared/ui/index.js';
|
import { info, error as logError } from '../../../shared/ui/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
|
||||||
import { runInstructMode } from './instructMode.js';
|
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 } from '../../interactive/index.js';
|
||||||
import { type BranchActionTarget, resolveTargetBranch } from './taskActionTarget.js';
|
import { type BranchActionTarget, resolveTargetBranch } from './taskActionTarget.js';
|
||||||
import { detectDefaultBranch } from '../../../infra/task/index.js';
|
|
||||||
import { appendRetryNote, selectRunSessionContext } from './requeueHelpers.js';
|
import { appendRetryNote, selectRunSessionContext } from './requeueHelpers.js';
|
||||||
|
import { executeAndCompleteTask } from '../execute/taskExecution.js';
|
||||||
|
|
||||||
const log = createLogger('list-tasks');
|
const log = createLogger('list-tasks');
|
||||||
|
|
||||||
@ -66,12 +74,17 @@ function getBranchContext(projectDir: string, branch: string): string {
|
|||||||
export async function instructBranch(
|
export async function instructBranch(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
target: BranchActionTarget,
|
target: BranchActionTarget,
|
||||||
_options?: TaskExecutionOptions,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!('kind' in target)) {
|
if (!('kind' in target)) {
|
||||||
throw new Error('Instruct requeue requires a task target.');
|
throw new Error('Instruct requeue requires a task target.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!target.worktreePath || !fs.existsSync(target.worktreePath)) {
|
||||||
|
logError(`Worktree directory does not exist for task: ${target.name}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const worktreePath = target.worktreePath;
|
||||||
|
|
||||||
const branch = resolveTargetBranch(target);
|
const branch = resolveTargetBranch(target);
|
||||||
|
|
||||||
const selectedPiece = await selectPiece(projectDir);
|
const selectedPiece = await selectPiece(projectDir);
|
||||||
@ -90,23 +103,30 @@ export async function instructBranch(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
const runSessionContext = await selectRunSessionContext(projectDir, lang);
|
// Runs data lives in the worktree (written during previous execution)
|
||||||
|
const runSessionContext = await selectRunSessionContext(worktreePath, lang);
|
||||||
|
|
||||||
const branchContext = getBranchContext(projectDir, branch);
|
const branchContext = getBranchContext(projectDir, branch);
|
||||||
const result = await runInstructMode(projectDir, branchContext, branch, pieceContext, runSessionContext);
|
|
||||||
|
|
||||||
const requeueWithInstruction = async (instruction: string): Promise<boolean> => {
|
const result = await runInstructMode(
|
||||||
const runner = new TaskRunner(projectDir);
|
worktreePath, branchContext, branch,
|
||||||
|
target.name, target.content, target.data?.retry_note ?? '',
|
||||||
|
pieceContext, runSessionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const executeWithInstruction = async (instruction: string): Promise<boolean> => {
|
||||||
const retryNote = appendRetryNote(target.data?.retry_note, instruction);
|
const retryNote = appendRetryNote(target.data?.retry_note, instruction);
|
||||||
runner.requeueTask(target.name, ['completed', 'failed'], undefined, retryNote);
|
const runner = new TaskRunner(projectDir);
|
||||||
success(`Task requeued with additional instructions: ${target.name}`);
|
const taskInfo = runner.startReExecution(target.name, ['completed', 'failed'], undefined, retryNote);
|
||||||
info(` Branch: ${branch}`);
|
|
||||||
log.info('Requeued task from instruct mode', {
|
log.info('Starting re-execution of instructed task', {
|
||||||
name: target.name,
|
name: target.name,
|
||||||
|
worktreePath,
|
||||||
branch,
|
branch,
|
||||||
piece: selectedPiece,
|
piece: selectedPiece,
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
|
return executeAndCompleteTask(taskInfo, runner, projectDir, selectedPiece);
|
||||||
};
|
};
|
||||||
|
|
||||||
return dispatchConversationAction(result, {
|
return dispatchConversationAction(result, {
|
||||||
@ -114,7 +134,13 @@ export async function instructBranch(
|
|||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
execute: async ({ task }) => requeueWithInstruction(task),
|
execute: async ({ task }) => executeWithInstruction(task),
|
||||||
save_task: async ({ task }) => requeueWithInstruction(task),
|
save_task: async ({ task }) => {
|
||||||
|
const retryNote = appendRetryNote(target.data?.retry_note, task);
|
||||||
|
const runner = new TaskRunner(projectDir);
|
||||||
|
runner.requeueTask(target.name, ['completed', 'failed'], undefined, retryNote);
|
||||||
|
info(`Task "${target.name}" has been requeued.`);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,31 @@
|
|||||||
/**
|
/**
|
||||||
* Retry actions for failed tasks.
|
* Retry actions for failed tasks.
|
||||||
*
|
*
|
||||||
* Provides interactive retry functionality including
|
* Uses the existing worktree (clone) for conversation and direct re-execution.
|
||||||
* failure info display and movement selection.
|
* The worktree is preserved after initial execution, so no clone creation is needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
import type { TaskListItem } from '../../../infra/task/index.js';
|
import type { TaskListItem } from '../../../infra/task/index.js';
|
||||||
import { TaskRunner } from '../../../infra/task/index.js';
|
import { TaskRunner } from '../../../infra/task/index.js';
|
||||||
import { loadPieceByIdentifier, loadGlobalConfig, getPieceDescription } from '../../../infra/config/index.js';
|
import { loadPieceByIdentifier, loadGlobalConfig, getPieceDescription } from '../../../infra/config/index.js';
|
||||||
|
import { selectPiece } from '../../pieceSelection/index.js';
|
||||||
import { selectOption } from '../../../shared/prompt/index.js';
|
import { selectOption } from '../../../shared/prompt/index.js';
|
||||||
import { success, error as logError, info, header, blankLine, status } from '../../../shared/ui/index.js';
|
import { info, header, blankLine, status } from '../../../shared/ui/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
import type { PieceConfig } from '../../../core/models/index.js';
|
import type { PieceConfig } from '../../../core/models/index.js';
|
||||||
import { runInstructMode } from './instructMode.js';
|
import {
|
||||||
import type { PieceContext } from '../../interactive/interactive.js';
|
findRunForTask,
|
||||||
import { resolveLanguage, selectRun, loadRunSessionContext, listRecentRuns, type RunSessionContext } from '../../interactive/index.js';
|
loadRunSessionContext,
|
||||||
import { getLabel } from '../../../shared/i18n/index.js';
|
getRunPaths,
|
||||||
import { confirm } from '../../../shared/prompt/index.js';
|
formatRunSessionForPrompt,
|
||||||
|
runRetryMode,
|
||||||
|
type RetryContext,
|
||||||
|
type RetryFailureInfo,
|
||||||
|
type RetryRunInfo,
|
||||||
|
} from '../../interactive/index.js';
|
||||||
|
import { executeAndCompleteTask } from '../execute/taskExecution.js';
|
||||||
|
import { appendRetryNote } from './requeueHelpers.js';
|
||||||
|
|
||||||
const log = createLogger('list-tasks');
|
const log = createLogger('list-tasks');
|
||||||
|
|
||||||
@ -58,42 +67,53 @@ async function selectStartMovement(
|
|||||||
return await selectOption<string>('Start from movement:', options);
|
return await selectOption<string>('Start from movement:', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendRetryNote(existing: string | undefined, additional: string): string {
|
function buildRetryFailureInfo(task: TaskListItem): RetryFailureInfo {
|
||||||
const trimmedAdditional = additional.trim();
|
return {
|
||||||
if (trimmedAdditional === '') {
|
taskName: task.name,
|
||||||
throw new Error('Additional instruction is empty.');
|
taskContent: task.content,
|
||||||
}
|
createdAt: task.createdAt,
|
||||||
if (!existing || existing.trim() === '') {
|
failedMovement: task.failure?.movement ?? '',
|
||||||
return trimmedAdditional;
|
error: task.failure?.error ?? '',
|
||||||
}
|
lastMessage: task.failure?.last_message ?? '',
|
||||||
return `${existing}\n\n${trimmedAdditional}`;
|
retryNote: task.data?.retry_note ?? '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRetryBranchContext(task: TaskListItem): string {
|
function buildRetryRunInfo(
|
||||||
const lines = [
|
runsBaseDir: string,
|
||||||
'## 失敗情報',
|
slug: string,
|
||||||
`- タスク名: ${task.name}`,
|
): RetryRunInfo {
|
||||||
`- 失敗日時: ${task.createdAt}`,
|
const paths = getRunPaths(runsBaseDir, slug);
|
||||||
];
|
const sessionContext = loadRunSessionContext(runsBaseDir, slug);
|
||||||
if (task.failure?.movement) {
|
const formatted = formatRunSessionForPrompt(sessionContext);
|
||||||
lines.push(`- 失敗ムーブメント: ${task.failure.movement}`);
|
return {
|
||||||
|
logsDir: paths.logsDir,
|
||||||
|
reportsDir: paths.reportsDir,
|
||||||
|
task: formatted.runTask,
|
||||||
|
piece: formatted.runPiece,
|
||||||
|
status: formatted.runStatus,
|
||||||
|
movementLogs: formatted.runMovementLogs,
|
||||||
|
reports: formatted.runReports,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorktreePath(task: TaskListItem): string {
|
||||||
|
if (!task.worktreePath) {
|
||||||
|
throw new Error(`Worktree path is not set for task: ${task.name}`);
|
||||||
}
|
}
|
||||||
if (task.failure?.error) {
|
if (!fs.existsSync(task.worktreePath)) {
|
||||||
lines.push(`- エラー: ${task.failure.error}`);
|
throw new Error(`Worktree directory does not exist: ${task.worktreePath}`);
|
||||||
}
|
}
|
||||||
if (task.failure?.last_message) {
|
return task.worktreePath;
|
||||||
lines.push(`- 最終メッセージ: ${task.failure.last_message}`);
|
|
||||||
}
|
|
||||||
if (task.data?.retry_note) {
|
|
||||||
lines.push('', '## 既存の再投入メモ', task.data.retry_note);
|
|
||||||
}
|
|
||||||
return `${lines.join('\n')}\n`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry a failed task.
|
* Retry a failed task.
|
||||||
*
|
*
|
||||||
* @returns true if task was requeued, false if cancelled
|
* Runs the retry conversation in the existing worktree, then directly
|
||||||
|
* re-executes the task there (auto-commit + push + status update).
|
||||||
|
*
|
||||||
|
* @returns true if task was re-executed successfully, false if cancelled or failed
|
||||||
*/
|
*/
|
||||||
export async function retryFailedTask(
|
export async function retryFailedTask(
|
||||||
task: TaskListItem,
|
task: TaskListItem,
|
||||||
@ -103,15 +123,21 @@ export async function retryFailedTask(
|
|||||||
throw new Error(`retryFailedTask requires failed task. received: ${task.kind}`);
|
throw new Error(`retryFailedTask requires failed task. received: ${task.kind}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const worktreePath = resolveWorktreePath(task);
|
||||||
|
|
||||||
displayFailureInfo(task);
|
displayFailureInfo(task);
|
||||||
|
|
||||||
|
const selectedPiece = await selectPiece(projectDir);
|
||||||
|
if (!selectedPiece) {
|
||||||
|
info('Cancelled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const globalConfig = loadGlobalConfig();
|
const globalConfig = loadGlobalConfig();
|
||||||
const pieceName = task.data?.piece ?? globalConfig.defaultPiece ?? 'default';
|
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
||||||
const pieceConfig = loadPieceByIdentifier(pieceName, projectDir);
|
|
||||||
|
|
||||||
if (!pieceConfig) {
|
if (!pieceConfig) {
|
||||||
logError(`Piece "${pieceName}" not found. Cannot determine available movements.`);
|
throw new Error(`Piece "${selectedPiece}" not found after selection.`);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedMovement = await selectStartMovement(pieceConfig, task.failure?.movement ?? null);
|
const selectedMovement = await selectStartMovement(pieceConfig, task.failure?.movement ?? null);
|
||||||
@ -119,72 +145,51 @@ export async function retryFailedTask(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pieceDesc = getPieceDescription(pieceName, projectDir, globalConfig.interactivePreviewMovements);
|
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
||||||
const pieceContext: PieceContext = {
|
const pieceContext = {
|
||||||
name: pieceDesc.name,
|
name: pieceDesc.name,
|
||||||
description: pieceDesc.description,
|
description: pieceDesc.description,
|
||||||
pieceStructure: pieceDesc.pieceStructure,
|
pieceStructure: pieceDesc.pieceStructure,
|
||||||
movementPreviews: pieceDesc.movementPreviews,
|
movementPreviews: pieceDesc.movementPreviews,
|
||||||
};
|
};
|
||||||
|
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
// Runs data lives in the worktree (written during previous execution)
|
||||||
let runSessionContext: RunSessionContext | undefined;
|
const matchedSlug = findRunForTask(worktreePath, task.content);
|
||||||
const hasRuns = listRecentRuns(projectDir).length > 0;
|
const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null;
|
||||||
if (hasRuns) {
|
|
||||||
const shouldReferenceRun = await confirm(
|
|
||||||
getLabel('interactive.runSelector.confirm', lang),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
if (shouldReferenceRun) {
|
|
||||||
const selectedSlug = await selectRun(projectDir, lang);
|
|
||||||
if (selectedSlug) {
|
|
||||||
runSessionContext = loadRunSessionContext(projectDir, selectedSlug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blankLine();
|
blankLine();
|
||||||
const branchContext = buildRetryBranchContext(task);
|
|
||||||
const branchName = task.branch ?? task.name;
|
const branchName = task.branch ?? task.name;
|
||||||
const instructResult = await runInstructMode(
|
const retryContext: RetryContext = {
|
||||||
projectDir,
|
failure: buildRetryFailureInfo(task),
|
||||||
branchContext,
|
|
||||||
branchName,
|
branchName,
|
||||||
pieceContext,
|
pieceContext,
|
||||||
runSessionContext,
|
run: runInfo,
|
||||||
);
|
};
|
||||||
if (instructResult.action !== 'execute') {
|
|
||||||
|
const retryResult = await runRetryMode(worktreePath, retryContext);
|
||||||
|
if (retryResult.action === 'cancel') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const startMovement = selectedMovement !== pieceConfig.initialMovement
|
||||||
const runner = new TaskRunner(projectDir);
|
? selectedMovement
|
||||||
const startMovement = selectedMovement !== pieceConfig.initialMovement
|
: undefined;
|
||||||
? selectedMovement
|
const retryNote = appendRetryNote(task.data?.retry_note, retryResult.task);
|
||||||
: undefined;
|
const runner = new TaskRunner(projectDir);
|
||||||
const retryNote = appendRetryNote(task.data?.retry_note, instructResult.task);
|
|
||||||
|
|
||||||
|
if (retryResult.action === 'save_task') {
|
||||||
runner.requeueTask(task.name, ['failed'], startMovement, retryNote);
|
runner.requeueTask(task.name, ['failed'], startMovement, retryNote);
|
||||||
|
info(`Task "${task.name}" has been requeued.`);
|
||||||
success(`Task requeued: ${task.name}`);
|
|
||||||
if (startMovement) {
|
|
||||||
info(` Will start from: ${startMovement}`);
|
|
||||||
}
|
|
||||||
info(' Retry note: updated');
|
|
||||||
info(` File: ${task.filePath}`);
|
|
||||||
|
|
||||||
log.info('Requeued failed task', {
|
|
||||||
name: task.name,
|
|
||||||
tasksFile: task.filePath,
|
|
||||||
startMovement,
|
|
||||||
retryNote,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
|
||||||
const msg = getErrorMessage(err);
|
|
||||||
logError(`Failed to requeue task: ${msg}`);
|
|
||||||
log.error('Failed to requeue task', { name: task.name, error: msg });
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const taskInfo = runner.startReExecution(task.name, ['failed'], startMovement, retryNote);
|
||||||
|
|
||||||
|
log.info('Starting re-execution of failed task', {
|
||||||
|
name: task.name,
|
||||||
|
worktreePath,
|
||||||
|
startMovement,
|
||||||
|
});
|
||||||
|
|
||||||
|
return executeAndCompleteTask(taskInfo, runner, projectDir, selectedPiece);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Global configuration loader
|
* Global configuration loader
|
||||||
*
|
*
|
||||||
* Manages ~/.takt/config.yaml and project-level debug settings.
|
* Manages ~/.takt/config.yaml.
|
||||||
* GlobalConfigManager encapsulates the config cache as a singleton.
|
* GlobalConfigManager encapsulates the config cache as a singleton.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -9,10 +9,10 @@ import { readFileSync, existsSync, writeFileSync, statSync, accessSync, constant
|
|||||||
import { isAbsolute } from 'node:path';
|
import { isAbsolute } from 'node:path';
|
||||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||||
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
||||||
import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js';
|
import type { GlobalConfig, Language } from '../../../core/models/index.js';
|
||||||
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
||||||
import { normalizeProviderOptions } from '../loaders/pieceParser.js';
|
import { normalizeProviderOptions } from '../loaders/pieceParser.js';
|
||||||
import { getGlobalConfigPath, getProjectConfigPath } from '../paths.js';
|
import { getGlobalConfigPath } from '../paths.js';
|
||||||
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
||||||
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
||||||
|
|
||||||
@ -168,10 +168,6 @@ export class GlobalConfigManager {
|
|||||||
logLevel: parsed.log_level,
|
logLevel: parsed.log_level,
|
||||||
provider: parsed.provider,
|
provider: parsed.provider,
|
||||||
model: parsed.model,
|
model: parsed.model,
|
||||||
debug: parsed.debug ? {
|
|
||||||
enabled: parsed.debug.enabled,
|
|
||||||
logFile: parsed.debug.log_file,
|
|
||||||
} : undefined,
|
|
||||||
observability: parsed.observability ? {
|
observability: parsed.observability ? {
|
||||||
providerEvents: parsed.observability.provider_events,
|
providerEvents: parsed.observability.provider_events,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
@ -228,12 +224,6 @@ export class GlobalConfigManager {
|
|||||||
if (config.model) {
|
if (config.model) {
|
||||||
raw.model = config.model;
|
raw.model = config.model;
|
||||||
}
|
}
|
||||||
if (config.debug) {
|
|
||||||
raw.debug = {
|
|
||||||
enabled: config.debug.enabled,
|
|
||||||
log_file: config.debug.logFile,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (config.observability && config.observability.providerEvents !== undefined) {
|
if (config.observability && config.observability.providerEvents !== undefined) {
|
||||||
raw.observability = {
|
raw.observability = {
|
||||||
provider_events: config.observability.providerEvents,
|
provider_events: config.observability.providerEvents,
|
||||||
@ -458,41 +448,3 @@ export function resolveOpencodeApiKey(): string | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load project-level debug configuration (from .takt/config.yaml) */
|
|
||||||
export function loadProjectDebugConfig(projectDir: string): DebugConfig | undefined {
|
|
||||||
const configPath = getProjectConfigPath(projectDir);
|
|
||||||
if (!existsSync(configPath)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const content = readFileSync(configPath, 'utf-8');
|
|
||||||
const raw = parseYaml(content);
|
|
||||||
if (raw && typeof raw === 'object' && 'debug' in raw) {
|
|
||||||
const debug = raw.debug;
|
|
||||||
if (debug && typeof debug === 'object') {
|
|
||||||
return {
|
|
||||||
enabled: Boolean(debug.enabled),
|
|
||||||
logFile: typeof debug.log_file === 'string' ? debug.log_file : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore parse errors
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get effective debug config (project overrides global) */
|
|
||||||
export function getEffectiveDebugConfig(projectDir?: string): DebugConfig | undefined {
|
|
||||||
const globalConfig = loadGlobalConfig();
|
|
||||||
let debugConfig = globalConfig.debug;
|
|
||||||
|
|
||||||
if (projectDir) {
|
|
||||||
const projectDebugConfig = loadProjectDebugConfig(projectDir);
|
|
||||||
if (projectDebugConfig) {
|
|
||||||
debugConfig = projectDebugConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return debugConfig;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -16,8 +16,6 @@ export {
|
|||||||
resolveOpenaiApiKey,
|
resolveOpenaiApiKey,
|
||||||
resolveCodexCliPath,
|
resolveCodexCliPath,
|
||||||
resolveOpencodeApiKey,
|
resolveOpencodeApiKey,
|
||||||
loadProjectDebugConfig,
|
|
||||||
getEffectiveDebugConfig,
|
|
||||||
} from './globalConfig.js';
|
} from './globalConfig.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -28,6 +28,4 @@ export {
|
|||||||
loadGlobalConfig,
|
loadGlobalConfig,
|
||||||
saveGlobalConfig,
|
saveGlobalConfig,
|
||||||
invalidateGlobalConfigCache,
|
invalidateGlobalConfigCache,
|
||||||
loadProjectDebugConfig,
|
|
||||||
getEffectiveDebugConfig,
|
|
||||||
} from '../global/globalConfig.js';
|
} from '../global/globalConfig.js';
|
||||||
|
|||||||
@ -83,6 +83,15 @@ export class TaskRunner {
|
|||||||
return this.lifecycle.requeueTask(taskRef, allowedStatuses, startMovement, retryNote);
|
return this.lifecycle.requeueTask(taskRef, allowedStatuses, startMovement, retryNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startReExecution(
|
||||||
|
taskRef: string,
|
||||||
|
allowedStatuses: readonly TaskStatus[],
|
||||||
|
startMovement?: string,
|
||||||
|
retryNote?: string,
|
||||||
|
): TaskInfo {
|
||||||
|
return this.lifecycle.startReExecution(taskRef, allowedStatuses, startMovement, retryNote);
|
||||||
|
}
|
||||||
|
|
||||||
deletePendingTask(name: string): void {
|
deletePendingTask(name: string): void {
|
||||||
this.deletion.deletePendingTask(name);
|
this.deletion.deletePendingTask(name);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,6 +156,49 @@ export class TaskLifecycleService {
|
|||||||
return this.requeueTask(taskRef, ['failed'], startMovement, retryNote);
|
return this.requeueTask(taskRef, ['failed'], startMovement, retryNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically transition a completed/failed task to running for re-execution.
|
||||||
|
* Avoids the race condition of requeueTask(→ pending) + claimNextTasks(→ running).
|
||||||
|
*/
|
||||||
|
startReExecution(
|
||||||
|
taskRef: string,
|
||||||
|
allowedStatuses: readonly TaskStatus[],
|
||||||
|
startMovement?: string,
|
||||||
|
retryNote?: string,
|
||||||
|
): TaskInfo {
|
||||||
|
const taskName = this.normalizeTaskRef(taskRef);
|
||||||
|
let found: TaskRecord | undefined;
|
||||||
|
|
||||||
|
this.store.update((current) => {
|
||||||
|
const index = current.tasks.findIndex((task) => (
|
||||||
|
task.name === taskName
|
||||||
|
&& allowedStatuses.includes(task.status)
|
||||||
|
));
|
||||||
|
if (index === -1) {
|
||||||
|
const expectedStatuses = allowedStatuses.join(', ');
|
||||||
|
throw new Error(`Task not found for re-execution: ${taskRef} (expected status: ${expectedStatuses})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = current.tasks[index]!;
|
||||||
|
const updated: TaskRecord = {
|
||||||
|
...target,
|
||||||
|
status: 'running',
|
||||||
|
started_at: nowIso(),
|
||||||
|
owner_pid: process.pid,
|
||||||
|
failure: undefined,
|
||||||
|
start_movement: startMovement,
|
||||||
|
retry_note: retryNote,
|
||||||
|
};
|
||||||
|
|
||||||
|
found = updated;
|
||||||
|
const tasks = [...current.tasks];
|
||||||
|
tasks[index] = updated;
|
||||||
|
return { tasks };
|
||||||
|
});
|
||||||
|
|
||||||
|
return toTaskInfo(this.projectDir, this.tasksFile, found!);
|
||||||
|
}
|
||||||
|
|
||||||
requeueTask(
|
requeueTask(
|
||||||
taskRef: string,
|
taskRef: string,
|
||||||
allowedStatuses: readonly TaskStatus[],
|
allowedStatuses: readonly TaskStatus[],
|
||||||
|
|||||||
87
src/shared/prompts/en/score_instruct_system_prompt.md
Normal file
87
src/shared/prompts/en/score_instruct_system_prompt.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<!--
|
||||||
|
template: score_instruct_system_prompt
|
||||||
|
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
|
||||||
|
caller: features/tasks/list/instructMode
|
||||||
|
-->
|
||||||
|
# Additional Instruction Assistant
|
||||||
|
|
||||||
|
Reviews completed task artifacts and creates additional instructions for re-execution.
|
||||||
|
|
||||||
|
## How TAKT Works
|
||||||
|
|
||||||
|
1. **Additional Instruction Assistant (your role)**: Review branch changes and execution results, then converse with users to create additional instructions for re-execution
|
||||||
|
2. **Piece Execution**: Pass the created instructions to the piece, where multiple AI agents execute sequentially
|
||||||
|
|
||||||
|
## Role Boundaries
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Explain the current situation based on branch changes (diffs, commit history)
|
||||||
|
- Answer user questions with awareness of the change context
|
||||||
|
- Create concrete additional instructions for the work that still needs to be done
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
- Fix code (piece's job)
|
||||||
|
- Execute tasks directly (piece's job)
|
||||||
|
- Mention slash commands
|
||||||
|
|
||||||
|
## Task Information
|
||||||
|
|
||||||
|
**Task name:** {{taskName}}
|
||||||
|
**Original instruction:** {{taskContent}}
|
||||||
|
**Branch:** {{branchName}}
|
||||||
|
|
||||||
|
## Branch Changes
|
||||||
|
|
||||||
|
{{branchContext}}
|
||||||
|
{{#if retryNote}}
|
||||||
|
|
||||||
|
## Existing Retry Note
|
||||||
|
|
||||||
|
Instructions added from previous attempts.
|
||||||
|
|
||||||
|
{{retryNote}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasPiecePreview}}
|
||||||
|
|
||||||
|
## Piece Structure
|
||||||
|
|
||||||
|
This task will be processed through the following workflow:
|
||||||
|
{{pieceStructure}}
|
||||||
|
|
||||||
|
### Agent Details
|
||||||
|
|
||||||
|
The following agents will process the task sequentially. Understand each agent's capabilities and instructions to improve the quality of your task instructions.
|
||||||
|
|
||||||
|
{{movementDetails}}
|
||||||
|
|
||||||
|
### Delegation Guidance
|
||||||
|
|
||||||
|
- Do not include excessive detail in instructions for things the agents above can investigate and determine on their own
|
||||||
|
- Clearly include information that agents cannot resolve on their own (user intent, priorities, constraints, etc.)
|
||||||
|
- Delegate codebase investigation, implementation details, and dependency analysis to the agents
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasRunSession}}
|
||||||
|
|
||||||
|
## Previous Run Reference
|
||||||
|
|
||||||
|
The user has selected a previous run for reference. Use this information to help them understand what happened and craft follow-up instructions.
|
||||||
|
|
||||||
|
**Task:** {{runTask}}
|
||||||
|
**Piece:** {{runPiece}}
|
||||||
|
**Status:** {{runStatus}}
|
||||||
|
|
||||||
|
### Movement Logs
|
||||||
|
|
||||||
|
{{runMovementLogs}}
|
||||||
|
|
||||||
|
### Reports
|
||||||
|
|
||||||
|
{{runReports}}
|
||||||
|
|
||||||
|
### Guidance
|
||||||
|
|
||||||
|
- Reference specific movement results when discussing issues or improvements
|
||||||
|
- Help the user identify what went wrong or what needs additional work
|
||||||
|
- Suggest concrete follow-up instructions based on the run results
|
||||||
|
{{/if}}
|
||||||
97
src/shared/prompts/en/score_retry_system_prompt.md
Normal file
97
src/shared/prompts/en/score_retry_system_prompt.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!--
|
||||||
|
template: score_retry_system_prompt
|
||||||
|
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
|
||||||
|
caller: features/interactive/retryMode
|
||||||
|
-->
|
||||||
|
# Retry Assistant
|
||||||
|
|
||||||
|
Diagnoses failed tasks and creates additional instructions for re-execution.
|
||||||
|
|
||||||
|
## How TAKT Works
|
||||||
|
|
||||||
|
1. **Retry Assistant (your role)**: Analyze failure causes and converse with users to create instructions for re-execution
|
||||||
|
2. **Piece Execution**: Pass the created instructions to the piece, where multiple AI agents execute sequentially
|
||||||
|
|
||||||
|
## Role Boundaries
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Analyze failure information and explain possible causes to the user
|
||||||
|
- Answer user questions with awareness of the failure context
|
||||||
|
- Create concrete additional instructions that will help the re-execution succeed
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
- Fix code (piece's job)
|
||||||
|
- Execute tasks directly (piece's job)
|
||||||
|
- Mention slash commands
|
||||||
|
|
||||||
|
## Failure Information
|
||||||
|
|
||||||
|
**Task name:** {{taskName}}
|
||||||
|
**Original instruction:** {{taskContent}}
|
||||||
|
**Branch:** {{branchName}}
|
||||||
|
**Failed at:** {{createdAt}}
|
||||||
|
{{#if failedMovement}}
|
||||||
|
**Failed movement:** {{failedMovement}}
|
||||||
|
{{/if}}
|
||||||
|
**Error:** {{failureError}}
|
||||||
|
{{#if failureLastMessage}}
|
||||||
|
|
||||||
|
### Last Message
|
||||||
|
|
||||||
|
{{failureLastMessage}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if retryNote}}
|
||||||
|
|
||||||
|
## Existing Retry Note
|
||||||
|
|
||||||
|
Instructions added from previous retry attempts.
|
||||||
|
|
||||||
|
{{retryNote}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasPiecePreview}}
|
||||||
|
|
||||||
|
## Piece Structure
|
||||||
|
|
||||||
|
This task will be processed through the following workflow:
|
||||||
|
{{pieceStructure}}
|
||||||
|
|
||||||
|
### Agent Details
|
||||||
|
|
||||||
|
The following agents will process the task sequentially. Understand each agent's capabilities and instructions to improve the quality of your task instructions.
|
||||||
|
|
||||||
|
{{movementDetails}}
|
||||||
|
|
||||||
|
### Delegation Guidance
|
||||||
|
|
||||||
|
- Do not include excessive detail in instructions for things the agents above can investigate and determine on their own
|
||||||
|
- Clearly include information that agents cannot resolve on their own (user intent, priorities, constraints, etc.)
|
||||||
|
- Delegate codebase investigation, implementation details, and dependency analysis to the agents
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasRun}}
|
||||||
|
|
||||||
|
## Previous Run Data
|
||||||
|
|
||||||
|
Logs and reports from the previous execution are available for reference. Use them to identify the failure cause.
|
||||||
|
|
||||||
|
**Logs directory:** {{runLogsDir}}
|
||||||
|
**Reports directory:** {{runReportsDir}}
|
||||||
|
|
||||||
|
**Task:** {{runTask}}
|
||||||
|
**Piece:** {{runPiece}}
|
||||||
|
**Status:** {{runStatus}}
|
||||||
|
|
||||||
|
### Movement Logs
|
||||||
|
|
||||||
|
{{runMovementLogs}}
|
||||||
|
|
||||||
|
### Reports
|
||||||
|
|
||||||
|
{{runReports}}
|
||||||
|
|
||||||
|
### Analysis Guidance
|
||||||
|
|
||||||
|
- Focus on the movement logs where the error occurred
|
||||||
|
- 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}}
|
||||||
87
src/shared/prompts/ja/score_instruct_system_prompt.md
Normal file
87
src/shared/prompts/ja/score_instruct_system_prompt.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<!--
|
||||||
|
template: score_instruct_system_prompt
|
||||||
|
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
|
||||||
|
caller: features/tasks/list/instructMode
|
||||||
|
-->
|
||||||
|
# 追加指示アシスタント
|
||||||
|
|
||||||
|
完了済みタスクの成果物を確認し、再実行のための追加指示を作成する。
|
||||||
|
|
||||||
|
## TAKTの仕組み
|
||||||
|
|
||||||
|
1. **追加指示アシスタント(あなたの役割)**: ブランチの変更内容と実行結果を確認し、ユーザーと対話して再実行用の追加指示を作成する
|
||||||
|
2. **ピース実行**: 作成した指示書をピースに渡し、複数のAIエージェントが順次実行する
|
||||||
|
|
||||||
|
## 役割の境界
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- ブランチの変更内容(差分・コミット履歴)を踏まえて状況を説明する
|
||||||
|
- ユーザーの質問に変更コンテキストを踏まえて回答する
|
||||||
|
- 追加で必要な作業を具体的な指示として作成する
|
||||||
|
|
||||||
|
**やらないこと:**
|
||||||
|
- コードの修正(ピースの仕事)
|
||||||
|
- タスクの直接実行(ピースの仕事)
|
||||||
|
- スラッシュコマンドへの言及
|
||||||
|
|
||||||
|
## タスク情報
|
||||||
|
|
||||||
|
**タスク名:** {{taskName}}
|
||||||
|
**元の指示:** {{taskContent}}
|
||||||
|
**ブランチ:** {{branchName}}
|
||||||
|
|
||||||
|
## ブランチの変更内容
|
||||||
|
|
||||||
|
{{branchContext}}
|
||||||
|
{{#if retryNote}}
|
||||||
|
|
||||||
|
## 既存の再投入メモ
|
||||||
|
|
||||||
|
以前の追加指示で設定された内容です。
|
||||||
|
|
||||||
|
{{retryNote}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasPiecePreview}}
|
||||||
|
|
||||||
|
## ピース構成
|
||||||
|
|
||||||
|
このタスクは以下のワークフローで処理されます:
|
||||||
|
{{pieceStructure}}
|
||||||
|
|
||||||
|
### エージェント詳細
|
||||||
|
|
||||||
|
以下のエージェントが順次タスクを処理します。各エージェントの能力と指示内容を理解し、指示書の質を高めてください。
|
||||||
|
|
||||||
|
{{movementDetails}}
|
||||||
|
|
||||||
|
### 委譲ガイダンス
|
||||||
|
|
||||||
|
- 上記エージェントが自ら調査・判断できる内容は、指示書に過度な詳細を含める必要はありません
|
||||||
|
- エージェントが自力で解決できない情報(ユーザーの意図、優先度、制約条件など)を指示書に明確に含めてください
|
||||||
|
- コードベースの調査、実装詳細の特定、依存関係の解析はエージェントに委ねてください
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasRunSession}}
|
||||||
|
|
||||||
|
## 前回実行の参照
|
||||||
|
|
||||||
|
ユーザーが前回の実行結果を参照として選択しました。この情報を使って、何が起きたかを理解し、追加指示の作成を支援してください。
|
||||||
|
|
||||||
|
**タスク:** {{runTask}}
|
||||||
|
**ピース:** {{runPiece}}
|
||||||
|
**ステータス:** {{runStatus}}
|
||||||
|
|
||||||
|
### ムーブメントログ
|
||||||
|
|
||||||
|
{{runMovementLogs}}
|
||||||
|
|
||||||
|
### レポート
|
||||||
|
|
||||||
|
{{runReports}}
|
||||||
|
|
||||||
|
### ガイダンス
|
||||||
|
|
||||||
|
- 問題点や改善点を議論する際は、具体的なムーブメントの結果を参照してください
|
||||||
|
- 何がうまくいかなかったか、追加作業が必要な箇所をユーザーが特定できるよう支援してください
|
||||||
|
- 実行結果に基づいて、具体的なフォローアップ指示を提案してください
|
||||||
|
{{/if}}
|
||||||
97
src/shared/prompts/ja/score_retry_system_prompt.md
Normal file
97
src/shared/prompts/ja/score_retry_system_prompt.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!--
|
||||||
|
template: score_retry_system_prompt
|
||||||
|
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
|
||||||
|
caller: features/interactive/retryMode
|
||||||
|
-->
|
||||||
|
# リトライアシスタント
|
||||||
|
|
||||||
|
失敗したタスクの診断と、再実行のための追加指示作成を担当する。
|
||||||
|
|
||||||
|
## TAKTの仕組み
|
||||||
|
|
||||||
|
1. **リトライアシスタント(あなたの役割)**: 失敗原因を分析し、ユーザーと対話して再実行用の指示書を作成する
|
||||||
|
2. **ピース実行**: 作成した指示書をピースに渡し、複数のAIエージェントが順次実行する
|
||||||
|
|
||||||
|
## 役割の境界
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- 失敗情報を分析し、考えられる原因をユーザーに説明する
|
||||||
|
- ユーザーの質問に失敗コンテキストを踏まえて回答する
|
||||||
|
- 再実行時に成功するための具体的な追加指示を作成する
|
||||||
|
|
||||||
|
**やらないこと:**
|
||||||
|
- コードの修正(ピースの仕事)
|
||||||
|
- タスクの直接実行(ピースの仕事)
|
||||||
|
- スラッシュコマンドへの言及
|
||||||
|
|
||||||
|
## 失敗情報
|
||||||
|
|
||||||
|
**タスク名:** {{taskName}}
|
||||||
|
**元の指示:** {{taskContent}}
|
||||||
|
**ブランチ:** {{branchName}}
|
||||||
|
**失敗日時:** {{createdAt}}
|
||||||
|
{{#if failedMovement}}
|
||||||
|
**失敗ムーブメント:** {{failedMovement}}
|
||||||
|
{{/if}}
|
||||||
|
**エラー:** {{failureError}}
|
||||||
|
{{#if failureLastMessage}}
|
||||||
|
|
||||||
|
### 最終メッセージ
|
||||||
|
|
||||||
|
{{failureLastMessage}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if retryNote}}
|
||||||
|
|
||||||
|
## 既存の再投入メモ
|
||||||
|
|
||||||
|
以前のリトライで追加された指示です。
|
||||||
|
|
||||||
|
{{retryNote}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasPiecePreview}}
|
||||||
|
|
||||||
|
## ピース構成
|
||||||
|
|
||||||
|
このタスクは以下のワークフローで処理されます:
|
||||||
|
{{pieceStructure}}
|
||||||
|
|
||||||
|
### エージェント詳細
|
||||||
|
|
||||||
|
以下のエージェントが順次タスクを処理します。各エージェントの能力と指示内容を理解し、指示書の質を高めてください。
|
||||||
|
|
||||||
|
{{movementDetails}}
|
||||||
|
|
||||||
|
### 委譲ガイダンス
|
||||||
|
|
||||||
|
- 上記エージェントが自ら調査・判断できる内容は、指示書に過度な詳細を含める必要はありません
|
||||||
|
- エージェントが自力で解決できない情報(ユーザーの意図、優先度、制約条件など)を指示書に明確に含めてください
|
||||||
|
- コードベースの調査、実装詳細の特定、依存関係の解析はエージェントに委ねてください
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasRun}}
|
||||||
|
|
||||||
|
## 前回実行データ
|
||||||
|
|
||||||
|
前回の実行ログとレポートを参照できます。失敗原因の特定に活用してください。
|
||||||
|
|
||||||
|
**ログディレクトリ:** {{runLogsDir}}
|
||||||
|
**レポートディレクトリ:** {{runReportsDir}}
|
||||||
|
|
||||||
|
**タスク:** {{runTask}}
|
||||||
|
**ピース:** {{runPiece}}
|
||||||
|
**ステータス:** {{runStatus}}
|
||||||
|
|
||||||
|
### ムーブメントログ
|
||||||
|
|
||||||
|
{{runMovementLogs}}
|
||||||
|
|
||||||
|
### レポート
|
||||||
|
|
||||||
|
{{runReports}}
|
||||||
|
|
||||||
|
### 分析ガイダンス
|
||||||
|
|
||||||
|
- エラーが発生したムーブメントのログを重点的に確認してください
|
||||||
|
- レポートに記録された計画や実装内容と、実際の失敗箇所を照合してください
|
||||||
|
- ユーザーが詳細を知りたい場合は、上記ディレクトリのファイルを Read ツールで参照できます
|
||||||
|
{{/if}}
|
||||||
Loading…
x
Reference in New Issue
Block a user