From 67ae3e8ae525dcf7a978d118a3ca4c8a092cf373 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:22:49 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20piece=E8=A8=AD=E5=AE=9A=E8=A7=A3?= =?UTF-8?q?=E6=B1=BA=E3=81=A8config=E5=84=AA=E5=85=88=E9=A0=86=E4=BD=8D?= =?UTF-8?q?=E3=81=AE=E5=8F=82=E7=85=A7=E7=B5=8C=E8=B7=AF=E3=82=92=E7=B5=B1?= =?UTF-8?q?=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/cli-routing-issue-resolve.test.ts | 1 + src/__tests__/piece-selection.test.ts | 14 +++++++------- .../pieceExecution-debug-prompts.test.ts | 12 ++++++++---- .../pieceExecution-session-loading.test.ts | 10 +++++++++- src/__tests__/selectAndExecute-autoPr.test.ts | 12 +++++------- src/__tests__/switchPiece.test.ts | 7 ++++--- src/__tests__/taskExecution.test.ts | 8 ++++---- src/__tests__/taskInstructionActions.test.ts | 2 +- src/__tests__/taskRetryActions.test.ts | 8 ++++---- src/__tests__/watchTasks.test.ts | 8 ++++---- src/app/cli/commands.ts | 4 ++-- src/app/cli/routing.ts | 13 ++++++++----- src/features/catalog/catalogFacets.ts | 4 ++-- src/features/config/switchPiece.ts | 4 ++-- src/features/pieceSelection/index.ts | 4 ++-- src/features/prompt/preview.ts | 6 +++--- src/features/tasks/execute/pieceExecution.ts | 6 +++--- src/features/tasks/execute/postExecution.ts | 5 ++--- src/features/tasks/execute/resolveTask.ts | 4 ++-- src/features/tasks/execute/session.ts | 4 ++-- src/features/tasks/execute/taskExecution.ts | 6 +++--- src/features/tasks/list/instructMode.ts | 4 ++-- .../tasks/list/taskInstructionActions.ts | 4 ++-- src/features/tasks/list/taskRetryActions.ts | 4 ++-- src/features/tasks/watch/index.ts | 5 ++--- src/infra/config/global/pieceCategories.ts | 4 ++-- src/infra/config/index.ts | 1 + src/infra/config/loadConfig.ts | 4 +++- src/infra/config/loaders/pieceCategories.ts | 8 ++++---- src/infra/config/loaders/pieceParser.ts | 4 ++-- src/infra/config/loaders/pieceResolver.ts | 8 ++++---- src/infra/config/resolvePieceConfigValue.ts | 17 +++++++++++++++++ src/infra/config/types.ts | 6 +++++- 33 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 src/infra/config/resolvePieceConfigValue.ts diff --git a/src/__tests__/cli-routing-issue-resolve.test.ts b/src/__tests__/cli-routing-issue-resolve.test.ts index 483b5c3..9e3875c 100644 --- a/src/__tests__/cli-routing-issue-resolve.test.ts +++ b/src/__tests__/cli-routing-issue-resolve.test.ts @@ -76,6 +76,7 @@ vi.mock('../infra/task/index.js', () => ({ vi.mock('../infra/config/index.js', () => ({ getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })), + resolveConfigValue: vi.fn((_: string, key: string) => (key === 'piece' ? 'default' : false)), resolveConfigValues: vi.fn(() => ({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' })), })); diff --git a/src/__tests__/piece-selection.test.ts b/src/__tests__/piece-selection.test.ts index 94ab056..feab25c 100644 --- a/src/__tests__/piece-selection.test.ts +++ b/src/__tests__/piece-selection.test.ts @@ -39,7 +39,7 @@ const configMock = vi.hoisted(() => ({ loadAllPiecesWithSources: vi.fn(), getPieceCategories: vi.fn(), buildCategorizedPieces: vi.fn(), - getCurrentPiece: vi.fn(), + resolveConfigValue: vi.fn(), findPieceCategories: vi.fn(() => []), })); @@ -258,13 +258,13 @@ describe('selectPiece', () => { configMock.loadAllPiecesWithSources.mockReset(); configMock.getPieceCategories.mockReset(); configMock.buildCategorizedPieces.mockReset(); - configMock.getCurrentPiece.mockReset(); + configMock.resolveConfigValue.mockReset(); }); it('should return default piece when no pieces found and fallbackToDefault is true', async () => { configMock.getPieceCategories.mockReturnValue(null); configMock.listPieces.mockReturnValue([]); - configMock.getCurrentPiece.mockReturnValue('default'); + configMock.resolveConfigValue.mockReturnValue('default'); const result = await selectPiece('/cwd'); @@ -274,7 +274,7 @@ describe('selectPiece', () => { it('should return null when no pieces found and fallbackToDefault is false', async () => { configMock.getPieceCategories.mockReturnValue(null); configMock.listPieces.mockReturnValue([]); - configMock.getCurrentPiece.mockReturnValue('default'); + configMock.resolveConfigValue.mockReturnValue('default'); const result = await selectPiece('/cwd', { fallbackToDefault: false }); @@ -287,7 +287,7 @@ describe('selectPiece', () => { configMock.listPieceEntries.mockReturnValue([ { name: 'only-piece', path: '/tmp/only-piece.yaml', source: 'user' }, ]); - configMock.getCurrentPiece.mockReturnValue('only-piece'); + configMock.resolveConfigValue.mockReturnValue('only-piece'); selectOptionMock.mockResolvedValueOnce('only-piece'); const result = await selectPiece('/cwd'); @@ -307,7 +307,7 @@ describe('selectPiece', () => { configMock.getPieceCategories.mockReturnValue({ categories: ['Dev'] }); configMock.loadAllPiecesWithSources.mockReturnValue(pieceMap); configMock.buildCategorizedPieces.mockReturnValue(categorized); - configMock.getCurrentPiece.mockReturnValue('my-piece'); + configMock.resolveConfigValue.mockReturnValue('my-piece'); selectOptionMock.mockResolvedValueOnce('__current__'); @@ -321,7 +321,7 @@ describe('selectPiece', () => { configMock.getPieceCategories.mockReturnValue(null); configMock.listPieces.mockReturnValue(['piece-a', 'piece-b']); configMock.listPieceEntries.mockReturnValue(entries); - configMock.getCurrentPiece.mockReturnValue('piece-a'); + configMock.resolveConfigValue.mockReturnValue('piece-a'); selectOptionMock .mockResolvedValueOnce('custom') diff --git a/src/__tests__/pieceExecution-debug-prompts.test.ts b/src/__tests__/pieceExecution-debug-prompts.test.ts index d6ab8e5..93b9119 100644 --- a/src/__tests__/pieceExecution-debug-prompts.test.ts +++ b/src/__tests__/pieceExecution-debug-prompts.test.ts @@ -90,10 +90,14 @@ vi.mock('../infra/config/index.js', () => ({ updatePersonaSession: vi.fn(), loadWorktreeSessions: vi.fn().mockReturnValue({}), updateWorktreeSession: vi.fn(), - loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }), - loadConfig: vi.fn().mockReturnValue({ - global: { provider: 'claude' }, - project: {}, + resolvePieceConfigValues: vi.fn().mockReturnValue({ + notificationSound: true, + notificationSoundEvents: {}, + provider: 'claude', + runtime: undefined, + preventSleep: false, + model: undefined, + observability: undefined, }), saveSessionState: vi.fn(), ensureDir: vi.fn(), diff --git a/src/__tests__/pieceExecution-session-loading.test.ts b/src/__tests__/pieceExecution-session-loading.test.ts index e6402da..daae53d 100644 --- a/src/__tests__/pieceExecution-session-loading.test.ts +++ b/src/__tests__/pieceExecution-session-loading.test.ts @@ -59,7 +59,15 @@ vi.mock('../infra/config/index.js', () => ({ updatePersonaSession: vi.fn(), loadWorktreeSessions: mockLoadWorktreeSessions, updateWorktreeSession: vi.fn(), - loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }), + resolvePieceConfigValues: vi.fn().mockReturnValue({ + notificationSound: true, + notificationSoundEvents: {}, + provider: 'claude', + runtime: undefined, + preventSleep: false, + model: undefined, + observability: undefined, + }), saveSessionState: vi.fn(), ensureDir: vi.fn(), writeFileAtomic: vi.fn(), diff --git a/src/__tests__/selectAndExecute-autoPr.test.ts b/src/__tests__/selectAndExecute-autoPr.test.ts index 470dff2..3c9157d 100644 --- a/src/__tests__/selectAndExecute-autoPr.test.ts +++ b/src/__tests__/selectAndExecute-autoPr.test.ts @@ -9,6 +9,7 @@ const { mockCompleteTask, mockFailTask, mockExecuteTask, + mockResolvePieceConfigValue, } = vi.hoisted(() => ({ mockAddTask: vi.fn(() => ({ name: 'test-task', @@ -21,6 +22,7 @@ const { mockCompleteTask: vi.fn(), mockFailTask: vi.fn(), mockExecuteTask: vi.fn(), + mockResolvePieceConfigValue: vi.fn((_: string, key: string) => (key === 'autoPr' ? undefined : 'default')), })); vi.mock('../shared/prompt/index.js', () => ({ @@ -28,11 +30,10 @@ vi.mock('../shared/prompt/index.js', () => ({ })); vi.mock('../infra/config/index.js', () => ({ - getCurrentPiece: vi.fn(), + resolvePieceConfigValue: (...args: unknown[]) => mockResolvePieceConfigValue(...args), listPieces: vi.fn(() => ['default']), listPieceEntries: vi.fn(() => []), isPiecePath: vi.fn(() => false), - loadConfig: vi.fn(() => ({ global: {}, project: {} })), })); vi.mock('../infra/task/index.js', () => ({ @@ -102,7 +103,7 @@ beforeEach(() => { describe('resolveAutoPr default in selectAndExecuteTask', () => { it('should call auto-PR confirm with default true when no CLI option or config', async () => { - // Given: worktree is enabled via override, no autoPr option, no global config autoPr + // Given: worktree is enabled via override, no autoPr option, no config autoPr mockConfirm.mockResolvedValue(true); mockSummarizeTaskName.mockResolvedValue('test-task'); mockCreateSharedClone.mockReturnValue({ @@ -121,10 +122,7 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => { createWorktree: true, }); - // Then: the 'Create pull request?' confirm is called with default true - const autoPrCall = mockConfirm.mock.calls.find( - (call) => call[0] === 'Create pull request?', - ); + const autoPrCall = mockConfirm.mock.calls.find((call) => call[0] === 'Create pull request?'); expect(autoPrCall).toBeDefined(); expect(autoPrCall![1]).toBe(true); }); diff --git a/src/__tests__/switchPiece.test.ts b/src/__tests__/switchPiece.test.ts index 4f1857c..266c936 100644 --- a/src/__tests__/switchPiece.test.ts +++ b/src/__tests__/switchPiece.test.ts @@ -6,7 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../infra/config/index.js', () => ({ loadPiece: vi.fn(() => null), - getCurrentPiece: vi.fn(() => 'default'), + resolveConfigValue: vi.fn(() => 'default'), setCurrentPiece: vi.fn(), })); @@ -20,11 +20,11 @@ vi.mock('../shared/ui/index.js', () => ({ error: vi.fn(), })); -import { getCurrentPiece, loadPiece, setCurrentPiece } from '../infra/config/index.js'; +import { resolveConfigValue, loadPiece, setCurrentPiece } from '../infra/config/index.js'; import { selectPiece } from '../features/pieceSelection/index.js'; import { switchPiece } from '../features/config/switchPiece.js'; -const mockGetCurrentPiece = vi.mocked(getCurrentPiece); +const mockResolveConfigValue = vi.mocked(resolveConfigValue); const mockLoadPiece = vi.mocked(loadPiece); const mockSetCurrentPiece = vi.mocked(setCurrentPiece); const mockSelectPiece = vi.mocked(selectPiece); @@ -32,6 +32,7 @@ const mockSelectPiece = vi.mocked(selectPiece); describe('switchPiece', () => { beforeEach(() => { vi.clearAllMocks(); + mockResolveConfigValue.mockReturnValue('default'); }); it('should call selectPiece with fallbackToDefault: false', async () => { diff --git a/src/__tests__/taskExecution.test.ts b/src/__tests__/taskExecution.test.ts index 349fd9b..7e17aeb 100644 --- a/src/__tests__/taskExecution.test.ts +++ b/src/__tests__/taskExecution.test.ts @@ -5,12 +5,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { TaskInfo } from '../infra/task/index.js'; -const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockResolveConfigValues, mockBuildTaskResult, mockPersistTaskResult, mockPersistTaskError, mockPostExecutionFlow } = +const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockResolvePieceConfigValues, mockBuildTaskResult, mockPersistTaskResult, mockPersistTaskError, mockPostExecutionFlow } = vi.hoisted(() => ({ mockResolveTaskExecution: vi.fn(), mockExecutePiece: vi.fn(), mockLoadPieceByIdentifier: vi.fn(), - mockResolveConfigValues: vi.fn(), + mockResolvePieceConfigValues: vi.fn(), mockBuildTaskResult: vi.fn(), mockPersistTaskResult: vi.fn(), mockPersistTaskError: vi.fn(), @@ -38,7 +38,7 @@ vi.mock('../features/tasks/execute/postExecution.js', () => ({ vi.mock('../infra/config/index.js', () => ({ loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args), isPiecePath: () => false, - resolveConfigValues: (...args: unknown[]) => mockResolveConfigValues(...args), + resolvePieceConfigValues: (...args: unknown[]) => mockResolvePieceConfigValues(...args), })); vi.mock('../shared/ui/index.js', () => ({ @@ -83,7 +83,7 @@ describe('executeAndCompleteTask', () => { name: 'default', movements: [], }); - mockResolveConfigValues.mockReturnValue({ + mockResolvePieceConfigValues.mockReturnValue({ language: 'en', provider: 'claude', model: undefined, diff --git a/src/__tests__/taskInstructionActions.test.ts b/src/__tests__/taskInstructionActions.test.ts index 663cd7b..d1840be 100644 --- a/src/__tests__/taskInstructionActions.test.ts +++ b/src/__tests__/taskInstructionActions.test.ts @@ -48,7 +48,7 @@ vi.mock('../infra/task/index.js', () => ({ })); vi.mock('../infra/config/index.js', () => ({ - resolveConfigValues: vi.fn(() => ({ interactivePreviewMovements: 3, language: 'en' })), + resolvePieceConfigValues: vi.fn(() => ({ interactivePreviewMovements: 3, language: 'en' })), getPieceDescription: vi.fn(() => ({ name: 'default', description: 'desc', diff --git a/src/__tests__/taskRetryActions.test.ts b/src/__tests__/taskRetryActions.test.ts index 46dd579..bdd6e9d 100644 --- a/src/__tests__/taskRetryActions.test.ts +++ b/src/__tests__/taskRetryActions.test.ts @@ -4,7 +4,7 @@ const { mockExistsSync, mockSelectPiece, mockSelectOption, - mockResolveConfigValue, + mockResolvePieceConfigValue, mockLoadPieceByIdentifier, mockGetPieceDescription, mockRunRetryMode, @@ -16,7 +16,7 @@ const { mockExistsSync: vi.fn(() => true), mockSelectPiece: vi.fn(), mockSelectOption: vi.fn(), - mockResolveConfigValue: vi.fn(), + mockResolvePieceConfigValue: vi.fn(), mockLoadPieceByIdentifier: vi.fn(), mockGetPieceDescription: vi.fn(() => ({ name: 'default', @@ -60,7 +60,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ })); vi.mock('../infra/config/index.js', () => ({ - resolveConfigValue: (...args: unknown[]) => mockResolveConfigValue(...args), + resolvePieceConfigValue: (...args: unknown[]) => mockResolvePieceConfigValue(...args), loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args), getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args), })); @@ -126,7 +126,7 @@ beforeEach(() => { mockExistsSync.mockReturnValue(true); mockSelectPiece.mockResolvedValue('default'); - mockResolveConfigValue.mockReturnValue(3); + mockResolvePieceConfigValue.mockReturnValue(3); mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig); mockSelectOption.mockResolvedValue('plan'); mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' }); diff --git a/src/__tests__/watchTasks.test.ts b/src/__tests__/watchTasks.test.ts index 81991ee..48a74d3 100644 --- a/src/__tests__/watchTasks.test.ts +++ b/src/__tests__/watchTasks.test.ts @@ -14,7 +14,7 @@ const { mockSuccess, mockWarn, mockError, - mockGetCurrentPiece, + mockResolveConfigValue, } = vi.hoisted(() => ({ mockRecoverInterruptedRunningTasks: vi.fn(), mockGetTasksDir: vi.fn(), @@ -28,7 +28,7 @@ const { mockSuccess: vi.fn(), mockWarn: vi.fn(), mockError: vi.fn(), - mockGetCurrentPiece: vi.fn(), + mockResolveConfigValue: vi.fn(), })); vi.mock('../infra/task/index.js', () => ({ @@ -61,7 +61,7 @@ vi.mock('../shared/i18n/index.js', () => ({ })); vi.mock('../infra/config/index.js', () => ({ - getCurrentPiece: mockGetCurrentPiece, + resolveConfigValue: mockResolveConfigValue, })); import { watchTasks } from '../features/tasks/watch/index.js'; @@ -69,7 +69,7 @@ import { watchTasks } from '../features/tasks/watch/index.js'; describe('watchTasks', () => { beforeEach(() => { vi.clearAllMocks(); - mockGetCurrentPiece.mockReturnValue('default'); + mockResolveConfigValue.mockReturnValue('default'); mockRecoverInterruptedRunningTasks.mockReturnValue(0); mockGetTasksDir.mockReturnValue('/project/.takt/tasks.yaml'); mockExecuteAndCompleteTask.mockResolvedValue(true); diff --git a/src/app/cli/commands.ts b/src/app/cli/commands.ts index bb1e9a3..428e138 100644 --- a/src/app/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -4,7 +4,7 @@ * Registers all named subcommands (run, watch, add, list, switch, clear, eject, prompt, catalog). */ -import { clearPersonaSessions, getCurrentPiece } from '../../infra/config/index.js'; +import { clearPersonaSessions, resolveConfigValue } from '../../infra/config/index.js'; import { success } from '../../shared/ui/index.js'; import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js'; import { switchPiece, ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES, resetCategoriesToDefault, deploySkill } from '../../features/config/index.js'; @@ -17,7 +17,7 @@ program .command('run') .description('Run all pending tasks from .takt/tasks.yaml') .action(async () => { - const piece = getCurrentPiece(resolvedCwd); + const piece = resolveConfigValue(resolvedCwd, 'piece'); await runAllTasks(resolvedCwd, piece, resolveAgentOverrides(program)); }); diff --git a/src/app/cli/routing.ts b/src/app/cli/routing.ts index f6fbbd4..5b7c5e9 100644 --- a/src/app/cli/routing.ts +++ b/src/app/cli/routing.ts @@ -23,8 +23,7 @@ import { dispatchConversationAction, type InteractiveModeResult, } from '../../features/interactive/index.js'; -import { getPieceDescription, resolveConfigValues } from '../../infra/config/index.js'; -import { DEFAULT_PIECE_NAME } from '../../shared/constants.js'; +import { getPieceDescription, resolveConfigValue, resolveConfigValues } from '../../infra/config/index.js'; import { program, resolvedCwd, pipelineMode } from './program.js'; import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js'; import { loadTaskHistory } from './taskHistory.js'; @@ -85,8 +84,12 @@ export async function executeDefaultAction(task?: string): Promise { const opts = program.opts(); const agentOverrides = resolveAgentOverrides(program); const createWorktreeOverride = parseCreateWorktreeOption(opts.createWorktree as string | undefined); + const resolvedPipelinePiece = (opts.piece as string | undefined) ?? resolveConfigValue(resolvedCwd, 'piece'); + const resolvedPipelineAutoPr = opts.autoPr === true + ? true + : (resolveConfigValue(resolvedCwd, 'autoPr') ?? false); const selectOptions: SelectAndExecuteOptions = { - autoPr: opts.autoPr === true, + autoPr: opts.autoPr === true ? true : undefined, repo: opts.repo as string | undefined, piece: opts.piece as string | undefined, createWorktree: createWorktreeOverride, @@ -97,9 +100,9 @@ export async function executeDefaultAction(task?: string): Promise { const exitCode = await executePipeline({ issueNumber: opts.issue as number | undefined, task: opts.task as string | undefined, - piece: (opts.piece as string | undefined) ?? DEFAULT_PIECE_NAME, + piece: resolvedPipelinePiece, branch: opts.branch as string | undefined, - autoPr: opts.autoPr === true, + autoPr: resolvedPipelineAutoPr, repo: opts.repo as string | undefined, skipGit: opts.skipGit === true, cwd: resolvedCwd, diff --git a/src/features/catalog/catalogFacets.ts b/src/features/catalog/catalogFacets.ts index 5f781c5..88160c3 100644 --- a/src/features/catalog/catalogFacets.ts +++ b/src/features/catalog/catalogFacets.ts @@ -11,7 +11,7 @@ import chalk from 'chalk'; import type { PieceSource } from '../../infra/config/loaders/pieceResolver.js'; import { getLanguageResourcesDir } from '../../infra/resources/index.js'; import { getGlobalConfigDir, getProjectConfigDir } from '../../infra/config/paths.js'; -import { resolveConfigValues } from '../../infra/config/index.js'; +import { resolvePieceConfigValues } from '../../infra/config/index.js'; import { section, error as logError, info } from '../../shared/ui/index.js'; const FACET_TYPES = [ @@ -62,7 +62,7 @@ function getFacetDirs( facetType: FacetType, cwd: string, ): { dir: string; source: PieceSource }[] { - const config = resolveConfigValues(cwd, ['enableBuiltinPieces', 'language']); + const config = resolvePieceConfigValues(cwd, ['enableBuiltinPieces', 'language']); const dirs: { dir: string; source: PieceSource }[] = []; if (config.enableBuiltinPieces !== false) { diff --git a/src/features/config/switchPiece.ts b/src/features/config/switchPiece.ts index d2b26c1..59d0fe5 100644 --- a/src/features/config/switchPiece.ts +++ b/src/features/config/switchPiece.ts @@ -4,7 +4,7 @@ import { loadPiece, - getCurrentPiece, + resolveConfigValue, setCurrentPiece, } from '../../infra/config/index.js'; import { info, success, error } from '../../shared/ui/index.js'; @@ -16,7 +16,7 @@ import { selectPiece } from '../pieceSelection/index.js'; */ export async function switchPiece(cwd: string, pieceName?: string): Promise { if (!pieceName) { - const current = getCurrentPiece(cwd); + const current = resolveConfigValue(cwd, 'piece'); info(`Current piece: ${current}`); const selected = await selectPiece(cwd, { fallbackToDefault: false }); diff --git a/src/features/pieceSelection/index.ts b/src/features/pieceSelection/index.ts index f90f85c..7aefa74 100644 --- a/src/features/pieceSelection/index.ts +++ b/src/features/pieceSelection/index.ts @@ -17,7 +17,7 @@ import { loadAllPiecesWithSources, getPieceCategories, buildCategorizedPieces, - getCurrentPiece, + resolveConfigValue, type PieceDirEntry, type PieceCategoryNode, type CategorizedPieces, @@ -522,7 +522,7 @@ export async function selectPiece( ): Promise { const fallbackToDefault = options?.fallbackToDefault !== false; const categoryConfig = getPieceCategories(cwd); - const currentPiece = getCurrentPiece(cwd); + const currentPiece = resolveConfigValue(cwd, 'piece'); if (categoryConfig) { const allPieces = loadAllPiecesWithSources(cwd); diff --git a/src/features/prompt/preview.ts b/src/features/prompt/preview.ts index 9b540ba..a4faa7b 100644 --- a/src/features/prompt/preview.ts +++ b/src/features/prompt/preview.ts @@ -5,7 +5,7 @@ * Useful for debugging and understanding what prompts agents will receive. */ -import { loadPieceByIdentifier, getCurrentPiece, resolveConfigValue } from '../../infra/config/index.js'; +import { loadPieceByIdentifier, resolvePieceConfigValue } from '../../infra/config/index.js'; import { InstructionBuilder } from '../../core/piece/instruction/InstructionBuilder.js'; import { ReportInstructionBuilder } from '../../core/piece/instruction/ReportInstructionBuilder.js'; import { StatusJudgmentBuilder } from '../../core/piece/instruction/StatusJudgmentBuilder.js'; @@ -21,7 +21,7 @@ import { header, info, error, blankLine } from '../../shared/ui/index.js'; * the Phase 1, Phase 2, and Phase 3 prompts with sample variable values. */ export async function previewPrompts(cwd: string, pieceIdentifier?: string): Promise { - const identifier = pieceIdentifier ?? getCurrentPiece(cwd); + const identifier = pieceIdentifier ?? resolvePieceConfigValue(cwd, 'piece'); const config = loadPieceByIdentifier(identifier, cwd); if (!config) { @@ -29,7 +29,7 @@ export async function previewPrompts(cwd: string, pieceIdentifier?: string): Pro return; } - const language = resolveConfigValue(cwd, 'language') as Language; + const language = resolvePieceConfigValue(cwd, 'language') as Language; header(`Prompt Preview: ${config.name}`); info(`Movements: ${config.movements.length}`); diff --git a/src/features/tasks/execute/pieceExecution.ts b/src/features/tasks/execute/pieceExecution.ts index bacbc07..c043c70 100644 --- a/src/features/tasks/execute/pieceExecution.ts +++ b/src/features/tasks/execute/pieceExecution.ts @@ -17,7 +17,7 @@ import { updatePersonaSession, loadWorktreeSessions, updateWorktreeSession, - resolveConfigValues, + resolvePieceConfigValues, saveSessionState, type SessionState, } from '../../../infra/config/index.js'; @@ -317,7 +317,7 @@ export async function executePiece( // Load saved agent sessions only on retry; normal runs start with empty sessions const isWorktree = cwd !== projectCwd; - const globalConfig = resolveConfigValues( + const globalConfig = resolvePieceConfigValues( projectCwd, ['notificationSound', 'notificationSoundEvents', 'provider', 'runtime', 'preventSleep', 'model', 'observability'], ); @@ -326,7 +326,7 @@ export async function executePiece( const shouldNotifyIterationLimit = shouldNotify && notificationSoundEvents?.iterationLimit !== false; const shouldNotifyPieceComplete = shouldNotify && notificationSoundEvents?.pieceComplete !== false; const shouldNotifyPieceAbort = shouldNotify && notificationSoundEvents?.pieceAbort !== false; - const currentProvider = globalConfig.provider ?? 'claude'; + const currentProvider = globalConfig.provider; const effectivePieceConfig: PieceConfig = { ...pieceConfig, runtime: resolveRuntimeConfig(globalConfig.runtime, pieceConfig.runtime), diff --git a/src/features/tasks/execute/postExecution.ts b/src/features/tasks/execute/postExecution.ts index b29d18d..f20ae61 100644 --- a/src/features/tasks/execute/postExecution.ts +++ b/src/features/tasks/execute/postExecution.ts @@ -5,7 +5,7 @@ * instructBranch (instruct mode from takt list). */ -import { resolveConfigValue } from '../../../infra/config/index.js'; +import { resolvePieceConfigValue } from '../../../infra/config/index.js'; import { confirm } from '../../../shared/prompt/index.js'; import { autoCommitAndPush } from '../../../infra/task/index.js'; import { info, error, success } from '../../../shared/ui/index.js'; @@ -23,11 +23,10 @@ export async function resolveAutoPr(optionAutoPr: boolean | undefined, cwd: stri return optionAutoPr; } - const autoPr = resolveConfigValue(cwd, 'autoPr'); + const autoPr = resolvePieceConfigValue(cwd, 'autoPr'); if (typeof autoPr === 'boolean') { return autoPr; } - return confirm('Create pull request?', true); } diff --git a/src/features/tasks/execute/resolveTask.ts b/src/features/tasks/execute/resolveTask.ts index 799b7ce..4367a6f 100644 --- a/src/features/tasks/execute/resolveTask.ts +++ b/src/features/tasks/execute/resolveTask.ts @@ -4,7 +4,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { resolveConfigValue } from '../../../infra/config/index.js'; +import { resolvePieceConfigValue } from '../../../infra/config/index.js'; import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js'; import { withProgress } from '../../../shared/ui/index.js'; import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js'; @@ -141,7 +141,7 @@ export async function resolveTaskExecution( if (data.auto_pr !== undefined) { autoPr = data.auto_pr; } else { - autoPr = resolveConfigValue(defaultCwd, 'autoPr') ?? false; + autoPr = resolvePieceConfigValue(defaultCwd, 'autoPr') ?? false; } return { diff --git a/src/features/tasks/execute/session.ts b/src/features/tasks/execute/session.ts index d762978..62547ff 100644 --- a/src/features/tasks/execute/session.ts +++ b/src/features/tasks/execute/session.ts @@ -2,7 +2,7 @@ * Session management helpers for agent execution */ -import { loadPersonaSessions, updatePersonaSession, resolveConfigValue } from '../../../infra/config/index.js'; +import { loadPersonaSessions, updatePersonaSession, resolvePieceConfigValue } from '../../../infra/config/index.js'; import type { AgentResponse } from '../../../core/models/index.js'; /** @@ -15,7 +15,7 @@ export async function withPersonaSession( fn: (sessionId?: string) => Promise, provider?: string ): Promise { - const resolvedProvider = provider ?? resolveConfigValue(cwd, 'provider') ?? 'claude'; + const resolvedProvider = provider ?? resolvePieceConfigValue(cwd, 'provider'); const sessions = loadPersonaSessions(cwd, resolvedProvider); const sessionId = sessions[personaName]; diff --git a/src/features/tasks/execute/taskExecution.ts b/src/features/tasks/execute/taskExecution.ts index 79ba68f..98c7af3 100644 --- a/src/features/tasks/execute/taskExecution.ts +++ b/src/features/tasks/execute/taskExecution.ts @@ -2,7 +2,7 @@ * Task execution logic */ -import { loadPieceByIdentifier, isPiecePath, resolveConfigValues } from '../../../infra/config/index.js'; +import { loadPieceByIdentifier, isPiecePath, resolvePieceConfigValues } from '../../../infra/config/index.js'; import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js'; import { header, @@ -86,7 +86,7 @@ async function executeTaskWithResult(options: ExecuteTaskOptions): Promise s.name), }); - const config = resolveConfigValues(projectCwd, [ + const config = resolvePieceConfigValues(projectCwd, [ 'language', 'provider', 'model', @@ -238,7 +238,7 @@ export async function runAllTasks( options?: TaskExecutionOptions, ): Promise { const taskRunner = new TaskRunner(cwd); - const globalConfig = resolveConfigValues( + const globalConfig = resolvePieceConfigValues( cwd, ['notificationSound', 'notificationSoundEvents', 'concurrency', 'taskPollIntervalMs'], ); diff --git a/src/features/tasks/list/instructMode.ts b/src/features/tasks/list/instructMode.ts index 1bd3476..4d305da 100644 --- a/src/features/tasks/list/instructMode.ts +++ b/src/features/tasks/list/instructMode.ts @@ -23,7 +23,7 @@ import { import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js'; import { loadTemplate } from '../../../shared/prompts/index.js'; import { getLabelObject } from '../../../shared/i18n/index.js'; -import { resolveConfigValues } from '../../../infra/config/index.js'; +import { resolvePieceConfigValues } from '../../../infra/config/index.js'; export type InstructModeAction = 'execute' | 'save_task' | 'cancel'; @@ -109,7 +109,7 @@ export async function runInstructMode( pieceContext?: PieceContext, runSessionContext?: RunSessionContext, ): Promise { - const globalConfig = resolveConfigValues(cwd, ['language', 'provider']); + const globalConfig = resolvePieceConfigValues(cwd, ['language', 'provider']); const lang = resolveLanguage(globalConfig.language); if (!globalConfig.provider) { diff --git a/src/features/tasks/list/taskInstructionActions.ts b/src/features/tasks/list/taskInstructionActions.ts index b2d3668..acd05d6 100644 --- a/src/features/tasks/list/taskInstructionActions.ts +++ b/src/features/tasks/list/taskInstructionActions.ts @@ -11,7 +11,7 @@ import { TaskRunner, detectDefaultBranch, } from '../../../infra/task/index.js'; -import { resolveConfigValues, getPieceDescription } from '../../../infra/config/index.js'; +import { resolvePieceConfigValues, getPieceDescription } from '../../../infra/config/index.js'; import { info, error as logError } from '../../../shared/ui/index.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; import { runInstructMode } from './instructMode.js'; @@ -93,7 +93,7 @@ export async function instructBranch( return false; } - const globalConfig = resolveConfigValues(projectDir, ['interactivePreviewMovements', 'language']); + const globalConfig = resolvePieceConfigValues(projectDir, ['interactivePreviewMovements', 'language']); const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements); const pieceContext: PieceContext = { name: pieceDesc.name, diff --git a/src/features/tasks/list/taskRetryActions.ts b/src/features/tasks/list/taskRetryActions.ts index 2a68bd5..b51c66c 100644 --- a/src/features/tasks/list/taskRetryActions.ts +++ b/src/features/tasks/list/taskRetryActions.ts @@ -8,7 +8,7 @@ import * as fs from 'node:fs'; import type { TaskListItem } from '../../../infra/task/index.js'; import { TaskRunner } from '../../../infra/task/index.js'; -import { loadPieceByIdentifier, resolveConfigValue, getPieceDescription } from '../../../infra/config/index.js'; +import { loadPieceByIdentifier, resolvePieceConfigValue, getPieceDescription } from '../../../infra/config/index.js'; import { selectPiece } from '../../pieceSelection/index.js'; import { selectOption } from '../../../shared/prompt/index.js'; import { info, header, blankLine, status } from '../../../shared/ui/index.js'; @@ -133,7 +133,7 @@ export async function retryFailedTask( return false; } - const previewCount = resolveConfigValue(projectDir, 'interactivePreviewMovements'); + const previewCount = resolvePieceConfigValue(projectDir, 'interactivePreviewMovements'); const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir); if (!pieceConfig) { diff --git a/src/features/tasks/watch/index.ts b/src/features/tasks/watch/index.ts index d2131fa..91feaaa 100644 --- a/src/features/tasks/watch/index.ts +++ b/src/features/tasks/watch/index.ts @@ -6,7 +6,7 @@ */ import { TaskRunner, type TaskInfo, TaskWatcher } from '../../../infra/task/index.js'; -import { getCurrentPiece } from '../../../infra/config/index.js'; +import { resolveConfigValue } from '../../../infra/config/index.js'; import { header, info, @@ -15,7 +15,6 @@ import { blankLine, } from '../../../shared/ui/index.js'; import { executeAndCompleteTask } from '../execute/taskExecution.js'; -import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js'; import { EXIT_SIGINT } from '../../../shared/exitCodes.js'; import { ShutdownManager } from '../execute/shutdownManager.js'; import type { TaskExecutionOptions } from '../execute/types.js'; @@ -25,7 +24,7 @@ import type { TaskExecutionOptions } from '../execute/types.js'; * Runs until Ctrl+C. */ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): Promise { - const pieceName = getCurrentPiece(cwd) || DEFAULT_PIECE_NAME; + const pieceName = resolveConfigValue(cwd, 'piece'); const taskRunner = new TaskRunner(cwd); const watcher = new TaskWatcher(cwd); const recovered = taskRunner.recoverInterruptedRunningTasks(); diff --git a/src/infra/config/global/pieceCategories.ts b/src/infra/config/global/pieceCategories.ts index bd59b1b..b927ae1 100644 --- a/src/infra/config/global/pieceCategories.ts +++ b/src/infra/config/global/pieceCategories.ts @@ -7,7 +7,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { getGlobalConfigDir } from '../paths.js'; -import { resolveConfigValue } from '../resolveConfigValue.js'; +import { resolvePieceConfigValue } from '../resolvePieceConfigValue.js'; const INITIAL_USER_CATEGORIES_CONTENT = 'piece_categories: {}\n'; @@ -17,7 +17,7 @@ function getDefaultPieceCategoriesPath(): string { /** Get the path to the user's piece categories file. */ export function getPieceCategoriesPath(cwd: string): string { - const pieceCategoriesFile = resolveConfigValue(cwd, 'pieceCategoriesFile'); + const pieceCategoriesFile = resolvePieceConfigValue(cwd, 'pieceCategoriesFile'); if (pieceCategoriesFile) { return pieceCategoriesFile; } diff --git a/src/infra/config/index.ts b/src/infra/config/index.ts index 585a844..87bde56 100644 --- a/src/infra/config/index.ts +++ b/src/infra/config/index.ts @@ -7,3 +7,4 @@ export * from './loaders/index.js'; export * from './global/index.js'; export * from './project/index.js'; export * from './resolveConfigValue.js'; +export * from './resolvePieceConfigValue.js'; diff --git a/src/infra/config/loadConfig.ts b/src/infra/config/loadConfig.ts index 088b540..c907a6f 100644 --- a/src/infra/config/loadConfig.ts +++ b/src/infra/config/loadConfig.ts @@ -7,6 +7,7 @@ import { envVarNameFromPath } from './env/config-env-overrides.js'; export interface LoadedConfig extends GlobalConfig { piece: string; + provider: NonNullable; verbose: boolean; providerOptions?: MovementProviderOptions; providerProfiles?: ProviderPermissionProfiles; @@ -15,12 +16,13 @@ export interface LoadedConfig extends GlobalConfig { export function loadConfig(projectDir: string): LoadedConfig { const global = loadGlobalConfig(); const project = loadProjectConfig(projectDir); - const provider = project.provider ?? global.provider; + const provider = (project.provider ?? global.provider ?? 'claude') as NonNullable; return { ...global, piece: project.piece ?? 'default', provider, + autoPr: project.auto_pr ?? global.autoPr, model: resolveModel(global, provider), verbose: resolveVerbose(project.verbose, global.verbose), providerOptions: mergeProviderOptions(global.providerOptions, project.providerOptions), diff --git a/src/infra/config/loaders/pieceCategories.ts b/src/infra/config/loaders/pieceCategories.ts index d4f327d..a503e46 100644 --- a/src/infra/config/loaders/pieceCategories.ts +++ b/src/infra/config/loaders/pieceCategories.ts @@ -13,7 +13,7 @@ import { z } from 'zod/v4'; import { getPieceCategoriesPath } from '../global/pieceCategories.js'; import { getLanguageResourcesDir } from '../../resources/index.js'; import { listBuiltinPieceNames } from './pieceResolver.js'; -import { resolveConfigValues } from '../resolveConfigValue.js'; +import { resolvePieceConfigValues } from '../resolvePieceConfigValue.js'; import type { PieceWithSource } from './pieceResolver.js'; const CategoryConfigSchema = z.object({ @@ -233,7 +233,7 @@ function resolveOthersCategoryName(defaultConfig: ParsedCategoryConfig, userConf * Returns null if file doesn't exist or has no piece_categories. */ export function loadDefaultCategories(cwd: string): CategoryConfig | null { - const { language: lang } = resolveConfigValues(cwd, ['language']); + const { language: lang } = resolvePieceConfigValues(cwd, ['language']); const filePath = join(getLanguageResourcesDir(lang), 'piece-categories.yaml'); const parsed = loadCategoryConfigFromPath(filePath, filePath); @@ -256,7 +256,7 @@ export function loadDefaultCategories(cwd: string): CategoryConfig | null { /** Get the path to the builtin default categories file. */ export function getDefaultCategoriesPath(cwd: string): string { - const { language: lang } = resolveConfigValues(cwd, ['language']); + const { language: lang } = resolvePieceConfigValues(cwd, ['language']); return join(getLanguageResourcesDir(lang), 'piece-categories.yaml'); } @@ -378,7 +378,7 @@ export function buildCategorizedPieces( config: CategoryConfig, cwd: string, ): CategorizedPieces { - const globalConfig = resolveConfigValues(cwd, ['enableBuiltinPieces', 'disabledBuiltins']); + const globalConfig = resolvePieceConfigValues(cwd, ['enableBuiltinPieces', 'disabledBuiltins']); const ignoreMissing = new Set(); if (globalConfig.enableBuiltinPieces === false) { for (const name of listBuiltinPieceNames(cwd, { includeDisabled: true })) { diff --git a/src/infra/config/loaders/pieceParser.ts b/src/infra/config/loaders/pieceParser.ts index 33ef929..fbedd07 100644 --- a/src/infra/config/loaders/pieceParser.ts +++ b/src/infra/config/loaders/pieceParser.ts @@ -11,7 +11,7 @@ import { parse as parseYaml } from 'yaml'; import type { z } from 'zod'; import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js'; import type { PieceConfig, PieceMovement, PieceRule, OutputContractEntry, OutputContractItem, LoopMonitorConfig, LoopMonitorJudge, ArpeggioMovementConfig, ArpeggioMergeMovementConfig, TeamLeaderConfig } from '../../../core/models/index.js'; -import { resolveConfigValue } from '../resolveConfigValue.js'; +import { resolvePieceConfigValue } from '../resolvePieceConfigValue.js'; import { type PieceSections, type FacetResolutionContext, @@ -439,7 +439,7 @@ export function loadPieceFromFile(filePath: string, projectDir: string): PieceCo const pieceDir = dirname(filePath); const context: FacetResolutionContext = { - lang: resolveConfigValue(projectDir, 'language'), + lang: resolvePieceConfigValue(projectDir, 'language'), projectDir, }; diff --git a/src/infra/config/loaders/pieceResolver.ts b/src/infra/config/loaders/pieceResolver.ts index f6871e0..5b62385 100644 --- a/src/infra/config/loaders/pieceResolver.ts +++ b/src/infra/config/loaders/pieceResolver.ts @@ -10,7 +10,7 @@ import { join, resolve, isAbsolute } from 'node:path'; import { homedir } from 'node:os'; import type { PieceConfig, PieceMovement, InteractiveMode } from '../../../core/models/index.js'; import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js'; -import { resolveConfigValues } from '../resolveConfigValue.js'; +import { resolvePieceConfigValues } from '../resolvePieceConfigValue.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; import { loadPieceFromFile } from './pieceParser.js'; @@ -24,7 +24,7 @@ export interface PieceWithSource { } export function listBuiltinPieceNames(cwd: string, options?: { includeDisabled?: boolean }): string[] { - const config = resolveConfigValues(cwd, ['language', 'disabledBuiltins']); + const config = resolvePieceConfigValues(cwd, ['language', 'disabledBuiltins']); const lang = config.language; const dir = getBuiltinPiecesDir(lang); const disabled = options?.includeDisabled ? undefined : (config.disabledBuiltins ?? []); @@ -37,7 +37,7 @@ export function listBuiltinPieceNames(cwd: string, options?: { includeDisabled?: /** Get builtin piece by name */ export function getBuiltinPiece(name: string, projectCwd: string): PieceConfig | null { - const config = resolveConfigValues(projectCwd, ['enableBuiltinPieces', 'language', 'disabledBuiltins']); + const config = resolvePieceConfigValues(projectCwd, ['enableBuiltinPieces', 'language', 'disabledBuiltins']); if (config.enableBuiltinPieces === false) return null; const lang = config.language; const disabled = config.disabledBuiltins ?? []; @@ -373,7 +373,7 @@ function* iteratePieceDir( /** Get the 3-layer directory list (builtin → user → project-local) */ function getPieceDirs(cwd: string): { dir: string; source: PieceSource; disabled?: string[] }[] { - const config = resolveConfigValues(cwd, ['enableBuiltinPieces', 'language', 'disabledBuiltins']); + const config = resolvePieceConfigValues(cwd, ['enableBuiltinPieces', 'language', 'disabledBuiltins']); const disabled = config.disabledBuiltins ?? []; const lang = config.language; const dirs: { dir: string; source: PieceSource; disabled?: string[] }[] = []; diff --git a/src/infra/config/resolvePieceConfigValue.ts b/src/infra/config/resolvePieceConfigValue.ts new file mode 100644 index 0000000..98b0375 --- /dev/null +++ b/src/infra/config/resolvePieceConfigValue.ts @@ -0,0 +1,17 @@ +import type { ConfigParameterKey } from './resolveConfigValue.js'; +import { resolveConfigValue, resolveConfigValues } from './resolveConfigValue.js'; +import type { LoadedConfig } from './loadConfig.js'; + +export function resolvePieceConfigValue( + projectDir: string, + key: K, +): LoadedConfig[K] { + return resolveConfigValue(projectDir, key); +} + +export function resolvePieceConfigValues( + projectDir: string, + keys: readonly K[], +): Pick { + return resolveConfigValues(projectDir, keys); +} diff --git a/src/infra/config/types.ts b/src/infra/config/types.ts index ae83e49..e806e3c 100644 --- a/src/infra/config/types.ts +++ b/src/infra/config/types.ts @@ -10,7 +10,11 @@ export interface ProjectLocalConfig { /** Current piece name */ piece?: string; /** Provider selection for agent runtime */ - provider?: 'claude' | 'codex' | 'opencode'; + provider?: 'claude' | 'codex' | 'opencode' | 'mock'; + /** Auto-create PR after worktree execution */ + auto_pr?: boolean; + /** Auto-create PR after worktree execution (camelCase alias) */ + autoPr?: boolean; /** Verbose output mode */ verbose?: boolean; /** Provider-specific options (overrides global, overridden by piece/movement) */