← build-log

My to-do list is a web page my agents can read and write

· #build-log#agents#shared-state

The last few entries were all about the agent’s interior — the things it keeps to itself. Memory is what the desk has earned and kept. Governance is how a roomful of agents stays coherent. Continuity is how the work survives the gap between one session and the next. All of it lives on the agent’s side of the glass.

This one is about the glass: the single surface where the operator and the agents actually meet — where the same document is a thing I read and a thing they query.

Two documents that are supposed to be one

Humans think in documents. A plan, a spec, a to-do list — something you open, scan, and reason about top to bottom. Agents think in structured data — a row you can SELECT, a status you can filter on, a field you can write back.

So the normal setup keeps two copies of the same truth: the document I look at, and whatever machine-readable state the automation actually runs off. And the two drift immediately and forever. I check a box on the doc; the cron job never hears about it. A script marks something done; my doc still says open. Every shared-state-between-people-and-software problem is a version of this — keeping two representations of one truth in sync by hand is a tax you pay until you stop paying attention, and then it’s just quietly wrong.

The fix I landed on isn’t a better sync job between the two. It’s to delete one of them — to make the document I read be the database the agents query. One artifact, two faces.

The artifact: a spec that renders to a page

This desk drafts its substantial specs and plans with a skill that renders them as a single standalone HTML file — dark-themed, navigable, diagrams inline, no external dependencies, works offline. It’s the format I hand myself a plan in: checklists, decision callouts, the works. I read it in a browser like any other document.

Ordinary so far. The interesting part is what one of those checklists is underneath.

The page is the database

The mechanics are deliberately dull. Each item is a row keyed by doc_id plus an item_key. On load, the server copy wins and is cached locally; on every change, the new value is pushed up fire-and-forget. An agent — or a cron job, or the nightly review from the governance post — reads the operator’s live priorities with one query:

SELECT item_key, label, state, outcome
FROM   bp_task_state
WHERE  doc_id = '<slug>'

No export step, no “please remember to update the tracker.” The act of me reading and ticking the document is the update.

Not done / not-done — a lifecycle and a decision

The naïve version of this is a checkbox: done or not. That’s too lossy to run anything off. So an item carries a four-state lifecycle — open / in_progress / done / stale — and a decision item records which branch I chose in an outcome field, not merely that a decision happened.

That distinction matters because the thing an agent usually needs from me isn’t “is this finished.” It’s “which way did you decide to go,” and “what’s actually live right now versus what went stale.” The control surface has to carry the judgment, not just the completion. (checked still mirrors state = 'done' so older readers keep working — a small fail-soft seam.)

The gotcha: what is a row’s identity?

The hard question under all of this is the one that always bites shared state: when I edit the doc tomorrow — reword an item, reorder the list, add a section — how does a row keep its identity so my saved progress doesn’t scramble?

The answer is that identity is the content, not the position. A row is keyed by the document id, the heading above the list, and the item’s text. So you can reorder freely, rename the document’s title, reorganize other sections — state follows the item. But change an item’s wording and you’ve minted a new row; the old progress orphans.

Closing the loop: board vs. memory

This is where it wires back into the governance layer. An item can be authored with a pointer to a Satori memory id — it rides into the table’s memory_id column — so the board now has a thread back to the thing it’s supposed to reflect.

Which means the desk can compare two sources of truth that are meant to agree: what the board says (the human-facing checkbox) and what the owning project’s memory says (what the work actually concluded). When they disagree — board reads done, memory still reads open — that’s drift, and the same master_stats pass from the CEO-desk post flags every row where the two come apart.

Fail-soft, as usual

If the sync endpoint is missing or the network drops, the whole thing degrades to a plain browser document saving its checkboxes to local storage. The shared-table superpower simply isn’t there; nothing breaks, nothing throws. That’s the same posture as every other piece on this desk — the clever layer is an enhancement, and its absence is the boring default everyone already has, never a crash.

The demo is the post

The operator board — the single view of every project’s live tasks that the CEO desk reads each morning — is one of these documents. It was restructured live, in the browser, the same week I wrote this. When I drag a task to in_progress on that page, the next agent session opens and reads it as in_progress. The thing describing the mechanism is running on the mechanism.

Honest close

There’s no new service here. It’s the document skill that already drafts these specs, a Supabase table that already existed for memory, a few <meta> tags, and a couple hundred lines of fire-and-forget fetch. The novelty isn’t the stack — it’s the decision to stop maintaining a human document and an agent database as two separate things. A spec that’s also a table. A to-do list that’s also an API.

Almost nobody ships a surface where the human and the agents share live state directly — there’s usually a person in the middle, retyping one into the other and absorbing the drift. On this desk that person was me, and deleting that job is the whole point.