takt: implement-export-codex-skill
This commit is contained in:
parent
16596eff09
commit
a38879dfec
@ -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 prompt [piece]` | Preview assembled prompts for each movement and phase |
|
||||||
| `takt catalog [type]` | List available facets (personas, policies, knowledge, etc.) |
|
| `takt catalog [type]` | List available facets (personas, policies, knowledge, etc.) |
|
||||||
| `takt export-cc` | Export takt pieces/agents as Claude Code Skill (~/.claude/) |
|
| `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 config` | Reset global config to builtin template |
|
||||||
| `takt reset categories` | Reset piece categories to builtin defaults |
|
| `takt reset categories` | Reset piece categories to builtin defaults |
|
||||||
| `takt metrics review` | Show review quality metrics |
|
| `takt metrics review` | Show review quality metrics |
|
||||||
@ -270,6 +271,7 @@ builtins/ # Bundled defaults (builtin, read from dist/ at runtim
|
|||||||
ja/ # Japanese (same structure)
|
ja/ # Japanese (same structure)
|
||||||
project/ # Project-level template files
|
project/ # Project-level template files
|
||||||
skill/ # Claude Code skill 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.
|
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.
|
||||||
|
|||||||
203
builtins/skill-codex/SKILL.md
Normal file
203
builtins/skill-codex/SKILL.md
Normal file
@ -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の構造定義とフィールド説明 |
|
||||||
6
builtins/skill-codex/agents/openai.yaml
Normal file
6
builtins/skill-codex/agents/openai.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "TAKT Piece Engine"
|
||||||
|
short_description: "Multi-agent orchestration via piece YAML workflows"
|
||||||
|
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
228
builtins/skill-codex/references/engine.md
Normal file
228
builtins/skill-codex/references/engine.md
Normal file
@ -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
|
||||||
|
│ ↓
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
224
builtins/skill-codex/references/yaml-schema.md
Normal file
224
builtins/skill-codex/references/yaml-schema.md
Normal file
@ -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` フィールドの方を権限制御に使用する。
|
||||||
@ -251,6 +251,15 @@ takt clear
|
|||||||
takt export-cc
|
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
|
### takt catalog
|
||||||
|
|
||||||
レイヤー間で利用可能なファセットの一覧を表示します。
|
レイヤー間で利用可能なファセットの一覧を表示します。
|
||||||
|
|||||||
@ -251,6 +251,15 @@ Deploy builtin pieces/personas as a Claude Code Skill.
|
|||||||
takt export-cc
|
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
|
### takt catalog
|
||||||
|
|
||||||
List available facets across layers.
|
List available facets across layers.
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
provider: claude
|
provider: claude
|
||||||
language: en
|
language: en
|
||||||
log_level: info
|
logging:
|
||||||
|
level: info
|
||||||
provider_options:
|
provider_options:
|
||||||
codex:
|
codex:
|
||||||
network_access: true
|
network_access: true
|
||||||
|
|||||||
@ -53,6 +53,7 @@ vi.mock('../app/cli/program.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
|
clearPersonaSessions: vi.fn(),
|
||||||
resolveConfigValue: vi.fn(),
|
resolveConfigValue: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -73,7 +74,6 @@ vi.mock('../features/tasks/index.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/config/index.js', () => ({
|
vi.mock('../features/config/index.js', () => ({
|
||||||
clearPersonaSessions: vi.fn(),
|
|
||||||
ejectBuiltin: vi.fn(),
|
ejectBuiltin: vi.fn(),
|
||||||
ejectFacet: vi.fn(),
|
ejectFacet: vi.fn(),
|
||||||
parseFacetType: vi.fn(),
|
parseFacetType: vi.fn(),
|
||||||
@ -81,6 +81,7 @@ vi.mock('../features/config/index.js', () => ({
|
|||||||
resetCategoriesToDefault: vi.fn(),
|
resetCategoriesToDefault: vi.fn(),
|
||||||
resetConfigToDefault: vi.fn(),
|
resetConfigToDefault: vi.fn(),
|
||||||
deploySkill: vi.fn(),
|
deploySkill: vi.fn(),
|
||||||
|
deploySkillCodex: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/prompt/index.js', () => ({
|
vi.mock('../features/prompt/index.js', () => ({
|
||||||
@ -111,6 +112,7 @@ vi.mock('../commands/repertoire/list.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import '../app/cli/commands.js';
|
import '../app/cli/commands.js';
|
||||||
|
const configFeatures = await import('../features/config/index.js');
|
||||||
|
|
||||||
describe('CLI add command', () => {
|
describe('CLI add command', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -151,6 +153,22 @@ describe('CLI add command', () => {
|
|||||||
expect(calledCommandNames).not.toContain('switch');
|
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<string, unknown>).deploySkillCodex;
|
||||||
|
expect(deploySkillCodex).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('should describe prompt piece argument as defaulting to "default"', () => {
|
it('should describe prompt piece argument as defaulting to "default"', () => {
|
||||||
const promptCommand = commandMocks.get('root.prompt');
|
const promptCommand = commandMocks.get('root.prompt');
|
||||||
expect(promptCommand).toBeTruthy();
|
expect(promptCommand).toBeTruthy();
|
||||||
|
|||||||
205
src/__tests__/deploySkillCodex.test.ts
Normal file
205
src/__tests__/deploySkillCodex.test.ts
Normal file
@ -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<string, unknown>).deploySkillCodex as () => Promise<void>;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/__tests__/deploySkillWrappers.test.ts
Normal file
49
src/__tests__/deploySkillWrappers.test.ts
Normal file
@ -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 <piece-name> <task>',
|
||||||
|
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 <piece-name> <task>',
|
||||||
|
usageExample: '例: $takt passthrough "Hello World テスト"',
|
||||||
|
showReferencesSummary: true,
|
||||||
|
includeAgentsDirectory: true,
|
||||||
|
showAgentsSummary: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -84,7 +84,7 @@ describe('createIsolatedEnv', () => {
|
|||||||
const config = parseYaml(configRaw) as Record<string, unknown>;
|
const config = parseYaml(configRaw) as Record<string, unknown>;
|
||||||
|
|
||||||
expect(config.language).toBe('en');
|
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).toBe(false);
|
||||||
expect(config.notification_sound_events).toEqual({
|
expect(config.notification_sound_events).toEqual({
|
||||||
iteration_limit: false,
|
iteration_limit: false,
|
||||||
@ -175,7 +175,8 @@ describe('createIsolatedEnv', () => {
|
|||||||
`${isolated.taktDir}/config.yaml`,
|
`${isolated.taktDir}/config.yaml`,
|
||||||
[
|
[
|
||||||
'language: en',
|
'language: en',
|
||||||
'log_level: info',
|
'logging:',
|
||||||
|
' level: info',
|
||||||
'notification_sound: true',
|
'notification_sound: true',
|
||||||
'notification_sound_events: true',
|
'notification_sound_events: true',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
|
|||||||
58
src/__tests__/skillCodexDocs.test.ts
Normal file
58
src/__tests__/skillCodexDocs.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -9,7 +9,16 @@ import { clearPersonaSessions, resolveConfigValue } from '../../infra/config/ind
|
|||||||
import { getGlobalConfigDir } from '../../infra/config/paths.js';
|
import { getGlobalConfigDir } from '../../infra/config/paths.js';
|
||||||
import { success, info } from '../../shared/ui/index.js';
|
import { success, info } from '../../shared/ui/index.js';
|
||||||
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/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 { previewPrompts } from '../../features/prompt/index.js';
|
||||||
import { showCatalog } from '../../features/catalog/index.js';
|
import { showCatalog } from '../../features/catalog/index.js';
|
||||||
import { computeReviewMetrics, formatReviewMetrics, parseSinceDuration, purgeOldEvents } from '../../features/analytics/index.js';
|
import { computeReviewMetrics, formatReviewMetrics, parseSinceDuration, purgeOldEvents } from '../../features/analytics/index.js';
|
||||||
@ -127,6 +136,13 @@ program
|
|||||||
await deploySkill();
|
await deploySkill();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('export-codex')
|
||||||
|
.description('Export takt pieces/agents as Codex Skill (~/.agents/)')
|
||||||
|
.action(async () => {
|
||||||
|
await deploySkillCodex();
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('catalog')
|
.command('catalog')
|
||||||
.description('List available facets (personas, policies, knowledge, instructions, output-contracts)')
|
.description('List available facets (personas, policies, knowledge, instructions, output-contracts)')
|
||||||
|
|||||||
@ -1,199 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* takt export-cc — Deploy takt skill files to Claude Code.
|
* 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 { deploySkillInternal } from './deploySkillInternal.js';
|
||||||
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';
|
|
||||||
|
|
||||||
/** 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<void> {
|
export async function deploySkill(): Promise<void> {
|
||||||
header('takt export-cc — Deploy to Claude Code');
|
await deploySkillInternal({
|
||||||
|
headerTitle: 'takt export-cc — Deploy to Claude Code',
|
||||||
const lang = getLanguage();
|
skillRootDir: '.claude',
|
||||||
const skillResourcesDir = join(getResourcesDir(), 'skill');
|
skillResourceDirName: 'skill',
|
||||||
const langResourcesDir = getLanguageResourcesDir(lang);
|
existingInstallMessage: 'Claude Code Skill が既にインストールされています。',
|
||||||
const skillDir = getSkillDir();
|
usageCommand: '使い方: /takt <piece-name> <task>',
|
||||||
|
usageExample: '例: /takt passthrough "Hello World テスト"',
|
||||||
// Verify source directories exist
|
showReferencesSummary: false,
|
||||||
if (!existsSync(skillResourcesDir)) {
|
includeAgentsDirectory: false,
|
||||||
warn('Skill resources not found. Ensure takt is installed correctly.');
|
showAgentsSummary: false,
|
||||||
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 <piece-name> <task>');
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/features/config/deploySkillCodex.ts
Normal file
19
src/features/config/deploySkillCodex.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* takt export-codex — Deploy takt skill files to Codex.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { deploySkillInternal } from './deploySkillInternal.js';
|
||||||
|
|
||||||
|
export async function deploySkillCodex(): Promise<void> {
|
||||||
|
await deploySkillInternal({
|
||||||
|
headerTitle: 'takt export-codex — Deploy to Codex',
|
||||||
|
skillRootDir: '.agents',
|
||||||
|
skillResourceDirName: 'skill-codex',
|
||||||
|
existingInstallMessage: 'Codex Skill が既にインストールされています。',
|
||||||
|
usageCommand: '使い方: $takt <piece-name> <task>',
|
||||||
|
usageExample: '例: $takt passthrough "Hello World テスト"',
|
||||||
|
showReferencesSummary: true,
|
||||||
|
includeAgentsDirectory: true,
|
||||||
|
showAgentsSummary: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
187
src/features/config/deploySkillInternal.ts
Normal file
187
src/features/config/deploySkillInternal.ts
Normal file
@ -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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,3 +6,4 @@ export { ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES } from './e
|
|||||||
export { resetCategoriesToDefault } from './resetCategories.js';
|
export { resetCategoriesToDefault } from './resetCategories.js';
|
||||||
export { resetConfigToDefault } from './resetConfig.js';
|
export { resetConfigToDefault } from './resetConfig.js';
|
||||||
export { deploySkill } from './deploySkill.js';
|
export { deploySkill } from './deploySkill.js';
|
||||||
|
export { deploySkillCodex } from './deploySkillCodex.js';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user