RubyKaigi 2026で聞いたSoftware FactoryをClaude Code Hookで部分的に再現してみた
RubyKaigi 2026でNate Berkopecが紹介した「LLMループ4種(Agents/Ralph/Autoresearch/Factory)」のうち、最も検証が複雑なSoftware Factoryを、Claude Code Hookで部分的に再現してみた話です。
PreToolUse+
Stopの2種類のhookで完了の判定とLLMの採点を組み、6ケースで動作確認しました。再現できなかった部分(構造の抜け/試行数)も整理しています。
1. RubyKaigi初参加の感想
先月、会社から機会をいただいて、RubyKaigi 2026に参加してきました。自分は新卒2年目で、最初はRubyを触っていて、直近はAI駆動開発でTypeScriptを使っています。
言語関連の会議に参加するのは初めてでしたが、Rubyコミュニティの暖かい雰囲気を感じました。イベントやセッションが豊富で、ノベルティもたくさんいただきました笑。Rubyコミッターたちは普段こういうことをやってるんだ、という刺激も受けました。
2. Berkopecのセッションで聞いた話
たくさんのセッションを聞きに行きましたが、中でも印象に残ったのがNate Berkopecのセッションでした。Claude CodeのRalphプラグインを知っていたので、気になりました。
実際に聞いてみると、BerkopecはLLMループをAgents/Ralph/Autoresearch/Factoryの4種類に整理していました。ループにこんなに種類があるんだ、とハッとしました。中でもSoftware Factoryは初めて聞く単語でした。
https://rubykaigi.org/2026/presentations/nateberkopec.html
入り口は、Andrej Karpathyが2026年3月に出したautoresearchの話。AI agentがtrain.pyを書き換えながら5分のtrainingを回し、ベンチマークがよくなったらcommit、悪化したらrevertする仕組みです。Karpathy本人が2日ほど動かしたところ、約700回の試行で20件ほど改善が積み上がり、ベンチマークが11%縮んだそうです。
https://github.com/karpathy/autoresearch
中盤で耳に残ったのが、「人間の判断ゲートをソフトウェアゲートに変換すると、判断が明示的になり、ばらつきが減る」という話でした(Berkopecのスライドより)。これが後ほどSoftware Factoryの話に繋がります。
3. 4つのLLMループ
LLMループは、LLMに何度か出力させて完了まで自走させる仕組みです。何を「完了」とするかの判定(ゲート)が、LLM自身の判断、buildとtestの結果、ベンチマークの差、scenarioの採点など色々あって、それが各ループの違いになります。
人間が結果を見て「次どうする?」を毎回決めなくていいぶん、止め時(ゲート)の設計が大事です。
セッションでは4ループを1枚の表で並べていました。
| Agents | Ralph | Autoresearch | Factory | |
|---|---|---|---|---|
| ゲート | LLM self-stops | build + tests | benchmark delta | many checks |
| 出力の型 | discrete | discrete | continuous | multivariate |
| 成果物 | final reply | green commits | winning diffs | passing PRs |
| 設計する部分 | tools, prompt | tools, prompt | metric, benchmark | gates + specs |
| 向く用途 | one-shot tasks | stubborn bugs | perf tuning | feature backlog |
(BerkopecのスライドDECKSET.mdからの引用)
3.1 Agents loop
普段CursorやClaude Codeに話しかけて1つの返事をもらっているとき、裏側はだいたいこれです。LLMがtoolを呼びながら1つの返事を組み立てて、自分で「もう終わり」と判断したら停止します。
messages = [user_prompt] loop do reply = llm.call(messages, tools: TOOLS) break puts(reply.text) unless reply.tool_call? result = run_tool(reply.tool_name, reply.arguments) messages << reply messages << tool_result(result) end
3.2 Ralph loop
Geoffrey Huntleyが提唱したパターン。
PROMPT.mdをbuild/testゲートがpassするまでひたすら投げ続けます。
loop do
agent.run(File.read("PROMPT.md"))
next unless build_and_test
git_commit("ralph: passing build")
git_push
git_tag(next_patch_tag)
end
ゲートはbuild + testsのpass/failだけで、頑固なバグを「テストが通るまで殴る」用途に向いています。
2026年に入ってRalphはcoding agent側に取り込まれました:
3.3 Autoresearch loop
Autoresearchは、連続値のベンチマーク差分をゲートにするパターン。改善した変更だけcommit、悪化したらrevertする形で回します。
best = benchmark
loop do
change = agent.propose_optimization
apply(change)
score = benchmark
if score > best
git_commit(change.summary)
best = score
else
git_revert
end
end
Karpathy autoresearchがまさにこの形で、もともとML training用ですが、Ruby側でもShopifyのTobi LütkeがLiquid(Shopify製のRubyテンプレートエンジン)をAutoresearchで53%高速化しています。
https://github.com/Shopify/liquid/pull/2056
3.4 Factory loop
Factory loopは、1つの機能ではなく機能のbacklog(やることリスト)を1つずつ消化していくパターンです。二重ループになっていて:
- 外側: backlogから1つ取り出して「次はこれをやる」と決める
- 内側: その1つが完了になるまでLLMに実装をやり直させる
内側だけ見るとRalphと同じ収束ループです。違いは「完了の判定」で、Factoryは異種のゲート(決定的なtest/lint、LLM judgeなど)を複数並べた多変量判定で測ります。「ユーザーが満足する条件」を複数並べて、そのほぼ全部を満たすまで内側を回し続けます。
THRESHOLD = 70
backlog.each do |spec|
loop do
code = agent.implement(spec)
next unless build_and_test(code) && lint(code) # 決定的ゲート
scores = scenarios.map { |s| s.satisfaction(code) } # LLM judge(複数試行で平均)
break if scores.all? { |score| score >= THRESHOLD }
end
ship(code)
end
"In the factory, the software is mush, the gates are the artifact"
「ソフトウェアは何度でも崩せるもの。ゲートこそが残る成果物。」(Berkopecのスライドより)
3.5 4つの使い分け
単発タスク(1つの依頼で完結するもの)はAgents、頑固なバグはRalph、パフォーマンス改善はAutoresearch、backlog消化はFactory、という使い分けです。
4. mini Software Factoryを作ってみた
Software Factoryの本物はStrongDMが公開しているフレームワークで、中核はAttractorという対話なしで動くcoding agent(non-interactive coding agent)で、NLSpec(Natural Language Spec、自然言語で書かれた仕様書)で定義されています。リポジトリにはコードが1行もなく、Markdownで書かれた仕様書(NLSpec)だけが置いてあります。StrongDMは、読んだ人がAttractorを実装して、自分のSoftware Factoryを作ることを想定しています。
https://github.com/strongdm/attractor
Attractorと同じ構造をClaude Code Hookで組めるかなと思って、試してみました。AttractorはmdのNLSpecだけで全部LLMに解釈させる作りですが、ゲートをhookで書けば「failのときは必ず止まる」をshell側から強制できます。hook分だけ縛りは強くなりますが、期待通りに止まるかを確認しやすくなります。
全コードは下記リポジトリで公開しています。
https://github.com/ruiy03/mini-factory-demo
4.1 何を再現するか
AttractorのNLSpecだけでもmdが5,700行超あって、個人で完全に再現するのは現実的じゃないので、作れる部分だけ組みました。
| Software Factoryの概念 | 役割 | mini demoでの再現 |
|---|---|---|
| NLSpec | 自然言語で書かれた仕様書 |
.claude/intent.md、backlogにspec 3つ |
| goal_gate | 「完了したか」の機械的判定 |
StophookでRSpec/RuboCop/surface check(メソッド定義の有無を確認) |
| scenarios + satisfaction | ユーザー満足条件と、その満足度(satisfaction)のLLM判定 |
.claude/scenarios.jsonを Stophookの judge.shでLLM採点(詳細§4.5) |
4.2 全体フロー
PreToolUse(
Edit/
Writeの直前に走る)と
Stop(応答完了の直前に走る、= goal_gate)の2つのhookを置きました。
Stopに入るとRSpec → RuboCop → surface check → LLM judgeの順に検証します。
4.3 仕様の二層構造
仕様は2つのファイルに分けています。
-
intent.md
:Claudeへの実装指示書。入出力表が中心 -
scenarios.json
:LLM judgeへの採点基準。「ユーザー満足の条件」を一文ずつ
RSpecで測れるpass/failは
intent.md、LLMじゃないと判断できない部分は
scenarios.json、という分け方です。
intent.mdのspec 1の入出力表を抜粋するとこんな形:
| # | Input | Expected |
|---|---|---|
| 1 | User.new(first_name: "Alice", last_name: "Smith") |
"Alice Smith" |
| 2 |
User.new(first_name: "Alice")(last_name nil) |
"Alice" |
| 3 |
User.new(last_name: "Smith")(first_name nil) |
"Smith" |
| 4 |
User.new(both nil) |
"" |
| 5 | User.new(first_name: " Alice ", last_name: "Smith") |
"Alice Smith" |
scenarios.jsonは13個のscenarioを並べたもの(1つだけ抜粋、本物はもっと定性的な要件が並びます):
{
"id": 1,
"spec": "full_name",
"name": "joins both names with a single space",
"intent": "When both first_name and last_name are given, full_name returns them joined by a single ASCII space. No leading, trailing, or doubled whitespace appears in the result."
}
あと
scenarios.jsonは
PreToolUseで編集できないようにしています。Claudeがtestを緩めてpassする近道(StrongDMが言う"reward hacking")を塞ぐためです。
4.4 hookの中身
stop.shでは、RSpec/RuboCop/surface check/LLM judgeの4段をbashで順に叩きます。どれか1つでもfailしたら exit 2 + stderr で抜けます。
exit 2はClaude Codeで特別扱いされて、stderrに書いた内容がそのまま次のターンでClaudeへの指示として渡されます。RSpecのゲートはこんな感じ:
RSPEC_JSON=$(bundle exec rspec --format json 2>/dev/null)
TOTAL=$(echo "$RSPEC_JSON" | jq -r '.summary.example_count')
FAILED=$(echo "$RSPEC_JSON" | jq -r '.summary.failure_count')
if [ "$FAILED" != "0" ]; then
echo "Stop gate: rspec ${TOTAL} examples, ${FAILED} failed" >&2
echo "$RSPEC_JSON" | jq -r '.examples[] | select(.status != "passed") | " - " + .full_description + " (" + .status + ")"' >&2
exit 2
fi
各hookの実装はリポジトリの .claude/hooks/に置いています。
4.5 LLM judge
LLM judgeはscenarioの満足度をLLMに採点させるゲートです。RSpecやRuboCopは決定的なpass/failを見ますが、「ユーザーが満足する条件」は文章で書かれているのでLLMに読ませて採点します。
実装(
judge.sh)は
stop.shから
claude --printを3回呼ぶ形にしました。Anthropic APIを直接叩いてもよかったんですが、手元のClaude Code設定でそのまま動かせるので
claude --printにしました。promptには
scenarios.json(採点基準)と
user.rb(今の実装)の中身を入れて、「各scenarioのsatisfactionを0-100で採点してJSONで返して」と頼みます。LLMの採点はブレがあるので3回の平均を取り、threshold(70%)未満ならexit 2 + stderrでfailにします。
PROMPT="You are the scenario-satisfaction judge. Score each scenario 0-100 and return JSON. === scenarios.json === $SCENARIOS === current user.rb === $TARGET_SRC " for trial in $(seq 1 3); do echo "$PROMPT" | claude --print --setting-sources user # --setting-sources user で hook の再帰を防ぐ # outputのJSONを取り出してresultsに追加 done # scenarios.idごとの平均を取って、thresholdを下回ったらexit 2
全passのときは、stderrにこんな出力が出ます:
judge gate: all 13 scenarios pass at >= 70% average satisfaction across 3 trials - id=1 avg=95% (trials: [95,95,95]) - id=2 avg=92% (trials: [90,92,95]) ...
逆にthresholdを下回るscenarioがあると、こんな出力が出ます:
judge gate: average satisfaction across 3 trials is below 70% for one or more scenarios - id=5 avg=40% (trials: [50,40,30])
4.6 走らせた結果
hookが期待通り動くか確かめるために、まずshellから叩いて6ケースを確認しました(再現コマンドと期待結果は docs/hook-verification.md)。
そのあとClaude Codeを起動して、
intent.mdを読ませてbacklogを消化させました。
期待していたのはClaudeが「specを1つずつ実装 → fail → retry → 次のspec」という流れですが、実際はClaude(Opus 4.7)が1回の応答で2つのメソッド両方を実装し、 Stop hook 1回で全passという結果でした。
Stophookが1回しか発火しなかったので、内側ループのretryは
起きませんでした。Software FactoryのretryはLLMが間違うことを前提にした仕組みですが、高性能LLMはこの程度のタスクなら一発で通せるのでretryが起きる場面が少なそうです。
ただし複雑な要件(複数のメソッドの整合性、I/OやDB操作など)や、build/type check/security scanが絡む検証pipelineなら、高性能LLMでも1回で全ゲートは通せずretryが走るはずです。これは今回再現できていない仮説で、mini demoのゲートが単純すぎたのが原因だと思います。
mini-factory-demoでは3つのうち1つ目のspecだけ実装してあって、残り2つはTODOのままです。手元で
claudeを立ち上げて残りを実装させれば、hookが動く様子が見えます。
5. 再現できた/できなかったところ
ここまで組んだhook + LLM judgeで、Software Factoryの主な構成要素(NLSpec / goal_gate / scenarios + satisfaction)ができました。ただ、mini demoでは届かなかった部分や、意図的にやらなかった部分もあるので順に書いていきます。
5.1 構造でやらなかったこと
外側backlogループはClaude任せにしました。次のspecをどれにするかは、
intent.mdを読んだClaude自身が判断する作りです。本物のAttractorはworkflowを有向グラフ(各specをnodeとして並べる)で組んで、エッジで順序を強制しますが、hookで同じことをやってみると実装が複雑になったので、mini demoには入れていません。
5.2 satisfactionの限界
mini demoのLLM judgeは13個のscenarioを3試行で測りますが、本物のSoftware Factoryは大量のscenarioを多数の試行で測ります。scenario数も試行数も少ないので、本物の精度には届かないはずです。
加えて、同じClaudeセッションが実装と採点の両方をやっているので、採点する側と書く側がお互いに影響しあっているかもしれません。独立した判定にしたいなら、別プロセスでAnthropic APIを直接叩く必要がありそうです。
6. おわりに
scenariosの書き方で、LLM judgeの結果が大きく変わります。RSpecやRuboCopは既存の仕組みに乗れますが、「ユーザーが満足する条件」は誰かが書かないと出てきません。scenariosに限らず、StrongDMが言うように、本物のSoftware Factoryだと、人間の仕事の中心はコードを書くことから、intentを書く方に移るんだと思います。ここでいうintentには、システムが何をすべきか・ユーザーが何で満足するか・何を変えてはいけないかが含まれます。Berkopecが言った「ゲートこそが残る成果物」、再現してみてだんだん意味がわかった気がします。
参考リソース
Berkopecのセッション
Ralph
- Geoffrey Huntley "Ralph Wiggum as a software engineer"
- Claude Code:
ralph-wiggum
プラグイン - Codex:
/goal
コマンド
autoresearch
Software Factory
- StrongDM Software Factory公式
- StrongDM Attractor(NLSpecのリポジトリ)
- StrongDM blog「The Software Factory: Building Software with AI」
mini-factory-demo(この記事の実装)
Claude Code