skill-code-review について
15 状態の FSM で git diff のコードレビューを進行し、専門レビュアーを調整して GO、CONDITIONAL、NO-GO の判定を出すスキルです。決定的な重複排除、リリース準備のゲート、永続レポートを要するレビュー向けです。
主な機能
- 0115 状態 FSM によるレビュー進行
- 02専門レビュアーのバッチ分配
- 03決定的な指摘の重複排除
- 048 項目のリリース準備評価
- 05Markdown、JSON、マニフェスト形式のレポート
- 06状態ごとの検証イベント追跡
活用シーン
- 01git のリビジョン間の変更レビュー
- 02専門レビューの並列実行の調整
- 03明示的なゲートによるリリース準備の評価
- 04保存されたレポートとイベントによる実行監査
MCP 対応
このスキルは MCP サーバーをクライアントに登録する手順を含み、`fsm.*` MCP ツール群でレビューを進行します。FSM ツールで実行を開始し、ブリーフを取得してワーカーの出力を登録します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ctxr-dev/ skill-code-review --skill skill-code-reviewインストール後、Claude Code で/skill-code-reviewを入力して読み込まれたか確認します。
詳細
skill-code-review
Git diff のコードレビューを実施し、GO/
CONDITIONAL/NO-GO の判定を下す 15 状態の FSM です。重複排除は決定的に行い、リリース準備状況を 8 つのゲートで総合評価します。また、永続的なディスク上の実行ディレクトリ
(report.md + report.json + manifest.json)を使用します。
ブートストラップ(最初に実行してください)
レビュー作業を始める前に、
@.ctxr-fsm/memory/bootstrap.md に従って、
ctxr-fsm がインストールされていること、プロジェクトが初期化されていること、このクライアントに MCP
サーバーが登録されていること、そしてスーパーバイザーが実行中であることを確認してください。
ブートストラップは冪等で、スーパーバイザーと MCP サーバーがすでに起動している場合は、ウォームパスで高速に完了するよう設計されています(約 1.5〜1.8 秒)。
パッケージが見つからない場合、インストールコマンドを実行する前にユーザーに確認してください。 チャットに、提案するコマンドをそのまま表示してください(ブートストラップの表にある正確な
uv add 'ctxr-fsm[all]' / pipx install 'ctxr-fsm[all]' の行です)。続行するには、明示的な承認を得る必要があります。
自動インストールは行わないでください。複数のインストール方法を連続して試さないでください。これは、スキルの起動時に適用される原則 1(要件の事前確認と、満たすための依頼)の「パッケージが見つからない場合」の分岐です。
次に、プロジェクトごとに 1 回、このスキルの仕様とインラインハンドラーを登録します。
uv run python -m code_review.install
インストーラーは、結果を要約した小さな JSON エンベロープを出力します。
{
"db_path": ".ctxr-fsm/fsm.db",
"handlers_registered": 9,
"spec_created": true,
"spec_id": "code-reviewer",
"spec_version": 1
}
エンベロープ内のパスはプロジェクトルートからの相対パスです。そのため、成果物は git にプッシュしたり、マシン間で移動したりしても保持されます。
仕様本文に変更がない場合、インストーラーを再実行しても何も行われません(spec_created は false になり、同じバージョンが再利用されます)。新しい Python プロセスで反映されるように、インラインハンドラーは常に再登録されます。
レビューを実行する
ブートストラップが完了し、仕様が登録されたら、fsm.* MCP ツールファミリーを通じて実行を進めます。
-
実行を開始します。
fsm.start_run(spec_id="code-reviewer", args={"base": "<sha>", "head": "<sha>"})を呼び出します。必要に応じて、argsの下に任意のフィールドを追加します。追加できるのは、full(bool)、scope-dir、scope-lang、scope-framework、scope-reviewer、scope-severity、scope-gate、max-reviewers、format(markdown|json|auto)です。返されたrun_idを記録します。 -
ループします。
fsm.get_brief(run_id)を繰り返し呼び出し、返された brief に応じて処理します。-
ターミナル brief。 実行の最後の状態から
verdictとrun_dir_pathを読み取ります。<run_dir_path>/report.mdの内容をそのまま出力します。これで完了です。 -
ワーカー brief(
brief.has_worker == true)。prompt = brief.worker.prompt_template、inputs = brief.inputsとしてサブエージェントに処理を割り当てます。サブエージェントの構造化 JSON レスポンスがワーカーの出力です。fsm.commit_outputs(run_id, outputs=<sub-agent-response>, signature=<cosignature>)を呼び出します。ループを続けます。 -
ループ brief(
brief.has_loop == true)。ワーカー brief と同じ形式で処理を割り当てます。brief.iteration_nに現在の反復インデックスが格納されています。反復の出力をコミットすると、エンジンが次に進むか、もう一度反復するかを判断します。すべての反復を完了すること――ループを途中で終了しないでください。 エンジンがループ brief を返した場合、コミットのたびに
fsm.get_brief(run_id)を呼び出し、別の状態の brief が返されるまで続ける必要があります。ループを終了するかどうかはエンジン自身が判断します(ワーカーのloop_doneフラグとプランナーのtotal_batchesに基づきます)。1 回のコミットが 1 回の反復にあたります。バッチが N 個ある場合は、N 回の反復を完了しなければなりません。最初のループ brief では、
iteration_n = 1です。反復 1 の出力をコミットすると、エンジンは同じ状態に戻り、iteration_n = 2と新しい brief を返します。fsm.get_briefがstate_idがdispatch_specialistsではない brief を返すまで、ループを続けてください。オーケストレーターは独自のカウンターに基づいてディスパッチループを終了してはなりません。プランナーのバッチがすべて処理されたかどうかを把握しているのはエンジンだけです。このバグの症状:オーケストレーターはディスパッチしたサブエージェントがすべて成功したと判断しているのに、最終的な
report.mdでは、スペシャリストがstatus: failとなり、skip_reason: "no per-leaf output written"または"no output written for shard <n>"と表示されます。原因は、オーケストレーターが反復 1 の出力をコミットした後、次の brief をポーリングせずに(たとえばmerge_specialist_outputsを直接呼び出すなどして)dispatch_specialistsを通過してしまったことです。するとマージ処理はloop_itersに最初のバッチのユニットしか見つけられず、ディスパッチされなかったすべてのユニットについて失敗行を合成します。その結果、実行は NO-GO となり、ほとんどのレビュー担当者が気付かないうちに失敗扱いになります。現在、このような状況ではランナーがDispatchLoopExitedEarlyErrorをハードエラーとして発生させます(劣化したレポートを黙って出力するのではなく)。これにより、失敗は事後検証エラーとして表面化します。 -
インライン brief。 これらが返されることはありません。インライン状態(
risk_tier_triage、activate_leaves、collect_findings、verify_coverage、synthesize_release_readiness、write_run_directory、emit_stdout、short_circuit_exit、stage_a_empty)は、ctxr-fsm 内でサーバー側にて進行します。ワーカーの出力をコミットした後に返される brief は、1 つ以上のインラインステップの後に返された brief である場合があります。
-
-
停止します。 brief がターミナルになったとき、またはコミットでエンベロープエラーが返されたときに停止します(
.ctxr-fsm/memory/principles.mdの原則 4~5 を参照してください)。
ワーカーのディスパッチ — 並行処理
ファンアウトを行うのは、dispatch_specialists 状態のワーカーだけです。これは Loop 状態です。上流の plan_specialist_batches インラインハンドラーが、選択されたすべてのリーフを、1 つ以上の決定論的なバッチ(通常はティアに応じて 1 バッチあたり 3~5 ユニット)に分割し、エンジンはバッチごとに 1 回の Loop イテレーションを実行します。各イテレーションのブリーフには、次の内容が含まれます。
iteration_n— 1 から始まるイテレーションカウンター。inputs.specialist_batches[]— プラン全体。inputs.total_batches— プランナーが想定するループ回数。
各イテレーションでは、specialist_batches[iteration_n - 1].units[] 内のすべてのユニットを、クライアントの並列ツール呼び出し機能(Claude Code:1 つのメッセージ内で複数の Task / Agent 呼び出し、Codex:同等の機能)を使って並行してディスパッチします。すべてのサブエージェントの出力を収集した後、ワーカーの response_schema に一致する集約ペイロード({batch_index, iter_outputs[], loop_done})を fsm.commit_outputs にコミットします。
ループを終了するかどうかはエンジンが決定します。 最後のイテレーションでは loop_done = (iteration_n == total_batches) に設定します。ただし、自分で dispatch_specialists を先に進めてはいけません。コミットのたびに、再度 fsm.get_brief(run_id) を呼び出します。次のブリーフに state_id = "dispatch_specialists" が含まれている限り、まだ実行すべきイテレーションがあります。次のブリーフで状態が先に進んだ場合(通常は merge_specialist_outputs へ移行した場合)にのみ、ディスパッチは完了です。
空になるまでループする例(擬似コード)
while True:
brief = fsm.get_brief(run_id)
if brief.terminal:
print(brief.run_dir_path)
break
if brief.has_loop and brief.state_id == "dispatch_specialists":
# One iteration = one planner batch. Drain every unit IN PARALLEL.
current_batch = brief.inputs["specialist_batches"][brief.iteration_n - 1]
iter_outputs = dispatch_units_in_parallel(current_batch["units"])
loop_done = brief.iteration_n == brief.inputs["total_batches"]
fsm.commit_outputs(run_id, outputs={
"batch_index": brief.iteration_n,
"iter_outputs": iter_outputs,
"loop_done": loop_done,
})
# Do NOT break here. Continue the WHILE loop so we re-poll
# fsm.get_brief; the engine reissues another dispatch_specialists
# iteration (with iteration_n + 1) until total_batches is reached.
continue
if brief.has_worker:
outputs = dispatch_single_agent(brief.worker)
fsm.commit_outputs(run_id, outputs=outputs)
continue
1 回のイテレーション内で並行して実行できるスペシャリストの上限は、risk_tier_triage の cap フィールドです。値は trivial=3、lite=8、full=20、sensitive=30 で、args["max-reviewers"] で上書きできます([3, 50] の範囲に制限されます)。イテレーション回数はこれとは独立しており、プランナーが指定する total_batches です。full ティアで 100 個のリーフをレビューし、batch_size を 5 にすると、20 回のイテレーションが実行され、それぞれ最大 5 ユニットを並行してディスパッチします。
診断:DispatchLoopExitedEarlyError
マージャーが、loop_iters[] に含まれる (leaf_id, sub_index) ユニットの数が、specialist_batches[] で計画された数より少ないと検出した場合、スペシャリストの失敗が黙って見過ごされた状態で NO-GO レポートを出力するのではなく、DispatchLoopExitedEarlyError を発生させます。エラーメッセージには不足しているユニット ID が含まれるため、オペレーターはオーケストレーターがスキップしたイテレーションをすぐに確認できます。修正すべき箇所は常にオーケストレーター側です。fsm.get_brief の状態が dispatch_specialists から移るまでループを再開してください。
状態ごとのツールの範囲
各ワーカー状態では、FSM 仕様に allowed_tools の許可リストが固定されています。
このリストは、その状態に対してディスパッチされたサブエージェントが呼び出せるハーネスツールの正確な集合です。ツール ID は Claude Code の権限形式を使用します(スコープ付きシェルコマンドには Bash(<prefix>:*)、それ以外はツール名のみ)。他のハーネスではディスパッチ時に変換されます。
| 状態 | allowed_tools |
|---|---|
scan_project | Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git ls-files:*), Bash(cat:*), Read, Glob |
tree_descend | Read |
llm_trim | (none — brief に基づく純粋な推論) |
tool_discovery | Bash(eslint:*), Bash(ruff:*), Bash(mypy:*), Bash(npm test:*), Bash(pytest:*), Bash(cargo:*), Bash(go test:*), Bash(which:*), Read |
dispatch_specialists | Read, Grep, Glob, WebFetch, Bash(git diff:*), Bash(git log:*) |
インライン状態(risk_tier_triage、activate_leaves、
collect_findings、verify_coverage、synthesize_release_readiness、
write_run_directory、emit_stdout、short_circuit_exit、
stage_a_empty)と terminal 状態の許可リストは空です —
これらは ctxr-fsm 内でサーバー側で実行され、サブエージェントには決してディスパッチされません。
ワーカー状態に対してサブエージェントをディスパッチする際は、この状態の allowed_tools をそのままサブエージェントのツール権限形式に渡してください(Claude Code:--allowedTools=<list>、Codex 相当:--tools、Cursor:相当する設定)。その後、サブエージェントが行うすべての fsm.* 以外のツール呼び出しで fsm.observe_tool_call を呼び出し、ドリフト検出器が監査できるようにしてください。違反すると off_allowlist_tool_call(重み 5.0)が発生し、累積値が 10 を超えると実行が自動的に一時停止されます。
このスキルが生成するもの
<project>/.skill-code-review/<yyyy>/<mm>/<dd>/<shard>/<rest>/ の下に、report.md(Markdown)、report.json(機械可読形式)、manifest.json(スキル側の実行メタデータ)が生成されます。正確な形式については report-format.md に記載されています。
判定:GO、CONDITIONAL、または NO-GO。
可観測性
スキルの状態ごとの検証パネルは、ワーカーのコミットごとに verifier_passed と verifier_rejected イベントを FSM イベントストリームに出力します。これらをリアルタイムで確認するには、次の手順に従ってください。
http://localhost:7475/runs/<run_id>で fsm UI を開きます(start_run時にオーケストレーターが出力する URL です。ポートはctxr-fsmスーパーバイザーで設定された UI ポートと一致します)。- 実行の AdminSheet を開きます。Drift セクションに、状態ごとの検証結果のタイムラインが表示されます。
- 任意のワーカー状態(
scan_project、tree_descend、llm_trim、tool_discovery、dispatch_specialists)をクリックし、Events for this state タブに切り替えます。その状態のすべてのverifier_passed/verifier_rejectedイベントが、パネルの投票者ごとの理由文字列とともに一覧表示されます。
同じワーカー状態で連続却下の上限(現在は 3。code_review.handlers._VERIFIER_REJECTION_LIMIT で定義)に達すると、オーケストレーターは実行をインラインの verifier_stuck 状態に移行します。この状態は degraded_run エンベロープを出力し、synthesize_release_readiness がそれを受け取って判定を引き下げます(カバレッジが部分的な実行で GO が生成されることはありません)。fsm UI では、行き詰まりが該当状態の Sheet 上に黄色のチップとして表示されます。
関連項目
code-reviewer.md— 11 ステップのオーケストレーター 設計ドキュメントです。release-readiness.md— 8 ゲートの 判定仕様です。report-format.md— マニフェストとレポートのスキーマです。reviewers.wiki/— 約 476 個のリーフ レビュアーのコーパスです。CHANGELOG.md— リリース履歴です。
プログラムから実行するランナー(本番オーケストレーション)
code_review/runner.py::run_review(args, dispatch_worker, dispatch_specialist, ...)
は FSM をプロセス内で駆動し、制御されたフォールトトレラントなスレッドプールを介して各リーフのスペシャリストにディスパッチします。並列処理と耐障害性を担います。
- 上限付きの適応型並列処理(AIMD)。
_AdaptiveLimiterは稼働中のワーカー数を[min_workers, max_workers]の範囲に保ちます。レート制限のシグナルを受けると半減し、成功が続くと 1 つずつ増やします(スレッドには ThreadPoolExecutor を使用します)。 - レート制限への耐性。
RateLimitErrorを発生させた単位は、乗算的バックオフと再試行を行い、プールを縮小します。 - コンテキスト超過への耐性。
ContextOverflowErrorを発生させた単位はサブシャード化(ファイルを半分に分割)して再ディスパッチします。分割できない単一ファイルはfailed単位になり、破棄されることはありません。 - 100%のカバレッジ。 計画されたすべての単位をディスパッチします。それでも失敗した単位は
status: "failed"となります(マージ段階でファイルの見落としがないことを確認します)。 - モデルに依存しません。 状態ごと/リーフごとの LLM 呼び出しは、注入されたフック(
dispatch_worker/dispatch_specialist、または環境変数CTXR_SCR_WORKER_DISPATCH/CTXR_SCR_SPECIALIST_DISPATCH)です。
この SKILL.md に記載された LLM オーケストレーターパスと、このプログラムから実行するランナーは、同じ FSM とインラインハンドラーを共有します。ランナーは、コードからレビューを実行するための決定論的で並列かつフォールトトレラントな方法です。