refactor: piece設定解決とconfig優先順位の参照経路を統一

This commit is contained in:
nrslib 2026-02-19 11:22:49 +09:00
parent 6b425d64fc
commit 67ae3e8ae5
33 changed files with 124 additions and 87 deletions

View File

@ -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' })),
}));

View File

@ -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')

View File

@ -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(),

View File

@ -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(),

View File

@ -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);
});

View File

@ -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 () => {

View File

@ -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,

View File

@ -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',

View File

@ -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' });

View File

@ -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);

View File

@ -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));
});

View File

@ -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<void> {
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<void> {
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,

View File

@ -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) {

View File

@ -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<boolean> {
if (!pieceName) {
const current = getCurrentPiece(cwd);
const current = resolveConfigValue(cwd, 'piece');
info(`Current piece: ${current}`);
const selected = await selectPiece(cwd, { fallbackToDefault: false });

View File

@ -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<string | null> {
const fallbackToDefault = options?.fallbackToDefault !== false;
const categoryConfig = getPieceCategories(cwd);
const currentPiece = getCurrentPiece(cwd);
const currentPiece = resolveConfigValue(cwd, 'piece');
if (categoryConfig) {
const allPieces = loadAllPiecesWithSources(cwd);

View File

@ -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<void> {
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}`);

View File

@ -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),

View File

@ -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);
}

View File

@ -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 {

View File

@ -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<AgentResponse>,
provider?: string
): Promise<AgentResponse> {
const resolvedProvider = provider ?? resolveConfigValue(cwd, 'provider') ?? 'claude';
const resolvedProvider = provider ?? resolvePieceConfigValue(cwd, 'provider');
const sessions = loadPersonaSessions(cwd, resolvedProvider);
const sessionId = sessions[personaName];

View File

@ -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<Piece
movements: pieceConfig.movements.map((s: { name: string }) => s.name),
});
const config = resolveConfigValues(projectCwd, [
const config = resolvePieceConfigValues(projectCwd, [
'language',
'provider',
'model',
@ -238,7 +238,7 @@ export async function runAllTasks(
options?: TaskExecutionOptions,
): Promise<void> {
const taskRunner = new TaskRunner(cwd);
const globalConfig = resolveConfigValues(
const globalConfig = resolvePieceConfigValues(
cwd,
['notificationSound', 'notificationSoundEvents', 'concurrency', 'taskPollIntervalMs'],
);

View File

@ -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<InstructModeResult> {
const globalConfig = resolveConfigValues(cwd, ['language', 'provider']);
const globalConfig = resolvePieceConfigValues(cwd, ['language', 'provider']);
const lang = resolveLanguage(globalConfig.language);
if (!globalConfig.provider) {

View File

@ -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,

View File

@ -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) {

View File

@ -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<void> {
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();

View File

@ -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;
}

View File

@ -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';

View File

@ -7,6 +7,7 @@ import { envVarNameFromPath } from './env/config-env-overrides.js';
export interface LoadedConfig extends GlobalConfig {
piece: string;
provider: NonNullable<GlobalConfig['provider']>;
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<GlobalConfig['provider']>;
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),

View File

@ -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<string>();
if (globalConfig.enableBuiltinPieces === false) {
for (const name of listBuiltinPieceNames(cwd, { includeDisabled: true })) {

View File

@ -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,
};

View File

@ -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[] }[] = [];

View File

@ -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<K extends ConfigParameterKey>(
projectDir: string,
key: K,
): LoadedConfig[K] {
return resolveConfigValue(projectDir, key);
}
export function resolvePieceConfigValues<K extends ConfigParameterKey>(
projectDir: string,
keys: readonly K[],
): Pick<LoadedConfig, K> {
return resolveConfigValues(projectDir, keys);
}

View File

@ -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) */