v0.6.0
Breaking. Two configurations that started a server on 0.5.9 now refuse to start. Both failures name the offending value and exit from main(), rather than surfacing later as a traceback or a confusing runtime error. Read the two sections below before upgrading a running server.
Agent ids are validated where an agent arrives, not where one is written. An agent id is a YAML mapping key, so it is whatever the file says. The regex describing its shape lived in the dashboard — one of the two ways an agent arrives. The other way, an operator editing config.yaml or the managed agents.yaml by hand, reached every consumer without passing it: a scratch directory's name, the target_agent enum in the published tool schema, and a redirect header. The check now runs in load_consult_config, which is the layer where a mapping key becomes an id. An id must start with a letter or digit and use only lowercase letters, digits, dots, dashes and underscores, up to 64 characters; anything else is refused at startup with a message naming the key. Lowercase is enforced rather than normalized, because Foo and foo can be distinct keys — including across the two files that merge — so lowercasing one could collide with or silently become the other.
The same fix corrects an re.match that was never a whole-string test: $ also matches just before a trailing newline, so an id ending in one passed a check that was supposed to prevent exactly that. ConsultDashboard.delete interpolated the posted id straight into a Location header, which send_header writes verbatim. That redirect now percent-encodes the id rather than refusing it — the managed file is re-read on every request, so a key hand-edited in after boot is rendered with a delete button beside it, and refusing the id would leave the only control that can remove it unable to. Refusing the key at load is what stops it surviving a restart.
A roots entry that is not a directory on this machine fails at startup. consult.review.roots and consult.workflow.roots were checked only when a request used them, so a typo started the server, got the tools advertised, and reported itself on the first call that needed it — for review, after a reviewer had already been spawned. This is a hard failure rather than a dropped entry: an allowlist quietly shorter than the file says reads fewer trees than whoever wrote it thinks. The cost is one-sided and real — a machine that has not cloned one of several configured trees cannot start until the entry goes or the directory arrives. WorkflowConfig._expand_roots also gains the absolute-path check it never had; a relative root was being resolved against the server's own working directory, which is wherever the client happened to spawn it. The request-time checks stay, since a directory can be deleted while the server runs. config.example.yaml now ships roots: [] in both blocks instead of naming ~/src.
Release workflow — a comment correction only, no change to what the step does. It said the dist check "reads the files"; it globs dist/, counts two entries, and matches their names against the tag. Nothing opens a distribution.
Full changelog: v0.5.9...v0.6.0