ANTIGRAVITY LABEN
記事一覧/Agents & Manager
Agents & Manager/2026-07-16上級

AGENTS.md を厚くするほどルールが守られなくなった — 遵守率を計測して指示を削るまで

AGENTS.md に書いたルールが守られない。衝突でも読み込み失敗でもなく、単に無視されていました。ルールを検証可能な述語に分解し、遵守率を継続計測して指示を削るまでの運用記録です。

AGENTS.md12Antigravity336エージェント運用10計測3品質ゲート5プロンプト設計2

プレミアム記事

console.log が本番のバンドルに乗っているのを、リリース後にブラウザのコンソールで見つけました。

AGENTS.md には「console.log を本番コードに残さない」と書いてあります。書いたのは私です。ファイルは1つ、monorepo でもなく、ルートに置いた47行。読み込み順序の衝突を疑う余地はありませんでした。

エージェントはそのファイルを読んでいて、それでも守っていなかった。

最初に思ったのは「モデルの機嫌」でした。しかし機嫌のせいにすると、打てる手が何もなくなります。そこで、守られたかどうかを私の記憶で判定するのをやめて、機械で数えることにしました。

数え始めてから分かったのは、守られないルールには規則性があるということです。

「守られている気がする」で運用していた

それまでの私の運用は、こうでした。エージェントに実装を任せる。返ってきた差分を読む。気になったところを指摘する。指摘が多い日は「今日は調子が悪い」と思う。

この運用の問題は、分母がないことです。

10個のルールのうち何個が守られたのか、先週と比べて改善したのか悪化したのか、47行のうちどの行が効いているのか。何も分からないまま、私は AGENTS.md に行を足し続けていました。守られないルールを見つけるたびに、より強い言葉で、より詳しく書き直す。ファイルは3月に12行だったものが、7月には47行になっていました。

そして遵守率は、体感で下がっていました。体感で、というのが問題です。

ルールには2種類ある

計測しようとして最初に詰まったのが、「このルールは守られたか」を機械が答えられない、という点でした。

たとえば「関数は最大30行とし、それを超える場合は分割する」は数えられます。一方で「コードの意図が伝わる命名にする」は数えられません。前者は述語に落ちますが、後者は落ちません。

この線引きが、後で効いてきます。とりあえず落ちるものだけを対象にしました。47行のうち、検証可能な述語に分解できたのは19個でした。残りは「良いコードを書く」の言い換えでした。

半分以上が、機械で確かめられないルールだったわけです。この時点で、私は自分のファイルに何を書いていたのか怪しくなってきました。

ルールを述語に分解する

19個のルールを、それぞれ「差分に対して真偽を返す関数」に書き直しました。

エージェントが変更したファイルの内容を受け取り、違反していれば違反箇所を返す。それだけの関数です。難しいことはしていません。

# tools/adherence/rules.py
# AGENTS.md の各ルールに 1 対 1 で対応する検証関数を並べる。
# ここに書けないルールは AGENTS.md にも書かない、という方針の受け皿にする。
 
import re
from dataclasses import dataclass
from typing import Callable
 
@dataclass
class Violation:
    rule_id: str
    file: str
    line: int
    excerpt: str
 
@dataclass
class Rule:
    id: str            # AGENTS.md 側にも同じ ID を書いて対応を明示する
    description: str
    applies_to: str    # ファイルパスの正規表現
    check: Callable[[str, str], list]
 
def no_console_log(path: str, content: str) -> list:
    out = []
    for i, line in enumerate(content.splitlines(), start=1):
        # コメント行の説明文まで拾わないよう、行頭の呼び出しに限定する
        if re.search(r'(?<!//\s)\bconsole\.(log|debug)\s*\(', line):
            out.append(Violation("R-001", path, i, line.strip()))
    return out
 
def function_length_limit(path: str, content: str, limit: int = 30) -> list:
    out = []
    lines = content.splitlines()
    start, name, depth = None, None, 0
    for i, line in enumerate(lines, start=1):
        m = re.match(r'\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)', line)
        if m and start is None:
            start, name, depth = i, m.group(1), 0
        if start is not None:
            depth += line.count("{") - line.count("}")
            if depth <= 0 and i > start:
                if (i - start) > limit:
                    out.append(Violation("R-004", path, start,
                                         f"{name}: {i - start} 行 (上限 {limit})"))
                start, name = None, None
    return out
 
def no_any_type(path: str, content: str) -> list:
    out = []
    for i, line in enumerate(content.splitlines(), start=1):
        # 意図的な any は `// allow-any:` の理由コメントで明示する運用にしている
        if re.search(r':\s*any\b', line) and "allow-any:" not in line:
            out.append(Violation("R-002", path, i, line.strip()))
    return out
 
RULES = [
    Rule("R-001", "console.log を本番コードに残さない",
         r"^src/(?!.*\.test\.).*\.(ts|tsx)$", no_console_log),
    Rule("R-002", "any 型を使わない",
         r"^src/.*\.(ts|tsx)$", no_any_type),
    Rule("R-004", "関数は 30 行以内",
         r"^src/.*\.(ts|tsx)$", function_length_limit),
    # ... 19 個ぶん
]

書いていて気づいたのは、no_console_log の正規表現に「コメント行を除外する」という但し書きが必要になったことです。

これは些細な実装都合に見えて、実は重要でした。私が AGENTS.md に書いていたルールは、自分で厳密に定義したことが一度もなかったのです。「console.log を残さない」の「残さない」がテストコードを含むのか、コメントアウトされたものを含むのか、決めていませんでした。決めていないルールを、エージェントが私の意図どおりに解釈してくれるはずがありません。

述語に落とす作業は、それ自体がルールの棚卸しになりました。

差分に対して回す

検証関数ができたら、エージェントの作業後に走らせます。git の差分から対象ファイルを拾い、該当するルールだけを当てる形にしました。

# tools/adherence/measure.py
import json
import re
import subprocess
from datetime import datetime, timezone
from rules import RULES
 
def changed_files(base: str = "HEAD~1") -> list:
    out = subprocess.run(
        ["git", "diff", "--name-only", "--diff-filter=d", base, "HEAD"],
        capture_output=True, text=True, check=True,
    )
    return [p for p in out.stdout.splitlines() if p]
 
def read_at_head(path: str) -> str:
    out = subprocess.run(["git", "show", f"HEAD:{path}"],
                         capture_output=True, text=True)
    return out.stdout if out.returncode == 0 else ""
 
def measure(base: str = "HEAD~1") -> dict:
    files = changed_files(base)
    per_rule = {r.id: {"applicable": 0, "violated": 0,
                       "description": r.description} for r in RULES}
    violations = []
 
    for path in files:
        content = read_at_head(path)
        if not content:
            continue
        for rule in RULES:
            if not re.match(rule.applies_to, path):
                continue
            # 「適用対象になった回数」を分母にする。
            # 対象外のファイルまで分母に入れると遵守率が甘く出る。
            per_rule[rule.id]["applicable"] += 1
            found = rule.check(path, content)
            if found:
                per_rule[rule.id]["violated"] += 1
                violations.extend(found)
 
    for rid, s in per_rule.items():
        s["adherence"] = (
            None if s["applicable"] == 0
            else round(1 - s["violated"] / s["applicable"], 3)
        )
 
    scored = [s for s in per_rule.values() if s["adherence"] is not None]
    overall = (round(sum(s["adherence"] for s in scored) / len(scored), 3)
               if scored else None)
 
    return {
        "measured_at": datetime.now(timezone.utc).isoformat(),
        "commit": subprocess.run(["git", "rev-parse", "HEAD"],
                                 capture_output=True, text=True).stdout.strip(),
        "files_changed": len(files),
        "overall_adherence": overall,
        "per_rule": per_rule,
        "violations": [v.__dict__ for v in violations],
    }
 
if __name__ == "__main__":
    result = measure()
    with open("adherence.jsonl", "a") as f:
        f.write(json.dumps(result, ensure_ascii=False) + "\n")
    print(f"overall={result['overall_adherence']} "
          f"violations={len(result['violations'])}")

分母の取り方には少し悩みました。

最初は「全ファイル数」を分母にしていましたが、これだと .md ファイルばかり触った日の遵守率が不当に高く出ます。ルールが適用対象になった回数だけを数える形に変えて、ようやく日ごとの比較に耐える数字になりました。

計測は毎回のコミット後に走らせ、1行の JSON として追記していきます。これで推移が追えます。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
ルールを検証可能な述語へ分解し、エージェントの出力差分に対して遵守率を自動計測するハーネスの実装
ルール数を増やすほど遵守率が下がる関係と、ファイル内の記述位置による落ち方の実測
AGENTS.md に残すルールと、CI ゲートへ移すルールを分ける判断基準
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

Agents & Manager2026-07-10
エージェントが書いたファイルが .gitignore に飲まれる — commit 前に気づくための検出ゲート
エージェントが生成したファイルが .gitignore に一致していると、差分レビューでは正常に見えるのに commit には含まれません。ignored な生成物を push 前に検出するゲートスクリプトと、その運用調整をまとめました。
Agents & Manager2026-06-28
Antigravity の計画を、承認する前に削る
Planning モードが出してきた実行プランを、丸ごと承認するのではなく、危険なステップだけ削ってから渡す。部分編集という運用を、個人開発の現場目線で整理しました。
Agents & Manager2026-06-27
一度きりのプロンプトを、再利用できるサブエージェントに残す
Antigravity 2.0 の dynamic sub-agents で生まれた『うまくいったプロンプト』を、会話履歴に埋もれさせず再利用可能な定義として残す方法を、実際のファイル構成と蒸留手順とともにまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →