diff --git a/plugins/ndf-codex/README.md b/plugins/ndf-codex/README.md index ff0e11e3..85dd2324 100644 --- a/plugins/ndf-codex/README.md +++ b/plugins/ndf-codex/README.md @@ -20,6 +20,69 @@ codex plugin add ndf@ai-plugins Claude Code 専用の agents、statusline 自動設定、transcript retention 自動設定は含めません。Codex runtime が読むファイルはこの `plugins/ndf-codex` 配下だけで完結します。 +## 暗黙起動を抑止する Skill + +破壊的操作を伴う以下 6 個の Skill は、`skills//agents/openai.yaml` の `policy.allow_implicit_invocation: false` によって **Codex の暗黙起動 (モデルが自分で選んで起動する経路) を抑止**しています。共有 Skill の frontmatter が `disable-model-invocation: true` のものが対象で、`scripts/build-runtime-plugins.sh` が自動生成します。 + +| Skill | 内容 | +|-------|------| +| `cherry-pick-pr` | 環境ブランチへの cherry-pick PR 作成 | +| `deploy` | 環境ブランチ (qa/staging, release/v2 等) への deploy PR 作成 | +| `merged` | PR マージ後のクリーンアップ | +| `pr` | commit / push / PR 作成・更新 | +| `pr-tests` | PR テストプランの実行と結果コメント | +| `review` | PR / ブランチ差分のレビューと approve・request-changes 投稿 | + +### 利用者への影響と起動方法 + +抑止されるのは **暗黙起動だけ**です。`$` による明示起動は抑止後も従来どおり動きます。 + +| 起動経路 | 抑止後の挙動 | +|----------|-------------| +| 暗黙起動 (モデルが自分で選ぶ) | **起動しない**。セッションの skill 一覧 (`## Skills` の `### Available skills`) に載らない | +| 明示起動 `$deploy` | **起動する**。Codex CLI が `$<名前>` を展開し、SKILL.md 本文を `` ブロックとして注入する | +| 名前だけの自然文依頼 (`deploy skill を実行して`) | **起動しない**。一覧に無いため拒否され、別の Skill で代替されることがある | +| SKILL.md の絶対パスを示す | 通常のファイル読み取りとして読み込まれ、本文どおり実行される | + +推奨は `$` です。対話モード / 非対話モード (`codex exec`) のどちらでも同じ展開が行われます。 + +```text +# 動く: 明示起動 (推奨) +$deploy qa/staging + +# 動く: 実体パスを示して読ませる +~/.codex/plugins/cache/ai-plugins/ndf/4.20.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 + +# 動かない: 名前だけで起動を依頼する +deploy skill を実行してください。 +``` + +対話モード (`codex` を引数なしで起動) では `/skills` で Skill 一覧と有効・無効を確認できます。 + +### プラグイン Skill のファイル探索に関する注意 + +marketplace 経由でインストールした場合、Skill の実体は **ワークスペース外**の Codex プラグインキャッシュに置かれます。 + +```text +$CODEX_HOME/plugins/cache////skills//SKILL.md +# 既定 ($CODEX_HOME=~/.codex) の例: +# ~/.codex/plugins/cache/ai-plugins/ndf/4.20.1/skills/deploy/SKILL.md +``` + +そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。`$` はキャッシュ配下の Skill も skill roots から解決するため、まず `$` 起動を使ってください。パスで指示したい場合は `codex plugin list` で実体パスを確認し、絶対パスを渡します。 + +### 実機検証結果 (codex-cli 0.146.1 / gpt-5.5) + +`.agents/skills/` 配下に検証用 Skill (`probe-explicit` = 本プラグインと同じ `openai.yaml` を配置 / `probe-open` = 抑止なし) を置き、`codex exec` で確認した結果です。 + +| 検証 | 内容 | 結果 | +|------|------|------| +| 暗黙起動の抑止 | 「Available skills のうち probe で始まるものを列挙」と依頼 | `probe-open` のみ。`probe-explicit` は **載らない**。エラー・警告は出ない | +| 明示起動 | `codex exec '$probe-explicit'` | **起動した**。セッションログに `probe-explicit` + SKILL.md 本文が注入される | +| プラグイン Skill の明示起動 | ワークスペース外にインストール済みの `ndf` に対し、無関係な作業ディレクトリで `codex exec '$deploy'` | **解決した**。`~/.codex/plugins/cache/ai-plugins/ndf/4.20.1/skills/deploy/SKILL.md` を読み込んで実行 | + +暗黙起動の抑止と明示起動の可否は独立しており、抑止した Skill も `$` で起動できることを確認しています。 + ## Slack 通知 Codex 版の Stop hook は `NDF_CODEX_SLACK_NOTIFY=true` が設定されている場合だけ Slack 通知を送ります。通知を使う場合は、利用プロジェクト側で以下の環境変数を設定します。 diff --git a/plugins/ndf-codex/skills/cherry-pick-pr/agents/openai.yaml b/plugins/ndf-codex/skills/cherry-pick-pr/agents/openai.yaml new file mode 100644 index 00000000..31590300 --- /dev/null +++ b/plugins/ndf-codex/skills/cherry-pick-pr/agents/openai.yaml @@ -0,0 +1,4 @@ +policy: + allow_implicit_invocation: false +interface: + default_prompt: " (例: qa/staging, release/v2)" diff --git a/plugins/ndf-codex/skills/deploy/agents/openai.yaml b/plugins/ndf-codex/skills/deploy/agents/openai.yaml new file mode 100644 index 00000000..68c5828d --- /dev/null +++ b/plugins/ndf-codex/skills/deploy/agents/openai.yaml @@ -0,0 +1,4 @@ +policy: + allow_implicit_invocation: false +interface: + default_prompt: " (例: qa/staging, release/v2)" diff --git a/plugins/ndf-codex/skills/merged/agents/openai.yaml b/plugins/ndf-codex/skills/merged/agents/openai.yaml new file mode 100644 index 00000000..35f2e747 --- /dev/null +++ b/plugins/ndf-codex/skills/merged/agents/openai.yaml @@ -0,0 +1,4 @@ +policy: + allow_implicit_invocation: false +interface: + default_prompt: "[PR番号]" diff --git a/plugins/ndf-codex/skills/pr-tests/agents/openai.yaml b/plugins/ndf-codex/skills/pr-tests/agents/openai.yaml new file mode 100644 index 00000000..35f2e747 --- /dev/null +++ b/plugins/ndf-codex/skills/pr-tests/agents/openai.yaml @@ -0,0 +1,4 @@ +policy: + allow_implicit_invocation: false +interface: + default_prompt: "[PR番号]" diff --git a/plugins/ndf-codex/skills/pr/agents/openai.yaml b/plugins/ndf-codex/skills/pr/agents/openai.yaml new file mode 100644 index 00000000..ce8499a5 --- /dev/null +++ b/plugins/ndf-codex/skills/pr/agents/openai.yaml @@ -0,0 +1,4 @@ +policy: + allow_implicit_invocation: false +interface: + default_prompt: "[--draft] [base-branch] or [commit-message]" diff --git a/plugins/ndf-codex/skills/review/agents/openai.yaml b/plugins/ndf-codex/skills/review/agents/openai.yaml new file mode 100644 index 00000000..22e2c991 --- /dev/null +++ b/plugins/ndf-codex/skills/review/agents/openai.yaml @@ -0,0 +1,4 @@ +policy: + allow_implicit_invocation: false +interface: + default_prompt: "[PR番号 | --branch] [AIエージェント(codex|gemini)] [--focus AREA]" diff --git a/scripts/build-runtime-plugins.sh b/scripts/build-runtime-plugins.sh index b3c895eb..f4c7ff8b 100755 --- a/scripts/build-runtime-plugins.sh +++ b/scripts/build-runtime-plugins.sh @@ -99,6 +99,67 @@ rewrite_codex_skill_paths() { done } +# Codex は Skill ごとの `/agents/openai.yaml` で暗黙起動を制御する。 +# SKILL.md の frontmatter を読み、`disable-model-invocation: true` を持つ Skill だけへ生成する。 +write_codex_skill_policies() { + local skills_dir="$1" + + python3 - "$skills_dir" <<'PY' +import sys +from pathlib import Path + +skills_dir = Path(sys.argv[1]) + + +def parse_frontmatter(text: str) -> dict[str, str]: + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + return {} + fields: dict[str, str] = {} + for line in lines[1:]: + if line.strip() == "---": + break + # ネストした値(allowed-tools のリストなど)はここでは扱わない + if not line or line[0].isspace() or ":" not in line: + continue + key, _, value = line.partition(":") + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": + value = value[1:-1] + fields[key.strip()] = value + return fields + + +def yaml_double_quoted(value: str) -> str: + return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +for skill_dir in sorted(p for p in skills_dir.iterdir() if p.is_dir()): + skill_md = skill_dir / "SKILL.md" + if not skill_md.is_file(): + continue + + agents_dir = skill_dir / "agents" + policy_path = agents_dir / "openai.yaml" + # 前回の生成物を必ず捨ててから作り直す(対象から外れた Skill に残さない) + policy_path.unlink(missing_ok=True) + + fields = parse_frontmatter(skill_md.read_text(encoding="utf-8")) + if fields.get("disable-model-invocation") != "true": + if agents_dir.is_dir() and not any(agents_dir.iterdir()): + agents_dir.rmdir() + continue + + lines = ["policy:", " allow_implicit_invocation: false"] + argument_hint = fields.get("argument-hint") + if argument_hint: + lines += ["interface:", f" default_prompt: {yaml_double_quoted(argument_hint)}"] + + agents_dir.mkdir(parents=True, exist_ok=True) + policy_path.write_text("\n".join(lines) + "\n", encoding="utf-8") +PY +} + rewrite_kiro_skill_paths() { local skills_dir="$1" local file @@ -175,6 +236,7 @@ sync_skills() { if [ "$variant" = codex-runtime ]; then rewrite_codex_skill_paths "$tmp_dir" skills + write_codex_skill_policies "$tmp_dir" elif [ "$variant" = kiro-runtime ]; then rewrite_kiro_skill_paths "$tmp_dir" fi