エージェント型コーディングツールへのメトリクスのフィード
Claude Code や opencode のようなエージェント型コーディングツールは、保守性フィードバックの急成長中の消費者であり、エディタの中の人間とは異なる形でそのフィードバックを求めます。人間はキー入力をするので、エディタのループは言語サーバーに頼ります。didChange のたびにパースし、複雑度をマージンに描画し、1 文字ごとに更新する、という具合です。エージェントはキー入力をしません。ツール呼び出しで編集全体を書き込み、ターンを明け渡します。その消費者にとって正しいフィードバックは、バッチで、編集後に、構造化されて届くもの — まさに bca check がすでに出力しているものです。
そのため、このレシピは新しいバイナリの表面を一切追加しません。bca check は、エージェントが必要とするすべてをすでに提供しています:
- A machine-parseable offender list (per-violation rows on stdout, or
--report-format sarif | code-climate | clang-warning | msvc-warning | checkstylefor a structured document). - 段階化された終了コード — 違反があれば
2、クリーンなら0、ツールエラーなら1— により、フックは何もパースせずに「この編集はコードを複雑にしすぎたか?」で分岐できます。 - Baseline filtering, in-source suppression markers, and
[check.exclude]globs, so the signal an agent sees is the same ratcheted signal a human sees — provided the project's exclusions live under[check]rather than in a walker deny-set.
欠けていたのは配線です。このページがその配線です。ツールごとにコピー&ペーストできるフィードバックループと、ループが裏目に出ないようにするためのエージェント向けガイダンスを提供します。
キーストローク時ではなく編集後に
このレシピは、提案中の bca lsp サーバー (#384) と意図的に対をなすものです。両者は異なる消費者に仕え、互いに依存しません:
bca lsp (#384) | 本レシピ | |
|---|---|---|
| 消費者 | エディタ内の人間 | ツールループ内のエージェント |
| トリガー | キーストローク(didChange) | ツール呼び出しの完了(編集の反映) |
| 再パース | インクリメンタル | 編集ごとにファイル全体を 1 回 |
| 出力先 | マージンの診断表示 | モデルへフィードバックされるテキスト |
| ステータス | 提案段階 | bca check で今日から動作 |
人が入力しているときは LSP を、エージェントが編集しているときはこちらを選んでください。エージェントを LSP のインクリメンタルな didChange 経路につなぐのは、エージェントが決して使わない機構の代価を払うことになります。
以下のツール別セクションがすべて呼び出すコマンドは、手で実行するのと同じものです:
# 終了コード 2 ⇒ このファイルに違反が少なくとも 1 件あります。しきい値は
# リポジトリルートの bca.toml(自動的に発見されます)から来ます。その場限りの
# 上書きは 1 つ以上の --threshold フラグで行います。
bca check path/to/edited_file.rs --threshold cognitive=10
An agent loop wants tighter limits than a blocking CI gate: a false positive costs one wasted refactor rather than a blocked merge, and the signal arrives while the code is still being written. See the agent feedback profile for the rest of the table, and Choosing thresholds for where the shipped defaults come from.
リポジトリルートに bca.toml があれば(ローカルしきい値ゲート を参照)、--threshold フラグは不要です。素の bca check <file> がコミット済みの限度値・ベースライン・除外設定を読み込むため、エージェントのループは CI とまったく同じ条件でゲートします。
Put the hook's exclusions under [check]
One thing does not carry over from the CI invocation, and it will produce false positives if you skip it. A hook names one file per run, and an explicitly named path overrides every walker exclude — the rg convention that a path you named is a direct request. So a file your project keeps out of scope with -X, --exclude-from, a .bcaignore, or a manifest exclude list is nonetheless analyzed the moment the hook passes it by name, and reported as an offender under an exit 2 that frames it as a problem to address.
Walker excludes shape what gets analyzed. Check excludes shape what gets gated. A per-file hook invocation only respects the second:
# bca.toml — survives an explicit path, so the hook sees what CI sees.
[check]
exclude = ["./utils/**", "./benches/**"]
bca warns on stderr whenever an explicitly named path overrides a walker exclude, naming the glob, so the miswiring is visible rather than silent:
bca: warning: utils/gate.py matches an exclude pattern (./utils/**) but was named explicitly; analyzing anyway
Treat that line as a to-do: the entry it names wants moving to [check] exclude. This repository moved its own dev-tooling globs there for exactly this reason.
One constraint on where the hook runs: while #1164 is open, a [check] exclude glob resolves against the working directory rather than the manifest root when the path is named explicitly. Both hooks below inherit the agent's working directory, which is the project root, so they are unaffected — but a hook that cds into a subdirectory first would see its exemptions stop matching.
Claude Code
仕組み: .claude/settings.json 内の PostToolUse フック。matcher はファイル編集ツールにスコープします。
フィードバックチャネル: これはあらゆるエージェント型ツールの中で最も強力な適合です。PostToolUse フックは編集が反映された瞬間に発火し、テキストを直接モデルへ注入できます — メッセージを stderr に出して終了コード 2 で終了する(Claude は stderr を何が起きたかのコンテキストとして読みます)か、hookSpecificOutput.additionalContext を含む JSON を出力するかのいずれかです。フィードバックは編集境界そのものに届き、言うべきことが生じるまでトークンコストはゼロです。
.claude/settings.json にフックを配線します:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bca-check.sh"
}
]
}
]
}
}
ラッパースクリプトは、フックの stdin に届く JSON から編集されたファイルのパスを読み取り、そのファイルだけに bca check を実行し、ゲートに引っかかったときに限り、違反リストとそれに続くエージェント向けガイダンスブロックを stderr に出力して 2 で終了します:
#!/usr/bin/env bash
# .claude/hooks/bca-check.sh — 編集後に、編集された 1 ファイルをゲートします。
set -euo pipefail
# PostToolUse はツール呼び出しを JSON として stdin に渡します。編集された
# ファイルのパスは、Edit/Write/MultiEdit では .tool_input.file_path です。
file_path="$(jq -r '.tool_input.file_path // empty')"
[ -n "$file_path" ] || exit 0 # チェック対象なし。何も出力しません。
[ -f "$file_path" ] || exit 0 # ファイルが消えています(削除など)。
# Thresholds, baseline, and excludes come from the repo-root bca.toml.
# --no-summary / --no-remediation keep the feedback to the offender
# rows themselves; the guidance below tells the agent what to do. The
# rows are on stdout and any diagnostic on stderr, so `2>&1` captures
# whichever the run produced.
status=0
report="$(bca check "$file_path" --no-summary --no-remediation 2>&1)" || status=$?
# bca check の終了コードは、クリーンなら 0、違反ありなら 2、ツールエラーなら 1。
# 設定や IO のエラーが「複雑度」と誤ラベルされないよう、2 に限定して分岐します。
case "$status" in
0) exit 0 ;; # クリーンな場合 ⇒ 何も言わない。
2) ;; # 違反がある場合 ⇒ 以下で報告する。
*) printf 'bca check could not run (exit %s):\n%s\n' "$status" "$report" >&2
exit 0 ;;
esac
# 終了コード 2 により、Claude は stderr を編集に関する文脈として読みます。
# `set -u` の下では未設定の変数がフックを中断させ、ファイルが無い場合は
# 案内文の代わりに素の `cat` エラーが出るため、CLAUDE_PROJECT_DIR に既定値を
# 与え、案内ファイルの存在を確認します。
guidance="${CLAUDE_PROJECT_DIR:-$PWD}/.claude/hooks/bca-guidance.txt"
cat >&2 <<EOF
bca flagged complexity in the file you just edited:
$report
$([ -f "$guidance" ] && cat "$guidance")
EOF
exit 2
エージェント向けガイダンスブロック を .claude/hooks/bca-guidance.txt に保存し、フックのフィードバックと CLAUDE.md が同一の文言を参照するようにします。(依存関係は jq のみで、ほとんどのエージェントイメージに同梱されており、なくても 1 行でインストールできます。)
exit 2 シグナルを使わずに助言的なコンテキストを注入したい場合は、stderr に書き込む代わりに stdout へ JSON を出力します:
jq -n --arg ctx "$offenders" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}'
exit 0
違反リストをメモとして Claude の視界に入れたい場合は additionalContext を、先へ進む前に対処すべき問題として提示したい場合は exit 2 を使います。どちらも編集はそのまま残ります — PostToolUse はツールの実行 後 に動作するため、いずれも編集を取り消すことはできません。
opencode
仕組み: プラグイン(hooks オブジェクトを返す非同期関数をエクスポートする JavaScript または TypeScript モジュール)で、ツール実行後(ファイル変更ツールの write と edit を含む)に発火する tool.execute.after フックを使用します。
フィードバックチャネル: after フックはthrow することで問題をエージェントに提示します。opencode の公開プラグインページには throw によるシグナルパターンは記載されていますが、after フック用の助言的な戻り値は記載されていないため、本レシピでは throw を既定とします — throw された Error のメッセージは、ツールの失敗としてエージェントに返されます。
引数の形に注意 — before フックとは異なります。 公開されている @opencode-ai/plugin の型定義では、tool.execute.after のシグネチャは (input, output) で、ツール名は input.tool、ツールの引数は input.args にあります(ファイルパスは input.args.filePath)。ここが罠です。ドキュメント唯一の実例は tool.execute.before のもので、そこでは引数が output.args(実行前で可変)にあります。それを after フックにコピーすると output.args は undefined になり、下記のガードが常に作動して、プラグインは一切実行されないまま沈黙します — インストール済みに見える no-op です。after フックでは input.args.filePath を読み取ってください。
このファイルを .opencode/plugins/ に配置します(プロジェクトレベルで自動読み込みされるため、opencode.json へのエントリは不要です。そのキーは npm 公開プラグイン用です)。以下のプラグインはプレーンな JavaScript です。TypeScript プラグインにする場合は import type { Plugin } from "@opencode-ai/plugin" してエクスポートに型注釈を付け、追加の依存関係は .opencode/package.json に宣言します(opencode が Bun でインストールします):
// .opencode/plugins/bca-check.js
const GUIDANCE = `
Responding to bca metric feedback: make the code genuinely simpler,
not the number smaller. Do not extract a meaningless helper or split a
cohesive function to dodge the count — a spurious helper often raises
file-level nom/nargs and helps nothing. If the complexity is essential
and the function is clearest left whole, add a suppression marker with
a one-line reason instead of contorting the code. Keep the fix in the
function that was flagged rather than widening it into a module
rewrite.
`.trim()
export const BcaCheck = async ({ $ }) => {
return {
// 注意: after フックの引数は `input.args` にあり、`output.args` ではない
// (output はツールの結果 title/output/metadata を保持する)。
"tool.execute.after": async (input, _output) => {
// ファイル書き込みツールにのみ反応する。(パッチ形式の編集ツールは
// 単一の filePath を持たないため、意図的に対象外とする。)
if (input.tool !== "write" && input.tool !== "edit") return
const filePath = input.args?.filePath
if (!filePath) return
// `bca check` はクリーンなら 0、違反があれば 2、ツールエラーなら 1 で終了する。
// Bun の $ はデフォルトで非ゼロ終了時に throw するため、正確なコードで
// 分岐できるようにキャプチャする。
const res = await $`bca check ${filePath} --no-summary --no-remediation`
.quiet()
.nothrow()
// 0 はクリーン、1 はツールエラーで、複雑度の問題ではない。`=== 2` ではなく
// `< 2` を使うことで、段階的終了コード(`--exit-codes=tiered` /
// `exit_codes = "tiered"` による 3-5)も報告される。
if (res.exitCode < 2) return
// Surface the offenders to the agent by throwing. The rows are on
// stdout; stderr is the fallback for a run whose only output is a
// diagnostic.
const offenders = res.stdout.toString().trim() || res.stderr.toString().trim()
throw new Error(`bca flagged complexity in ${filePath}:\n\n${offenders}\n\n${GUIDANCE}`)
},
}
}
GUIDANCE 文字列は、下記のそのまま使用するブロックと同期を保ってください(または共有ファイルから読み込みます)。チャネルが throw されたエラーであるため、opencode はこれを編集後ステップの失敗として報告します — これは意図どおりの「続行する前に対処せよ」という位置付けです。編集自体は反映されます。tool.execute.after は write / edit ツールがファイルを書き込み終えた後に実行されるため、throw は変更を取り消すことなく次のステップを方向付けます。
プラグイン追加後は opencode を再起動してください。 プラグインは起動時に一度だけ読み込まれ、ホットリロードされません。配置したばかりの .opencode/plugins/bca-check.js は実行中のセッションでは何もしません。opencode を終了して再起動し、しきい値を超えているとわかっているファイルを編集して、edit/write ツールが bca の失敗を報告することを確認してください。インストール済みなのに動かないプラグインは最もよくある症状で、古いセッションが最もよくある原因です。
堅牢化されたリファレンスとして、このリポジトリは独自のコピーを .opencode/plugins/bca-check.js に同梱しています。最小例では省かれている 3 つのガードが追加されており、いずれも実プロジェクトに移植する価値があります:
- リポジトリスコープガード。 パスを解決してプロジェクトルート外のものはスキップし、エージェントがディスク上の別の場所で編集したファイルに対してフックが
bcaを実行しないようにします。 - ローカルビルドの解決。
$BCA、次にチェックアウト内のtarget/release/bca、最後にPATH上のbcaの順で優先します。bcaを自前でビルドするプロジェクトは、グローバルにインストールされている何かではなく、自身のアナライザーでゲートできます。 - 共有ガイダンス。 このプラグインと Claude Code フックの両方が参照する 1 つのファイルからガイダンステキストを読み込み、両者が乖離しないようにします。
エージェント向けガイダンス(このまま使用する)
フィードバックチャネルはレシピの半分にすぎません。素の「cognitive 26 > 25」は、確実にメトリクスゲーミングの動きを誘発します — エージェントは関数ごとの数値を削るために意味的に空のヘルパーを抽出し、関数単位の複雑度を 下げ ながらファイル単位の nom/nargs を 上げ、コードを悪化させます。緩和策は、違反が何を意味し、どう対処すべきかをエージェントに伝えることです。このブロックをエージェントルールファイル(CLAUDE.md、opencode の AGENTS.md)とフックのフィードバックテキストの両方に貼り付け、恒常的なポリシーとしても違反の瞬間にも指示が存在するようにしてください:
**Responding to `bca` metric feedback.** A threshold violation
(cognitive, cyclomatic, ABC, …) means *this function is hard for a
human to follow*. The number is a proxy for that, not the goal. Your
job is to make the code genuinely simpler — not to make the number go
down.
- **Do not game the metric.** Do not extract a helper that exists only
to move complexity off one function, split a cohesive function at an
arbitrary line, collapse readable branches into a dense expression,
or inline/obfuscate logic to dodge the count. These lower the
per-function score while making the code worse — and a spurious
helper often *raises* file-level `nom`/`nargs`, so you have not even
helped the file.
- **Refactor only when it truly clarifies.** A good split has a name
that means something and a boundary a reader would have drawn anyway.
If you cannot name the extracted piece without inventing a
`foo_part2`, the split is gaming — stop.
- **When the complexity is essential, suppress with a reason.** Some
functions are irreducibly complex *and clearest left whole* — a
dispatch `match`, a hand-rolled parser table, an exhaustive state
machine. For these, do not contort the code: add a suppression marker
with the rationale on the same line —
`// bca: suppress(cognitive) — exhaustive opcode dispatch` — and move
on. A clear function with an honest marker is better than a
"compliant" tangle.
- **Keep the fix where the violation is.** The flag is scoped to the
function you just edited. Fix it there, mention anything larger you
noticed, and do not widen the change into a module rewrite to bring
the number down.
誠実な抑制(正確な構文)
上記のガイダンスは、抑制が正当な手段であることをエージェントに伝えます。それが機能するには、エージェントがマーカーを正しく綴る必要があります — マーカー構文は静かな no-op の頻出原因です。正確に教えてください(完全なリファレンス: 抑制マーカー):
- Per function — place the marker in a comment inside the function body, naming the metric(s):
// bca: suppress(cyclomatic, abc) — hand-rolled parser table. A bare// bca: suppress(no list) silences every metric for that function. - ファイル全体 —
// bca: suppress-file(halstead, nargs, nexits)をファイル内の任意の場所に置きます。リストのない// bca: suppress-file形式は、ファイル全体ですべてのメトリクスを抑制します。 - Put the rationale on the marker line, after a metric list. Anything after the list is free text and does not need a separator, so the reason lives where the next reader of the flagged function will see it. (A bare verb takes no trailing text at all — nothing there distinguishes a rationale from prose about the marker, so naming the metrics is what buys you a reason.) Write it every time: a reviewer — human or agent — needs to tell an honest exemption from a dodge, and
bca exemptionslists every marker in the tree for exactly that audit. - Use canonical metric names. The accepted identifiers are
abc,cognitive,cyclomatic,halstead,loc,mi,nargs,nexits,nom,npa,npm,wmc. It isnexits, notexit(the legacyexitalias was retired). An unknown identifier warns and is skipped; the recognized names beside it still suppress, sosuppress(cognitive, exit)silencescognitiveand complains aboutexit.tokensis deliberately not suppressible; treat it as a hard resource cap.
編集ごとではなくタスク境界でゲートする
上記の編集ごとのフックは早期警告のための利便機能であり、ゲートではありません。ゲートは、人間がタスク完了を宣言する前に実行するのと同じチェック、すなわち 2 段構えの make self-scan / pre-commit パターン(ハード層は CI をミラーし、ソフト層は上限の 95% のヘッドルーム帯)です。エージェントには「完了と言う前」のステップとしてこれを指し示してください。
タスク境界より細かい粒度は、エージェントにとって価値が低いか、むしろ逆効果です。複雑度のしきい値は正しさのゲートではなくプロキシなので(下記の注意点を参照)、リファクタリング途中の あらゆる 微小編集の後に再実行しても、数編集後には自然に解消される一時的な違反が生まれるだけです — エージェントはそのノイズの「修正」に 1 ターンを浪費します。エージェントに一貫した変更を仕上げさせ、それから一度だけゲートしてください。
ルールファイルに書かないほうがよい指示が 1 つあります。フックの上に重ねて「修正を検証し、メトリクスを再チェックする」という常設の手順を置くことです。現在のモデルは、指示されなくても自分の編集を読み直し、再チェックします。明示的な指示はその挙動と重なり、入力が変わっていないゲートの再実行にループがターンを費やすことになります。編集が着地すれば、フックは自ら発火します。それを合図としてください。
注意点
本レシピが依存する 3 つの注意点です。無視すると、このループは益より害をもたらします。
- グッドハートの法則 / メトリクスゲーミング。 「この数値を小さくせよ」は「このコードを簡潔にせよ」と同じ指示ではなく、前者を告げられた LLM は最も安上がりな方法でそれを満たします — 通常は複雑さを取り除くのではなく、関数境界の向こうへ動かすだけです。エージェント向けガイダンスブロック と 誠実な抑制のセクション がその緩和策であり、これらは任意の飾りではなく構造上不可欠です。フックと一緒に出荷しなければ、ゲーミングの動きを覚悟してください。
- しきい値はプロキシであり、正しさのゲートではありません。 コンパイル失敗や赤いテストには曖昧さがなく、それらに対するタイトなエージェントループは収束します。複雑度のしきい値はもっと柔らかいものです。しきい値超過は「これがまだ読みやすいか人間が確認すべき」という意味であり、判断の問題であって欠陥ではありません。それに応じて期待値を設定してください — 複雑度フィードバックは、何としてもゼロに追い込むべき合否判定ではなく、エージェントが重み付けして考慮する助言として配線します。
- 複雑度フィードバックはスコープクリープを招きます。 「この関数は追いにくい」という指摘は、エージェントには再構成への誘いとして読まれます。そしてその再構成は、フックが名指しした関数で確実に止まるとは限りません — 2 行の修正が、誰もレビューを頼んでいないモジュール書き換えになります。ルールファイルで範囲を区切ってください。違反は、それを引き起こした編集に限定されます。指摘された場所で直し、それより大きな気づきがあれば言及するにとどめ、数字を追って変更を広げないでください。