diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index db6b723..5534c97 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -501,6 +501,60 @@ describe('analytics config resolution', () => { }); }); +describe('model config resolution', () => { + let testDir: string; + let originalTaktConfigDir: string | undefined; + + beforeEach(() => { + testDir = join(tmpdir(), `takt-test-${randomUUID()}`); + mkdirSync(testDir, { recursive: true }); + originalTaktConfigDir = process.env.TAKT_CONFIG_DIR; + process.env.TAKT_CONFIG_DIR = join(testDir, 'global-takt'); + invalidateGlobalConfigCache(); + }); + + afterEach(() => { + if (originalTaktConfigDir === undefined) { + delete process.env.TAKT_CONFIG_DIR; + } else { + process.env.TAKT_CONFIG_DIR = originalTaktConfigDir; + } + invalidateGlobalConfigCache(); + + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true, force: true }); + } + }); + + it('should resolve project model over global model', () => { + const projectConfigDir = getProjectConfigDir(testDir); + mkdirSync(projectConfigDir, { recursive: true }); + writeFileSync(join(projectConfigDir, 'config.yaml'), 'piece: default\nmodel: project-model\n'); + + const globalConfigDir = process.env.TAKT_CONFIG_DIR!; + mkdirSync(globalConfigDir, { recursive: true }); + writeFileSync(join(globalConfigDir, 'config.yaml'), 'model: global-model\n'); + + expect(resolveConfigValue(testDir, 'model')).toBe('project-model'); + }); + + it('should fallback to global model when project model is not set', () => { + const projectConfigDir = getProjectConfigDir(testDir); + mkdirSync(projectConfigDir, { recursive: true }); + writeFileSync(join(projectConfigDir, 'config.yaml'), 'piece: default\n'); + + const globalConfigDir = process.env.TAKT_CONFIG_DIR!; + mkdirSync(globalConfigDir, { recursive: true }); + writeFileSync(join(globalConfigDir, 'config.yaml'), 'model: global-model\n'); + + expect(resolveConfigValue(testDir, 'model')).toBe('global-model'); + }); + + it('should return undefined when neither project nor global model is set', () => { + expect(resolveConfigValue(testDir, 'model')).toBeUndefined(); + }); +}); + describe('isVerboseMode', () => { let testDir: string; let originalTaktConfigDir: string | undefined; diff --git a/src/core/models/persisted-global-config.ts b/src/core/models/persisted-global-config.ts index 981a7b5..972114e 100644 --- a/src/core/models/persisted-global-config.ts +++ b/src/core/models/persisted-global-config.ts @@ -133,6 +133,7 @@ export interface PersistedGlobalConfig { export interface ProjectConfig { piece?: string; provider?: 'claude' | 'codex' | 'opencode' | 'mock'; + model?: string; providerOptions?: MovementProviderOptions; /** Provider-specific permission profiles */ providerProfiles?: ProviderPermissionProfiles; diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index fa5d356..092cede 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -491,6 +491,7 @@ export const GlobalConfigSchema = z.object({ export const ProjectConfigSchema = z.object({ piece: z.string().optional(), provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(), + model: z.string().optional(), provider_options: MovementProviderOptionsSchema, provider_profiles: ProviderPermissionProfilesSchema, /** Base branch to clone from (overrides global base_branch) */ diff --git a/src/infra/config/resolveConfigValue.ts b/src/infra/config/resolveConfigValue.ts index e184018..562658e 100644 --- a/src/infra/config/resolveConfigValue.ts +++ b/src/infra/config/resolveConfigValue.ts @@ -118,6 +118,8 @@ function getLocalLayerValue( return project.piece as LoadedConfig[K] | undefined; case 'provider': return project.provider as LoadedConfig[K] | undefined; + case 'model': + return project.model as LoadedConfig[K] | undefined; case 'autoPr': return project.auto_pr as LoadedConfig[K] | undefined; case 'draftPr': diff --git a/src/infra/config/types.ts b/src/infra/config/types.ts index eb039b5..44dc5a4 100644 --- a/src/infra/config/types.ts +++ b/src/infra/config/types.ts @@ -12,6 +12,8 @@ export interface ProjectLocalConfig { piece?: string; /** Provider selection for agent runtime */ provider?: 'claude' | 'codex' | 'opencode' | 'mock'; + /** Model selection for agent runtime */ + model?: string; /** Auto-create PR after worktree execution */ auto_pr?: boolean; /** Create PR as draft */