diff --git a/.gitignore b/.gitignore index a93ef3b..253b2c8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ npm-debug.log* # Test coverage coverage/ +# E2E test results +e2e/results/ + # Environment .env .env.local diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aca091..8be5952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/builtins/skill/SKILL.md b/builtins/skill/SKILL.md index 0072aad..d7317cb 100644 --- a/builtins/skill/SKILL.md +++ b/builtins/skill/SKILL.md @@ -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 で全て読み込む。読み込んだ内容はチームメイトへのプロンプト構築に使う。 diff --git a/builtins/skill/references/engine.md b/builtins/skill/references/engine.md index 1782aa1..ca1a8ae 100644 --- a/builtins/skill/references/engine.md +++ b/builtins/skill/references/engine.md @@ -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` ## プロンプト構築 diff --git a/docs/CHANGELOG.ja.md b/docs/CHANGELOG.ja.md index 7b3c2dc..cd12892 100644 --- a/docs/CHANGELOG.ja.md +++ b/docs/CHANGELOG.ja.md @@ -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 diff --git a/docs/builtin-catalog.ja.md b/docs/builtin-catalog.ja.md index 166d573..9d4705e 100644 --- a/docs/builtin-catalog.ja.md +++ b/docs/builtin-catalog.ja.md @@ -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) が分析・投票。 | diff --git a/docs/builtin-catalog.md b/docs/builtin-catalog.md index e06bd19..e9d1fca 100644 --- a/docs/builtin-catalog.md +++ b/docs/builtin-catalog.md @@ -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. | diff --git a/e2e/specs/cli-export-cc.e2e.ts b/e2e/specs/cli-export-cc.e2e.ts index 7181106..c93acc7 100644 --- a/e2e/specs/cli-export-cc.e2e.ts +++ b/e2e/specs/cli-export-cc.e2e.ts @@ -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); diff --git a/e2e/specs/codex-permission-mode.e2e.ts b/e2e/specs/codex-permission-mode.e2e.ts index bf3e876..16f1577 100644 --- a/e2e/specs/codex-permission-mode.e2e.ts +++ b/e2e/specs/codex-permission-mode.e2e.ts @@ -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' }, diff --git a/e2e/specs/opencode-conversation.e2e.ts b/e2e/specs/opencode-conversation.e2e.ts index 4130e42..086074b 100644 --- a/e2e/specs/opencode-conversation.e2e.ts +++ b/e2e/specs/opencode-conversation.e2e.ts @@ -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(); }); diff --git a/package-lock.json b/package-lock.json index 3780665..6a45609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2267d13..134d2e7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/__tests__/deploySkill.test.ts b/src/__tests__/deploySkill.test.ts index 2b7354a..c69a484 100644 --- a/src/__tests__/deploySkill.test.ts +++ b/src/__tests__/deploySkill.test.ts @@ -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); }); }); diff --git a/src/features/config/deploySkill.ts b/src/features/config/deploySkill.ts index 4096751..f52bc97 100644 --- a/src/features/config/deploySkill.ts +++ b/src/features/config/deploySkill.ts @@ -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 { 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 { 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) { diff --git a/src/infra/config/configNormalizers.ts b/src/infra/config/configNormalizers.ts new file mode 100644 index 0000000..455fe92 --- /dev/null +++ b/src/infra/config/configNormalizers.ts @@ -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 }> | 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 }> | 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 }>; +} + +export function normalizePieceOverrides( + raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | 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 } | undefined { + if (!overrides) return undefined; + const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } = {}; + 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; +} diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index 778170b..658b3eb 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -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 }> | 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 }> | 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 }>; -} - -/** Normalize piece_overrides from snake_case (YAML) to camelCase (internal) */ -function normalizePieceOverrides( - raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | 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 } | undefined { - if (!overrides) return undefined; - const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } = {}; - 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(). diff --git a/src/infra/config/project/projectConfig.ts b/src/infra/config/project/projectConfig.ts index ce5beed..c86bd40 100644 --- a/src/infra/config/project/projectConfig.ts +++ b/src/infra/config/project/projectConfig.ts @@ -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 }> | 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 }> | 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 }>; -} - function normalizeAnalytics(raw: Record | 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 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 } | 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 } | undefined { - if (!overrides) return undefined; - const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } = {}; - 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 */ diff --git a/vitest.config.e2e.base.ts b/vitest.config.e2e.base.ts index 4e0f413..49737cd 100644 --- a/vitest.config.e2e.base.ts +++ b/vitest.config.e2e.base.ts @@ -13,4 +13,5 @@ export const e2eBaseTestConfig: UserConfig['test'] = { singleThread: true, }, }, + reporters: ['verbose', 'json'], }; diff --git a/vitest.config.e2e.provider.ts b/vitest.config.e2e.provider.ts index a6d93e3..0a9ecf0 100644 --- a/vitest.config.e2e.provider.ts +++ b/vitest.config.e2e.provider.ts @@ -1,20 +1,33 @@ import { defineConfig } from 'vitest/config'; import { e2eBaseTestConfig } from './vitest.config.e2e.base'; +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/team-leader.e2e.ts', + 'e2e/specs/team-leader-worker-pool.e2e.ts', + 'e2e/specs/team-leader-refill-threshold.e2e.ts', +]; + +const providerSpecificTests: Record = { + codex: ['e2e/specs/codex-permission-mode.e2e.ts'], + opencode: ['e2e/specs/opencode-conversation.e2e.ts'], +}; + export default defineConfig({ test: { ...e2eBaseTestConfig, include: [ - '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', + ...commonTests, + ...(providerSpecificTests[provider] ?? []), ], }, });