My Dashboard Said Four Plans Were Ready to Dispatch. One Was.
My Dashboard Said Four Plans Were Ready to Dispatch. One Was. My Grafana dashboard listed five plans for the next release. Four of them said ready to dispatch. One of them really was. The other three would have put two
My Dashboard Said Four Plans Were Ready to Dispatch. One Was.
My Grafana dashboard listed five plans for the next release. Four of them said ready to dispatch. One of them really was. The other three would have put two AI coding agents to work editing the same files at the same time.
Nothing crashed, and nobody dispatched the wrong thing. I caught it the way you usually catch a dashboard lying: it disagreed with something else I trusted. This is how a column that only exists to say "safe to start" ended up saying it for work that wasn't.
The setup
I run a small Kubernetes platform from one git repo, built by me and a group of AI coding agents. Work comes in as plans: a markdown doc per feature with a spec, the files it will change, and acceptance tests. When a plan is approved, a coordinator (another AI agent) hands it to a coding agent in its own git worktree. That's a dispatch. Every dispatch appends events to a ledger file: start, agent_exit, land, abandon.
Two rules keep the agents out of each other's way: at most three dispatches open at once, and one dispatch per file. Two agents editing the same Makefile in parallel means a merge conflict at best and a silent revert at worst.
The Plans table on my dashboard has a Blocked by column that applies those rules for me. For each plan it checks whether the plan is approved, whether the release branch exists, whether its dependencies landed, and whether any open dispatch holds one of its files. If nothing blocks it, the column says "ready to dispatch".
First, everything needed my decision
The morning started the other way round. I had approved all five plans, and the dashboard still showed "⚠ needs your decision" under Dispatch approval for every one of them.
The exporter behind the table reads a Dispatch: line at the top of each plan doc. I had given my go in a chat with the coordinator, and it had written that go into a grooming note, not into the five docs. No line meant "unknown", and "unknown" is displayed as "needs your decision". Fair enough. The coordinator added Dispatch:
go to each doc and pushed.
On the exporter's next run the column flipped. Four plans: ready to dispatch. One: intents need approval. That looked like progress. It was the second bug.
Three ways to say "ready" when the answer is "unknown"
When the coordinator ran the same exporter on my laptop, it gave a different answer. Two plans now read waits on test-hygiene-batch (Makefile). The test-hygiene batch was a running dispatch, and both plans edit the Makefile. So why did my dashboard say ready?
1. The hub can't see the ledger. The exporter runs on the hub machine, on a schedule. The dispatch ledger lives in a local file on my laptop, where the agents are started. On the hub, the file doesn't exist, so the overlap check finds zero open dispatches and every plan passes. The plan docs come from git, which both machines have; the dispatch state comes from a file only one machine has. Any column that mixes the two is right on one machine and wrong on the other.
2. A plan with no file list overlaps with nothing. One plan, which adds an alert for commits waiting to be promoted, edits the same two scripts as the other running dispatch. Even on my laptop it said "ready". Its spec described the files in prose but had no ## Files section, and the check reads only that section. No section, no files, no overlap.
3. An abandoned agent finished its work anyway. The laptop run also said two plans were waiting on a task from the previous release, one I had closed. Here is that task's history in the ledger, four tries at one fix:
| Try | Ledger |
|---|---|
| r1 |
start 11-10 04:17 → abandon 11-11 04:48 |
| r2 |
start 11-11 04:49 → abandon 07:44 → agent_exit 10:31
|
| r3 |
start 07:45 → abandon 09:01 |
| r4 |
start 09:02 → land 10:05 |
Abandoning r2 wrote an event. It didn't stop the agent. That agent kept working for almost three hours after it was abandoned, then exited with a scope violation. The exporter takes the last lifecycle event as the state, so a late agent_exit after an abandon reads as "still running". The dispatch tool itself treats land and abandon as final, which is why its own open command, and the agent dashboard, never showed the task. Two readers of the same append-only log, with two different ideas of what "closed" means.
All three bugs have the same shape: a missing input was read as "nothing", not as "I don't know". "No ledger" became
"no open dispatches". "No file list" became "no files". "Event after abandon" became "reopened".
How I caught it
Not by reading the dashboard. By disagreeing with my coordinator.
When the coordinator first saw the closed task in the laptop output, it proposed abandoning "four stale v1.43.15 rows"
to clean up. I replied that I didn't see any stale rows on the dashboard. I was right: r1 and r3 were abandoned, r4
had landed, r2 was abandoned. There was nothing to abandon. The agent had read the exporter's output and believed it,
instead of reading the ledger events behind it. When it did read them, the late agent_exit was right there.
So the coordinator and the dashboard made the same mistake from the same input, and the thing that broke the tie was a human glancing at a second view that computed "open" differently.
What changed
- The five plan docs now carry the
**Dispatch:**line the exporter reads, so a decision I've made shows up as made. - The alert plan has a
## Filessection, so it shows as waiting on the dispatch it shares files with. - The three causes are captured in one bug doc for the next triage: ship the open-dispatch list to the hub (or push it as a metric from the laptop), treat
landandabandonas final, and show a missing file list as "unknown" instead of "no overlap". - Until that lands, I read "ready to dispatch" as "approved", not as "free to start", and the coordinator checks the dispatch tool's own
openlist before every dispatch. It already did; that's why nothing went wrong.
The rule I'd keep: a "safe to proceed" signal must fail closed. If the check can't see its input, the answer is "unknown", in a color you can't mistake for green. A dashboard that says "ready" when it means "I couldn't tell" is worse than no dashboard, because you stop checking.
What does your "ready" column show when its data source is missing?
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.