← the Atlas

The campaign surface — plain-file knowledge, an observed board, a graph of terminal-actors

Features·seedling·proposed·

Unify Atlas notes, the hand-maintained orchestrator dashboard, and the coming chat bot into one zero-setup surface: notes become plain markdown rendered live by kolu itself (the Astro pipeline is deleted, not improved), campaign state becomes a Board tab derived from what kolu already observes, and the board's true shape is the supervision graph of terminals — because kolu's terminals already are actors in the actor-model sense.

Why: the scribe problem, and the Atlas problem

Running kolu’s development as a multi-agent campaign uses three disconnected artifacts today, and each has a structural defect.

The dashboard is hand-written. A JSON file the coordinator agent updates after every event, rendered by a static HTML skill asset. It violates the philosophy twice — the interface is supposed to be built by observing what users already do, and facts like branch/PR/CI status derive from the terminal’s cwd. Hand-maintained derived state drifts (stale boards, notes disagreeing with the board, links dying on terminal re-key), and keeping it honest required a Stop hook whose only job is to nag the scribe.

The Atlas is a static-site generator bolted onto the repo. Measured, this repo: 196 MB of node_modules, a 4,404-line lockfile, 23 components, an Astro config and build recipes — per repo that wants a knowledge base. 6.9 MB of generated HTML is committed, and 1,151 commits have touched dist/; a CI gate (atlas-sync) exists purely to police the generated artifact. Every note edit is a branch → PR → build → stage-dist → check-sync → merge ceremony. The adoption verdict is already in: drishti and odu never set it up — the per-repo cost lost.

The decisive fact: kolu already ships its own markdown renderer@kolu/solid-markdown (rendering, highlighting, URL policy) — wired into the client today, including the Code tab’s file dispatcher. The committed-HTML pipeline existed so notes were “reviewable in the Code tab without a dev server”; kolu since became that renderer. The generator is vestigial.

The model: it’s actors all the way down

The actor model: an actor is the primitive unit of computation with private state and no shared memory; it has a mailbox and processes messages one at a time; on a message it may create more actors, send messages, or decide its own next state; supervisors monitor actors and reset crashed ones to a stable state (“let it crash”); everything has an address.

kolu’s campaign is already this, piece for piece:

Actor model kolu, today
actor: private state, no shared memory a terminal: its PTY, its session — nothing shared
address the terminal UUID
mailbox, one message at a time kaval serializes all input — “when input arrives, last one wins” is the single-writer mailbox; the coordinator’s own inbox is serialization-by-conversation
create actors / send messages / decide next state the coordinator spawns terminals (lifecycle_create), sends briefs (sendInput), and revises its plan
supervisor resets a crashed actor to stable state the falsify-and-redirect loop: a wrong brief → stand down, /compact (reset to stable), re-brief. And the daemons already do it literally: padi adopt-or-recycles its kaval; the binder drains and re-provisions padi
hierarchy — everything is an actor coordinator → worker terminals → split terminals (selected-agent debates); padi → kaval beneath them

Inside the coordinator: the receive loop

The coordinator is not special machinery — it is one more actor, an agent in an ordinary kolu terminal, and its behavior is exactly the actor triad, observable in kolu’s own development campaigns:

Its mailbox is its conversation. Everything arrives as messages into one serialized stream — the human’s directives, workers’ reports, gate-workflow results — and is processed one at a time, in order. There is no other input path: a worker cannot interrupt a ruling in progress; two workers’ escalations queue. (This is why chat integration is cheap: a thread bound to the coordinator is just more mailbox.)

Its private state is the campaign: what is in flight, what each gate ruled, who holds which CI box, what must merge first. Some of it is durable — written into notes as decisions — and the rest is deliberately ephemeral conversation state, reset on compaction. No other actor can read it; workers know only what they are told in briefs.

On each message it does the triad, nothing else:

Message arriving create actors send messages decide next state
human: “dispatch X” cut worktree = branch = lane, create the terminal the brief (symptom + hypotheses-to-falsify, never mechanism-as-fact) X is in flight, gate pending
worker: gate request (a design file + pointer, blocking) adversarial review swarm (its own short-lived actors) the ruling: GO / deltas / redirect the design is ratified — or the coordinator’s own premise is falsified and its state revises
worker: escalation (“the brief is wrong”) verify against the tree, then a corrected ruling supervision runs both ways: a child’s message can crash the parent’s belief
worker: CI red (report-before-rerun) venue arbitration, rerun approval or stop box leases update
worker: merge-ready relay to the human (the coordinator never merges) the lane is done-pending-human
human: “merged” / “close it” stand-down to the worker the lane retires

As a supervisor it practices let-it-crash, not defensive repair. A worker whose premise is falsified or whose context is corrupted is not patched in place — it is stood down, reset to a stable state (/compact, or a fresh terminal), and re-briefed. The reset is cheap because the durable state was never in the actor: it is in the files (the branch, the note, the PR).

And it is itself supervised. Above the coordinator sits the human — the root of the tree, who merges every PR and can crash-and-redirect the coordinator’s own plans (this note exists because of exactly such a message). Below the terminals runs the other supervision tree — binder → padi → kaval, adopt-or-recycle — so the full picture is two supervision hierarchies meeting at the terminal: one governing work, one governing process lifetimes, both actor-shaped.

This is not an analogy to adopt — it is a description of what shipped. The consequence for the Board: a campaign is not a list of lanes, it is a supervision graph of terminal-actors, and most of its edges are already recorded: parentId is a field of terminal creation (splits carry their parent today), and a terminal created through the kolu MCP knows which session asked for it.The remaining edge — “which terminal drives which” when input arrives over a CLI rather than the MCP — is not recorded today. The graph renders what is observed and nothing more; an unattributed lane simply hangs off the repo root. Observation can grow (input provenance is knowable at the MCP face), but the Board never guesses.

Implementation: what kolu builds vs what it observes

The agent-agnostic principle draws the implementation line sharply: kolu implements the actor substrate; the coordinator’s brain is never product code. The coordinator is whatever agent CLI runs in that terminal — its discipline (briefs, gates, rulings) is a prompt contract (the /bridge skill), swappable like any agent. kolu’s job is to make the substrate real and observed:

Model concept Implementing component Status
actor, address, mailbox a kaval terminal: UUID + serialized input (“last one wins”) shipped
create-an-actor (+ parent edge) padi lifecycle.create with parentId shipped
creator attribution (who spawned whom via the act door) record the calling session on MCP-created terminals — one server-side field small addition
observe: working / asking / idle, which agent padi’s agent sensors shipped
observe: branch → PR → CI per lane the server’s existing cwd-derivation for terminal chrome shipped — reused, not duplicated
the campaign graph a derived collection in kolu-server joining the rows above, keyed by repo root — no new daemon, no new store, the same derive-don’t-write shape as every other cell new, small
the Board tab a client view subscribing to campaign(repoOf(cwd)) — the third right-panel tab new
note rendering @kolu/solid-markdown + standard marked extensions (alerts + footnotes shipped; add: live #1234 pills via the server’s forge access, directive syntax, the html fence → the existing sandboxed preview iframe) P0, small
durable knowledge plain files in git — no new store, no schema shipped (it’s a filesystem)
reset-to-stable (let it crash) the agent CLI’s own compaction / a fresh terminal shipped — nothing to build
the attention flow — a blocking ask can’t exist without a listener padi delivers [kolu] worker <id> (<intent>) needs you into the SUPERVISOR terminal’s mailbox (the parentId/creator edge names it; kaval’s serialized input is the transport), fired only when agent-state is awaiting/waiting AND raw PTY output settled ≥ N s (the conjunction — agent-idle alone false-fires on subagent/background churn). Only agent-terminal supervisors are notified, never a human shell. new, small — both signals shipped (wait_agentState + wait_outputSettled); the edge and the mailbox exist
the chat mirror pesu: postMessage out, sendInput in — the chat-native note’s bridge P3, per that note

Read the Status column’s shape: most rows are shipped. The feature is three small derived/rendering additions on an actor substrate kolu already is — which is exactly what the model-not-analogy claim predicts. What is never on the list: a coordinator engine, a workflow DSL, a state machine for campaigns — the brain stays an agent, the campaign state stays files and observation.

The attention flow, as its own class-kill

The recurring failure this design must end: a worker blocks on the coordinator, and the ask waits to be discovered rather than delivered. Observe pulls the coordinator (a merge, a human message re-invoke it); a worker quietly entering “await coordinator” pushes nothing — so a gate request is the one event most likely to rot, and in practice it did, twice, overnight.

Patches that add a listener (a watcher process, a Stop hook, a discipline) all fail the bar identically: the listener is a separate, optional act, so “dispatch a worker without arming its notification” stays spellable. P4’s target is to make ask-without-listener unrepresentable — which happens only when the ask and the listener are the same fact.

They already are, latent: the supervision edge (parentId / creator attribution) records exactly who should hear a worker’s ask, and the mailbox (serialized terminal input — the coordinator’s real inbox) is exactly how to deliver it. So the notification is not a thing you attach; it is what the edge means. A worker going blocked delivers into its supervisor’s mailbox by construction — the supervisor is re-invoked like any message, no watcher, no poll, nothing to forget. Dispatching a worker creates its notification path because the edge is the subscription (P5: padi is the knowing endpoint for “who needs whom”; delivery rides the one door).

The signal must be the conjunction, not agent-state alone: a coding agent’s subagents and background tasks flap its detected state to idle while the terminal is still working. Fire only when agent-state is awaiting/waiting and raw PTY output has settled for a debounce window — both already shipped as wait_agentState and wait_outputSettled (“terminal-activity idle”, distinct from agent-state idle). Guard: notify only supervisors that are themselves agent terminals — never inject into a human’s shell.

Interim, until this ships: every dispatch carrying an “ask me” gate arms wait --until idle:<ms> (the same conjunction) as a background task in the same turn — the harness re-invokes the coordinator when it fires. The ask and its listener created together, never apart.

The Board — a third right-panel tab, shaped like the graph

The right panel today has two tabs (inspector | code). The Board is the third: the repo’s supervision graph, live.

InspectorCodeBoard
1 asking you 3 working 1 idle
RT-fable · coordinatoratlas #1878
claude · working — the act door (supervisor)
└─spawn-detection▤ agent-spawnPR draft
claude · awaiting your answer — 12m
└─codex splitdebate r2
codex · working — parentId edge, observed
└─philosophy-restore#1889 merged
claude · idle — lane retiring to shipped
scratchno PR
plain shell · unattributed — hangs off the repo root
shipped · 15

Scoping: whose board is this?

The right panel is per-terminal, so the Board uses the association the Code tab already uses: the terminal’s cwd names the repo, and the repo is the campaign. cwd → repo root → every worktree of that repo → their terminals, branches, PRs, sensor states, parent edges. Two same-repo terminals see the same board (a per-terminal viewport on repo-scoped content); a drishti terminal sees drishti’s campaign — multi-project coordination with zero configuration; a repo-less terminal sees an honest empty tab. No campaign registry, no picker.

Every element is already observed: the lanes are terminals (cwd → worktree); the dots and sub-lines are padi’s agent sensors; the PR/CI chips are the cwd → branch → PR derivation the server already does for terminal chrome; the ▤ chip is the branch ↔ note-slug convention; the tree edges are parentId + MCP creator attribution; shipped is merged-PRs-whose-branch- had-a-lane. Nobody writes any of it, so none of it can go stale.

Knowledge: plain markdown, rendered by the product

Notes become plain GFM files — no imports, no JSX, no build, no committed dist, no sync gate. The rich vocabulary survives as renderer capabilities in @kolu/solid-markdown, shipped once in the product:

Today (MDX component) Tomorrow (plain file + smarter renderer)
<Callout> (76 uses) GitHub-native alerts: > [!NOTE] / [!WARNING]
<PrLink> / <Issue> (90 uses) a bare #1234 — the renderer resolves it to a live status pill (draft/open/merged/closed, CI). Notes stop containing outcomes, so they cannot go stale
<Footnote> (29) GFM footnotes
<Svg> (37) an ordinary image link to a committed .svg
<Pill> / <D> / <Cite> inline conventions the renderer styles
<PhaseTree> / <Terminal> / <Roadmap> fenced blocks with a language tag the renderer upgrades — the mermaid pattern

GitHub renders the same files acceptably (alerts and footnotes are GitHub-native), so “reviewable anywhere” improves — today’s MDX shows as source on github.com. And any repo gets the whole experience by containing .md files, which drishti and odu already do. Zero setup, every repo, every host.

The write path: git remains the store — durable, versioned, offline (the files-are-the-state doctrine). What dies is the ceremony: with no build artifacts, a note edit is a one-file diff, and kolu itself can be the editor (the act door; scratch.write is the wire precedent) committing to a branch or straight to master per repo policy — the format no longer forces a pipeline on anyone.

How UI prototypes live in notes

This note’s own mockups are JSX — which plain markdown forbids. The ladder, each rung a plain-file citizen:

  1. ASCII sketches in plain fences — structure and layout, terminal-native, renders everywhere, zero tooling. The default for thinking.
  2. SVG files — the designed-mockup standard. Text, diffable, committed beside the note, embedded as an ordinary image; agents author SVG fluently. (The Atlas already leans on SVG diagrams — 37 uses.)
  3. An html fence the renderer upgrades to a sandboxed live preview — the high-fidelity rung. The fence’s source stays visible as code on GitHub; in kolu it renders inside the existing opaque-origin sandboxed preview iframe (the Code-tab preview machinery, already shipped and e2e-hardened). Interactive prototypes with zero new infrastructure.
  4. Screenshots — for as-built states, small and compressed, the evidence flow’s conventions.

This note’s mockups migrate to rung 3 verbatim — they are already plain HTML-with-inline-styles wearing JSX syntax.

The flows

Dispatch. The coordinator cuts worktree = branch = lane, creates a terminal (a child actor — the parent edge recorded at birth), briefs it. The lane appears on the graph by observation. Zero writes.

Attention. An agent flips to asking-you → amber dot, who-needs-me count, notify seam (desktop today, the pesu thread tomorrow). Click the lane → you are in the terminal. Answer, step out.

Reading the plan. The lane’s ▤ chip opens its note in the Code tab; the note’s #1234 pills show live outcomes. One click from what to why.

Shipping. The human merges (unchanged). The chip flips because GitHub says so; the note’s pill flips identically — same fact, two renderings; the lane retires to shipped when its terminal closes.

Failure. Let it crash: a falsified brief or a wedged agent doesn’t get defensive programming — the supervisor (coordinator) stands the actor down, resets it to a stable state (/compact, or a fresh terminal), and re-briefs. The graph shows the reset for what it is: the same address, a new incarnation.

Chat. pesu renders this same graph into the thread and pings on attention — the chat-native note’s “mirror of the coordinator,” now with its data source shipped rather than invented.

What stays conversational (deliberately off-board). Gate rulings, CI venue arbitration, box leases — supervisor knowledge with no observable substrate. They live in dialogue and in notes as decisions, never on the board as state. Putting them on a board is what created the scribe.

What this deletes

Phases

Phase Scope Deletes
P0 — the renderer learns three tricks live #1234 pills, [!NOTE] alerts, footnotes, the fence upgrades (phase-tree, html-preview via the existing sandboxed iframe) — all in @kolu/solid-markdown nothing yet; unlocks everything
P1 — notes go plain migrate the 104 notes MDX → GFM (mechanical for the 10-component vocabulary); delete the Astro pipeline; the /atlas skill shrinks to “write markdown here” the 196 MB, the dist, the sync gate, the ceremony
P2 — the Board tab the derived campaign graph on the server (terminals ⋈ worktrees ⋈ branches ⋈ PRs ⋈ CI ⋈ sensors ⋈ parent edges, reusing the existing cwd-derivation machinery) + the right-panel tab the dashboard file, its HTML, the freshness hook
P3 — the thread mirror pesu renders the graph + attention pings (the chat-native note’s own plan) nothing — it completes the picture