The chart room
How I keep twenty-odd projects, five machines, and two AI agents in sync from a single dashboard built entirely out of plain Markdown and YAML files. The folder layout, why every status check is recomputed live instead of stored, and the plain-text handoff format that lets two agents continue each other's work.
Too many hulls, one clipboard
Some months back I had a fleet. Not ships. Projects: web apps, a couple of game backends, a documentation site, a few things that were live and earning their keep and a few that had run aground and I had not yet decided whether to refloat or scuttle. The better part of twenty of them, most built alongside an AI agent, either Claude or Codex, and every one of them running on the same handful of machines.
A fleet needs the same few things kept true across every hull, or it stops being a fleet and becomes a scatter of boats. Mine needed:
- The same standard of seaworthiness. Every project in version control, with a remote so a lost disk is an inconvenience and not a funeral. A written spec. Enough context checked in that a fresh agent could pick up the work without me narrating the last three months.
- The same way of putting to sea. A predictable deploy, the same shape of container, the same reverse proxy in front, so that shipping a change to one project did not mean relearning how that particular one was wired.
- The same way of handing over the watch. When an agent ran out of context partway through a job, the next session needed to know what was actually finished and what had only been attempted.
I was doing all of it by hand. Rowing out to each hull in turn with the same clipboard, checking the same boxes, rowing back. It worked in the sense that nothing sank. It did not work in the sense that it ate an afternoon a week, and it leaked constantly. A project would drift out of standard and I would not notice for a fortnight. An agent would start work against a stale note and redo something that was already done. I would go to deploy a small fix and have to remember which of five slightly different procedures this one used.
The clipboard does not scale. At some point you need a room you can stand in and see every deck at once.
So I built Control Room
Control Room is a single web page I keep open on a second monitor. It lists every project and every machine, tells me which ones are healthy, which have drifted out of standard, and where an agent should pick the work back up.
It is built around one rule, and the rule shaped everything else: every fact it knows is a plain Markdown or YAML file on disk. No database. The test I held every feature to was “could I do this with a text editor alone?” If the answer was no, the design was wrong.
The reason is not nostalgia for text files. It is that more than one kind of
crew has to read and write this. Claude does. Codex does. I do, sometimes
straight in the editor. A folder of .md and .yml files means every one of
them can take part with no integration work at all. A database would mean
picking one way in and building a door in front of it that everything else has
to learn to open.
The rest of this is a walk through each part of the room, and for each one, why it is there and how it works.
Overview: the harbour master’s board
Why it is here. The first question every morning is “is anything on fire?” I wanted that answered in one glance, before coffee, without clicking into anything.
How it works. The Overview reads every project’s project.yml and shows
three things per row: its real-world status, how many required standards it is
currently failing, and how stale its last handoff is. A project that is Live,
fully in standard, and was checkpointed an hour ago is a quiet grey line. One
that is failing the “has a remote” check, or whose handoff went stale mid-task
three days ago, is not. The board is sorted so the boats taking on water float
to the top.
Projects: every vessel stays legible
Why it is here. A fleet is not all the same. Some hulls are live and busy. Some are tied up at the dock, paused. Some I have quietly abandoned and just not admitted it. Some run at a far anchorage I can barely see. I did not want a system that forced the same checklist onto all of them and then drowned me in false warnings about the abandoned ones.
How it works. Each project is a folder: data/projects/<slug>/. Inside is
a project.yml with the metadata (name, lifecycle stage, which machine it runs
on, its repo, its containers) and up to four documents kept in the project’s own
voice: SPEC.md, STATUS.md, IDEAS.md, and HANDOFF.md.
The detail page for one project pulls together everything I used to gather by hand:
- Is it actually running? Read live from the Docker socket on the machine it is hosted on.
- Repo facts. Last commit, how many uncommitted files, whether it has a remote. Read straight from the working tree, which is mounted into Control Room read-only.
- Computed standards. Covered next.
- The documents, rendered. The spec and the status in one scroll.
- The latest handoff, and a clock on how stale it is.
A project changes lifecycle by editing one field in its project.yml. It never
moves folders. The record for a beached project is as real as the record for a
live one; it just answers fewer questions.
Standards: seaworthiness, checked not claimed
Why it is here. “Hold every project to the same bar” was the single most tedious part of the clipboard round, and the least reliable, because a checklist a person fills in starts drifting from the truth the moment nobody is looking.
How it works. A file, standards.yml, lists the checks: has a git repo, has
a remote, has agent context, has a spec, committed in the last thirty days.
Every check is a fixed, named function that re-derives its answer from the
actual state of the world each time it is asked. “Has a remote” runs
git remote. “Is it running” asks the Docker socket. “Has a spec” looks at the
filesystem. There is no field anywhere that a person or an agent can tick.
The other half of honest checking is refusing to pretend. A check can come back pass, fail, not applicable (a vendored third-party service is not expected to have a spec), unknown (the host was unreachable), or stale. Those are five different answers and the page shows them as five different things. A grid of green that includes three “I could not actually check this” cells dressed up as passes is worse than no grid.
Planning: ideas need their own water
Why it is here. Half-formed ideas kept washing into the real project list and cluttering it. An idea is not a project. It has no repo, no container, no standard to meet. It needs somewhere to sit and be thought about without pretending to be a ship.
How it works. A parallel set of folders, planning/IDEA-N/, each with a
short task.yml and a NOTES.md. An idea moves along a track: idea, then
planning, then ready, then graduated. When it graduates, I do not move any
files. I edit the stage in place and fill in the host, path and repo it now
has. The planning note stays as the record of where the project came from.
Servers: the near deck and the far anchorages
Why it is here. “What is actually running, and is a disk about to fill”
should not require me to SSH around five machines and run df on each one.
How it works. There are two cases.
Caspar, the machine that does most of the work, is where Control Room itself
runs. It reads that host directly: the Docker socket for every container, and a
fixed read-only script for disk, memory, load and docker stats. Live, every
few seconds.
The far hosts, lighthouse and the two production boxes, are reached through a single dedicated SSH key that can only ever run one read-only reporting script and nothing else. No shell, no file writes, no reaching further into the network. That key, and how it was locked down, is its own note. The data from those hosts is a snapshot from the last time the page asked, not a live feed, and the page says so rather than pretending otherwise.
Handoff: the log the last watch left
Why it is here. This is the one that actually hurt. An agent session would hit its context limit or its usage limit partway through a job, and the next session, sometimes a different agent entirely, had no reliable way to tell what had been finished from what had only been tried. A log that says “should work” when the truth is “untested” is worse than no log, because it invites the next watch to build on sand.
How it works. Every project folder can carry a HANDOFF.md: an append-only
log, newest entry on top, each entry a timestamp, which agent wrote it, what was
just done, the state of things said plainly, and the concrete next step.
Alongside it sits HANDOFF.yml, a bounded snapshot: the current checkpoint in
full, plus a short trail of the last few compressed to one line each, so a
hasty checkpoint cannot erase the trajectory that led to it.
Both files are only ever written through one small command line tool. You run
start when you begin, checkpoint after every verified milestone and at least
every half hour, and finish before a planned stop. On takeover you run
resume and read only that snapshot, never the whole history. The tool
serialises writers with a per-project lock and refuses a checkpoint from an
agent that is not the current owner. That is what stops two sessions quietly
writing over each other.
What makes it work is the discipline, more than the mechanism. A checkpoint has
to separate verified from attempted, include the actual test output, name the
blockers still open, and give exactly one next action. “Ran the suite, 14 pass,
2 fail on the timezone helper, next is toLocal()” is a good checkpoint. “Made
progress” is not.
What is verified vs assumed
Verified. This is the page I open first, and it is now the only place I go
to check the fleet. Claude and Codex have both handed work between each other
through the handoff files on real projects, including this website, with the
resume snapshot plus git status being enough to carry on. The standards grid
has caught two projects that had drifted off their remotes.
Assumed, and worth watching. That plain files stay comfortable past thirty
or forty projects before the folder itself gets unwieldy. That the computed
checks stay cheap as the fleet grows, rather than turning every page load into a
minute of git and Docker calls. That the half-hour checkpoint cadence is the
right one and not just the first one I picked. It has been a week. None of those
has bitten yet, which is not the same as none of them being real.
The crew, such as it is
I said at the top that I have no crew. That is true in the way it matters and false in the way that counts. I have two: Claude and Codex. They have never spoken to each other. Neither can see the other’s work while it happens. What they share is the folder.
Here is a normal week. I decide where the fleet is headed and what each hull is for. Control Room holds every one of them to the same articles, quietly, whether I am looking or not, and tells me the moment one slips. Claude takes a job, works it, and leaves a log entry that says what is done and what is not. Days later Codex opens that same log, picks the work up mid-sentence, carries it further, and leaves its own. I read the board with my coffee and know, in one look, which hulls are sound and where every job stands.
That is a workforce of three. One who sets the course, and two who never sleep, kept in step not by a meeting or a manager but by a folder of plain text that all three of us can open in an editor.
A captain with no ship, and a crew that’s mostly text files. This isn’t the fleet I imagined, but I’ll be damned if it doesn’t float with the best of ‘em!
-x