Skip to content

Meet Tempo: when the orchestrator becomes a Go binary

Most of the posts in this series describe the orchestrator that builds this project as if it were finished. It is not. While it has been running -- cutting phases into tickets, spawning disposable clones, merging reviewed PRs -- a second version of it has been quietly taking shape next to it, ticket by ticket. That second version is a Go command called Tempo, and it lives in the Tempo implementation in the repo right now.

I want to write about Tempo precisely because it is unfinished. A rewrite caught in the middle is one of the most honest teaching artifacts an AI-driven project can offer. It forces three questions into the open that a polished system hides: what do you keep, what do you replace, and how do you replace the load-bearing part of a working system without lying to yourself about how far you have actually gotten? If you are learning to build this way, the in-flight rewrite is not a detour from the lesson. It is the lesson.

This post is a companion to the orchestration lifecycle, which describes the loop Tempo is re-implementing, and to when a stage fails: what is mechanical and what is the LLM?, which describes the failure-recovery behavior the rewritten spine is responsible for getting exactly right. Read either of those first if you want the full picture of what is being ported. Here I am going to stay on Tempo: why it exists, how it is shaped, and the honest edge of how far the port reaches today.

The name is the first design decision

Before any code, Tempo made a decision that has nothing to do with Go and everything to do with working alongside an AI: it picked a deliberately new name.

The existing orchestrator is driven through shell wrappers named dev.sh. There are two of them -- a project-side one and an orchestrator-side one -- and the developer-facing surface on top of those is a set of /dev-* slash commands plus the orchestration control plane control plane that maps a ticket to the right engine and model. Useful, but the overlapping dev naming has a cost that only shows up once an AI is the one reading your instructions. In a chat session, "run dev" or "the dev script" is ambiguous: which wrapper, which responsibility? An agent that guesses wrong wastes a turn, or worse, runs the wrong thing.

So the rewrite does not inherit the dev name. As the Tempo design memo states directly in its "Name" section, the name is deliberately separate from the existing wrappers, to give the future orchestration layer one clear, unambiguous command name while the current wrappers keep owning today's workflow. That is a small choice with a large lesson behind it: in AI-assisted development, naming is not cosmetic. A name an agent cannot disambiguate is a latent bug. Spending a naming decision up front to remove that ambiguity is cheaper than paying for it on every future session.

Tempo is a real Go module, not a sketch. Its the Tempo implementation declares module github.com/protus75/tempo, and the CLI itself lives in the Tempo implementation. Behind it sit roughly three dozen merged tempo-* tickets and a long, candid the Tempo design memo that reads more like a design memo than a usage page. This is built, not proposed.

The central lesson: rewrite the spine, keep the proven engines

Here is the architectural idea worth carrying away from this whole post, because it generalizes far beyond this one project.

A rewrite tempts you toward two extremes. One is "change nothing meaningful" -- a mechanical transliteration that buys you nothing. The other is "rewrite everything" -- a from-scratch big bang that throws away years of hard-won correctness along with the code you actually wanted to replace. Tempo takes neither. It draws a line through the system: it rewrites the orchestration spine in Go, and it keeps the proven analytical engines as vendored Python, called from the Go shell.

The spine is the part that has to be exactly repeatable: control flow between stages, lifecycle state transitions, the disposable-clone lifecycle, the cross-ticket locks and merge mutex. That is what the lifecycle post calls the deterministic harness -- the dumb, legible code that an LLM is deliberately not allowed to be. It is a good fit for a typed, compiled language, and it is the part Tempo is genuinely rewriting.

The engines are different. The codemap analyzer, the DuckDB-backed history index, and the lint gates are not control flow -- they are accumulated, tested, correct analytical code. Rewriting them in Go would mean re-deriving a lot of subtle behavior for no real gain. So Tempo keeps them as Python and reaches into them from Go. You can see the seam in the repo: the Tempo implementation, the Tempo implementation, and the Tempo implementation's table of gate modules are all dispatched as Python through one small subprocess helper, the Tempo implementation. The Go layer assembles arguments and environment; the Tempo implementation resolves a Python interpreter and runs the vendored module, capturing its output. Go decides what and when; Python does the analysis.

There is one more detail in that seam that is itself a lesson in discipline. The Python runtime Tempo uses is a tempo-owned virtual environment, and pyrun.go actively rejects the surrounding repository's venv/ -- it refuses to fall back to the boardgame project's interpreter and instead stops with guidance to set up Tempo's own. That sounds fussy until you have watched a tool silently pick up the wrong interpreter, run against the wrong dependencies, and produce results that match nobody's environment. Tempo makes its runtime boundary explicit and enforces it in code, so "which Python ran this?" always has a single, deterministic answer. Keep the proven engine, but keep it on your runtime, not whichever one happened to be on the path.

If you are building something like this, that is the transferable move: find the line in your system between the part that must be deterministic and repeatable, and the part that is accumulated domain logic you have already gotten right. Rewrite across the first. Vendor across the second. A rewrite that respects that line is re-architecture; one that ignores it is a gamble dressed up as progress.

What actually got ported: a batch of tickets, not a big bang

It would be easy to describe Tempo as "the orchestrator, rewritten in Go" and leave it there. That phrasing hides the part a learning developer most needs to see: it did not happen in one heroic commit. It happened as the tempo-orchestrator-* PORT batch -- a stack of individually scoped, individually reviewed tickets, each carrying one slice of the spine across.

Reading the merged ticket stems is itself an outline of how you decompose a rewrite. There is a ticket for the dispatcher skeleton, one for the config and project model, one for the state store, one for the ticket and backlog model, one for the clone and git layer, and separate tickets for the stage verbs -- the ticket/implement stage, the review preflight stage, the ship-and-merge stage. The analytical engines came across as their own vendoring tickets: the codemap engine, the history index, the lint gates. And -- the part I will come back to -- there is a ticket for a parity harness, the thing that checks the port against the original.

The discipline here is the same discipline the orchestration-lifecycle post describes for ordinary feature work, turned on the orchestrator itself: cut the big thing into contract-shaped tickets, give each a bounded scope, let each land and be reviewed on its own. A rewrite is just a phase whose subject happens to be the build system. Treating it as a batch -- not a single undifferentiated rewrite an agent churns on for a day -- is what keeps each piece reviewable and keeps a bad slice from sinking the good ones. "It's a rewrite" is not an excuse to abandon decomposition; it is the place decomposition matters most.

The honest edge: read-surface parity, so far

Now the part that makes this a teaching artifact instead of a press release.

It is tempting, when a rewrite reaches "it runs and the output looks right," to call it done. Tempo's own README refuses to let you. The way the port checks itself against the original is a parity harness: it runs the frozen original orchestrator and Tempo over identical fixture inputs and compares their output. But it compares a specific, narrow slice of output -- the read surfaces, like the backlog-list and status views that report state without changing it. And the README says, in as many words, that the parity report also lists the lifecycle surfaces that are not yet covered -- the mutating ticket -> review -> ship -> merge execution path -- so that a future cutover expands coverage deliberately instead of mistaking a read-surface check for complete orchestrator parity.

Sit with how careful that is. The system has a check that could be quietly oversold as "Tempo matches the original," and instead the check is built to announce its own blind spots. Matching the read surfaces means the two systems agree on what they report. It does not mean they agree on what they do to a ticket as it moves through review and ship and merge -- the irreversible, state-mutating part where a divergence actually costs you a corrupted merge. Full lifecycle parity is deferred, and the project says so out loud rather than letting an impressive-looking diff imply more than it proves.

So, stated plainly: Tempo has not replaced the existing dev.sh dispatcher. The current orchestrator still runs every real ticket, including the one that produced this very post. Tempo is a real, building, in-flight rewrite with read-surface parity demonstrated and full lifecycle parity still ahead. The work of proving the mutating path -- and the careful, verb-by-verb way you would have to earn confidence in a cutover -- is its own future story, which I will tell in a planned Track E follow-up, From read-surface parity to a lifecycle cutover.

That honesty is not a disclaimer tacked onto the end. It is the most important thing the rewrite has to teach. Anyone can show you a green check. The skill -- the part worth learning if you want to build this way -- is knowing exactly what your green check does and does not cover, and being willing to write the gap down where everyone can see it. A parity harness that brags is a liability. A parity harness that enumerates what it has not yet checked is an asset, because it turns "are we done?" from a vibe into a list.

What to take from a rewrite caught in the act

Three things, if you are new to building with AI and watching this one over its shoulder.

Name things so an agent cannot misread them. Tempo spent its first decision on a name, because ambiguity an agent has to guess through is a recurring tax, not a one-time annoyance.

Find the deterministic/analytical line and rewrite across it, not through it. The Go shell over vendored Python engines, with a runtime boundary enforced in code, is what lets the rewrite move the part that benefits from a rewrite while keeping the part that was already correct.

Let your verification declare its own blind spots. Read-surface parity is real progress and an honest stopping point, named as such, with the uncovered lifecycle path written down instead of glossed over.

A finished system can hide all three of these behind a clean surface. A rewrite caught in the middle cannot, which is exactly why it is the better teacher.


Or: subscribe for the Track E follow-up on taking the port from read-surface parity to a full lifecycle cutover.