v0 implementation plan: static routing, health, streaming proxy
For the implementing model: do not work from this file. The owner gives you one task file at a time (
01-…to05-…). This file is the index for the owner and the reviewer.
Goal: a crossbar binary that reads a TOML file, polls each host's /health and
/v1/models, and forwards /{route}/v1/* to the first healthy host on that route's preference
list, streaming responses chunk by chunk, marking a host down when a proxied request fails, and
showing the health table at /_crossbar/hosts. This is PLAN.md §10 v0: no leases, no SQLite,
no /slots, no queueing.
Architecture: four small packages under internal/ — config (TOML + validation), health
(poller + table), proxy (route → host → httputil.ReverseProxy), admin (read-only JSON) —
and two binaries under cmd/: crossbar and the given fakeupstream. Behaviour is pinned by
the tests in _files/, which were run against a private reference implementation at each task's
end state; the reference is not in this repository and the implementer must not look for it.
Tech stack: Go 1.26, standard library, github.com/BurntSushi/toml v1.6.0. Nothing else.
Global constraints
- Everything in
AGENTS.md. - Branch
v0. One task, one fresh OpenCode session, one commit.gofmt -wbefore the gate. - No source file over 400 lines. Library code never panics on input, never logs a body.
- The given files are copied and never edited. If a copied test fails, the code is wrong.
Tasks
| # | File | Delivers | Tests that define it |
|---|---|---|---|
| 01 | 01-module-gate-config.md |
go.mod, the gate, internal/config |
internal/config/config_test.go + testdata/ |
| 02 | 02-health.md |
internal/health: poller and table |
internal/health/health_test.go |
| 03 | 03-proxy.md |
internal/proxy: routing reverse proxy |
internal/proxy/proxy_test.go |
| 04 | 04-admin-main.md |
internal/admin, cmd/crossbar, the given cmd/fakeupstream |
internal/admin/admin_test.go, a start/stop check |
| 05 | 05-smoke-readme-deploy.md |
tools/smoke.sh run, README.md, deploy/crossbar.service |
make smoke |
At the end: make gate prints gate: ok, make smoke prints smoke: ok (stream spread N ms).
For the owner: running a task
From a clean checkout on master:
tools/run-plan.sh docs/plans/v0 # all tasks, each in a fresh `opencode run` session
tools/run-plan.sh docs/plans/v0 03 # from task 03
The default model is llama.cpp/ornith-1.5-35b-a3b (override with CROSSBAR_MODEL). The driver
stops at the first task that does not end with a commit, a clean tree and a done row in
docs/implementer-log.md. Keep the OpenCode TUI closed while it runs.
For the reviewer: after task 05
git log --oneline master..v0: five commits with theImplemented-Bytrailer.- Copied files are unchanged:
for f in $(cd docs/plans/v0/_files && find . -type f); do cmp "docs/plans/v0/_files/$f" "$f"; done git diff master..v0 --stat -- PLAN.md AGENTS.md docs/plansis empty.make gateandmake smokeon straylight.- Read every source file against its task. Probe from outside with inputs the tests do not
contain: a route with query strings and encoded characters, a host that hangs after headers, a
3 MB request body,
/_crossbar/hostswhile a poll is in flight, SIGTERM during a stream. - Write findings under "Reviews" in
docs/implementer-log.md.
Changes during the run
- 2026-09-25, task 01, first attempt: the given files lived under
docs/plans/v0/files/, which Go's./...pattern includes, sogo vet ./...in the gate tried to build the tests of tasks 02–04 against packages that did not exist yet. Ornith diagnosed this correctly and stopped investigating instead of editing the Makefile. Fixed by moving the directory to_files/(directories starting with_are ignored by the go tool); every task file updated. Task 01 restarted from a clean tree.