diff --git a/CLAUDE.md b/CLAUDE.md index 82df6ce..17aacfe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,7 @@ TAKT (TAKT Agent Koordination Topology) is a multi-agent orchestration system fo | `takt prompt [piece]` | Preview assembled prompts for each movement and phase | | `takt catalog [type]` | List available facets (personas, policies, knowledge, etc.) | | `takt export-cc` | Export takt pieces/agents as Claude Code Skill (~/.claude/) | +| `takt export-codex` | Export takt skill files to Codex Skill (~/.agents/skills/takt/) | | `takt reset config` | Reset global config to builtin template | | `takt reset categories` | Reset piece categories to builtin defaults | | `takt metrics review` | Show review quality metrics | @@ -270,6 +271,7 @@ builtins/ # Bundled defaults (builtin, read from dist/ at runtim ja/ # Japanese (same structure) project/ # Project-level template files skill/ # Claude Code skill files + skill-codex/ # Codex skill files ``` Builtin resources are embedded in the npm package (`builtins/`). Project files in `.takt/` take highest priority, then user files in `~/.takt/`, then builtins. Use `takt eject` to copy builtins for customization. diff --git a/builtins/skill-codex/SKILL.md b/builtins/skill-codex/SKILL.md new file mode 100644 index 0000000..58daed1 --- /dev/null +++ b/builtins/skill-codex/SKILL.md @@ -0,0 +1,203 @@ +--- +name: takt +description: > + TAKT ピースエンジン。codex exec でサブエージェントを起動し、ピース YAML ワークフローに従って + マルチエージェントオーケストレーションを実行する。 +--- + +# TAKT Piece Engine + +## 引数の解析 + +$ARGUMENTS を以下のように解析する: + +``` +$takt {piece} [permission] {task...} +``` + +- **第1トークン**: ピース名またはYAMLファイルパス(必須) +- **第2トークン**: 権限モード(任意)。以下のキーワードの場合は権限モードとして解釈する: + - `--permit-full` - `codex exec --sandbox danger-full-access` + - `--permit-edit` - `codex exec --full-auto` + - 上記以外 → タスク内容の一部として扱う +- **残りのトークン**: タスク内容(省略時は AskUserQuestion でユーザーに入力を求める) +- **権限モード省略時のデフォルト**: `default`(`codex exec (オプションなし)`) + +例: +- `$takt coding FizzBuzzを作って` → coding ピース、default 権限 +- `$takt coding --permit-full FizzBuzzを作って` → coding ピース、danger-full-access +- `$takt /path/to/custom.yaml 実装して` → カスタムYAML、default 権限 + +## 事前準備: リファレンスの読み込み + +手順を開始する前に、以下の2ファイルを **Read tool で読み込む**: + +1. `~/.agents/skills/takt/references/engine.md` - プロンプト構築、レポート管理、ループ検出の詳細 +2. `~/.agents/skills/takt/references/yaml-schema.md` - ピースYAMLの構造定義 + +## あなたの役割: Team Lead + +あなたは **Team Lead(オーケストレーター)** である。 +ピースYAMLに定義されたワークフロー(状態遷移マシン)に従って movement を実行する。 + +### 禁止事項 + +- **自分で作業するな** - コーディング、レビュー、設計、テスト等は全てサブエージェントに委任する +- **タスクを自分で分析して1つの実装にまとめるな** - movement を1つずつ順番に実行せよ +- **movement をスキップするな** - 必ず initial_movement から開始し、Rule 評価で決まった次の movement に進む +- **"yolo" をピース名と誤解するな** - "yolo" は YOLO(You Only Live Once)の俗語で「無謀・適当・いい加減」という意味。「yolo ではレビューして」= 「適当にやらずにちゃんとレビューして」という意味であり、ピース作成の指示ではない + +### あなたの仕事は4つだけ + +1. ピースYAML を読んでワークフローを理解する +2. 各 movement のプロンプトを構築する(references/engine.md 参照) +3. **Write tool + Bash tool (`codex exec`)** でサブエージェントを起動して作業を委任する +4. サブエージェントの出力から Rule 評価を行い、次の movement を決定する + +**重要**: ユーザーが明示的に指示するまで git commit を実行してはならない。実装完了 ≠ コミット許可。 + +### ツールの使い分け(重要) + +| やること | 使うツール | 説明 | +|---------|-----------|------| +| プロンプト一時保存 | **Write** tool | movement名を含めない安全なランダム名(例: `/tmp/takt-prompt-{timestamp}-{uuid}.md`)で書き出す | +| サブエージェント起動 | **Bash** tool | `codex exec {権限オプション} - < /tmp/...` を実行 | + +## 手順(この順序で厳密に実行せよ) + +### 手順 1: ピース解決と読み込み + +引数の第1トークンからピースYAMLファイルを特定して Read で読む。 + +**第1トークンがない場合(ピース名未指定):** +→ ユーザーに「ピース名を指定してください。例: `$takt coding タスク内容`」と伝えて終了する。 + +**ピースYAMLの検索順序:** +1. `.yaml` / `.yml` で終わる、または `/` を含む → ファイルパスとして直接 Read +2. ピース名として検索: + - `~/.takt/pieces/{name}.yaml` (ユーザーカスタム、優先) + - `~/.agents/skills/takt/pieces/{name}.yaml` (Skill同梱ビルトイン) +3. 見つからない場合: 上記2ディレクトリを Glob で列挙し、AskUserQuestion で選択させる + +YAMLから以下を抽出する(→ references/yaml-schema.md 参照): +- `name`, `max_movements`, `initial_movement`, `movements` 配列 +- セクションマップ: `personas`, `policies`, `instructions`, `output_contracts`, `knowledge` + +### 手順 2: セクションリソースの事前読み込み + +ピースYAMLのセクションマップ(`personas:`, `policies:`, `instructions:`, `output_contracts:`, `knowledge:`)から全ファイルパスを収集する。 +パスは **ピースYAMLファイルのディレクトリからの相対パス** で解決する。 + +例: ピースが `~/.agents/skills/takt/pieces/default.yaml` にあり、`personas:` に `coder: ../facets/personas/coder.md` がある場合 +→ 絶対パスは `~/.agents/skills/takt/facets/personas/coder.md` + +重複を除いて Read で全て読み込む。読み込んだ内容はサブエージェントへのプロンプト構築に使う。 + +### 手順 3: 初期化 + +`initial_movement` の名前を確認し、`movements` 配列から該当する movement を取得する。 +**以下の変数を初期化する:** +- `iteration = 1` +- `current_movement = initial_movement の movement 定義` +- `previous_response = ""` +- `permission_mode = コマンドで解析された権限モード("danger-full-access" / "full-auto" / "default")` +- `movement_history = []`(遷移履歴。Loop Monitor 用) + +**実行ディレクトリ**: いずれかの movement に `report` フィールドがある場合、`.takt/runs/{YYYYMMDD-HHmmss}-{slug}/` を作成し、以下を配置する。 +- `reports/`(レポート出力) +- `context/knowledge/`(Knowledge スナップショット) +- `context/policy/`(Policy スナップショット) +- `context/previous_responses/`(Previous Response 履歴 + `latest.md`) +- `logs/`(実行ログ) +- `meta.json`(run メタデータ) + +レポート出力先パスを `report_dir` 変数(`.takt/runs/{slug}/reports`)として保持する。 + +次に **手順 4** に進む。 + +### 手順 4: サブエージェント起動 + +**iteration が max_movements を超えていたら → 手順 7(ABORT: イテレーション上限)に進む。** + +current_movement のプロンプトを構築する(→ references/engine.md のプロンプト構築を参照)。 + +プロンプト構築の要素: +1. **ペルソナ**: `persona:` キー → `personas:` セクション → .md ファイル内容 +2. **ポリシー**: `policy:` キー → `policies:` セクション → .md ファイル内容(複数可、末尾にリマインダー再掲) +3. **実行コンテキスト**: cwd, ピース名, movement名, イテレーション情報 +4. **ナレッジ**: `knowledge:` キー → `knowledge:` セクション → .md ファイル内容 +5. **インストラクション**: `instruction:` キー → `instructions:` セクション → .md ファイル内容(テンプレート変数展開済み) +6. **タスク/前回出力/レポート指示/タグ指示**: 自動注入 + +**通常 movement の場合(parallel フィールドなし):** + +1. Write tool でプロンプトを一時ファイルに保存する。 + - movement名やsubstep名をファイル名に含めず、`/tmp/takt-prompt-{timestamp}-{uuid}.md` のような安全なランダム名を使う +2. Bash tool で `codex exec` を実行する。 + - `--permit-full` の場合: `codex exec --sandbox danger-full-access - < "$tmp_prompt_file"` + - `--permit-edit` の場合: `codex exec --full-auto - < "$tmp_prompt_file"` + - デフォルト: `codex exec - < "$tmp_prompt_file"` +3. `stdout` をサブエージェント出力として扱う。 +4. **手順 5** に進む。 + +**parallel movement の場合:** + +1. parallel 配列の各サブステップ用プロンプトをそれぞれ安全なランダム名(例: `/tmp/takt-parallel-{timestamp}-{uuid}.md`)で保存する。 +2. **1つのメッセージで**、サブステップ数分の Bash tool (`codex exec`) を並列実行する。 +3. 全 `stdout` を収集して **手順 5** に進む。 + +```bash +# 例: 2サブステップを並列実行 +codex exec --full-auto - < "$tmp_prompt_file_1" +codex exec --full-auto - < "$tmp_prompt_file_2" +``` + +### 手順 5: レポート抽出と Loop Monitor + +**レポート抽出**(current_movement に `report` フィールドがある場合のみ): +サブエージェント出力から ```markdown ブロックを抽出し、Write tool で `{report_dir}/{ファイル名}` に保存する。 +詳細は references/engine.md の「レポートの抽出と保存」を参照。 + +**Loop Monitor チェック**(ピースに `loop_monitors` がある場合のみ): +`movement_history` に current_movement の名前を追加する。 +遷移履歴が loop_monitor の `cycle` パターンに `threshold` 回以上マッチした場合、judge サブエージェントを起動して遷移先をオーバーライドする。 +詳細は references/engine.md の「Loop Monitors」を参照。 + +### 手順 6: Rule 評価 + +`codex exec` から返ってきたサブエージェント出力から matched_rule を決定する。 + +**通常 movement:** +1. 出力に `[STEP:N]` タグがあるか探す(複数ある場合は最後のタグを採用) +2. タグがあれば → rules[N] を選択(0始まりインデックス) +3. タグがなければ → 出力全体を読み、全 condition と比較して最も近いものを選択 + +**parallel movement:** +1. 各サブステップの `codex exec` 出力に対して、サブステップの rules で条件マッチを判定 +2. マッチした condition 文字列を記録 +3. 親 movement の rules で aggregate 評価: + - `all("X")`: 全サブステップが "X" にマッチしたら true + - `any("X")`: いずれかのサブステップが "X" にマッチしたら true + - `all("X", "Y")`: サブステップ1が "X"、サブステップ2が "Y" にマッチしたら true +4. 親 rules を上から順に評価し、最初に true になった rule を選択 + +matched_rule が決まったら次に進む。 +- `next = COMPLETE` → **手順 7(COMPLETE)** +- `next = ABORT` → **手順 7(ABORT)** +- `next = movement 名` → `previous_response` 更新、`iteration += 1`、次 movement を取得して **手順 4** に戻る + +どの rule にもマッチしなかったら → **手順 7(ABORT: ルール不一致)** に進む。 + +### 手順 7: 終了 + +ユーザーに結果を報告する: +- **COMPLETE**: 最後のサブエージェント出力のサマリーを表示 +- **ABORT**: 失敗理由を表示 +- **イテレーション上限**: 強制終了を通知 + +## 詳細リファレンス + +| ファイル | 内容 | +|---------|------| +| `references/engine.md` | プロンプト構築、レポート管理、ループ検出の詳細 | +| `references/yaml-schema.md` | ピースYAMLの構造定義とフィールド説明 | diff --git a/builtins/skill-codex/agents/openai.yaml b/builtins/skill-codex/agents/openai.yaml new file mode 100644 index 0000000..d4304e8 --- /dev/null +++ b/builtins/skill-codex/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "TAKT Piece Engine" + short_description: "Multi-agent orchestration via piece YAML workflows" + +policy: + allow_implicit_invocation: false diff --git a/builtins/skill-codex/references/engine.md b/builtins/skill-codex/references/engine.md new file mode 100644 index 0000000..68f28f9 --- /dev/null +++ b/builtins/skill-codex/references/engine.md @@ -0,0 +1,228 @@ +# TAKT 実行エンジン詳細 + +## サブエージェントの起動方法 + +全ての movement は `codex exec` でサブエージェントを起動して実行する。 +**あなた(Team Lead)が直接作業することは禁止。** + +### 実行フロー(Write + Bash) + +1. プロンプト全文を一時ファイルへ保存する +2. Bash tool で `codex exec` を実行する +3. `stdout` を movement の出力として扱う + +```bash +# 例 +codex exec --full-auto - < "$tmp_prompt_file" +``` + +### permission_mode のマッピング + +コマンド引数で解析された `permission_mode` を以下にマップして `codex exec` に渡す。 + +- `$takt coding --permit-full タスク` + - `permission_mode = "danger-full-access"` + - 実行: `codex exec --sandbox danger-full-access - < /tmp/...` +- `$takt coding --permit-edit タスク` + - `permission_mode = "full-auto"` + - 実行: `codex exec --full-auto - < /tmp/...` +- `$takt coding タスク` + - `permission_mode = "default"` + - 実行: `codex exec - < /tmp/...` + +## 通常 Movement の実行 + +通常の movement(`parallel` フィールドなし)は、`codex exec` を1回実行する。 + +1. プロンプトを構築する(後述の「プロンプト構築」参照) +2. movement名を含めない安全なランダム名(例: `/tmp/takt-prompt-{timestamp}-{uuid}.md`)で保存する +3. `codex exec {権限オプション} - < /tmp/...` を実行する +4. `stdout` を受け取り Rule 評価で次 movement を決定する + +## Parallel Movement の実行 + +`parallel` フィールドを持つ movement は、複数サブステップを並列実行する。 + +### 実行手順 + +1. parallel 配列の各サブステップごとにプロンプトを構築する +2. 各プロンプトを substep名を含めない安全なランダム名で保存する +3. **1つのメッセージで** サブステップ数分の Bash tool(`codex exec`)を並列実行する +4. 各 `stdout` を収集する +5. 各サブステップの `rules` で条件マッチを判定する +6. 親 movement の `rules` で aggregate 評価(`all()` / `any()`)を行う + +### サブステップ条件マッチ判定 + +各サブステップ出力に対する判定優先順位: + +1. `[STEP:N]` タグがあればインデックスで照合(最後のタグを採用) +2. タグがなければ出力全文と条件文の意味一致で判定 + +マッチした condition 文字列を記録し、親 movement の aggregate 評価に使う。 + +## セクションマップの解決 + +ピースYAMLトップレベルの `personas:`, `policies:`, `instructions:`, `output_contracts:`, `knowledge:` はキーとファイルパスの対応表。movement 内ではキー名で参照する。 + +### 解決手順 + +1. ピースYAMLを読み込む +2. 各セクションマップのパスを、**ピースYAMLファイルのディレクトリ基準**で絶対パスへ変換する +3. movement のキー参照(例: `persona: coder`)から実ファイルを Read で取得する + +例: ピースが `~/.agents/skills/takt/pieces/default.yaml` の場合 +- `personas.coder: ../facets/personas/coder.md` → `~/.agents/skills/takt/facets/personas/coder.md` +- `policies.coding: ../facets/policies/coding.md` → `~/.agents/skills/takt/facets/policies/coding.md` +- `instructions.plan: ../facets/instructions/plan.md` → `~/.agents/skills/takt/facets/instructions/plan.md` + +## プロンプト構築 + +各 movement 実行時、以下を上から順に結合してプロンプトを作る。 + +1. ペルソナ(`persona:` 参照先 .md 全文) +2. 区切り線 `---` +3. ポリシー(`policy:` 参照先 .md。複数可) +4. 区切り線 `---` +5. 実行コンテキスト(cwd / piece / movement / iteration) +6. ナレッジ(`knowledge:` 参照先 .md) +7. インストラクション(`instruction:` または `instruction_template:`) +8. タスク(`{task}` 未使用時は末尾に自動追加) +9. 前回出力(`pass_previous_response: true` のとき) +10. レポート出力指示(`report` または `output_contracts.report` があるとき) +11. ステータスタグ出力指示(`rules` があるとき) +12. ポリシーリマインダー(ポリシーを末尾再掲) + +### テンプレート変数展開 + +インストラクション内のプレースホルダーを置換する。 + +- `{task}`: ユーザー入力タスク +- `{previous_response}`: 前 movement 出力 +- `{iteration}`: ピース全体イテレーション +- `{max_movements}`: 最大イテレーション数 +- `{movement_iteration}`: 当該 movement 実行回数 +- `{report_dir}`: `.takt/runs/{slug}/reports` +- `{report:ファイル名}`: 指定レポート内容(存在しない場合は `(レポート未作成)`) + +## レポート出力指示と保存 + +movement がレポートを要求する場合、プロンプト末尾に必須指示を注入する。 + +### 形式1: name + format + +```yaml +report: + name: 01-plan.md + format: plan +``` + +`output_contracts`(または `report_formats`)のキー参照先内容を読み込み、出力契約として渡す。 + +### 形式2: 複数レポート配列 + +```yaml +report: + - Summary: summary.md + - Scope: 01-scope.md +``` + +各レポートを見出し付き ` ```markdown ` ブロックで出力するよう指示する。 + +### 抽出と保存(Team Lead が実施) + +`codex exec` 出力から ` ```markdown ` ブロックを抽出し、`{report_dir}/{ファイル名}` に Write で保存する。 + +- 実行ディレクトリ: `.takt/runs/{YYYYMMDD-HHmmss}-{slug}/` +- 保存先: + - `reports/` + - `context/knowledge/` + - `context/policy/` + - `context/previous_responses/`(`latest.md` を含む) + - `logs/` + - `meta.json` + +## ステータスタグ出力指示 + +movement に `rules` がある場合、最後に1つだけタグを出力するよう指示する。 + +```text +[STEP:0] = {rules[0].condition} +[STEP:1] = {rules[1].condition} +... +``` + +- `ai("...")` は括弧を外した条件文を表示する +- parallel サブステップでも同様に適用する + +## Rule 評価 + +### 通常 Movement + +1. 出力中の `[STEP:N]` を検出(複数なら最後を採用) +2. 該当 index の rule を採用 +3. タグがない場合は全文を読み condition と意味照合して最も近い rule を採用 + +### Parallel Movement(Aggregate) + +- `all("X")`: 全サブステップが `X` に一致 +- `any("X")`: いずれかが `X` に一致 +- `all("X", "Y")`: サブステップ位置対応で一致 + +親 rules を上から順に評価し、最初の一致を採用する。 + +### 不一致時 + +どの rule にも一致しない場合は ABORT し、不一致理由をユーザーへ報告する。 + +## ループ検出 + +### 基本 + +- 同じ movement が連続3回以上なら警告 +- `max_movements` 到達で ABORT + +### カウンター + +- `iteration`: 全体実行回数 +- `movement_iteration[name]`: movement 別実行回数 +- `consecutive_count[name]`: 連続実行回数 + +## Loop Monitors + +`loop_monitors` がある場合、指定サイクルを監視する。 + +1. movement 遷移履歴を記録する +2. `cycle` が `threshold` 回以上連続出現したら judge を実行する +3. judge は `persona` + `instruction_template` + `rules` でプロンプト構築する +4. judge も同じく `codex exec` で起動する +5. judge の評価結果 `next` で遷移先を上書きする + +## 状態遷移の全体像 + +```text +[開始] + ↓ +ピースYAML読み込み + セクションマップ解決 + ↓ +実行ディレクトリ作成 + ↓ +initial_movement 取得 + ↓ +┌─→ codex exec で movement 実行(通常/parallel) +│ ↓ +│ 出力受信 +│ ↓ +│ レポート抽出・保存 +│ ↓ +│ Loop Monitor チェック(必要時 judge を codex exec で実行) +│ ↓ +│ Rule 評価(タグ優先、未タグ時は意味照合) +│ ↓ +│ next 決定 +│ ├── COMPLETE → 終了報告 +│ ├── ABORT → エラー報告 +│ └── movement名 → 次 movement +│ ↓ +└──────────────────────┘ +``` diff --git a/builtins/skill-codex/references/yaml-schema.md b/builtins/skill-codex/references/yaml-schema.md new file mode 100644 index 0000000..d9cadd1 --- /dev/null +++ b/builtins/skill-codex/references/yaml-schema.md @@ -0,0 +1,224 @@ +# ピースYAML スキーマリファレンス + +このドキュメントはピースYAMLの構造を定義する。具体的なピース定義は含まない。 + +## トップレベルフィールド + +```yaml +name: piece-name # ピース名(必須) +description: 説明テキスト # ピースの説明(任意) +max_movements: 10 # 最大イテレーション数(必須) +initial_movement: plan # 最初に実行する movement 名(必須) + +# セクションマップ(キー → ファイルパスの対応表) +policies: # ポリシー定義(任意) + coding: ../policies/coding.md + review: ../policies/review.md +personas: # ペルソナ定義(任意) + coder: ../personas/coder.md + reviewer: ../personas/architecture-reviewer.md +instructions: # 指示テンプレート定義(任意) + plan: ../instructions/plan.md + implement: ../instructions/implement.md +report_formats: # レポートフォーマット定義(任意) + plan: ../output-contracts/plan.md + review: ../output-contracts/architecture-review.md +knowledge: # ナレッジ定義(任意) + architecture: ../knowledge/architecture.md + +movements: [...] # movement 定義の配列(必須) +loop_monitors: [...] # ループ監視設定(任意) +``` + +### セクションマップの解決 + +各セクションマップのパスは **ピースYAMLファイルのディレクトリからの相対パス** で解決する。 +movement 内では**キー名**で参照する(パスを直接書かない)。 + +例: ピースが `~/.claude/skills/takt/pieces/coding.yaml` にあり、`personas:` セクションに `coder: ../personas/coder.md` がある場合 +→ 絶対パスは `~/.claude/skills/takt/personas/coder.md` +→ movement では `persona: coder` で参照 + +## Movement 定義 + +### 通常 Movement + +```yaml +- name: movement-name # movement 名(必須、一意) + persona: coder # ペルソナキー(personas マップを参照、任意) + policy: coding # ポリシーキー(policies マップを参照、任意) + policy: [coding, testing] # 複数指定も可(配列) + instruction: implement # 指示テンプレートキー(instructions マップを参照、任意) + knowledge: architecture # ナレッジキー(knowledge マップを参照、任意) + edit: true # ファイル編集可否(必須) + required_permission_mode: edit # 必要最小権限: edit / readonly / full(任意) + session: refresh # セッション管理(任意) + pass_previous_response: true # 前の出力を渡すか(デフォルト: true) + allowed_tools: [...] # 許可ツール一覧(任意、参考情報) + instruction_template: | # 指示テンプレート(参照解決またはインライン、任意) + 指示内容... + output_contracts: [...] # 出力契約設定(任意) + quality_gates: [...] # 品質ゲート(AIへの指示、任意) + rules: [...] # 遷移ルール(必須) +``` + +**`instruction` vs `instruction_template`**: どちらも同じ参照解決ルート(セクションマップ → パス → 3-layer facet → インライン)を使う。`instruction_template` はインライン文字列もそのまま使える。通常はどちらか一方を使用する。 + +### Parallel Movement + +```yaml +- name: reviewers # 親 movement 名(必須) + parallel: # 並列サブステップ配列(これがあると parallel movement) + - name: arch-review + persona: architecture-reviewer + policy: review + knowledge: architecture + edit: false + instruction: review-arch + output_contracts: + report: + - name: 05-architect-review.md + format: architecture-review + rules: + - condition: "approved" + - condition: "needs_fix" + + - name: qa-review + persona: qa-reviewer + policy: review + edit: false + instruction: review-qa + rules: + - condition: "approved" + - condition: "needs_fix" + + rules: # 親の rules(aggregate 条件で遷移先を決定) + - condition: all("approved") + next: supervise + - condition: any("needs_fix") + next: fix +``` + +**重要**: サブステップの `rules` は結果分類のための condition 定義のみ。`next` は無視される(親の rules が遷移先を決定)。 + +## Rules 定義 + +```yaml +rules: + - condition: 条件テキスト # マッチ条件(必須) + next: next-movement # 遷移先 movement 名(必須、サブステップでは任意) + requires_user_input: true # ユーザー入力が必要(任意) + interactive_only: true # インタラクティブモードのみ(任意) + appendix: | # 追加情報(任意) + 補足テキスト... +``` + +### Condition 記法 + +| 記法 | 説明 | 例 | +|-----|------|-----| +| 文字列 | AI判定またはタグで照合 | `"タスク完了"` | +| `ai("...")` | AI が出力に対して条件を評価 | `ai("コードに問題がある")` | +| `all("...")` | 全サブステップがマッチ(parallel 親のみ) | `all("approved")` | +| `any("...")` | いずれかがマッチ(parallel 親のみ) | `any("needs_fix")` | +| `all("X", "Y")` | 位置対応で全マッチ(parallel 親のみ) | `all("問題なし", "テスト成功")` | + +### 特殊な next 値 + +| 値 | 意味 | +|---|------| +| `COMPLETE` | ピース成功終了 | +| `ABORT` | ピース失敗終了 | +| movement 名 | 指定された movement に遷移 | + +## Output Contracts 定義 + +Movement の出力契約(レポート定義)。`output_contracts.report` 配列形式で指定する。 + +### 形式1: name + format(フォーマット参照) + +```yaml +output_contracts: + report: + - name: 01-plan.md + format: plan # report_formats マップのキーを参照 +``` + +`format` がキー文字列の場合、トップレベル `report_formats:` セクションから対応する .md ファイルを読み込み、出力契約指示として使用する。 + +### 形式1b: name + format(インライン) + +```yaml +output_contracts: + report: + - name: 01-plan.md + format: | # インラインでフォーマットを記述 + # レポートタイトル + ## セクション + {内容} +``` + +### 形式2: label + path(ラベル付きパス) + +```yaml +output_contracts: + report: + - Summary: summary.md + - Scope: 01-scope.md + - Decisions: 02-decisions.md +``` + +各要素のキーがレポート種別名(ラベル)、値がファイル名。 + +## Quality Gates 定義 + +Movement 完了時の品質要件を AI への指示として定義する。自動検証は行わない。 + +```yaml +quality_gates: + - 全てのテストがパスすること + - TypeScript の型エラーがないこと + - ESLint 違反がないこと +``` + +配列で複数の品質基準を指定できる。エージェントはこれらの基準を満たしてから Movement を完了する必要がある。 + +## テンプレート変数 + +`instruction_template`(またはインストラクションファイル)内で使用可能な変数: + +| 変数 | 説明 | +|-----|------| +| `{task}` | ユーザーのタスク入力(template に含まれない場合は自動追加) | +| `{previous_response}` | 前の movement の出力(pass_previous_response: true 時、自動追加) | +| `{iteration}` | ピース全体のイテレーション数 | +| `{max_movements}` | 最大イテレーション数 | +| `{movement_iteration}` | この movement の実行回数 | +| `{report_dir}` | レポートディレクトリ名 | +| `{report:ファイル名}` | 指定レポートファイルの内容を展開 | +| `{user_inputs}` | 蓄積されたユーザー入力 | +| `{cycle_count}` | loop_monitors 内で使用するサイクル回数 | + +## Loop Monitors(任意) + +```yaml +loop_monitors: + - cycle: [movement_a, movement_b] # 監視対象の movement サイクル + threshold: 3 # 発動閾値(サイクル回数) + judge: + persona: supervisor # ペルソナキー参照 + instruction_template: | # 判定用指示 + サイクルが {cycle_count} 回繰り返されました。 + 健全性を判断してください。 + rules: + - condition: 健全(進捗あり) + next: movement_a + - condition: 非生産的(改善なし) + next: alternative_movement +``` + +特定の movement 間のサイクルが閾値に達した場合、judge が介入して遷移先を判断する。 + +## allowed_tools について + +`allowed_tools` は TAKT 本体のエージェントプロバイダーで使用されるフィールド。Claude Code の Skill として実行する場合、Task tool のエージェントが使用可能なツールは Claude Code の設定に従う。このフィールドは参考情報として扱い、`edit` フィールドの方を権限制御に使用する。 diff --git a/docs/cli-reference.ja.md b/docs/cli-reference.ja.md index 6c2feba..c699712 100644 --- a/docs/cli-reference.ja.md +++ b/docs/cli-reference.ja.md @@ -251,6 +251,15 @@ takt clear takt export-cc ``` +### takt export-codex + +TAKT のスキルファイルを Codex Skill(`~/.agents/skills/takt/`)としてデプロイします。 +このコマンドは `SKILL.md`、`references/`、`agents/`、`pieces/`、`facets/`、`templates/` をデプロイします。 + +```bash +takt export-codex +``` + ### takt catalog レイヤー間で利用可能なファセットの一覧を表示します。 diff --git a/docs/cli-reference.md b/docs/cli-reference.md index a97014b..ec663ff 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -251,6 +251,15 @@ Deploy builtin pieces/personas as a Claude Code Skill. takt export-cc ``` +### takt export-codex + +Deploy TAKT skill files as a Codex Skill (`~/.agents/skills/takt/`). +This command deploys `SKILL.md`, `references/`, `agents/`, `pieces/`, `facets/`, and `templates/`. + +```bash +takt export-codex +``` + ### takt catalog List available facets across layers. diff --git a/e2e/fixtures/config.e2e.yaml b/e2e/fixtures/config.e2e.yaml index cf47a6e..2170a00 100644 --- a/e2e/fixtures/config.e2e.yaml +++ b/e2e/fixtures/config.e2e.yaml @@ -1,6 +1,7 @@ provider: claude language: en -log_level: info +logging: + level: info provider_options: codex: network_access: true diff --git a/src/__tests__/commands-add.test.ts b/src/__tests__/commands-add.test.ts index 508f081..c0c3cfb 100644 --- a/src/__tests__/commands-add.test.ts +++ b/src/__tests__/commands-add.test.ts @@ -53,6 +53,7 @@ vi.mock('../app/cli/program.js', () => ({ })); vi.mock('../infra/config/index.js', () => ({ + clearPersonaSessions: vi.fn(), resolveConfigValue: vi.fn(), })); @@ -73,7 +74,6 @@ vi.mock('../features/tasks/index.js', () => ({ })); vi.mock('../features/config/index.js', () => ({ - clearPersonaSessions: vi.fn(), ejectBuiltin: vi.fn(), ejectFacet: vi.fn(), parseFacetType: vi.fn(), @@ -81,6 +81,7 @@ vi.mock('../features/config/index.js', () => ({ resetCategoriesToDefault: vi.fn(), resetConfigToDefault: vi.fn(), deploySkill: vi.fn(), + deploySkillCodex: vi.fn(), })); vi.mock('../features/prompt/index.js', () => ({ @@ -111,6 +112,7 @@ vi.mock('../commands/repertoire/list.js', () => ({ })); import '../app/cli/commands.js'; +const configFeatures = await import('../features/config/index.js'); describe('CLI add command', () => { beforeEach(() => { @@ -151,6 +153,22 @@ describe('CLI add command', () => { expect(calledCommandNames).not.toContain('switch'); }); + it('should register export-codex command', () => { + const calledCommandNames = rootCommand.command.mock.calls + .map((call: unknown[]) => call[0] as string); + + expect(calledCommandNames).toContain('export-codex'); + }); + + it('should invoke deploySkillCodex for export-codex command', async () => { + const exportCodexAction = commandActions.get('root.export-codex'); + expect(exportCodexAction).toBeTypeOf('function'); + + await exportCodexAction?.(); + const deploySkillCodex = (configFeatures as Record).deploySkillCodex; + expect(deploySkillCodex).toHaveBeenCalledTimes(1); + }); + it('should describe prompt piece argument as defaulting to "default"', () => { const promptCommand = commandMocks.get('root.prompt'); expect(promptCommand).toBeTruthy(); diff --git a/src/__tests__/deploySkillCodex.test.ts b/src/__tests__/deploySkillCodex.test.ts new file mode 100644 index 0000000..eff1132 --- /dev/null +++ b/src/__tests__/deploySkillCodex.test.ts @@ -0,0 +1,205 @@ +/** + * Tests for deploySkillCodex (export-codex) command + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +const testHomeDir = mkdtempSync(join(tmpdir(), 'takt-deploy-codex-test-')); + +vi.mock('node:os', async () => { + const actual = await vi.importActual('node:os'); + return { + ...actual, + homedir: () => testHomeDir, + }; +}); + +vi.mock('../shared/prompt/index.js', () => ({ + confirm: vi.fn().mockResolvedValue(true), +})); + +vi.mock('../shared/ui/index.js', () => ({ + header: vi.fn(), + success: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + blankLine: vi.fn(), +})); + +vi.mock('../infra/config/index.js', () => ({ + getLanguage: vi.fn().mockReturnValue('en'), +})); + +let fakeResourcesDir: string; + +vi.mock('../infra/resources/index.js', async () => { + const actual = await vi.importActual('../infra/resources/index.js'); + return { + ...actual, + getResourcesDir: () => fakeResourcesDir, + getLanguageResourcesDir: (lang: string) => join(fakeResourcesDir, lang), + }; +}); + +const configFeatures = await import('../features/config/index.js'); +const deploySkillCodex = (configFeatures as Record).deploySkillCodex as () => Promise; +const { warn } = await import('../shared/ui/index.js'); +const { confirm } = await import('../shared/prompt/index.js'); + +describe('deploySkillCodex', () => { + let skillDir: string; + + beforeEach(() => { + fakeResourcesDir = mkdtempSync(join(tmpdir(), 'takt-resources-codex-')); + + const skillResourcesDir = join(fakeResourcesDir, 'skill-codex'); + mkdirSync(skillResourcesDir, { recursive: true }); + writeFileSync(join(skillResourcesDir, 'SKILL.md'), '# SKILL Codex'); + + const refsDir = join(skillResourcesDir, 'references'); + mkdirSync(refsDir, { recursive: true }); + writeFileSync(join(refsDir, 'engine.md'), '# Engine'); + writeFileSync(join(refsDir, 'yaml-schema.md'), '# Schema'); + + const agentsDir = join(skillResourcesDir, 'agents'); + mkdirSync(agentsDir, { recursive: true }); + writeFileSync(join(agentsDir, 'openai.yaml'), 'interface:\n display_name: TAKT'); + + const langDir = join(fakeResourcesDir, 'en'); + mkdirSync(join(langDir, 'pieces'), { recursive: true }); + mkdirSync(join(langDir, 'facets', 'personas'), { recursive: true }); + mkdirSync(join(langDir, 'facets', 'policies'), { recursive: true }); + mkdirSync(join(langDir, 'facets', 'instructions'), { recursive: true }); + mkdirSync(join(langDir, 'facets', 'knowledge'), { recursive: true }); + mkdirSync(join(langDir, 'facets', 'output-contracts'), { recursive: true }); + mkdirSync(join(langDir, 'templates'), { recursive: true }); + + writeFileSync(join(langDir, 'pieces', 'default.yaml'), 'name: default'); + writeFileSync(join(langDir, 'facets', 'personas', 'coder.md'), '# Coder'); + writeFileSync(join(langDir, 'facets', 'policies', 'coding.md'), '# Coding'); + writeFileSync(join(langDir, 'facets', 'instructions', 'init.md'), '# Init'); + writeFileSync(join(langDir, 'facets', 'knowledge', 'patterns.md'), '# Patterns'); + writeFileSync(join(langDir, 'facets', 'output-contracts', 'summary.md'), '# Summary'); + writeFileSync(join(langDir, 'templates', 'task.md'), '# Task'); + + skillDir = join(testHomeDir, '.agents', 'skills', 'takt'); + mkdirSync(skillDir, { recursive: true }); + + vi.clearAllMocks(); + }); + + afterEach(() => { + if (existsSync(testHomeDir)) { + rmSync(testHomeDir, { recursive: true, force: true }); + } + if (existsSync(fakeResourcesDir)) { + rmSync(fakeResourcesDir, { recursive: true, force: true }); + } + mkdirSync(testHomeDir, { recursive: true }); + }); + + describe('when codex skill resources exist', () => { + it('should copy SKILL.md to codex skill directory', async () => { + await deploySkillCodex(); + + expect(existsSync(join(skillDir, 'SKILL.md'))).toBe(true); + expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf-8')).toBe('# SKILL Codex'); + }); + + it('should copy references directory', async () => { + await deploySkillCodex(); + + const refsDir = join(skillDir, 'references'); + expect(existsSync(refsDir)).toBe(true); + expect(existsSync(join(refsDir, 'engine.md'))).toBe(true); + expect(existsSync(join(refsDir, 'yaml-schema.md'))).toBe(true); + }); + + it('should copy agents/openai.yaml', async () => { + await deploySkillCodex(); + + expect(existsSync(join(skillDir, 'agents', 'openai.yaml'))).toBe(true); + }); + + it('should copy all language resource directories', async () => { + await deploySkillCodex(); + + expect(existsSync(join(skillDir, 'pieces', 'default.yaml'))).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); + }); + }); + + describe('cleanDir behavior', () => { + it('should remove stale files from previous deployments', async () => { + const piecesDir = join(skillDir, 'pieces'); + mkdirSync(piecesDir, { recursive: true }); + writeFileSync(join(piecesDir, 'stale.yaml'), 'name: stale'); + + await deploySkillCodex(); + + expect(existsSync(join(piecesDir, 'stale.yaml'))).toBe(false); + expect(existsSync(join(piecesDir, 'default.yaml'))).toBe(true); + }); + + it('should clean agents directory before copy', async () => { + const agentsDir = join(skillDir, 'agents'); + mkdirSync(agentsDir, { recursive: true }); + writeFileSync(join(agentsDir, 'legacy.yaml'), 'legacy'); + + await deploySkillCodex(); + + expect(existsSync(join(agentsDir, 'legacy.yaml'))).toBe(false); + expect(existsSync(join(agentsDir, 'openai.yaml'))).toBe(true); + }); + }); + + describe('when codex skill resources do not exist', () => { + it('should warn and return early', async () => { + rmSync(join(fakeResourcesDir, 'skill-codex'), { recursive: true }); + + await deploySkillCodex(); + + expect(warn).toHaveBeenCalledWith('Skill resources not found. Ensure takt is installed correctly.'); + }); + }); + + describe('when skill already exists', () => { + it('should ask for confirmation before overwriting', async () => { + writeFileSync(join(skillDir, 'SKILL.md'), '# Old Skill'); + + await deploySkillCodex(); + + expect(confirm).toHaveBeenCalledWith( + '既存のスキルファイルをすべて削除し、最新版に置き換えます。続行しますか?', + false, + ); + }); + + it('should cancel when user declines confirmation', async () => { + vi.mocked(confirm).mockResolvedValueOnce(false); + writeFileSync(join(skillDir, 'SKILL.md'), '# Old Skill'); + + await deploySkillCodex(); + + expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf-8')).toBe('# Old Skill'); + }); + }); + + describe('when language resources directory is empty', () => { + it('should handle missing resource subdirectories gracefully', async () => { + const langDir = join(fakeResourcesDir, 'en'); + rmSync(langDir, { recursive: true }); + mkdirSync(langDir, { recursive: true }); + + await expect(deploySkillCodex()).resolves.not.toThrow(); + }); + }); +}); diff --git a/src/__tests__/deploySkillWrappers.test.ts b/src/__tests__/deploySkillWrappers.test.ts new file mode 100644 index 0000000..bcc037f --- /dev/null +++ b/src/__tests__/deploySkillWrappers.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../features/config/deploySkillInternal.js', () => ({ + deploySkillInternal: vi.fn().mockResolvedValue(undefined), +})); + +const { deploySkill } = await import('../features/config/deploySkill.js'); +const { deploySkillCodex } = await import('../features/config/deploySkillCodex.js'); +const { deploySkillInternal } = await import('../features/config/deploySkillInternal.js'); + +describe('deploy skill wrappers', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should delegate export-cc configuration to shared deploy implementation', async () => { + await deploySkill(); + + expect(deploySkillInternal).toHaveBeenCalledTimes(1); + expect(deploySkillInternal).toHaveBeenCalledWith({ + headerTitle: 'takt export-cc — Deploy to Claude Code', + skillRootDir: '.claude', + skillResourceDirName: 'skill', + existingInstallMessage: 'Claude Code Skill が既にインストールされています。', + usageCommand: '使い方: /takt ', + usageExample: '例: /takt passthrough "Hello World テスト"', + showReferencesSummary: false, + includeAgentsDirectory: false, + showAgentsSummary: false, + }); + }); + + it('should delegate export-codex configuration to shared deploy implementation', async () => { + await deploySkillCodex(); + + expect(deploySkillInternal).toHaveBeenCalledTimes(1); + expect(deploySkillInternal).toHaveBeenCalledWith({ + headerTitle: 'takt export-codex — Deploy to Codex', + skillRootDir: '.agents', + skillResourceDirName: 'skill-codex', + existingInstallMessage: 'Codex Skill が既にインストールされています。', + usageCommand: '使い方: $takt ', + usageExample: '例: $takt passthrough "Hello World テスト"', + showReferencesSummary: true, + includeAgentsDirectory: true, + showAgentsSummary: true, + }); + }); +}); diff --git a/src/__tests__/e2e-helpers.test.ts b/src/__tests__/e2e-helpers.test.ts index 2bcc169..5d58944 100644 --- a/src/__tests__/e2e-helpers.test.ts +++ b/src/__tests__/e2e-helpers.test.ts @@ -84,7 +84,7 @@ describe('createIsolatedEnv', () => { const config = parseYaml(configRaw) as Record; expect(config.language).toBe('en'); - expect(config.log_level).toBe('info'); + expect(config.logging).toEqual({ level: 'info' }); expect(config.notification_sound).toBe(false); expect(config.notification_sound_events).toEqual({ iteration_limit: false, @@ -175,7 +175,8 @@ describe('createIsolatedEnv', () => { `${isolated.taktDir}/config.yaml`, [ 'language: en', - 'log_level: info', + 'logging:', + ' level: info', 'notification_sound: true', 'notification_sound_events: true', ].join('\n'), diff --git a/src/__tests__/skillCodexDocs.test.ts b/src/__tests__/skillCodexDocs.test.ts new file mode 100644 index 0000000..5f5a7d8 --- /dev/null +++ b/src/__tests__/skillCodexDocs.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const skillDoc = readFileSync(join(process.cwd(), 'builtins', 'skill-codex', 'SKILL.md'), 'utf-8'); +const engineDoc = readFileSync(join(process.cwd(), 'builtins', 'skill-codex', 'references', 'engine.md'), 'utf-8'); +const schemaDoc = readFileSync(join(process.cwd(), 'builtins', 'skill-codex', 'references', 'yaml-schema.md'), 'utf-8'); +const sharedSchemaDoc = readFileSync(join(process.cwd(), 'builtins', 'skill', 'references', 'yaml-schema.md'), 'utf-8'); + +describe('skill-codex document safety guidance', () => { + it('should define required YAML frontmatter for Codex SKILL.md', () => { + expect(skillDoc).toMatch(/^---\nname: takt\n/); + expect(skillDoc).toContain('description: >'); + expect(skillDoc).toContain('\n---\n'); + }); + + it('should describe codex exec workflow and permission mapping', () => { + expect(skillDoc).toContain('Write tool + Bash tool (`codex exec`)'); + expect(skillDoc).toContain('codex exec --sandbox danger-full-access'); + expect(skillDoc).toContain('codex exec --full-auto'); + expect(skillDoc).toContain('codex exec (オプションなし)'); + }); + + it('should consistently use ~/.agents/skills/takt path base for codex skill resources', () => { + expect(skillDoc).toContain('~/.agents/skills/takt/'); + expect(engineDoc).toContain('~/.agents/skills/takt/'); + expect(skillDoc).not.toContain('~/.claude/skills/takt/'); + expect(engineDoc).not.toContain('~/.claude/skills/takt/'); + }); + + it('should remove Task tool instructions from codex-specific engine docs', () => { + expect(engineDoc).not.toContain('Task tool'); + expect(engineDoc).not.toContain('TeamCreate'); + expect(engineDoc).not.toContain('TeamDelete'); + expect(engineDoc).toContain('codex exec'); + }); + + it('should require random temp file names instead of movement or substep names', () => { + expect(skillDoc).toContain('movement名やsubstep名をファイル名に含めず'); + expect(engineDoc).toContain('movement名を含めない安全なランダム名'); + }); + + it('should avoid examples that interpolate movement or substep names into shell paths', () => { + expect(skillDoc).not.toMatch(/takt-\{movement/i); + expect(skillDoc).not.toMatch(/takt-\{substep/i); + expect(engineDoc).not.toMatch(/takt-\{movement/i); + expect(engineDoc).not.toMatch(/takt-\{substep/i); + }); + + it('should show quoted temp-file variable usage for codex exec stdin redirection', () => { + expect(skillDoc).toContain('< "$tmp_prompt_file"'); + expect(engineDoc).toContain('< "$tmp_prompt_file"'); + }); + + it('should keep yaml schema identical to shared schema reference', () => { + expect(schemaDoc).toBe(sharedSchemaDoc); + }); +}); diff --git a/src/app/cli/commands.ts b/src/app/cli/commands.ts index 8ed174c..2d1e716 100644 --- a/src/app/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -9,7 +9,16 @@ import { clearPersonaSessions, resolveConfigValue } from '../../infra/config/ind import { getGlobalConfigDir } from '../../infra/config/paths.js'; import { success, info } from '../../shared/ui/index.js'; import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js'; -import { ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES, resetCategoriesToDefault, resetConfigToDefault, deploySkill } from '../../features/config/index.js'; +import { + ejectBuiltin, + ejectFacet, + parseFacetType, + VALID_FACET_TYPES, + resetCategoriesToDefault, + resetConfigToDefault, + deploySkill, + deploySkillCodex, +} from '../../features/config/index.js'; import { previewPrompts } from '../../features/prompt/index.js'; import { showCatalog } from '../../features/catalog/index.js'; import { computeReviewMetrics, formatReviewMetrics, parseSinceDuration, purgeOldEvents } from '../../features/analytics/index.js'; @@ -127,6 +136,13 @@ program await deploySkill(); }); +program + .command('export-codex') + .description('Export takt pieces/agents as Codex Skill (~/.agents/)') + .action(async () => { + await deploySkillCodex(); + }); + program .command('catalog') .description('List available facets (personas, policies, knowledge, instructions, output-contracts)') diff --git a/src/features/config/deploySkill.ts b/src/features/config/deploySkill.ts index f52bc97..ec06406 100644 --- a/src/features/config/deploySkill.ts +++ b/src/features/config/deploySkill.ts @@ -1,199 +1,19 @@ /** * takt export-cc — Deploy takt skill files to Claude Code. - * - * Copies the following to ~/.claude/skills/takt/: - * SKILL.md — Engine overview (user-invocable as /takt) - * references/ — Engine logic + YAML schema - * pieces/ — Builtin piece YAML files - * personas/ — Builtin persona .md files - * policies/ — Builtin policy files - * instructions/ — Builtin instruction files - * knowledge/ — Builtin knowledge files - * output-contracts/ — Builtin output contract files - * templates/ — Builtin template files - * - * Piece YAML persona paths (../personas/...) work as-is because - * the directory structure is mirrored. */ -import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs'; -import { homedir } from 'node:os'; -import { join, dirname, relative } from 'node:path'; +import { deploySkillInternal } from './deploySkillInternal.js'; -import { getLanguage } from '../../infra/config/index.js'; -import { getResourcesDir, getLanguageResourcesDir } from '../../infra/resources/index.js'; -import { confirm } from '../../shared/prompt/index.js'; -import { header, success, info, warn, blankLine } from '../../shared/ui/index.js'; - -/** Files to skip during directory copy */ -const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']); - -/** Target paths under ~/.claude/ */ -function getSkillDir(): string { - return join(homedir(), '.claude', 'skills', 'takt'); -} - -/** Directories directly under builtins/{lang}/ */ -const DIRECT_DIRS = ['pieces', 'templates'] as const; - -/** Facet directories under builtins/{lang}/facets/ */ -const FACET_DIRS = ['personas', 'policies', 'instructions', 'knowledge', 'output-contracts'] as const; - - -/** - * Deploy takt skill to Claude Code (~/.claude/). - */ export async function deploySkill(): Promise { - header('takt export-cc — Deploy to Claude Code'); - - const lang = getLanguage(); - const skillResourcesDir = join(getResourcesDir(), 'skill'); - const langResourcesDir = getLanguageResourcesDir(lang); - const skillDir = getSkillDir(); - - // Verify source directories exist - if (!existsSync(skillResourcesDir)) { - warn('Skill resources not found. Ensure takt is installed correctly.'); - return; - } - - // Check if skill already exists and ask for confirmation - const skillExists = existsSync(join(skillDir, 'SKILL.md')); - if (skillExists) { - info('Claude Code Skill が既にインストールされています。'); - const overwrite = await confirm( - '既存のスキルファイルをすべて削除し、最新版に置き換えます。続行しますか?', - false, - ); - if (!overwrite) { - info('キャンセルしました。'); - return; - } - blankLine(); - } - - const copiedFiles: string[] = []; - - // 1. Deploy SKILL.md - const skillSrc = join(skillResourcesDir, 'SKILL.md'); - const skillDest = join(skillDir, 'SKILL.md'); - copyFile(skillSrc, skillDest, copiedFiles); - - // 2. Deploy references/ (engine.md, yaml-schema.md) - const refsSrcDir = join(skillResourcesDir, 'references'); - const refsDestDir = join(skillDir, 'references'); - cleanDir(refsDestDir); - copyDirRecursive(refsSrcDir, refsDestDir, copiedFiles); - - // 3. Deploy direct resource directories from builtins/{lang}/ - for (const dir of DIRECT_DIRS) { - const srcDir = join(langResourcesDir, dir); - const destDir = join(skillDir, dir); - cleanDir(destDir); - copyDirRecursive(srcDir, destDir, copiedFiles); - } - - // 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(facetsDestDir, dir); - copyDirRecursive(srcDir, destDir, copiedFiles); - } - - // Report results - blankLine(); - if (copiedFiles.length > 0) { - success(`${copiedFiles.length} ファイルをデプロイしました。`); - blankLine(); - - // Show summary by category - const skillBase = join(homedir(), '.claude'); - const skillFiles = copiedFiles.filter( - (f) => - f.startsWith(skillDir) && - !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('/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) { - info(` スキル: ${skillFiles.length} ファイル`); - for (const f of skillFiles) { - info(` ${relative(skillBase, f)}`); - } - } - if (pieceFiles.length > 0) { - info(` ピース: ${pieceFiles.length} ファイル`); - } - if (personaFiles.length > 0) { - info(` ペルソナ: ${personaFiles.length} ファイル`); - } - if (policyFiles.length > 0) { - info(` ポリシー: ${policyFiles.length} ファイル`); - } - if (instructionFiles.length > 0) { - info(` インストラクション: ${instructionFiles.length} ファイル`); - } - if (knowledgeFiles.length > 0) { - info(` ナレッジ: ${knowledgeFiles.length} ファイル`); - } - if (outputContractFiles.length > 0) { - info(` 出力契約: ${outputContractFiles.length} ファイル`); - } - if (templateFiles.length > 0) { - info(` テンプレート: ${templateFiles.length} ファイル`); - } - - blankLine(); - info('使い方: /takt '); - info('例: /takt passthrough "Hello World テスト"'); - } else { - info('デプロイするファイルがありませんでした。'); - } -} - -/** Remove a directory and all its contents so stale files don't persist across deploys. */ -function cleanDir(dir: string): void { - if (existsSync(dir)) { - rmSync(dir, { recursive: true }); - } -} - -/** Copy a single file, creating parent directories as needed. */ -function copyFile(src: string, dest: string, copiedFiles: string[]): void { - if (!existsSync(src)) return; - mkdirSync(dirname(dest), { recursive: true }); - writeFileSync(dest, readFileSync(src)); - copiedFiles.push(dest); -} - -/** Recursively copy directory contents, always overwriting. */ -function copyDirRecursive(srcDir: string, destDir: string, copiedFiles: string[]): void { - if (!existsSync(srcDir)) return; - mkdirSync(destDir, { recursive: true }); - - for (const entry of readdirSync(srcDir)) { - if (SKIP_FILES.has(entry)) continue; - - const srcPath = join(srcDir, entry); - const destPath = join(destDir, entry); - const stat = statSync(srcPath); - - if (stat.isDirectory()) { - copyDirRecursive(srcPath, destPath, copiedFiles); - } else { - writeFileSync(destPath, readFileSync(srcPath)); - copiedFiles.push(destPath); - } - } + await deploySkillInternal({ + headerTitle: 'takt export-cc — Deploy to Claude Code', + skillRootDir: '.claude', + skillResourceDirName: 'skill', + existingInstallMessage: 'Claude Code Skill が既にインストールされています。', + usageCommand: '使い方: /takt ', + usageExample: '例: /takt passthrough "Hello World テスト"', + showReferencesSummary: false, + includeAgentsDirectory: false, + showAgentsSummary: false, + }); } diff --git a/src/features/config/deploySkillCodex.ts b/src/features/config/deploySkillCodex.ts new file mode 100644 index 0000000..21623f9 --- /dev/null +++ b/src/features/config/deploySkillCodex.ts @@ -0,0 +1,19 @@ +/** + * takt export-codex — Deploy takt skill files to Codex. + */ + +import { deploySkillInternal } from './deploySkillInternal.js'; + +export async function deploySkillCodex(): Promise { + await deploySkillInternal({ + headerTitle: 'takt export-codex — Deploy to Codex', + skillRootDir: '.agents', + skillResourceDirName: 'skill-codex', + existingInstallMessage: 'Codex Skill が既にインストールされています。', + usageCommand: '使い方: $takt ', + usageExample: '例: $takt passthrough "Hello World テスト"', + showReferencesSummary: true, + includeAgentsDirectory: true, + showAgentsSummary: true, + }); +} diff --git a/src/features/config/deploySkillInternal.ts b/src/features/config/deploySkillInternal.ts new file mode 100644 index 0000000..5d83d29 --- /dev/null +++ b/src/features/config/deploySkillInternal.ts @@ -0,0 +1,187 @@ +import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join, dirname, relative } from 'node:path'; + +import { getLanguage } from '../../infra/config/index.js'; +import { getResourcesDir, getLanguageResourcesDir } from '../../infra/resources/index.js'; +import { confirm } from '../../shared/prompt/index.js'; +import { header, success, info, warn, blankLine } from '../../shared/ui/index.js'; + +const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']); +const DIRECT_DIRS = ['pieces', 'templates'] as const; +const FACET_DIRS = ['personas', 'policies', 'instructions', 'knowledge', 'output-contracts'] as const; + +export type DeploySkillOptions = { + headerTitle: string; + skillRootDir: string; + skillResourceDirName: string; + existingInstallMessage: string; + usageCommand: string; + usageExample: string; + showReferencesSummary: boolean; + includeAgentsDirectory: boolean; + showAgentsSummary: boolean; +}; + +export async function deploySkillInternal(options: DeploySkillOptions): Promise { + header(options.headerTitle); + + const lang = getLanguage(); + const skillResourcesDir = join(getResourcesDir(), options.skillResourceDirName); + const langResourcesDir = getLanguageResourcesDir(lang); + const skillDir = join(homedir(), options.skillRootDir, 'skills', 'takt'); + + if (!existsSync(skillResourcesDir)) { + warn('Skill resources not found. Ensure takt is installed correctly.'); + return; + } + + const skillExists = existsSync(join(skillDir, 'SKILL.md')); + if (skillExists) { + info(options.existingInstallMessage); + const overwrite = await confirm( + '既存のスキルファイルをすべて削除し、最新版に置き換えます。続行しますか?', + false, + ); + if (!overwrite) { + info('キャンセルしました。'); + return; + } + blankLine(); + } + + const copiedFiles: string[] = []; + + copyFile(join(skillResourcesDir, 'SKILL.md'), join(skillDir, 'SKILL.md'), copiedFiles); + + const referencesDestDir = join(skillDir, 'references'); + cleanDir(referencesDestDir); + copyDirRecursive(join(skillResourcesDir, 'references'), referencesDestDir, copiedFiles); + + if (options.includeAgentsDirectory) { + const agentsDestDir = join(skillDir, 'agents'); + cleanDir(agentsDestDir); + copyDirRecursive(join(skillResourcesDir, 'agents'), agentsDestDir, copiedFiles); + } + + for (const dir of DIRECT_DIRS) { + const destDir = join(skillDir, dir); + cleanDir(destDir); + copyDirRecursive(join(langResourcesDir, dir), destDir, copiedFiles); + } + + const facetsDestDir = join(skillDir, 'facets'); + cleanDir(facetsDestDir); + for (const dir of FACET_DIRS) { + copyDirRecursive(join(langResourcesDir, 'facets', dir), join(facetsDestDir, dir), copiedFiles); + } + + blankLine(); + if (copiedFiles.length === 0) { + info('デプロイするファイルがありませんでした。'); + return; + } + + success(`${copiedFiles.length} ファイルをデプロイしました。`); + blankLine(); + + const skillBase = join(homedir(), options.skillRootDir); + const skillFiles = copiedFiles.filter( + (filePath) => + filePath.startsWith(skillDir) + && !filePath.includes('/pieces/') + && !filePath.includes('/facets/') + && !filePath.includes('/templates/') + && !filePath.includes('/references/') + && !filePath.includes('/agents/'), + ); + const referenceFiles = copiedFiles.filter((filePath) => filePath.includes('/references/')); + const agentFiles = copiedFiles.filter((filePath) => filePath.includes('/agents/')); + const pieceFiles = copiedFiles.filter((filePath) => filePath.includes('/pieces/')); + const personaFiles = copiedFiles.filter((filePath) => filePath.includes('/facets/personas/')); + const policyFiles = copiedFiles.filter((filePath) => filePath.includes('/facets/policies/')); + const instructionFiles = copiedFiles.filter((filePath) => filePath.includes('/facets/instructions/')); + const knowledgeFiles = copiedFiles.filter((filePath) => filePath.includes('/facets/knowledge/')); + const outputContractFiles = copiedFiles.filter((filePath) => filePath.includes('/facets/output-contracts/')); + const templateFiles = copiedFiles.filter((filePath) => filePath.includes('/templates/')); + + if (skillFiles.length > 0) { + info(` スキル: ${skillFiles.length} ファイル`); + for (const filePath of skillFiles) { + info(` ${relative(skillBase, filePath)}`); + } + } + if (options.showReferencesSummary && referenceFiles.length > 0) { + info(` 参照資料: ${referenceFiles.length} ファイル`); + } + if (options.showAgentsSummary && agentFiles.length > 0) { + info(` エージェント設定: ${agentFiles.length} ファイル`); + } + if (pieceFiles.length > 0) { + info(` ピース: ${pieceFiles.length} ファイル`); + } + if (personaFiles.length > 0) { + info(` ペルソナ: ${personaFiles.length} ファイル`); + } + if (policyFiles.length > 0) { + info(` ポリシー: ${policyFiles.length} ファイル`); + } + if (instructionFiles.length > 0) { + info(` インストラクション: ${instructionFiles.length} ファイル`); + } + if (knowledgeFiles.length > 0) { + info(` ナレッジ: ${knowledgeFiles.length} ファイル`); + } + if (outputContractFiles.length > 0) { + info(` 出力契約: ${outputContractFiles.length} ファイル`); + } + if (templateFiles.length > 0) { + info(` テンプレート: ${templateFiles.length} ファイル`); + } + + blankLine(); + info(options.usageCommand); + info(options.usageExample); +} + +function cleanDir(dir: string): void { + if (existsSync(dir)) { + rmSync(dir, { recursive: true }); + } +} + +function copyFile(src: string, dest: string, copiedFiles: string[]): void { + if (!existsSync(src)) { + return; + } + + mkdirSync(dirname(dest), { recursive: true }); + writeFileSync(dest, readFileSync(src)); + copiedFiles.push(dest); +} + +function copyDirRecursive(srcDir: string, destDir: string, copiedFiles: string[]): void { + if (!existsSync(srcDir)) { + return; + } + + mkdirSync(destDir, { recursive: true }); + + for (const entry of readdirSync(srcDir)) { + if (SKIP_FILES.has(entry)) { + continue; + } + + const srcPath = join(srcDir, entry); + const destPath = join(destDir, entry); + const stat = statSync(srcPath); + + if (stat.isDirectory()) { + copyDirRecursive(srcPath, destPath, copiedFiles); + continue; + } + + writeFileSync(destPath, readFileSync(srcPath)); + copiedFiles.push(destPath); + } +} diff --git a/src/features/config/index.ts b/src/features/config/index.ts index 2041cc5..510d447 100644 --- a/src/features/config/index.ts +++ b/src/features/config/index.ts @@ -6,3 +6,4 @@ export { ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES } from './e export { resetCategoriesToDefault } from './resetCategories.js'; export { resetConfigToDefault } from './resetConfig.js'; export { deploySkill } from './deploySkill.js'; +export { deploySkillCodex } from './deploySkillCodex.js';