AIエージェントの Skill は書くだけでは足りない ⇒ Waza で評価して APM で配ろう!【ハーネスエンジニアリング】
はじめに
こんにちは、Matsumoto です。
GitHub Copilot などの Agentic なCoding ツールを日常で使っていると、SKILL.md で繰り返しの作業手順やレビュー観点をスキルとして渡せるのが便利だなと思います。
一方で、 SKill は書くだけでは足りません。本当に意図どおりに発火するのか、書き換えたあとにデグレしていないのか、別モデルでも同じ品質で動くのかは、確認・評価しないと分かりません。
そこで Microsoft の GitHub org で公開されている 2 つの OSS を触ってみました。
- microsoft/apm:AI エージェントの設定一式(instructions、prompts、skills、agents、MCP 設定など)を npm や pip のように依存関係で配布する CLI。本記事ではこれら設定の集合を便宜上「ハーネス(エージェント設定一式)」と呼びます
- microsoft/waza: SKill が期待どおりに動くかを YAML で宣言した評価タスクで実行する Go 製 CLI
役割分担はシンプルです。
- APM は SKill を「どう配るか」を担当する
- Waza は SKill が「効いているかをどう評価するか」を担当する
この 2 つを組み合わせると、「Skillを書く → 評価する → 改善する → 配布する」というループが回せます。
本記事では、コミットメッセージを Conventional Commits 形式(feat: や fix: などの type を付けてコミットを分類する規約)で書く Skill を題材に、Waza で評価し、APM で配布し、Pull Request の CI(GitHub Actions)で配布物のドリフトや評価設定の崩れをチェックする流れを試します。

評価駆動で回すループの全体像。Waza が中心で、APM は配布層を受け持つ。
題材にコミットメッセージを選んだ理由は 2 つあります。
1 つは、GitHub Issue 連携の文脈に近いこと。Closes #42 のような footer 行は Issue 自動クローズに効きますし、Refs #42 のような参照も履歴を追ううえで役立ちます。本記事では、まず footer に Issue 参照を残せるかを評価し、closing keyword の厳密化(Closes/Fixes/Resolves 限定)は次のイテレーションで扱います。
もう 1 つは、LLM がふだん書き慣れている形式だから です。Conventional Commits は OSS のコミット履歴で広く使われていて、SKILL.md を薄く書いても最初からそれなりに書けてしまう題材です。それでも SKILL.md の書き分けで挙動差が出るのか、という 効きが小さく出やすい条件での検証 だと思ってもらえれば近いです。社内テンプレや独自規約みたいに LLM があまり書き慣れていない題材だと、SKILL.md を直したときの差がもっと大きく出るかもしれません。
両ツールのリポジトリはこちらです。
ドキュメントはそれぞれ以下です。
APMのドキュメント
Wazaのドキュメント
この記事で見ていくこと
| 章 | 内容 |
|---|---|
| 2 | 「書いたスキル、本当に効いてる?」問題 |
| 3 | APM:エージェント設定の依存管理 |
| 4 | Waza が解こうとしている問題 |
| 5 | 評価ループを回す:commit-message-writer を題材に |
| 6 | スキルを改善する:3 回の改訂で見えたこと |
| 7 | CI で評価パイプラインを検証する |
| 8 | 検証の総括 |
記事のスタンス
触ってみた範囲の事実と、そこから個人的に感じたことはなるべく分けて書きます。
検証できていない部分は「未検証」と明記します。公式ドキュメントとの差があれば公式を優先してください。
2. 「書いた skill、本当に効いてる?」問題
APM と Waza の中身に入る前に、なぜ評価駆動という発想を試したのかを書いておきます。
AI コーディングエージェントの指示書を書いていると、すぐ気づく問題があります。
書き手は SKILL.md やプロンプトに自分の意図を書くわけですが、エージェントが意図どおり動いているかは、ふだん使うなかで「あれ、変だな」と気づくしかない。気づかないまま PR が積まれていく、ということも普通に起こります。
具体的にはこういう状況です。
- SKill を書き換えたあと、以前は安定して指摘していたパターンが指摘されなくなる(デグレ)
- モデルが
gpt-5.4からclaude-opus-4.6に切り替わったとたん、出力が大きく変わる(モデル依存) - 同じ SKill でも、トリガーワード次第で起動したりしなかったりする(トリガー精度)
ソフトウェア開発で言えば、テストを書かずに機能追加を続けるのと同じ状況です。Skill は「書く」工程だけが膨らんで、「評価する」工程がぽっかり空いている。
microsoft/waza は、ここを埋めるためのフレームワークです。
microsoft/apm は、Waza で評価して合格したスキルを、複数の配布先に同じバージョンで配るための仕組みになります。両方が揃って、「評価駆動で SKill を改善する」ループが成立します。

評価が先、配布が後。配ったあとも CI で再評価して、Waza に戻る。
3. APM:エージェント設定の依存管理
評価駆動ループに入る前に、配布層の APM を押さえます。
Waza の評価はあくまで「配布前の SKill が意図どおり動くか」を確認する工程なので、配布される側の構造を知っておくと後段の話が入りやすくなります。
APM は AI エージェントのハーネス(instructions、prompts、skills、agents、MCP 設定)を依存関係で配布する CLI で、npm や pip のパッケージ管理と発想が近いです。
1 つの原本(.apm/ 配下)を、Copilot、Claude Code、Cursor といった複数のエージェント環境向けに展開できます。
target ごとに配置先や形式が変わるので、「同じものをそのままコピーして配る」というよりは、「同じ原本から各環境向けに変換して置く」イメージのほうが近いです。
3-1. 用語の整理
APM が扱う配布物は次の単位に分かれます。
| 種類 | 役割 | 配置先(Copilot 向け) |
|---|---|---|
| Instructions | 言語別・ファイル別のコーディング規約 | .github/instructions/ |
| Prompts | 再利用するプロンプト | .github/prompts/ |
| Skills | スキル本体(SKILL.md) | .agents/skills/ |
| Agents | エージェント定義(system prompt 等) | .github/agents/ |
| MCP 設定 | サーバー定義 |
.vscode/mcp.json 等 |
ここで気をつけたいのは Skills の配置先です。APM 0.12 系から、skill は cross-client な .agents/skills/ に集約されるようになりました。Copilot ネイティブの .github/skills/ にも置きたい場合は、apm.yml の target に [copilot, agent-skills] と並べておくか、apm install --target copilot,agent-skills のように CLI 引数で渡します。
なお、以下の記事もAPMの理解を深めるうえで参考になります。
3-2. 最小構成のセットアップ
apm.yml がプロジェクトのマニフェストです。展開先(target)はここに書いておくのが、手元と CI で挙動を揃える上で楽です。
apm.yml の最小例
name: copilot-skill-apm-waza-demo
version: 0.1.0
description: |
GitHub Copilot CLI のスキルを APM で配り、Waza で評価するデモ。
commit-message-writer スキル(Conventional Commits ライクなコミットメッセージ生成)を題材にする。
target: [copilot, agent-skills]
dependencies:
apm: []
.apm/ 配下に instructions / prompts / skills を書いて、apm install で各エージェント向けに展開します。target フィールドに copilot, agent-skills を書いておけば、引数なしの apm install だけで両方に展開されます。
apm install
実行すると、ログに Active project targets: copilot, agent-skills と出て、.apm/ の中身が .github/(Copilot 向け)と .agents/skills/(cross-client な集約配置)に展開されます。手元で find を打つと次のように見えます。
.github/prompts/security-review.prompt.md
.agents/skills/commit-message-writer/SKILL.md
3-3. apm install で何が起こるか
apm install を走らせると、apm.yml の target を読み取って、.apm/ 配下の各ファイルが target ごとに変換・配置されます。
APM 0.12 系で 1 つ大きく変わったのが、skill の配置先です。agent-skills target 経由で .agents/skills/ に集約配置されるようになりました。Copilot 向けの .github/skills/ にも同時に展開したい場合は、target: [copilot, agent-skills] のように両方指定します。
この .agents/ 配置は commit 対象 です。apm install を回さないと Copilot がスキルを認識しないので、配布物そのものをリポジトリに置いてあげる必要があります。.apm/(原本)と .agents/(配布物)の両方を commit する運用が公式の推奨です。
3-4. なぜ「APM で配る」のか
APM の特徴は、apm install 1 回で複数のエージェント向けに 1 つの原本から展開できる点です。スキルの実体は .apm/skills/ にあり、Copilot 用には .agents/skills/ に展開、Claude Code 用には .claude/skills/ に展開、というふうに target ごとに配り分けられます。
SKill だけならコピペでもいいかもしれませんが、複数のエージェント環境で同じ原本を使いたい、CI で配布結果を検証したい、依存関係(別の APM パッケージを引いてくる)を扱いたい、というケースでは APM が効いてきます。
Hidden Unicode 監査も標準で組み込まれていて、配布物に紛れ込んだ Bidirectional 制御文字を検出してくれます。
4. Waza が解こうとしている問題
APM 側で「どう配るか」が見えたので、もう一方の「効いているかを評価する」担当である Waza に話を移します。
Waza は「 SKill を評価する」ための CLI です。Go 製のバイナリで、YAML で書いた評価タスクを順番に実行してスコアを出します。
4-1. 評価の単位
Waza の評価は次の単位で構成されます。
| 単位 | ファイル | 役割 |
|---|---|---|
| Task | evals/<skill>/tasks/*.yaml |
1 つの評価ケース。プロンプトと期待値 |
| Fixture | evals/<skill>/fixtures/* |
task が参照する素材(diff、ソースコードなど) |
| Eval | evals/<skill>/eval.yaml |
task の集合と grader の設定 |
| Grader |
eval.yaml 内 or task 内 |
出力を評価する判定器 |
Grader には複数の type があります。
-
text:正規表現マッチで出力をチェック(確定的、安価) -
behavior:ツール呼び出し回数やトークン数の上限を見る -
file/diff:ファイル変更の差分を見る -
prompt:別の LLM をジャッジとして呼んで評価(柔軟だが高い) -
skill_invocation:意図したスキルが起動したかを見る
Waza 公式 docs の eval-yaml ガイド(microsoft.github.io/waza)にも「Layer your checks」とあるとおり、確定的なグレーダー(text、file、diff、behavior)を主軸に重ねて、prompt グレーダーは人手レビューしきれない主観評価が必要なときだけ、という切り分けが現実的です。
この単位の関係を図にしておきます。

Waza の処理単位。Inputs を Executor が走らせ、Outputs を graders が評価する。
4-2. Mock Executor と Copilot SDK Executor
私が 0.31.0 で動かした範囲では、config.executor に書ける値で実際に動いたのは次の 2 つでした。
-
mock:API を呼ばずに、フィクスチャの内容を出力としてエコーするドライラン用 -
copilot-sdk:実モデル評価。GitHub Copilot サブスクリプション認証で動く
eval.yaml の config.executor で切り替えます。CLI フラグ(--executor のような)は現バージョンに存在しません。モデルだけ切り替えたいときは --model で上書きできます。
4-3. 最初に Mock を使うとありがたい理由
Waza 0.31.0 の eval spec では executor のデフォルトは copilot-sdk(実モデル)です。それでも本記事では eval.yaml に executor: mock を明示して、最初は mock で動かしています。
評価駆動ループを回し始めの段階では、grader 設計やタスク定義を試行錯誤するために何度も評価を走らせることになります。
実モデルで全部やると お金が想像以上に消えていく(後述)。
Mock executor でフローを固めてから、実モデルで本番評価する流れが安全です。
5. 評価ループを回す:commit-message-writer を題材に
ここから実装に入ります。
題材は commit-message-writer:Git の diff を渡したらコミットメッセージを Conventional Commits 形式で書いて返す SKill です。
本記事では Conventional Commits そのものの正誤を判定するのではなく、GitHub Issue 連携まで含めたこのリポジトリのコミットメッセージ規約を、Copilot の SKill として実装し、Waza でデグレを検出できるか を検証します。
scope や body や footer を Conventional Commits 公式仕様より厳しく扱うのは、リポジトリ運用上のチームルールとして課しているためです。
選定理由は次のとおりです。
- 出力フォーマットが厳密に決まっている(
<type>(<scope>): <subject>+ body + footer)ので、textグレーダーで確定的に判定できる - GitHub の Issue 連携と直結している(
Closes #42で Issue 自動クローズ) - gpt-5.4 と claude-opus-4.6 のモデル間差が出やすい(後述)
5-1. プロジェクト構造
mkdir copilot-skill-apm-waza-demo && cd copilot-skill-apm-waza-demo
git init
apm init
mkdir -p .apm/skills/commit-message-writer
mkdir -p evals/commit-message-writer/{fixtures,tasks}
5-2. SKILL.md(初版)
最初は最小限。Conventional Commits の規約は LLM の事前知識でカバーされていそうなので、わざと 書かない方針 で出します。あとで Waza に評価させて、足りないところを後付けします。
.apm/skills/commit-message-writer/SKILL.md(初版)
---
name: commit-message-writer
description: |
Git のコミットメッセージを書く。
USE FOR: コミットメッセージの作成
DO NOT USE FOR: コードのリファクタリング、PR タイトルの作成
Triggers: "コミットメッセージを書いて", "commit message"
---
# Commit Message Writer
渡された diff からコミットメッセージを書く。
これで apm install を走らせると、apm.yml の target: [copilot, agent-skills] を読み取って、.agents/skills/commit-message-writer/SKILL.md に展開されます。
SKILL.md は意図的にこの薄さで止めています。Conventional Commits の規約名は次節の task 側プロンプトで明示するためです。
claude-opus-4.6 も gpt-5.4 も Conventional Commits はふだん書き慣れている形式のはずです。なので、まずは SKILL.md を最小化した baseline で何が出るかを Before として見ます。
ただし AGENTS.md は引き続き読まれているので、ここでの baseline は「モデル素の地力」というよりは、現行ハーネス込みの出発点だと思ってもらえれば近いです。冒頭で書いたとおり、本記事の題材は LLM がふだん書き慣れている形式なので、ここで見える baseline はハードルが低めに出ます。
5-3. 評価タスクを書く
2 つの diff を fixture として用意します。
- feat(API のレート制限追加):複数ファイル、テストもセット。Issue #42 に対応
- fix(README タイポ修正):1 行だけの差分。Issue #99 に対応
意図的に難易度の異なる diff を 2 つ並べます。fix のような短い変更でも、コミットメッセージとして body と Issue 参照を省略しないでほしい、という要件をスキルにどう書くかが肝になります。
fixtures/diff-feat-rate-limit.txt
diff --git a/src/api/handler.py b/src/api/handler.py
index 7d4b8a1..2c9f3e0 100644
--- a/src/api/handler.py
+++ b/src/api/handler.py
@@ -1,8 +1,18 @@
import time
+from collections import deque
+
+_RATE_LIMIT_WINDOW: deque[float] = deque(maxlen=100)
+_RATE_LIMIT_PERIOD = 60.0
def handle_request(req):
+ now = time.time()
+ _RATE_LIMIT_WINDOW.append(now)
+ if (
+ len(_RATE_LIMIT_WINDOW) >= 100
+ and now - _RATE_LIMIT_WINDOW[0] < _RATE_LIMIT_PERIOD
+ ):
+ raise RuntimeError("rate limit exceeded")
return process(req)
diff --git a/tests/test_handler.py b/tests/test_handler.py
index 1a2b3c4..5d6e7f8 100644
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -10,3 +10,12 @@ def test_handle_request_passes_through():
req = make_request()
resp = handle_request(req)
assert resp.status == 200
+
+
+def test_handle_request_rejects_over_rate_limit():
+ for _ in range(100):
+ handle_request(make_request())
+ with pytest.raises(RuntimeError, match="rate limit"):
+ handle_request(make_request())
fixtures/diff-fix-typo-readme.txt
diff --git a/README.md b/README.md
index aaaaaaa..bbbbbbb 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ A small CLI tool for managing tasks.
## Installation
-Run `pip install taskcli` to insatll.
+Run `pip install taskcli` to install.
## Usage
tasks/commit-feat-rate-limit.yaml
id: commit-message-feat-rate-limit
name: Write commit message for feat (rate limit)
description: API ハンドラへのレート制限追加とテスト追加に対する Conventional Commits 形式のメッセージを書けるか
tags:
- commit-message
- conventional-commits
- feat
inputs:
prompt: |
diff-feat-rate-limit.txt は `git diff --staged` の出力です。
この変更は Issue #42 に対応します(ユーザーから API 過負荷の報告)。
この変更に対するコミットメッセージを 1 つ書いてください。
出力はメッセージ本体のみで、コードブロックや前置きは不要です。
files:
- path: diff-feat-rate-limit.txt
expected:
output_contains_any:
- "rate limit"
- "rate-limit"
- "rate_limit"
behavior:
max_tool_calls: 5
max_response_time_ms: 60000
tasks/commit-fix-typo.yaml
id: commit-message-fix-typo
name: Write commit message for fix (typo in README)
description: README のタイポ修正に対する Conventional Commits 形式のメッセージを書けるか(fix / docs どちらかで判定)
tags:
- commit-message
- conventional-commits
- fix
inputs:
prompt: |
diff-fix-typo-readme.txt は `git diff --staged` の出力です。
この変更は Issue #99 のタイポ報告に対応します。
この変更に対するコミットメッセージを 1 つ書いてください。
出力はメッセージ本体のみで、コードブロックや前置きは不要です。
files:
- path: diff-fix-typo-readme.txt
expected:
output_contains_any:
- "typo"
- "Insatll"
- "install"
- "README"
behavior:
max_tool_calls: 5
max_response_time_ms: 60000
5-4. eval.yaml(grader 設計)
両 task で共通に効く grader を eval.yaml の graders: 直下に置きます。grader は 7 つにしました。
| # | grader | weight | 何を見るか |
|---|---|---|---|
| 1 | has_conventional_type |
2.0 |
feat(...) fix(...) のような Conventional Commits の type プレフィックス |
| 2 | has_typed_scope |
2.0 | scope が () で明示されているか |
| 3 | has_body |
1.5 | subject の後に空行を挟んで本文があるか |
| 4 | references_issue |
2.0 |
Closes/Fixes/Refs #N で Issue を参照しているか |
| 5 | subject_within_72_chars |
1.0 | 1 行目が 72 文字以下 |
| 6 | subject_no_trailing_period |
0.5 | 1 行目末尾にピリオドなし |
| 7 | tool_call_budget |
0.5 | tool 呼び出し回数(主軸)と token 数(許容上限)の上限 |
eval.yaml の全文
name: commit-message-writer-eval
description: commit-message-writer スキルの評価スイート
skill: commit-message-writer
version: "1.0"
config:
trials_per_task: 1
timeout_seconds: 300
parallel: false
# 両 task の結果を取るため fail_fast は false
fail_fast: false
# ローカル検証時は executor: mock で API を呼ばない
# 実モデル評価時は executor: copilot-sdk に切り替える
executor: mock
model: claude-opus-4.6
# APM が配布した .agents/skills/ を直接 Waza の探索対象にする
skill_directories:
- ".agents/skills"
graders:
# 1. Conventional Commits の type プレフィックス
- type: text
name: has_conventional_type
weight: 2.0
config:
regex_match:
- '^(feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert)(\([^)]+\))?!?:\s'
# 2. scope を必須にする(モジュール特定の手がかり)
- type: text
name: has_typed_scope
weight: 2.0
config:
regex_match:
- '^[a-z]+\([a-z][a-z0-9_-]*\)!?:\s'
# 3. body 行を含む(subject の後に空行 + 本文)
- type: text
name: has_body
weight: 1.5
config:
regex_match:
- '\A.+\n\n[^\n]'
# 4. Issue 参照(Closes / Refs / Fixes #N)
- type: text
name: references_issue
weight: 2.0
config:
regex_match:
- '(Closes|Fixes|Refs|closes|fixes|refs) #\d+'
# 5. subject 1 行目が 72 文字以下
- type: text
name: subject_within_72_chars
weight: 1.0
config:
regex_match:
- '^.{1,72}(\n|$)'
# 6. subject の末尾にピリオドを付けない
- type: text
name: subject_no_trailing_period
weight: 0.5
config:
regex_match:
- '[^.]\s*(\n|$)'
# 7. 振る舞いチェック: tool 呼び出しと token の上限
- type: behavior
name: tool_call_budget
weight: 0.5
config:
max_tool_calls: 5
max_tokens: 100000
tasks:
- "tasks/*.yaml"
config.skill_directories: [".agents/skills"] は地味に効いている設定です。Waza のデフォルト探索パスは skills/ ですが、本記事では APM が配布した .agents/skills/ をそのまま Waza の評価対象に渡したいので、追加の探索ディレクトリとして .agents/skills を指定しています。これで APM が配った成果物 をそのまま Waza が評価する という流れが、リポジトリ構成にも素直に表れます。
max_tokens: 100000 の設定値は実測ベースです。Copilot SDK 経由は cached read 込みで 4〜10 万トークン消費するため、この水準に合わせています。grader 名を tool_call_budget にしたのは、本記事で実際に見ているのが tool 呼び出し回数(max_tool_calls: 5)だからです。max_tokens はこの evaluation で観測される token 数の許容上限として置いてあるだけで、採点軸の主役は tool 呼び出し回数の方です。
5-5. 評価を走らせる
ローカルでは executor: mock のままドライランしてフローを確認します。本記事の構成では、waza run には eval ファイルを直接指定 します。.agents/skills/ 配下を Waza に見てもらうために eval.yaml の skill_directories を使っているので、waza run commit-message-writer のような skill 名指定では skill が解決されません。
waza run evals/commit-message-writer/eval.yaml -v

mock executor で waza run -v を流した結果。grader 単位の pass/fail と Aggregate Score が確認できる。
実モデルで動かすときは eval.yaml の config.executor を copilot-sdk に切り替えてから、同じコマンドを叩きます。モデル切替は --model で。
waza run evals/commit-message-writer/eval.yaml --model gpt-5.4 -o results-before-gpt5.json -v
waza run evals/commit-message-writer/eval.yaml --model claude-opus-4.6 -o results-before-opus.json -v
6. スキルを改善する(3 回の改訂で見えたこと)
ここからが、Waza を使ってみていちばん効くと感じた工程です。
評価結果を見て、SKILL.md を書き換え、再評価するループを回します。
今回は SKILL.md を 3 回書き直しました。期待した方向に動かなかった改訂もそのまま記録します。
3 回の改訂で aggregate スコアがどう動いたかを先に俯瞰しておきます。

3 回の改訂と aggregate スコアの動き。gpt-5.4 は v1 で悪化、v3 で 1.00 に。claude-opus-4.6 は v1 で微改善、v2 で悪化、v3 で v0 と同水準に戻った。

waza serve のダッシュボード。手元の results-*.json を読み取って、モデル別のスコア・トークン・コストを一覧する。
wazaのdashboardについての公式ドキュメントは以下です。
書き手が善意で増やした指示が逆効果になる挙動が、v1 と v2 で観測されました。
以下、各改訂を順に見ていきます。
6-1. Before のスコア
最小 SKILL.md(章 5-2)で gpt-5.4 と claude-opus-4.6 を走らせた結果です。
| aggregate | feat task | fix task | 落ちた grader | |
|---|---|---|---|---|
claude-opus-4.6 |
0.88 | 0.88 | 0.88 |
has_typed_scope(両 task) |
gpt-5.4 |
0.88 | 1.00 | 0.75 |
has_body、references_issue(fix task) |
異なる失敗パターン が見えました。
claude-opus-4.6 は両 task で feat: fix: と書き、scope を省略しています。一方 body と Closes #42 Fixes #99 はちゃんと書いている。
これは AGENTS.md(プロジェクトルート)に Conventional Commits を 書く とだけ記載があり、scope の必須性に触れていないことが影響している可能性があります。
ただし、AGENTS.md を削除・変更した A/B 実験は本記事では未実施なので、ここは仮説段階の解釈として扱います(後述 8-3 で再度取り上げます)。
gpt-5.4 は逆に scope を必ず書く(feat(api): fix(readme):)一方で、fix task では fix(readme): correct installation typo in README の 1 行だけ返してきて body と Issue 参照が抜ける。短い変更では省略してよい という判断を働かせている挙動です。
6-2. SKILL.md v1:5 つの必須要素を強調
両モデルで Fail している箇所を埋めるために、5 つの必須要素を列挙する書き方に改訂しました。
v1 の本文(一部)
# Commit Message Writer
次の 5 つを必ず含めて返す。
1. `<type>` … feat / fix / docs / chore など
2. `<scope>` … `()` で囲んで必ず付ける
3. `<subject>` … 命令形、72 文字以内、末尾にピリオドなし
4. `<body>` … 変更理由を 1〜2 文。subject の後に空行 1 つ
5. `Closes #N` / `Fixes #N` / `Refs #N` … 必ず footer に付ける
走らせたら、gpt-5.4 は 悪化、claude-opus-4.6 は 微改善 という非対称な動き方をしました。
| Before | After v1 | 変化 | |
|---|---|---|---|
claude-opus-4.6 |
0.88 | 0.94 | ⬆️ 微改善(fix task の has_typed_scope だけ依然 fail) |
gpt-5.4 |
0.88 | 0.75 | ⬇️ 悪化 |
gpt-5.4 の悪化を final_output で見ると、両 task で subject 1 行だけを返して body と footer を省略する挙動が広がっていました。feat task は v0 で 1.00 だったのが v1 で 0.75。「5 要素を必ず含めて返せ」と書いたら、5 要素を順に積むのではなく 1 行に圧縮して済ませてしまう 挙動です。tool_call_count は 1〜3 で過剰調査ではなく、tool_call_budget も passed のまま。スコア低下の主因は出力の短文化でした。
claude-opus-4.6 の方は逆に、列挙された必須要素に強く反応して body と Closes #N をきっちり書くようになり、feat task が 1.00 まで上がりました。ただし scope は feat: fix: のままで、fix task の has_typed_scope は依然 Fail。SKILL.md で「scope を書け」と強めに書いても scope の Fail は変わらず、AGENTS.md グローバルルール由来の影響では、というのが現時点の解釈です(A/B 未実施なので仮説段階)。
ここで分かったのは、指示を増やすと挙動も増える ということです。
列挙された必須要素は、モデルによっては「全要素を全行に展開する」と解釈する一方で、別のモデルでは「全要素を 1 行に圧縮して済ませる」と解釈しました。少なくとも今回の 2 task では、「指示を増やせばどのモデルでも守ってくれる」とは言えませんでした。
6-3. SKILL.md v2:テンプレート + 例 2 個
v1 を捨てて、テンプレートと例を見せる方針に振り直しました。
v2 の本文(一部)
# Commit Message Writer
次のテンプレートをそのまま埋めて返す。**要素を 1 つも省略しない**。
```
<type>(<scope>): <subject>
<body>
<footer>
```
例 1:
```
feat(api): add per-process rate limiting to handler
60 秒間に 100 リクエストを超えた場合 RuntimeError を返す。
Closes #42
```
(例 2 省略)
| Before | v1 | v2 | 変化 | |
|---|---|---|---|---|
claude-opus-4.6 |
0.88 | 0.94 | 0.81 | ⬇️ v1 から悪化 |
gpt-5.4 |
0.88 | 0.75 | 0.88 | ⬆️ v0 まで戻った |
claude-opus-4.6 で悪化。理由を見て地味に驚きました。fix task の final_output がこんな形で返ってきていました。
fix: correct typo in README.md installation instructions (#99)
Update the typo in the README under installation instructions.
Issue 番号を subject 内に括弧で埋め込んでいる うえに、footer の Fixes #99 が消えています。references_issue の正規表現は出力中のどこかに (Closes|Fixes|Refs) #数字 が現れるかを見る粒度なので、subject 内の (#99) だけでは Fail。v1 では書けていた Closes #99 が、v2 でテンプレートを見せた途端に消えてしまった形です。
gpt-5.4 の方は v0 と同じ 0.88 に戻りました。テンプレートが具体例として効いて、v1 の subject 1 行圧縮が解けた格好です。ただし fix task では docs: fix typo in README installation instructions と返してきて type を docs: で判定しつつ scope を省略しており、has_typed_scope だけ依然 fail。テンプレートの <scope> プレースホルダを「省略可能なフィールド」と読み替えた可能性があります。
次に分かったのは、SKILL.md にテンプレートを書きすぎると、モデルがテンプレートの「見た目」を真似ようとして、本来の規約から逸脱する ということです。(#99) の位置が <subject> の枠内に見えるから入れられた、と推測しています。同じテンプレートでもモデルによって「枠の真似方」が違うのも、今回の検証で見えた挙動です。
6-4. SKILL.md v3:絞り込み
v1 v2 から学んだことを反映して、絶対に省略しないもの 2 つだけを強調する形に書き直しました。
v3 の本文(採用版)
---
name: commit-message-writer
description: |
Conventional Commits 形式の Git コミットメッセージを書く。subject、body、Issue 参照を必ず含める。
USE FOR: コミットメッセージの作成、コミット前のメッセージ整理
DO NOT USE FOR: コードのリファクタリング、PR タイトルの作成、リリースノートの作成
Triggers: "コミットメッセージを書いて", "commit message", "コミット文"
---
# Commit Message Writer
Git のコミットメッセージを Conventional Commits 形式で書く。
## 構造
```
<type>(<scope>): <subject>
<body>
<footer>
```
## 絶対に省略しない 2 つ
短い変更でも、タイポ修正でも、次の 2 つは必ず書く。
1. **body** を 1 文以上書く(subject の後に空行を 1 つ挟む)
2. **footer** に Issue 参照を `Closes #N` / `Fixes #N` / `Refs #N` のいずれかで書く
`Closes/Fixes/Refs` の文字列は subject 内ではなく必ず最後の footer 行に置く。
## type と scope
- `<type>` … `feat` `fix` `docs` `chore` `refactor` `test` `style` `perf` `ci` `build` `revert`
- `<scope>` … モジュール名を `()` で必ず囲む(例: `(api)`、`(readme)`)。diff から特定できない場合は `(general)` を使う
## subject
命令形、72 文字以内、末尾にピリオドなし。
## 例
例 1(feat):
```
feat(api): add per-process rate limiting to handler
60 秒間に 100 リクエストを超えた場合 RuntimeError を返す。
スライディングウィンドウ方式のレート制限を実装する。
Closes #42
```
例 2(fix、短い変更でも body と footer は省略しない):
```
fix(readme): correct installation typo
README の "insatll" を "install" に修正する。
Fixes #99
```
## 調査ルール
task で渡された diff を 1 回だけ読む。それ以上はファイルを開かない。
v2 との違いは 2 点です。
-
*cope は「必ず付ける、特定できないときは
(general)」へ- claude-opus-4.6 で
feat:fix:の scope 省略が 3 回の改訂を通じて続いた点を、SKILL.md からどこまで押し返せるかを試す改訂です。完全に必須としたうえで、diff から特定不能な場合のフォールバックを明示しました(プロジェクトルートの AGENTS.md 由来かは仮説段階で、後述 8-3 の宿題)
- claude-opus-4.6 で
-
footer の位置を明示
-
Closes/Fixes/Refsは subject 内ではなく 必ず最後の footer 行に置く、と書き加えた
-
走らせた結果がこれです。
| Before | v1 | v2 | v3(採用) | |
|---|---|---|---|---|
claude-opus-4.6 |
0.88 | 0.94 | 0.81 | 0.88 |
gpt-5.4 |
0.88 | 0.75 | 0.88 | 1.00 ⬆️ |
gpt-5.4 で aggregate 1.00 に到達。fix task でも fix(readme): correct installation typo + body + Fixes #99 の 3 行構造を返すようになりました。
claude-opus-4.6 は has_typed_scope だけ Fail のまま(scope を feat: fix: で省略する癖が抜けない)。
`waza compare` の出力(v2 → v3 の差分、両モデル)
$ waza compare results-v2-gpt5.json results-after-gpt5.json
======================================================================
COMPARISON REPORT
======================================================================
[1] results-v2-gpt5.json (model: gpt-5.4)
[2] results-after-gpt5.json (model: gpt-5.4)
----------------------------------------------------------------------
AGGREGATE
----------------------------------------------------------------------
Metric [1] [2] Delta
Score 0.8750 1.0000 +0.1250
Success Rate 0.0 % 100.0 % +100.0%
Duration (ms) 41191 38368 -2823
----------------------------------------------------------------------
PER-TASK DELTAS
----------------------------------------------------------------------
Task [1] Score [2] Score Delta
Write commit message f... 0.8750 1.0000 ↑+0.1250
Write commit message f... 0.8750 1.0000 ↑+0.1250
$ waza compare results-v2-opus.json results-after-opus.json
======================================================================
COMPARISON REPORT
======================================================================
[1] results-v2-opus.json (model: claude-opus-4.6)
[2] results-after-opus.json (model: claude-opus-4.6)
----------------------------------------------------------------------
AGGREGATE
----------------------------------------------------------------------
Metric [1] [2] Delta
Score 0.8125 0.8750 +0.0625
Success Rate 0.0 % 0.0 % +0.0%
Duration (ms) 20864 17459 -3405
----------------------------------------------------------------------
PER-TASK DELTAS
----------------------------------------------------------------------
Task [1] Score [2] Score Delta
Write commit message f... 0.8750 0.8750 +0.0000
Write commit message f... 0.7500 0.8750 ↑+0.1250
Success Rate は すべての grader が pass した task の割合 です。gpt-5.4 は v3 で両 task の grader が全 pass して 100%。claude-opus-4.6 は has_typed_scope を引き続き落としているので 0% のまま、ただし aggregate スコア(grader 単位の重み付き平均)は 0.8125 → 0.8750 で改善しています。
6-5. 3 回の改訂で見えた SKILL.md の特性
v3 で採用に至りましたが、ここまでの 3 回の改訂で見えた挙動には、v3 そのもの以上に書き留めておきたい部分がありました。v3 で「絶対に省略しない 2 つだけ」に絞ったのも、Closes/Fixes/Refs の位置を明示したのも、scope に (general) の逃げ道を許したのも、それぞれ v1 v2 で見えた挙動への直接の対応です。整理しておきます。
なお、ここで挙げる 3 点はすべて 2 task × 2 model × trials_per_task: 1 の観測です。一般則として読むには検証量が足りないので、今回の eval 構成で見えた傾向 として読んでください。
今回の 2 task では、指示を増やしたときの反応がモデルで割れた:v1 で 5 つの必須要素を列挙したら、gpt-5.4 は両 task で subject 1 行に圧縮する挙動になり、aggregate 0.88 → 0.75 に。claude-opus-4.6 は逆に 0.88 → 0.94 と上がりました(必須要素を全行に展開)。書く量と質は別物 であるだけでなく、同じ書き方への反応もモデルで割れる 可能性があるというのが v1 の収穫です。v3 で必須項目を 2 つに絞ったのは、ここで見えた「展開と圧縮のばらつき」を抑える狙いです。
今回の v2 では、テンプレートの見た目に引っぱられた挙動が観測された:v2 で <type>(<scope>): <subject> のテンプレートを書いたら、claude-opus-4.6 が fix task で fix: correct typo in README.md installation instructions (#99) と Issue 番号を subject 内に埋め込んできました。gpt-5.4 の方は同じ fix task で docs: fix typo in README installation instructions と type を docs: 判定しつつ scope を省略。テンプレートの見た目は、見本としてかなり強く引っぱります が、それゆえに本来の規約と矛盾する挙動を引き出すこともあります。v3 で Closes/Fixes/Refs を「subject 内ではなく必ず最後の footer 行」と書き加えたのは、ここの再発を防ぐためです。
AGENTS.md グローバルルールが SKILL を上書きしている可能性がある:claude-opus-4.6 の has_typed_scope は 3 回の改訂を通じて Fail のままでした。プロジェクトルートの AGENTS.md に「Conventional Commits を使う」とだけ書いてあって scope の必須性に触れていない点が影響していそう、という 仮説段階 の解釈です。確定させるには AGENTS.md を書き換えた A/B 実験が必要で、本記事の検証では未実施。後述の 8-3 で改めて取り上げます。v3 で scope に (general) の逃げ道を許したのは、SKILL.md から「絶対 scope を書け」と押すだけでは届かないと判断したためです。
評価駆動でやらないと、これらの発見は表に出ない:SKILL.md を v1 から v2 に書き直したとき「ちょっと改善したっぽいな」と感覚で済ませていたら、claude-opus-4.6 の 0.94 → 0.81 の悪化(特に Closes #99 が消えた挙動)は気づかなかったでしょう。Waza が 悪化を即時に検出してくれる から、書き直しを安全に試行できます。
6-6. モデル間の差を見る
waza run は --model で実行モデルを切り替えられます。同じ評価スイートを複数モデルで走らせて、waza compare で比較できます。先ほどと同様、eval ファイルを直接指定します。
waza run evals/commit-message-writer/eval.yaml --model gpt-5.4 -o results-after-gpt5.json
waza run evals/commit-message-writer/eval.yaml --model claude-opus-4.6 -o results-after-opus.json
waza compare results-after-gpt5.json results-after-opus.json
利用できるモデル名は waza models で一覧が出ます。私の環境(個人の GitHub Copilot サブスクリプション、2026 年 5 月時点)では Copilot SDK 経由で 13 モデルにアクセスでき、claude-opus-4.6 claude-sonnet-4.6 gpt-5.4 などが含まれていました。利用できるモデル数や種類は契約・組織ポリシー・時点によって変わります。

7. CI で評価パイプラインを検証する
ここまでで手元の評価ループが揃いました。次は、Pull Request ごとに自動で「評価設定が壊れていないか」を確認する CI を作ります。本記事の CI は、品質回帰(モデルが書く文章が悪化したか)まで見るものではありません。それは手元の copilot-sdk 実行で見る前提で、CI では 配布物のドリフト検出 と eval / task / fixture / grader が壊れていないかの mock 動作チェック に役割を絞ります。

PR トリガーから paths filter で apm-install.yml と waza-eval.yml に分岐し、それぞれが SARIF / ドリフト検出 / 評価レポート artifact を出す構成。
7-1. APM 配布の検証ジョブ
APM 公式の apm-action を使うと、CI 上で APM CLI のセットアップと apm install をまとめて済ませられます。audit-report: "true" を付けると、Hidden Unicode などの監査結果を SARIF で出してくれるので、Code Scanning タブで一覧できます。
.github/workflows/apm-install.yml
name: APM Install Check
on:
pull_request:
paths:
- "apm.yml"
- "apm.lock.yaml"
- ".apm/**"
- ".github/**"
- ".agents/**"
- ".github/workflows/apm-install.yml"
push:
branches:
- main
paths:
- "apm.yml"
- "apm.lock.yaml"
- ".apm/**"
- ".github/**"
- ".agents/**"
- ".github/workflows/apm-install.yml"
permissions:
contents: read
security-events: write
jobs:
apm-install:
name: Install and verify APM primitives
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Install APM primitives
id: apm
uses: microsoft/apm-action@v1
with:
# 記事本文の検証バージョンに固定。読者が再現したときに本文と CI で挙動が
# 揃うようにする。最新版を試したい場合は "latest" に変更する。
apm-version: "0.12.1"
audit-report: "true"
- name: Upload APM audit SARIF
uses: github/codeql-action/upload-sarif@v3
if: always() && steps.apm.outputs.audit-report-path
# Code Scanning が無効化されている repo(GitHub Advanced Security 未契約の private repo など)
# では upload-sarif が "Resource not accessible by integration" で落ちる。
# SARIF を取得することが目的なので、upload 失敗で CI を止めない。
continue-on-error: true
with:
sarif_file: ${{ steps.apm.outputs.audit-report-path }}
category: apm-audit
- name: Detect deployment drift
run: |
# 本リポジトリの target は copilot,agent-skills なので、
# drift check の対象も apm install で更新される .github と .agents に絞る。
if ! git diff --exit-code -- .github .agents; then
echo "::error::apm install の結果と一致しません。ローカルで apm install を実行し、.github/、.agents/ をコミットしてください。"
exit 1
fi
ポイントは 5 つ。
-
target は
apm.ymlに書いておく-
apm-actionのデフォルト動作はapm installを引数なしで実行するので、target がapm.ymlに書いてあれば手元と CI で完全に同じコマンドで揃います。apm-actionのtarget:入力は別物(pack: true専用の bundle target)なので混同しないように注意。audit-reportを使う場合、ソースで確認するとsetup-onlyとは排他なので、SARIF を取りたいならこちらの構成を採ります
-
-
ドリフト検出は
.githubと.agentsに絞る- 本リポジトリの target は
copilot,agent-skillsなので、apm installで更新されるのもこの 2 ディレクトリだけです。.claude.cursorまでは drift check に含めていません(target に入っていないため、apm install しても何も配布されない)。target を増やすときはgit diff --exit-codeの引数も合わせて広げます
- 本リポジトリの target は
-
apm-version: "0.12.1"で固定- 本記事の検証バージョンに合わせて固定しています。
apm-action@v1は内部でapm-version: "latest"をデフォルトにしているので、何もしないと将来 APM のメジャー更新が入ったタイミングで挙動が変わる可能性があります。記事と CI の挙動を揃えたい場合はここを明示するのが安全です。最新版を試したい場合は"latest"に変更します。なお、microsoft/apm-action@v1という参照そのものも floating tag なので、より厳密に再現性を担保するならapm-actionを commit SHA で pin することも検討してください(本記事では読みやすさを優先して@v1のままにしています)
- 本記事の検証バージョンに合わせて固定しています。
-
SARIF upload は
continue-on-error: true- Code Scanning が無効化された repo(GitHub Advanced Security 未契約の private repo など)では
upload-sarif@v3がResource not accessible by integrationで落ちます。SARIF 出力自体は audit-report で取れるので、Code Scanning に流せなくても CI を緑のままにする構成にしています。public 化または GHAS 有効化のタイミングでcontinue-on-errorを外せば、Code Scanning タブで Hidden Unicode などの監査結果を一覧できる状態に戻せます
- Code Scanning が無効化された repo(GitHub Advanced Security 未契約の private repo など)では
-
paths にワークフロー自身を含める
- CI 修正でハマったポイントです。
pathsに self(.github/workflows/apm-install.yml)を含めないと、ワークフローを直すコミットを push したときにそのコミット自身がトリガーされず、修正が効いているかを次の関連コミットまで確認できません。地味ですが押さえておくと安全
- CI 修正でハマったポイントです。
gh run list で見ると、上記の 5 つのポイントを潰す前と後の差がはっきり出ます。失敗していたものを 1 つずつ直して、最新 4 ランが緑になっています。

7-2. Waza 評価ジョブ
評価ループの自動化です。本記事の workflow は eval.yaml の config.executor: mock 前提のドライラン として構成しています。実モデル評価は手元(Copilot CLI 認証済みの環境)で実施しました。
.github/workflows/waza-eval.yml
name: Waza Skill Eval
# 本ワークフローは eval.yaml の config.executor: mock 前提で動かすドライランです。
# モデル品質の比較ではなく、eval.yaml、task、fixture、grader が壊れていない
# ことを確認するためのものです。実モデル評価は手元(Copilot CLI 認証済みの環境)で
# 実施しています。
# CI で実モデル評価まで回す場合は、Copilot CLI のログインを GitHub Actions 上で
# 用意する必要があります(本記事の検証範囲外)。
on:
pull_request:
paths:
- "evals/**"
- ".apm/skills/**"
- ".agents/skills/**"
- ".github/workflows/waza-eval.yml"
push:
branches:
- main
paths:
- "evals/**"
- ".apm/skills/**"
- ".agents/skills/**"
- ".github/workflows/waza-eval.yml"
permissions:
contents: read
jobs:
evaluate:
name: Mock dry-run for label ${{ matrix.model }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# mock executor では実モデルは呼ばれない。--model は結果ファイル名の
# ラベル分けに使うだけで、品質比較にはならない。
model:
- gpt-5.4
- claude-opus-4.6
max-parallel: 2
steps:
- uses: actions/checkout@v4
- name: Install waza
# install.sh は latest release を取得する。本記事の実機検証は v0.31.0 で
# 行っているため、厳密に同じ条件で再現する場合は GitHub Releases の
# v0.31.0 asset を直接取得する構成にしてください。
run: |
curl -fsSL https://raw.githubusercontent.com/microsoft/waza/main/install.sh | bash
echo "$HOME/bin" >> "$GITHUB_PATH"
- name: Verify waza installed
run: waza --version
- name: Run evaluations (mock dry-run)
run: |
set +e
# eval.yaml を直接指定して実行する。eval.yaml の skill_directories で
# .agents/skills/ を Waza の探索対象に加えているため、APM が配布した
# SKILL.md がそのまま評価対象になる。
waza run evals/commit-message-writer/eval.yaml \
--model "${{ matrix.model }}" \
--output "results-${{ matrix.model }}.json" \
--verbose
status=$?
# Waza の exit code:
# 0 = 全 grader pass
# 1 = 1 つ以上の grader fail(mock では想定内)
# 2 = 設定エラー / 実行エラー(CI を落とす)
if [ $status -eq 2 ]; then
echo "::error::waza run が設定/実行エラーで終了しました (exit 2)"
exit 2
fi
if [ ! -f "results-${{ matrix.model }}.json" ]; then
echo "::error::waza run が結果ファイルを生成できませんでした"
exit 1
fi
echo "Mock dry-run completed; result file generated for ${{ matrix.model }} (waza exit $status)."
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: results-${{ matrix.model }}
path: results-${{ matrix.model }}.json
retention-days: 30
CI で押さえておきたい点。
--model は結果ファイル名のラベル分けにしか使われない。
eval.yaml の config.executor を mock にしている本ワークフローでは、実モデルは呼ばれません。matrix で gpt-5.4 と claude-opus-4.6 を並べているのは、結果 JSON のファイル名を分けて取り出しやすくするだけのためです。モデル品質の比較は手元の copilot-sdk 実行で行い、CI ではあくまで eval.yaml / task / fixture / grader が壊れていないことだけを確認します。
CI 上の認証は別途セットアップが要ります。
Waza 0.31.0 の copilot-sdk engine は GitHub Copilot サブスクリプション認証で動くため、OPENAI_API_KEY や ANTHROPIC_API_KEY を Secrets に登録しても Waza 自身は読みません。Waza 公式の CI 例で env に GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} が出てくることもありますが、少なくとも手元検証では これだけでは copilot-sdk engine の認証は通りませんでした。GitHub Actions 上で実モデル評価を回すには、Copilot CLI を事前にログインさせる(copilot auth login と同等の状態を再現する)など別ステップが必要になります。本記事の検証では 手元での実モデル評価のみ実施 し、GitHub Actions 上で copilot-sdk 実行までは未確認です。CI で実モデル評価を回す場合は、まず executor: mock でドライラン用ジョブを作って、実モデル評価は別ジョブまたはローカルに切り出す構成が安全です。
APM は固定し、Waza は実行時のバージョンを確認する。
apm-action の apm-version を "0.12.1" で固定、Waza の install script は latest を取得する点を本文に明記しています。CI と記事本文で APM のバージョンが揃わないと、読者が再現したときに本文の挙動と CI の出力で食い違うことが出てきます。Waza は install.sh 経由なので CI では latest が入ります。厳密に同じ条件で再現するなら、Waza の GitHub Releases から v0.31.0 の asset を直接取得する構成に切り替えてください。
結果は artifact に上げる。
if: always() を付けて、評価ジョブが落ちたケースでも JSON を取り出せる状態にしておくのがコツです。
mock dry-run では grader fail で waza run が exit 1 を返す。
config.executor: mock は固定回答を返すだけなので、grader が大半 fail するのは想定動作です。Waza 0.31.0 の実機で確認した範囲では、waza run の exit code は 0(全 grader pass)/ 1(grader fail)/ 2(設定または実行エラー)の 3 値で動いていました。step では set +e で握ってから exit 2 だけ受け止めて落とす 形にしています。1 は mock の想定内、2 は grader fail とは別系統の壊れ方なのでこちらは CI を red にする、という分け方です。実モデル評価のジョブを作るなら、そちらは exit code をそのまま尊重すれば OK です。
パスフィルターで暴発を防ぐ。
README の typo 修正で評価が走って Copilot のクォータが跳ねる、というのは避けたい事態です。
7-3. Waza 0.31.0 の制約と運用上の注意
ここからは公式ドキュメントだけ読んでいると見えてこない、手元で詰まった部分の記録です。私が触った 0.31.0 で確認した範囲の話なので、バージョンが変われば挙動も変わる前提で読んでください。
最初にハマったのが executor の選択肢です。config.executor に書ける値として、私が試した範囲では mock と copilot-sdk の 2 つしか有効に動きませんでした。foundry や azure-openai のような独立した engine は実装されていないようで、書くと unknown engine type で落ちます。
ここに関連して、API キー認証は Waza 単体では効かないようです。OPENAI_API_KEY ANTHROPIC_API_KEY AZURE_OPENAI_* を Secrets に登録しても、少なくともモデル実行の認証としては Waza 自身に読まれていません。copilot-sdk engine が認証に使うのは GitHub Copilot のサブスクリプションです。Microsoft Foundry や Azure OpenAI を直接叩きたい場合、Waza 0.31.0 単体ではその構成に到達できませんでした。
もう 1 つ詰まったのが skill_invocation グレーダーです。「スキルが起動したか」を評価できる便利なグレーダーですが、今回の Waza v0.31.0 + copilot-sdk 実行では precision / recall / f1 がいずれも 0 になりました。起動シグナルが取れていないようです。同じ用途で動かしたい場合は、SKILL.md 側に署名となる文言を仕込んで task の expected.output_contains で間接的に検出する、というのが現実的でした。
地味なところで、--executor --engine のような CLI フラグも見当たりません。古いブログ記事や生成 AI の出力にこれらが書かれていることがあるので、コピペで動かないときはまずここを疑うのが早いです。executor の切替は eval.yaml の config.executor でだけ行います。
これらは将来のバージョンで変わる可能性がありますが、2026 年 5 月時点では Copilot サブスクリプション経由が前提です。それを踏まえて CI を組むのが現実解だと思っています。
利用できるモデル一覧は waza models で確認できます。--model フラグの値はここに出ているものから選びます。
`waza models` の出力(v0.31.0 / 2026-05 検証時点)
$ waza models
Available models for waza:
• claude-haiku-4.5
• claude-opus-4.5
• claude-opus-4.6
• claude-opus-4.6-1m
• claude-sonnet-4
• claude-sonnet-4.5
• claude-sonnet-4.6
• gpt-4.1
• gpt-5-mini
• gpt-5.2
• gpt-5.2-codex
• gpt-5.3-codex
• gpt-5.4
Use these model identifiers with --model flag.
ラインアップは GitHub Copilot 側のモデル提供状況に追従するので、随時変わります。
8. 検証の総括
ここから先は事実ではなく、触ってみたうえでの整理です。
8-1. 何が解けて、何が残るか
評価駆動ループを回してみて、APM × Waza の組み合わせが解いてくれそうな問題と、そうでない問題が見えてきました。
今回の構成で解けたところ
- スキルを書いて配るだけだった運用に、「評価する」工程が手元で習慣化される
- 評価設定の崩れ(eval / task / fixture / grader が壊れていないか)と配布物のドリフト(
apm installの結果と commit 済みファイルのズレ)は CI で自動化できる - モデル間の挙動差を、感覚ではなく数値で比較できる
- 品質回帰(モデルの書き方が悪化していないか)は手元の実モデル評価で押さえる前提(Copilot CLI 認証を CI に持っていく工程はまた別途)
特に大きいのは 1 番目です。これまで「スキルを書く」工程だけが膨らんで、「効いているか確認する」工程はおざなりになりがちでした。Waza があると、スキルを書くたびに「評価する」が習慣として組み込まれます。
実機検証で見えたこと(独自の発見)
今回の題材で評価駆動ループを 3 周回して、SKILL.md の書き方そのものに対する気づきが大きく 3 つありました。いずれも 2 task × 2 model × trials_per_task: 1 の観測なので、一般則というより 今回の eval 構成で見えた傾向 として読んでください(A/B の確証は次節 8-3 で挙げる検証ポイント)。
いちばん意外だったのは、SKILL.md は厚く書くほど効くわけではない という挙動です。
v1 で「5 つの必須要素」を列挙したら、gpt-5.4 のスコアが 0.88 → 0.75 に下がりました。両 task で subject 1 行だけ返して body と footer を省略する挙動が広がっていて、書き手が善意で増やした指示が逆に省略を誘発していました。同じ v1 で claude-opus-4.6 は 0.88 → 0.94 に上がっていて、指示を増やしたときの反応がモデルで割れる という事実は、評価で動かしてみないと見えなかったと思います。
2 つ目は、テンプレートの見た目に引っぱられる挙動が出たこと。
v2 でテンプレートを丁寧に見せたら、claude-opus-4.6 が (#99) を subject に埋め込む挙動を見せて、本来 footer に置くべき Issue 参照が消えました。SKILL.md のテンプレート例は 形式の手本として強く効きますが、「形式の枠」が強すぎると本来の規約より優先されることがあります。
3 つ目は、AGENTS.md グローバルルールが SKILL を上書きしている可能性がある、という仮説です。
claude-opus-4.6 の has_typed_scope Fail は 3 回の改訂を通じて変わらず、SKILL.md で押し返せませんでした。プロジェクトルートの AGENTS.md に scope の必須性が書かれていないので、SKILL.md だけ書き直しても届かなかった、という解釈をしています。確証はまだ持てていない(次節 8-3 の検証ポイント)ですが、スキル単体で書き直しても効かないことがあるな、ハーネス全体で揃えないと届かないところもありそうだな という感覚を持つようになりました。
残った課題(現時点で確認した範囲)
一方で、ツールが整っても残る課題もあります。
- 「合格基準」の設計は結局書き手任せです。weight を何点にするか、何種類のグレーダーを重ねるかは、スキルごとに考える必要があります
- prompt グレーダーを多用するとトークンコストが読みにくくなります。確定的グレーダーをいかに重ねて主観評価を減らすかが運用設計の肝
- AGENTS.md など上位ハーネスとの整合は、Waza が直接見てくれるわけではありません。SKILL を改訂しても上位ルールが矛盾していれば効きません
8-2. 評価駆動ループが効くケース
今回の検証から、APM × Waza の組み合わせが特に効きそうなケースを 3 つ挙げます。
- 同じスキルを複数モデル、または複数開発者で運用するケース
- モデル切替や個人差で挙動がぶれやすい状況で、ぶれを数値で検知できる
- SKILL.md を頻繁に書き直すケース
- v1 v2 v3 のような実験的な改訂を回したいときに、悪化を即座に検出できる
- GitHub の運用ルール(Conventional Commits、Issue 連携)にスキルを噛ませたいケース
- 今回の題材のように、規約への準拠そのものが価値になる場面
逆に、本記事の commit-message-writer は LLM がふだん書き慣れている題材です。SKILL.md を薄くしても最初からそれなりに書けてしまうため、v0 → v3 で見えた挙動の幅は、この題材という、効きが小さく出やすい条件で観測したもの だと思っていてもらえれば。LLM があまり書き慣れていない skill(社内テンプレ、独自規約、特定言語のセキュリティ観点など)だと、SKILL.md の書き直しで挙動を動かせる幅も、書き換えた途端に挙動が崩れる場面も、本記事よりはっきり出ると思います。それでも、ふだん書き慣れている題材ですら v0 → v3 で差分が出た、という事実自体は素直に収穫でした。
8-3. 今後の検証ポイント
実際に運用に乗せていくうえで、もう少し詰めたい点を挙げておきます。特に AGENTS.md と SKILL.md の関係は、本記事で「仮説」として書いた挙動を確証するために A/B 実験を回したい部分です。
-
AGENTS.md と SKILL.md の整合
-
claude-opus-4.6の scope 抜けが AGENTS.md 由来かを確かめるなら、(A) AGENTS.md に「scope を必ず書く」を追記、(B) AGENTS.md の Conventional Commits 行を削除、(C) AGENTS.md と SKILL.md の双方に scope 必須を書く、(D) SKILL.md だけに書く、の 4 条件で同じ評価スイートを回して、has_typed_scopeの通過率を比較するのが堅いやり方です。本記事では (D) のみ実施
-
-
eval suite に対する過学習のリスク
- 今回の改訂は同じ 2 task を見ながら回したので、SKILL.md が この 2 ケースに最適化 されている可能性が残ります。検証の終盤に hold-out task(feat/fix とは別の type)を 1 つ追加して再評価したところ、後述のとおり実際にここで割れる挙動が観測できました
-
weightのチューニング- 合成スコアがどれくらい安定するか、試行回数を増やしての分散測定
-
waza qualityの使い所- 通常の
waza runと組み合わせたときの相補性
- 通常の
-
GitHub Actions 上での実モデル評価
- 本記事は手元のみで実施。Copilot CLI の認証まで含めた CI 構成の検証
公式ドキュメントの更新を追いながら、継続して検証していきます。
8-4. hold-out task で過学習を疑う
ここまでの評価はすべて feat / fix の 2 task で回してきました。SKILL.md を書く側がこの 2 task の Fail を見ながら改訂していると、ルールが 「2 task の Fail を消すための条件」に過適応 している可能性があります。
これを確かめるため、eval suite に含まれていなかった type を 1 つ用意して v3 SKILL.md のまま走らせました。題材は chore 種別の package.json devDependencies 一括更新(Issue #123)で、本記事の改訂中に一度も使っていない diff です。これを gpt-5.4 と claude-opus-4.6 の 2 モデルで評価しました。
結果は次のとおりです。aggregate スコアは grader 単位の重み付き平均で、pass_rate は 全 grader が pass した task の割合 です。
| v3 採用時 feat | v3 採用時 fix | hold-out chore | hold-out 再実行 feat | hold-out 再実行 fix | |
|---|---|---|---|---|---|
gpt-5.4 |
1.00 | 1.00 | 0.62 | 1.00 | 0.75 ⬇️ |
claude-opus-4.6 |
0.88 | 0.88 | 0.88 | 1.00 ⬆️ | 0.88 |
⬆️ / ⬇️ の矢印は、hold-out 再実行 列の値が v3 採用時 の同 task に対してどう動いたかを表しています。
hold-out chore列は新規追加の task なので、Before との比較対象はありません。
gpt-5.4 の hold-out chore は subject 1 行だけで返ってきました。
chore(deps): bump prettier, typescript, and vitest devDependencies for #123 という形で、body 抜け、footer 抜け、72 文字超過の 3 つを同時に Fail。fix task を再実行したら fix(readme): correct installation typo for Issue #99 と返してきて、こちらも body 抜け、footer 抜け、subject 内に for Issue #99。v3 採用時の 1.00 から 0.75 に 後退 しました。
trials_per_task: 1 で取った v3 採用時のスコアは、1 試行ぶんのばらつきの上振れを拾った可能性があります。
claude-opus-4.6 の hold-out chore は body と scope はちゃんと書いてくれましたが、Issue 参照を (#123) の形で subject に埋め込んでしまい、footer の Closes/Fixes/Refs #123 が消えました。
これは v2 で観測した挙動が hold-out でも再現した形で、v3 で「subject 内ではなく必ず最後の footer 行に置く」と書き加えたルールが、fix 系の文脈では効いていたのに chore 系で外れた、というのが現状の解釈です。
一方で feat task は scope 込みで feat(api): add request rate limiting to prevent overload + body + Closes #42 が揃って 1.00。v3 採用時の 0.88 から 改善 していますが、これも 1 試行のばらつきとして読むのが妥当でしょう。
この hold-out から見えたのは 2 つです。
-
今回の hold-out(chore 1 件)では、v3 SKILL.md は eval 中に見せた
feat/fixほど効きませんでした。1 type の追加だけで「過学習」と確証することはできませんが、suite 外 task で挙動が割れる気配は見えました -
trials_per_task: 1のスコアは、モデルのばらつきと SKILL.md の改善効果が混ざって見えています。同じ v3 SKILL.md でgpt-5.4の fix task が 1 回目 1.00、2 回目 0.75 に、claude-opus-4.6の feat task が 1 回目 0.88、2 回目 1.00 に振れた以上、本来は試行回数を増やして分散込みで議論すべきです
これは「評価駆動が効かない」という結論ではなく、評価駆動を回すなら hold-out task と試行回数の両方を最初から設計に入れる必要がある という意味です。
本記事の改訂は trials_per_task: 1 × tasks=2 で進めましたが、運用に乗せるなら trials_per_task: 3 以上 + hold-out task を最低 1 つは別建てで持つ構成が現実的です。
Waza は waza run --trials で各 task を複数回実行でき、waza serve のダッシュボードでは trials のスコア分布も確認できます。
waza compare 自体は run 同士のスコア差分や pass rate 差分を一覧するコマンドなので、--trials で増やした試行を waza serve で分布として見つつ、改訂前後の run を waza compare で比較する、という分担が素直な使い方です。
おわりに
GitHub Copilot などの Agentic なCoding ツールの Skill を自分ひとりで使う分には、プロンプトを工夫すれば十分です。ただ、同じ命令を複数人で共有する、もしくは「その命令が効いたかを評価する」となると、話は変わります。
microsoft/apm は前者を、microsoft/waza は後者を担う形で用意されていて、2 つを組み合わせると、個人検証から CI までが一続きになります。
今回 SKILL.md を 3 回書き直して、毎回 Waza が「悪化した」「改善した」を即座に教えてくれたのが、いちばん助かりました。書き手の善意で書いた指示が、実は逆効果だったというのは、評価で動かしてみないと分かりません。書く側の感覚と、モデルの挙動には、想像以上にギャップがあります。
Skill をチームで使うなら、Waza で「評価する」工程を最初から組み込むのが、書き手にとってもチームにとっても安全だと感じました。
検証で使ったリポジトリはこちらです。本記事の apm.yml / eval.yaml / tasks/*.yaml はコピーすればそのまま動く粒度で書いてありますが、リポジトリ全体を git clone して試すほうが手早いです。
また、私の他のGitHub Copilot関係記事もご参考までにリンクを置いておきます。
参考リンク
免責事項
本記事は筆者個人の見解/検証結果であり、所属組織の公式見解ではありません。記事中のコマンドやコード例は、環境によって動作が異なる場合があります。また、記載されたサービスや機能はプレビュー段階のものを含み、仕様は予告なく変更される場合があります。
本記事は情報提供を目的としており、2026年5月5日時点の情報に基づいています。本記事について、内容の正確性・完全性は保証されず、誤りを含む可能性があります。公式ドキュメントで最新情報をご確認ください。記事内のコードサンプルおよび筆者のGitHubリポジトリは自己責任でご利用ください。本記事内容の利用によって生じたいかなる損害(サービスの中断、データ損失、営業損失等を含む)についても、著者は一切の責任を負いません。本記事に掲載されている各社製品・サービスは各社の利用規約に従ってご利用ください。
Discussion