← 기사 목록
日本語https://zenn.dev/topics/llm/feed

RubyKaigi 2026で聞いたSoftware FactoryをClaude Code Hookで部分的に再現してみた

추출된 키워드

37
Software Factory·5Claude Code Hook·5RubyKaigi 2026·5LLM judge·4Natural Language Spec·4NLSpec·4Attractor·4StrongDM·4Claude Code·4LLMループ·4Nate Berkopec·4Opus 4.7·3RuboCop·3RSpec·3mini-factory-demo·3intent.md·3scenarios.json·3Stop·3PreToolUse·3satisfaction·3goal_gate·3AI agent·3Andrej Karpathy·3scenarios·3Agents·3Ralph·3Autoresearch·3Factory·3AI駆動開発·2reward hacking·2Anthropic API·2Liquid·2Shopify·2Tobi Lütke·2Geoffrey Huntley·2Ruby·2TypeScript·2

원문

15,147
RubyKaigi 2026で聞いたSoftware FactoryをClaude Code Hookで部分的に再現してみた

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枚の表で並べていました。

AgentsRalphAutoresearchFactory
ゲートLLM self-stopsbuild + testsbenchmark deltamany checks
出力の型discretediscretecontinuousmultivariate
成果物final replygreen commitswinning diffspassing PRs
設計する部分tools, prompttools, promptmetric, benchmarkgates + specs
向く用途one-shot tasksstubborn bugsperf tuningfeature 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「完了したか」の機械的判定
Stop
hookでRSpec/RuboCop/surface check(メソッド定義の有無を確認)
scenarios + satisfactionユーザー満足条件と、その満足度(satisfaction)のLLM判定
.claude/scenarios.json
Stop
hookの
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の入出力表を抜粋するとこんな形:
#InputExpected
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という結果でした。

Stop
hookが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

autoresearch

Software Factory

mini-factory-demo(この記事の実装)

Claude Code