✍️

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 で配って Copilot CLI / Agent から再評価する。
評価駆動で回すループの全体像。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 を直したときの差がもっと大きく出るかもしれません。

両ツールのリポジトリはこちらです。

https://github.com/microsoft/apm

https://github.com/microsoft/waza

ドキュメントはそれぞれ以下です。

APMのドキュメント
https://microsoft.github.io/apm/

Wazaのドキュメント
https://microsoft.github.io/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 を改善する」ループが成立します。

評価駆動ループの 5 ステップ。スキルを書く → Waza で評価 → 結果を見て改善 → APM で配布。配ったあとは CI で再評価して、Waza に戻るフィードバックループになる。
評価が先、配布が後。配ったあとも 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.ymltarget[copilot, agent-skills] と並べておくか、apm install --target copilot,agent-skills のように CLI 引数で渡します。

なお、以下の記事もAPMの理解を深めるうえで参考になります。
https://zenn.dev/microsoft/articles/agent-package-manager-handson

3-2. 最小構成のセットアップ

apm.yml がプロジェクトのマニフェストです。展開先(target)はここに書いておくのが、手元と CI で挙動を揃える上で楽です。

apm.yml の最小例
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.ymltarget を読み取って、.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(eval.yaml / tasks / fixtures)、Executor(mock / copilot-sdk)、Outputs(graders → Score)の 3 レーンで構成。
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.yamlconfig.executor で切り替えます。CLI フラグ(--executor のような)は現バージョンに存在しません。モデルだけ切り替えたいときは --model で上書きできます。

4-3. 最初に Mock を使うとありがたい理由

Waza 0.31.0 の eval spec では executor のデフォルトは copilot-sdk(実モデル)です。それでも本記事では eval.yamlexecutor: 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.ymltarget: [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
evals/commit-message-writer/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
evals/commit-message-writer/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
evals/commit-message-writer/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
evals/commit-message-writer/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.yamlgraders: 直下に置きます。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 の全文
evals/commit-message-writer/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.yamlskill_directories を使っているので、waza run commit-message-writer のような skill 名指定では skill が解決されません。

waza run evals/commit-message-writer/eval.yaml -v

waza run -v の実行画面。GRADER の ✓/✗、Aggregate Score 0.50、PER-TASK BREAKDOWN、Failed Tests が一望できる。
mock executor で waza run -v を流した結果。grader 単位の pass/fail と Aggregate Score が確認できる。

実モデルで動かすときは eval.yamlconfig.executorcopilot-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 スコアがどう動いたかを先に俯瞰しておきます。

v0 から v3 までの aggregate スコア推移。 は 0.88 → 0.75 → 0.88 → 1.00、 は 0.88 → 0.94 → 0.81 → 0.88 で、両モデルの動きが非対称であることが折れ線で見える。
3 回の改訂と aggregate スコアの動き。gpt-5.4 は v1 で悪化、v3 で 1.00 に。claude-opus-4.6 は v1 で微改善、v2 で悪化、v3 で v0 と同水準に戻った。

Waza eval dashboard の Runs ビュー。commit-message-writer と python-code-review の評価ランがモデル別に並び、Pass Rate / Tokens / Cost / Duration が見える。
waza serve のダッシュボード。手元の results-*.json を読み取って、モデル別のスコア・トークン・コストを一覧する。

wazaのdashboardについての公式ドキュメントは以下です。
https://microsoft.github.io/waza/guides/dashboard/

書き手が善意で増やした指示が逆効果になる挙動が、v1 と v2 で観測されました。
以下、各改訂を順に見ていきます。

6-1. Before のスコア

最小 SKILL.md(章 5-2)で gpt-5.4claude-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_bodyreferences_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 がこんな形で返ってきていました。

results-v2-opus.json から 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 の本文(採用版)
.apm/skills/commit-message-writer/SKILL.md
---
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 の宿題)
  • 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.6has_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.6has_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 などが含まれていました。利用できるモデル数や種類は契約・組織ポリシー・時点によって変わります。

waza compare の出力。AGGREGATE で gpt-5.4 が 1.0000、claude-opus-4.6 が 0.8750、Delta -0.1250。PER-TASK DELTAS で 2 task とも同じ差分が ↓ で表示されている。

7. CI で評価パイプラインを検証する

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

CI の全体像。PR から paths filter を経て、apm-install.yml が SARIF と ドリフト検出に分岐し、waza-eval.yml が評価レポート artifact を出す構成図。
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
.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-actiontarget: 入力は別物(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 の引数も合わせて広げます
  • 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@v3Resource not accessible by integration で落ちます。SARIF 出力自体は audit-report で取れるので、Code Scanning に流せなくても CI を緑のままにする構成にしています。public 化または GHAS 有効化のタイミングで continue-on-error を外せば、Code Scanning タブで Hidden Unicode などの監査結果を一覧できる状態に戻せます
  • paths にワークフロー自身を含める

    • CI 修正でハマったポイントです。paths に self(.github/workflows/apm-install.yml)を含めないと、ワークフローを直すコミットを push したときにそのコミット自身がトリガーされず、修正が効いているかを次の関連コミットまで確認できません。地味ですが押さえておくと安全

gh run list で見ると、上記の 5 つのポイントを潰す前と後の差がはっきり出ます。失敗していたものを 1 つずつ直して、最新 4 ランが緑になっています。

gh run list --limit 8 の出力。最新 4 ラン(APM Install Check / Waza Skill Eval)が success(緑)、過去 4 ランが failure(赤)で並ぶ。

7-2. Waza 評価ジョブ

評価ループの自動化です。本記事の workflow は eval.yamlconfig.executor: mock 前提のドライラン として構成しています。実モデル評価は手元(Copilot CLI 認証済みの環境)で実施しました。

.github/workflows/waza-eval.yml
.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.yamlconfig.executormock にしている本ワークフローでは、実モデルは呼ばれません。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_KEYANTHROPIC_API_KEY を Secrets に登録しても Waza 自身は読みません。Waza 公式の CI 例で envGITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} が出てくることもありますが、少なくとも手元検証では これだけでは copilot-sdk engine の認証は通りませんでした。GitHub Actions 上で実モデル評価を回すには、Copilot CLI を事前にログインさせる(copilot auth login と同等の状態を再現する)など別ステップが必要になります。本記事の検証では 手元での実モデル評価のみ実施 し、GitHub Actions 上で copilot-sdk 実行までは未確認です。CI で実モデル評価を回す場合は、まず executor: mock でドライラン用ジョブを作って、実モデル評価は別ジョブまたはローカルに切り出す構成が安全です。

APM は固定し、Waza は実行時のバージョンを確認する。
apm-actionapm-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 に書ける値として、私が試した範囲では mockcopilot-sdk の 2 つしか有効に動きませんでした。foundryazure-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.yamlconfig.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.4claude-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 つです。

  1. 今回の hold-out(chore 1 件)では、v3 SKILL.md は eval 中に見せた feat / fix ほど効きませんでした。1 type の追加だけで「過学習」と確証することはできませんが、suite 外 task で挙動が割れる気配は見えました
  2. 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 して試すほうが手早いです。

https://github.com/naoki1213mj/copilot-skill-apm-waza-demo

また、私の他のGitHub Copilot関係記事もご参考までにリンクを置いておきます。
https://zenn.dev/microsoft/articles/9fb85cdca2fb84
https://zenn.dev/microsoft/articles/b8ec09b8599716
https://zenn.dev/microsoft/articles/bdbed630a7df9b

参考リンク

https://github.com/microsoft/apm

https://github.com/microsoft/waza

https://github.com/microsoft/apm-action

https://github.com/microsoft/apm/releases

https://github.com/microsoft/waza/releases

https://docs.github.com/en/copilot/how-tos/copilot-cli/use-copilot-cli/overview

免責事項

本記事は筆者個人の見解/検証結果であり、所属組織の公式見解ではありません。記事中のコマンドやコード例は、環境によって動作が異なる場合があります。また、記載されたサービスや機能はプレビュー段階のものを含み、仕様は予告なく変更される場合があります。
本記事は情報提供を目的としており、2026年5月5日時点の情報に基づいています。本記事について、内容の正確性・完全性は保証されず、誤りを含む可能性があります。公式ドキュメントで最新情報をご確認ください。記事内のコードサンプルおよび筆者のGitHubリポジトリは自己責任でご利用ください。本記事内容の利用によって生じたいかなる損害(サービスの中断、データ損失、営業損失等を含む)についても、著者は一切の責任を負いません。本記事に掲載されている各社製品・サービスは各社の利用規約に従ってご利用ください。

Microsoft (有志)

Discussion