The campaign surface — plain-file knowledge, an observed board, a graph of terminal-actors
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.
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:
- ASCII sketches in plain fences — structure and layout, terminal-native, renders everywhere, zero tooling. The default for thinking.
- 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.)
- An
htmlfence 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. - 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
orchestrator-data.jsand the same-turn update ritual- the static dashboard HTML skill asset, and the board-freshness Stop hook
- the entire per-repo Astro pipeline: 196 MB node_modules, the lockfile,
the build, 6.9 MB committed dist, the
atlas-syncCI gate - hand-stamped statuses in notes, and their drift
- terminal-UUID note links that die on re-key
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 |