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