x‑hakt

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.

control standards

dockergityamlnextjs

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.

by handone room
Before: one person, a clipboard, and a lot of rowing. After: one room with a window onto every deck.

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.

data/projects/<slug>/project.ymlSPEC.mdSTATUS.mdHANDOFF.mdClaudeCodexa humandocker socketgit worktree
One project is one folder of plain files. Three kinds of writer read and write it directly; the detail page also pulls live facts from the machine it runs on.
The Control Room detail page for the playtopia project, showing its status, running containers, standards results and latest handoff.
One vessel's page. Playtopia: the lifecycle stage, the containers read live off the Docker socket, the standards column, and the newest handoff with a staleness clock. Nothing on this page is stored; it is all read at the moment I open it.

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.

a checkgit log / git remotethe Docker socketthe filesystempassfailn/aunknownstale
A check does not read a stored result. It re-runs against git, the Docker socket, or the filesystem every time, and it is allowed to answer 'I don't know'.
The Control Room standards page: a grid of projects against standards checks, colour-coded pass, stale, not-applicable and unknown.
The audit matrix: one row per hull, one column per article. Green is a pass checked against the real thing, amber is stale, a dash is 'not applicable to this vessel', a blank is 'I could not tell'. The point is being able to see the whole fleet's seaworthiness in one glance instead of one project at a time.

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.

Control RoomCasparsocket, livekeylighthousegp-formspodusapoint-in-time
The host Control Room lives on is read directly. The far hosts are read through one key that can only run one script.
The Control Room server page for Caspar: host resource stats followed by its containers grouped by project.
Caspar's page: the host itself at the top (disk, memory, load, all off the fixed read-only script), then every container on it, grouped by the project it belongs to. The far anchorages get the same layout, marked point-in-time.

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.

ClaudeCodexa humanHANDOFF.mdappend-only, newest firstresumeHANDOFF.ymllatest checkpoint + trailthe next watch
Everyone appends to HANDOFF.md. The next watch reads only HANDOFF.yml: the last checkpoint in full, plus a short trail so a thin one cannot erase the trajectory.

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.

x (the course)Control Roomstandards + the handoff logthe boardClaudeCodexthe watch, handed over through the log
The loop. Nothing in it is clever. A person sets the course, one dashboard holds the standard and the shared memory, two agents work the decks and hand the watch between each other through the same files, and the board reads back to the person.

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