github-issue-256-takt-list-instruct (#267)

* fix: OpenCode SDKサーバー起動タイムアウトを30秒に延長

* takt: github-issue-256-takt-list-instruct

* refactor: 会話後アクションフローを共通化
This commit is contained in:
nrs 2026-02-13 22:08:28 +09:00 committed by GitHub
parent 02272e595c
commit 4e58c86643
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 686 additions and 83 deletions

View File

@ -0,0 +1,39 @@
import { describe, it, expect, vi } from 'vitest';
import { dispatchConversationAction } from '../features/interactive/actionDispatcher.js';
describe('dispatchConversationAction', () => {
it('should dispatch to matching handler with full result payload', async () => {
const execute = vi.fn().mockResolvedValue('executed');
const saveTask = vi.fn().mockResolvedValue('saved');
const cancel = vi.fn().mockResolvedValue('cancelled');
const result = await dispatchConversationAction(
{ action: 'save_task', task: 'refine branch docs' },
{
execute,
save_task: saveTask,
cancel,
},
);
expect(result).toBe('saved');
expect(saveTask).toHaveBeenCalledWith({ action: 'save_task', task: 'refine branch docs' });
expect(execute).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
});
it('should support synchronous handlers', async () => {
const result = await dispatchConversationAction(
{ action: 'cancel', task: '' },
{
execute: () => true,
save_task: () => true,
cancel: () => false,
},
);
expect(result).toBe(false);
});
});

View File

@ -0,0 +1,282 @@
/**
* Tests for instruct mode
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
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(),
}));
vi.mock('../shared/i18n/index.js', () => ({
getLabel: vi.fn((_key: string, _lang: string) => 'Mock label'),
getLabelObject: vi.fn(() => ({
intro: 'Instruct mode intro',
resume: 'Resuming',
noConversation: 'No conversation',
summarizeFailed: 'Summarize failed',
continuePrompt: 'Continue',
proposed: 'Proposed task:',
actionPrompt: 'What to do?',
actions: {
execute: 'Execute',
saveTask: 'Save task',
continue: 'Continue',
},
cancelled: 'Cancelled',
})),
}));
vi.mock('../shared/prompts/index.js', () => ({
loadTemplate: vi.fn((_name: string, _lang: string) => 'Mock template content'),
}));
import { getProvider } from '../infra/providers/index.js';
import { runInstructMode } from '../features/tasks/list/instructMode.js';
import { selectOption } from '../shared/prompt/index.js';
import { info } from '../shared/ui/index.js';
const mockGetProvider = vi.mocked(getProvider);
const mockSelectOption = vi.mocked(selectOption);
const mockInfo = vi.mocked(info);
let savedIsTTY: boolean | undefined;
let savedIsRaw: boolean | undefined;
let savedSetRawMode: typeof process.stdin.setRawMode | undefined;
let savedStdoutWrite: typeof process.stdout.write;
let savedStdinOn: typeof process.stdin.on;
let savedStdinRemoveListener: typeof process.stdin.removeListener;
let savedStdinResume: typeof process.stdin.resume;
let savedStdinPause: typeof process.stdin.pause;
function setupRawStdin(rawInputs: string[]): void {
savedIsTTY = process.stdin.isTTY;
savedIsRaw = process.stdin.isRaw;
savedSetRawMode = process.stdin.setRawMode;
savedStdoutWrite = process.stdout.write;
savedStdinOn = process.stdin.on;
savedStdinRemoveListener = process.stdin.removeListener;
savedStdinResume = process.stdin.resume;
savedStdinPause = process.stdin.pause;
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
Object.defineProperty(process.stdin, 'isRaw', { value: false, configurable: true, writable: true });
process.stdin.setRawMode = vi.fn((mode: boolean) => {
(process.stdin as unknown as { isRaw: boolean }).isRaw = mode;
return process.stdin;
}) as unknown as typeof process.stdin.setRawMode;
process.stdout.write = vi.fn(() => true) as unknown as typeof process.stdout.write;
process.stdin.resume = vi.fn(() => process.stdin) as unknown as typeof process.stdin.resume;
process.stdin.pause = vi.fn(() => process.stdin) as unknown as typeof process.stdin.pause;
let currentHandler: ((data: Buffer) => void) | null = null;
let inputIndex = 0;
process.stdin.on = vi.fn(((event: string, handler: (...args: unknown[]) => void) => {
if (event === 'data') {
currentHandler = handler as (data: Buffer) => void;
if (inputIndex < rawInputs.length) {
const data = rawInputs[inputIndex]!;
inputIndex++;
queueMicrotask(() => {
if (currentHandler) {
currentHandler(Buffer.from(data, 'utf-8'));
}
});
}
}
return process.stdin;
}) as typeof process.stdin.on);
process.stdin.removeListener = vi.fn(((event: string) => {
if (event === 'data') {
currentHandler = null;
}
return process.stdin;
}) as typeof process.stdin.removeListener);
}
function restoreStdin(): void {
if (savedIsTTY !== undefined) {
Object.defineProperty(process.stdin, 'isTTY', { value: savedIsTTY, configurable: true });
}
if (savedIsRaw !== undefined) {
Object.defineProperty(process.stdin, 'isRaw', { value: savedIsRaw, configurable: true, writable: true });
}
if (savedSetRawMode) {
process.stdin.setRawMode = savedSetRawMode;
}
if (savedStdoutWrite) {
process.stdout.write = savedStdoutWrite;
}
if (savedStdinOn) {
process.stdin.on = savedStdinOn;
}
if (savedStdinRemoveListener) {
process.stdin.removeListener = savedStdinRemoveListener;
}
if (savedStdinResume) {
process.stdin.resume = savedStdinResume;
}
if (savedStdinPause) {
process.stdin.pause = savedStdinPause;
}
}
function toRawInputs(inputs: (string | null)[]): string[] {
return inputs.map((input) => {
if (input === null) return '\x04';
return input + '\r';
});
}
function setupMockProvider(responses: string[]): void {
let callIndex = 0;
const mockCall = vi.fn(async () => {
const content = callIndex < responses.length ? responses[callIndex] : 'AI response';
callIndex++;
return {
persona: 'instruct',
status: 'done' as const,
content: content!,
timestamp: new Date(),
};
});
const mockProvider = {
setup: () => ({ call: mockCall }),
_call: mockCall,
};
mockGetProvider.mockReturnValue(mockProvider);
}
beforeEach(() => {
vi.clearAllMocks();
mockSelectOption.mockResolvedValue('execute');
});
afterEach(() => {
restoreStdin();
});
describe('runInstructMode', () => {
it('should return action=cancel when user types /cancel', async () => {
setupRawStdin(toRawInputs(['/cancel']));
setupMockProvider([]);
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
expect(result.action).toBe('cancel');
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 () => {
setupRawStdin(toRawInputs(['add more tests', '/go']));
setupMockProvider(['What kind of tests?', 'Add unit tests for the feature.']);
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
expect(result.action).toBe('execute');
expect(result.task).toBe('Add unit tests for the feature.');
});
it('should return action=save_task when user selects save task', async () => {
setupRawStdin(toRawInputs(['describe task', '/go']));
setupMockProvider(['response', 'Summarized task.']);
mockSelectOption.mockResolvedValue('save_task');
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
expect(result.action).toBe('save_task');
expect(result.task).toBe('Summarized task.');
});
it('should continue editing when user selects continue', async () => {
setupRawStdin(toRawInputs(['describe task', '/go', '/cancel']));
setupMockProvider(['response', 'Summarized task.']);
mockSelectOption.mockResolvedValueOnce('continue');
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
expect(result.action).toBe('cancel');
});
it('should reject /go with no prior conversation', async () => {
setupRawStdin(toRawInputs(['/go', '/cancel']));
setupMockProvider([]);
const result = await runInstructMode('/project', 'branch context', 'feature-branch');
expect(result.action).toBe('cancel');
});
it('should use custom action selector without create_issue option', async () => {
setupRawStdin(toRawInputs(['task', '/go']));
setupMockProvider(['response', 'Task summary.']);
await runInstructMode('/project', 'branch context', 'feature-branch');
const selectCall = mockSelectOption.mock.calls.find((call) =>
Array.isArray(call[1])
);
expect(selectCall).toBeDefined();
const options = selectCall![1] as Array<{ value: string }>;
const values = options.map((o) => o.value);
expect(values).toContain('execute');
expect(values).toContain('save_task');
expect(values).toContain('continue');
expect(values).not.toContain('create_issue');
});
});

View File

@ -22,6 +22,7 @@ import {
resolveLanguage,
type InteractiveModeResult,
} from '../../features/interactive/index.js';
import { dispatchConversationAction } from '../../features/interactive/actionDispatcher.js';
import { getPieceDescription, loadGlobalConfig } from '../../infra/config/index.js';
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
import { program, resolvedCwd, pipelineMode } from './program.js';
@ -202,33 +203,27 @@ export async function executeDefaultAction(task?: string): Promise<void> {
}
}
switch (result.action) {
case 'execute':
await dispatchConversationAction(result, {
execute: async ({ task: confirmedTask }) => {
selectOptions.interactiveUserInput = true;
selectOptions.piece = pieceId;
selectOptions.interactiveMetadata = { confirmed: true, task: result.task };
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
break;
case 'create_issue':
{
const issueNumber = createIssueFromTask(result.task);
if (issueNumber !== undefined) {
await saveTaskFromInteractive(resolvedCwd, result.task, pieceId, {
issue: issueNumber,
confirmAtEndMessage: 'Add this issue to tasks?',
});
}
selectOptions.interactiveMetadata = { confirmed: true, task: confirmedTask };
await selectAndExecuteTask(resolvedCwd, confirmedTask, selectOptions, agentOverrides);
},
create_issue: async ({ task: confirmedTask }) => {
const issueNumber = createIssueFromTask(confirmedTask);
if (issueNumber !== undefined) {
await saveTaskFromInteractive(resolvedCwd, confirmedTask, pieceId, {
issue: issueNumber,
confirmAtEndMessage: 'Add this issue to tasks?',
});
}
break;
case 'save_task':
await saveTaskFromInteractive(resolvedCwd, result.task, pieceId);
break;
case 'cancel':
break;
}
},
save_task: async ({ task: confirmedTask }) => {
await saveTaskFromInteractive(resolvedCwd, confirmedTask, pieceId);
},
cancel: () => undefined,
});
}
program

View File

@ -0,0 +1,20 @@
/**
* Shared dispatcher for post-conversation actions.
*/
export interface ConversationActionResult<A extends string> {
action: A;
task: string;
}
export type ConversationActionHandler<A extends string, R> = (
result: ConversationActionResult<A>,
) => Promise<R> | R;
export async function dispatchConversationAction<A extends string, R>(
result: ConversationActionResult<A>,
handlers: Record<A, ConversationActionHandler<A, R>>,
): Promise<R> {
return handlers[result.action](result);
}

View File

@ -28,6 +28,7 @@ import {
type InteractiveModeResult,
type InteractiveUIText,
type ConversationMessage,
type PostSummaryAction,
resolveLanguage,
buildSummaryPrompt,
selectPostSummaryAction,
@ -171,6 +172,8 @@ export async function callAIWithRetry(
}
}
export type { PostSummaryAction } from './interactive.js';
/** Strategy for customizing conversation loop behavior */
export interface ConversationStrategy {
/** System prompt for AI calls */
@ -181,6 +184,8 @@ export interface ConversationStrategy {
transformPrompt: (userMessage: string) => string;
/** Intro message displayed at start */
introMessage: string;
/** Custom action selector (optional). If not provided, uses default selectPostSummaryAction. */
selectAction?: (task: string, lang: 'en' | 'ja') => Promise<PostSummaryAction | null>;
}
/**
@ -284,7 +289,9 @@ export async function runConversationLoop(
return { action: 'cancel', task: '' };
}
const task = summaryResult.content.trim();
const selectedAction = await selectPostSummaryAction(task, ui.proposed, ui);
const selectedAction = strategy.selectAction
? await strategy.selectAction(task, ctx.lang)
: await selectPostSummaryAction(task, ui.proposed, ui);
if (selectedAction === 'continue' || selectedAction === null) {
info(ui.continuePrompt);
continue;

View File

@ -169,21 +169,90 @@ export function buildSummaryPrompt(
export type PostSummaryAction = InteractiveModeAction | 'continue';
export async function selectPostSummaryAction(
export type SummaryActionValue = 'execute' | 'create_issue' | 'save_task' | 'continue';
export interface SummaryActionOption {
label: string;
value: SummaryActionValue;
}
export type SummaryActionLabels = {
execute: string;
createIssue?: string;
saveTask: string;
continue: string;
};
export const BASE_SUMMARY_ACTIONS: readonly SummaryActionValue[] = [
'execute',
'save_task',
'continue',
];
export function buildSummaryActionOptions(
labels: SummaryActionLabels,
append: readonly SummaryActionValue[] = [],
): SummaryActionOption[] {
const order = [...BASE_SUMMARY_ACTIONS, ...append];
const seen = new Set<SummaryActionValue>();
const options: SummaryActionOption[] = [];
for (const action of order) {
if (seen.has(action)) continue;
seen.add(action);
if (action === 'execute') {
options.push({ label: labels.execute, value: action });
continue;
}
if (action === 'create_issue') {
if (labels.createIssue) {
options.push({ label: labels.createIssue, value: action });
}
continue;
}
if (action === 'save_task') {
options.push({ label: labels.saveTask, value: action });
continue;
}
options.push({ label: labels.continue, value: action });
}
return options;
}
export async function selectSummaryAction(
task: string,
proposedLabel: string,
ui: InteractiveUIText,
actionPrompt: string,
options: SummaryActionOption[],
): Promise<PostSummaryAction | null> {
blankLine();
info(proposedLabel);
console.log(task);
return selectOption<PostSummaryAction>(ui.actionPrompt, [
{ label: ui.actions.execute, value: 'execute' },
{ label: ui.actions.createIssue, value: 'create_issue' },
{ label: ui.actions.saveTask, value: 'save_task' },
{ label: ui.actions.continue, value: 'continue' },
]);
return selectOption<PostSummaryAction>(actionPrompt, options);
}
export async function selectPostSummaryAction(
task: string,
proposedLabel: string,
ui: InteractiveUIText,
): Promise<PostSummaryAction | null> {
return selectSummaryAction(
task,
proposedLabel,
ui.actionPrompt,
buildSummaryActionOptions(
{
execute: ui.actions.execute,
createIssue: ui.actions.createIssue,
saveTask: ui.actions.saveTask,
continue: ui.actions.continue,
},
['create_issue'],
),
);
}
export type InteractiveModeAction = 'execute' | 'save_task' | 'create_issue' | 'cancel';

View File

@ -44,6 +44,12 @@ export {
instructBranch,
} from './taskActions.js';
export {
type InstructModeAction,
type InstructModeResult,
runInstructMode,
} from './instructMode.js';
/** Task action type for pending task action selection menu */
type PendingTaskAction = 'delete';

View File

@ -0,0 +1,123 @@
/**
* Instruct mode for branch-based tasks.
*
* Provides conversation loop for additional instructions on existing branches,
* similar to interactive mode but with branch context and limited actions.
*/
import {
initializeSession,
displayAndClearSessionState,
runConversationLoop,
type SessionContext,
type ConversationStrategy,
type PostSummaryAction,
} from '../../interactive/conversationLoop.js';
import {
resolveLanguage,
buildSummaryActionOptions,
selectSummaryAction,
} from '../../interactive/interactive.js';
import { loadTemplate } from '../../../shared/prompts/index.js';
import { getLabelObject } from '../../../shared/i18n/index.js';
import { loadGlobalConfig } from '../../../infra/config/index.js';
export type InstructModeAction = 'execute' | 'save_task' | 'cancel';
export interface InstructModeResult {
action: InstructModeAction;
task: string;
}
export interface InstructUIText {
intro: string;
resume: string;
noConversation: string;
summarizeFailed: string;
continuePrompt: string;
proposed: string;
actionPrompt: string;
actions: {
execute: string;
saveTask: string;
continue: string;
};
cancelled: string;
}
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,
}),
);
};
}
export async function runInstructMode(
cwd: string,
branchContext: string,
branchName: string,
): Promise<InstructModeResult> {
const globalConfig = loadGlobalConfig();
const lang = resolveLanguage(globalConfig.language);
if (!globalConfig.provider) {
throw new Error('Provider is not configured.');
}
const baseCtx = initializeSession(cwd, 'instruct');
const ctx: SessionContext = { ...baseCtx, lang, personaName: 'instruct' };
displayAndClearSessionState(cwd, ctx.lang);
const ui = getLabelObject<InstructUIText>('instruct.ui', ctx.lang);
const systemPrompt = loadTemplate('score_interactive_system_prompt', ctx.lang, {
hasPiecePreview: false,
pieceStructure: '',
movementDetails: '',
});
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, {});
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: INSTRUCT_TOOLS,
transformPrompt: injectPolicy,
introMessage,
selectAction: createSelectInstructAction(ui),
};
const result = await runConversationLoop(cwd, ctx, strategy, undefined, undefined);
if (result.action === 'cancel') {
return { action: 'cancel', task: '' };
}
return { action: result.action as InstructModeAction, task: result.task };
}

View File

@ -19,13 +19,16 @@ import {
autoCommitAndPush,
type BranchListItem,
} from '../../../infra/task/index.js';
import { selectOption, promptInput } from '../../../shared/prompt/index.js';
import { selectOption } from '../../../shared/prompt/index.js';
import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js';
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
import { executeTask } from '../execute/taskExecution.js';
import type { TaskExecutionOptions } from '../execute/types.js';
import { encodeWorktreePath } from '../../../infra/config/project/sessionStore.js';
import { runInstructMode } from './instructMode.js';
import { saveTaskFile } from '../add/index.js';
import { selectPiece } from '../../pieceSelection/index.js';
import { dispatchConversationAction } from '../../interactive/actionDispatcher.js';
const log = createLogger('list-tasks');
@ -302,8 +305,8 @@ function getBranchContext(projectDir: string, branch: string): string {
}
/**
* Instruct branch: create a temp clone, give additional instructions,
* auto-commit+push, then remove clone.
* Instruct branch: create a temp clone, give additional instructions via
* interactive conversation, then auto-commit+push or save as task file.
*/
export async function instructBranch(
projectDir: string,
@ -312,54 +315,81 @@ export async function instructBranch(
): Promise<boolean> {
const { branch } = item.info;
const instruction = await promptInput('Enter instruction');
if (!instruction) {
info('Cancelled');
return false;
}
const branchContext = getBranchContext(projectDir, branch);
const result = await runInstructMode(projectDir, branchContext, branch);
let selectedPiece: string | null = null;
const selectedPiece = await selectPiece(projectDir);
if (!selectedPiece) {
info('Cancelled');
return false;
}
log.info('Instructing branch via temp clone', { branch, piece: selectedPiece });
info(`Running instruction on ${branch}...`);
const clone = createTempCloneForBranch(projectDir, branch);
try {
const branchContext = getBranchContext(projectDir, branch);
const fullInstruction = branchContext
? `${branchContext}## 追加指示\n${instruction}`
: instruction;
const taskSuccess = await executeTask({
task: fullInstruction,
cwd: clone.path,
pieceIdentifier: selectedPiece,
projectCwd: projectDir,
agentOverrides: options,
});
if (taskSuccess) {
const commitResult = autoCommitAndPush(clone.path, item.taskSlug, projectDir);
if (commitResult.success && commitResult.commitHash) {
info(`Auto-committed & pushed: ${commitResult.commitHash}`);
} else if (!commitResult.success) {
warn(`Auto-commit skipped: ${commitResult.message}`);
}
success(`Instruction completed on ${branch}`);
log.info('Instruction completed', { branch });
} else {
logError(`Instruction failed on ${branch}`);
log.error('Instruction failed', { branch });
const ensurePieceSelected = async (): Promise<string | null> => {
if (selectedPiece) {
return selectedPiece;
}
selectedPiece = await selectPiece(projectDir);
if (!selectedPiece) {
info('Cancelled');
return null;
}
return selectedPiece;
};
return taskSuccess;
} finally {
removeClone(clone.path);
removeCloneMeta(projectDir, branch);
}
return dispatchConversationAction(result, {
cancel: () => {
info('Cancelled');
return false;
},
save_task: async ({ task }) => {
const piece = await ensurePieceSelected();
if (!piece) {
return false;
}
const created = await saveTaskFile(projectDir, task, { piece });
success(`Task saved: ${created.taskName}`);
info(` File: ${created.tasksFile}`);
log.info('Task saved from instruct mode', { branch, piece });
return true;
},
execute: async ({ task }) => {
const piece = await ensurePieceSelected();
if (!piece) {
return false;
}
log.info('Instructing branch via temp clone', { branch, piece });
info(`Running instruction on ${branch}...`);
const clone = createTempCloneForBranch(projectDir, branch);
try {
const fullInstruction = branchContext
? `${branchContext}## 追加指示\n${task}`
: task;
const taskSuccess = await executeTask({
task: fullInstruction,
cwd: clone.path,
pieceIdentifier: piece,
projectCwd: projectDir,
agentOverrides: options,
});
if (taskSuccess) {
const commitResult = autoCommitAndPush(clone.path, item.taskSlug, projectDir);
if (commitResult.success && commitResult.commitHash) {
info(`Auto-committed & pushed: ${commitResult.commitHash}`);
} else if (!commitResult.success) {
warn(`Auto-commit skipped: ${commitResult.message}`);
}
success(`Instruction completed on ${branch}`);
log.info('Instruction completed', { branch });
} else {
logError(`Instruction failed on ${branch}`);
log.error('Instruction failed', { branch });
}
return taskSuccess;
} finally {
removeClone(clone.path);
removeCloneMeta(projectDir, branch);
}
},
});
}

View File

@ -68,6 +68,22 @@ piece:
sigintTimeout: "Graceful shutdown timed out after {timeoutMs}ms"
sigintForce: "Ctrl+C: Force exit"
# ===== Instruct Mode UI (takt list -> instruct) =====
instruct:
ui:
intro: "Instruct mode - describe additional instructions. Commands: /go (summarize), /cancel (exit)"
resume: "Resuming previous session"
noConversation: "No conversation yet. Please describe your instructions first."
summarizeFailed: "Failed to summarize conversation. Please try again."
continuePrompt: "Okay, continue describing your instructions."
proposed: "Proposed additional instructions:"
actionPrompt: "What would you like to do?"
actions:
execute: "Execute now"
saveTask: "Save as Task"
continue: "Continue editing"
cancelled: "Cancelled"
run:
notifyComplete: "Run complete ({total} tasks)"
notifyAbort: "Run finished with errors ({failed})"

View File

@ -68,6 +68,22 @@ piece:
sigintTimeout: "graceful停止がタイムアウトしました ({timeoutMs}ms)"
sigintForce: "Ctrl+C: 強制終了します"
# ===== Instruct Mode UI (takt list -> instruct) =====
instruct:
ui:
intro: "指示モード - 追加指示を入力してください。コマンド: /go要約, /cancel終了"
resume: "前回のセッションを再開します"
noConversation: "まだ会話がありません。まず追加指示を入力してください。"
summarizeFailed: "会話の要約に失敗しました。再度お試しください。"
continuePrompt: "続けて追加指示を入力してください。"
proposed: "提案された追加指示:"
actionPrompt: "どうしますか?"
actions:
execute: "実行する"
saveTask: "タスクにつむ"
continue: "会話を続ける"
cancelled: "キャンセルしました"
run:
notifyComplete: "run完了 ({total} tasks)"
notifyAbort: "runはエラー終了 ({failed})"