From ec983089f9910412b55d1d30ffbbdfa42f861146 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:56:12 +0900 Subject: [PATCH] =?UTF-8?q?takt=20export-cc=20=E3=82=B3=E3=83=9E=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0:=20=E3=83=93=E3=83=AB?= =?UTF-8?q?=E3=83=88=E3=82=A4=E3=83=B3=E3=83=94=E3=83=BC=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A8=E3=83=BC=E3=82=B8=E3=82=A7=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=20Claude=20Code=20Skill=20=E3=81=A8=E3=81=97=E3=81=A6=E3=83=87?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/skill/SKILL.md | 105 ++++++ resources/skill/references/engine.md | 375 ++++++++++++++++++++++ resources/skill/references/yaml-schema.md | 164 ++++++++++ resources/skill/takt-command.md | 28 ++ src/app/cli/commands.ts | 9 +- src/features/config/deploySkill.ts | 165 ++++++++++ src/features/config/index.ts | 1 + 7 files changed, 846 insertions(+), 1 deletion(-) create mode 100644 resources/skill/SKILL.md create mode 100644 resources/skill/references/engine.md create mode 100644 resources/skill/references/yaml-schema.md create mode 100644 resources/skill/takt-command.md create mode 100644 src/features/config/deploySkill.ts diff --git a/resources/skill/SKILL.md b/resources/skill/SKILL.md new file mode 100644 index 0000000..a4d017f --- /dev/null +++ b/resources/skill/SKILL.md @@ -0,0 +1,105 @@ +--- +name: takt-engine +description: TAKT ピースエンジンのリファレンス。/takt コマンドから使用される。ピースYAML定義に従ったマルチエージェントオーケストレーション。 +--- + +# TAKT Piece Engine + +ピースYAMLファイルを読み込み、定義されたワークフロー(状態遷移マシン)に従って複数のAIエージェントをオーケストレーションするエンジン。 + +## 設計原則 + +- **Skill = 純粋なエンジンロジックのみ** +- ピースYAML、エージェント .md は全て **ファイル参照** で実行時に Read する +- Skill 内にピース定義・エージェント定義を一切埋め込まない +- このスキルが持つのは「YAMLの読み方」「状態遷移の回し方」「ルール評価の仕方」だけ + +## ピース解決 + +### ピースYAMLの検索 + +引数の第1トークンからピースYAMLファイルを特定する。 + +1. ファイルパス判定: `.yaml` / `.yml` で終わる、または `/` を含む → Read で直接読む +2. ピース名検索(以下の順で Glob/Read を試行): + - `~/.takt/pieces/{name}.yaml` (ユーザーカスタム、優先) + - `~/.claude/skills/takt/pieces/{name}.yaml` (Skill同梱ビルトイン) +3. 見つからない場合: 上記2ディレクトリを Glob (`*.yaml`) で列挙し、AskUserQuestion で選択させる + +### エージェント .md の解決 + +ピースYAML内の `agent:` フィールドは、**ピースYAMLファイルのディレクトリからの相対パス**。 + +例: ピースが `~/.claude/skills/takt/pieces/default.yaml` にあり、`agent: ../agents/default/coder.md` の場合 +→ 絶対パスは `~/.claude/skills/takt/agents/default/coder.md` + +解決手順: +1. ピースYAMLのディレクトリパスを取得 +2. 各 movement の `agent:` の相対パスを絶対パスに変換 +3. Read tool で .md ファイルの内容を読み込む +4. 読み込んだ内容をエージェントのシステムプロンプトとして使用する + +**全てのエージェント .md を事前に読み込む**(状態遷移ループ開始前に)。 + +## 実行フロー + +### ステップ 1: ピースYAMLの読み込みと解析 + +1. ピース解決でYAMLファイルパスを特定し、Read で読み込む +2. YAML内容を解析して以下を抽出する(→ references/yaml-schema.md 参照): + - `name`: ピース名 + - `max_iterations`: 最大イテレーション数 + - `initial_movement`: 開始 movement 名 + - `movements`: 全 movement 定義の配列 + +### ステップ 2: エージェントの事前読み込み + +全 movement(parallel のサブステップ含む)から `agent:` パスを収集し、重複を除いて Read で読み込む。 + +### ステップ 3: 状態遷移ループ + +**詳細は references/engine.md を参照。** + +``` +iteration = 0 +current_movement = initial_movement +previous_response = "" + +LOOP: + iteration++ + if iteration > max_iterations → 強制終了(ABORT) + + movement = movements[current_movement] を取得 + + if movement が parallel: + → 並列実行(engine.md の「Parallel Movement の実行」参照) + else: + → 通常実行(engine.md の「通常 Movement の実行」参照) + + agent_output を取得 + + rule 評価(engine.md の「Rule 評価」参照) + → matched_rule を決定 + + next = matched_rule.next + + if next == "COMPLETE" → 成功終了、ユーザーに結果を報告 + if next == "ABORT" → 失敗終了、ユーザーにエラーを報告 + + previous_response = agent_output + current_movement = next + → LOOP に戻る +``` + +### ステップ 4: 完了報告 + +- COMPLETE: 最後の agent 出力のサマリーをユーザーに表示 +- ABORT: 失敗理由をユーザーに表示 +- max_iterations 到達: 強制終了を通知 + +## 詳細リファレンス + +| ファイル | 内容 | +|---------|------| +| `references/engine.md` | Movement 実行、プロンプト構築、Rule 評価の詳細ロジック | +| `references/yaml-schema.md` | ピースYAMLの構造定義とフィールド説明 | diff --git a/resources/skill/references/engine.md b/resources/skill/references/engine.md new file mode 100644 index 0000000..beaa2aa --- /dev/null +++ b/resources/skill/references/engine.md @@ -0,0 +1,375 @@ +# TAKT 実行エンジン詳細 + +## 通常 Movement の実行 + +通常の movement(`parallel` フィールドを持たない movement)は、Task tool で1つのエージェントを起動する。 + +### Task tool の呼び出し + +``` +Task tool: + subagent_type: "general-purpose" + description: "{movement名} - {ピース名}" (3-5語) + prompt: <後述のプロンプト構築で組み立てた内容> + mode: +``` + +### permission_mode の決定 + +movement の `edit` フィールドと `permission_mode` フィールドから決定する: + +| edit | permission_mode | Task tool の mode | +|------|----------------|-------------------| +| true | 未指定 | "bypassPermissions" | +| true | "edit" | "bypassPermissions" | +| true | "full" | "bypassPermissions" | +| false | 未指定 | "default" | +| false | "readonly" | "default" | + +`edit: false` の movement は読み取り専用。`edit: true` の movement はファイル編集が可能。 + +## Parallel Movement の実行 + +`parallel` フィールドを持つ movement は、複数のサブステップを並列実行する。 + +### 実行手順 + +1. parallel 配列の各サブステップに対して Task tool を起動する +2. **全ての Task tool を1つのメッセージで並列に呼び出す**(依存関係がないため) +3. 全エージェントの完了を待つ +4. 各サブステップの出力を収集する +5. 各サブステップの出力に対して、そのサブステップの `rules` で条件マッチを判定する +6. 親 movement の `rules` で aggregate 評価(all()/any())を行う + +### サブステップの条件マッチ判定 + +各サブステップの出力テキストに対して、そのサブステップの `rules` の中からマッチする condition を特定する。 + +判定方法(通常 movement の Rule 評価と同じ優先順位): +1. `[STEP:N]` タグがあればインデックスで照合(最後のタグを採用) +2. タグがなければ、出力全体を読んでどの condition に最も近いかを判断する + +マッチした condition 文字列を記録する(次の aggregate 評価で使う)。 + +## プロンプト構築 + +各 movement のエージェント起動時、以下を結合してプロンプトを組み立てる。 + +### 構成要素(上から順に結合) + +``` +1. エージェントプロンプト(agent: で参照される .md の全内容) +2. ---(区切り線) +3. 実行コンテキスト情報 +4. instruction_template の内容(テンプレート変数を展開済み) +5. ユーザーのタスク({task} が template に含まれない場合、末尾に自動追加) +6. 前の movement の出力(pass_previous_response: true の場合、自動追加) +7. レポート出力指示(report フィールドがある場合、自動追加) +8. ステータスタグ出力指示(rules がある場合、自動追加) +``` + +### 実行コンテキスト情報 + +``` +## 実行コンテキスト +- ワーキングディレクトリ: {cwd} +- ピース: {piece_name} +- Movement: {movement_name} +- イテレーション: {iteration} / {max_iterations} +- Movement イテレーション: {movement_iteration} 回目 +``` + +### テンプレート変数の展開 + +`instruction_template` 内の以下のプレースホルダーを置換する: + +| 変数 | 値 | +|-----|-----| +| `{task}` | ユーザーが入力したタスク内容 | +| `{previous_response}` | 前の movement のエージェント出力 | +| `{iteration}` | ピース全体のイテレーション数(1始まり) | +| `{max_iterations}` | ピースの max_iterations 値 | +| `{movement_iteration}` | この movement が実行された回数(1始まり) | +| `{report_dir}` | レポートディレクトリパス | +| `{report:ファイル名}` | 指定レポートファイルの内容(Read で取得) | + +### {report:ファイル名} の処理 + +`instruction_template` 内に `{report:04-ai-review.md}` のような記法がある場合: +1. レポートディレクトリ内に対応するレポートファイルがあれば Read で読む +2. 読み込んだ内容をプレースホルダーに展開する +3. ファイルが存在しない場合は「(レポート未作成)」に置換する + +### agent フィールドがない場合 + +`agent:` が指定されていない movement の場合、エージェントプロンプト部分を省略し、`instruction_template` の内容のみでプロンプトを構成する。 + +## レポート出力指示の自動注入 + +movement に `report` フィールドがある場合、プロンプト末尾にレポート出力指示を自動追加する。これにより、takt 本体の Phase 2(レポート出力フェーズ)を1回の呼び出しに統合する。 + +### 形式1: name + format + +```yaml +report: + name: 01-plan.md + format: | + # タスク計画 + ## 元の要求 + ... +``` + +→ プロンプトに追加する指示: + +``` +--- +## レポート出力(必須) +作業完了後、以下のフォーマットに従ってレポートを出力してください。 +レポートは ```markdown ブロックで囲んで出力してください。 + +ファイル名: 01-plan.md +フォーマット: +# タスク計画 +## 元の要求 +... +``` + +### 形式2: 配列(複数レポート) + +```yaml +report: + - Summary: summary.md + - Scope: 01-scope.md +``` + +→ プロンプトに追加する指示: + +``` +--- +## レポート出力(必須) +作業完了後、以下の各レポートを出力してください。 +各レポートは見出し付きの ```markdown ブロックで囲んで出力してください。 + +1. Summary → ファイル名: summary.md +2. Scope → ファイル名: 01-scope.md +``` + +### レポートの抽出と保存 + +エージェントの出力からレポート内容を抽出し、Write tool でレポートディレクトリに保存する。 + +**レポートディレクトリ**: `.takt/reports/{timestamp}-{slug}/` に作成する(takt 本体と同じ構造)。 +- `{timestamp}`: `YYYYMMDD-HHmmss` 形式 +- `{slug}`: タスク内容の先頭30文字をスラグ化 + +抽出方法: +- 出力内の ```markdown ブロックからレポート内容を取得する +- ファイル名の手がかり(見出しやコメント)から対応するレポートを特定する +- 特定できない場合は出力全体をレポートとして保存する + +## ステータスタグ出力指示の自動注入 + +movement に `rules` がある場合、プロンプト末尾にステータスタグ出力指示を自動追加する。これにより、takt 本体の Phase 3(ステータス判定フェーズ)を1回の呼び出しに統合する。 + +### 注入する指示 + +``` +--- +## ステータス出力(必須) +全ての作業とレポート出力が完了した後、最後に以下のいずれかのタグを出力してください。 +あなたの作業結果に最も合致するものを1つだけ選んでください。 + +[STEP:0] = {rules[0].condition} +[STEP:1] = {rules[1].condition} +[STEP:2] = {rules[2].condition} +... +``` + +### ai() 条件の場合 + +condition が `ai("条件テキスト")` 形式の場合でも、同じくタグ出力指示に含める: + +``` +[STEP:0] = 条件テキスト +[STEP:1] = 別の条件テキスト +``` + +これにより、エージェントが自ら判断して適切なタグを出力する。ai() の括弧は除去して condition テキストのみを表示する。 + +### サブステップの場合 + +parallel のサブステップにも同様にタグ出力指示を注入する。サブステップの rules からタグリストを生成する。 + +## Rule 評価 + +movement 実行後、エージェントの出力テキストからどの rule にマッチするかを判定する。 + +### 通常 Movement の Rule 評価 + +判定優先順位(最初にマッチしたものを採用): + +#### 1. タグベース検出(優先) + +エージェント出力に `[STEP:N]` タグ(N は 0始まりのインデックス)が含まれる場合、そのインデックスに対応する rule を選択する。複数のタグがある場合は **最後のタグ** を採用する。 + +例: rules が `["タスク完了", "進行できない"]` で出力に `[STEP:0]` → "タスク完了" を選択 + +#### 2. フォールバック(AI 判定) + +タグが出力に含まれない場合、出力テキスト全体を読み、全ての condition と比較して最もマッチするものを選択する。 + +**出力テキストの判定例**: +- rules: `["実装完了", "判断できない"]` +- 出力: 「全てのファイルを修正し、テストもパスしました。」 +- → "実装完了" にマッチ + +### Parallel Movement の Rule 評価(Aggregate) + +親 movement の rules に `all()` / `any()` の aggregate 条件を使用する。 + +#### all() の評価 + +```yaml +- condition: all("approved") + next: COMPLETE +``` + +**引数が1つ**: 全サブステップのマッチ条件が "approved" であれば true。 + +```yaml +- condition: all("AI特有の問題なし", "すべて問題なし") + next: COMPLETE +``` + +**引数が複数(位置対応)**: サブステップ1が "AI特有の問題なし" にマッチ AND サブステップ2が "すべて問題なし" にマッチ であれば true。 + +#### any() の評価 + +```yaml +- condition: any("needs_fix") + next: fix +``` + +いずれかのサブステップのマッチ条件が "needs_fix" であれば true。 + +```yaml +- condition: any("AI特有の問題あり") + next: ai_fix +``` + +**引数が1つ**: いずれかのサブステップが "AI特有の問題あり" にマッチすれば true。 + +#### Aggregate 評価の順序 + +親 rules を上から順に評価し、最初にマッチした rule を採用する。 + +### Rule にマッチしない場合 + +全ての rule を評価してもマッチしない場合は ABORT する。エラーメッセージとともに、マッチしなかった出力の要約をユーザーに報告する。 + +## ループ検出 + +### 基本ルール + +- 同じ movement が連続3回以上実行されたら警告を表示する +- `max_iterations` に到達したら強制終了(ABORT)する + +### カウンター管理 + +以下のカウンターを管理する: + +| カウンター | 説明 | リセットタイミング | +|-----------|------|-------------------| +| `iteration` | ピース全体の movement 実行回数 | リセットしない | +| `movement_iteration[name]` | 各 movement の実行回数 | リセットしない | +| `consecutive_count[name]` | 同じ movement の連続実行回数 | 別の movement に遷移したとき | + +## Loop Monitors + +ピースに `loop_monitors` が定義されている場合、特定の movement サイクルを監視する。 + +### 動作 + +```yaml +loop_monitors: + - cycle: [ai_review, ai_fix] + threshold: 3 + judge: + agent: ../agents/default/supervisor.md + instruction_template: | + サイクルが {cycle_count} 回繰り返されました... + rules: + - condition: 健全 + next: ai_review + - condition: 非生産的 + next: reviewers +``` + +### 検出ロジック + +1. movement 遷移履歴を記録する(例: `[plan, implement, ai_review, ai_fix, ai_review, ai_fix, ...]`) +2. 各 loop_monitor の `cycle` パターン(例: `[ai_review, ai_fix]`)が履歴の末尾に `threshold` 回以上連続で出現するかチェックする +3. 閾値に達した場合: + a. judge の `agent` を Read で読み込む + b. `instruction_template` の `{cycle_count}` を実際のサイクル回数に置換する + c. Task tool で judge エージェントを起動する + d. judge の出力を judge の `rules` で評価する + e. マッチした rule の `next` に遷移する(通常のルール評価をオーバーライドする) + +## レポート管理 + +### レポートディレクトリの作成 + +ピース実行開始時にレポートディレクトリを作成する: + +``` +.takt/reports/{YYYYMMDD-HHmmss}-{slug}/ +``` + +このパスを `{report_dir}` 変数として全 movement から参照可能にする。 + +### レポートの保存 + +エージェント出力からレポート内容を抽出し、Write tool でレポートディレクトリに保存する。 + +抽出手順: +1. 出力内の ```markdown ブロックを検索する +2. レポートのファイル名やセクション見出しから対応するレポートを特定する +3. Write tool で `{report_dir}/{ファイル名}` に保存する + +### レポートの参照 + +後続の movement の `instruction_template` 内で `{report:ファイル名}` として参照すると、engine がそのレポートファイルを Read して内容をプレースホルダーに展開する。 + +## 状態遷移の全体像 + +``` +[開始] + ↓ +レポートディレクトリ作成 + ↓ +initial_movement を取得 + ↓ +┌─→ movement を実行 +│ ├── 通常: Task tool (1エージェント) +│ │ prompt = agent.md + context + instruction + task +│ │ + previous_response + レポート指示 + タグ指示 +│ └── parallel: Task tool (複数エージェント並列) +│ 各サブステップも同様のプロンプト構築 +│ ↓ +│ 出力からレポート抽出 → Write で保存 +│ ↓ +│ Loop Monitor チェック(該当サイクルがあれば judge 介入) +│ ↓ +│ Rule 評価 +│ ├── タグ検出 [STEP:N] → rule 選択 +│ └── タグなし → AI フォールバック判定 +│ ├── parallel: サブステップ条件 → aggregate(all/any) +│ ↓ +│ next を決定 +│ ├── COMPLETE → [成功終了] ユーザーに結果報告 +│ ├── ABORT → [失敗終了] ユーザーにエラー報告 +│ └── movement名 → ループ検出チェック → 次の movement +│ ↓ +└──────────────────────────────────────────────┘ +``` diff --git a/resources/skill/references/yaml-schema.md b/resources/skill/references/yaml-schema.md new file mode 100644 index 0000000..edd455f --- /dev/null +++ b/resources/skill/references/yaml-schema.md @@ -0,0 +1,164 @@ +# ピースYAML スキーマリファレンス + +このドキュメントはピースYAMLの構造を定義する。具体的なピース定義は含まない。 + +## トップレベルフィールド + +```yaml +name: piece-name # ピース名(必須) +description: 説明テキスト # ピースの説明(任意) +max_iterations: 10 # 最大イテレーション数(必須) +initial_movement: plan # 最初に実行する movement 名(必須) +movements: [...] # movement 定義の配列(必須) +loop_monitors: [...] # ループ監視設定(任意) +``` + +## Movement 定義 + +### 通常 Movement + +```yaml +- name: movement-name # movement 名(必須、一意) + agent: ../agents/path.md # エージェントプロンプトへの相対パス(任意) + agent_name: coder # 表示名(任意) + edit: true # ファイル編集可否(必須) + permission_mode: edit # 権限モード: edit / readonly / full(任意) + session: refresh # セッション管理(任意) + pass_previous_response: true # 前の出力を渡すか(デフォルト: true) + allowed_tools: [...] # 許可ツール一覧(任意、参考情報) + instruction_template: | # ステップ固有の指示テンプレート(任意) + 指示内容... + report: ... # レポート設定(任意) + rules: [...] # 遷移ルール(必須) +``` + +### Parallel Movement + +```yaml +- name: reviewers # 親 movement 名(必須) + parallel: # 並列サブステップ配列(これがあると parallel movement) + - name: sub-step-1 # サブステップ名 + agent: ../agents/a.md + edit: false + instruction_template: | + ... + rules: # サブステップの rules(condition のみ、next は無視される) + - condition: "approved" + - condition: "needs_fix" + # report, allowed_tools 等も指定可能 + + - name: sub-step-2 + agent: ../agents/b.md + edit: false + instruction_template: | + ... + rules: + - condition: "passed" + - condition: "failed" + + rules: # 親の rules(aggregate 条件で遷移先を決定) + - condition: all("approved", "passed") + next: complete-step + - condition: any("needs_fix", "failed") + next: fix-step +``` + +**重要**: サブステップの `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 に遷移 | + +## Report 定義 + +### 形式1: 単一レポート(name + format) + +```yaml +report: + name: 01-plan.md + format: | + ```markdown + # レポートタイトル + ## セクション + {内容} + ``` +``` + +`format` はエージェントへの出力フォーマット指示。レポート抽出時の参考情報。 + +### 形式2: 複数レポート(配列) + +```yaml +report: + - Summary: summary.md + - Scope: 01-scope.md + - Decisions: 02-decisions.md +``` + +各要素のキーがレポート種別名、値がファイル名。 + +## テンプレート変数 + +`instruction_template` 内で使用可能な変数: + +| 変数 | 説明 | +|-----|------| +| `{task}` | ユーザーのタスク入力(template に含まれない場合は自動追加) | +| `{previous_response}` | 前の movement の出力(pass_previous_response: true 時、自動追加) | +| `{iteration}` | ピース全体のイテレーション数 | +| `{max_iterations}` | 最大イテレーション数 | +| `{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: + agent: ../agents/supervisor.md # 判定エージェント + 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/resources/skill/takt-command.md b/resources/skill/takt-command.md new file mode 100644 index 0000000..d8f1de2 --- /dev/null +++ b/resources/skill/takt-command.md @@ -0,0 +1,28 @@ +--- +name: takt +description: TAKT ピースランナー。ピースYAMLワークフローに従ってマルチエージェントを実行する。 +--- + +TAKT ピースランナーを実行する。 + +## 引数 + +$ARGUMENTS を以下のように解析する: + +- 第1トークン: ピース名またはYAMLファイルパス(必須) +- 残りのトークン: タスク内容(省略時は AskUserQuestion でユーザーに入力を求める) + +例: +- `/takt passthrough タスクを実行` +- `/takt default src/foo.ts のバグを修正` +- `/takt /path/to/custom.yaml 実装して` + +## 実行手順 + +以下のファイルを **Read tool で読み込み**、記載された手順に従って実行する: + +1. `~/.claude/skills/takt/SKILL.md` - エンジン概要とピース解決 +2. `~/.claude/skills/takt/references/engine.md` - 実行エンジンの詳細ロジック +3. `~/.claude/skills/takt/references/yaml-schema.md` - ピースYAML構造リファレンス + +**重要**: これら3ファイルを最初に全て読み込んでから、SKILL.md の「実行フロー」に従って処理を開始する。 diff --git a/src/app/cli/commands.ts b/src/app/cli/commands.ts index dabb185..b0fab4c 100644 --- a/src/app/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -7,7 +7,7 @@ import { clearAgentSessions, getCurrentPiece } from '../../infra/config/index.js'; import { success } from '../../shared/ui/index.js'; import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js'; -import { switchPiece, switchConfig, ejectBuiltin, resetCategoriesToDefault } from '../../features/config/index.js'; +import { switchPiece, switchConfig, ejectBuiltin, resetCategoriesToDefault, deploySkill } from '../../features/config/index.js'; import { previewPrompts } from '../../features/prompt/index.js'; import { program, resolvedCwd } from './program.js'; import { resolveAgentOverrides } from './helpers.js'; @@ -108,3 +108,10 @@ program .action(async (piece?: string) => { await previewPrompts(resolvedCwd, piece); }); + +program + .command('export-cc') + .description('Export takt pieces/agents as Claude Code Skill (~/.claude/)') + .action(async () => { + await deploySkill(); + }); diff --git a/src/features/config/deploySkill.ts b/src/features/config/deploySkill.ts new file mode 100644 index 0000000..88094b6 --- /dev/null +++ b/src/features/config/deploySkill.ts @@ -0,0 +1,165 @@ +/** + * takt export-cc — Deploy takt pieces and agents as Claude Code Skill. + * + * Copies the following to ~/.claude/: + * commands/takt.md — /takt command entry point + * skills/takt/SKILL.md — Engine overview + * skills/takt/references/ — Engine logic + YAML schema + * skills/takt/pieces/ — Builtin piece YAML files + * skills/takt/agents/ — Builtin agent .md files + * + * Piece YAML agent paths (../agents/...) work as-is because + * the directory structure is mirrored. + */ + +import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join, dirname, relative } from 'node:path'; + +import { + getBuiltinPiecesDir, + getBuiltinAgentsDir, + getLanguage, +} from '../../infra/config/index.js'; +import { getResourcesDir } 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'); +} + +function getCommandDir(): string { + return join(homedir(), '.claude', 'commands'); +} + +/** + * 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 builtinPiecesDir = getBuiltinPiecesDir(lang); + const builtinAgentsDir = getBuiltinAgentsDir(lang); + const skillDir = getSkillDir(); + const commandDir = getCommandDir(); + + // 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 command file: ~/.claude/commands/takt.md + const commandSrc = join(skillResourcesDir, 'takt-command.md'); + const commandDest = join(commandDir, 'takt.md'); + copyFile(commandSrc, commandDest, copiedFiles); + + // 2. Deploy SKILL.md + const skillSrc = join(skillResourcesDir, 'SKILL.md'); + const skillDest = join(skillDir, 'SKILL.md'); + copyFile(skillSrc, skillDest, copiedFiles); + + // 3. Deploy references/ (engine.md, yaml-schema.md) + const refsSrcDir = join(skillResourcesDir, 'references'); + const refsDestDir = join(skillDir, 'references'); + copyDirRecursive(refsSrcDir, refsDestDir, copiedFiles); + + // 4. Deploy builtin piece YAMLs → skills/takt/pieces/ + const piecesDestDir = join(skillDir, 'pieces'); + copyDirRecursive(builtinPiecesDir, piecesDestDir, copiedFiles); + + // 5. Deploy builtin agent .md files → skills/takt/agents/ + const agentsDestDir = join(skillDir, 'agents'); + copyDirRecursive(builtinAgentsDir, agentsDestDir, copiedFiles); + + // Report results + blankLine(); + if (copiedFiles.length > 0) { + success(`${copiedFiles.length} ファイルをデプロイしました。`); + blankLine(); + + // Show summary by category + const skillBase = join(homedir(), '.claude'); + const commandFiles = copiedFiles.filter((f) => f.startsWith(commandDir)); + const skillFiles = copiedFiles.filter( + (f) => f.startsWith(skillDir) && !f.includes('/pieces/') && !f.includes('/agents/'), + ); + const pieceFiles = copiedFiles.filter((f) => f.includes('/pieces/')); + const agentFiles = copiedFiles.filter((f) => f.includes('/agents/')); + + if (commandFiles.length > 0) { + info(` コマンド: ${commandFiles.length} ファイル`); + for (const f of commandFiles) { + info(` ${relative(skillBase, f)}`); + } + } + if (skillFiles.length > 0) { + info(` スキル: ${skillFiles.length} ファイル`); + for (const f of skillFiles) { + info(` ${relative(skillBase, f)}`); + } + } + if (pieceFiles.length > 0) { + info(` ピース: ${pieceFiles.length} ファイル`); + } + if (agentFiles.length > 0) { + info(` エージェント: ${agentFiles.length} ファイル`); + } + + blankLine(); + info('使い方: /takt '); + info('例: /takt passthrough "Hello World テスト"'); + } else { + info('デプロイするファイルがありませんでした。'); + } +} + +/** 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); + } + } +} diff --git a/src/features/config/index.ts b/src/features/config/index.ts index e5bd085..39b2ccc 100644 --- a/src/features/config/index.ts +++ b/src/features/config/index.ts @@ -6,3 +6,4 @@ export { switchPiece } from './switchPiece.js'; export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './switchConfig.js'; export { ejectBuiltin } from './ejectBuiltin.js'; export { resetCategoriesToDefault } from './resetCategories.js'; +export { deploySkill } from './deploySkill.js';