Files

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-… to 05-…). 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 -w before 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

  1. git log --oneline master..v0: five commits with the Implemented-By trailer.
  2. Copied files are unchanged: for f in $(cd docs/plans/v0/_files && find . -type f); do cmp "docs/plans/v0/_files/$f" "$f"; done
  3. git diff master..v0 --stat -- PLAN.md AGENTS.md docs/plans is empty.
  4. make gate and make smoke on straylight.
  5. 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/hosts while a poll is in flight, SIGTERM during a stream.
  6. 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, so go 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.
  • 2026-09-25, task 04: the step-5 check timeout --signal=TERM 3 bin/crossbar …; echo $? expected exit=0, but GNU timeout reports 124 whenever it had to signal the child, whatever the child's own exit status. Ornith noticed, sent SIGTERM directly, confirmed exit 0 that way, logged the deviation and finished. Task-text fault (case: my task, not the model); fixed with --preserve-status.