Merge pull request #459 from nrslib/release/v0.29.0

Release v0.29.0
This commit is contained in:
nrs 2026-03-04 02:08:37 +09:00 committed by GitHub
commit 54ecc38d42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 248 additions and 210 deletions

3
.gitignore vendored
View File

@ -23,6 +23,9 @@ npm-debug.log*
# Test coverage
coverage/
# E2E test results
e2e/results/
# Environment
.env
.env.local

View File

@ -6,6 +6,41 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [0.29.0] - 2026-03-04
### Added
- レビュー+修正ループピース群を追加: `review-fix`(多角レビュー)、`frontend-review-fix``backend-review-fix``dual-review-fix``dual-cqrs-review-fix``backend-cqrs-review-fix` および対応するレビュー専用ピース群を追加。コードレビューと自動修正を反復するワークフロー
- `takt-default-review-fix` ピースを追加: TAKT 自己開発向けのレビュー+修正ループワークフロー
- `quality_gates` のグローバル/プロジェクトレベルオーバーライドをサポート: `~/.takt/config.yaml` および `.takt/config.yaml``piece_overrides.quality_gates` でビルトインピースの品質ゲートを上書き可能に (#384)
- タスクの `base_branch` 設定: `takt add` 時に現在のブランチを base_branch として記録し、タスク実行時にそのブランチから分岐するよう設定可能に (#455)
- プロバイダー設定の統一: `.takt/config.yaml``provider` ブロックに `type`/`model`/プロバイダー固有オプション(`network_access` 等)をまとめて記述可能に (#457)
- ワーカープール超過時のリキュー: タスク実行がワーカー上限を超えた場合、タスクを自動的に再キューイングするよう対応 (#366)
- `--pr` インタラクティブモードで `create_issue` アクションを除外し、`save_task` 時に PR のブランチ名を `base_branch` として自動設定
- team_leader の `decomposeTask`/`requestMoreParts`/Phase 3 ステータス判定のプロバイダーイベントをロギング: `provider-events.jsonl` に記録されるようになり、デバッグ・分析が可能に
### Fixed
- `export-cc``facets/` のサブディレクトリ構造(`personas/``policies/` 等)が出力先に再現されなかった問題を修正 (#8dcb23b)
- `cc-resolve` コマンドがコンフリクト解決後にマージコミットを生成するよう修正 (#1b1f758)
- グローバル設定 (`~/.takt/config.yaml`) の `piece` フィールドがピース解決チェーンで無視されるバグを修正 (#458)
- Codex プロバイダーでプロバイダー優先のパーミッションモード解決が機能しない問題と EPERM エラーの E2E テストを追加 (#d2b48fd)
- レビューコメントがない PR で `--pr` を使用した際にエラーになる問題を修正
- `--auto-pr`/`--draft` オプションをパイプラインモード専用に制限(インタラクティブモードでの誤用を防止)
- team_leader のストリーミングでバウンダリの先行フラッシュによる断片化を修正 (#769bd87, #bddb66f)
- team_leader のエラーメッセージが空文字列になるバグを修正 (#52968ac)
- `decomposeTask`/`requestMoreParts``maxTurns` を 2 から 4 に増加(複雑なタスク分解でタイムアウトしていた問題を緩和)
- Copilot プロバイダーのクライアント実装のバグを修正 (#434)
### Internal
- E2E プロバイダー別テストをコンフィグレベル(`vitest.config.e2e.provider.ts`)で振り分けるよう変更。テストファイル内の `skip` ロジックを廃止し、JSON レポート出力を追加
- 共有ノーマライザを `configNormalizers.ts` に抽出してプロバイダー設定解析を整理
- `agent-usecases`/`schema-loader` を移動し `pieceExecution` の責務を分割
- `check:release` で全プロバイダーclaude/codex/opencodeの E2E を実行するよう変更
- CI: PR と push の重複実行を concurrency グループで抑制
- CI: feature ブランチへの push と手動実行に対応
## [0.28.1] - 2026-03-02
### Changed

View File

@ -91,8 +91,8 @@ YAMLから以下を抽出する→ references/yaml-schema.md 参照):
ピースYAMLのセクションマップ`personas:`, `policies:`, `instructions:`, `output_contracts:`, `knowledge:`)から全ファイルパスを収集する。
パスは **ピースYAMLファイルのディレクトリからの相対パス** で解決する。
例: ピースが `~/.claude/skills/takt/pieces/default.yaml` にあり、`personas:``coder: ../personas/coder.md` がある場合
→ 絶対パスは `~/.claude/skills/takt/personas/coder.md`
例: ピースが `~/.claude/skills/takt/pieces/default.yaml` にあり、`personas:``coder: ../facets/personas/coder.md` がある場合
→ 絶対パスは `~/.claude/skills/takt/facets/personas/coder.md`
重複を除いて Read で全て読み込む。読み込んだ内容はチームメイトへのプロンプト構築に使う。

View File

@ -67,9 +67,9 @@ Task tool:
3. movement の `persona: coder``personas:` セクションの `coder` キー → ファイルパス → Read で内容を取得
例: ピースが `~/.claude/skills/takt/pieces/default.yaml` の場合
- `personas.coder: ../personas/coder.md` → `~/.claude/skills/takt/personas/coder.md`
- `policies.coding: ../policies/coding.md` → `~/.claude/skills/takt/policies/coding.md`
- `instructions.plan: ../instructions/plan.md` → `~/.claude/skills/takt/instructions/plan.md`
- `personas.coder: ../facets/personas/coder.md` → `~/.claude/skills/takt/facets/personas/coder.md`
- `policies.coding: ../facets/policies/coding.md` → `~/.claude/skills/takt/facets/policies/coding.md`
- `instructions.plan: ../facets/instructions/plan.md` → `~/.claude/skills/takt/facets/instructions/plan.md`
## プロンプト構築

View File

@ -6,6 +6,41 @@
フォーマットは [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) に基づいています。
## [0.29.0] - 2026-03-04
### Added
- レビュー+修正ループピース群を追加: `review-fix`(多角レビュー)、`frontend-review-fix``backend-review-fix``dual-review-fix``dual-cqrs-review-fix``backend-cqrs-review-fix` および対応するレビュー専用ピース群を追加。コードレビューと自動修正を反復するワークフロー
- `takt-default-review-fix` ピースを追加: TAKT 自己開発向けのレビュー+修正ループワークフロー
- `quality_gates` のグローバル/プロジェクトレベルオーバーライドをサポート: `~/.takt/config.yaml` および `.takt/config.yaml``piece_overrides.quality_gates` でビルトインピースの品質ゲートを上書き可能に (#384)
- タスクの `base_branch` 設定: `takt add` 時に現在のブランチを base_branch として記録し、タスク実行時にそのブランチから分岐するよう設定可能に (#455)
- プロバイダー設定の統一: `.takt/config.yaml``provider` ブロックに `type`/`model`/プロバイダー固有オプション(`network_access` 等)をまとめて記述可能に (#457)
- ワーカープール超過時のリキュー: タスク実行がワーカー上限を超えた場合、タスクを自動的に再キューイングするよう対応 (#366)
- `--pr` インタラクティブモードで `create_issue` アクションを除外し、`save_task` 時に PR のブランチ名を `base_branch` として自動設定
- team_leader の `decomposeTask`/`requestMoreParts`/Phase 3 ステータス判定のプロバイダーイベントをロギング: `provider-events.jsonl` に記録されるようになり、デバッグ・分析が可能に
### Fixed
- `export-cc``facets/` のサブディレクトリ構造(`personas/``policies/` 等)が出力先に再現されなかった問題を修正
- `cc-resolve` コマンドがコンフリクト解決後にマージコミットを生成するよう修正
- グローバル設定 (`~/.takt/config.yaml`) の `piece` フィールドがピース解決チェーンで無視されるバグを修正 (#458)
- Codex プロバイダーでプロバイダー優先のパーミッションモード解決が機能しない問題を修正
- レビューコメントがない PR で `--pr` を使用した際にエラーになる問題を修正
- `--auto-pr`/`--draft` オプションをパイプラインモード専用に制限(インタラクティブモードでの誤用を防止)
- team_leader のストリーミングでバウンダリの先行フラッシュによる断片化を修正
- team_leader のエラーメッセージが空文字列になるバグを修正
- `decomposeTask`/`requestMoreParts``maxTurns` を 2 から 4 に増加(複雑なタスク分解でタイムアウトしていた問題を緩和)
- Copilot プロバイダーのクライアント実装のバグを修正
### Internal
- E2E プロバイダー別テストをコンフィグレベル(`vitest.config.e2e.provider.ts`)で振り分けるよう変更。テストファイル内の `skip` ロジックを廃止し、JSON レポート出力を追加
- 共有ノーマライザを `configNormalizers.ts` に抽出してプロバイダー設定解析を整理
- `agent-usecases`/`schema-loader` を移動し `pieceExecution` の責務を分割
- `check:release` で全プロバイダーclaude/codex/opencodeの E2E を実行するよう変更
- CI: PR と push の重複実行を concurrency グループで抑制
- CI: feature ブランチへの push と手動実行に対応
## [0.28.1] - 2026-03-02
### Changed

View File

@ -33,10 +33,22 @@ TAKT に同梱されているすべてのビルトイン piece と persona の
| | `dual-cqrs` | フロントエンド+バックエンド開発 piece (CQRS+ES 特化): CQRS+ES、frontend、security、QA レビューと修正ループ付き。 |
| 🏗️ インフラストラクチャ | `terraform` | Terraform IaC 開発 piece: plan → implement → 並列レビュー → 監督検証 → 修正 → 完了。 |
| 🔍 レビュー | `review` | 多角コードレビュー: PR/ブランチ/作業中の差分を自動判定し、5つの並列観点arch/security/QA/testing/requirementsからレビューして統合結果を出力。 |
| | `review-fix` | 多角レビュー修正ループarchitecture/security/QA/testing/requirements — 5並列レビュー反復修正。 |
| | `frontend-review` | フロントエンド特化レビュー構造、モジュール化、コンポーネント設計、セキュリティ、QA。 |
| | `frontend-review-fix` | フロントエンド特化レビュー修正ループ構造、モジュール化、コンポーネント設計、セキュリティ、QA。 |
| | `backend-review` | バックエンド特化レビュー構造、モジュール化、ヘキサゴナルアーキテクチャ、セキュリティ、QA。 |
| | `backend-review-fix` | バックエンド特化レビュー修正ループ構造、モジュール化、ヘキサゴナルアーキテクチャ、セキュリティ、QA。 |
| | `dual-review` | フロントエンドバックエンド特化レビュー構造、モジュール化、コンポーネント設計、セキュリティ、QA。 |
| | `dual-review-fix` | フロントエンドバックエンド特化レビュー修正ループ構造、モジュール化、コンポーネント設計、セキュリティ、QA。 |
| | `dual-cqrs-review` | フロントエンドCQRS+ES 特化レビュー構造、モジュール化、ドメインモデル、コンポーネント設計、セキュリティ、QA。 |
| | `dual-cqrs-review-fix` | フロントエンドCQRS+ES 特化レビュー修正ループ構造、モジュール化、ドメインモデル、コンポーネント設計、セキュリティ、QA。 |
| | `backend-cqrs-review` | CQRS+ES 特化レビュー構造、モジュール化、ドメインモデル、セキュリティ、QA。 |
| | `backend-cqrs-review-fix` | CQRS+ES 特化レビュー修正ループ構造、モジュール化、ドメインモデル、セキュリティ、QA。 |
| 🧪 テスト | `unit-test` | ユニットテスト特化 piece: テスト分析 -> テスト実装 -> レビュー -> 修正。 |
| | `e2e-test` | E2E テスト特化 piece: E2E 分析 -> E2E 実装 -> レビュー -> 修正 (Vitest ベースの E2E フロー)。 |
| 🎵 TAKT開発 | `takt-default` | TAKT 開発 piece: 計画 → テスト作成 → 実装 → AIアンチパターンレビュー → 5並列レビュー → 修正 → 監督 → 完了。 |
| | `takt-default-team-leader` | TAKT 開発 pieceチームリーダー版: 計画 → テスト作成 → チームリーダー実装 → AIアンチパターンレビュー → 5並列レビュー → 修正 → 監督 → 完了。 |
| | `takt-default-review-fix` | TAKT 開発コードレビュー修正ループ5並列レビュー: architecture/security/QA/testing/requirements — 反復修正付き)。 |
| その他 | `research` | リサーチ piece: planner -> digger -> supervisor。質問せずに自律的にリサーチを実行。 |
| | `deep-research` | ディープリサーチ piece: plan -> dig -> analyze -> supervise。発見駆動型の調査で、浮上した疑問を多角的に分析。 |
| | `magi` | エヴァンゲリオンにインスパイアされた合議システム。3つの AI persona (MELCHIOR, BALTHASAR, CASPER) が分析・投票。 |

View File

@ -33,10 +33,22 @@ Organized by category.
| | `dual-cqrs` | Frontend + backend development piece (CQRS+ES specialized): CQRS+ES, frontend, security, QA reviews with fix loops. |
| 🏗️ Infrastructure | `terraform` | Terraform IaC development piece: plan → implement → parallel review → supervisor validation → fix → complete. |
| 🔍 Review | `review` | Multi-perspective code review: auto-detects PR/branch/working diff, reviews from 5 parallel perspectives (arch/security/QA/testing/requirements), outputs consolidated results. |
| | `review-fix` | Multi-perspective review + fix loop (architecture, security, QA, testing, requirements — 5 parallel reviewers with iterative fixes). |
| | `frontend-review` | Frontend-focused review (structure, modularization, component design, security, QA). |
| | `frontend-review-fix` | Frontend-focused review + fix loop (structure, modularization, component design, security, QA). |
| | `backend-review` | Backend-focused review (structure, modularization, hexagonal architecture, security, QA). |
| | `backend-review-fix` | Backend-focused review + fix loop (structure, modularization, hexagonal architecture, security, QA). |
| | `dual-review` | Frontend + backend focused review (structure, modularization, component design, security, QA). |
| | `dual-review-fix` | Frontend + backend focused review + fix loop (structure, modularization, component design, security, QA). |
| | `dual-cqrs-review` | Frontend + CQRS+ES focused review (structure, modularization, domain model, component design, security, QA). |
| | `dual-cqrs-review-fix` | Frontend + CQRS+ES focused review + fix loop (structure, modularization, domain model, component design, security, QA). |
| | `backend-cqrs-review` | CQRS+ES focused review (structure, modularization, domain model, security, QA). |
| | `backend-cqrs-review-fix` | CQRS+ES focused review + fix loop (structure, modularization, domain model, security, QA). |
| 🧪 Testing | `unit-test` | Unit test focused piece: test analysis -> test implementation -> review -> fix. |
| | `e2e-test` | E2E test focused piece: E2E analysis -> E2E implementation -> review -> fix (Vitest-based E2E flow). |
| 🎵 TAKT Development | `takt-default` | TAKT development piece: plan → write tests → implement → AI antipattern review → 5-parallel review → fix → supervise → complete. |
| | `takt-default-team-leader` | TAKT development piece with team leader: plan → write tests → team-leader implement → AI antipattern review → 5-parallel review → fix → supervise → complete. |
| | `takt-default-review-fix` | TAKT development code review + fix loop (5 parallel reviewers: architecture, security, QA, testing, requirements — with iterative fixes). |
| Others | `research` | Research piece: planner -> digger -> supervisor. Autonomously executes research without asking questions. |
| | `deep-research` | Deep research piece: plan -> dig -> analyze -> supervise. Discovery-driven investigation that follows emerging questions with multi-perspective analysis. |
| | `magi` | Deliberation system inspired by Evangelion. Three AI personas (MELCHIOR, BALTHASAR, CASPER) analyze and vote. |

View File

@ -64,7 +64,7 @@ describe('E2E: Export-cc command (takt export-cc)', () => {
const pieceFiles = readdirSync(piecesDir);
expect(pieceFiles.length).toBeGreaterThan(0);
const personasDir = join(skillDir, 'personas');
const personasDir = join(skillDir, 'facets', 'personas');
expect(existsSync(personasDir)).toBe(true);
const personaFiles = readdirSync(personasDir);
expect(personaFiles.length).toBeGreaterThan(0);

View File

@ -5,9 +5,6 @@ import { createIsolatedEnv, type IsolatedEnv, updateIsolatedConfig } from '../he
import { createLocalRepo, type LocalRepo } from '../helpers/test-repo';
import { runTakt } from '../helpers/takt-runner';
const provider = process.env.TAKT_E2E_PROVIDER;
const codexIt = provider === 'codex' ? it : it.skip;
describe('E2E: Codex permission mode readonly/full', () => {
let isolatedEnv: IsolatedEnv;
let repo: LocalRepo;
@ -48,7 +45,7 @@ describe('E2E: Codex permission mode readonly/full', () => {
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
});
codexIt('readonly で失敗し full で成功する', () => {
it('readonly で失敗し full で成功する', () => {
updateIsolatedConfig(isolatedEnv.taktDir, {
provider_profiles: {
codex: { default_permission_mode: 'readonly' },

View File

@ -1,31 +1,10 @@
/**
* OpenCode real E2E conversation test.
*
* Tests the full stack with a real OpenCode server:
* OpenCodeProvider callOpenCode OpenCodeClient createOpencode (real server)
*
* Skipped automatically if the opencode binary is not found.
* Run with: npm run test:e2e:opencode
*/
import { describe, it, expect, afterAll } from 'vitest';
import { execSync } from 'node:child_process';
import { resetSharedServer } from '../../src/infra/opencode/client.js';
import { OpenCodeProvider } from '../../src/infra/providers/opencode.js';
function isOpencodeAvailable(): boolean {
try {
execSync('which opencode', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
const MODEL = process.env.OPENCODE_E2E_MODEL ?? 'minimax/MiniMax-M2.5-highspeed';
const enabled = isOpencodeAvailable() && process.env.TAKT_E2E_PROVIDER === 'opencode';
describe.skipIf(!enabled)('OpenCode real E2E conversation', () => {
describe('OpenCode real E2E conversation', () => {
afterAll(() => {
resetSharedServer();
});

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "takt",
"version": "0.28.1",
"version": "0.29.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "takt",
"version": "0.28.1",
"version": "0.29.0",
"license": "MIT",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.47",

View File

@ -1,6 +1,6 @@
{
"name": "takt",
"version": "0.28.1",
"version": "0.29.0",
"description": "TAKT: TAKT Agent Koordination Topology - AI Agent Piece Orchestration",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -15,13 +15,13 @@
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "tmp=\"$(mktemp -t takt-e2e.XXXXXX)\"; npm run test:e2e:mock >\"$tmp\" 2>&1; code=$?; cat \"$tmp\"; if grep -q \"error connecting to api.github.com\" \"$tmp\"; then echo \"[takt] GitHub connectivity error detected in E2E output\"; code=1; fi; rm -f \"$tmp\"; if [ \"$code\" -eq 0 ]; then msg='test:e2e passed'; else msg=\"test:e2e failed (exit=$code)\"; fi; if command -v osascript >/dev/null 2>&1; then osascript -e \"display notification \\\"$msg\\\" with title \\\"takt\\\" subtitle \\\"E2E\\\"\" >/dev/null 2>&1 || true; fi; echo \"[takt] $msg\"; exit $code",
"test:e2e:mock": "TAKT_E2E_PROVIDER=mock vitest run --config vitest.config.e2e.mock.ts --reporter=verbose",
"test:e2e:mock": "TAKT_E2E_PROVIDER=mock vitest run --config vitest.config.e2e.mock.ts --outputFile.json=e2e/results/mock.json",
"test:e2e:all": "npm run test:e2e:mock && npm run test:e2e:provider",
"test:e2e:provider": "npm run test:e2e:provider:claude && npm run test:e2e:provider:codex",
"test:e2e:provider:claude": "TAKT_E2E_PROVIDER=claude vitest run --config vitest.config.e2e.provider.ts --reporter=verbose",
"test:e2e:provider:codex": "TAKT_E2E_PROVIDER=codex vitest run --config vitest.config.e2e.provider.ts --reporter=verbose",
"test:e2e:provider:opencode": "TAKT_E2E_PROVIDER=opencode vitest run --config vitest.config.e2e.provider.ts --reporter=verbose",
"test:e2e:provider:cursor": "TAKT_AUTO_PR=false TAKT_E2E_PROVIDER=cursor vitest run --config vitest.config.e2e.cursor.ts --reporter=verbose",
"test:e2e:provider:claude": "TAKT_E2E_PROVIDER=claude vitest run --config vitest.config.e2e.provider.ts --outputFile.json=e2e/results/claude.json",
"test:e2e:provider:codex": "TAKT_E2E_PROVIDER=codex vitest run --config vitest.config.e2e.provider.ts --outputFile.json=e2e/results/codex.json",
"test:e2e:provider:opencode": "TAKT_E2E_PROVIDER=opencode vitest run --config vitest.config.e2e.provider.ts --outputFile.json=e2e/results/opencode.json",
"test:e2e:provider:cursor": "TAKT_AUTO_PR=false TAKT_E2E_PROVIDER=cursor vitest run --config vitest.config.e2e.cursor.ts --outputFile.json=e2e/results/cursor.json",
"test:e2e:claude": "npm run test:e2e:provider:claude",
"test:e2e:codex": "npm run test:e2e:provider:codex",
"test:e2e:opencode": "npm run test:e2e:provider:opencode",

View File

@ -132,11 +132,11 @@ describe('deploySkill', () => {
// Verify each resource directory is copied
expect(existsSync(join(skillDir, 'pieces', 'default.yaml'))).toBe(true);
expect(existsSync(join(skillDir, 'personas', 'coder.md'))).toBe(true);
expect(existsSync(join(skillDir, 'policies', 'coding.md'))).toBe(true);
expect(existsSync(join(skillDir, 'instructions', 'init.md'))).toBe(true);
expect(existsSync(join(skillDir, 'knowledge', 'patterns.md'))).toBe(true);
expect(existsSync(join(skillDir, 'output-contracts', 'summary.md'))).toBe(true);
expect(existsSync(join(skillDir, 'facets', 'personas', 'coder.md'))).toBe(true);
expect(existsSync(join(skillDir, 'facets', 'policies', 'coding.md'))).toBe(true);
expect(existsSync(join(skillDir, 'facets', 'instructions', 'init.md'))).toBe(true);
expect(existsSync(join(skillDir, 'facets', 'knowledge', 'patterns.md'))).toBe(true);
expect(existsSync(join(skillDir, 'facets', 'output-contracts', 'summary.md'))).toBe(true);
expect(existsSync(join(skillDir, 'templates', 'task.md'))).toBe(true);
});
});

View File

@ -39,8 +39,6 @@ const DIRECT_DIRS = ['pieces', 'templates'] as const;
/** Facet directories under builtins/{lang}/facets/ */
const FACET_DIRS = ['personas', 'policies', 'instructions', 'knowledge', 'output-contracts'] as const;
/** All resource directory names (used for summary filtering) */
const RESOURCE_DIRS = [...DIRECT_DIRS, ...FACET_DIRS] as const;
/**
* Deploy takt skill to Claude Code (~/.claude/).
@ -95,11 +93,12 @@ export async function deploySkill(): Promise<void> {
copyDirRecursive(srcDir, destDir, copiedFiles);
}
// 4. Deploy facet directories from builtins/{lang}/facets/
// 4. Deploy facet directories from builtins/{lang}/facets/ (preserving facets/ structure)
const facetsDestDir = join(skillDir, 'facets');
cleanDir(facetsDestDir);
for (const dir of FACET_DIRS) {
const srcDir = join(langResourcesDir, 'facets', dir);
const destDir = join(skillDir, dir);
cleanDir(destDir);
const destDir = join(facetsDestDir, dir);
copyDirRecursive(srcDir, destDir, copiedFiles);
}
@ -114,14 +113,17 @@ export async function deploySkill(): Promise<void> {
const skillFiles = copiedFiles.filter(
(f) =>
f.startsWith(skillDir) &&
!RESOURCE_DIRS.some((dir) => f.includes(`/${dir}/`)),
!f.includes('/pieces/') &&
!f.includes('/facets/') &&
!f.includes('/templates/') &&
!f.includes('/references/'),
);
const pieceFiles = copiedFiles.filter((f) => f.includes('/pieces/'));
const personaFiles = copiedFiles.filter((f) => f.includes('/personas/'));
const policyFiles = copiedFiles.filter((f) => f.includes('/policies/'));
const instructionFiles = copiedFiles.filter((f) => f.includes('/instructions/'));
const knowledgeFiles = copiedFiles.filter((f) => f.includes('/knowledge/'));
const outputContractFiles = copiedFiles.filter((f) => f.includes('/output-contracts/'));
const personaFiles = copiedFiles.filter((f) => f.includes('/facets/personas/'));
const policyFiles = copiedFiles.filter((f) => f.includes('/facets/policies/'));
const instructionFiles = copiedFiles.filter((f) => f.includes('/facets/instructions/'));
const knowledgeFiles = copiedFiles.filter((f) => f.includes('/facets/knowledge/'));
const outputContractFiles = copiedFiles.filter((f) => f.includes('/facets/output-contracts/'));
const templateFiles = copiedFiles.filter((f) => f.includes('/templates/'));
if (skillFiles.length > 0) {

View File

@ -0,0 +1,79 @@
/**
* Shared normalizer/denormalizer functions for config snake_case <-> camelCase conversion.
*
* Used by both globalConfig.ts and projectConfig.ts.
*/
import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js';
import type { PieceOverrides } from '../../core/models/persisted-global-config.js';
export function normalizeProviderProfiles(
raw: Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined,
): ProviderPermissionProfiles | undefined {
if (!raw) return undefined;
const entries = Object.entries(raw).map(([provider, profile]) => [provider, {
defaultPermissionMode: profile.default_permission_mode,
movementPermissionOverrides: profile.movement_permission_overrides,
}]);
return Object.fromEntries(entries) as ProviderPermissionProfiles;
}
export function denormalizeProviderProfiles(
profiles: ProviderPermissionProfiles | undefined,
): Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }> | undefined {
if (!profiles) return undefined;
const entries = Object.entries(profiles);
if (entries.length === 0) return undefined;
return Object.fromEntries(entries.map(([provider, profile]) => [provider, {
default_permission_mode: profile.defaultPermissionMode,
...(profile.movementPermissionOverrides
? { movement_permission_overrides: profile.movementPermissionOverrides }
: {}),
}])) as Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
}
export function normalizePieceOverrides(
raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | undefined,
): PieceOverrides | undefined {
if (!raw) return undefined;
return {
qualityGates: raw.quality_gates,
qualityGatesEditOnly: raw.quality_gates_edit_only,
movements: raw.movements
? Object.fromEntries(
Object.entries(raw.movements).map(([name, override]) => [
name,
{ qualityGates: override.quality_gates },
])
)
: undefined,
};
}
export function denormalizePieceOverrides(
overrides: PieceOverrides | undefined,
): { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | undefined {
if (!overrides) return undefined;
const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } = {};
if (overrides.qualityGates !== undefined) {
result.quality_gates = overrides.qualityGates;
}
if (overrides.qualityGatesEditOnly !== undefined) {
result.quality_gates_edit_only = overrides.qualityGatesEditOnly;
}
if (overrides.movements) {
result.movements = Object.fromEntries(
Object.entries(overrides.movements).map(([name, override]) => {
const movementOverride: { quality_gates?: string[] } = {};
if (override.qualityGates !== undefined) {
movementOverride.quality_gates = override.qualityGates;
}
return [name, movementOverride];
})
);
}
return Object.keys(result).length > 0 ? result : undefined;
}

View File

@ -10,12 +10,17 @@ import { isAbsolute } from 'node:path';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { GlobalConfigSchema } from '../../../core/models/index.js';
import type { Language } from '../../../core/models/index.js';
import type { PersistedGlobalConfig, PersonaProviderEntry, PieceOverrides } from '../../../core/models/persisted-global-config.js';
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
import type { PersistedGlobalConfig, PersonaProviderEntry } from '../../../core/models/persisted-global-config.js';
import {
normalizeConfigProviderReference,
type ConfigProviderReference,
} from '../providerReference.js';
import {
normalizeProviderProfiles,
denormalizeProviderProfiles,
normalizePieceOverrides,
denormalizePieceOverrides,
} from '../configNormalizers.js';
import { getGlobalConfigPath } from '../paths.js';
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
@ -112,79 +117,6 @@ function normalizePersonaProviders(
);
}
function normalizeProviderProfiles(
raw: Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined,
): ProviderPermissionProfiles | undefined {
if (!raw) return undefined;
const entries = Object.entries(raw).map(([provider, profile]) => [provider, {
defaultPermissionMode: profile.default_permission_mode,
movementPermissionOverrides: profile.movement_permission_overrides,
}]);
return Object.fromEntries(entries) as ProviderPermissionProfiles;
}
function denormalizeProviderProfiles(
profiles: ProviderPermissionProfiles | undefined,
): Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }> | undefined {
if (!profiles) return undefined;
const entries = Object.entries(profiles);
if (entries.length === 0) return undefined;
return Object.fromEntries(entries.map(([provider, profile]) => [provider, {
default_permission_mode: profile.defaultPermissionMode,
...(profile.movementPermissionOverrides
? { movement_permission_overrides: profile.movementPermissionOverrides }
: {}),
}])) as Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
}
/** Normalize piece_overrides from snake_case (YAML) to camelCase (internal) */
function normalizePieceOverrides(
raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | undefined,
): PieceOverrides | undefined {
if (!raw) return undefined;
return {
qualityGates: raw.quality_gates,
qualityGatesEditOnly: raw.quality_gates_edit_only,
movements: raw.movements
? Object.fromEntries(
Object.entries(raw.movements).map(([name, override]) => [
name,
{ qualityGates: override.quality_gates },
])
)
: undefined,
};
}
/** Denormalize piece_overrides from camelCase (internal) to snake_case (YAML) */
function denormalizePieceOverrides(
overrides: PieceOverrides | undefined,
): { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | undefined {
if (!overrides) return undefined;
const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } = {};
if (overrides.qualityGates !== undefined) {
result.quality_gates = overrides.qualityGates;
}
if (overrides.qualityGatesEditOnly !== undefined) {
result.quality_gates_edit_only = overrides.qualityGatesEditOnly;
}
if (overrides.movements) {
result.movements = Object.fromEntries(
Object.entries(overrides.movements).map(([name, override]) => {
const movementOverride: { quality_gates?: string[] } = {};
if (override.qualityGates !== undefined) {
movementOverride.quality_gates = override.qualityGates;
}
return [name, movementOverride];
})
);
}
return Object.keys(result).length > 0 ? result : undefined;
}
/**
* Manages global configuration loading and caching.
* Singleton use GlobalConfigManager.getInstance().

View File

@ -10,13 +10,18 @@ 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 { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
import type { AnalyticsConfig, PieceOverrides, SubmoduleSelection } from '../../../core/models/persisted-global-config.js';
import type { AnalyticsConfig, SubmoduleSelection } from '../../../core/models/persisted-global-config.js';
import { applyProjectConfigEnvOverrides } from '../env/config-env-overrides.js';
import {
normalizeConfigProviderReference,
type ConfigProviderReference,
} from '../providerReference.js';
import {
normalizeProviderProfiles,
denormalizeProviderProfiles,
normalizePieceOverrides,
denormalizePieceOverrides,
} from '../configNormalizers.js';
import { invalidateResolvedConfigCache } from '../resolutionCache.js';
export type { ProjectLocalConfig } from '../types.js';
@ -86,28 +91,6 @@ function getConfigPath(projectDir: string): string {
return join(getConfigDir(projectDir), 'config.yaml');
}
function normalizeProviderProfiles(raw: Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined): ProviderPermissionProfiles | undefined {
if (!raw) return undefined;
return Object.fromEntries(
Object.entries(raw).map(([provider, profile]) => [provider, {
defaultPermissionMode: profile.default_permission_mode,
movementPermissionOverrides: profile.movement_permission_overrides,
}]),
) as ProviderPermissionProfiles;
}
function denormalizeProviderProfiles(profiles: ProviderPermissionProfiles | undefined): Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }> | undefined {
if (!profiles) return undefined;
const entries = Object.entries(profiles);
if (entries.length === 0) return undefined;
return Object.fromEntries(entries.map(([provider, profile]) => [provider, {
default_permission_mode: profile.defaultPermissionMode,
...(profile.movementPermissionOverrides
? { movement_permission_overrides: profile.movementPermissionOverrides }
: {}),
}])) as Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
}
function normalizeAnalytics(raw: Record<string, unknown> | undefined): AnalyticsConfig | undefined {
if (!raw) return undefined;
const enabled = typeof raw.enabled === 'boolean' ? raw.enabled : undefined;
@ -133,51 +116,6 @@ function denormalizeAnalytics(config: AnalyticsConfig | undefined): Record<strin
return Object.keys(raw).length > 0 ? raw : undefined;
}
/** Normalize piece_overrides from snake_case (YAML) to camelCase (internal) */
function normalizePieceOverrides(
raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | undefined,
): PieceOverrides | undefined {
if (!raw) return undefined;
return {
qualityGates: raw.quality_gates,
qualityGatesEditOnly: raw.quality_gates_edit_only,
movements: raw.movements
? Object.fromEntries(
Object.entries(raw.movements).map(([name, override]) => [
name,
{ qualityGates: override.quality_gates },
])
)
: undefined,
};
}
/** Denormalize piece_overrides from camelCase (internal) to snake_case (YAML) */
function denormalizePieceOverrides(
overrides: PieceOverrides | undefined,
): { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | undefined {
if (!overrides) return undefined;
const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } = {};
if (overrides.qualityGates !== undefined) {
result.quality_gates = overrides.qualityGates;
}
if (overrides.qualityGatesEditOnly !== undefined) {
result.quality_gates_edit_only = overrides.qualityGatesEditOnly;
}
if (overrides.movements) {
result.movements = Object.fromEntries(
Object.entries(overrides.movements).map(([name, override]) => {
const movementOverride: { quality_gates?: string[] } = {};
if (override.qualityGates !== undefined) {
movementOverride.quality_gates = override.qualityGates;
}
return [name, movementOverride];
})
);
}
return Object.keys(result).length > 0 ? result : undefined;
}
/**
* Load project configuration from .takt/config.yaml
*/

View File

@ -13,4 +13,5 @@ export const e2eBaseTestConfig: UserConfig['test'] = {
singleThread: true,
},
},
reporters: ['verbose', 'json'],
};

View File

@ -1,20 +1,33 @@
import { defineConfig } from 'vitest/config';
import { e2eBaseTestConfig } from './vitest.config.e2e.base';
export default defineConfig({
test: {
...e2eBaseTestConfig,
include: [
const provider = process.env.TAKT_E2E_PROVIDER;
if (!provider) {
throw new Error('TAKT_E2E_PROVIDER must be set');
}
const commonTests = [
'e2e/specs/add-and-run.e2e.ts',
'e2e/specs/worktree.e2e.ts',
'e2e/specs/pipeline.e2e.ts',
'e2e/specs/github-issue.e2e.ts',
'e2e/specs/structured-output.e2e.ts',
'e2e/specs/codex-permission-mode.e2e.ts',
'e2e/specs/opencode-conversation.e2e.ts',
'e2e/specs/team-leader.e2e.ts',
'e2e/specs/team-leader-worker-pool.e2e.ts',
'e2e/specs/team-leader-refill-threshold.e2e.ts',
];
const providerSpecificTests: Record<string, string[]> = {
codex: ['e2e/specs/codex-permission-mode.e2e.ts'],
opencode: ['e2e/specs/opencode-conversation.e2e.ts'],
};
export default defineConfig({
test: {
...e2eBaseTestConfig,
include: [
...commonTests,
...(providerSpecificTests[provider] ?? []),
],
},
});