The routing verdict that ended up in every draft

What do you get when your orchestrator and your content generator share the same output channel?

I run a dashboard that generates drafts by shelling out to claude p. My orchestrator policy, written in CLAUDE.md, prepends a routing verdict to every response: something like ▸ T0 · main thread · generate draft · interactive. The verdict is useful for me. At a glance I know what tier the request hit, whether it spun up a subagent, whether the free chain was healthy.

The problem is claude p does not know or care about the difference between metadata for the orchestrator and content for the user. It returns one string. So every draft coming out of the generation endpoint opened with that routing line. ▸ T0 · main thread ·... then a blank line, then the actual post. Fine in a terminal log where it blends with other orchestrator output. Not fine when the dashboard is displaying that draft for review and eventual publish.

I caught it during a dry run. The draft looked normal inside the generation log because the routing line looked like all the other orchestrator chatter. But when the dashboard rendered it, there it was at the top: internal scaffolding shipped as content.

The fix was a _clean() function. Pattern match on the routing verdict format ( followed by a tier identifier), strip any matching leading lines, trim whitespace. Four lines. Applied before any draft hits the database or the display layer.

It works. Drafts are clean. But the fix lands downstream of the actual problem, which is structural: the orchestrator writes its routing decisions into stdout, and content generation also writes into stdout. Same channel, different purposes. That mix was always going to cause this.

The tradeoff in choosing claude p as the interface is simplicity over separation. You shell out, you get text back, you parse it. No protocol, no envelope format, no headers. The routing metadata I layered on top is just more text, with no way to signal that this is control plane output and not content. _clean() is the pragmatic answer to that gap.

It is also a band aid. If the orchestrator changes its verdict format, the stripping logic breaks. If a draft ever legitimately opens with (unlikely but not impossible), it strips content it should not. Fragile by design.

What I would do differently from the start: emit routing verdicts to a separate channel. Stderr, a structured log file, a dedicated endpoint the dashboard polls independently from the content stream. Keep the content channel clean by construction, not by post processing. Then there is no stripping logic at all, no regex to maintain, no edge cases to defend against.

This is a small bug but it surfaces something that shows up constantly in agentic pipelines. The model is generating content. The orchestrator is generating instructions. They are both talking through the same pipe. The more layers of orchestrator logic you add, the higher the probability that something meant for the control plane ends up in the output.

_clean() is the right call for today. Four lines beat a protocol redesign on a dashboard that is still being built. But the right long term answer is a protocol. Content out on one channel, metadata out on another. The shell interface does not give you that for free, which means you build the separation yourself or keep patching the leak.

Write a comment