v0 plan: AGENTS.md, gate, five task files with given tests, run-plan driver

Tests were run against a private reference implementation: gate ok after every
task in order, smoke ok (stream spread ~1000 ms). The reference is not in the
repository.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 01:51:53 -07:00
co-authored by Claude Fable 5.1
parent b52126ba0b
commit 89e47d83f8
25 changed files with 1847 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
# v0 task 01: module, gate and the config package
**Branch:** `v0` (create it: `git switch -c v0`; `git status --short` must be empty first, otherwise stop)
**Commit subject:** `Add the Go module, the gate and the config package`
## Goal
Create the Go module and the gate every later task must pass, and write `internal/config`: the
TOML file crossbar starts from, with defaults and validation. At the end `make gate` prints
`gate: ok`.
## Context
crossbar is an HTTP reverse proxy in front of several `llama-server` routers ("hosts"). A client's
identity is the first segment of its request path (its "route"); each route has an ordered list
of hosts to try. The config file names the hosts, which models each serves, and the routes. It
must be strict: a misspelt key or a route naming a host that does not exist is an error at start,
not a surprise at 3 a.m. The one dependency is `github.com/BurntSushi/toml` v1.6.0; `go.sum` for it
is given.
## Files
- Copy (never edit afterwards): `Makefile`, `scripts/check-lines.sh`, `go.sum`,
`internal/config/config_test.go`, `internal/config/testdata/` (5 files)
- Create: `go.mod`, `internal/config/config.go`
- Modify: `docs/implementer-log.md`
## Interfaces
Produces, in `internal/config/config.go`, package `config`:
```go
// Duration is a time.Duration that TOML reads from a string such as "60s" or "30m".
type Duration struct{ time.Duration }
func (d *Duration) UnmarshalText(text []byte) error // time.ParseDuration
type Model struct { Parallel int `toml:"parallel"` }
type Host struct {
BaseURL string `toml:"base_url"`
Weight float64 `toml:"weight"`
Models map[string]Model `toml:"models"`
}
type Route struct {
Hosts []string `toml:"hosts"`
DefaultModel string `toml:"default_model"`
}
type Config struct {
Listen string `toml:"listen"`
PollInterval Duration `toml:"poll_interval"`
QueueMax int `toml:"queue_max"`
Hosts map[string]Host `toml:"hosts"`
Routes map[string]Route `toml:"routes"`
}
// Error is a validation error naming the field it is about.
type Error struct { Field, Msg string }
func (e *Error) Error() string // "config: " + Field + ": " + Msg
const (
DefaultPollInterval = 60 * time.Second
DefaultQueueMax = 8
MinPollInterval = time.Second
)
func Load(path string) (*Config, error) // open, then Parse
func Parse(r io.Reader) (*Config, error) // decode, defaults, validate
func (c *Config) Serves(host, model string) bool // host exists and lists model
func IsError(err error) (*Error, bool) // errors.As on *Error
```
Rules the tests check:
1. **Decoding.** `toml.NewDecoder(r).Decode(&c)`. A TOML syntax error is returned as
`fmt.Errorf("config: %w", err)` — it is *not* an `*Error`. If `md.Undecoded()` is non-empty,
the error is `&Error{Field: <first undecoded key, keys sorted>, Msg: "unknown key"}`. `Load`
wraps an open failure the same way (`config: …`).
2. **Validation, in this order, first problem wins.** Every problem is an `*Error` with exactly
this `Field`:
- `listen`: required; must be `host:port` (`net.SplitHostPort`); the host part must not be
empty and must not be an unspecified address (`0.0.0.0`, `::`).
- `poll_interval`: `0` (absent) becomes `DefaultPollInterval`; less than `MinPollInterval` is
an error.
- `queue_max`: `0` becomes `DefaultQueueMax`; negative is an error.
- `hosts`: at least one. Then for each host, **in sorted name order**:
- `hosts.<name>.base_url`: `url.Parse` must succeed, scheme `http` or `https`, non-empty
host, no query, no fragment. A trailing `/` is trimmed (`strings.TrimRight(url, "/")`)
and the trimmed value stored.
- `hosts.<name>.weight`: `0` becomes `1`; negative is an error.
- `hosts.<name>.models`: at least one. For each model in sorted order,
`hosts.<name>.models.<model>.parallel`: `0` becomes `1`; negative is an error.
- `routes`: at least one. For each route in sorted name order:
- `routes.<name>`: the name must match `^[a-z0-9][a-z0-9-]*$`.
- `routes.<name>.hosts`: at least one; every entry must be a configured host; no host twice.
- `routes.<name>.default_model`: if set, at least one of the route's hosts must list it.
3. Defaults are written back into the returned `Config` (the tests read `Weight == 1`,
`Parallel == 1`, `PollInterval == DefaultPollInterval` after parsing files that omit them).
4. Nothing here panics on a bad file: every map lookup and slice access is on data you checked.
## Steps
- [ ] **1. Branch and copy.**
```sh
git switch -c v0
mkdir -p scripts internal/config/testdata
cp docs/plans/v0/files/Makefile docs/plans/v0/files/go.sum .
cp docs/plans/v0/files/scripts/check-lines.sh scripts/
cp docs/plans/v0/files/internal/config/config_test.go internal/config/
cp docs/plans/v0/files/internal/config/testdata/*.toml internal/config/testdata/
```
Read `Makefile` and `internal/config/config_test.go`. The test names every rule above.
- [ ] **2. Write `go.mod`** with exactly this content:
```
module git.wntrmute.dev/kyle/crossbar
go 1.26
require github.com/BurntSushi/toml v1.6.0
```
Then `go mod download github.com/BurntSushi/toml` (this needs the network once) and
`go mod verify`. Expected: `all modules verified`.
- [ ] **3. See the test fail.** `go test ./internal/config/`. Expected: it does not compile
(`no Go files` or undefined names).
- [ ] **4. Write `internal/config/config.go`.** Run `gofmt -w internal/config/`.
- [ ] **5. See the test pass.** `go test ./internal/config/`. Expected: `ok`.
- [ ] **6. Run the gate.** `make gate`. Expected last line: `gate: ok`.
- [ ] **7. Log and commit.** Add your row to `docs/implementer-log.md` (Task `v0/01-module-gate-config`).
```sh
git add go.mod go.sum Makefile scripts/check-lines.sh internal/config docs/implementer-log.md
git commit
```
## Done when
- `go test ./internal/config/` is `ok`; `make gate` prints `gate: ok`.
- `cmp internal/config/config_test.go docs/plans/v0/files/internal/config/config_test.go` prints nothing.
## Stop and report if
- `go mod download` cannot fetch the module (no network): stop, log `stopped`.
- A test expects a `Field` you cannot produce under the rules above: stop; do not edit the test.