takt: add-piece-reuse-confirm (#468)
This commit is contained in:
parent
8403a7c892
commit
3649ce40f9
@ -139,4 +139,15 @@ describe('label integrity', () => {
|
|||||||
expect(() => getLabelObject(key, 'ja')).not.toThrow();
|
expect(() => getLabelObject(key, 'ja')).not.toThrow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps only confirm-based retry piece reuse labels', () => {
|
||||||
|
expect(() => getLabel('retry.usePreviousPieceConfirm', 'en', { piece: 'default' })).not.toThrow();
|
||||||
|
expect(() => getLabel('retry.usePreviousPieceConfirm', 'ja', { piece: 'default' })).not.toThrow();
|
||||||
|
expect(() => getLabel('retry.workflowPrompt', 'en')).toThrow('Label key not found');
|
||||||
|
expect(() => getLabel('retry.usePreviousWorkflow', 'en')).toThrow('Label key not found');
|
||||||
|
expect(() => getLabel('retry.changeWorkflow', 'en')).toThrow('Label key not found');
|
||||||
|
expect(() => getLabel('retry.workflowPrompt', 'ja')).toThrow('Label key not found');
|
||||||
|
expect(() => getLabel('retry.usePreviousWorkflow', 'ja')).toThrow('Label key not found');
|
||||||
|
expect(() => getLabel('retry.changeWorkflow', 'ja')).toThrow('Label key not found');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,19 @@
|
|||||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
const { mockDebug } = vi.hoisted(() => ({
|
const {
|
||||||
|
mockDebug,
|
||||||
|
mockConfirm,
|
||||||
|
mockGetLabel,
|
||||||
|
mockSelectPiece,
|
||||||
|
mockIsPiecePath,
|
||||||
|
mockLoadAllPiecesWithSources,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
mockDebug: vi.fn(),
|
mockDebug: vi.fn(),
|
||||||
|
mockConfirm: vi.fn(),
|
||||||
|
mockGetLabel: vi.fn((_key: string, _lang?: string, vars?: Record<string, string>) => `Use previous piece "${vars?.piece ?? ''}"?`),
|
||||||
|
mockSelectPiece: vi.fn(),
|
||||||
|
mockIsPiecePath: vi.fn(() => false),
|
||||||
|
mockLoadAllPiecesWithSources: vi.fn(() => new Map<string, unknown>([['default', {}], ['selected-piece', {}]])),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
@ -15,7 +27,25 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { hasDeprecatedProviderConfig } from '../features/tasks/list/requeueHelpers.js';
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
|
confirm: (...args: unknown[]) => mockConfirm(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/i18n/index.js', () => ({
|
||||||
|
getLabel: (...args: unknown[]) => mockGetLabel(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/pieceSelection/index.js', () => ({
|
||||||
|
selectPiece: (...args: unknown[]) => mockSelectPiece(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
isPiecePath: (...args: unknown[]) => mockIsPiecePath(...args),
|
||||||
|
loadAllPiecesWithSources: (...args: unknown[]) => mockLoadAllPiecesWithSources(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { hasDeprecatedProviderConfig, selectPieceWithOptionalReuse } from '../features/tasks/list/requeueHelpers.js';
|
||||||
|
|
||||||
describe('hasDeprecatedProviderConfig', () => {
|
describe('hasDeprecatedProviderConfig', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -86,3 +116,48 @@ describe('hasDeprecatedProviderConfig', () => {
|
|||||||
expect(hasDeprecatedProviderConfig(orderContent)).toBe(false);
|
expect(hasDeprecatedProviderConfig(orderContent)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('selectPieceWithOptionalReuse', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockIsPiecePath.mockReturnValue(false);
|
||||||
|
mockLoadAllPiecesWithSources.mockReturnValue(new Map<string, unknown>([['default', {}], ['selected-piece', {}]]));
|
||||||
|
mockSelectPiece.mockResolvedValue('selected-piece');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('内部ヘルパーを公開 API に露出しない', async () => {
|
||||||
|
const requeueHelpersModule = await import('../features/tasks/list/requeueHelpers.js');
|
||||||
|
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(requeueHelpersModule, 'resolveReusablePieceName')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('前回 piece 再利用を確認して Yes ならそのまま返す', async () => {
|
||||||
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const selected = await selectPieceWithOptionalReuse('/project', 'default', 'en');
|
||||||
|
|
||||||
|
expect(selected).toBe('default');
|
||||||
|
expect(mockConfirm).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockSelectPiece).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('前回 piece 再利用を拒否した場合は piece 選択にフォールバックする', async () => {
|
||||||
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const selected = await selectPieceWithOptionalReuse('/project', 'default', 'en');
|
||||||
|
|
||||||
|
expect(selected).toBe('selected-piece');
|
||||||
|
expect(mockConfirm).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('未登録の前回 piece 名は確認せず拒否して piece 選択にフォールバックする', async () => {
|
||||||
|
mockLoadAllPiecesWithSources.mockReturnValue(new Map<string, unknown>([['default', {}]]));
|
||||||
|
|
||||||
|
const selected = await selectPieceWithOptionalReuse('/project', 'tampered-piece', 'en');
|
||||||
|
|
||||||
|
expect(selected).toBe('selected-piece');
|
||||||
|
expect(mockConfirm).not.toHaveBeenCalled();
|
||||||
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -14,8 +14,11 @@ const {
|
|||||||
mockListRecentRuns,
|
mockListRecentRuns,
|
||||||
mockSelectRun,
|
mockSelectRun,
|
||||||
mockLoadRunSessionContext,
|
mockLoadRunSessionContext,
|
||||||
|
mockFindRunForTask,
|
||||||
mockFindPreviousOrderContent,
|
mockFindPreviousOrderContent,
|
||||||
mockWarn,
|
mockWarn,
|
||||||
|
mockIsPiecePath,
|
||||||
|
mockLoadAllPiecesWithSources,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
mockExistsSync: vi.fn(() => true),
|
mockExistsSync: vi.fn(() => true),
|
||||||
mockStartReExecution: vi.fn(),
|
mockStartReExecution: vi.fn(),
|
||||||
@ -30,8 +33,14 @@ const {
|
|||||||
mockListRecentRuns: vi.fn(() => []),
|
mockListRecentRuns: vi.fn(() => []),
|
||||||
mockSelectRun: vi.fn(() => null),
|
mockSelectRun: vi.fn(() => null),
|
||||||
mockLoadRunSessionContext: vi.fn(),
|
mockLoadRunSessionContext: vi.fn(),
|
||||||
|
mockFindRunForTask: vi.fn(() => null),
|
||||||
mockFindPreviousOrderContent: vi.fn(() => null),
|
mockFindPreviousOrderContent: vi.fn(() => null),
|
||||||
mockWarn: vi.fn(),
|
mockWarn: vi.fn(),
|
||||||
|
mockIsPiecePath: vi.fn(() => false),
|
||||||
|
mockLoadAllPiecesWithSources: vi.fn(() => new Map<string, unknown>([
|
||||||
|
['default', {}],
|
||||||
|
['selected-piece', {}],
|
||||||
|
])),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('node:fs', async (importOriginal) => ({
|
vi.mock('node:fs', async (importOriginal) => ({
|
||||||
@ -59,6 +68,8 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
pieceStructure: [],
|
pieceStructure: [],
|
||||||
movementPreviews: [],
|
movementPreviews: [],
|
||||||
})),
|
})),
|
||||||
|
isPiecePath: (...args: unknown[]) => mockIsPiecePath(...args),
|
||||||
|
loadAllPiecesWithSources: (...args: unknown[]) => mockLoadAllPiecesWithSources(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/tasks/list/instructMode.js', () => ({
|
vi.mock('../features/tasks/list/instructMode.js', () => ({
|
||||||
@ -86,7 +97,7 @@ vi.mock('../features/interactive/index.js', () => ({
|
|||||||
listRecentRuns: (...args: unknown[]) => mockListRecentRuns(...args),
|
listRecentRuns: (...args: unknown[]) => mockListRecentRuns(...args),
|
||||||
selectRun: (...args: unknown[]) => mockSelectRun(...args),
|
selectRun: (...args: unknown[]) => mockSelectRun(...args),
|
||||||
loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args),
|
loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args),
|
||||||
findRunForTask: vi.fn(() => null),
|
findRunForTask: (...args: unknown[]) => mockFindRunForTask(...args),
|
||||||
findPreviousOrderContent: (...args: unknown[]) => mockFindPreviousOrderContent(...args),
|
findPreviousOrderContent: (...args: unknown[]) => mockFindPreviousOrderContent(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -123,11 +134,25 @@ describe('instructBranch direct execution flow', () => {
|
|||||||
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' }));
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
mockGetLabel.mockReturnValue("Reference a previous run's results?");
|
mockGetLabel.mockImplementation((key: string, _lang?: string, vars?: Record<string, string>) => {
|
||||||
|
if (key === 'interactive.runSelector.confirm') {
|
||||||
|
return "Reference a previous run's results?";
|
||||||
|
}
|
||||||
|
if (vars?.piece) {
|
||||||
|
return `Use previous piece "${vars.piece}"?`;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
});
|
||||||
mockResolveLanguage.mockReturnValue('en');
|
mockResolveLanguage.mockReturnValue('en');
|
||||||
mockListRecentRuns.mockReturnValue([]);
|
mockListRecentRuns.mockReturnValue([]);
|
||||||
mockSelectRun.mockResolvedValue(null);
|
mockSelectRun.mockResolvedValue(null);
|
||||||
|
mockFindRunForTask.mockReturnValue(null);
|
||||||
mockFindPreviousOrderContent.mockReturnValue(null);
|
mockFindPreviousOrderContent.mockReturnValue(null);
|
||||||
|
mockIsPiecePath.mockImplementation((piece: string) => piece.startsWith('/') || piece.startsWith('~') || piece.startsWith('./') || piece.startsWith('../') || piece.endsWith('.yaml') || piece.endsWith('.yml'));
|
||||||
|
mockLoadAllPiecesWithSources.mockReturnValue(new Map<string, unknown>([
|
||||||
|
['default', {}],
|
||||||
|
['selected-piece', {}],
|
||||||
|
]));
|
||||||
mockStartReExecution.mockReturnValue({
|
mockStartReExecution.mockReturnValue({
|
||||||
name: 'done-task',
|
name: 'done-task',
|
||||||
content: 'done',
|
content: 'done',
|
||||||
@ -185,6 +210,117 @@ describe('instructBranch direct execution flow', () => {
|
|||||||
expect(originalTaskInfo.data.piece).toBe('original-piece');
|
expect(originalTaskInfo.data.piece).toBe('original-piece');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reuse previous piece when confirmed', async () => {
|
||||||
|
mockFindRunForTask.mockReturnValue('run-previous');
|
||||||
|
mockLoadRunSessionContext.mockReturnValue({
|
||||||
|
task: 'done',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'completed',
|
||||||
|
movementLogs: [],
|
||||||
|
reports: [],
|
||||||
|
});
|
||||||
|
mockConfirm
|
||||||
|
.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
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(mockSelectPiece).not.toHaveBeenCalled();
|
||||||
|
const [message, defaultYes] = mockConfirm.mock.calls[0] ?? [];
|
||||||
|
expect(message).toEqual(expect.stringContaining('"default"'));
|
||||||
|
expect(defaultYes ?? true).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call selectPiece when previous piece reuse is declined', async () => {
|
||||||
|
mockFindRunForTask.mockReturnValue('run-previous');
|
||||||
|
mockLoadRunSessionContext.mockReturnValue({
|
||||||
|
task: 'done',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'completed',
|
||||||
|
movementLogs: [],
|
||||||
|
reports: [],
|
||||||
|
});
|
||||||
|
mockConfirm
|
||||||
|
.mockResolvedValueOnce(false);
|
||||||
|
mockSelectPiece.mockResolvedValue('selected-piece');
|
||||||
|
|
||||||
|
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(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
|
expect(mockStartReExecution).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore previous piece when run metadata contains piece path', async () => {
|
||||||
|
mockFindRunForTask.mockReturnValue('run-previous');
|
||||||
|
mockLoadRunSessionContext.mockReturnValue({
|
||||||
|
task: 'done',
|
||||||
|
piece: '../secrets.yaml',
|
||||||
|
status: 'completed',
|
||||||
|
movementLogs: [],
|
||||||
|
reports: [],
|
||||||
|
});
|
||||||
|
mockSelectPiece.mockResolvedValue('selected-piece');
|
||||||
|
|
||||||
|
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(mockConfirm).not.toHaveBeenCalled();
|
||||||
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
|
expect(mockStartReExecution).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when replacement piece selection is cancelled after declining reuse', async () => {
|
||||||
|
mockFindRunForTask.mockReturnValue('run-previous');
|
||||||
|
mockLoadRunSessionContext.mockReturnValue({
|
||||||
|
task: 'done',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'completed',
|
||||||
|
movementLogs: [],
|
||||||
|
reports: [],
|
||||||
|
});
|
||||||
|
mockConfirm.mockResolvedValueOnce(false);
|
||||||
|
mockSelectPiece.mockResolvedValue(null);
|
||||||
|
|
||||||
|
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(mockStartReExecution).not.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 () => {
|
||||||
await instructBranch('/project', {
|
await instructBranch('/project', {
|
||||||
kind: 'completed',
|
kind: 'completed',
|
||||||
|
|||||||
@ -4,20 +4,26 @@ const {
|
|||||||
mockExistsSync,
|
mockExistsSync,
|
||||||
mockSelectPiece,
|
mockSelectPiece,
|
||||||
mockSelectOptionWithDefault,
|
mockSelectOptionWithDefault,
|
||||||
|
mockConfirm,
|
||||||
mockResolvePieceConfigValue,
|
mockResolvePieceConfigValue,
|
||||||
mockLoadPieceByIdentifier,
|
mockLoadPieceByIdentifier,
|
||||||
mockGetPieceDescription,
|
mockGetPieceDescription,
|
||||||
mockRunRetryMode,
|
mockRunRetryMode,
|
||||||
mockFindRunForTask,
|
mockFindRunForTask,
|
||||||
mockFindPreviousOrderContent,
|
mockFindPreviousOrderContent,
|
||||||
|
mockLoadRunSessionContext,
|
||||||
|
mockFormatRunSessionForPrompt,
|
||||||
mockStartReExecution,
|
mockStartReExecution,
|
||||||
mockRequeueTask,
|
mockRequeueTask,
|
||||||
mockExecuteAndCompleteTask,
|
mockExecuteAndCompleteTask,
|
||||||
mockWarn,
|
mockWarn,
|
||||||
|
mockIsPiecePath,
|
||||||
|
mockLoadAllPiecesWithSources,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
mockExistsSync: vi.fn(() => true),
|
mockExistsSync: vi.fn(() => true),
|
||||||
mockSelectPiece: vi.fn(),
|
mockSelectPiece: vi.fn(),
|
||||||
mockSelectOptionWithDefault: vi.fn(),
|
mockSelectOptionWithDefault: vi.fn(),
|
||||||
|
mockConfirm: vi.fn(),
|
||||||
mockResolvePieceConfigValue: vi.fn(),
|
mockResolvePieceConfigValue: vi.fn(),
|
||||||
mockLoadPieceByIdentifier: vi.fn(),
|
mockLoadPieceByIdentifier: vi.fn(),
|
||||||
mockGetPieceDescription: vi.fn(() => ({
|
mockGetPieceDescription: vi.fn(() => ({
|
||||||
@ -29,10 +35,20 @@ const {
|
|||||||
mockRunRetryMode: vi.fn(),
|
mockRunRetryMode: vi.fn(),
|
||||||
mockFindRunForTask: vi.fn(() => null),
|
mockFindRunForTask: vi.fn(() => null),
|
||||||
mockFindPreviousOrderContent: vi.fn(() => null),
|
mockFindPreviousOrderContent: vi.fn(() => null),
|
||||||
|
mockLoadRunSessionContext: vi.fn(),
|
||||||
|
mockFormatRunSessionForPrompt: vi.fn((sessionContext?: { piece?: string }) => ({
|
||||||
|
runTask: '',
|
||||||
|
runPiece: sessionContext?.piece ?? '',
|
||||||
|
runStatus: '',
|
||||||
|
runMovementLogs: '',
|
||||||
|
runReports: '',
|
||||||
|
})),
|
||||||
mockStartReExecution: vi.fn(),
|
mockStartReExecution: vi.fn(),
|
||||||
mockRequeueTask: vi.fn(),
|
mockRequeueTask: vi.fn(),
|
||||||
mockExecuteAndCompleteTask: vi.fn(),
|
mockExecuteAndCompleteTask: vi.fn(),
|
||||||
mockWarn: vi.fn(),
|
mockWarn: vi.fn(),
|
||||||
|
mockIsPiecePath: vi.fn(() => false),
|
||||||
|
mockLoadAllPiecesWithSources: vi.fn(() => new Map<string, unknown>([['default', {}]])),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('node:fs', async (importOriginal) => ({
|
vi.mock('node:fs', async (importOriginal) => ({
|
||||||
@ -46,6 +62,7 @@ vi.mock('../features/pieceSelection/index.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../shared/prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
selectOptionWithDefault: (...args: unknown[]) => mockSelectOptionWithDefault(...args),
|
selectOptionWithDefault: (...args: unknown[]) => mockSelectOptionWithDefault(...args),
|
||||||
|
confirm: (...args: unknown[]) => mockConfirm(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
@ -68,19 +85,15 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
resolvePieceConfigValue: (...args: unknown[]) => mockResolvePieceConfigValue(...args),
|
resolvePieceConfigValue: (...args: unknown[]) => mockResolvePieceConfigValue(...args),
|
||||||
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
||||||
getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args),
|
getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args),
|
||||||
|
isPiecePath: (...args: unknown[]) => mockIsPiecePath(...args),
|
||||||
|
loadAllPiecesWithSources: (...args: unknown[]) => mockLoadAllPiecesWithSources(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/interactive/index.js', () => ({
|
vi.mock('../features/interactive/index.js', () => ({
|
||||||
findRunForTask: (...args: unknown[]) => mockFindRunForTask(...args),
|
findRunForTask: (...args: unknown[]) => mockFindRunForTask(...args),
|
||||||
loadRunSessionContext: vi.fn(),
|
loadRunSessionContext: (...args: unknown[]) => mockLoadRunSessionContext(...args),
|
||||||
getRunPaths: vi.fn(() => ({ logsDir: '/tmp/logs', reportsDir: '/tmp/reports' })),
|
getRunPaths: vi.fn(() => ({ logsDir: '/tmp/logs', reportsDir: '/tmp/reports' })),
|
||||||
formatRunSessionForPrompt: vi.fn(() => ({
|
formatRunSessionForPrompt: (...args: unknown[]) => mockFormatRunSessionForPrompt(...args),
|
||||||
runTask: '',
|
|
||||||
runPiece: 'default',
|
|
||||||
runStatus: '',
|
|
||||||
runMovementLogs: '',
|
|
||||||
runReports: '',
|
|
||||||
})),
|
|
||||||
runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args),
|
runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args),
|
||||||
findPreviousOrderContent: (...args: unknown[]) => mockFindPreviousOrderContent(...args),
|
findPreviousOrderContent: (...args: unknown[]) => mockFindPreviousOrderContent(...args),
|
||||||
}));
|
}));
|
||||||
@ -101,13 +114,11 @@ vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/i18n/index.js', () => ({
|
vi.mock('../shared/i18n/index.js', () => ({
|
||||||
getLabel: vi.fn((key: string) => {
|
getLabel: vi.fn((key: string, _lang?: string, vars?: Record<string, string>) => {
|
||||||
const labels: Record<string, string> = {
|
if (vars?.piece) {
|
||||||
'retry.workflowPrompt': 'Select workflow:',
|
return `Use previous piece "${vars.piece}"?`;
|
||||||
'retry.usePreviousWorkflow': 'Use previous',
|
}
|
||||||
'retry.changeWorkflow': 'Change workflow',
|
return key;
|
||||||
};
|
|
||||||
return labels[key] ?? key;
|
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -146,12 +157,22 @@ beforeEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockExistsSync.mockReturnValue(true);
|
mockExistsSync.mockReturnValue(true);
|
||||||
|
|
||||||
|
mockConfirm.mockResolvedValue(true);
|
||||||
mockSelectPiece.mockResolvedValue('default');
|
mockSelectPiece.mockResolvedValue('default');
|
||||||
mockResolvePieceConfigValue.mockReturnValue(3);
|
mockResolvePieceConfigValue.mockReturnValue(3);
|
||||||
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
||||||
|
mockIsPiecePath.mockImplementation((piece: string) => piece.startsWith('/') || piece.startsWith('~') || piece.startsWith('./') || piece.startsWith('../') || piece.endsWith('.yaml') || piece.endsWith('.yml'));
|
||||||
|
mockLoadAllPiecesWithSources.mockReturnValue(new Map<string, unknown>([['default', {}], ['selected-piece', {}]]));
|
||||||
mockSelectOptionWithDefault.mockResolvedValue('plan');
|
mockSelectOptionWithDefault.mockResolvedValue('plan');
|
||||||
mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
||||||
mockFindPreviousOrderContent.mockReturnValue(null);
|
mockFindPreviousOrderContent.mockReturnValue(null);
|
||||||
|
mockLoadRunSessionContext.mockReturnValue({
|
||||||
|
task: 'Do something',
|
||||||
|
piece: 'default',
|
||||||
|
status: 'failed',
|
||||||
|
movementLogs: [],
|
||||||
|
reports: [],
|
||||||
|
});
|
||||||
mockStartReExecution.mockReturnValue({
|
mockStartReExecution.mockReturnValue({
|
||||||
name: 'my-task',
|
name: 'my-task',
|
||||||
content: 'Do something',
|
content: 'Do something',
|
||||||
@ -337,29 +358,24 @@ describe('retryFailedTask', () => {
|
|||||||
expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A');
|
expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when previous workflow exists', () => {
|
describe('when previous piece exists', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockFindRunForTask.mockReturnValue('run-123');
|
mockFindRunForTask.mockReturnValue('run-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show workflow selection prompt when runInfo.piece exists', async () => {
|
it('should ask whether to reuse previous piece with default yes', async () => {
|
||||||
const task = makeFailedTask();
|
const task = makeFailedTask();
|
||||||
|
|
||||||
await retryFailedTask(task, '/project');
|
await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
expect(mockSelectOptionWithDefault).toHaveBeenCalledWith(
|
const [message, defaultYes] = mockConfirm.mock.calls[0] ?? [];
|
||||||
'Select workflow:',
|
expect(message).toEqual(expect.stringContaining('"default"'));
|
||||||
expect.arrayContaining([
|
expect(defaultYes ?? true).toBe(true);
|
||||||
expect.objectContaining({ value: 'use_previous' }),
|
|
||||||
expect.objectContaining({ value: 'change' }),
|
|
||||||
]),
|
|
||||||
'use_previous',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use previous workflow when use_previous is selected', async () => {
|
it('should use previous piece when reuse is confirmed', async () => {
|
||||||
const task = makeFailedTask();
|
const task = makeFailedTask();
|
||||||
mockSelectOptionWithDefault.mockResolvedValue('use_previous');
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
|
||||||
await retryFailedTask(task, '/project');
|
await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
@ -367,23 +383,41 @@ describe('retryFailedTask', () => {
|
|||||||
expect(mockLoadPieceByIdentifier).toHaveBeenCalledWith('default', '/project');
|
expect(mockLoadPieceByIdentifier).toHaveBeenCalledWith('default', '/project');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call selectPiece when change is selected', async () => {
|
it('should call selectPiece when reuse is declined', async () => {
|
||||||
const task = makeFailedTask();
|
const task = makeFailedTask();
|
||||||
mockSelectOptionWithDefault.mockResolvedValue('change');
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
|
||||||
await retryFailedTask(task, '/project');
|
await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when workflow selection is cancelled', async () => {
|
it('should return false when selecting replacement piece is cancelled after declining reuse', async () => {
|
||||||
const task = makeFailedTask();
|
const task = makeFailedTask();
|
||||||
mockSelectOptionWithDefault.mockResolvedValue(null);
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
mockSelectPiece.mockResolvedValue(null);
|
||||||
|
|
||||||
const result = await retryFailedTask(task, '/project');
|
const result = await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled();
|
expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore previous piece when run metadata contains piece path', async () => {
|
||||||
|
const task = makeFailedTask();
|
||||||
|
mockLoadRunSessionContext.mockReturnValue({
|
||||||
|
task: 'Do something',
|
||||||
|
piece: '../secrets.yaml',
|
||||||
|
status: 'failed',
|
||||||
|
movementLogs: [],
|
||||||
|
reports: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await retryFailedTask(task, '/project');
|
||||||
|
|
||||||
|
expect(mockConfirm).not.toHaveBeenCalled();
|
||||||
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
|
expect(mockLoadPieceByIdentifier).toHaveBeenCalledWith('default', '/project');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { confirm } from '../../../shared/prompt/index.js';
|
import { confirm } from '../../../shared/prompt/index.js';
|
||||||
import { getLabel } from '../../../shared/i18n/index.js';
|
import { getLabel } from '../../../shared/i18n/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
|
import { isPiecePath, loadAllPiecesWithSources } from '../../../infra/config/index.js';
|
||||||
|
import { selectPiece } from '../../pieceSelection/index.js';
|
||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
import {
|
import {
|
||||||
selectRun,
|
selectRun,
|
||||||
@ -24,6 +26,42 @@ export function appendRetryNote(existing: string | undefined, additional: string
|
|||||||
return `${existing}\n\n${trimmedAdditional}`;
|
return `${existing}\n\n${trimmedAdditional}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveReusablePieceName(
|
||||||
|
previousPiece: string | undefined,
|
||||||
|
projectDir: string,
|
||||||
|
): string | null {
|
||||||
|
if (!previousPiece || previousPiece.trim() === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (isPiecePath(previousPiece)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const availablePieces = loadAllPiecesWithSources(projectDir);
|
||||||
|
if (!availablePieces.has(previousPiece)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return previousPiece;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectPieceWithOptionalReuse(
|
||||||
|
projectDir: string,
|
||||||
|
previousPiece: string | undefined,
|
||||||
|
lang?: 'en' | 'ja',
|
||||||
|
): Promise<string | null> {
|
||||||
|
const reusablePiece = resolveReusablePieceName(previousPiece, projectDir);
|
||||||
|
if (reusablePiece) {
|
||||||
|
const shouldReusePreviousPiece = await confirm(
|
||||||
|
getLabel('retry.usePreviousPieceConfirm', lang, { piece: reusablePiece }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (shouldReusePreviousPiece) {
|
||||||
|
return reusablePiece;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectPiece(projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
function extractYamlCandidates(content: string): string[] {
|
function extractYamlCandidates(content: string): string[] {
|
||||||
const blockPattern = /```(?:yaml|yml)\s*\n([\s\S]*?)```/gi;
|
const blockPattern = /```(?:yaml|yml)\s*\n([\s\S]*?)```/gi;
|
||||||
const candidates: string[] = [];
|
const candidates: string[] = [];
|
||||||
|
|||||||
@ -15,15 +15,15 @@ import { resolvePieceConfigValues, getPieceDescription } from '../../../infra/co
|
|||||||
import { info, warn, error as logError } from '../../../shared/ui/index.js';
|
import { info, warn, 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 { runInstructMode } from './instructMode.js';
|
import { runInstructMode } from './instructMode.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, findRunForTask, findPreviousOrderContent } from '../../interactive/index.js';
|
import { resolveLanguage, findRunForTask, findPreviousOrderContent, loadRunSessionContext } from '../../interactive/index.js';
|
||||||
import { type BranchActionTarget, resolveTargetBranch } from './taskActionTarget.js';
|
import { type BranchActionTarget, resolveTargetBranch } from './taskActionTarget.js';
|
||||||
import {
|
import {
|
||||||
appendRetryNote,
|
appendRetryNote,
|
||||||
DEPRECATED_PROVIDER_CONFIG_WARNING,
|
DEPRECATED_PROVIDER_CONFIG_WARNING,
|
||||||
hasDeprecatedProviderConfig,
|
hasDeprecatedProviderConfig,
|
||||||
|
selectPieceWithOptionalReuse,
|
||||||
selectRunSessionContext,
|
selectRunSessionContext,
|
||||||
} from './requeueHelpers.js';
|
} from './requeueHelpers.js';
|
||||||
import { executeAndCompleteTask } from '../execute/taskExecution.js';
|
import { executeAndCompleteTask } from '../execute/taskExecution.js';
|
||||||
@ -93,13 +93,18 @@ export async function instructBranch(
|
|||||||
|
|
||||||
const branch = resolveTargetBranch(target);
|
const branch = resolveTargetBranch(target);
|
||||||
|
|
||||||
const selectedPiece = await selectPiece(projectDir);
|
const globalConfig = resolvePieceConfigValues(projectDir, ['interactivePreviewMovements', 'language']);
|
||||||
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
const matchedSlug = findRunForTask(worktreePath, target.content);
|
||||||
|
const previousRunContext = matchedSlug
|
||||||
|
? loadRunSessionContext(worktreePath, matchedSlug)
|
||||||
|
: undefined;
|
||||||
|
const selectedPiece = await selectPieceWithOptionalReuse(projectDir, previousRunContext?.piece, lang);
|
||||||
if (!selectedPiece) {
|
if (!selectedPiece) {
|
||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalConfig = resolvePieceConfigValues(projectDir, ['interactivePreviewMovements', 'language']);
|
|
||||||
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
||||||
const pieceContext: PieceContext = {
|
const pieceContext: PieceContext = {
|
||||||
name: pieceDesc.name,
|
name: pieceDesc.name,
|
||||||
@ -108,10 +113,8 @@ export async function instructBranch(
|
|||||||
movementPreviews: pieceDesc.movementPreviews,
|
movementPreviews: pieceDesc.movementPreviews,
|
||||||
};
|
};
|
||||||
|
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
|
||||||
// Runs data lives in the worktree (written during previous execution)
|
// Runs data lives in the worktree (written during previous execution)
|
||||||
const runSessionContext = await selectRunSessionContext(worktreePath, lang);
|
const runSessionContext = await selectRunSessionContext(worktreePath, lang);
|
||||||
const matchedSlug = findRunForTask(worktreePath, target.content);
|
|
||||||
const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug);
|
const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug);
|
||||||
if (hasDeprecatedProviderConfig(previousOrderContent)) {
|
if (hasDeprecatedProviderConfig(previousOrderContent)) {
|
||||||
warn(DEPRECATED_PROVIDER_CONFIG_WARNING);
|
warn(DEPRECATED_PROVIDER_CONFIG_WARNING);
|
||||||
|
|||||||
@ -9,9 +9,7 @@ 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, resolvePieceConfigValue, getPieceDescription } from '../../../infra/config/index.js';
|
import { loadPieceByIdentifier, resolvePieceConfigValue, getPieceDescription } from '../../../infra/config/index.js';
|
||||||
import { selectPiece } from '../../pieceSelection/index.js';
|
|
||||||
import { selectOptionWithDefault } from '../../../shared/prompt/index.js';
|
import { selectOptionWithDefault } from '../../../shared/prompt/index.js';
|
||||||
import { getLabel } from '../../../shared/i18n/index.js';
|
|
||||||
import { info, header, blankLine, status, warn } from '../../../shared/ui/index.js';
|
import { info, header, blankLine, status, warn } from '../../../shared/ui/index.js';
|
||||||
import { createLogger } 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';
|
||||||
@ -31,6 +29,7 @@ import {
|
|||||||
appendRetryNote,
|
appendRetryNote,
|
||||||
DEPRECATED_PROVIDER_CONFIG_WARNING,
|
DEPRECATED_PROVIDER_CONFIG_WARNING,
|
||||||
hasDeprecatedProviderConfig,
|
hasDeprecatedProviderConfig,
|
||||||
|
selectPieceWithOptionalReuse,
|
||||||
} from './requeueHelpers.js';
|
} from './requeueHelpers.js';
|
||||||
import { prepareTaskForExecution } from './prepareTaskForExecution.js';
|
import { prepareTaskForExecution } from './prepareTaskForExecution.js';
|
||||||
|
|
||||||
@ -137,43 +136,12 @@ export async function retryFailedTask(
|
|||||||
const matchedSlug = findRunForTask(worktreePath, task.content);
|
const matchedSlug = findRunForTask(worktreePath, task.content);
|
||||||
const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null;
|
const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null;
|
||||||
|
|
||||||
let selectedPiece: string;
|
const selectedPiece = await selectPieceWithOptionalReuse(projectDir, runInfo?.piece);
|
||||||
if (runInfo?.piece) {
|
if (!selectedPiece) {
|
||||||
const usePreviousLabel = getLabel('retry.usePreviousWorkflow');
|
|
||||||
const changeWorkflowLabel = getLabel('retry.changeWorkflow');
|
|
||||||
const choice = await selectOptionWithDefault(
|
|
||||||
getLabel('retry.workflowPrompt'),
|
|
||||||
[
|
|
||||||
{ label: `${runInfo.piece} - ${usePreviousLabel}`, value: 'use_previous' },
|
|
||||||
{ label: changeWorkflowLabel, value: 'change' },
|
|
||||||
],
|
|
||||||
'use_previous',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (choice === null) {
|
|
||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (choice === 'use_previous') {
|
|
||||||
selectedPiece = runInfo.piece;
|
|
||||||
} else {
|
|
||||||
const selected = await selectPiece(projectDir);
|
|
||||||
if (!selected) {
|
|
||||||
info('Cancelled');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
selectedPiece = selected;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const selected = await selectPiece(projectDir);
|
|
||||||
if (!selected) {
|
|
||||||
info('Cancelled');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
selectedPiece = selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previewCount = resolvePieceConfigValue(projectDir, 'interactivePreviewMovements');
|
const previewCount = resolvePieceConfigValue(projectDir, 'interactivePreviewMovements');
|
||||||
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
||||||
|
|
||||||
|
|||||||
@ -99,9 +99,7 @@ instruct:
|
|||||||
retry:
|
retry:
|
||||||
ui:
|
ui:
|
||||||
intro: "Retry mode - describe additional instructions. Commands: /go (create instruction & run), /retry (rerun previous order), /cancel (exit)"
|
intro: "Retry mode - describe additional instructions. Commands: /go (create instruction & run), /retry (rerun previous order), /cancel (exit)"
|
||||||
workflowPrompt: "Select workflow:"
|
usePreviousPieceConfirm: "Use previous piece \"{piece}\"?"
|
||||||
usePreviousWorkflow: "Use previous"
|
|
||||||
changeWorkflow: "Change workflow"
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
notifyComplete: "Run complete ({total} tasks)"
|
notifyComplete: "Run complete ({total} tasks)"
|
||||||
|
|||||||
@ -99,9 +99,7 @@ instruct:
|
|||||||
retry:
|
retry:
|
||||||
ui:
|
ui:
|
||||||
intro: "リトライモード - 追加指示を入力してください。コマンド: /go(指示書作成・実行), /retry(前回の指示書で再実行), /cancel(終了)"
|
intro: "リトライモード - 追加指示を入力してください。コマンド: /go(指示書作成・実行), /retry(前回の指示書で再実行), /cancel(終了)"
|
||||||
workflowPrompt: "ワークフローを選択:"
|
usePreviousPieceConfirm: "前回のピース \"{piece}\" を使用しますか?"
|
||||||
usePreviousWorkflow: "前回のまま使用"
|
|
||||||
changeWorkflow: "ワークフローを変更"
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
notifyComplete: "run完了 ({total} tasks)"
|
notifyComplete: "run完了 ({total} tasks)"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user