Compare commits

...

1 Commits

Author SHA1 Message Date
nrslib
a38879dfec takt: implement-export-codex-skill 2026-03-06 08:58:41 +09:00
18 changed files with 1253 additions and 197 deletions

View File

@ -40,6 +40,7 @@ TAKT (TAKT Agent Koordination Topology) is a multi-agent orchestration system fo
| `takt prompt [piece]` | Preview assembled prompts for each movement and phase |
| `takt catalog [type]` | List available facets (personas, policies, knowledge, etc.) |
| `takt export-cc` | Export takt pieces/agents as Claude Code Skill (~/.claude/) |
| `takt export-codex` | Export takt skill files to Codex Skill (~/.agents/skills/takt/) |
| `takt reset config` | Reset global config to builtin template |
| `takt reset categories` | Reset piece categories to builtin defaults |
| `takt metrics review` | Show review quality metrics |
@ -270,6 +271,7 @@ builtins/ # Bundled defaults (builtin, read from dist/ at runtim
ja/ # Japanese (same structure)
project/ # Project-level template files
skill/ # Claude Code skill files
skill-codex/ # Codex skill files
```
Builtin resources are embedded in the npm package (`builtins/`). Project files in `.takt/` take highest priority, then user files in `~/.takt/`, then builtins. Use `takt eject` to copy builtins for customization.

View 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" は YOLOYou 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 を超えていたら → 手順 7ABORT: イテレーション上限)に進む。**
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` → **手順 7COMPLETE**
- `next = ABORT` → **手順 7ABORT**
- `next = movement 名``previous_response` 更新、`iteration += 1`、次 movement を取得して **手順 4** に戻る
どの rule にもマッチしなかったら → **手順 7ABORT: ルール不一致)** に進む。
### 手順 7: 終了
ユーザーに結果を報告する:
- **COMPLETE**: 最後のサブエージェント出力のサマリーを表示
- **ABORT**: 失敗理由を表示
- **イテレーション上限**: 強制終了を通知
## 詳細リファレンス
| ファイル | 内容 |
|---------|------|
| `references/engine.md` | プロンプト構築、レポート管理、ループ検出の詳細 |
| `references/yaml-schema.md` | ピースYAMLの構造定義とフィールド説明 |

View 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

View 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 MovementAggregate
- `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
│ ↓
└──────────────────────┘
```

View 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: # 親の rulesaggregate 条件で遷移先を決定)
- 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` フィールドの方を権限制御に使用する。

View File

@ -251,6 +251,15 @@ takt clear
takt export-cc
```
### takt export-codex
TAKT のスキルファイルを Codex Skill`~/.agents/skills/takt/`)としてデプロイします。
このコマンドは `SKILL.md``references/``agents/``pieces/``facets/``templates/` をデプロイします。
```bash
takt export-codex
```
### takt catalog
レイヤー間で利用可能なファセットの一覧を表示します。

View File

@ -251,6 +251,15 @@ Deploy builtin pieces/personas as a Claude Code Skill.
takt export-cc
```
### takt export-codex
Deploy TAKT skill files as a Codex Skill (`~/.agents/skills/takt/`).
This command deploys `SKILL.md`, `references/`, `agents/`, `pieces/`, `facets/`, and `templates/`.
```bash
takt export-codex
```
### takt catalog
List available facets across layers.

View File

@ -1,6 +1,7 @@
provider: claude
language: en
log_level: info
logging:
level: info
provider_options:
codex:
network_access: true

View File

@ -53,6 +53,7 @@ vi.mock('../app/cli/program.js', () => ({
}));
vi.mock('../infra/config/index.js', () => ({
clearPersonaSessions: vi.fn(),
resolveConfigValue: vi.fn(),
}));
@ -73,7 +74,6 @@ vi.mock('../features/tasks/index.js', () => ({
}));
vi.mock('../features/config/index.js', () => ({
clearPersonaSessions: vi.fn(),
ejectBuiltin: vi.fn(),
ejectFacet: vi.fn(),
parseFacetType: vi.fn(),
@ -81,6 +81,7 @@ vi.mock('../features/config/index.js', () => ({
resetCategoriesToDefault: vi.fn(),
resetConfigToDefault: vi.fn(),
deploySkill: vi.fn(),
deploySkillCodex: vi.fn(),
}));
vi.mock('../features/prompt/index.js', () => ({
@ -111,6 +112,7 @@ vi.mock('../commands/repertoire/list.js', () => ({
}));
import '../app/cli/commands.js';
const configFeatures = await import('../features/config/index.js');
describe('CLI add command', () => {
beforeEach(() => {
@ -151,6 +153,22 @@ describe('CLI add command', () => {
expect(calledCommandNames).not.toContain('switch');
});
it('should register export-codex command', () => {
const calledCommandNames = rootCommand.command.mock.calls
.map((call: unknown[]) => call[0] as string);
expect(calledCommandNames).toContain('export-codex');
});
it('should invoke deploySkillCodex for export-codex command', async () => {
const exportCodexAction = commandActions.get('root.export-codex');
expect(exportCodexAction).toBeTypeOf('function');
await exportCodexAction?.();
const deploySkillCodex = (configFeatures as Record<string, unknown>).deploySkillCodex;
expect(deploySkillCodex).toHaveBeenCalledTimes(1);
});
it('should describe prompt piece argument as defaulting to "default"', () => {
const promptCommand = commandMocks.get('root.prompt');
expect(promptCommand).toBeTruthy();

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

View 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,
});
});
});

View File

@ -84,7 +84,7 @@ describe('createIsolatedEnv', () => {
const config = parseYaml(configRaw) as Record<string, unknown>;
expect(config.language).toBe('en');
expect(config.log_level).toBe('info');
expect(config.logging).toEqual({ level: 'info' });
expect(config.notification_sound).toBe(false);
expect(config.notification_sound_events).toEqual({
iteration_limit: false,
@ -175,7 +175,8 @@ describe('createIsolatedEnv', () => {
`${isolated.taktDir}/config.yaml`,
[
'language: en',
'log_level: info',
'logging:',
' level: info',
'notification_sound: true',
'notification_sound_events: true',
].join('\n'),

View 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);
});
});

View File

@ -9,7 +9,16 @@ import { clearPersonaSessions, resolveConfigValue } from '../../infra/config/ind
import { getGlobalConfigDir } from '../../infra/config/paths.js';
import { success, info } from '../../shared/ui/index.js';
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
import { ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES, resetCategoriesToDefault, resetConfigToDefault, deploySkill } from '../../features/config/index.js';
import {
ejectBuiltin,
ejectFacet,
parseFacetType,
VALID_FACET_TYPES,
resetCategoriesToDefault,
resetConfigToDefault,
deploySkill,
deploySkillCodex,
} from '../../features/config/index.js';
import { previewPrompts } from '../../features/prompt/index.js';
import { showCatalog } from '../../features/catalog/index.js';
import { computeReviewMetrics, formatReviewMetrics, parseSinceDuration, purgeOldEvents } from '../../features/analytics/index.js';
@ -127,6 +136,13 @@ program
await deploySkill();
});
program
.command('export-codex')
.description('Export takt pieces/agents as Codex Skill (~/.agents/)')
.action(async () => {
await deploySkillCodex();
});
program
.command('catalog')
.description('List available facets (personas, policies, knowledge, instructions, output-contracts)')

View File

@ -1,199 +1,19 @@
/**
* takt export-cc Deploy takt skill files to Claude Code.
*
* Copies the following to ~/.claude/skills/takt/:
* SKILL.md Engine overview (user-invocable as /takt)
* references/ Engine logic + YAML schema
* pieces/ Builtin piece YAML files
* personas/ Builtin persona .md files
* policies/ Builtin policy files
* instructions/ Builtin instruction files
* knowledge/ Builtin knowledge files
* output-contracts/ Builtin output contract files
* templates/ Builtin template files
*
* Piece YAML persona paths (../personas/...) work as-is because
* the directory structure is mirrored.
*/
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname, relative } from 'node:path';
import { deploySkillInternal } from './deploySkillInternal.js';
import { getLanguage } from '../../infra/config/index.js';
import { getResourcesDir, getLanguageResourcesDir } from '../../infra/resources/index.js';
import { confirm } from '../../shared/prompt/index.js';
import { header, success, info, warn, blankLine } from '../../shared/ui/index.js';
/** Files to skip during directory copy */
const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']);
/** Target paths under ~/.claude/ */
function getSkillDir(): string {
return join(homedir(), '.claude', 'skills', 'takt');
}
/** Directories directly under builtins/{lang}/ */
const DIRECT_DIRS = ['pieces', 'templates'] as const;
/** Facet directories under builtins/{lang}/facets/ */
const FACET_DIRS = ['personas', 'policies', 'instructions', 'knowledge', 'output-contracts'] as const;
/**
* Deploy takt skill to Claude Code (~/.claude/).
*/
export async function deploySkill(): Promise<void> {
header('takt export-cc — Deploy to Claude Code');
const lang = getLanguage();
const skillResourcesDir = join(getResourcesDir(), 'skill');
const langResourcesDir = getLanguageResourcesDir(lang);
const skillDir = getSkillDir();
// Verify source directories exist
if (!existsSync(skillResourcesDir)) {
warn('Skill resources not found. Ensure takt is installed correctly.');
return;
}
// Check if skill already exists and ask for confirmation
const skillExists = existsSync(join(skillDir, 'SKILL.md'));
if (skillExists) {
info('Claude Code Skill が既にインストールされています。');
const overwrite = await confirm(
'既存のスキルファイルをすべて削除し、最新版に置き換えます。続行しますか?',
false,
);
if (!overwrite) {
info('キャンセルしました。');
return;
}
blankLine();
}
const copiedFiles: string[] = [];
// 1. Deploy SKILL.md
const skillSrc = join(skillResourcesDir, 'SKILL.md');
const skillDest = join(skillDir, 'SKILL.md');
copyFile(skillSrc, skillDest, copiedFiles);
// 2. Deploy references/ (engine.md, yaml-schema.md)
const refsSrcDir = join(skillResourcesDir, 'references');
const refsDestDir = join(skillDir, 'references');
cleanDir(refsDestDir);
copyDirRecursive(refsSrcDir, refsDestDir, copiedFiles);
// 3. Deploy direct resource directories from builtins/{lang}/
for (const dir of DIRECT_DIRS) {
const srcDir = join(langResourcesDir, dir);
const destDir = join(skillDir, dir);
cleanDir(destDir);
copyDirRecursive(srcDir, destDir, copiedFiles);
}
// 4. Deploy facet directories from builtins/{lang}/facets/ (preserving facets/ structure)
const facetsDestDir = join(skillDir, 'facets');
cleanDir(facetsDestDir);
for (const dir of FACET_DIRS) {
const srcDir = join(langResourcesDir, 'facets', dir);
const destDir = join(facetsDestDir, dir);
copyDirRecursive(srcDir, destDir, copiedFiles);
}
// Report results
blankLine();
if (copiedFiles.length > 0) {
success(`${copiedFiles.length} ファイルをデプロイしました。`);
blankLine();
// Show summary by category
const skillBase = join(homedir(), '.claude');
const skillFiles = copiedFiles.filter(
(f) =>
f.startsWith(skillDir) &&
!f.includes('/pieces/') &&
!f.includes('/facets/') &&
!f.includes('/templates/') &&
!f.includes('/references/'),
);
const pieceFiles = copiedFiles.filter((f) => f.includes('/pieces/'));
const personaFiles = copiedFiles.filter((f) => f.includes('/facets/personas/'));
const policyFiles = copiedFiles.filter((f) => f.includes('/facets/policies/'));
const instructionFiles = copiedFiles.filter((f) => f.includes('/facets/instructions/'));
const knowledgeFiles = copiedFiles.filter((f) => f.includes('/facets/knowledge/'));
const outputContractFiles = copiedFiles.filter((f) => f.includes('/facets/output-contracts/'));
const templateFiles = copiedFiles.filter((f) => f.includes('/templates/'));
if (skillFiles.length > 0) {
info(` スキル: ${skillFiles.length} ファイル`);
for (const f of skillFiles) {
info(` ${relative(skillBase, f)}`);
}
}
if (pieceFiles.length > 0) {
info(` ピース: ${pieceFiles.length} ファイル`);
}
if (personaFiles.length > 0) {
info(` ペルソナ: ${personaFiles.length} ファイル`);
}
if (policyFiles.length > 0) {
info(` ポリシー: ${policyFiles.length} ファイル`);
}
if (instructionFiles.length > 0) {
info(` インストラクション: ${instructionFiles.length} ファイル`);
}
if (knowledgeFiles.length > 0) {
info(` ナレッジ: ${knowledgeFiles.length} ファイル`);
}
if (outputContractFiles.length > 0) {
info(` 出力契約: ${outputContractFiles.length} ファイル`);
}
if (templateFiles.length > 0) {
info(` テンプレート: ${templateFiles.length} ファイル`);
}
blankLine();
info('使い方: /takt <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);
}
}
await deploySkillInternal({
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,
});
}

View 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,
});
}

View 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);
}
}

View File

@ -6,3 +6,4 @@ export { ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES } from './e
export { resetCategoriesToDefault } from './resetCategories.js';
export { resetConfigToDefault } from './resetConfig.js';
export { deploySkill } from './deploySkill.js';
export { deploySkillCodex } from './deploySkillCodex.js';