Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions plugins/ndf-codex/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/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 名>` による明示起動は抑止後も従来どおり動きます。

| 起動経路 | 抑止後の挙動 |
|----------|-------------|
| 暗黙起動 (モデルが自分で選ぶ) | **起動しない**。セッションの skill 一覧 (`## Skills` の `### Available skills`) に載らない |
| 明示起動 `$deploy` | **起動する**。Codex CLI が `$<名前>` を展開し、SKILL.md 本文を `<skill>` ブロックとして注入する |
| 名前だけの自然文依頼 (`deploy skill を実行して`) | **起動しない**。一覧に無いため拒否され、別の Skill で代替されることがある |
| SKILL.md の絶対パスを示す | 通常のファイル読み取りとして読み込まれ、本文どおり実行される |

推奨は `$<skill 名>` です。対話モード / 非対話モード (`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/<marketplace>/<plugin>/<version>/skills/<skill>/SKILL.md
# 既定 ($CODEX_HOME=~/.codex) の例:
# ~/.codex/plugins/cache/ai-plugins/ndf/4.20.1/skills/deploy/SKILL.md
```

そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。`$<skill 名>` はキャッシュ配下の 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'` | **起動した**。セッションログに `<skill><name>probe-explicit</name><path>…</path>` + 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 通知を送ります。通知を使う場合は、利用プロジェクト側で以下の環境変数を設定します。
Expand Down
4 changes: 4 additions & 0 deletions plugins/ndf-codex/skills/cherry-pick-pr/agents/openai.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
policy:
allow_implicit_invocation: false
interface:
default_prompt: "<base-branch> (例: qa/staging, release/v2)"
4 changes: 4 additions & 0 deletions plugins/ndf-codex/skills/deploy/agents/openai.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
policy:
allow_implicit_invocation: false
interface:
default_prompt: "<env-branch> (例: qa/staging, release/v2)"
4 changes: 4 additions & 0 deletions plugins/ndf-codex/skills/merged/agents/openai.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
policy:
allow_implicit_invocation: false
interface:
default_prompt: "[PR番号]"
4 changes: 4 additions & 0 deletions plugins/ndf-codex/skills/pr-tests/agents/openai.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
policy:
allow_implicit_invocation: false
interface:
default_prompt: "[PR番号]"
4 changes: 4 additions & 0 deletions plugins/ndf-codex/skills/pr/agents/openai.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
policy:
allow_implicit_invocation: false
interface:
default_prompt: "[--draft] [base-branch] or [commit-message]"
4 changes: 4 additions & 0 deletions plugins/ndf-codex/skills/review/agents/openai.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
policy:
allow_implicit_invocation: false
interface:
default_prompt: "[PR番号 | --branch] [AIエージェント(codex|gemini)] [--focus AREA]"
62 changes: 62 additions & 0 deletions scripts/build-runtime-plugins.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,67 @@ rewrite_codex_skill_paths() {
done
}

# Codex は Skill ごとの `<Skill 名>/agents/openai.yaml` で暗黙起動を制御する。
# SKILL.md の frontmatter を読み、`disable-model-invocation: true` を持つ Skill だけへ生成する。
write_codex_skill_policies() {
Comment thread
takemi-ohama marked this conversation as resolved.
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"]
Comment thread
takemi-ohama marked this conversation as resolved.
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
Expand DownExpand Up@@ -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
Expand Down
Loading