pfmls-stylepack(それを紹介する記事では「Stylepack」と略されている)は、コーディングエージェントにチームの規約を守らせるために Toss Bank のあるチームが作ったプラグインである。AI にチームのルールを守らせる方法 では、この記事が詳しく論じている問題への答えとして提示されている。その問題とは、プロジェクトルートの指示ファイルに書かれた規約はセッションの開始時には読まれるものの、セッションが長くなるにつれて適用されなくなるというもので、記事はこの失敗を Lost in the Middle に帰している。

このプラグインは、そのファイルを改善するのではなく、規約をエージェント自身のループの中に置き、エージェントフック を使って、エージェントが実際にコードを書いている瞬間に規約を届ける。この設計は、モデルがコードを書いた後にリンターを実行してそのエラーをフィードバックするという以前からあるパターンを明示的に借用したものである。流れは同じで、チェックされる内容が「構文的に誤っている」から「これは私たちのチームのやり方ではない」に変わっている。

記事はこのプラグインを一般公開されているものとしては説明していない。ここに記録されている内容はすべて、同記事による説明である。

機能

1 つのルールは 1 つのファイルであり、そのファイルはルールが発火する条件と、エージェントが受け取るテキストそのものの両方を保持する。トリガーはファイル名パターン、コードに照合される 1 つ以上の正規表現、そして除外パターンで表現され、注入されるテキストはそれらと同じファイル内の別のキーの下に置かれる。

ルールの選択では、意図的にモデルを呼び出さない。フックはファイルが書き込まれるたびに実行されるため即座に返る必要があり、絞り込みはファイル名パターンと正規表現が担う。記事によれば、関連性の判定を AI に求めていた以前のバージョンでは、リクエストごとに約 10 秒が加わっていた。また記事は、ルールを受け取るエージェントがそれを適用すべきかどうかを最終的に判断するので、絞り込みは正確である必要はないと論じている。

注入ポイントは 2 つあり、両者は程度ではなく性質が異なるものとして説明されている。

  • ファイルが書き込まれた直後 — フックは書き込まれたばかりのファイルの本文だけを読み、高速に動作し、最大 2 つのルールを注入する。同じルールが注入されるのは 1 セッションにつき 1 回だけである。その目的は、コードが書かれたその場で修正させることにある。具体例として挙げられているのは、StrEnum に新しいメンバーが追加された一方で、ファイル内の別の場所にある match 文がそのメンバーのカバーを黙って失うケースである。注入されたルールは、欠けている分岐と、次の漏れを型チェッカーが捕捉できるようにするための case _: assert_never(...) のフォールバックを追加するようエージェントに促す。
  • エージェントが作業を終える直前 — フックは変更全体を git diff として受け取り、より時間をかけて動作し、最大 4 つのルールを注入する。これが存在するのは、単一ファイル向けのフックでは複数のファイルにまたがる欠陥を見つけられないからである。記事の例は、サービスがリポジトリのインターフェースではなく実装に依存しているケースで、2 つのファイルはそれぞれ単独で読めば正しい。ここで注入されるテキストは、自らをハードな失敗ではなくレビューのためのリマインダーだと述べている。

これとは別に、プラグインはセッション開始時に、リポジトリの言語環境に応じて選ばれたチーム全体のコンテキストを注入する。pyproject.toml があれば Python のガイダンスが、build.gradle があれば Spring のガイダンスが取り込まれる。その根拠は、違う言語向けのルールは注意をそらすものでしかないというものである。

採用とエコシステム

配布はコピーではなく一元的に行われる。記事は規約をテンプレートリポジトリに置くやり方を退けている。テンプレートはコピーされた瞬間から古くなり始め、オリジナルへの改善はすでにそこから複製されたリポジトリには決して届かないからである。代わりにルールは 1 つの中央リポジトリに置かれ、プラグインはセッションが開始されるたびにバックグラウンドで最新版を取得する。そのため、誰も何もしなくても最新のルールが適用される。

チームはまた、どのルールがいつ、どの条件で発火したかをログに記録し、プラグインそのものを計測している。そのログから 2 つの修正が報告されている。1 つ目のトリガーは、狙いとしていた辞書リテラルだけでなく Python の f 文字列の書式指定子にもマッチしてしまい、21 のセッションにわたって発火しながら 1 つのコード変更も生まなかった。これはコロンの後に空白文字を必須とすることで修正された。記事の結論は、誤った提案は提案がないよりも悪い、なぜならそれはエージェントにルールを無視することを教えてしまうから、というものである。もう 1 つのトリガーは狭すぎた。for と .forEach にしかマッチしなかったが、チームが実際にレビューで見つけた N+1 クエリは .map やリスト内包表記の形で現れていた。この 2 つ目のケースから、記事は線引きを導いている。コードの形から認識できる欠陥はトリガー付きのルールに、実行時にしか現れない欠陥は常に読み込まれるコンテキストに属する、というものである。

新しいルールは、リファクタリング中にエンジニアが追加するものと、レビューコメントを集めてプルリクエストをまたいで繰り返されているフィードバックを見つけ出す別の受付経路から生まれるものの両方がある。記事は、1 つのリポジトリの 2 つのプルリクエストで同じコメントが繰り返された後にこの方法で作られたルールとして、@dataclass として宣言されたステートフルなクラスに関するルールを 1 つ挙げている。