Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

エージェント型コーディングツールへのメトリクスのフィード

このレシピでは、ファイル編集ツールの実行後に、保守性に関する指摘を Claude Code や OpenCode に渡す方法を説明します。プロジェクトで bca check を実行でき、コーディングエージェントの設定を変更できることを前提としています。指摘はエージェントへの助言です。変更が完了したかどうかは、引き続きプロジェクトの最終検証ゲートで判断します。

The hooks use the existing bca check command, which provides:

  • 機械的にパース可能な違反リスト(stdout への違反ごとの行、または構造化ドキュメントとしての --report-format sarif | code-climate | clang-warning | msvc-warning | checkstyle)。
  • Exit codes that distinguish current findings (2, or 2–5 in tiered mode), a clean check (0), and tool errors (1). The findings cover the whole file, not only the latest edit.
  • Filtering by the project’s policy: baselines, in-source suppression markers, and [check] exclude globs. Put exclusions that must apply to explicitly named files under [check].

キーストローク時ではなく編集後に

エディタとの連携では、Language Server Protocol を使って入力中に診断を更新できます。エージェントのフックは、ファイルの編集が完了してからチェックします。提案中の bca lsp サーバー (#384) と本レシピのフックは、それぞれ次の用途を対象としています。

bca lsp (#384)本レシピ
利用者エディタを使う人ツールループ内のエージェント
トリガーキーストローク(didChange)ツール呼び出しの完了(編集の反映)
再パースインクリメンタル編集ごとにファイル全体を 1 回
出力先マージンの診断表示モデルへフィードバックされるテキスト
ステータス提案段階bca check で今日から動作

入力中のフィードバックにはエディタ連携を、エージェントの編集ループ内でのフィードバックにはツール実行後のフックを使ってください。

以下のツール別セクションがすべて呼び出すコマンドは、手で実行するのと同じものです:

# 終了コード 2 ⇒ このファイルに違反が少なくとも 1 件あります。しきい値は
# リポジトリルートの bca.toml(自動的に発見されます)から来ます。その場限りの
# 上書きは 1 つ以上の --threshold フラグで行います。
bca check path/to/edited_file.rs --threshold cognitive=10

エージェントフィードバック用プロファイル は、合否を決める CI ゲートより厳しいしきい値を提案しています。採用する前に、増える指摘を評価してください。不要なリファクタリングは、作業範囲の拡大やバグの混入につながることがあります。デフォルト値とほかのプロファイルは、しきい値の選択で説明しています。

bca.toml にしきい値を設定すると(ローカルしきい値ゲートを参照)、bca check <file> はコミット済みのしきい値、ベースライン、除外設定を読み込みます。CI と同じ基準で判定する場合は、設定と判定の層(tier)を CI とそろえてください。

フックの除外は [check] の下に置く

One thing does not carry over from the CI invocation, and skipping it produces false positives. A hook names one file per run, and an explicitly named path overrides every walker exclude, following 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 still 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.

ウォーカーの除外は何が解析されるかを形作ります。check の除外は何がゲートされるかを形作ります。ファイル単位のフック呼び出しが尊重するのは後者だけです。

# 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:

warning: utils/gate.py matches an exclude pattern (./utils/**) but was named explicitly; analyzing anyway

Treat that line as a to-do: move the entry it names to [check] exclude. This repository moved its own dev-tooling globs there for this reason.

The hook uses the nearest bca.toml above the session’s working directory, stopping at the checkout boundary. The reference hooks also recognize targets inside another nested checkout and apply that checkout’s policy.

Claude Code

Claude Code の PostToolUse フックを使うと、ファイル編集の成功後に、現在のしきい値違反をモデルへ渡せます。リファレンス実装は stdout に hookSpecificOutput.additionalContext を返し、終了コード 0 で終了します。ツールの元の実行結果は保持されます。

Prerequisites:

  • Claude Code 2.1.139 or later; that release added the hook args field used below (see the official changelog).
  • Node.js LTS and bca.
  • A nonempty [thresholds] table in bca.toml, from bca init or written by hand. Run bca check path/to/a/source-file directly to confirm the configuration loads.

Then copy these files into the same paths in your project:

  • .claude/hooks/bca-check.mjs: フックのイベントを読み取り、助言を含む JSON を返します。
  • .claude/hooks/bca-common.mjs: アナライザーを実行し、出力量を制限します。OpenCode プラグインもこのヘルパーを使います。
  • .claude/hooks/bca-guidance.txt: 実行時に渡す短い注意事項です。末尾の make コマンドは、自分のプロジェクトの検証コマンドに置き換えてください。

既存の設定を保持したまま、次の設定を .claude/settings.json に追加してください。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/bca-check.mjs"],
            "timeout": 10
          }
        ]
      }
    ]
  }
}

The exec form passes the script path as one argument, including spaces and shell metacharacters. The hook requires Node on PATH; its JSON handling uses no npm packages or jq. For an existing bca-check.sh registration, replace the old script with the current wrapper and copy the .mjs files alongside it, or replace that registration with the Node configuration above. Keep only one registration to avoid duplicate checks.

How the hook finds its configuration and analyzer:

  • The event’s cwd selects the nearest manifest within the current checkout. For a target in a different nested checkout, the hook discovers configuration from that target’s directory.
  • CLAUDE_PROJECT_DIR locates the script, but stays at the original project when Claude enters a worktree. Using it to select the analyzer configuration would check the wrong tree.
  • Select $BCA first, then the current checkout’s executable target/release/bca (bca.exe on Windows), then bca on PATH. An explicit but unavailable $BCA stays unavailable rather than falling back to another binary.
  • Within one checkout, the session’s manifest also applies when it edits a sibling package. Change the session directory to select that package’s own manifest.
  • Nested checkouts, including Git submodules, are separate configuration boundaries. If a nested checkout has no thresholds, the check is unavailable; it does not inherit the parent’s policy.

アナライザーの結果は次のように扱います。フック自体の終了コードは 0 です。

  • 終了コード 0: 指摘がないファイルや除外されたファイルについては、モデルにコンテキストを追加しません。
  • 終了コード 2–5: stdout の指摘を助言としてモデルのコンテキストに追加します。4,000 文字に制限し、省略した場合はその旨を示します。
  • アナライザーの失敗: バイナリがない場合、終了コード 1、異常終了、タイムアウト、出力上限の超過では、モデルにコンテキストを追加しません。各子プロセスの実行時間は 3 秒、出力はストリームごとに 64 KiB が上限です。タイムアウト、キャンセル、出力上限の超過が起きると、POSIX では起動したプロセスグループを、Windows ではプロセスツリーを終了させます。
  • 解析中のファイル変更: 解析中にファイルの識別情報、サイズ、タイムスタンプが変わった場合は、指摘を破棄します。

このアダプターは重複排除の状態を永続化しないため、変化のない違反も次の編集で再び報告されることがあります。チェック対象はファイル全体です。[new] と [regr] は直前の編集ではなく、設定したベースラインとの比較を示します。基本方針に従い、対応をタスクの範囲内に収めてください。この matcher の対象は Edit、Write、MultiEdit です。シェルコマンドや外部エディタには別の連携が必要です。

Verify model delivery after installation. Using Claude’s Write tool, create a disposable supported source file that breaches a configured limit, and ask Claude to quote the finding it receives. Then simplify the file through Edit and confirm the clean result adds no advisory. In debug output, [bca-check] JSON records distinguish findings, clean/excluded files, out-of-scope paths, superseded results, and unavailable checks. These records go to stderr with exit 0, so Claude does not receive them as additional context. They record the checkout, analyzer, outcome, and up to 1,000 characters of analyzer stderr for troubleshooting. Offender rows remain in the advisory; CI annotations and summary-file side effects are disabled. The reference suite runs with make plugin-test, including the shared OpenCode helper regressions.

OpenCode V1(旧バージョン)

この節の例は V1 のプラグイン API を使います。本リポジトリの現在の V2 プラグインは V2 フック APIを使い、.claude/hooks/bca-common.mjs も必要です。V1 プラグインを移植する際は、移行ガイドを参照してください。

Mechanism: implement a plugin: a JavaScript or TypeScript module that exports an async function returning a hooks object. Register tool.execute.after, which runs after a tool completes, including the write and edit tools.

Feedback channel: the after-hook surfaces a problem to the agent by throwing. opencode’s public plugins page documents the throw-to-signal pattern but not an advisory return value for the after-hook, so this recipe defaults to throwing. A thrown Error carries its message back to the agent as the tool’s failure.

Mind the argument shape; it differs from the before hook. The published @opencode-ai/plugin types give tool.execute.after the signature (input, output) where the tool name is input.tool and the tool arguments are on input.args (the file path is input.args.filePath). This is the trap: the docs’ only worked example is for tool.execute.before, where args live on output.args (mutable, pre-execution). Copy that into an after hook and output.args is undefined, the guard below always trips, and the plugin silently never runs: a no-op that looks installed. Read input.args.filePath in the after hook.

Drop this file in .opencode/plugins/. It is project-level and auto-loaded; no opencode.json entry is needed, since that key is for npm-published plugins. The plugin below is plain JavaScript. For a TypeScript plugin, import type { Plugin } from "@opencode-ai/plugin" and annotate the export, and declare any extra deps in .opencode/package.json (opencode installs it with 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

      // throw して違反をエージェントに提示する。行は stdout にあり、
      // stderr は唯一の出力が診断メッセージである実行のための
      // フォールバック。
      const offenders = res.stdout.toString().trim() || res.stderr.toString().trim()
      throw new Error(`bca flagged complexity in ${filePath}:\n\n${offenders}\n\n${GUIDANCE}`)
    },
  }
}

Keep the GUIDANCE string in sync with the verbatim block below (or read it from a shared file). Because the channel is a thrown error, opencode reports it as a failed post-edit step, which is the intended “address this before continuing” framing. The edit itself still lands: tool.execute.after runs once the write or edit tool has already written the file, so the throw frames the next step without undoing the change.

Restart opencode after adding the plugin. Plugins are loaded once at startup and are not hot-reloaded. A freshly dropped .opencode/plugins/bca-check.js does nothing in the running session. Quit and relaunch opencode, then confirm by editing a file you know is over threshold and checking that the edit/write tool reports the bca failure. An installed-but-inert plugin is the most likely symptom, and a stale session is the most likely cause.

The repository ships a V2 reference at .opencode/plugins/bca-check.js. It uses a different plugin API and adds guards worth porting to a V1 setup:

  • リポジトリスコープガード。 パスを解決してプロジェクトルート外のものはスキップし、エージェントがディスク上の別の場所で編集したファイルに対してフックが bca を実行しないようにします。
  • ローカルビルドの解決。 $BCA、次にチェックアウト内の target/release/bca、最後に PATH 上の bca の順で優先します。bca を自前でビルドするプロジェクトは、グローバルにインストールされている何かではなく、自身のアナライザーでゲートできます。
  • 共有ガイダンス。 このプラグインと Claude Code フックの両方が参照する 1 つのファイルからガイダンステキストを読み込み、両者が乖離しないようにします。

エージェント向けガイダンス(このまま使用する)

数値だけのフィードバックは、役割のないヘルパーの抽出など、読みやすさを改善せずにスコアだけを下げる変更を促すことがあります。指摘への対応方針をエージェントに伝えてください。次のブロックを基本方針としてエージェントのルールファイル(CLAUDE.md または AGENTS.md)に貼り付けます。実行時のフィードバックには短い bca-guidance.txt を使ってください。リファレンス実装では、このテキストの長さを指摘とは別に制限しています。

以下の方針文は、翻訳版でも英語のまま掲載しています。そのままコピーして使ってください。

**Responding to `bca` metric feedback.** A threshold violation is a
reason to review readability, not proof of a defect. Improve the code
where that helps the current task; lowering a score is not itself a goal.

- **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 without necessarily improving readability.
  New helpers also add names and interfaces that readers must understand.
- **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`, reconsider the split.
- **When the complexity is essential, suppress with a reason.** Some
  functions are clearest left whole, such as 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`.
  A clear function with an honest marker is better than a
  "compliant" tangle.
- **Keep the fix within the task.** Review findings relevant to the
  current change. Findings may concern other functions or the whole
  file; mention unrelated findings without expanding the edit.

誠実な抑制(正確な構文)

抑制には、抑制マーカーで説明する正確な構文が必要です。

  • 関数単位: 関数本体内のコメントに、対象メトリクスを指定したマーカーを置きます。例: // bca: suppress(cyclomatic, abc) -- hand-rolled parser table。一覧を省略した // bca: suppress は、その関数の抑制可能なすべてのメトリクスを抑制します。tokens の制限は引き続き適用されます。
  • ファイル全体: ファイル内の任意の場所に // bca: suppress-file(halstead, nargs, nexits) を置きます。一覧を省略した // bca: suppress-file は、ファイル全体で抑制可能なすべてのメトリクスを抑制します。tokens は抑制しません。
  • 理由は、マーカーと同じ行のメトリクス一覧の後に書いてください。 一覧の後には自由に理由を書けます。区切り文字は不要です。メトリクス一覧を省略した形式では、マーカーに関する説明文と区別できないため、末尾に理由を書けません。適用除外の妥当性をレビュアーが判断できるよう、理由を添えてください。bca exemptions で、その確認に使うマーカー一覧を表示できます。
  • 正規のメトリクス名を使ってください。 受け付けられる識別子は abc、cognitive、cyclomatic、halstead、loc、mi、nargs、nexits、nom、npa、npm、wmc です。exit ではなく nexits です(レガシーの exit エイリアスは廃止されました)。未知の識別子は警告が出た上でスキップされ、併記された認識済みの名前は引き続き抑制されます。そのため suppress(cognitive, exit) は cognitive を抑制しつつ exit について警告します。tokens は意図的に抑制「不可」です。ハードなリソース上限として扱ってください。

編集ごとではなくタスク境界でゲートする

編集ごとのフックは、早い段階で助言を提供します。ひとまとまりの変更を終えてから、テストやリンターを含むプロジェクトの検証ゲートを実行してください。make self-scan / pre-commit パターンでは、ハードゲートに CI と同じしきい値を使います。値が大きいほど悪いメトリクスでは、デフォルトのソフトしきい値をハード上限の 95% に設定し、上限に近づいた段階で警告します。

編集の途中では、リファクタリングの完了時に解消される一時的な指摘が出ることがあります。それらに対応する前に、エージェントにひとまとまりの編集を終えさせてください。メトリクスは読みやすさを判断する目安であり、コードの正しさを証明するものではありません。

フックがすでにチェックしている場合は、編集のたびに追加のメトリクスチェックを要求しないでください。最終検証を実行する義務は明示し、入力が変わった場合や前回の結果を利用できない場合にチェックを再実行します。

注意点

  • 数値だけを改善する変更: 代理指標の最適化が、本来の目標を損なうことがあります。グッドハートの法則が説明するリスクです。数値を下げるよう求めると、読みやすさを改善せずに複雑さを別の関数へ移す変更を促すことがあります。エージェント向けの指針を含め、根拠のある抑制を説明してください。
  • しきい値は目安であり、正しさを判定するものではありません: しきい値を超えた場合は、読みやすさを確認してください。それだけでコードに欠陥があるとは言えません。指摘はエージェントが判断材料として使う助言として渡し、変更に適したテストやほかのチェックで動作を検証してください。
  • 作業範囲の拡大: 指摘への対応が、タスクに必要な範囲を超えるリファクタリングにつながることがあります。修正は現在のタスクの範囲に収めてください。指摘には、無関係な関数やファイル全体に関するものも含まれます。無関係な指摘は報告にとどめ、修正範囲を広げないでください。