My to-do list is a web page my agents can read and write
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.