{"slug": "my-dashboard-said-four-plans-were-ready-to-dispatch-one-was", "title": "My Dashboard Said Four Plans Were Ready to Dispatch. One Was.", "summary": "A developer running a small Kubernetes platform staffed by AI coding agents found that a dashboard's \"ready to dispatch\" column falsely cleared three plans that would have had two agents editing the same files concurrently. The exporter behind the column ran on a hub machine that lacked the local dispatch ledger, so its file-overlap check saw zero open dispatches; a plan with no `## Files` section also passed the check, and an abandoned agent kept working for nearly three hours before exiting with a scope violation, corrupting the last-event state logic.", "body_md": "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.\n\nNothing 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.\n\nI 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`.\n\nTwo 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.\n\nThe 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\".\n\nThe 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.\n\nThe 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:**\n\ngo to each doc and pushed.\n\nOn 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.\n\nWhen 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?\n\n**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.\n\n**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.\n\n**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:\n\n| Try | Ledger | \n|---|---|\n| r1 | `start` 11-10 04:17 →`abandon` 11-11 04:48 | \n| r2 | `start` 11-11 04:49 →`abandon` 07:44 →`agent_exit`**10:31** | \n| r3 | `start` 07:45 →`abandon` 09:01 | \n| r4 | `start` 09:02 →`land` 10:05 | \n\nAbandoning 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.\n\nAll three bugs have the same shape: a missing input was read as \"nothing\", not as \"I don't know\". \"No ledger\" became\n\n\"no open dispatches\". \"No file list\" became \"no files\". \"Event after abandon\" became \"reopened\".\n\nNot by reading the dashboard. By disagreeing with my coordinator.\n\nWhen the coordinator first saw the closed task in the laptop output, it proposed abandoning \"four stale v1.43.15 rows\"\n\nto clean up. I replied that I didn't see any stale rows on the dashboard. I was right: r1 and r3 were abandoned, r4\n\nhad landed, r2 was abandoned. There was nothing to abandon. The agent had read the exporter's output and believed it,\n\ninstead of reading the ledger events behind it. When it did read them, the late `agent_exit` was right there.\n\nSo 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.\n\n`**Dispatch:**` line the exporter reads, so a decision I've made shows up as made.`## Files` section, so it shows as waiting on the dispatch it shares files with.`land` and `abandon` as final, and show a missing file list as \"unknown\" instead of \"no overlap\".`open` list before every dispatch. It already did; that's why nothing went wrong.\nThe 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.\n\n*What does your \"ready\" column show when its data source is missing?*", "url": "https://wpnews.pro/news/my-dashboard-said-four-plans-were-ready-to-dispatch-one-was", "canonical_source": "https://dev.to/wilddog64/my-dashboard-said-four-plans-were-ready-to-dispatch-one-was-3mbe", "published_at": "2026-11-14 14:18:02+00:00", "updated_at": "2026-11-14 14:21:08.471228+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "mlops", "developer-tools"], "entities": ["Grafana", "Kubernetes"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/my-dashboard-said-four-plans-were-ready-to-dispatch-one-was", "markdown": "https://wpnews.pro/news/my-dashboard-said-four-plans-were-ready-to-dispatch-one-was.md", "text": "https://wpnews.pro/news/my-dashboard-said-four-plans-were-ready-to-dispatch-one-was.txt", "jsonld": "https://wpnews.pro/news/my-dashboard-said-four-plans-were-ready-to-dispatch-one-was.jsonld"}}