diff --git a/builtins/en/config.yaml b/builtins/en/config.yaml index ff5e93b..72c9232 100644 --- a/builtins/en/config.yaml +++ b/builtins/en/config.yaml @@ -30,7 +30,6 @@ language: en # UI language: en | ja # pr_body_template: "{report}" # PR body template. Variables: {issue_body}, {report}, {issue} # Output / notifications -# verbose: false # Shortcut: enable trace/debug and set logging.level=debug # minimal_output: false # Suppress detailed agent output # notification_sound: true # Master switch for sounds # notification_sound_events: # Per-event sound toggle (unset means true) diff --git a/builtins/en/facets/instructions/supervise.md b/builtins/en/facets/instructions/supervise.md index a7dda1d..4f49123 100644 --- a/builtins/en/facets/instructions/supervise.md +++ b/builtins/en/facets/instructions/supervise.md @@ -1,9 +1,11 @@ Run tests, verify the build, and perform final approval. **Overall piece verification:** -1. Whether the plan and implementation results are consistent -2. Whether findings from each review movement have been addressed -3. Whether each task spec requirement has been achieved +1. Check all reports in the report directory and verify overall piece consistency + - Does implementation match the plan? + - Were all review movement findings properly addressed? + - Was the original task objective achieved? +2. Whether each task spec requirement has been achieved - Extract requirements one by one from the task spec - For each requirement, identify the implementing code (file:line) - Verify the code actually fulfills the requirement (read the file, run the test) diff --git a/builtins/en/facets/personas/supervisor.md b/builtins/en/facets/personas/supervisor.md index 8e038b4..bf80567 100644 --- a/builtins/en/facets/personas/supervisor.md +++ b/builtins/en/facets/personas/supervisor.md @@ -81,15 +81,7 @@ You are the **human proxy** in the automated piece. Before approval, verify the | Production ready | No mock/stub/TODO remaining? | | Operation | Actually works as expected? | -### 6. Backward Compatibility Code Detection - -**Backward compatibility code is unnecessary unless explicitly instructed.** REJECT if found: - -- Unused re-exports, `_var` renames, `// removed` comments -- Fallbacks, old API maintenance, migration code -- Legacy support kept "just in case" - -### 7. Spec Compliance Final Check +### 6. Spec Compliance Final Check **Final verification that changes comply with the project's documented specifications.** @@ -115,66 +107,6 @@ Additions can be reverted, but restoring deleted flows is difficult. - A "UI fix" task includes structural changes to backend domain models - A "display change" task rewrites business logic flows -### 8. Piece Overall Review - -**Check all reports in the report directory and verify overall piece consistency.** - -Check: -- Does implementation match the plan (00-plan.md)? -- Were all review step issues properly addressed? -- Was the original task objective achieved? - -**Piece-wide issues:** -| Issue | Action | -|-------|--------| -| Plan-implementation gap | REJECT - Request plan revision or implementation fix | -| Unaddressed review feedback | REJECT - Point out specific unaddressed items | -| Deviation from original purpose | REJECT - Request return to objective | -| Scope creep | REJECT - Deletions outside task order must be reverted | - -### 9. Improvement Suggestion Check - -**Check review reports for unaddressed improvement suggestions.** - -Check: -- "Improvement Suggestions" section in Architect report -- Warnings and suggestions in AI Reviewer report -- Recommendations in Security report - -**If there are unaddressed improvement suggestions:** -- Judge if the improvement should be addressed in this task -- If it should be addressed, **REJECT** and request fix -- If it should be addressed in next task, record as "technical debt" in report - -**Judgment criteria:** -| Type of suggestion | Decision | -|--------------------|----------| -| Minor fix in same file | Address now (REJECT) | -| Fixable in seconds to minutes | Address now (REJECT) | -| Redundant code / unnecessary expression removal | Address now (REJECT) | -| Affects other features | Address in next task (record only) | -| External impact (API changes, etc.) | Address in next task (record only) | -| Requires significant refactoring (large scope) | Address in next task (record only) | - -### Boy Scout Rule - -**"Functionally harmless" is not a free pass.** Classifying a near-zero-cost fix as "non-blocking" or "next task" is a compromise. There is no guarantee it will be addressed in a future task, and it accumulates as technical debt. - -**Principle:** If a reviewer found it and it can be fixed in minutes, make the coder fix it now. Do not settle for recording it as a "non-blocking improvement suggestion." - -## Workaround Detection - -**REJECT** if any of the following remain: - -| Pattern | Example | -|---------|---------| -| TODO/FIXME | `// TODO: implement later` | -| Commented out | Code that should be deleted remains | -| Hardcoded | Values that should be config are hardcoded | -| Mock data | Dummy data unusable in production | -| console.log | Forgotten debug output | -| Skipped tests | `@Disabled`, `.skip()` | - ## Important - **Actually run**: Don't just look at files, execute and verify diff --git a/builtins/en/pieces/takt-default-team-leader.yaml b/builtins/en/pieces/takt-default-team-leader.yaml index a0ac386..de9bbbd 100644 --- a/builtins/en/pieces/takt-default-team-leader.yaml +++ b/builtins/en/pieces/takt-default-team-leader.yaml @@ -15,19 +15,7 @@ loop_monitors: threshold: 3 judge: persona: supervisor - instruction_template: | - The ai_review ↔ ai_fix loop has repeated {cycle_count} times. - - Review the reports from each cycle and determine whether this loop - is healthy (making progress) or unproductive (repeating the same issues). - - **Reports to reference:** - - AI Review results: {report:ai-review.md} - - **Judgment criteria:** - - Are new issues being found/fixed in each cycle? - - Are the same findings being repeated? - - Are fixes actually being applied? + instruction_template: loop-monitor-ai-fix rules: - condition: Healthy (making progress) next: ai_review @@ -158,7 +146,7 @@ movements: - condition: No implementation (report only) next: ai_review - condition: Cannot proceed, insufficient info - next: ai_review + next: plan - condition: User input required next: implement requires_user_input: true @@ -392,6 +380,7 @@ movements: edit: false persona: supervisor policy: review + knowledge: architecture provider_options: claude: allowed_tools: diff --git a/builtins/ja/config.yaml b/builtins/ja/config.yaml index b239282..a0ce244 100644 --- a/builtins/ja/config.yaml +++ b/builtins/ja/config.yaml @@ -30,7 +30,6 @@ language: ja # 表示言語: ja | en # pr_body_template: "{report}" # PR本文テンプレート。変数: {issue_body}, {report}, {issue} # 出力・通知 -# verbose: false # ショートカット: trace/debug有効化 + logging.level=debug # minimal_output: false # エージェント詳細出力を抑制 # notification_sound: true # 通知音全体のON/OFF # notification_sound_events: # イベント別通知音(未指定はtrue扱い) diff --git a/builtins/ja/facets/instructions/supervise.md b/builtins/ja/facets/instructions/supervise.md index 2e0518e..52c64d6 100644 --- a/builtins/ja/facets/instructions/supervise.md +++ b/builtins/ja/facets/instructions/supervise.md @@ -1,9 +1,11 @@ テスト実行、ビルド確認、最終承認を行ってください。 **ピース全体の確認:** -1. 計画と実装結果が一致しているか -2. 各レビュームーブメントの指摘が対応されているか -3. タスク指示書の各要件が達成されているか +1. レポートディレクトリ内の全レポートを確認し、ピース全体の整合性をチェックする + - 計画と実装結果が一致しているか + - 各レビュームーブメントの指摘が適切に対応されているか + - タスクの本来の目的が達成されているか +2. タスク指示書の各要件が達成されているか - タスク指示書から要件を1つずつ抽出する - 各要件について、実装されたコード(ファイル:行)を特定する - コードが要件を満たしていることを実際に確認する(ファイルを読む、テストを実行する) diff --git a/builtins/ja/facets/personas/supervisor.md b/builtins/ja/facets/personas/supervisor.md index d519e74..44f0b2d 100644 --- a/builtins/ja/facets/personas/supervisor.md +++ b/builtins/ja/facets/personas/supervisor.md @@ -79,31 +79,6 @@ | 本番 Ready | モック・スタブ・TODO が残っていないか | | 動作 | 実際に期待通り動くか | -### 後方互換コードの検出 - -明示的な指示がない限り、後方互換コードは不要。以下を見つけたら REJECT。 - -- 未使用の re-export、`_var` リネーム、`// removed` コメント -- フォールバック、古い API 維持、移行期コード -- 「念のため」残されたレガシー対応 - -### その場しのぎの検出 - -以下が残っていたら REJECT。 - -| パターン | 例 | -|---------|-----| -| TODO/FIXME | `// TODO: implement later` | -| コメントアウト | 消すべきコードが残っている | -| ハードコード | 本来設定値であるべきものが直書き | -| モックデータ | 本番で使えないダミーデータ | -| console.log | デバッグ出力の消し忘れ | -| スキップされたテスト | `@Disabled`、`.skip()` | - -### ボーイスカウトルール - -「機能的に無害」は免罪符ではない。修正コストがほぼゼロの指摘を「非ブロッキング」「次回タスク」に分類することは妥協である。レビュアーが発見し、数分以内に修正できる問題は今回のタスクで修正させる。 - ### スコープクリープの検出(削除は最重要チェック) ファイルの**削除**と既存機能の**除去**はスコープクリープの最も危険な形態。 @@ -119,10 +94,3 @@ - 「UI修正」タスクでバックエンドのドメインモデルが構造変更されている - 「表示変更」タスクでビジネスロジックのフローが書き換えられている -### ピース全体の見直し - -レポートディレクトリ内の全レポートを確認し、ピース全体の整合性をチェックする。 - -- 計画と実装結果が一致しているか -- 各レビュームーブメントの指摘が適切に対応されているか -- タスクの本来の目的が達成されているか diff --git a/builtins/ja/pieces/takt-default-team-leader.yaml b/builtins/ja/pieces/takt-default-team-leader.yaml index 4bdba04..f4cb45c 100644 --- a/builtins/ja/pieces/takt-default-team-leader.yaml +++ b/builtins/ja/pieces/takt-default-team-leader.yaml @@ -15,19 +15,7 @@ loop_monitors: threshold: 3 judge: persona: supervisor - instruction_template: | - ai_review と ai_fix のループが {cycle_count} 回繰り返されました。 - - 各サイクルのレポートを確認し、このループが健全(進捗がある)か、 - 非生産的(同じ問題を繰り返している)かを判断してください。 - - **参照するレポート:** - - AIレビュー結果: {report:ai-review.md} - - **判断基準:** - - 各サイクルで新しい問題が発見・修正されているか - - 同じ指摘が繰り返されていないか - - 修正が実際に反映されているか + instruction_template: loop-monitor-ai-fix rules: - condition: 健全(進捗あり) next: ai_review @@ -158,7 +146,7 @@ movements: - condition: 実装未着手(レポートのみ) next: ai_review - condition: 判断できない、情報不足 - next: ai_review + next: plan - condition: ユーザー入力が必要 next: implement requires_user_input: true @@ -392,6 +380,7 @@ movements: edit: false persona: supervisor policy: review + knowledge: architecture provider_options: claude: allowed_tools: diff --git a/src/__tests__/config-api-boundary.test.ts b/src/__tests__/config-api-boundary.test.ts index 879b01a..8db5969 100644 --- a/src/__tests__/config-api-boundary.test.ts +++ b/src/__tests__/config-api-boundary.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest'; describe('config API boundary', () => { - it('should expose migrated fallback loader from global config module', async () => { + it('should not expose migration-era fallback loader from global config module', async () => { const globalConfig = await import('../infra/config/global/globalConfig.js'); - expect('loadGlobalMigratedProjectLocalFallback' in globalConfig).toBe(true); + expect('loadGlobalMigratedProjectLocalFallback' in globalConfig).toBe(false); }); it('should not expose GlobalConfigManager from config public module', async () => { diff --git a/src/__tests__/config-env-overrides.test.ts b/src/__tests__/config-env-overrides.test.ts index 74c4999..8218e73 100644 --- a/src/__tests__/config-env-overrides.test.ts +++ b/src/__tests__/config-env-overrides.test.ts @@ -53,7 +53,6 @@ describe('config env overrides', () => { it('should apply project env overrides from generated env names', () => { process.env.TAKT_MODEL = 'gpt-5'; - process.env.TAKT_VERBOSE = 'true'; process.env.TAKT_CONCURRENCY = '3'; process.env.TAKT_ANALYTICS_EVENTS_PATH = '/tmp/project-analytics'; @@ -61,7 +60,6 @@ describe('config env overrides', () => { applyProjectConfigEnvOverrides(raw); expect(raw.model).toBe('gpt-5'); - expect(raw.verbose).toBe(true); expect(raw.concurrency).toBe(3); expect(raw.analytics).toEqual({ events_path: '/tmp/project-analytics', diff --git a/src/__tests__/config-migrated-keys-contract.test.ts b/src/__tests__/config-migrated-keys-contract.test.ts deleted file mode 100644 index b9a12f2..0000000 --- a/src/__tests__/config-migrated-keys-contract.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { PersistedGlobalConfig } from '../core/models/persisted-global-config.js'; -import type { ProjectLocalConfig } from '../infra/config/types.js'; -import type { MigratedProjectLocalConfigKey } from '../infra/config/migratedProjectLocalKeys.js'; -import * as migratedProjectLocalKeysModule from '../infra/config/migratedProjectLocalKeys.js'; - -type Assert = T; -type IsNever = [T] extends [never] ? true : false; - -const globalConfigTypeBoundaryGuard: Assert< - IsNever> -> = true; -void globalConfigTypeBoundaryGuard; - -const projectConfigTypeBoundaryGuard: Assert< - IsNever> -> = true; -void projectConfigTypeBoundaryGuard; - -describe('migrated config key contracts', () => { - it('should expose only runtime exports needed by migrated key metadata module', () => { - expect(Object.keys(migratedProjectLocalKeysModule).sort()).toEqual([ - 'MIGRATED_PROJECT_LOCAL_CONFIG_KEYS', - 'MIGRATED_PROJECT_LOCAL_CONFIG_METADATA', - ]); - }); - - it('should not expose helper exports that bypass metadata contract', () => { - expect('isMigratedProjectLocalConfigKey' in migratedProjectLocalKeysModule).toBe(false); - }); -}); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index fac6de0..bd4477d 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -906,7 +906,6 @@ describe('analytics config resolution', () => { describe('isVerboseMode', () => { let testDir: string; let originalTaktConfigDir: string | undefined; - let originalTaktVerbose: string | undefined; let originalTaktLoggingDebug: string | undefined; let originalTaktLoggingTrace: string | undefined; @@ -914,11 +913,9 @@ describe('isVerboseMode', () => { testDir = join(tmpdir(), `takt-test-${randomUUID()}`); mkdirSync(testDir, { recursive: true }); originalTaktConfigDir = process.env.TAKT_CONFIG_DIR; - originalTaktVerbose = process.env.TAKT_VERBOSE; originalTaktLoggingDebug = process.env.TAKT_LOGGING_DEBUG; originalTaktLoggingTrace = process.env.TAKT_LOGGING_TRACE; process.env.TAKT_CONFIG_DIR = join(testDir, 'global-takt'); - delete process.env.TAKT_VERBOSE; delete process.env.TAKT_LOGGING_DEBUG; delete process.env.TAKT_LOGGING_TRACE; invalidateGlobalConfigCache(); @@ -930,11 +927,6 @@ describe('isVerboseMode', () => { } else { process.env.TAKT_CONFIG_DIR = originalTaktConfigDir; } - if (originalTaktVerbose === undefined) { - delete process.env.TAKT_VERBOSE; - } else { - process.env.TAKT_VERBOSE = originalTaktVerbose; - } if (originalTaktLoggingDebug === undefined) { delete process.env.TAKT_LOGGING_DEBUG; } else { @@ -951,43 +943,7 @@ describe('isVerboseMode', () => { } }); - it('should return project verbose when project config has verbose: true', () => { - const projectConfigDir = getProjectConfigDir(testDir); - mkdirSync(projectConfigDir, { recursive: true }); - writeFileSync(join(projectConfigDir, 'config.yaml'), 'verbose: true\n'); - - const globalConfigDir = process.env.TAKT_CONFIG_DIR!; - mkdirSync(globalConfigDir, { recursive: true }); - writeFileSync(join(globalConfigDir, 'config.yaml'), 'language: en\n'); - - expect(isVerboseMode(testDir)).toBe(true); - }); - - it('should return project verbose when project config has verbose: false', () => { - const projectConfigDir = getProjectConfigDir(testDir); - mkdirSync(projectConfigDir, { recursive: true }); - writeFileSync(join(projectConfigDir, 'config.yaml'), 'verbose: false\n'); - - const globalConfigDir = process.env.TAKT_CONFIG_DIR!; - mkdirSync(globalConfigDir, { recursive: true }); - writeFileSync(join(globalConfigDir, 'config.yaml'), 'language: en\n'); - - expect(isVerboseMode(testDir)).toBe(false); - }); - - it('should use default verbose=false when project verbose is not set', () => { - const projectConfigDir = getProjectConfigDir(testDir); - mkdirSync(projectConfigDir, { recursive: true }); - writeFileSync(join(projectConfigDir, 'config.yaml'), ''); - - const globalConfigDir = process.env.TAKT_CONFIG_DIR!; - mkdirSync(globalConfigDir, { recursive: true }); - writeFileSync(join(globalConfigDir, 'config.yaml'), 'language: en\n'); - - expect(isVerboseMode(testDir)).toBe(false); - }); - - it('should return false when neither project nor global verbose is set', () => { + it('should return false when neither project nor global logging.debug is set', () => { expect(isVerboseMode(testDir)).toBe(false); }); @@ -1051,28 +1007,10 @@ describe('isVerboseMode', () => { expect(isVerboseMode(testDir)).toBe(true); }); - it('should prioritize TAKT_VERBOSE over project and global config', () => { - const projectConfigDir = getProjectConfigDir(testDir); - mkdirSync(projectConfigDir, { recursive: true }); - writeFileSync(join(projectConfigDir, 'config.yaml'), 'verbose: false\n'); - - const globalConfigDir = process.env.TAKT_CONFIG_DIR!; - mkdirSync(globalConfigDir, { recursive: true }); - writeFileSync(join(globalConfigDir, 'config.yaml'), 'language: en\n'); - - process.env.TAKT_VERBOSE = 'true'; + it('should return true when TAKT_LOGGING_DEBUG=true overrides config', () => { + process.env.TAKT_LOGGING_DEBUG = 'true'; expect(isVerboseMode(testDir)).toBe(true); }); - - it('should throw on TAKT_VERBOSE=0', () => { - process.env.TAKT_VERBOSE = '0'; - expect(() => isVerboseMode(testDir)).toThrow('TAKT_VERBOSE must be one of: true, false'); - }); - - it('should throw on invalid TAKT_VERBOSE value', () => { - process.env.TAKT_VERBOSE = 'yes'; - expect(() => isVerboseMode(testDir)).toThrow('TAKT_VERBOSE must be one of: true, false'); - }); }); describe('loadInputHistory', () => { diff --git a/src/__tests__/globalConfig-defaults.test.ts b/src/__tests__/globalConfig-defaults.test.ts index 46e18f3..5a7bf92 100644 --- a/src/__tests__/globalConfig-defaults.test.ts +++ b/src/__tests__/globalConfig-defaults.test.ts @@ -24,7 +24,6 @@ const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache, - loadGlobalMigratedProjectLocalFallback, } = await import('../infra/config/global/globalConfig.js'); const { getGlobalConfigPath } = await import('../infra/config/paths.js'); @@ -48,28 +47,25 @@ describe('loadGlobalConfig', () => { expect(config.model).toBeUndefined(); }); - it('should not expose migrated project-local fields from global config', () => { - const config = loadGlobalConfig() as Record; + it('should not have project-local fields set by default', () => { + const config = loadGlobalConfig(); - expect(config).not.toHaveProperty('logLevel'); - expect(config).not.toHaveProperty('pipeline'); - expect(config).not.toHaveProperty('personaProviders'); - expect(config).not.toHaveProperty('branchNameStrategy'); - expect(config).not.toHaveProperty('minimalOutput'); - expect(config).not.toHaveProperty('concurrency'); - expect(config).not.toHaveProperty('taskPollIntervalMs'); - expect(config).not.toHaveProperty('interactivePreviewMovements'); - expect(config).not.toHaveProperty('verbose'); + expect(config.pipeline).toBeUndefined(); + expect(config.personaProviders).toBeUndefined(); + expect(config.branchNameStrategy).toBeUndefined(); + expect(config.minimalOutput).toBeUndefined(); + expect(config.concurrency).toBeUndefined(); + expect(config.taskPollIntervalMs).toBeUndefined(); + expect(config.interactivePreviewMovements).toBeUndefined(); }); - it('should accept migrated project-local keys in global config.yaml for resolver fallback', () => { + it('should accept project-local keys in global config.yaml', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', - 'log_level: debug', 'pipeline:', ' default_branch_prefix: "global/"', 'persona_providers:', @@ -80,31 +76,27 @@ describe('loadGlobalConfig', () => { 'concurrency: 3', 'task_poll_interval_ms: 1000', 'interactive_preview_movements: 2', - 'verbose: true', ].join('\n'), 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); - const config = loadGlobalConfig() as Record; - expect(config).not.toHaveProperty('logLevel'); - expect(config).not.toHaveProperty('pipeline'); - expect(config).not.toHaveProperty('personaProviders'); - expect(config).not.toHaveProperty('branchNameStrategy'); - expect(config).not.toHaveProperty('minimalOutput'); - expect(config).not.toHaveProperty('concurrency'); - expect(config).not.toHaveProperty('taskPollIntervalMs'); - expect(config).not.toHaveProperty('interactivePreviewMovements'); - expect(config).not.toHaveProperty('verbose'); + const config = loadGlobalConfig(); + expect(config.pipeline).toEqual({ defaultBranchPrefix: 'global/' }); + expect(config.personaProviders).toEqual({ coder: { provider: 'codex' } }); + expect(config.branchNameStrategy).toBe('ai'); + expect(config.minimalOutput).toBe(true); + expect(config.concurrency).toBe(3); + expect(config.taskPollIntervalMs).toBe(1000); + expect(config.interactivePreviewMovements).toBe(2); }); - it('should not persist migrated project-local keys when saving global config', () => { + it('should persist project-local keys when saving global config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); - const config = loadGlobalConfig() as Record; - config.logLevel = 'debug'; + const config = loadGlobalConfig(); config.pipeline = { defaultBranchPrefix: 'global/' }; config.personaProviders = { coder: { provider: 'codex' } }; config.branchNameStrategy = 'ai'; @@ -112,19 +104,16 @@ describe('loadGlobalConfig', () => { config.concurrency = 4; config.taskPollIntervalMs = 1200; config.interactivePreviewMovements = 1; - config.verbose = true; - saveGlobalConfig(config as Parameters[0]); + saveGlobalConfig(config); const raw = readFileSync(getGlobalConfigPath(), 'utf-8'); - expect(raw).not.toContain('log_level:'); - expect(raw).not.toContain('pipeline:'); - expect(raw).not.toContain('persona_providers:'); - expect(raw).not.toContain('branch_name_strategy:'); - expect(raw).not.toContain('minimal_output:'); - expect(raw).not.toContain('concurrency:'); - expect(raw).not.toContain('task_poll_interval_ms:'); - expect(raw).not.toContain('interactive_preview_movements:'); - expect(raw).not.toContain('verbose:'); + expect(raw).toContain('pipeline:'); + expect(raw).toContain('persona_providers:'); + expect(raw).toContain('branch_name_strategy:'); + expect(raw).toContain('minimal_output:'); + expect(raw).toContain('concurrency:'); + expect(raw).toContain('task_poll_interval_ms:'); + expect(raw).toContain('interactive_preview_movements:'); }); it('should return the same cached object on subsequent calls', () => { @@ -264,7 +253,7 @@ describe('loadGlobalConfig', () => { } }); - it('should accept pipeline in global config for migrated fallback', () => { + it('should accept pipeline in global config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( @@ -279,18 +268,20 @@ describe('loadGlobalConfig', () => { ); expect(() => loadGlobalConfig()).not.toThrow(); - const config = loadGlobalConfig() as Record; - expect(config).not.toHaveProperty('pipeline'); + const config = loadGlobalConfig(); + expect(config.pipeline).toEqual({ + defaultBranchPrefix: 'feat/', + commitMessageTemplate: 'fix: {title} (#{issue})', + }); }); it('should save and reload pipeline config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); - // Create minimal config first writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); - (config as Record).pipeline = { + config.pipeline = { defaultBranchPrefix: 'takt/', commitMessageTemplate: 'feat: {title} (#{issue})', }; @@ -298,7 +289,10 @@ describe('loadGlobalConfig', () => { invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); - expect((reloaded as Record).pipeline).toBeUndefined(); + expect(reloaded.pipeline).toEqual({ + defaultBranchPrefix: 'takt/', + commitMessageTemplate: 'feat: {title} (#{issue})', + }); }); it('should load auto_pr config from config.yaml', () => { @@ -631,7 +625,7 @@ describe('loadGlobalConfig', () => { }); }); - it('should accept interactive_preview_movements in global config for migrated fallback', () => { + it('should accept interactive_preview_movements in global config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( @@ -641,8 +635,8 @@ describe('loadGlobalConfig', () => { ); expect(() => loadGlobalConfig()).not.toThrow(); - const config = loadGlobalConfig() as Record; - expect(config).not.toHaveProperty('interactivePreviewMovements'); + const config = loadGlobalConfig(); + expect(config.interactivePreviewMovements).toBe(5); }); it('should save and reload interactive_preview_movements config', () => { @@ -651,24 +645,24 @@ describe('loadGlobalConfig', () => { writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); - (config as Record).interactivePreviewMovements = 7; + config.interactivePreviewMovements = 7; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); - expect((reloaded as Record).interactivePreviewMovements).toBeUndefined(); + expect(reloaded.interactivePreviewMovements).toBe(7); }); - it('should default interactive_preview_movements to 3', () => { + it('should default interactive_preview_movements to undefined', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); - expect((config as Record).interactivePreviewMovements).toBeUndefined(); + expect(config.interactivePreviewMovements).toBeUndefined(); }); - it('should accept interactive_preview_movements=0 in global config for migrated fallback', () => { + it('should accept interactive_preview_movements=0 in global config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( @@ -678,8 +672,8 @@ describe('loadGlobalConfig', () => { ); expect(() => loadGlobalConfig()).not.toThrow(); - const config = loadGlobalConfig() as Record; - expect(config).not.toHaveProperty('interactivePreviewMovements'); + const config = loadGlobalConfig(); + expect(config.interactivePreviewMovements).toBe(0); }); describe('persona_providers', () => { diff --git a/src/__tests__/globalConfig.test.ts b/src/__tests__/globalConfig.test.ts index 37016f0..47b0d89 100644 --- a/src/__tests__/globalConfig.test.ts +++ b/src/__tests__/globalConfig.test.ts @@ -9,7 +9,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import type { PersistedGlobalConfig } from '../core/models/persisted-global-config.js'; +import type { GlobalConfig } from '../core/models/config-types.js'; // Mock the getGlobalConfigPath to use a test directory let testConfigPath: string; @@ -102,7 +102,7 @@ piece_overrides: }); it('should preserve non-empty quality_gates array', () => { - const config: PersistedGlobalConfig = { + const config: GlobalConfig = { pieceOverrides: { qualityGates: ['Test 1', 'Test 2'], }, diff --git a/src/__tests__/it-config-project-local-priority.test.ts b/src/__tests__/it-config-project-local-priority.test.ts index cf60a46..ded5b94 100644 --- a/src/__tests__/it-config-project-local-priority.test.ts +++ b/src/__tests__/it-config-project-local-priority.test.ts @@ -10,25 +10,20 @@ const projectDir = join(rootDir, 'project'); vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => { const original = await importOriginal() as Record; - const globalMigratedValues = { - logLevel: 'info', - pipeline: { defaultBranchPrefix: 'global/' }, - personaProviders: { coder: { provider: 'claude', model: 'claude-3-5-sonnet-latest' } }, - branchNameStrategy: 'ai', - minimalOutput: false, - concurrency: 2, - taskPollIntervalMs: 2000, - interactivePreviewMovements: 4, - verbose: false, - } as const; return { ...original, loadGlobalConfig: () => ({ language: 'en', provider: 'claude', autoFetch: false, + pipeline: { defaultBranchPrefix: 'global/' }, + personaProviders: { coder: { provider: 'claude', model: 'claude-3-5-sonnet-latest' } }, + branchNameStrategy: 'ai', + minimalOutput: false, + concurrency: 2, + taskPollIntervalMs: 2000, + interactivePreviewMovements: 4, }), - loadGlobalMigratedProjectLocalFallback: () => globalMigratedValues, invalidateGlobalConfigCache: () => undefined, }; }); @@ -40,7 +35,7 @@ const { invalidateGlobalConfigCache, } = await import('../infra/config/index.js'); -describe('IT: migrated config keys should prefer project over global', () => { +describe('IT: project-local config keys should prefer project over global', () => { beforeEach(() => { mkdirSync(projectDir, { recursive: true }); mkdirSync(join(projectDir, '.takt'), { recursive: true }); @@ -48,7 +43,6 @@ describe('IT: migrated config keys should prefer project over global', () => { writeFileSync( join(projectDir, '.takt', 'config.yaml'), [ - 'log_level: debug', 'pipeline:', ' default_branch_prefix: "project/"', 'persona_providers:', @@ -60,7 +54,6 @@ describe('IT: migrated config keys should prefer project over global', () => { 'concurrency: 5', 'task_poll_interval_ms: 1300', 'interactive_preview_movements: 1', - 'verbose: true', ].join('\n'), 'utf-8', ); @@ -77,9 +70,8 @@ describe('IT: migrated config keys should prefer project over global', () => { } }); - it('should resolve migrated keys from project config when global has conflicting values', () => { + it('should resolve keys from project config when global has conflicting values', () => { const resolved = resolveConfigValues(projectDir, [ - 'logLevel', 'pipeline', 'personaProviders', 'branchNameStrategy', @@ -87,10 +79,8 @@ describe('IT: migrated config keys should prefer project over global', () => { 'concurrency', 'taskPollIntervalMs', 'interactivePreviewMovements', - 'verbose', ]); - expect(resolved.logLevel).toBe('debug'); expect(resolved.pipeline).toEqual({ defaultBranchPrefix: 'project/', }); @@ -102,10 +92,9 @@ describe('IT: migrated config keys should prefer project over global', () => { expect(resolved.concurrency).toBe(5); expect(resolved.taskPollIntervalMs).toBe(1300); expect(resolved.interactivePreviewMovements).toBe(1); - expect(resolved.verbose).toBe(true); }); - it('should resolve migrated keys from global when project config does not set them', () => { + it('should resolve keys from global when project config does not set them', () => { writeFileSync( join(projectDir, '.takt', 'config.yaml'), '', @@ -115,7 +104,6 @@ describe('IT: migrated config keys should prefer project over global', () => { invalidateAllResolvedConfigCache(); const resolved = resolveConfigValues(projectDir, [ - 'logLevel', 'pipeline', 'personaProviders', 'branchNameStrategy', @@ -123,10 +111,8 @@ describe('IT: migrated config keys should prefer project over global', () => { 'concurrency', 'taskPollIntervalMs', 'interactivePreviewMovements', - 'verbose', ]); - expect(resolved.logLevel).toBe('info'); expect(resolved.pipeline).toEqual({ defaultBranchPrefix: 'global/' }); expect(resolved.personaProviders).toEqual({ coder: { provider: 'claude', model: 'claude-3-5-sonnet-latest' }, @@ -136,10 +122,9 @@ describe('IT: migrated config keys should prefer project over global', () => { expect(resolved.concurrency).toBe(2); expect(resolved.taskPollIntervalMs).toBe(2000); expect(resolved.interactivePreviewMovements).toBe(4); - expect(resolved.verbose).toBe(false); }); - it('should mark migrated key source as global when only global defines the key', () => { + it('should mark key source as global when only global defines the key', () => { writeFileSync( join(projectDir, '.takt', 'config.yaml'), '', @@ -148,8 +133,8 @@ describe('IT: migrated config keys should prefer project over global', () => { invalidateGlobalConfigCache(); invalidateAllResolvedConfigCache(); - expect(resolveConfigValueWithSource(projectDir, 'logLevel')).toEqual({ - value: 'info', + expect(resolveConfigValueWithSource(projectDir, 'pipeline')).toEqual({ + value: { defaultBranchPrefix: 'global/' }, source: 'global', }); }); diff --git a/src/__tests__/it-error-recovery.test.ts b/src/__tests__/it-error-recovery.test.ts index ee7a1a8..75199ba 100644 --- a/src/__tests__/it-error-recovery.test.ts +++ b/src/__tests__/it-error-recovery.test.ts @@ -42,7 +42,6 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), - loadGlobalMigratedProjectLocalFallback: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), diff --git a/src/__tests__/it-piece-execution.test.ts b/src/__tests__/it-piece-execution.test.ts index efb6d33..912fa8b 100644 --- a/src/__tests__/it-piece-execution.test.ts +++ b/src/__tests__/it-piece-execution.test.ts @@ -46,7 +46,6 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), - loadGlobalMigratedProjectLocalFallback: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); diff --git a/src/__tests__/it-three-phase-execution.test.ts b/src/__tests__/it-three-phase-execution.test.ts index 0c52612..82338a4 100644 --- a/src/__tests__/it-three-phase-execution.test.ts +++ b/src/__tests__/it-three-phase-execution.test.ts @@ -47,7 +47,6 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), - loadGlobalMigratedProjectLocalFallback: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), diff --git a/src/__tests__/opencode-config.test.ts b/src/__tests__/opencode-config.test.ts index f980e6a..4412fa8 100644 --- a/src/__tests__/opencode-config.test.ts +++ b/src/__tests__/opencode-config.test.ts @@ -16,10 +16,11 @@ describe('Schemas accept opencode provider', () => { expect(result.provider).toBe('opencode'); }); - it('should reject persona_providers in GlobalConfigSchema', () => { - expect(() => GlobalConfigSchema.parse({ + it('should accept persona_providers in GlobalConfigSchema', () => { + const result = GlobalConfigSchema.parse({ persona_providers: { coder: { provider: 'opencode' } }, - })).toThrow(); + }); + expect(result.persona_providers).toEqual({ coder: { provider: 'opencode' } }); }); it('should accept opencode_api_key in GlobalConfigSchema', () => { diff --git a/src/__tests__/projectConfig.test.ts b/src/__tests__/projectConfig.test.ts index db65610..e00cfaf 100644 --- a/src/__tests__/projectConfig.test.ts +++ b/src/__tests__/projectConfig.test.ts @@ -149,10 +149,9 @@ piece_overrides: }); describe('migrated project-local fields', () => { - it('should load migrated fields from project config yaml', () => { + it('should load project-local fields from project config yaml', () => { const configPath = join(testDir, '.takt', 'config.yaml'); const configContent = [ - 'log_level: debug', 'pipeline:', ' default_branch_prefix: "proj/"', ' commit_message_template: "feat: {title} (#{issue})"', @@ -165,12 +164,10 @@ piece_overrides: 'concurrency: 3', 'task_poll_interval_ms: 1200', 'interactive_preview_movements: 2', - 'verbose: true', ].join('\n'); writeFileSync(configPath, configContent, 'utf-8'); - const loaded = loadProjectConfig(testDir) as Record; - expect(loaded.logLevel).toBe('debug'); + const loaded = loadProjectConfig(testDir); expect(loaded.pipeline).toEqual({ defaultBranchPrefix: 'proj/', commitMessageTemplate: 'feat: {title} (#{issue})', @@ -183,12 +180,10 @@ piece_overrides: expect(loaded.concurrency).toBe(3); expect(loaded.taskPollIntervalMs).toBe(1200); expect(loaded.interactivePreviewMovements).toBe(2); - expect(loaded.verbose).toBe(true); }); - it('should save migrated fields as snake_case keys', () => { + it('should save project-local fields as snake_case keys', () => { const config = { - logLevel: 'warn', pipeline: { defaultBranchPrefix: 'task/', prBodyTemplate: 'Body {report}', @@ -201,13 +196,11 @@ piece_overrides: concurrency: 4, taskPollIntervalMs: 1500, interactivePreviewMovements: 1, - verbose: false, } as ProjectLocalConfig; saveProjectConfig(testDir, config); const raw = readFileSync(join(testDir, '.takt', 'config.yaml'), 'utf-8'); - expect(raw).toContain('log_level: warn'); expect(raw).toContain('pipeline:'); expect(raw).toContain('default_branch_prefix: task/'); expect(raw).toContain('pr_body_template: Body {report}'); @@ -218,7 +211,6 @@ piece_overrides: expect(raw).toContain('concurrency: 4'); expect(raw).toContain('task_poll_interval_ms: 1500'); expect(raw).toContain('interactive_preview_movements: 1'); - expect(raw).not.toContain('verbose: false'); }); it('should not persist empty pipeline object on save', () => { @@ -250,17 +242,15 @@ piece_overrides: expect(raw).not.toContain('personaProviders:'); }); - it('should not persist schema-injected default values on save', () => { + it('should not persist unset values on save', () => { const loaded = loadProjectConfig(testDir); saveProjectConfig(testDir, loaded); const raw = readFileSync(join(testDir, '.takt', 'config.yaml'), 'utf-8'); - expect(raw).not.toContain('log_level: info'); - expect(raw).not.toContain('minimal_output: false'); - expect(raw).not.toContain('concurrency: 1'); - expect(raw).not.toContain('task_poll_interval_ms: 500'); - expect(raw).not.toContain('interactive_preview_movements: 3'); - expect(raw).not.toContain('verbose: false'); + expect(raw).not.toContain('minimal_output:'); + expect(raw).not.toContain('concurrency:'); + expect(raw).not.toContain('task_poll_interval_ms:'); + expect(raw).not.toContain('interactive_preview_movements:'); }); it('should fail fast when project config contains global-only cli path keys', () => { diff --git a/src/__tests__/qualityGateOverrides.test.ts b/src/__tests__/qualityGateOverrides.test.ts index 41e9576..1ff7bee 100644 --- a/src/__tests__/qualityGateOverrides.test.ts +++ b/src/__tests__/qualityGateOverrides.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest'; import { applyQualityGateOverrides } from '../infra/config/loaders/qualityGateOverrides.js'; -import type { PieceOverrides } from '../core/models/persisted-global-config.js'; +import type { PieceOverrides } from '../core/models/config-types.js'; type ApplyOverridesArgs = [ string, diff --git a/src/__tests__/reset-global-config.test.ts b/src/__tests__/reset-global-config.test.ts index f93fbb0..804157c 100644 --- a/src/__tests__/reset-global-config.test.ts +++ b/src/__tests__/reset-global-config.test.ts @@ -36,14 +36,10 @@ describe('resetGlobalConfigToTemplate', () => { const newConfig = readFileSync(configPath, 'utf-8'); expect(newConfig).toContain('# TAKT グローバル設定サンプル'); expect(newConfig).toContain('language: ja'); - expect(newConfig).not.toContain('provider:'); - expect(newConfig).not.toContain('runtime:'); - expect(newConfig).not.toContain('branch_name_strategy:'); - expect(newConfig).not.toContain('concurrency:'); - expect(newConfig).not.toContain('minimal_output:'); - expect(newConfig).not.toContain('task_poll_interval_ms:'); - expect(newConfig).not.toContain('persona_providers:'); - expect(newConfig).not.toContain('pipeline:'); + // Template should only have 'language' as an active (non-commented) setting + const activeLines = newConfig.split('\n').filter(line => !line.startsWith('#') && line.trim() !== ''); + expect(activeLines.length).toBe(1); + expect(activeLines[0]).toMatch(/^language: ja/); }); it('should create config from default language template when config does not exist', () => { @@ -57,9 +53,8 @@ describe('resetGlobalConfigToTemplate', () => { const newConfig = readFileSync(configPath, 'utf-8'); expect(newConfig).toContain('# TAKT global configuration sample'); expect(newConfig).toContain('language: en'); - expect(newConfig).not.toContain('provider:'); - expect(newConfig).not.toContain('runtime:'); - expect(newConfig).not.toContain('branch_name_strategy:'); - expect(newConfig).not.toContain('concurrency:'); + const activeLines = newConfig.split('\n').filter(line => !line.startsWith('#') && line.trim() !== ''); + expect(activeLines.length).toBe(1); + expect(activeLines[0]).toMatch(/^language: en/); }); }); diff --git a/src/__tests__/resolveConfigValue-call-chain.test.ts b/src/__tests__/resolveConfigValue-call-chain.test.ts deleted file mode 100644 index d64ef1f..0000000 --- a/src/__tests__/resolveConfigValue-call-chain.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; - -describe('resolveConfigValue call-chain contract', () => { - afterEach(() => { - vi.resetModules(); - vi.doUnmock('../infra/config/global/globalConfig.js'); - vi.doUnmock('../infra/config/project/projectConfig.js'); - }); - - it('should fail fast when migrated fallback loader is missing and migrated key is resolved', async () => { - vi.doMock('../infra/config/project/projectConfig.js', () => ({ - loadProjectConfig: () => ({}), - })); - vi.doMock('../infra/config/global/globalConfig.js', () => ({ - loadGlobalConfig: () => ({ language: 'en' }), - })); - - const { resolveConfigValue } = await import('../infra/config/resolveConfigValue.js'); - - expect(() => resolveConfigValue('/tmp/takt-project', 'logLevel')).toThrow(); - }); -}); diff --git a/src/__tests__/resolveConfigValue-no-defaultValue.test.ts b/src/__tests__/resolveConfigValue-no-defaultValue.test.ts index 276efa8..a86fbd3 100644 --- a/src/__tests__/resolveConfigValue-no-defaultValue.test.ts +++ b/src/__tests__/resolveConfigValue-no-defaultValue.test.ts @@ -1,9 +1,8 @@ /** - * Tests for RESOLUTION_REGISTRY defaultValue removal. + * Tests for config resolution defaults and project-local priority. * - * Verifies that piece, verbose, and autoFetch no longer rely on - * RESOLUTION_REGISTRY defaultValue but instead use schema defaults - * or other guaranteed sources. + * Verifies that keys with PROJECT_LOCAL_DEFAULTS resolve correctly + * and that project config takes priority over global config. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -33,11 +32,9 @@ const { } = await import('../infra/config/resolveConfigValue.js'); const { invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js'); const { getProjectConfigDir } = await import('../infra/config/paths.js'); -const { MIGRATED_PROJECT_LOCAL_CONFIG_KEYS } = await import('../infra/config/migratedProjectLocalKeys.js'); -const { MIGRATED_PROJECT_LOCAL_DEFAULTS } = await import('../infra/config/migratedProjectLocalDefaults.js'); type ConfigParameterKey = import('../infra/config/resolveConfigValue.js').ConfigParameterKey; -describe('RESOLUTION_REGISTRY defaultValue removal', () => { +describe('config resolution defaults and project-local priority', () => { let projectDir: string; beforeEach(() => { @@ -57,68 +54,8 @@ describe('RESOLUTION_REGISTRY defaultValue removal', () => { } }); - describe('verbose', () => { - it('should resolve verbose to false via resolver default when not set anywhere', () => { - const value = resolveConfigValue(projectDir, 'verbose'); - expect(value).toBe(false); - }); - - it('should report source as default when verbose comes from resolver default', () => { - const result = resolveConfigValueWithSource(projectDir, 'verbose'); - expect(result.value).toBe(false); - expect(result.source).toBe('default'); - }); - - it('should resolve verbose default when project does not set it', () => { - writeFileSync(globalConfigPath, 'language: en\n', 'utf-8'); - invalidateGlobalConfigCache(); - - expect(resolveConfigValueWithSource(projectDir, 'verbose')).toEqual({ - value: false, - source: 'default', - }); - }); - - it('should resolve verbose from project config when project sets it', () => { - writeFileSync(globalConfigPath, 'language: en\n', 'utf-8'); - invalidateGlobalConfigCache(); - - const configDir = getProjectConfigDir(projectDir); - mkdirSync(configDir, { recursive: true }); - writeFileSync(join(configDir, 'config.yaml'), 'verbose: true\n'); - - const value = resolveConfigValue(projectDir, 'verbose'); - expect(value).toBe(true); - }); - }); - - describe('logLevel migration', () => { - it('should resolve logLevel from global logging.level after migration', () => { - writeFileSync( - globalConfigPath, - [ - 'language: en', - 'logging:', - ' level: warn', - ].join('\n'), - 'utf-8', - ); - invalidateGlobalConfigCache(); - - expect(resolveConfigValueWithSource(projectDir, 'logLevel')).toEqual({ - value: 'warn', - source: 'global', - }); - }); - }); - - describe('project-local priority for migrated keys', () => { + describe('project-local priority', () => { it.each([ - { - key: 'logLevel', - projectYaml: 'log_level: debug\n', - expected: 'debug', - }, { key: 'minimalOutput', projectYaml: 'minimal_output: true\n', @@ -144,11 +81,6 @@ describe('RESOLUTION_REGISTRY defaultValue removal', () => { projectYaml: 'concurrency: 3\n', expected: 3, }, - { - key: 'verbose', - projectYaml: 'verbose: true\n', - expected: true, - }, ])('should resolve $key from project config', ({ key, projectYaml, expected }) => { writeFileSync(globalConfigPath, 'language: en\n', 'utf-8'); invalidateGlobalConfigCache(); @@ -213,68 +145,48 @@ describe('RESOLUTION_REGISTRY defaultValue removal', () => { }); }); - it('should resolve migrated non-default keys as undefined when project keys are unset', () => { + it('should resolve non-default keys as undefined when project keys are unset', () => { const configDir = getProjectConfigDir(projectDir); mkdirSync(configDir, { recursive: true }); writeFileSync(join(configDir, 'config.yaml'), 'provider: claude\n', 'utf-8'); - writeFileSync( - globalConfigPath, - ['language: en'].join('\n'), - 'utf-8', - ); + writeFileSync(globalConfigPath, 'language: en\n', 'utf-8'); invalidateGlobalConfigCache(); const pipelineResult = resolveConfigValueWithSource(projectDir, 'pipeline' as ConfigParameterKey); const personaResult = resolveConfigValueWithSource(projectDir, 'personaProviders' as ConfigParameterKey); const branchStrategyResult = resolveConfigValueWithSource(projectDir, 'branchNameStrategy' as ConfigParameterKey); - expect(pipelineResult).toEqual({ - value: undefined, - source: 'default', - }); - expect(personaResult).toEqual({ - value: undefined, - source: 'default', - }); - expect(branchStrategyResult).toEqual({ - value: undefined, - source: 'default', - }); + expect(pipelineResult).toEqual({ value: undefined, source: 'default' }); + expect(personaResult).toEqual({ value: undefined, source: 'default' }); + expect(branchStrategyResult).toEqual({ value: undefined, source: 'default' }); }); - it('should resolve default-backed migrated keys from defaults when project keys are unset', () => { + it('should resolve default-backed keys from defaults when unset', () => { const configDir = getProjectConfigDir(projectDir); mkdirSync(configDir, { recursive: true }); writeFileSync(join(configDir, 'config.yaml'), 'provider: claude\n', 'utf-8'); - writeFileSync( - globalConfigPath, - ['language: en'].join('\n'), - 'utf-8', - ); + writeFileSync(globalConfigPath, 'language: en\n', 'utf-8'); invalidateGlobalConfigCache(); - expect(resolveConfigValueWithSource(projectDir, 'logLevel')).toEqual({ value: 'info', source: 'default' }); expect(resolveConfigValueWithSource(projectDir, 'minimalOutput')).toEqual({ value: false, source: 'default' }); expect(resolveConfigValueWithSource(projectDir, 'concurrency')).toEqual({ value: 1, source: 'default' }); expect(resolveConfigValueWithSource(projectDir, 'taskPollIntervalMs')).toEqual({ value: 500, source: 'default' }); expect(resolveConfigValueWithSource(projectDir, 'interactivePreviewMovements')).toEqual({ value: 3, source: 'default' }); }); - it('should resolve migrated keys from global legacy fields when project keys are unset', () => { + it('should resolve keys from global config when project keys are unset', () => { writeFileSync( globalConfigPath, [ 'language: en', - 'log_level: warn', 'pipeline:', - ' default_branch_prefix: "legacy/"', + ' default_branch_prefix: "global/"', 'persona_providers:', ' coder:', ' provider: codex', ' model: gpt-5', 'branch_name_strategy: ai', 'minimal_output: true', - 'verbose: true', 'concurrency: 3', 'task_poll_interval_ms: 1200', 'interactive_preview_movements: 2', @@ -283,9 +195,8 @@ describe('RESOLUTION_REGISTRY defaultValue removal', () => { ); invalidateGlobalConfigCache(); - expect(resolveConfigValueWithSource(projectDir, 'logLevel')).toEqual({ value: 'warn', source: 'global' }); expect(resolveConfigValueWithSource(projectDir, 'pipeline')).toEqual({ - value: { defaultBranchPrefix: 'legacy/' }, + value: { defaultBranchPrefix: 'global/' }, source: 'global', }); expect(resolveConfigValueWithSource(projectDir, 'personaProviders')).toEqual({ @@ -297,7 +208,6 @@ describe('RESOLUTION_REGISTRY defaultValue removal', () => { source: 'global', }); expect(resolveConfigValueWithSource(projectDir, 'minimalOutput')).toEqual({ value: true, source: 'global' }); - expect(resolveConfigValueWithSource(projectDir, 'verbose')).toEqual({ value: true, source: 'global' }); expect(resolveConfigValueWithSource(projectDir, 'concurrency')).toEqual({ value: 3, source: 'global' }); expect(resolveConfigValueWithSource(projectDir, 'taskPollIntervalMs')).toEqual({ value: 1200, source: 'global' }); expect(resolveConfigValueWithSource(projectDir, 'interactivePreviewMovements')).toEqual({ @@ -305,60 +215,6 @@ describe('RESOLUTION_REGISTRY defaultValue removal', () => { source: 'global', }); }); - - it('should resolve migrated numeric key from default when project key is unset', () => { - writeFileSync(globalConfigPath, 'language: en\n', 'utf-8'); - invalidateGlobalConfigCache(); - - expect(resolveConfigValueWithSource(projectDir, 'concurrency' as ConfigParameterKey)).toEqual({ - value: 1, - source: 'default', - }); - }); - - it('should resolve migrated persona_providers key from default when project key is unset', () => { - writeFileSync( - globalConfigPath, - ['language: en'].join('\n'), - 'utf-8', - ); - invalidateGlobalConfigCache(); - - expect(resolveConfigValueWithSource(projectDir, 'personaProviders' as ConfigParameterKey)).toEqual({ - value: undefined, - source: 'default', - }); - }); - - it('should resolve all migrated keys from project or defaults when project config has no migrated keys', () => { - const configDir = getProjectConfigDir(projectDir); - mkdirSync(configDir, { recursive: true }); - writeFileSync(join(configDir, 'config.yaml'), 'provider: claude\n', 'utf-8'); - writeFileSync( - globalConfigPath, - ['language: en'].join('\n'), - 'utf-8', - ); - invalidateGlobalConfigCache(); - - const expectedByKey: Partial> = { - logLevel: MIGRATED_PROJECT_LOCAL_DEFAULTS.logLevel, - pipeline: undefined, - personaProviders: undefined, - branchNameStrategy: undefined, - minimalOutput: MIGRATED_PROJECT_LOCAL_DEFAULTS.minimalOutput, - concurrency: MIGRATED_PROJECT_LOCAL_DEFAULTS.concurrency, - taskPollIntervalMs: MIGRATED_PROJECT_LOCAL_DEFAULTS.taskPollIntervalMs, - interactivePreviewMovements: MIGRATED_PROJECT_LOCAL_DEFAULTS.interactivePreviewMovements, - verbose: MIGRATED_PROJECT_LOCAL_DEFAULTS.verbose, - }; - - for (const key of MIGRATED_PROJECT_LOCAL_CONFIG_KEYS) { - const resolved = resolveConfigValueWithSource(projectDir, key); - expect(resolved.source).toBe('default'); - expect(resolved.value).toEqual(expectedByKey[key as ConfigParameterKey]); - } - }); }); describe('autoFetch', () => { diff --git a/src/app/cli/program.ts b/src/app/cli/program.ts index 7a0e10a..2257d32 100644 --- a/src/app/cli/program.ts +++ b/src/app/cli/program.ts @@ -71,13 +71,13 @@ export async function runPreActionHook(): Promise { const verbose = isVerboseMode(resolvedCwd); initDebugLogger(verbose ? { enabled: true } : undefined, resolvedCwd); - const config = resolveConfigValues(resolvedCwd, ['logLevel', 'minimalOutput']); + const config = resolveConfigValues(resolvedCwd, ['logging', 'minimalOutput']); if (verbose) { setVerboseConsole(true); setLogLevel('debug'); } else { - setLogLevel(config.logLevel); + setLogLevel(config.logging?.level ?? 'info'); } const quietMode = rootOpts.quiet === true || config.minimalOutput === true; diff --git a/src/core/models/persisted-global-config.ts b/src/core/models/config-types.ts similarity index 84% rename from src/core/models/persisted-global-config.ts rename to src/core/models/config-types.ts index f4bdfdb..f09350c 100644 --- a/src/core/models/persisted-global-config.ts +++ b/src/core/models/config-types.ts @@ -1,5 +1,10 @@ /** * Configuration types (global and project) + * + * 3-layer model: + * ProjectConfig — .takt/config.yaml (project-level) + * GlobalConfig — ~/.takt/config.yaml (user-level, superset of ProjectConfig) + * LoadedConfig — resolved values with NonNullable defaults (defined in resolvedConfig.ts) */ import type { MovementProviderOptions, PieceRuntimeConfig } from './piece-types.js'; @@ -91,27 +96,65 @@ export interface NotificationSoundEventsConfig { runAbort?: boolean; } -/** Persisted global configuration for ~/.takt/config.yaml */ -export interface PersistedGlobalConfig { - /** - * このインターフェースにはマシン/ユーザー固有の設定のみを定義する。 - * プロジェクト単位で変えたい設定は ProjectConfig に追加すること。 - * グローバル専用フィールドを追加する場合は @globalOnly を付ける。 - */ +/** + * Project-level configuration stored in .takt/config.yaml. + */ +export interface ProjectConfig { + /** Provider selection for agent runtime */ + provider?: 'claude' | 'codex' | 'opencode' | 'cursor' | 'copilot' | 'mock'; + /** Model selection for agent runtime */ + model?: string; + /** Auto-create PR after worktree execution */ + autoPr?: boolean; + /** Create PR as draft */ + draftPr?: boolean; + /** Base branch to clone from (overrides global baseBranch) */ + baseBranch?: string; + /** Submodule acquisition mode (all or explicit path list) */ + submodules?: SubmoduleSelection; + /** Compatibility flag for full submodule acquisition when submodules is unset */ + withSubmodules?: boolean; + /** Pipeline execution settings */ + pipeline?: PipelineConfig; + /** Per-persona provider/model overrides */ + personaProviders?: Record; + /** Branch name generation strategy */ + branchNameStrategy?: 'romaji' | 'ai'; + /** Minimal output mode */ + minimalOutput?: boolean; + /** Number of tasks to run concurrently in takt run (1-10) */ + concurrency?: number; + /** Polling interval in ms for task pickup */ + taskPollIntervalMs?: number; + /** Number of movement previews in interactive mode */ + interactivePreviewMovements?: number; + /** Project-level analytics overrides */ + analytics?: AnalyticsConfig; + /** Provider-specific options (overrides global, overridden by piece/movement) */ + providerOptions?: MovementProviderOptions; + /** Provider-specific permission profiles (project-level override) */ + providerProfiles?: ProviderPermissionProfiles; + /** Piece-level overrides (quality_gates, etc.) */ + pieceOverrides?: PieceOverrides; + /** Runtime environment configuration (project-level override) */ + runtime?: PieceRuntimeConfig; +} + +/** + * Global configuration persisted in ~/.takt/config.yaml. + * + * Extends ProjectConfig with global-only fields (API keys, CLI paths, etc.). + * For overlapping keys, ProjectConfig values take priority at runtime + * — handled by the resolution layer. + */ +export interface GlobalConfig extends Omit { /** @globalOnly */ language: Language; - provider?: 'claude' | 'codex' | 'opencode' | 'cursor' | 'copilot' | 'mock'; - model?: string; /** @globalOnly */ logging?: LoggingConfig; - analytics?: AnalyticsConfig; /** @globalOnly */ /** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */ worktreeDir?: string; - /** Auto-create PR after worktree execution (default: prompt in interactive mode) */ - autoPr?: boolean; - /** Create PR as draft (default: prompt in interactive mode when autoPr is true) */ - draftPr?: boolean; /** @globalOnly */ /** List of builtin piece/agent names to exclude from fallback loading */ disabledBuiltins?: string[]; @@ -163,12 +206,6 @@ export interface PersistedGlobalConfig { /** @globalOnly */ /** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */ pieceCategoriesFile?: string; - /** Global provider-specific options (lowest priority) */ - providerOptions?: MovementProviderOptions; - /** Provider-specific permission profiles */ - providerProfiles?: ProviderPermissionProfiles; - /** Global runtime environment defaults (can be overridden by piece runtime) */ - runtime?: PieceRuntimeConfig; /** @globalOnly */ /** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */ preventSleep?: boolean; @@ -181,45 +218,4 @@ export interface PersistedGlobalConfig { /** @globalOnly */ /** Opt-in: fetch remote before cloning to keep clones up-to-date (default: false) */ autoFetch: boolean; - /** Base branch to clone from (default: current branch) */ - baseBranch?: string; - /** Piece-level overrides (quality_gates, etc.) */ - pieceOverrides?: PieceOverrides; -} - -/** Project-level configuration */ -export interface ProjectConfig { - verbose?: boolean; - provider?: 'claude' | 'codex' | 'opencode' | 'cursor' | 'copilot' | 'mock'; - model?: string; - analytics?: AnalyticsConfig; - autoPr?: boolean; - draftPr?: boolean; - providerOptions?: MovementProviderOptions; - /** Provider-specific permission profiles */ - providerProfiles?: ProviderPermissionProfiles; - /** Project log level */ - logLevel?: 'debug' | 'info' | 'warn' | 'error'; - /** Pipeline execution settings */ - pipeline?: PipelineConfig; - /** Per-persona provider/model overrides */ - personaProviders?: Record; - /** Branch name generation strategy */ - branchNameStrategy?: 'romaji' | 'ai'; - /** Minimal output mode */ - minimalOutput?: boolean; - /** Number of tasks to run concurrently in takt run (1-10) */ - concurrency?: number; - /** Polling interval in ms for task pickup */ - taskPollIntervalMs?: number; - /** Number of movement previews in interactive mode */ - interactivePreviewMovements?: number; - /** Base branch to clone from (overrides global baseBranch) */ - baseBranch?: string; - /** Piece-level overrides (quality_gates, etc.) */ - pieceOverrides?: PieceOverrides; - /** Compatibility flag for full submodule acquisition when submodules is unset */ - withSubmodules?: boolean; - /** Submodule acquisition mode (all or explicit path list) */ - submodules?: SubmoduleSelection; -} +} \ No newline at end of file diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index edee62b..aef04da 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -502,83 +502,8 @@ export const PieceCategoryConfigNodeSchema: z.ZodType = export const PieceCategoryConfigSchema = z.record(z.string(), PieceCategoryConfigNodeSchema); -/** Global config schema */ -export const GlobalConfigSchema = z.object({ - language: LanguageSchema.optional().default(DEFAULT_LANGUAGE), - provider: ProviderReferenceSchema.optional().default('claude'), - model: z.string().optional(), - logging: LoggingConfigSchema.optional(), - analytics: AnalyticsConfigSchema.optional(), - /** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */ - worktree_dir: z.string().optional(), - /** Auto-create PR after worktree execution (default: prompt in interactive mode) */ - auto_pr: z.boolean().optional(), - /** Create PR as draft (default: prompt in interactive mode when auto_pr is true) */ - draft_pr: z.boolean().optional(), - /** List of builtin piece/agent names to exclude from fallback loading */ - disabled_builtins: z.array(z.string()).optional().default([]), - /** Enable builtin pieces from builtins/{lang}/pieces */ - enable_builtin_pieces: z.boolean().optional(), - /** Anthropic API key for Claude Code SDK (overridden by TAKT_ANTHROPIC_API_KEY env var) */ - anthropic_api_key: z.string().optional(), - /** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */ - openai_api_key: z.string().optional(), - /** Gemini API key (overridden by TAKT_GEMINI_API_KEY env var) */ - gemini_api_key: z.string().optional(), - /** Google API key (overridden by TAKT_GOOGLE_API_KEY env var) */ - google_api_key: z.string().optional(), - /** Groq API key (overridden by TAKT_GROQ_API_KEY env var) */ - groq_api_key: z.string().optional(), - /** OpenRouter API key (overridden by TAKT_OPENROUTER_API_KEY env var) */ - openrouter_api_key: z.string().optional(), - /** External Codex CLI path for Codex SDK override (overridden by TAKT_CODEX_CLI_PATH env var) */ - codex_cli_path: z.string().optional(), - /** External Claude Code CLI path (overridden by TAKT_CLAUDE_CLI_PATH env var) */ - claude_cli_path: z.string().optional(), - /** External cursor-agent CLI path (overridden by TAKT_CURSOR_CLI_PATH env var) */ - cursor_cli_path: z.string().optional(), - /** External Copilot CLI path (overridden by TAKT_COPILOT_CLI_PATH env var) */ - copilot_cli_path: z.string().optional(), - /** Copilot GitHub token (overridden by TAKT_COPILOT_GITHUB_TOKEN env var) */ - copilot_github_token: z.string().optional(), - /** OpenCode API key for OpenCode SDK (overridden by TAKT_OPENCODE_API_KEY env var) */ - opencode_api_key: z.string().optional(), - /** Cursor API key for Cursor Agent CLI/API (overridden by TAKT_CURSOR_API_KEY env var) */ - cursor_api_key: z.string().optional(), - /** Path to bookmarks file (default: ~/.takt/preferences/bookmarks.yaml) */ - bookmarks_file: z.string().optional(), - /** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */ - piece_categories_file: z.string().optional(), - /** Global provider-specific options (lowest priority) */ - provider_options: MovementProviderOptionsSchema, - /** Provider-specific permission profiles */ - provider_profiles: ProviderPermissionProfilesSchema, - /** Global runtime defaults (piece runtime overrides this) */ - runtime: RuntimeConfigSchema, - /** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */ - prevent_sleep: z.boolean().optional(), - /** Enable notification sounds (default: true when undefined) */ - notification_sound: z.boolean().optional(), - /** Notification sound toggles per event timing */ - notification_sound_events: z.object({ - iteration_limit: z.boolean().optional(), - piece_complete: z.boolean().optional(), - piece_abort: z.boolean().optional(), - run_complete: z.boolean().optional(), - run_abort: z.boolean().optional(), - }).optional(), - /** Opt-in: fetch remote before cloning to keep clones up-to-date (default: false) */ - auto_fetch: z.boolean().optional().default(false), - /** Base branch to clone from (default: current branch) */ - base_branch: z.string().optional(), - /** Piece-level overrides (quality_gates, etc.) */ - piece_overrides: PieceOverridesSchema, -}).strict(); - /** Project config schema */ export const ProjectConfigSchema = z.object({ - log_level: z.enum(['debug', 'info', 'warn', 'error']).optional(), - verbose: z.boolean().optional(), provider: ProviderReferenceSchema.optional(), model: z.string().optional(), analytics: AnalyticsConfigSchema.optional(), @@ -616,3 +541,71 @@ export const ProjectConfigSchema = z.object({ /** Compatibility flag for full submodule acquisition when submodules is unset */ with_submodules: z.boolean().optional(), }).strict(); + +/** Global-only fields (not in ProjectConfig) */ +const GlobalOnlyConfigSchema = z.object({ + language: LanguageSchema.optional().default(DEFAULT_LANGUAGE), + logging: LoggingConfigSchema.optional(), + /** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */ + worktree_dir: z.string().optional(), + /** List of builtin piece/agent names to exclude from fallback loading */ + disabled_builtins: z.array(z.string()).optional().default([]), + /** Enable builtin pieces from builtins/{lang}/pieces */ + enable_builtin_pieces: z.boolean().optional(), + /** Anthropic API key for Claude Code SDK (overridden by TAKT_ANTHROPIC_API_KEY env var) */ + anthropic_api_key: z.string().optional(), + /** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */ + openai_api_key: z.string().optional(), + /** Gemini API key (overridden by TAKT_GEMINI_API_KEY env var) */ + gemini_api_key: z.string().optional(), + /** Google API key (overridden by TAKT_GOOGLE_API_KEY env var) */ + google_api_key: z.string().optional(), + /** Groq API key (overridden by TAKT_GROQ_API_KEY env var) */ + groq_api_key: z.string().optional(), + /** OpenRouter API key (overridden by TAKT_OPENROUTER_API_KEY env var) */ + openrouter_api_key: z.string().optional(), + /** External Codex CLI path for Codex SDK override (overridden by TAKT_CODEX_CLI_PATH env var) */ + codex_cli_path: z.string().optional(), + /** External Claude Code CLI path (overridden by TAKT_CLAUDE_CLI_PATH env var) */ + claude_cli_path: z.string().optional(), + /** External cursor-agent CLI path (overridden by TAKT_CURSOR_CLI_PATH env var) */ + cursor_cli_path: z.string().optional(), + /** External Copilot CLI path (overridden by TAKT_COPILOT_CLI_PATH env var) */ + copilot_cli_path: z.string().optional(), + /** Copilot GitHub token (overridden by TAKT_COPILOT_GITHUB_TOKEN env var) */ + copilot_github_token: z.string().optional(), + /** OpenCode API key for OpenCode SDK (overridden by TAKT_OPENCODE_API_KEY env var) */ + opencode_api_key: z.string().optional(), + /** Cursor API key for Cursor Agent CLI/API (overridden by TAKT_CURSOR_API_KEY env var) */ + cursor_api_key: z.string().optional(), + /** Path to bookmarks file (default: ~/.takt/preferences/bookmarks.yaml) */ + bookmarks_file: z.string().optional(), + /** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */ + piece_categories_file: z.string().optional(), + /** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */ + prevent_sleep: z.boolean().optional(), + /** Enable notification sounds (default: true when undefined) */ + notification_sound: z.boolean().optional(), + /** Notification sound toggles per event timing */ + notification_sound_events: z.object({ + iteration_limit: z.boolean().optional(), + piece_complete: z.boolean().optional(), + piece_abort: z.boolean().optional(), + run_complete: z.boolean().optional(), + run_abort: z.boolean().optional(), + }).optional(), + /** Opt-in: fetch remote before cloning to keep clones up-to-date (default: false) */ + auto_fetch: z.boolean().optional().default(false), +}); + +/** Global config schema = ProjectConfig + global-only fields. + * For overlapping keys (provider, model, etc.), GlobalOnly definitions take precedence in the schema. + * Runtime value priority (project > global) is handled by the resolution layer. */ +export const GlobalConfigSchema = ProjectConfigSchema + .omit({ submodules: true, with_submodules: true }) + .merge(GlobalOnlyConfigSchema) + .extend({ + /** Override provider with default value for global config */ + provider: ProviderReferenceSchema.optional().default('claude'), + }) + .strict(); diff --git a/src/core/models/types.ts b/src/core/models/types.ts index 50eb559..d941dc1 100644 --- a/src/core/models/types.ts +++ b/src/core/models/types.ts @@ -68,4 +68,4 @@ export type { Language, PipelineConfig, ProjectConfig, -} from './persisted-global-config.js'; +} from './config-types.js'; diff --git a/src/core/piece/provider-resolution.ts b/src/core/piece/provider-resolution.ts index b6d0966..123cad5 100644 --- a/src/core/piece/provider-resolution.ts +++ b/src/core/piece/provider-resolution.ts @@ -1,5 +1,5 @@ import type { PieceMovement } from '../models/types.js'; -import type { PersonaProviderEntry } from '../models/persisted-global-config.js'; +import type { PersonaProviderEntry } from '../models/config-types.js'; import type { ProviderType } from './types.js'; export interface MovementProviderModelInput { diff --git a/src/core/piece/types.ts b/src/core/piece/types.ts index ab2bd41..3808804 100644 --- a/src/core/piece/types.ts +++ b/src/core/piece/types.ts @@ -7,7 +7,7 @@ import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'; import type { PieceMovement, AgentResponse, PieceState, Language, LoopMonitorConfig } from '../models/types.js'; -import type { PersonaProviderEntry } from '../models/persisted-global-config.js'; +import type { PersonaProviderEntry } from '../models/config-types.js'; import type { ProviderPermissionProfiles } from '../models/provider-profiles.js'; import type { MovementProviderOptions } from '../models/piece-types.js'; diff --git a/src/features/tasks/execute/types.ts b/src/features/tasks/execute/types.ts index 1508aaa..9b7fbaf 100644 --- a/src/features/tasks/execute/types.ts +++ b/src/features/tasks/execute/types.ts @@ -3,7 +3,7 @@ */ import type { Language } from '../../../core/models/index.js'; -import type { PersonaProviderEntry } from '../../../core/models/persisted-global-config.js'; +import type { PersonaProviderEntry } from '../../../core/models/config-types.js'; import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js'; import type { MovementProviderOptions } from '../../../core/models/piece-types.js'; import type { ProviderType } from '../../../infra/providers/index.js'; diff --git a/src/infra/config/configNormalizers.ts b/src/infra/config/configNormalizers.ts index 213515e..28c63cf 100644 --- a/src/infra/config/configNormalizers.ts +++ b/src/infra/config/configNormalizers.ts @@ -1,6 +1,6 @@ import type { MovementProviderOptions, PieceRuntimeConfig } from '../../core/models/piece-types.js'; import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js'; -import type { PieceOverrides, PersonaProviderEntry, PipelineConfig } from '../../core/models/persisted-global-config.js'; +import type { PieceOverrides, PersonaProviderEntry, PipelineConfig } from '../../core/models/config-types.js'; import { validateProviderModelCompatibility } from './providerModelCompatibility.js'; export function normalizeRuntime( diff --git a/src/infra/config/env/config-env-overrides.ts b/src/infra/config/env/config-env-overrides.ts index 2857b12..030a774 100644 --- a/src/infra/config/env/config-env-overrides.ts +++ b/src/infra/config/env/config-env-overrides.ts @@ -152,10 +152,8 @@ const GLOBAL_ENV_SPECS: readonly EnvSpec[] = [ ]; const PROJECT_ENV_SPECS: readonly EnvSpec[] = [ - { path: 'log_level', type: 'string' }, { path: 'provider', type: 'string' }, { path: 'model', type: 'string' }, - { path: 'verbose', type: 'boolean' }, { path: 'concurrency', type: 'number' }, { path: 'pipeline', type: 'json' }, { path: 'pipeline.default_branch_prefix', type: 'string' }, diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index 0b8310d..f10c53a 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -1,28 +1,11 @@ /** * Global configuration public API. * Keep this file as a stable facade and delegate implementations to focused modules. - * Global-only field ownership is defined in PersistedGlobalConfig via `@globalOnly` markers. */ -import type { PersistedGlobalConfig } from '../../../core/models/persisted-global-config.js'; -import type { MigratedProjectLocalConfigKey } from '../migratedProjectLocalKeys.js'; - -type Assert = T; -type IsNever = [T] extends [never] ? true : false; - -/** - * Compile-time guard: - * migrated project-local fields must not exist on PersistedGlobalConfig. - */ -const globalConfigMigratedFieldGuard: Assert< - IsNever> -> = true; -void globalConfigMigratedFieldGuard; - export { invalidateGlobalConfigCache, loadGlobalConfig, - loadGlobalMigratedProjectLocalFallback, saveGlobalConfig, validateCliPath, } from './globalConfigCore.js'; diff --git a/src/infra/config/global/globalConfigCore.ts b/src/infra/config/global/globalConfigCore.ts index 41f9fc5..7154e35 100644 --- a/src/infra/config/global/globalConfigCore.ts +++ b/src/infra/config/global/globalConfigCore.ts @@ -1,7 +1,7 @@ import { readFileSync, existsSync, writeFileSync } from 'node:fs'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { GlobalConfigSchema } from '../../../core/models/index.js'; -import type { PersistedGlobalConfig } from '../../../core/models/persisted-global-config.js'; +import type { GlobalConfig } from '../../../core/models/config-types.js'; import { normalizeConfigProviderReference, type ConfigProviderReference, @@ -9,16 +9,14 @@ import { import { normalizeProviderProfiles, normalizePieceOverrides, + normalizePipelineConfig, + normalizePersonaProviders, + normalizeRuntime, } from '../configNormalizers.js'; import { getGlobalConfigPath } from '../paths.js'; import { applyGlobalConfigEnvOverrides } from '../env/config-env-overrides.js'; import { invalidateAllResolvedConfigCache } from '../resolutionCache.js'; import { validateProviderModelCompatibility } from '../providerModelCompatibility.js'; -import { - extractMigratedProjectLocalFallback, - removeMigratedProjectLocalKeys, - type GlobalMigratedProjectLocalFallback, -} from './globalMigratedProjectLocalFallback.js'; import { sanitizeConfigValue } from './globalConfigLegacyMigration.js'; import { serializeGlobalConfig } from './globalConfigSerializer.js'; export { validateCliPath } from './cliPathValidator.js'; @@ -30,12 +28,11 @@ function getRecord(value: unknown): Record | undefined { return value as Record; } -type ProviderType = NonNullable; +type ProviderType = NonNullable; type RawProviderReference = ConfigProviderReference; export class GlobalConfigManager { private static instance: GlobalConfigManager | null = null; - private cachedConfig: PersistedGlobalConfig | null = null; - private cachedMigratedProjectLocalFallback: GlobalMigratedProjectLocalFallback | null = null; + private cachedConfig: GlobalConfig | null = null; private constructor() {} static getInstance(): GlobalConfigManager { @@ -51,10 +48,9 @@ export class GlobalConfigManager { invalidateCache(): void { this.cachedConfig = null; - this.cachedMigratedProjectLocalFallback = null; } - load(): PersistedGlobalConfig { + load(): GlobalConfig { if (this.cachedConfig !== null) { return this.cachedConfig; } @@ -78,17 +74,14 @@ export class GlobalConfigManager { } applyGlobalConfigEnvOverrides(rawConfig); - const migratedProjectLocalFallback = extractMigratedProjectLocalFallback(rawConfig); - const schemaInput = { ...rawConfig }; - removeMigratedProjectLocalKeys(schemaInput); - const parsed = GlobalConfigSchema.parse(schemaInput); + const parsed = GlobalConfigSchema.parse(rawConfig); const normalizedProvider = normalizeConfigProviderReference( parsed.provider as RawProviderReference, parsed.model, parsed.provider_options as Record | undefined, ); - const config: PersistedGlobalConfig = { + const config: GlobalConfig = { language: parsed.language, provider: normalizedProvider.provider, model: normalizedProvider.model, @@ -126,9 +119,7 @@ export class GlobalConfigManager { pieceCategoriesFile: parsed.piece_categories_file, providerOptions: normalizedProvider.providerOptions, providerProfiles: normalizeProviderProfiles(parsed.provider_profiles as Record }> | undefined), - runtime: parsed.runtime?.prepare && parsed.runtime.prepare.length > 0 - ? { prepare: [...new Set(parsed.runtime.prepare)] } - : undefined, + runtime: normalizeRuntime(parsed.runtime), preventSleep: parsed.prevent_sleep, notificationSound: parsed.notification_sound, notificationSoundEvents: parsed.notification_sound_events ? { @@ -148,22 +139,25 @@ export class GlobalConfigManager { personas?: Record; } | undefined ), + // Project-local keys (also accepted in global config) + pipeline: normalizePipelineConfig( + parsed.pipeline as { default_branch_prefix?: string; commit_message_template?: string; pr_body_template?: string } | undefined, + ), + personaProviders: normalizePersonaProviders( + parsed.persona_providers as Record | undefined, + ), + branchNameStrategy: parsed.branch_name_strategy as GlobalConfig['branchNameStrategy'], + minimalOutput: parsed.minimal_output as boolean | undefined, + concurrency: parsed.concurrency as number | undefined, + taskPollIntervalMs: parsed.task_poll_interval_ms as number | undefined, + interactivePreviewMovements: parsed.interactive_preview_movements as number | undefined, }; validateProviderModelCompatibility(config.provider, config.model); this.cachedConfig = config; - this.cachedMigratedProjectLocalFallback = migratedProjectLocalFallback; return config; } - loadMigratedProjectLocalFallback(): GlobalMigratedProjectLocalFallback { - if (this.cachedMigratedProjectLocalFallback !== null) { - return this.cachedMigratedProjectLocalFallback; - } - this.load(); - return this.cachedMigratedProjectLocalFallback ?? {}; - } - - save(config: PersistedGlobalConfig): void { + save(config: GlobalConfig): void { const configPath = getGlobalConfigPath(); const raw = serializeGlobalConfig(config); writeFileSync(configPath, stringifyYaml(raw), 'utf-8'); @@ -177,14 +171,10 @@ export function invalidateGlobalConfigCache(): void { invalidateAllResolvedConfigCache(); } -export function loadGlobalConfig(): PersistedGlobalConfig { +export function loadGlobalConfig(): GlobalConfig { return GlobalConfigManager.getInstance().load(); } -export function loadGlobalMigratedProjectLocalFallback(): GlobalMigratedProjectLocalFallback { - return GlobalConfigManager.getInstance().loadMigratedProjectLocalFallback(); -} - -export function saveGlobalConfig(config: PersistedGlobalConfig): void { +export function saveGlobalConfig(config: GlobalConfig): void { GlobalConfigManager.getInstance().save(config); } diff --git a/src/infra/config/global/globalConfigResolvers.ts b/src/infra/config/global/globalConfigResolvers.ts index da5f024..5b13e5b 100644 --- a/src/infra/config/global/globalConfigResolvers.ts +++ b/src/infra/config/global/globalConfigResolvers.ts @@ -1,4 +1,4 @@ -import type { PersistedGlobalConfig } from '../../../core/models/persisted-global-config.js'; +import type { GlobalConfig } from '../../../core/models/config-types.js'; import { envVarNameFromPath } from '../env/config-env-overrides.js'; import { loadGlobalConfig, validateCliPath } from './globalConfigCore.js'; @@ -24,7 +24,7 @@ export function resolveCodexCliPath(): string | undefined { return validateCliPath(envPath, 'TAKT_CODEX_CLI_PATH'); } - const config: PersistedGlobalConfig = loadGlobalConfig(); + const config: GlobalConfig = loadGlobalConfig(); if (config.codexCliPath === undefined) { return undefined; } @@ -37,7 +37,7 @@ export function resolveClaudeCliPath(): string | undefined { return validateCliPath(envPath, 'TAKT_CLAUDE_CLI_PATH'); } - const config: PersistedGlobalConfig = loadGlobalConfig(); + const config: GlobalConfig = loadGlobalConfig(); if (config.claudeCliPath === undefined) { return undefined; } @@ -50,7 +50,7 @@ export function resolveCursorCliPath(): string | undefined { return validateCliPath(envPath, 'TAKT_CURSOR_CLI_PATH'); } - const config: PersistedGlobalConfig = loadGlobalConfig(); + const config: GlobalConfig = loadGlobalConfig(); if (config.cursorCliPath === undefined) { return undefined; } @@ -79,7 +79,7 @@ export function resolveCopilotCliPath(): string | undefined { return validateCliPath(envPath, 'TAKT_COPILOT_CLI_PATH'); } - const config: PersistedGlobalConfig = loadGlobalConfig(); + const config: GlobalConfig = loadGlobalConfig(); if (config.copilotCliPath === undefined) { return undefined; } diff --git a/src/infra/config/global/globalConfigSerializer.ts b/src/infra/config/global/globalConfigSerializer.ts index 9a8e783..09e6d79 100644 --- a/src/infra/config/global/globalConfigSerializer.ts +++ b/src/infra/config/global/globalConfigSerializer.ts @@ -1,11 +1,11 @@ -import type { PersistedGlobalConfig } from '../../../core/models/persisted-global-config.js'; +import type { GlobalConfig } from '../../../core/models/config-types.js'; import { denormalizeProviderProfiles, denormalizePieceOverrides, denormalizeProviderOptions, } from '../configNormalizers.js'; -export function serializeGlobalConfig(config: PersistedGlobalConfig): Record { +export function serializeGlobalConfig(config: GlobalConfig): Record { const raw: Record = { language: config.language, provider: config.provider, @@ -147,5 +147,37 @@ export function serializeGlobalConfig(config: PersistedGlobalConfig): Record = {}; + if (config.pipeline.defaultBranchPrefix !== undefined) { + pipelineRaw.default_branch_prefix = config.pipeline.defaultBranchPrefix; + } + if (config.pipeline.commitMessageTemplate !== undefined) { + pipelineRaw.commit_message_template = config.pipeline.commitMessageTemplate; + } + if (config.pipeline.prBodyTemplate !== undefined) { + pipelineRaw.pr_body_template = config.pipeline.prBodyTemplate; + } + if (Object.keys(pipelineRaw).length > 0) raw.pipeline = pipelineRaw; + } + if (config.personaProviders && Object.keys(config.personaProviders).length > 0) { + raw.persona_providers = config.personaProviders; + } + if (config.branchNameStrategy !== undefined) { + raw.branch_name_strategy = config.branchNameStrategy; + } + if (config.minimalOutput !== undefined) { + raw.minimal_output = config.minimalOutput; + } + if (config.concurrency !== undefined) { + raw.concurrency = config.concurrency; + } + if (config.taskPollIntervalMs !== undefined) { + raw.task_poll_interval_ms = config.taskPollIntervalMs; + } + if (config.interactivePreviewMovements !== undefined) { + raw.interactive_preview_movements = config.interactivePreviewMovements; + } return raw; } diff --git a/src/infra/config/global/globalMigratedProjectLocalFallback.ts b/src/infra/config/global/globalMigratedProjectLocalFallback.ts deleted file mode 100644 index b96f5e9..0000000 --- a/src/infra/config/global/globalMigratedProjectLocalFallback.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ProjectConfigSchema } from '../../../core/models/index.js'; -import { - normalizePipelineConfig, - normalizePersonaProviders, -} from '../configNormalizers.js'; -import { - MIGRATED_PROJECT_LOCAL_CONFIG_METADATA, - type MigratedProjectLocalConfigKey, -} from '../migratedProjectLocalKeys.js'; -import type { ProjectLocalConfig } from '../types.js'; - -export type GlobalMigratedProjectLocalFallback = Partial< - Pick ->; - -export function removeMigratedProjectLocalKeys(config: Record): void { - for (const metadata of Object.values(MIGRATED_PROJECT_LOCAL_CONFIG_METADATA)) { - delete config[metadata.legacyGlobalYamlKey]; - } -} - -export function extractMigratedProjectLocalFallback( - rawConfig: Record, -): GlobalMigratedProjectLocalFallback { - const rawMigratedConfig: Record = {}; - for (const metadata of Object.values(MIGRATED_PROJECT_LOCAL_CONFIG_METADATA)) { - const value = rawConfig[metadata.legacyGlobalYamlKey]; - if (value !== undefined) { - rawMigratedConfig[metadata.legacyGlobalYamlKey] = value; - } - } - if (Object.keys(rawMigratedConfig).length === 0) { - return {}; - } - - const parsedMigratedConfig = ProjectConfigSchema.partial().parse(rawMigratedConfig); - const { - log_level, - pipeline, - persona_providers, - branch_name_strategy, - minimal_output, - verbose, - concurrency, - task_poll_interval_ms, - interactive_preview_movements, - } = parsedMigratedConfig; - - return { - logLevel: log_level as ProjectLocalConfig['logLevel'], - pipeline: normalizePipelineConfig( - pipeline as { - default_branch_prefix?: string; - commit_message_template?: string; - pr_body_template?: string; - } | undefined, - ), - personaProviders: normalizePersonaProviders( - persona_providers as Record | undefined, - ), - branchNameStrategy: branch_name_strategy as ProjectLocalConfig['branchNameStrategy'], - minimalOutput: minimal_output as ProjectLocalConfig['minimalOutput'], - verbose: verbose as ProjectLocalConfig['verbose'], - concurrency: concurrency as ProjectLocalConfig['concurrency'], - taskPollIntervalMs: task_poll_interval_ms as ProjectLocalConfig['taskPollIntervalMs'], - interactivePreviewMovements: interactive_preview_movements as ProjectLocalConfig['interactivePreviewMovements'], - }; -} diff --git a/src/infra/config/loaders/pieceParser.ts b/src/infra/config/loaders/pieceParser.ts index a53eb7a..1aecbe7 100644 --- a/src/infra/config/loaders/pieceParser.ts +++ b/src/infra/config/loaders/pieceParser.ts @@ -27,7 +27,7 @@ import { type RawStep = z.output; import type { MovementProviderOptions } from '../../../core/models/piece-types.js'; import { normalizeRuntime } from '../configNormalizers.js'; -import type { PieceOverrides } from '../../../core/models/persisted-global-config.js'; +import type { PieceOverrides } from '../../../core/models/config-types.js'; import { applyQualityGateOverrides } from './qualityGateOverrides.js'; import { loadProjectConfig } from '../project/projectConfig.js'; import { loadGlobalConfig } from '../global/globalConfig.js'; diff --git a/src/infra/config/loaders/qualityGateOverrides.ts b/src/infra/config/loaders/qualityGateOverrides.ts index 3972145..a0eae32 100644 --- a/src/infra/config/loaders/qualityGateOverrides.ts +++ b/src/infra/config/loaders/qualityGateOverrides.ts @@ -9,7 +9,7 @@ * Merge strategy: Additive (config gates + YAML gates) */ -import type { PieceOverrides } from '../../../core/models/persisted-global-config.js'; +import type { PieceOverrides } from '../../../core/models/config-types.js'; /** * Apply quality gate overrides to a movement. diff --git a/src/infra/config/migratedProjectLocalDefaults.ts b/src/infra/config/migratedProjectLocalDefaults.ts deleted file mode 100644 index bf7c41e..0000000 --- a/src/infra/config/migratedProjectLocalDefaults.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { LoadedConfig } from './resolvedConfig.js'; -import { - MIGRATED_PROJECT_LOCAL_CONFIG_KEYS, - MIGRATED_PROJECT_LOCAL_CONFIG_METADATA, - type MigratedProjectLocalConfigKey, -} from './migratedProjectLocalKeys.js'; - -const defaults: Record = {}; -for (const key of MIGRATED_PROJECT_LOCAL_CONFIG_KEYS) { - const metadata = MIGRATED_PROJECT_LOCAL_CONFIG_METADATA[key] as { defaultValue?: unknown }; - const defaultValue = metadata.defaultValue; - if (defaultValue !== undefined) { - defaults[key] = defaultValue; - } -} - -export const MIGRATED_PROJECT_LOCAL_DEFAULTS = - defaults as Partial>; diff --git a/src/infra/config/migratedProjectLocalKeys.ts b/src/infra/config/migratedProjectLocalKeys.ts deleted file mode 100644 index b46e77f..0000000 --- a/src/infra/config/migratedProjectLocalKeys.ts +++ /dev/null @@ -1,26 +0,0 @@ -type MigratedProjectLocalConfigMetadata = { - readonly defaultValue?: unknown; - readonly legacyGlobalYamlKey: string; -}; - -/** - * Project-local keys migrated from persisted global config. - * Keep this metadata as the single source of truth. - */ -export const MIGRATED_PROJECT_LOCAL_CONFIG_METADATA = { - logLevel: { defaultValue: 'info', legacyGlobalYamlKey: 'log_level' }, - pipeline: { legacyGlobalYamlKey: 'pipeline' }, - personaProviders: { legacyGlobalYamlKey: 'persona_providers' }, - branchNameStrategy: { legacyGlobalYamlKey: 'branch_name_strategy' }, - minimalOutput: { defaultValue: false, legacyGlobalYamlKey: 'minimal_output' }, - verbose: { defaultValue: false, legacyGlobalYamlKey: 'verbose' }, - concurrency: { defaultValue: 1, legacyGlobalYamlKey: 'concurrency' }, - taskPollIntervalMs: { defaultValue: 500, legacyGlobalYamlKey: 'task_poll_interval_ms' }, - interactivePreviewMovements: { defaultValue: 3, legacyGlobalYamlKey: 'interactive_preview_movements' }, -} as const satisfies Record; - -export type MigratedProjectLocalConfigKey = keyof typeof MIGRATED_PROJECT_LOCAL_CONFIG_METADATA; - -export const MIGRATED_PROJECT_LOCAL_CONFIG_KEYS = Object.freeze( - Object.keys(MIGRATED_PROJECT_LOCAL_CONFIG_METADATA) as MigratedProjectLocalConfigKey[], -); diff --git a/src/infra/config/project/projectConfig.ts b/src/infra/config/project/projectConfig.ts index ef9ac10..bf1c1dc 100644 --- a/src/infra/config/project/projectConfig.ts +++ b/src/infra/config/project/projectConfig.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { parse, stringify } from 'yaml'; import { ProjectConfigSchema } from '../../../core/models/index.js'; import { copyProjectResourcesToDir } from '../../resources/index.js'; -import type { ProjectLocalConfig } from '../types.js'; +import type { ProjectConfig } from '../types.js'; import { applyProjectConfigEnvOverrides } from '../env/config-env-overrides.js'; import { normalizeConfigProviderReference, @@ -19,8 +19,6 @@ import { normalizeRuntime, } from '../configNormalizers.js'; import { invalidateResolvedConfigCache } from '../resolutionCache.js'; -import { MIGRATED_PROJECT_LOCAL_DEFAULTS } from '../migratedProjectLocalDefaults.js'; -import type { MigratedProjectLocalConfigKey } from '../migratedProjectLocalKeys.js'; import { getProjectConfigDir, getProjectConfigPath } from './projectConfigPaths.js'; import { normalizeSubmodules, @@ -30,26 +28,15 @@ import { formatIssuePath, } from './projectConfigTransforms.js'; -export type { ProjectLocalConfig } from '../types.js'; +export type { ProjectConfig as ProjectLocalConfig } from '../types.js'; -type Assert = T; -type IsNever = [T] extends [never] ? true : false; - -/** - * Compile-time guard: - * migrated fields must be owned by ProjectLocalConfig. - */ -const projectLocalConfigMigratedFieldGuard: - Assert>> = true; -void projectLocalConfigMigratedFieldGuard; - -type ProviderType = NonNullable; +type ProviderType = NonNullable; type RawProviderReference = ConfigProviderReference; /** * Load project configuration from .takt/config.yaml */ -export function loadProjectConfig(projectDir: string): ProjectLocalConfig { +export function loadProjectConfig(projectDir: string): ProjectConfig { const configPath = getProjectConfigPath(projectDir); const rawConfig: Record = {}; if (existsSync(configPath)) { @@ -91,10 +78,8 @@ export function loadProjectConfig(projectDir: string): ProjectLocalConfig { provider_options, provider_profiles, analytics, - log_level, pipeline, persona_providers, - verbose, branch_name_strategy, minimal_output, concurrency, @@ -102,7 +87,6 @@ export function loadProjectConfig(projectDir: string): ProjectLocalConfig { interactive_preview_movements, piece_overrides, runtime, - ...rest } = parsedConfig; const normalizedProvider = normalizeConfigProviderReference( provider as RawProviderReference, @@ -115,21 +99,18 @@ export function loadProjectConfig(projectDir: string): ProjectLocalConfig { const normalizedPipeline = normalizePipelineConfig( pipeline as { default_branch_prefix?: string; commit_message_template?: string; pr_body_template?: string } | undefined, ); - const personaProviders = normalizePersonaProviders( + const normalizedPersonaProviders = normalizePersonaProviders( persona_providers as Record | undefined, ); return { - ...(rest as ProjectLocalConfig), - logLevel: log_level as ProjectLocalConfig['logLevel'], pipeline: normalizedPipeline, - personaProviders, - branchNameStrategy: branch_name_strategy as ProjectLocalConfig['branchNameStrategy'], + personaProviders: normalizedPersonaProviders, + branchNameStrategy: branch_name_strategy as ProjectConfig['branchNameStrategy'], minimalOutput: minimal_output as boolean | undefined, concurrency: concurrency as number | undefined, taskPollIntervalMs: task_poll_interval_ms as number | undefined, interactivePreviewMovements: interactive_preview_movements as number | undefined, - verbose: verbose as boolean | undefined, autoPr: auto_pr as boolean | undefined, draftPr: draft_pr as boolean | undefined, baseBranch: base_branch as string | undefined, @@ -155,7 +136,7 @@ export function loadProjectConfig(projectDir: string): ProjectLocalConfig { /** * Save project configuration to .takt/config.yaml */ -export function saveProjectConfig(projectDir: string, config: ProjectLocalConfig): void { +export function saveProjectConfig(projectDir: string, config: ProjectConfig): void { const configDir = getProjectConfigDir(projectDir); const configPath = getProjectConfigPath(projectDir); if (!existsSync(configDir)) { @@ -187,49 +168,15 @@ export function saveProjectConfig(projectDir: string, config: ProjectLocalConfig } delete savePayload.providerProfiles; delete savePayload.providerOptions; - delete savePayload.concurrency; - delete savePayload.verbose; if (config.autoPr !== undefined) savePayload.auto_pr = config.autoPr; if (config.draftPr !== undefined) savePayload.draft_pr = config.draftPr; if (config.baseBranch !== undefined) savePayload.base_branch = config.baseBranch; - if ( - config.logLevel !== undefined - && config.logLevel !== MIGRATED_PROJECT_LOCAL_DEFAULTS.logLevel - ) { - savePayload.log_level = config.logLevel; - } if (config.branchNameStrategy !== undefined) savePayload.branch_name_strategy = config.branchNameStrategy; - if ( - config.minimalOutput !== undefined - && config.minimalOutput !== MIGRATED_PROJECT_LOCAL_DEFAULTS.minimalOutput - ) { - savePayload.minimal_output = config.minimalOutput; - } - if ( - config.taskPollIntervalMs !== undefined - && config.taskPollIntervalMs !== MIGRATED_PROJECT_LOCAL_DEFAULTS.taskPollIntervalMs - ) { - savePayload.task_poll_interval_ms = config.taskPollIntervalMs; - } - if ( - config.interactivePreviewMovements !== undefined - && config.interactivePreviewMovements !== MIGRATED_PROJECT_LOCAL_DEFAULTS.interactivePreviewMovements - ) { - savePayload.interactive_preview_movements = config.interactivePreviewMovements; - } - if ( - config.concurrency !== undefined - && config.concurrency !== MIGRATED_PROJECT_LOCAL_DEFAULTS.concurrency - ) { - savePayload.concurrency = config.concurrency; - } - if ( - config.verbose !== undefined - && config.verbose !== MIGRATED_PROJECT_LOCAL_DEFAULTS.verbose - ) { - savePayload.verbose = config.verbose; - } + if (config.minimalOutput !== undefined) savePayload.minimal_output = config.minimalOutput; + if (config.taskPollIntervalMs !== undefined) savePayload.task_poll_interval_ms = config.taskPollIntervalMs; + if (config.interactivePreviewMovements !== undefined) savePayload.interactive_preview_movements = config.interactivePreviewMovements; + if (config.concurrency !== undefined) savePayload.concurrency = config.concurrency; delete savePayload.pipeline; if (config.pipeline) { const pipelineRaw: Record = {}; @@ -264,7 +211,6 @@ export function saveProjectConfig(projectDir: string, config: ProjectLocalConfig delete savePayload.draftPr; delete savePayload.baseBranch; delete savePayload.withSubmodules; - delete savePayload.logLevel; delete savePayload.branchNameStrategy; delete savePayload.minimalOutput; delete savePayload.taskPollIntervalMs; @@ -289,10 +235,10 @@ export function saveProjectConfig(projectDir: string, config: ProjectLocalConfig invalidateResolvedConfigCache(projectDir); } -export function updateProjectConfig( +export function updateProjectConfig( projectDir: string, key: K, - value: ProjectLocalConfig[K] + value: ProjectConfig[K] ): void { const config = loadProjectConfig(projectDir); config[key] = value; diff --git a/src/infra/config/project/projectConfigTransforms.ts b/src/infra/config/project/projectConfigTransforms.ts index 6df9399..69eef02 100644 --- a/src/infra/config/project/projectConfigTransforms.ts +++ b/src/infra/config/project/projectConfigTransforms.ts @@ -1,4 +1,4 @@ -import type { AnalyticsConfig, SubmoduleSelection } from '../../../core/models/persisted-global-config.js'; +import type { AnalyticsConfig, SubmoduleSelection } from '../../../core/models/config-types.js'; const SUBMODULES_ALL = 'all'; diff --git a/src/infra/config/project/resolvedSettings.ts b/src/infra/config/project/resolvedSettings.ts index f50343e..791ba2b 100644 --- a/src/infra/config/project/resolvedSettings.ts +++ b/src/infra/config/project/resolvedSettings.ts @@ -1,5 +1,5 @@ -import { isVerboseShortcutEnabled } from '../resolveConfigValue.js'; +import { isDebugLoggingEnabled } from '../resolveConfigValue.js'; export function isVerboseMode(projectDir: string): boolean { - return isVerboseShortcutEnabled(projectDir); + return isDebugLoggingEnabled(projectDir); } diff --git a/src/infra/config/resolveConfigValue.ts b/src/infra/config/resolveConfigValue.ts index 9d36606..361a7f8 100644 --- a/src/infra/config/resolveConfigValue.ts +++ b/src/infra/config/resolveConfigValue.ts @@ -9,11 +9,6 @@ import { setCachedResolvedValue, } from './resolutionCache.js'; import type { ConfigParameterKey, LoadedConfig } from './resolvedConfig.js'; -import { MIGRATED_PROJECT_LOCAL_DEFAULTS } from './migratedProjectLocalDefaults.js'; -import { - MIGRATED_PROJECT_LOCAL_CONFIG_KEYS, - type MigratedProjectLocalConfigKey, -} from './migratedProjectLocalKeys.js'; export type { ConfigParameterKey } from './resolvedConfig.js'; export { invalidateResolvedConfigCache, invalidateAllResolvedConfigCache } from './resolutionCache.js'; @@ -41,9 +36,14 @@ interface ResolutionRule { mergeMode?: 'analytics'; pieceValue?: (pieceContext: PieceContext | undefined) => LoadedConfig[K] | undefined; } -type GlobalMigratedProjectLocalFallback = Partial< - Pick ->; + +/** Default values for project-local keys that need NonNullable guarantees */ +const PROJECT_LOCAL_DEFAULTS: Partial> = { + minimalOutput: false, + concurrency: 1, + taskPollIntervalMs: 500, + interactivePreviewMovements: 3, +}; function loadProjectConfigCached(projectDir: string) { const cached = getCachedProjectConfig(projectDir); @@ -67,15 +67,7 @@ const PROVIDER_OPTIONS_ENV_PATHS = [ 'provider_options.claude.sandbox.excluded_commands', ] as const; -const MIGRATED_PROJECT_LOCAL_RESOLUTION_REGISTRY = Object.fromEntries( - MIGRATED_PROJECT_LOCAL_CONFIG_KEYS.map((key) => [key, { layers: ['local', 'global'] as const }]), -) as Partial<{ [K in ConfigParameterKey]: ResolutionRule }>; -const MIGRATED_PROJECT_LOCAL_CONFIG_KEY_SET = new Set( - MIGRATED_PROJECT_LOCAL_CONFIG_KEYS as ConfigParameterKey[], -); - const RESOLUTION_REGISTRY: Partial<{ [K in ConfigParameterKey]: ResolutionRule }> = { - logLevel: { layers: ['local', 'global'] }, provider: { layers: ['local', 'piece', 'global'], pieceValue: (pieceContext) => pieceContext?.provider, @@ -91,7 +83,6 @@ const RESOLUTION_REGISTRY: Partial<{ [K in ConfigParameterKey]: ResolutionRule( function getGlobalLayerValue( global: ReturnType, - globalMigratedProjectLocalFallback: GlobalMigratedProjectLocalFallback, key: K, ): LoadedConfig[K] | undefined { - if (key === 'logLevel' && global.logging?.level !== undefined) { - return global.logging.level as LoadedConfig[K]; - } - - if (isMigratedProjectLocalConfigKey(key)) { - return globalMigratedProjectLocalFallback[key] as LoadedConfig[K] | undefined; - } return global[key as keyof typeof global] as LoadedConfig[K] | undefined; } @@ -149,7 +132,6 @@ function resolveByRegistry( key: K, project: ReturnType, global: ReturnType, - globalMigratedProjectLocalFallback: GlobalMigratedProjectLocalFallback, options: ResolveConfigOptions | undefined, ): ResolvedConfigValue { const rule = (RESOLUTION_REGISTRY[key] ?? DEFAULT_RULE) as ResolutionRule; @@ -167,7 +149,7 @@ function resolveByRegistry( } else if (layer === 'piece') { value = rule.pieceValue?.(options?.pieceContext); } else { - value = getGlobalLayerValue(global, globalMigratedProjectLocalFallback, key); + value = getGlobalLayerValue(global, key); } if (value !== undefined) { if (layer === 'local') { @@ -183,7 +165,7 @@ function resolveByRegistry( } } - const fallbackDefaultValue = MIGRATED_PROJECT_LOCAL_DEFAULTS[key as keyof typeof MIGRATED_PROJECT_LOCAL_DEFAULTS]; + const fallbackDefaultValue = PROJECT_LOCAL_DEFAULTS[key]; if (fallbackDefaultValue !== undefined) { return { value: fallbackDefaultValue as LoadedConfig[K], source: 'default' }; } @@ -202,16 +184,7 @@ function resolveUncachedConfigValue( ): ResolvedConfigValue { const project = loadProjectConfigCached(projectDir); const global = globalConfigModule.loadGlobalConfig(); - const globalMigratedProjectLocalFallback = isMigratedProjectLocalConfigKey(key) - ? globalConfigModule.loadGlobalMigratedProjectLocalFallback() - : {}; - return resolveByRegistry(key, project, global, globalMigratedProjectLocalFallback, options); -} - -function isMigratedProjectLocalConfigKey( - key: ConfigParameterKey, -): key is MigratedProjectLocalConfigKey { - return MIGRATED_PROJECT_LOCAL_CONFIG_KEY_SET.has(key); + return resolveByRegistry(key, project, global, options); } export function resolveConfigValueWithSource( @@ -249,19 +222,10 @@ export function resolveConfigValues( return result; } -export function isVerboseShortcutEnabled( +export function isDebugLoggingEnabled( projectDir: string, options?: ResolveConfigOptions, ): boolean { - const verbose = resolveConfigValue(projectDir, 'verbose', options); - if (verbose === true) { - return true; - } - const logging = resolveConfigValue(projectDir, 'logging', options); - if (logging?.debug === true || logging?.trace === true) { - return true; - } - - return resolveConfigValue(projectDir, 'logLevel', options) === 'debug'; + return logging?.debug === true || logging?.trace === true || logging?.level === 'debug'; } diff --git a/src/infra/config/resolvedConfig.ts b/src/infra/config/resolvedConfig.ts index 0d0eeb4..d2a2c31 100644 --- a/src/infra/config/resolvedConfig.ts +++ b/src/infra/config/resolvedConfig.ts @@ -1,16 +1,13 @@ -import type { PersistedGlobalConfig } from '../../core/models/persisted-global-config.js'; -import type { ProjectLocalConfig } from './types.js'; -import type { MigratedProjectLocalConfigKey } from './migratedProjectLocalKeys.js'; +import type { GlobalConfig } from '../../core/models/config-types.js'; +import type { ProjectConfig } from './types.js'; export interface LoadedConfig - extends PersistedGlobalConfig, - Pick { - logLevel: NonNullable; - minimalOutput: NonNullable; - verbose: NonNullable; - concurrency: NonNullable; - taskPollIntervalMs: NonNullable; - interactivePreviewMovements: NonNullable; + extends GlobalConfig, + ProjectConfig { + minimalOutput: NonNullable; + concurrency: NonNullable; + taskPollIntervalMs: NonNullable; + interactivePreviewMovements: NonNullable; } export type ConfigParameterKey = keyof LoadedConfig; diff --git a/src/infra/config/types.ts b/src/infra/config/types.ts index 8ec543d..92fb9e6 100644 --- a/src/infra/config/types.ts +++ b/src/infra/config/types.ts @@ -1,62 +1,11 @@ /** * Config module type definitions + * + * ProjectConfig is now defined in core/models/config-types.ts. + * This file re-exports it for backward compatibility within the config module. */ -import type { MovementProviderOptions, PieceRuntimeConfig } from '../../core/models/piece-types.js'; -import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js'; -import type { - AnalyticsConfig, - PersonaProviderEntry, - PieceOverrides, - PipelineConfig, - SubmoduleSelection, -} from '../../core/models/persisted-global-config.js'; - -/** Project configuration stored in .takt/config.yaml */ -export interface ProjectLocalConfig { - /** Provider selection for agent runtime */ - provider?: 'claude' | 'codex' | 'opencode' | 'cursor' | 'copilot' | 'mock'; - /** Model selection for agent runtime */ - model?: string; - /** Auto-create PR after worktree execution */ - autoPr?: boolean; - /** Create PR as draft */ - draftPr?: boolean; - /** Base branch to clone from (overrides global baseBranch) */ - baseBranch?: string; - /** Submodule acquisition mode (all or explicit path list) */ - submodules?: SubmoduleSelection; - /** Compatibility flag for full submodule acquisition when submodules is unset */ - withSubmodules?: boolean; - /** Verbose output mode */ - verbose?: boolean; - /** Project log level */ - logLevel?: 'debug' | 'info' | 'warn' | 'error'; - /** Pipeline execution settings */ - pipeline?: PipelineConfig; - /** Per-persona provider/model overrides */ - personaProviders?: Record; - /** Branch name generation strategy */ - branchNameStrategy?: 'romaji' | 'ai'; - /** Minimal output mode */ - minimalOutput?: boolean; - /** Number of tasks to run concurrently in takt run (1-10) */ - concurrency?: number; - /** Polling interval in ms for task pickup */ - taskPollIntervalMs?: number; - /** Number of movement previews in interactive mode */ - interactivePreviewMovements?: number; - /** Project-level analytics overrides */ - analytics?: AnalyticsConfig; - /** Provider-specific options (overrides global, overridden by piece/movement) */ - providerOptions?: MovementProviderOptions; - /** Provider-specific permission profiles (project-level override) */ - providerProfiles?: ProviderPermissionProfiles; - /** Piece-level overrides (quality_gates, etc.) */ - pieceOverrides?: PieceOverrides; - /** Runtime environment configuration (project-level override) */ - runtime?: PieceRuntimeConfig; -} +export type { ProjectConfig, ProjectConfig as ProjectLocalConfig } from '../../core/models/config-types.js'; /** Persona session data for persistence */ export interface PersonaSessionData {