はじめに ─ この記事の位置づけ
これは「ローカルLLMをSV/UVM用に育てる」シリーズの4本目です。3本目では、RAG/LoRA の外で一番効くのは追加学習ではなく「生成 → シミュレーション → 直す」の自己修正ループだろう、そしてそれを動かす土台が MCP だろう、というところまで整理しました。今回はその続きで、では実際にどんなハーネスで回すのか、を一段具体に落とした設計案です。
前回までと同じく、まだ M5 Max 到着前で、これは机上の設計メモです。なので「こう組むつもり」という構想であって、回して測った結果ではありません。実際にエージェントが期待どおりループを回すかは到着後にやってみないと分からない、という留保は前回までと変わりません。
今回の前提を、先に3つ明示しておきます。
- 土台を OpenCode にする。汎用のコーディングエージェントの中で、プラグインのフックが今回やりたい「門番」に一番素直に対応していそうだ、という理由です1。
- 自己修正ループの門番を、プラグインのフックとして実装する。ループ本体はエージェントに持たせ、自分は門番だけを差し込む。
- シミュレータを MCP でも開放し、エージェント自身が「書いた → 通してみる」を自律的に確認するルートも用意する。ただし最終的な合否判定だけはエージェントに握らせない。
最後の点が今回の肝で、自律確認のルートと、決定的な門番のルートを二段重ねにする、という話が中心です。
1. おさらい ─ 自己修正ループの本体は「制御ループ」
最初に、前回の核心を一つだけ引き直しておきます。自己修正ループの実体は、巧いプロンプトそのものではなく、生成 → 実行 → 結果を次の入力に戻す、という外側の制御ループ(オーケストレーション)です。プロンプトの工夫はその中の一部品にすぎません。
SV/UVM でこの手が他分野より強いのは、確実な外部検証器(シミュレータ)が手元にあるからでした。外部検証器なしで LLM が自分の出力を見直すだけの内省的自己修正は、むしろ性能を下げうるという一般的知見があります。逆に言えば、ループの良し悪しは「確実な判定器を、エージェントの主観に薄めずに効かせられるか」で決まる、ということでもあります。今回の設計案は、ほぼこの一点をどう守るか、という話に集約されます。
2. なぜ OpenCode を土台にするか
自己修正ループを回すハーネスは、自前で100〜200行ほど書く道もありますし、Aider の --test-cmd に乗せる道もあります。今回 OpenCode を選ぶのは、プラグインのフックが門番にそのまま対応するからです。OpenCode のプラグインは、Tool(tool.execute.before / tool.execute.after)、Session(session.created / session.idle ほか)、File、Permission、Message といった各カテゴリのライフサイクルイベントにフックでき、AI が呼べるカスタムツールを足したり、ツール実行を横取りしたり、挙動を書き換えたりを TypeScript で書けます1。
発想の転換は一つです。自前ハーネスのときのように往復ループ自体を書くのではなく、ループとファイル編集はエージェントに持たせて、門番だけをフック点に差し込む。コードを書く量は減ります。
ただし正直に、代償もあります。ループの制御をエージェントに預けるぶん、エージェントが「直し終えた」と自己申告して止まろうとする瞬間が出てきます。その綱をどこで握るかが後述の停止ゲートで、ここが今回の設計でいちばん落としてはいけない部分です。先に断っておくと、この「停止を握る」部分は OpenCode の現行版だと素直にはいかず、本記事でいちばん設計をやり直した箇所でもあります(4章で詳述)。
3. 門番を二種類に分ける ─ 機械ゲートと意味ゲート
ハーネスの門番を、性質で二つに割ると設計が楽になります。
機械ゲートは、正規表現と diff で決定論的に答えが出る仕事です。具体的には、コード抽出、無変化・省略の検知、そして UVM_ERROR : 0 の合否判定。ここを LLM に渡すと三つ損します。全ターンにもう一回推論が挟まって遅くなる、同じ入力で結果がぶれて確率的になる、そして門番自体が幻覚する(「UVM_ERROR : 0 です」と誤読したら偽合格を追認してしまう)。判定器の信頼性がループの上限を決めるのに、その判定器を確率的な部品にするのは筋が悪い。ここはコードのままが正解です。
意味ゲートは、規則で書けない判断です。エラー群から「まず直すべき1件」を選ぶ、無変化 diff では拾えない「直したと言いつつ別の所を壊した」を見抜く、差分オラクルの食い違いを一次裁定する、そして生成役とは別人格のレビュアに「この修正は診断に本当に答えているか」を観させる。これらは意味理解が要るので LLM の出番です。
OpenCode に当てると、機械ゲートはフック(と、後述のとおり停止だけは外側のドライバ)、意味ゲートはサブエージェント、という割り付けになります。表にすると、こう分かれます。
| 機械ゲート | 意味ゲート | |
|---|---|---|
| 仕事 | コード抽出、無変化・省略の検知、UVM_ERROR:0 判定 | 直すべき1件の選定、壊しの検知、差分裁定、修正レビュー |
| 決め方 | 正規表現・diff で決定論的 | 意味理解が要る |
| 担当 | コード(フック/外側ドライバ) | LLM(サブエージェント) |
| LLM に渡すと | 遅い・確率的・幻覚する | 本来の出番 |
4. フックへの割り付け
具体的に、これまでの部品を OpenCode のどの機構に置くかを並べます。
シミュレータの実行は、カスタムツール(または後述の MCP)にします。OpenCode のカスタムツールは任意の言語で書けるので2、run_verilator / run_sukimasim / run_lint を、実体のツールをシェル起動して JSON を返す薄いラッパとして定義する。
sim 出力の整形と合否判定は、tool.execute.after フックに置きます。OpenCode は tool.execute.before(実行前に横取り・ブロック)と tool.execute.after(実行後の結果を受け取る)を持っています1。after フックで sim の JSON をパースし、カスケードを畳んで根本原因を数件に絞り、確信度の高いものだけ残す、という「料理」をする。そして決定的に重要なのが、UVM_ERROR : 0 の確認をここに置くこと。exit code を信じず、このフックで判定する。
コード抽出の問題は、ここでほぼ消えます。自前ハーネスで一番事故るのはフェンス抽出(```svの取りこぼし、複数ブロック、地の文混入)でしたが、OpenCode のエージェントは自分で edit/write ツールでファイルを直接書くので、こちらでコードブロックを切り出す必要がなくなります。残る「無変化・省略の検知」だけは、edit/write に対する tool.execute.after で diff が空かどうかを見れば拾えます。
早すぎる停止の阻止は、当初 stop フックに置くつもりでした。が、調べ直したらここが一番の落とし穴で、OpenCode の現行版には stop という名前のフックは存在しません。Session 系のイベントは session.created / session.idle / session.deleted などで、しかも session.idle は「ループが切れた後」に発火するので、プラグインが投げて(throw して)エージェントの完了を差し止める、という芸当はできません。「ループが切れる直前に発火して再投入できるフック(session.stopping)が欲しい」というのは、執筆時点でまだ未実装の機能要望として上がっている段階です3。
なので現実に取れる「まだ終わってない、続けろ」の入れ方は、session.idle を検知して client.session.prompt() で継続メッセージを注入し、ループを再起動する形になります。これはコミュニティの継続系プラグイン(todo の積み残しを検知して同じ要領で押し戻すもの)が実際に使っている手です。判定の中身(UVM_ERROR : 0 を見て pass でなければ押し戻す)は変わらず、置き場所が「停止を差し止めるフック」ではなく「idle 後の再投入」に変わる、という理解が正確でした。
ただしこの idle 再投入には二つ難点があります。注入した継続が(ユーザー発言として)会話に見えてしまう点と、非対話の opencode run で idle 後にプロセスが畳まれる競合があり、空の継続ターンが生じうる点です4。なので本記事の結論としては、停止ゲートをプラグイン内の idle 再投入に頼り切るのではなく、opencode serve を立てて外側の SDK スクリプトからセッションを駆動し、「sim が pass を出すまで次のプロンプトを投げる」というループを OpenCode の外で握るのが、いちばん堅いと考えています。これだと「停止を握る」のが文字どおりエージェントの外側になり、当初の狙い(最終宣言はエージェントの意思の外)とも素直に合います。session.stopping が将来入れば、そのときはプラグイン内で完結する綺麗なゲートに寄せ直す、という見立てです。
レビュアは、サブエージェントにします。OpenCode はエージェントごとに専用プロンプト・専用モデル・権限(permission)を持てて、組み込みの Plan エージェントは編集や bash が既定で ask の読み取り寄りのプライマリです5。これを真似て、別系統の小型モデル + 読み取り専用、のレビュア役を宣言する。サブエージェントは呼び出し元と別のモデルを指定できるので、「生成は活性中くらいのモデル、レビュアは別系統の小型」というモデル分離が設定だけで済みます。
一点、フックでサブエージェントを縛ろうとするときの注意があります。Task ツール経由で起動したサブエージェントのツール呼び出しには、プラグインの tool.execute.before フックが効かない(素通りする)という既知の挙動があります6。なのでレビュア役の制約は、フックではなくエージェント側の permission(読み取り専用)で掛けるのが確実です。
best-of-n は、1セッション内に収まらないので外側に出します。OpenCode は公式に JS/TS・Python・Go の SDK を持ち、いずれもセッションの作成・プロンプト送信・購読をプログラムから駆動できます7。opencode serve を立てて、そこへ SDK から複数セッションを並列に投げ、通ったものを拾う係を OpenCode の外の一枚上に置く、というのが現実的です。これは先の停止ゲートを「外側の SDK ドライバで握る」話とも自然につながります(同じ serve を相手にする)。なお Go の SDK だけは import パスが github.com/sst/opencode-sdk-go で、リポジトリ名(anomalyco)とずれているので、使うときは注意です。
ここまでを一覧にすると、こうなります。
| 部品 | OpenCode の置き場所 | 性質 | 注意点 |
|---|---|---|---|
| sim 実行 | カスタムツール/MCP | 道具 | 任意言語でシェル起動。MCP なら他エージェントにも移植可 |
| 合否判定(UVM_ERROR:0) |
tool.execute.after フック |
機械ゲート | exit code を信じない。ここで決定論的に判定 |
| 無変化・省略の検知 | edit/write の tool.execute.after
|
機械ゲート | diff が空かを見る。コード抽出自体はエージェントが担うので不要 |
| 早すぎる停止の阻止 |
session.idle 再投入/外側 SDK ドライバ |
停止ゲート |
stop フックは存在しない(4章)。外側で握るのが堅い |
| レビュア | サブエージェント(別モデル・読み取り専用) | 意味ゲート | 制約は permission で。フックは Task 経由だと素通り |
| best-of-n |
serve + SDK(外側) |
試行回数 | N本並列、通ったものを拾う |
絵にすると、こうなります。色付きが今回のキモで、青がエージェントの領分、橙がフックの門番、赤が外側ドライバの停止ゲートです。
エージェントはループを回すが、合格の宣言だけは外側(フックと停止ドライバ)が握る、という構図です。
5. MCP ルート ─ エージェント自身にも確認させる
ここが今回の追加点です。門番(フック)が最終合否を決定的に握る一方で、シミュレータを MCP で開放して、エージェント自身が「書いた → 通してみる → ログを見て直す」を自律的に回せるルートも用意する。前回 MCP4EDA で見た「EDA ツールを LLM の道具として公開する」使い方が、ちょうどこれに当たります。
二つのルートは目的が違います。ただし叩く run_sim は同じ1つで、経路が二股に分かれるのではなく、1回の実行を両側から見ている、という点に注意してください(4章の「run_sim を呼ぶ」と、ここの MCP は同じツール実行を指しています)。
点線=自律確認(エージェントが直すためにログを見る)、太線=門番(フックが同じ実行の JSON を読んで合否を宣言する)。同じ run_sim 実行を、エージェントは「直すために」、フックは「合否を宣言するために」見る、という二重取りです。だから「エージェントは sim を呼べるが、合格したとは言えない」が成り立ちます。
この二つは両立します。エージェントは MCP 経由で sim を叩いて、自分で直していける。でも「合格した」という最終宣言は、tool.execute.after が JSON を読んで決める。エージェントは sim を呼べるが、合格したとは言えない ── この状態を保つのが狙いです。
なぜ一括で MCP にしないか、を一応書いておきます。MCP は「エージェントが使う道具」という設計思想なので、合否判定まで MCP にしてエージェントに呼ばせると、呼ぶか呼ばないか・結果をどう解釈するかがエージェントの裁量になる。確実な外部検証器を、エージェントの裁量の内側に入れることになり、1章で書いた偽合格がここから入ってきます。SV/UVM でループが強いのは外部審判が手元にあるからで、その審判をエージェントが呼ぶ道具に格下げすると、強みを自分で薄めてしまう。だから判定はエージェントの意思の外側、という線は引いておきたい。
仕分けると、こうなります。
| 配置 | 何を置くか | なぜそこか |
|---|---|---|
| MCP(エージェントが呼ぶ道具) |
run_verilator / run_sukimasim / run_lint、RAG 参照、波形の問い合わせ |
エージェントが自律的に使ってよい。他エージェントにも移植できる |
| フック(エージェントに見せない門番) | UVM_ERROR:0 の合否判定(tool.execute.after)、無変化・省略の検知(edit/write の after) |
確実な検証器をエージェントの裁量の外に置く |
外側に握る(stop フックが無いため) |
早すぎる停止の阻止 → session.idle 再投入 / serve+SDK ドライバ |
完了を差し止めるフックが存在しないので外で握る |
MCP 化の利点は移植性です。sim を MCP サーバにしておけば、ハーネスを OpenCode から Claude Code や Cline に乗り換えても、同じ sim ツールがそのまま使えます。MCP サーバはローカルプロセスとして自前ホストできるので、エアギャップ方針とも合います。
つまり今回の答えは、sim 実行は MCP にする価値が高い、でも合否判定は MCP にしてはいけない、の二本立てです。前回までの「機械ゲートはコード、意味ゲートは LLM」に、もう一軸「道具は MCP、門番はフック」が重なる、という整理になります。
6. sim 側の出力をどう整えるか(フック前提)
フックが sim 出力をパースする以上、sim が頑張るべきは「LLM が読んで嬉しい散文」ではなく、ハーネスが確実にパースできる構造化出力です。LLM が最終的に見る文章は、フックが整形して作る。なので sim が出すべきは正確で構造化された素材で、LLM 向けの料理はフックの仕事、という分業になります。
フックがパースしやすいフィールドは、このあたりです。
- 安定した位置情報
file:line:col。これが無いと該当行を抜けない。可能なら Clang 風にソース行 + キャレットまで出せると、LLM の注意が一点に固定されて直しが速い。 - 安定したエラーコード(
SUKIMA-E1203のような不変 ID)。重複排除・件数カウント・種別ルーティングのキーになる。 - 根本原因を先頭に、派生エラーは抑制。1個の原因が下流に50個撒くと、ローカルモデルの狭いコンテキストが溢れて的外れな所を直し始める。
- 決定論。同じ入力で同じ出力・同じ順序。実行ごとに違うとループが安定せず、重複排除も壊れる。
- 内部ノイズを既定で出さない。C++ のスタックトレースや内部 assert ダンプは LLM には雑音なので、デバッグフラグの裏に隠す。
sukimasim 固有の勘所が一つあります。この構成で sukimasim は実行の主オラクルではなく、差分テストの第2オラクルです(前回7.4)。だとすると一番価値が高い出力は、普通のコンパイルエラーではなく、食い違いそのものを明示した診断 ──「Verilator が2-state で黙って通したものを sukimasim が拾った」という情報を、専用の severity クラスで出すことです。
そしてこの差分は、対称に枠付けするのが安全です。sukimasim はまだ UVM 完走では枝バグが残る段なので、「あなたのコードが間違い」と断定すると、実は sukimasim 側のバグだったときに LLM が幻のバグを延々と追います。だから「両者が食い違う(要調査)」という中立な書き方 + 確信度のティアを付ける。sv-tests を100%通している構文・elaborate 段は確信度 high で断定してよく、未成熟な領域は medium で「差分あり」止まり、と段を分けられると理想的です。
二チャネルにしておくのが現実的だと思っています。フック向けの JSON(1行1診断)と、人間がログを直接読むときのテキスト。
dut.sv:42:18: error[SUKIMA-E1203]: implicit net 'aw_ready' has no driver
42 | assign s_axil.bvalid = aw_ready & w_valid;
| ^~~~~~~~
note: 宣言が見当たりません。'awready' の誤記?
differential: Verilator は implicit wire として受理(要確認)
{"sev":"error","code":"SUKIMA-E1203","file":"dut.sv","line":42,"col":18,
"msg":"implicit net 'aw_ready' has no driver","oracle":"sukimasim",
"differential":{"verilator":"accept","sukimasim":"reject"},"confidence":"high"}
--diagnostics=json のような形で JSON をフックに渡し、テキストは人間用、と割り切る。こうすると、LLM 向けの最終文面はフックが握れるので、モデルごとの言い回し調整を sim を一切触らずに回せます。
7. 応用 ─ SVA(アサーション)生成への展開
ここまではRTL/UVMの生成を念頭に書いてきましたが、このループは SVA(SystemVerilog Assertions)の生成に応用すると、むしろ一番報われる対象だと考えています。SVAは普段から苦労する所で、難しさが二段あるからです。構文(|-> と |=>、##[1:$]、$rose/$stable、throughout、within あたり)で詰まる段と、書いたつもりの意味が実際の波形挙動とズレる段。この両方に外部検証器が当たるので、ループの旨味が大きい。
構文・elaborate の壁は、これまでの機械ゲート(lint → elaborate)がそのまま効きます。問題は意味のズレの方で、ここにSVA固有の落とし穴があります。
7.1 判定器の作り方が変わる ─ vacuous pass という天敵
RTL生成のループでは、判定器は「$finish 到達かつ UVM_ERROR : 0」でした。ところがSVAでは、アサーションが通っただけでは意味がありません。空virtually真(vacuous pass)という罠があるからです。
たとえば a |-> b というアサーションは、前件の a が一度も成立しなければ、中身を評価されないまま「pass」になります。LLMが間違ったアサーションを書いても、前件が起きない刺激なら緑になってしまう。これはRTL生成の偽陽性(甘いテストベンチ)と同じ構造の、SVA版の天敵です。
なので判定器は二値ではなく、最低でも三状態で読む必要があります。
fail : 反例があった。アサーション or 設計のどちらかが間違い
vacuous pass : 前件が一度も成立せず素通り(実質ノーカウント。危険)
non-vacuous pass : 前件が成立した上で成り立った(これだけが本物の合格)
機械ゲート(tool.execute.after)が見るべきは exit code でも「ERROR:0」でもなく、「non-vacuous pass が取れているか」になります。多くのシミュレータは vacuous pass を区別して報告でき、カバレッジ(cover property や assertion control)で前件成立をカウントできるので、フックでそこを読む。vacuous のときは「前件が一度も起きていない。アサーションが過剰制約か、刺激が足りない」という、SVA固有のメッセージを戻す。これがSVA版の合否判定の核です。
7.2 ミューテーションで「バグに反応するか」を測る
もう一段上の判定軸が、SVAには用意できます。ミューテーション(変異)テストです。検証対象のRTLにわざとバグを注入し、「このアサーションはそのバグを捕まえて fail するか?」を見る。捕まえられないアサーションは、書けてはいるが無力です。
これをループに組み込むと、判定器が「真である」だけでなく「バグに反応する」まで見るようになります。non-vacuous pass を取れた上で、注入バグでちゃんと fail する ── ここまで通って初めて合格、とする。RTL生成には無い、SVA生成だけの上等な判定軸です。フックの合否は、整理するとこう三段になります。
| 段 | 見るもの | 不合格時に戻すメッセージ |
|---|---|---|
| 構文・elaborate | lint / elaborate が通るか | 構文エラー位置(file:line:col) |
| non-vacuous pass | 前件が成立した上で pass か | 前件が起きていない。過剰制約 or 刺激不足 |
| ミューテーション | 注入バグで fail するか | このバグを見逃している。アサーションが弱い |
7.3 ループの差分は「判定器」だけ
骨格はRTL生成と全く同じで、変わるのは判定器の中身と戻すメッセージだけです。生成(LLMにSVA+バインド先を書かせる)も、判定器に通す(lint → elaborate → シミュレーション)も、session.idle 再投入で pass まで回す所も、これまでの図のまま。run_sva という薄いツールを足し、tool.execute.after の合否ロジックを上の三段に差し替える、という最小の改造で乗ります。
sukimasim の差分オラクルも、SVAだとむしろ出番が増えます。並行アサーション(##、|->)は前回7章のとおり Verilator がまだ限定的なので、ここは sukimasim の並行アサーション対応がどこまで進むか次第ですが、構文が複雑なぶん、構文・elaborate 段の差分チェックだけでも効きます。前回の補足で挙げたSVA研究(AssertionForge、Hybrid-NL2SVA、SVA生成にMCTSを使うSANGAM)が厚いのも、この「判定器が効く」性質ゆえだと見ています。
なお、ここはまだ設計の見立てで、vacuous 判定やミューテーションをフックの形にどこまで素直に落とせるかは、到着後に SVA を実際に回して確かめる予定です。SVAは vacuous / ミューテーションという固有論点が太いので、踏み込んだ実測は5本目で独立して扱うかもしれません。
8. 実装順(費用対効果順)
一気に全部を立ち上げると、どの層の責任で詰まったか切り分けられません。なので順に積みます。
- 機械ゲート(フック)と停止ゲート(外側ドライバ)を先に固める。
UVM_ERROR:0の合否判定をフックに、pass まで投げ直す制御をopencode serve+ SDK の外側に。これを先に決定的に効く状態にしておけば、後で MCP を足してエージェントが自律的に動き始めても、最終宣言だけは必ずハーネスが握っている、という安全網が先にある形になります。 - sim を MCP で開放する。エージェントの自律確認ルートを足す。ここで初めてエージェントが「書いて通して直す」を回し始めるので、門番が先にある状態で足すのが安全です。
- レビュアのサブエージェントを1体足す。意味ゲートの最初の一歩。効果を測ってから増やす。
- (応用)SVA生成に広げる。
run_svaを足し、合否を vacuous 弾き+ミューテーションの三段に差し替える。判定器の中身だけの改造なので、土台が固まってからが楽。 - best-of-n を外側に足す。
opencode serve+ SDK で並列に。夜通し無人で回すなら、権限(permission)でeditやbashをallowにしておかないと承認待ちで止まります。特にdoom_loopとexternal_directoryが既定askで、ヘッドレスだとここで黙ってハングしやすいので、明示的にallowにする(または--dangerously-skip-permissions)のが要注意点です。
前回8章の「一気に積まず、効いて頭打ちになってから次へ」と同じ温度で、門番 → 道具 → 意味ゲート → 試行回数、の順に一層ずつ、が無駄がないはずです。逆順(暴れ始めてから門番を後付け)だと、何が原因で詰まったか分かりにくくなります。
安全網(門番)を先に置いてから、エージェントが自律的に動く要素(MCP・並列)を後に足す、という左から右の順序がそのまま安全側です。
9. エアギャップの注意
ここは当初「ローカルプラグインはネット不要だからエアギャップと相性が良い」と軽く書いていたのですが、調べ直すと、その狭い主張は正しい一方で、OpenCode 全体が既定でオフライン清潔というわけではない、というのが正確でした。順に分けます。
狭い主張(正しい)。.opencode/plugins/(プロジェクト)や ~/.config/opencode/plugins/(グローバル)に置いたローカルプラグインは、起動時にネット無しで直接ロードされます1。npm 配布のプラグインや、外部パッケージに依存するローカルプラグイン(設定ディレクトリに package.json を置いた場合)は、起動時に Bun で install が走るのでネットを取りに行きます。なので厳格なエアギャップでは、ローカル TS で書き npm 依存を避ける(または事前にキャッシュを持ち込む)。--pure で外部プラグインを読まずに起動する手もあります。
広い注意(ここが抜けていた)。OpenCode 本体は、LLM 以外でも既定でいくつか外に出ます。起動時にモデル/プロバイダ情報を models.dev から取りに行く、LSP サーバを初回使用時にダウンロードする、自動アップデートを確認する、Web UI が外部にプロキシする、などです。公式の「オフラインモード」はまだ無く、未対応の要望として上がっている段階です。緩和は環境変数で個別に行う形で、モデル取得の無効化、LSP ダウンロードの無効化、自動アップデートの無効化、内部ミラーへの差し替え、NO_PROXY=localhost,127.0.0.1(TUI とローカルサーバ間がプロキシを迂回するように)などを組み合わせることになります。コミュニティには完全オフライン化のフォークも存在しますが非公式です。
なので結論としては、「ローカルのプラグインとツールはネット無しで動く。ただし OpenCode のプロセス自体は既定でオフライン清潔ではないので、隔離環境に入れる前に、実際の外向き通信をネットワーク遮断したコンテナ等で一度棚卸しして潰す」という温度で書くのが正直です。完全に塞げたと確認できて初めて「エアギャップ」と呼ぶ、という閾値にしておきます。
sim の MCP サーバ自体はローカルプロセスとして自前ホストできるので、ツール層はエアギャップと両立します。OS の棲み分けは前回までと同じで、Mac(M5 Max)上で LLM が生成し、Mac 上の Verilator/iverilog/sukimasim で回すのが基本ライン。RAL やカバレッジまで本格 UVM を試したいときだけ、WSL2 側の DSim に渡す一段が挟まります。
10. 正直な結論と注意点
これは設計案で、まだ一度も回していません。なので一番大きい不確実性を正直に書いておきます。
OpenCode のエージェントが実際にどこまで素直にループを回すか、特に「早すぎる停止」を本当に押さえ切れるか、はやってみないと分かりません。4章で書いたとおり、当てにしていた stop フックは存在せず、session.idle 後の再投入か、opencode serve + 外側 SDK ドライバで握る形に組み替えました。この外側ドライバが期待どおり「pass まで投げ直す」を安定して回せるか、が最大の不確実性です。ここがうまく効かないなら、エージェントに預けず自前の100〜200行ループに戻す、という判断も残しています。OpenCode 案の旨味は「ループとファイル編集を書かずに済む」ことなので、その代償(制御を預ける)を外側ドライバで取り戻せるか、が採否の分かれ目になりそうです。
もう一点、ローカルモデルを繋ぐとき、OpenAI 互換の /v1 エンドポイントを使うこと、Ollama の num_ctx を既定の4096から16k〜32k程度に上げないとツール呼び出しが黙って壊れること(モデルの公称コンテキストに関わらず Ollama 既定が4096)、tool 呼び出しに対応したモデル(Qwen3-Coder 等)を選ぶこと、は実装前に踏みやすい穴なので挙げておきます。
UVM 完走はサブセット前提という前回7章の留保はそのままです。フル機能(カバレッジ収束・RAL・4-state)は OSS だけでは未達で、そこは DSim か商用シムの領域。むしろこの半年で、CHIPS Alliance / Antmicro が「素の(upstream)UVM 2017-1.0 をパッチ無しで elaborate できる」と announce しており8、前回までの「elaborate できる ≠ 完走する ≠ 正しく検証できる」という段分けを補強する材料になっています。elaborate は upstream UVM で届くようになったが、実シミュレーション/カバレッジはまだ進行中、という温度です。自己修正ループの土台としては、RTL 生成も UVM サブセットも Verilator/iverilog で実用十分なので、ループを組むこと自体には支障ありません。
OpenCode の機能(フックのイベント種類、SDK、プラグインの作法)は活発に動いているので、実装時に最新のドキュメントを確認するのが安全です1。
まとめ
4本目の設計メモとして、現時点の構想を整理します。
- 土台は OpenCode。ループとファイル編集はエージェントに持たせ、門番だけをプラグインのフックに差す。
- 門番は二種類に割る。機械ゲート(抽出・無変化・UVM_ERROR:0)はフックで決定論的に、意味ゲート(レビュア)はサブエージェントで。
- 二段重ねが肝。シミュレータは道具として MCP でエージェントに開放し、その結果の合否はフックで横取りする。エージェントは sim を呼べるが「合格した」とは言えない。停止を握る部分は、現行 OpenCode に
stopフックが無いため、session.idle再投入か外側 SDK ドライバで実現する。 - 一括 MCP 化は罠。確実な外部検証器をエージェントの裁量の内側に入れると偽合格が入る。判定はエージェントの意思の外側に置く。
- sim 出力は機械可読を第一に。LLM 向けの文面はフックが作る。sukimasim の差分は別 severity + 確信度ティアで中立に。
- SVA生成は同じ土台の応用。判定器を vacuous pass 弾き+ミューテーションの三段に差し替えるのが肝で、ここはループが特に効きやすい。
- 実装は門番 → 道具(MCP)→ 意味ゲート →(応用)SVA → best-of-n の順に一層ずつ。
軸でまとめると、「機械ゲートはコード、意味ゲートは LLM」「道具は MCP、門番はフック」という二つの線で全部の部品が置き場所に収まる、という整理でした。数字はこれから埋めます。到着後に、まず機械ゲートのフックから手を動かして、5本目として結果をまとめる予定です。
補足:出典と注意書き
本文の OpenCode の機能(プラグイン・フック・カスタムツール・サブエージェント・プラグインのロード挙動)は、2026年6月時点の公式ドキュメント等で確認したものです。OpenCode は活発に開発が進んでいるため、フックのイベント種類や作法は版で変わりうる前提で、導入時に最新を確認するのが安全です。
前回までと同じく、ここで挙げた構成の多くは個人のエアギャップ環境での再現を念頭に置いていますが、効果は最終的に自分の環境で測らないと分からない、という点は変わりません。特に「エージェントにループを預けつつ、停止だけ外側で握る」という今回の中心アイデアは、session.idle 再投入か serve + SDK ドライバの効きを実機で確かめるまでは仮説です。OpenCode は週単位で更新が入り、フック一覧・CLI フラグ・権限キー・エージェント設定がいずれも2026年中に変わっているので、投稿時に公式ドキュメントと使用した OpenCode のバージョンを必ず確認・明記してください。
-
OpenCode のプラグインは JavaScript/TypeScript で書き、各カテゴリのライフサイクルイベント(
tool.execute.before/tool.execute.after、session.created/session.idleほかの Session 系、file.editedなどの File 系、permission.asked/permission.replied、chat.messageほか)にフックできる。Session 系にstopという名前のフックは無い。.opencode/plugins/(プロジェクト)や~/.config/opencode/plugins/(グローバル)に置いたローカルファイルは起動時にネット無しで直接ロードされ、npm 配布のプラグインや外部依存を持つプラグインは起動時に Bun で install される。現行の正規リポジトリはgithub.com/anomalyco/opencode(旧 sst/opencode)。出典: OpenCode 公式ドキュメント Plugins(opencode.ai/docs/plugins)、2026年6月時点。 ↩ ↩2 ↩3 ↩4 ↩5 -
カスタムツールは
.opencode/tools/(または~/.config/opencode/tools/)に置き、@opencode-ai/pluginのtool()ヘルパ(引数スキーマは Zod ベースのtool.schema)で定義する。ファイル名がツール名になり、同名の組み込みを上書きする。定義は TS/JS だがexecute()から任意の言語(例: Python)をシェル起動できるので「任意の言語で実装」は正しい。カスタムツールは OpenCode プロセス内で動き、MCP サーバは別プロセスで動く。単純な統合はカスタムツール、独立した複雑なサービスは MCP、と使い分ける。出典: OpenCode 公式ドキュメント Custom Tools(opencode.ai/docs/custom-tools)。 ↩ -
現行 OpenCode の Session 系イベントは
session.created/session.compacted/session.deleted/session.diff/session.error/session.idle/session.status/session.updatedで、stopは無い。session.idleは「エージェントのループが切れた後」に発火するため、プラグインから完了そのものを差し止めることはできない。「ループが切れる直前に発火し、output.stop = falseと継続メッセージで再投入できるsession.stoppingフックが欲しい」という要望がanomalyco/opencodeの Issue #16626 として上がっているが、執筆時点で未実装。出典: OpenCode 公式ドキュメント Plugins(Events)、GitHub anomalyco/opencode#16626。 ↩ -
session.idleを検知してclient.session.prompt()で継続を注入する手は、コミュニティの継続系プラグイン(todo の積み残しを押し戻すもの等)で実際に使われている。難点は二つで、注入した継続がユーザー発言として会話に見える点と、非対話のopencode runで idle 後にプロセス破棄と競合し空の継続ターン(parts: [])が生じうる点(GitHub anomalyco/opencode#15267)。本記事が「プラグイン内 idle 再投入」より「外側 SDK ドライバ」を勧めるのはこのため。 ↩ -
OpenCode のエージェントはエージェントごとに専用プロンプト・専用モデル・権限(
permission)を持てる(opencode.jsonか.opencode/agents/の Markdown で定義)。組み込みの Plan は編集/bash が既定askの制限付きプライマリ、Build は全ツールの既定プライマリ、組み込みサブエージェントに General / Explore / Scout がある。サブエージェントは既定では呼び出し元のモデルを継承するが、modelを指定すれば別モデルにできる(レビュアを別系統の小型モデルにする設計の根拠)。Tabでプライマリを切替、@でサブエージェントを言及。toolsフィールドはpermissionに、maxStepsはstepsに置き換え推奨。出典: OpenCode 公式ドキュメント Agents(opencode.ai/docs/agents)。 ↩ -
プラグインの
tool.execute.beforeフックは、Task ツール経由で起動したサブエージェントのツール呼び出しには発火しない(素通りする)という既知の挙動がある(GitHub anomalyco/opencode#5894)。レビュア等サブエージェントの制約は、フックではなくエージェント側のpermission(読み取り専用)で掛けるのが確実。 ↩ -
OpenCode は公式に JS/TS(
@opencode-ai/sdk)・Python(opencode_ai)・Go の SDK を持ち、いずれもセッションの作成/プロンプト送信/中断/イベント購読をプログラムから駆動できる(server の OpenAPI から生成)。Go の import パスだけはgithub.com/sst/opencode-sdk-go(パッケージ名opencode)で、リポジトリ名とずれる点に注意。ヘッドレス実行はopencode run "<prompt>"(-f jsonで生イベント、--attach <url>で既存 server に接続)とopencode serve(HTTP server、OPENCODE_SERVER_PASSWORDで簡易認証)。best-of-n の並列や停止ゲートの外部駆動は、このserve+ SDK で外側に組むのが現実的。出典: OpenCode 公式ドキュメント(opencode.ai/docs/cli ほか)、各 SDK リポジトリ(anomalyco)。 ↩ -
CHIPS Alliance / Antmicro は、Verilator が「素の(upstream)UVM 2017-1.0 をパッチや回避策無しで elaborate できる」ようになったと announce している(Antmicro の取り組みと Verilator メンテナ Wilson Snyder の作業による)。これは elaborate 段の話で、実シミュレーション完走やカバレッジは引き続き開発中。執筆時点の開発版は Verilator 5.049(2026-06-15)。出典: chipsalliance.org/news/uvm-verilator、verilator.org。 ↩