takt export-cc コマンドを追加: ビルトインピース・エージェントを Claude Code Skill としてデプロイ

This commit is contained in:
nrslib 2026-02-06 08:56:12 +09:00
parent 378f5477e4
commit ec983089f9
7 changed files with 846 additions and 1 deletions

105
resources/skill/SKILL.md Normal file
View File

@ -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: エージェントの事前読み込み
全 movementparallel のサブステップ含む)から `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の構造定義とフィールド説明 |

View File

@ -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 から決定>
```
### 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
│ ↓
└──────────────────────────────────────────────┘
```

View File

@ -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: # サブステップの rulescondition のみ、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: # 親の rulesaggregate 条件で遷移先を決定)
- 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` フィールドの方を権限制御に使用する。

View File

@ -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 の「実行フロー」に従って処理を開始する。

View File

@ -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();
});

View File

@ -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<void> {
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 <piece-name> <task>');
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);
}
}
}

View File

@ -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';