refactor: config 3層モデル整理 + supervisor ペルソナのファセット分離是正
Config: - PersistedGlobalConfig → GlobalConfig にリネーム、互換エイリアス削除 - persisted-global-config.ts → config-types.ts にリネーム - ProjectConfig → GlobalConfig extends Omit<ProjectConfig, ...> の継承構造に整理 - verbose/logLevel/log_level を削除(logging セクションに統一) - migration 機構(migratedProjectLocalKeys 等)を削除 Supervisor ペルソナ: - 後方互換コードの検出・その場しのぎの検出・ボーイスカウトルールを除去(review.md ポリシー / architecture.md ナレッジと重複) - ピース全体の見直しを supervise.md インストラクションに移動 takt-default-team-leader: - loop_monitor のインライン instruction_template を既存ファイル参照に変更 - implement の「判断できない」ルールを ai_review → plan に修正
This commit is contained in:
parent
ebbd1a67a9
commit
a8223d231d
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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扱い)
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
テスト実行、ビルド確認、最終承認を行ってください。
|
||||
|
||||
**ピース全体の確認:**
|
||||
1. 計画と実装結果が一致しているか
|
||||
2. 各レビュームーブメントの指摘が対応されているか
|
||||
3. タスク指示書の各要件が達成されているか
|
||||
1. レポートディレクトリ内の全レポートを確認し、ピース全体の整合性をチェックする
|
||||
- 計画と実装結果が一致しているか
|
||||
- 各レビュームーブメントの指摘が適切に対応されているか
|
||||
- タスクの本来の目的が達成されているか
|
||||
2. タスク指示書の各要件が達成されているか
|
||||
- タスク指示書から要件を1つずつ抽出する
|
||||
- 各要件について、実装されたコード(ファイル:行)を特定する
|
||||
- コードが要件を満たしていることを実際に確認する(ファイルを読む、テストを実行する)
|
||||
|
||||
@ -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修正」タスクでバックエンドのドメインモデルが構造変更されている
|
||||
- 「表示変更」タスクでビジネスロジックのフローが書き換えられている
|
||||
|
||||
### ピース全体の見直し
|
||||
|
||||
レポートディレクトリ内の全レポートを確認し、ピース全体の整合性をチェックする。
|
||||
|
||||
- 計画と実装結果が一致しているか
|
||||
- 各レビュームーブメントの指摘が適切に対応されているか
|
||||
- タスクの本来の目的が達成されているか
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 extends true> = T;
|
||||
type IsNever<T> = [T] extends [never] ? true : false;
|
||||
|
||||
const globalConfigTypeBoundaryGuard: Assert<
|
||||
IsNever<Extract<keyof PersistedGlobalConfig, MigratedProjectLocalConfigKey>>
|
||||
> = true;
|
||||
void globalConfigTypeBoundaryGuard;
|
||||
|
||||
const projectConfigTypeBoundaryGuard: Assert<
|
||||
IsNever<Exclude<MigratedProjectLocalConfigKey, keyof ProjectLocalConfig>>
|
||||
> = 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);
|
||||
});
|
||||
});
|
||||
@ -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', () => {
|
||||
|
||||
@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<typeof saveGlobalConfig>[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<string, unknown>;
|
||||
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<string, unknown>).pipeline = {
|
||||
config.pipeline = {
|
||||
defaultBranchPrefix: 'takt/',
|
||||
commitMessageTemplate: 'feat: {title} (#{issue})',
|
||||
};
|
||||
@ -298,7 +289,10 @@ describe('loadGlobalConfig', () => {
|
||||
invalidateGlobalConfigCache();
|
||||
|
||||
const reloaded = loadGlobalConfig();
|
||||
expect((reloaded as Record<string, unknown>).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<string, unknown>;
|
||||
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<string, unknown>).interactivePreviewMovements = 7;
|
||||
config.interactivePreviewMovements = 7;
|
||||
saveGlobalConfig(config);
|
||||
invalidateGlobalConfigCache();
|
||||
|
||||
const reloaded = loadGlobalConfig();
|
||||
expect((reloaded as Record<string, unknown>).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<string, unknown>).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<string, unknown>;
|
||||
expect(config).not.toHaveProperty('interactivePreviewMovements');
|
||||
const config = loadGlobalConfig();
|
||||
expect(config.interactivePreviewMovements).toBe(0);
|
||||
});
|
||||
|
||||
describe('persona_providers', () => {
|
||||
|
||||
@ -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'],
|
||||
},
|
||||
|
||||
@ -10,25 +10,20 @@ const projectDir = join(rootDir, 'project');
|
||||
|
||||
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
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',
|
||||
});
|
||||
});
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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<string, unknown>;
|
||||
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', () => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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<Record<ConfigParameterKey, unknown>> = {
|
||||
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', () => {
|
||||
|
||||
@ -71,13 +71,13 @@ export async function runPreActionHook(): Promise<void> {
|
||||
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;
|
||||
|
||||
@ -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<string, PersonaProviderEntry>;
|
||||
/** 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<ProjectConfig, 'submodules' | 'withSubmodules'> {
|
||||
/** @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<string, PersonaProviderEntry>;
|
||||
/** 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;
|
||||
}
|
||||
}
|
||||
@ -502,83 +502,8 @@ export const PieceCategoryConfigNodeSchema: z.ZodType<PieceCategoryConfigNode> =
|
||||
|
||||
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();
|
||||
|
||||
@ -68,4 +68,4 @@ export type {
|
||||
Language,
|
||||
PipelineConfig,
|
||||
ProjectConfig,
|
||||
} from './persisted-global-config.js';
|
||||
} from './config-types.js';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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(
|
||||
|
||||
2
src/infra/config/env/config-env-overrides.ts
vendored
2
src/infra/config/env/config-env-overrides.ts
vendored
@ -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' },
|
||||
|
||||
@ -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 extends true> = T;
|
||||
type IsNever<T> = [T] extends [never] ? true : false;
|
||||
|
||||
/**
|
||||
* Compile-time guard:
|
||||
* migrated project-local fields must not exist on PersistedGlobalConfig.
|
||||
*/
|
||||
const globalConfigMigratedFieldGuard: Assert<
|
||||
IsNever<Extract<keyof PersistedGlobalConfig, MigratedProjectLocalConfigKey>>
|
||||
> = true;
|
||||
void globalConfigMigratedFieldGuard;
|
||||
|
||||
export {
|
||||
invalidateGlobalConfigCache,
|
||||
loadGlobalConfig,
|
||||
loadGlobalMigratedProjectLocalFallback,
|
||||
saveGlobalConfig,
|
||||
validateCliPath,
|
||||
} from './globalConfigCore.js';
|
||||
|
||||
@ -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<string, unknown> | undefined {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
type ProviderType = NonNullable<PersistedGlobalConfig['provider']>;
|
||||
type ProviderType = NonNullable<GlobalConfig['provider']>;
|
||||
type RawProviderReference = ConfigProviderReference<ProviderType>;
|
||||
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<string, unknown> | 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<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | 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<string, { quality_gates?: string[] }>;
|
||||
} | 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<string, string | { type?: string; provider?: string; model?: string }> | 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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<string, unknown> {
|
||||
export function serializeGlobalConfig(config: GlobalConfig): Record<string, unknown> {
|
||||
const raw: Record<string, unknown> = {
|
||||
language: config.language,
|
||||
provider: config.provider,
|
||||
@ -147,5 +147,37 @@ export function serializeGlobalConfig(config: PersistedGlobalConfig): Record<str
|
||||
if (denormalizedPieceOverrides) {
|
||||
raw.piece_overrides = denormalizedPieceOverrides;
|
||||
}
|
||||
// Project-local keys (also accepted in global config)
|
||||
if (config.pipeline) {
|
||||
const pipelineRaw: Record<string, unknown> = {};
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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<ProjectLocalConfig, MigratedProjectLocalConfigKey>
|
||||
>;
|
||||
|
||||
export function removeMigratedProjectLocalKeys(config: Record<string, unknown>): void {
|
||||
for (const metadata of Object.values(MIGRATED_PROJECT_LOCAL_CONFIG_METADATA)) {
|
||||
delete config[metadata.legacyGlobalYamlKey];
|
||||
}
|
||||
}
|
||||
|
||||
export function extractMigratedProjectLocalFallback(
|
||||
rawConfig: Record<string, unknown>,
|
||||
): GlobalMigratedProjectLocalFallback {
|
||||
const rawMigratedConfig: Record<string, unknown> = {};
|
||||
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<string, string | { type?: string; provider?: string; model?: string }> | 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'],
|
||||
};
|
||||
}
|
||||
@ -27,7 +27,7 @@ import {
|
||||
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
||||
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';
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<string, unknown> = {};
|
||||
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<Pick<LoadedConfig, MigratedProjectLocalConfigKey>>;
|
||||
@ -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<string, MigratedProjectLocalConfigMetadata>;
|
||||
|
||||
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[],
|
||||
);
|
||||
@ -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 extends true> = T;
|
||||
type IsNever<T> = [T] extends [never] ? true : false;
|
||||
|
||||
/**
|
||||
* Compile-time guard:
|
||||
* migrated fields must be owned by ProjectLocalConfig.
|
||||
*/
|
||||
const projectLocalConfigMigratedFieldGuard:
|
||||
Assert<IsNever<Exclude<MigratedProjectLocalConfigKey, keyof ProjectLocalConfig>>> = true;
|
||||
void projectLocalConfigMigratedFieldGuard;
|
||||
|
||||
type ProviderType = NonNullable<ProjectLocalConfig['provider']>;
|
||||
type ProviderType = NonNullable<ProjectConfig['provider']>;
|
||||
type RawProviderReference = ConfigProviderReference<ProviderType>;
|
||||
|
||||
/**
|
||||
* 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<string, unknown> = {};
|
||||
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<string, string | { type?: string; provider?: string; model?: string }> | 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<string, unknown> = {};
|
||||
@ -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<K extends keyof ProjectLocalConfig>(
|
||||
export function updateProjectConfig<K extends keyof ProjectConfig>(
|
||||
projectDir: string,
|
||||
key: K,
|
||||
value: ProjectLocalConfig[K]
|
||||
value: ProjectConfig[K]
|
||||
): void {
|
||||
const config = loadProjectConfig(projectDir);
|
||||
config[key] = value;
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<K extends ConfigParameterKey> {
|
||||
mergeMode?: 'analytics';
|
||||
pieceValue?: (pieceContext: PieceContext | undefined) => LoadedConfig[K] | undefined;
|
||||
}
|
||||
type GlobalMigratedProjectLocalFallback = Partial<
|
||||
Pick<LoadedConfig, MigratedProjectLocalConfigKey>
|
||||
>;
|
||||
|
||||
/** Default values for project-local keys that need NonNullable guarantees */
|
||||
const PROJECT_LOCAL_DEFAULTS: Partial<Record<ConfigParameterKey, unknown>> = {
|
||||
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<K> }>;
|
||||
const MIGRATED_PROJECT_LOCAL_CONFIG_KEY_SET = new Set(
|
||||
MIGRATED_PROJECT_LOCAL_CONFIG_KEYS as ConfigParameterKey[],
|
||||
);
|
||||
|
||||
const RESOLUTION_REGISTRY: Partial<{ [K in ConfigParameterKey]: ResolutionRule<K> }> = {
|
||||
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<K
|
||||
autoPr: { layers: ['local', 'global'] },
|
||||
draftPr: { layers: ['local', 'global'] },
|
||||
analytics: { layers: ['local', 'global'], mergeMode: 'analytics' },
|
||||
...MIGRATED_PROJECT_LOCAL_RESOLUTION_REGISTRY,
|
||||
autoFetch: { layers: ['global'] },
|
||||
baseBranch: { layers: ['local', 'global'] },
|
||||
pieceOverrides: { layers: ['local', 'global'] },
|
||||
@ -132,16 +123,8 @@ function getLocalLayerValue<K extends ConfigParameterKey>(
|
||||
|
||||
function getGlobalLayerValue<K extends ConfigParameterKey>(
|
||||
global: ReturnType<typeof globalConfigModule.loadGlobalConfig>,
|
||||
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<K extends ConfigParameterKey>(
|
||||
key: K,
|
||||
project: ReturnType<typeof loadProjectConfigCached>,
|
||||
global: ReturnType<typeof globalConfigModule.loadGlobalConfig>,
|
||||
globalMigratedProjectLocalFallback: GlobalMigratedProjectLocalFallback,
|
||||
options: ResolveConfigOptions | undefined,
|
||||
): ResolvedConfigValue<K> {
|
||||
const rule = (RESOLUTION_REGISTRY[key] ?? DEFAULT_RULE) as ResolutionRule<K>;
|
||||
@ -167,7 +149,7 @@ function resolveByRegistry<K extends ConfigParameterKey>(
|
||||
} 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<K extends ConfigParameterKey>(
|
||||
}
|
||||
}
|
||||
|
||||
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<K extends ConfigParameterKey>(
|
||||
): ResolvedConfigValue<K> {
|
||||
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<K extends ConfigParameterKey>(
|
||||
@ -249,19 +222,10 @@ export function resolveConfigValues<K extends ConfigParameterKey>(
|
||||
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';
|
||||
}
|
||||
|
||||
@ -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<ProjectLocalConfig, MigratedProjectLocalConfigKey> {
|
||||
logLevel: NonNullable<ProjectLocalConfig['logLevel']>;
|
||||
minimalOutput: NonNullable<ProjectLocalConfig['minimalOutput']>;
|
||||
verbose: NonNullable<ProjectLocalConfig['verbose']>;
|
||||
concurrency: NonNullable<ProjectLocalConfig['concurrency']>;
|
||||
taskPollIntervalMs: NonNullable<ProjectLocalConfig['taskPollIntervalMs']>;
|
||||
interactivePreviewMovements: NonNullable<ProjectLocalConfig['interactivePreviewMovements']>;
|
||||
extends GlobalConfig,
|
||||
ProjectConfig {
|
||||
minimalOutput: NonNullable<ProjectConfig['minimalOutput']>;
|
||||
concurrency: NonNullable<ProjectConfig['concurrency']>;
|
||||
taskPollIntervalMs: NonNullable<ProjectConfig['taskPollIntervalMs']>;
|
||||
interactivePreviewMovements: NonNullable<ProjectConfig['interactivePreviewMovements']>;
|
||||
}
|
||||
|
||||
export type ConfigParameterKey = keyof LoadedConfig;
|
||||
|
||||
@ -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<string, PersonaProviderEntry>;
|
||||
/** 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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user