A decompose → contract → test-first → red/green harness that lets a local 27B model do agentic coding — no context-window expansion, no bigger hardware, no API bill.
Thesis: for local-model coding agents, the bottleneck people reach for — "longer context" or "buy a bigger GPU" — is usually the wrong lever. The right lever is the harness: decompose the task so the parent LLM never sees raw code, pin every name in a declared interface contract, generate tests first, then let a worker loop red→green against a real
pytestrun. With that scaffolding, a 24 GB / 27B setup builds working programs end-to-end.This repo is the harness, extracted and cleaned for the public. It was validated by having a local Huihui-Qwen3.6-27B (Q5_K_M) build a playable Tetris — engine, state, and pygame front-end — with a human only closing the last, un-automatable gap: does it actually feel right when you play it?
(日本語の説明は下 → 日本語)
Everyone building local coding agents hits the same wall and reaches for the same two levers:
- "Give the model more context." Then the parent that plans the whole build drowns in raw code, and the context that was supposed to help is what kills you.
- "Buy a DGX Spark / bigger GPU." Expensive, and for interactive coding the memory-bandwidth math often doesn't even favor it.
CODE FORGE bets on a third lever — structure — and it pays off:
- The parent never reads raw code. It only ever sees a task, a decomposition, and a declared interface (class/method/function signatures). So the parent's context does not grow with the codebase. This is the single design choice that makes local models viable here.
- One source of truth for names. Plan, tests, and implementation all reference the same declared interface contract, killing the classic local-model failure mode of name drift (
is_collisionin the plan,check_collisionin the code). - Test-first, worker can't touch
tests/. The worker may only write the files it declared it owns; it is forbidden from editing the tests it's being graded against. That's what makes "green" mean something instead of being cheated.
The harness organizes everything around a ladder of increasingly strong checks:
compile < launch (smoke) < unit-green (pytest) < behaves correctly
└─ auto ──────────────────────────────────────┘ └─ human ─┘
The machine can automatically hold everything up to "launches and unit tests pass." The top rung — is the behavior/feel actually right — is the human's job, and CODE FORGE is honest about that: it will tell you "I applied the fix; the behavior you'll have to confirm by playing it," rather than falsely declaring GREEN. The human is the oracle for defects — you describe what looks wrong, not how to fix it, and the agent localizes and repairs it.
GOAL
│
▼ PLAN parent decomposes → ordered subtasks, each with
│ (+plan-linter) goal / accept-criteria / declared interface / verify-kind
│ ── human gate: approve the plan & the interface table ──
▼
▼ TEST-FIRST generate pytest from accept-criteria (target module
│ not implemented yet = red phase)
▼
▼ BUILD (red→green) worker sees the contract + tests, emits `### FILE: path`
│ files → real pytest run → on failure, feed the full error
│ back and retry (keep-best across attempts, never regress
│ a passing test)
▼
▼ RUN human plays / exercises the artifact
│ ── L3 verdict: OK / issue / broken ── (the ground-truth signal)
▼
▼ FIX (L3 loop) human describes the defect in plain words → targeted
reading (grep/read to find the spot, not read-all) →
minimal edit → regression + smoke → repeat
Optional levers (toggled in the GUI): boundary probes, input triage, plan-linter, arbitration on stuck plateaus.
1. A local OpenAI-compatible server on localhost:8000. Anything that speaks /v1/chat/completions works — this was built against llama.cpp's llama-server. Start it with tool/--jinja support and a capable model. Reference setup used Huihui-Qwen3.6-27B (Q5_K_M) on a 24 GB card.
Tip from building this: for a 27B, tool-call / instruction-following survival tracks model generation, not size (Qwen3.5+ held up where older/smaller "uncensored" coders fell apart). Pick a recent generation.
2. Install deps:
pip install -r requirements.txt # PySide6 + requests + pytest3. Run the GUI:
python code_forge_gui.pyType a goal (e.g. "make a playable Tetris in pygame"), pick a working directory, and hit ▶ decompose & build. Approve the plan when prompted, watch the red→green scoreboard, then ▶ RUN to play the result and give your verdict.
Or run the headless HTTP service (stdlib only, no PySide6 needed — for a second client / automation):
python forge_service.py # or ./start_service.sh
# POST /build {goal, workdir?, levers?, budget?} → {session_id}
# GET /status/{id} · POST /answer|/review|/abort · GET /result/{id}forge_brain.py is a small hierarchical router. By default everything runs on your local model. Optionally, the few low-token roles where a 27B tends to hit a ceiling (PLAN / TESTGEN / CLARIFY / ESCALATE) can be routed to a "strong" model (Anthropic), while IMPL / EXPLORE / FIX stay local:
pip install anthropic
export ANTHROPIC_API_KEY=... # a real key; placeholders auto-fall-back to local
export FORGE_ALL_LOCAL=1 # force everything local (default behavior anyway)
export FORGE_ROLE_PLAN=strong # per-role override: local | strongIf anthropic isn't installed or there's no real key, strong roles silently fall back to local — so it never throws 401s and the "all-local" path is always intact. That local path is the whole thesis; the strong routing is just an optional ceiling-raiser.
forge_core/ Qt-independent engine (import this from anywhere)
orchestrator.py the state machine driving PLAN→TEST→BUILD→FIX
session.py re-entrant session (used by the HTTP service)
workers.py planner / test-gen / clarify workers
impl.py the red→green implementation loop + smoke driver
fix.py L3 targeted-reading fix loop
contract.py declared-interface contract (the single source of truth for names)
declaration.py interface declaration parsing
plan_linter.py rejects un-machine-checkable plans
test_linter.py rejects empty/degenerate generated tests
probe.py boundary probes
triage.py input triage
arbitration.py stuck-plateau arbitration
metrics.py JSONL event log (size-proxy vs convergence)
prompts.py role prompts
forge_brain.py hierarchical local/strong router (all-local by default)
code_forge_gui.py PySide6 GUI (thin skin over forge_core.orchestrator)
forge_probe_dialog.py probe-confirmation dialog
forge_service.py stdlib HTTP service (second client, zero deps)
Research-grade / MVP. It builds real, playable small programs from a one-line goal on a fully local 27B, and the human-in-the-loop verification gradient is wired end to end. Comments in the source are in Japanese (this was a personal research project); the code and this README are the public extraction. PRs and forks welcome.
「分解 → インターフェース宣言(契約)→ テストファースト → 赤緑ループ」で、ローカル27B にエージェント的コーディングをやらせるハーネス。コンテキスト長の拡張も、でかいGPUも、API課金も要らない。
ローカルのコーディングエージェントを作ると誰もが同じ壁にぶつかり、同じ2つのレバーに手を伸ばす — 「コンテキストを伸ばせ」「DGX Spark を買え」。CODE FORGE は 3つ目のレバー=構造 に賭ける:
- 親(PARENT)は生コードを一切読まない。 見るのはお題・分解・宣言インターフェース(クラス/メソッド/関数のシグネチャ)だけ。だから親のコンテキストはコード量に比例して膨らまない — これがローカルモデルを成立させる唯一で最大の設計判断。
- 名前の唯一の真実。 plan・test・実装がすべて同じ宣言インターフェース契約を参照するので、ローカルモデル定番の破綻=名前ドリフト(
is_collision→check_collision)が消える。 - テストファースト。ワーカーは
tests/に書けない。 ワーカーは自分が宣言して所有するファイルしか書けず、採点に使われるテストは編集不可。だから「GREEN」がチート(テスト書き潰し)でなく本物の意味を持つ。
compile < 起動(smoke) < unit緑(pytest) < 挙動が正しい
└─ 自動で守れる ─────────────────────────┘ └─ 人間 ─┘
機械が自動で守れるのは「起動して unit が緑」まで。一番上の段=挙動・手触りが本当に正しいか、は人間が審判で、ハーネスはそこに正直: 挙動修正で嘘の GREEN を出さず「適用した・挙動は▶RUNで確かめて」と返す。人間は欠陥のオラクル — 直し方でなく「どこがどう変か」を言えば、エージェントが場所を特定して直す。
ローカル Huihui-Qwen3.6-27B (Q5_K_M) ・24GB・ctx拡張なし/新ハードなしで、遊べるテトリス(logic エンジン+GameState+pygame front-end)を一気通貫で自走生成。人間が継いだのは最後の「実際に遊んで手触りが正しいか」だけ。最初の問い「DGX Spark やコンテキスト長が要るのか?」への答え=要らない、ハーネスで届いた。
27B を選ぶときのコツ: tool-call / 指示追従の生存はサイズでなく世代で決まる(Qwen3.5 以上は耐え、旧世代/小型の "uncensored" コーダーは形式が壊れた)。新しい世代を選ぶこと。
localhost:8000に OpenAI互換サーバー(llama.cppのllama-server想定、tool/--jinja対応で起動)。pip install -r requirements.txtpython code_forge_gui.py→ お題を入れて ▶ 分解してビルド → PLAN承認 → 赤緑スコアボード → ▶ RUN で遊んで判定。
ヘッドレスHTTPサービス(標準ライブラリのみ・PySide6不要): python forge_service.py
forge_brain.py は階層ルーター。既定は全ロールがローカル。 27Bが天井に当たる少数の低トークンロール(PLAN/TESTGEN/CLARIFY/ESCALATE)だけ強モデル(Anthropic)へ逃がすことも可能(ANTHROPIC_API_KEY と anthropic パッケージがある時のみ有効・無ければ静かにローカルへフォールバック)。全ローカル経路こそが主張の本体で、強モデルは任意の天井上げにすぎない。
MIT License · © 2026 muu