← the Atlas

pulam — the terminal-workspace surface as an ephemeral daemon on kaval

Features·budding·accepted·

Repeat the kaval decoupling one level up. The awareness sensors (git · PR · agent · foreground) plus fs/git become a generic library that fills one typed surface — kolu runs it in-process locally, and an ephemeral daemon (pulam) on top of kaval serves the same surface over ssh for remote terminals. kolu reads that observation through one seam, never folding a copy into its own record. One library, two homes. The standalone tools ship first; the R4.6–R4.8 forward roadmap (rename → pulam · fs/git in pulam-tui · pulam-web) graduates the browser-ws-stream primitive kolu's remote-terminals R8 depends on.

kaval decoupled the PTY out of kolu into a standalone, minimal, durable daemon. This note makes the same move one level up for the part kolu kept tangling: terminal awareness (git branch/dirty, PR status, agent detection, foreground). A new daemon — pulam (Tamil புலம், “field · domain” — and from the same root pulan = a sense; sibling to kaval = guard and odu = run) — sits on top of kaval, runs the sensors, and exposes the result as one typed @kolu/surface collection. One sensor library, two homes: locally kolu runs the sensors in-process; remotely the pulam daemon serves the same awareness over ssh. Either way the result is one typed observation surface that kolu reads — it never folds a copy into its own record. (That convergence is remote-terminals R8; until it lands, kolu’s local path still routes awareness into terminalMetadata.)

browsertiles · reads terminalMetadatakolu-serverreads the observation surfacemirror (remote only)kaval (local)durable PTY · tapspulam (host · ephemeral)runs the sensors · serves the slicekaval (host)durable PTY · tapshost fs · git · gh · ~/.claudepulam-tui (standalone) oRPCtaps · sensors in-process (local)dial (ssh)slice → mirror (remote)taps (dialed as client)readsdials
pulam sits on top of kaval (which stays byte-for-byte minimal). LOCAL terminals run the sensors inside kolu-server, in-process — no socket. REMOTE terminals run pulam host-side: it dials the host's kaval for taps, reads the host fs (git/gh/~/.claude), and serves the awareness slice over ssh, which kolu-server mirrors and reads through one seam (remote-terminals R8 deletes the old server-side fold). The same daemon is independently useful: pulam-tui dials it directly.

The core model

Three claims, each verified against the shipped code:

One library, two homes — kolu reads, never folds

Local and remote share the sensor library and one schema — and, after remote-terminals R8, one shape of consumption too: the sensors fill the terminalWorkspaceSurface.awareness collection, and kolu reads it. The only thing that differs is where that collection is backed:

So the old asymmetry — local mutates kolu’s record; remote serves → mirrors → folds — collapses to one shape, two backings: both fill the observation surface, kolu reads it, and local-vs-remote is a backing swap behind the seam. The one new artifact is the host-side hooks impl that reifies each mutate closure into a serialized slice frame — a wire carries values, not closures. (Until R8 lands, kolu’s local path still routes awareness into terminalMetadata; R8 is what moves it onto the surface kolu reads.)

mirrorRemoteSurface has graduated (#1497). It is now the one public mirror in @kolu/surface — the spec-driven dual of implementSurface that drives a served surface’s streaming primitives (cells/collections/streams) into caller-supplied sinks — mirroring procedures to a total dual is remote-terminals R7; mirrorRemoteCollection is demoted to its private per-key engine (“you don’t sell half a house”). The trigger was pulam growing a real activity stream (the green dot) — the second stream-bearing consumer the graduation waited for. It does not by itself dissolve kolu’s server-side fold — remote-terminals R8 does, by composing the surface into kolu’s own; until then the fold consumes mirrorRemoteSurface. As a @kolu/surface API change it rode surface.md’s drishti merge-gate.

The standalone tools — pulam + its two clients

Like kaval, the daemon is a deliverable in its own right, shipped and proven before kolu touches it. Where kaval-tui shows what’s running in each PTY, the pulam clients show what each terminal is in — the awareness slice, with zero kolu-server, dialable over ssh against a prod box. Two clients read it, split by richness, the kaval picture one layer up:

It ships self-contained: the sensors (@kolu/terminal-workspace/sensors.ts) import nothing from kolu but a logger, and the host runtime deps are just node · git · gh (SQLite via Node’s built-in node:sqlite — no native addon), so the bin travels cleanly over ssh. Define the contract once and the consumers fall out of one schema — the daemon serves it, the clients read it, and remotely kolu mirrors and reads it.

The R4 tree — shipped end-to-end

The standalone pulam story (extract → daemon → remote viewer → Bun → OpenTUI → live fleet → live activity) is complete. kolu’s consume — kolu dialing this host’s pulam over a long-lived HostSession, mirroring it, and reading it into its own canvas — is not pulam’s job; it lives in the parent remote-terminals roadmap as R8–R9 (R8 composes the surface and deletes the fold, R9 dials), gated on the remote dial. And the fs/git a remote Code tab needs is roadmapped there as R6: this awareness library grows into @kolu/terminal-workspaceone @kolu/surface surface that adds fs/git procedures + watcher streams beside the awareness collection — so kolu runs it in-process locally and pulam hosts the same surface remotely, never a second fs/git impl. pulam just points there. (The total mirror is proven independently in R7 — drishti gains a “Kill process” action, the first forwarded procedure on a mirrored surface — so R7 needs neither pulam nor kolu.)

Phase Ships #1413 etc.
R4.1 refactor extract @kolu/terminal-awareness — sensors + generic schemas, off kolu-common; the schema home inverts (kolu-common now imports the schemas and adds location). Behaviour-preserving — green CI is the proof. #1413
R4.2 refactor finish the provideradapter rename through the anyagent/anyforge leaves (symbols, files, exports — one adapter spine). The live Watcher handle + wire provider discriminant stay. Behaviour-preserving. #1419
R4.3 feature pulam + pulam-tui standalone — the daemon dials kaval and serves the awareness surface; the TUI reads it. Zero kolu-server. --stdio is the seam the ssh dial speaks to. #1428
R4.4 feature pulam-tui --host — dial + Nix-provision a remote pulam over ssh, render the same dashboard. The shared one-shot dial graduated to @kolu/surface-nix-host’s dialAgentOnce (a version-cell read is pulam’s connectivity probe). Zero kolu-server. #1439
R4.5 feature the fleet dashboard — see below. #1470 · #1479 · #1486 · #1497

R4.5 — the fleet board

The user’s framing made literal — “what is every agent doing, across every repo, across every machine” — as a dashboard you leave open on a second monitor: glance over from the game and a colour you can read across the room tells you whether any agent is blocked on you, and where. It stays a TUI, runs zero kolu-server, and shipped as four steps:

pulam-tui fleet⟳ live · 1s
2 agents need you— zest · pu-build-7
zest· 4 terminals
claudekolu · feat/dial-ssh #1412 ✓awaiting you3s
codexdrishti · masterworking0s
claudeanyforge · fix/checks #1408 ✗working4s
claudenotes · mainidle12m
pu-build-7· 2 terminals
claudekolu · fix/heap-oom #1427 ✓awaiting you9s
infra · deployworking1s
staging· unreachableECONNREFUSED
local· 1 terminal
claudepulam · feat/fleetidle2m
● 2 need you◜ 3 working○ 2 idle1 host down

When nothing needs you the board sits calm — cyan working spinners, dim idle rows, a quiet all clear ✓. The moment an agent hits awaiting_user its row lifts to the top of the fleet and a warm amber strip breathes, so a glance tells you someone’s waiting before you’ve read a word. Rows sort needs-you first across the whole fleet; --by agent regroups into one fleet-wide “who is waiting on input, anywhere” list; --json emits the flat [{ host, terminalId, ...AwarenessValue }] for a notifier. Unreachable / skew / empty hosts each render distinctly rather than silently vanishing.

pulam-tui fleet⟳ live · 1s
zest· 4 terminals
codexdrishti · masterworking0s
claudeanyforge · fix/checks #1408 ✗working4s
claudenotes · mainidle12m
pu-build-7· 2 terminals
claudekolu · fix/heap-oom #1427 ✓working9s
infra · deployworking1s
staging· unreachableECONNREFUSED
local· 1 terminal
claudepulam · feat/fleetidle2m
● 0 need you◜ 4 working○ 2 idleall clear ✓

Host is stamped at the dial site, never by the daemon — the awareness-layer echo of kolu’s kolu-side location stamp (AwarenessValue deliberately carries no hostId). The render reads the aggregate keyed by (host, terminalId), so two boxes’ identical terminal ids stay distinct.

The fleet board moved to the browser — and pulam-tui slimmed down

R4.5 shipped the fleet board as an OpenTUI dashboard inside pulam-tui — a Bun binary, a Zig renderer via Bun.dlopen, bun2nix packaging. That was the right call while the TUI was the only rich fleet view. It no longer is: pulam-web is the browser fleet dashboard, and it is the better home for the multi-host glance. So the OpenTUI/Bun half is walked back — pulam-tui reverts to a kaval-tui-style raw client (status / watch, one daemon over a socket or ssh), and the multi-host fleet lives in pulam-web. The full strip-back — what leaves the package and why — is pulam-tui’s own note.

The electricity call survives the move intact — it lands cleaner, in fact:

R4.6–R4.8 — the forward roadmap: what pulam proves before kolu’s R8

The discipline that built this epic: every hard primitive graduates through a standalone consumer before kolu touches it — kaval-tui proved the PTY dial, the fleet board proved the awareness mirror, drishti proved the surface mirror + forwarded procedures. Kolu’s remote-terminals fs/git leg (now R9) reached for one thing no standalone consumer had exercised — the fs/git Code-tab live updates over the browser ws — and the discarded #1510 spent months there. (The real block turned out to be the file-tree renderer not repainting under change-pulse churn, not the transport — but it was blind: the prod client build hides the console, the server log lives in an ephemeral sandbox.) These three phases pay the debt. Each is a real pulam feature and the graduation gate for remote-terminals R9 — the drishti pattern, one more turn.

Phase Ships …and the gate it is for kolu
R4.6 · rename refactor #1512 arivu → pulam — the daemon, pulam-tui, this note; the package stays @kolu/terminal-workspace. Behaviour-preserving; green CI is the proof. Deliberately just the rename — across code, Nix, docs, and the website. clears the “knowing” misnomer before new surface area is born under it
R4.7 · live git status in pulam-tui fleet feature #1519 each fleet row grows a live working-tree cell (changed-file count + branch ahead/behind), and a selected row (↑/↓ · Enter) drills in to the full git statusstaged · modified · untracked + the changed-file list. Driven by subscribeRepoChange’s {seq} pulse re-running git.getStatus; no file content — the changed-file list, never bodies or diffs. git.getStatus’s local arm grew the branch header + section counts and dropped the always-null base — a breaking reshape, so the workspace contract bumps 0.3 → 1.0. the server source-arm {seq} shape, end-to-end and observable — “is the shape sound?”
R4.8 · pulam-web feature the browser twin (its own note) — a drishti-shaped browser ↔ ssh app reading pulam’s surface over websocketLink + surfaceClient + Solid reconcile. Cheapest-first: R-pulamweb-1 drishti consumer ✅ · R-pulamweb-2 framework ✅ #1524 · R-pulamweb-3 agent dashboard ✅ #1535 · R-pulamweb-4 live git status + drill-in ◀ next. kolu’s exact failing leg — ws + surfaceClient + reconcile — proven kolu-free with a visible console; the recipe R9 rides on

Why a web twin, when the TUI sufficed for the glance view. pulam-tui rides stdioLink + mirror sinks, so it structurally cannot exercise the leg kolu fails on: browser websocketLinksurfaceClient → Solid reconcile. pulam-web is that leg, minus kolu — it reads the same surface the same way kolu’s browser will. So it is not a prettier dashboard (that would be the ceremony the fleet-board callout warned against); it is the standalone proof of kolu’s own consumption shape. Only once R4.7 (shape) and R4.8 (transport) are green does kolu’s R9 compose the surface — now a backing-swap onto twice-proven electricity, not a speculative one.

R4.7 — pulam-tui fleet: a live git status view (proving the {seq} shape)

The fleet board already shows each terminal’s repo·branch from the awareness collection — the primitive R4.5 proved. R4.7 consumes the other arm of the surface, the one kolu’s Code tab needs and nothing had exercised: the subscribeRepoChange {seq} watcher stream re-running the git.getStatus procedure. Each fleet row grows a live working-tree cell — a changed-file count plus the branch’s ahead/behind — and selecting a row (↑/↓) and pressing Enter drills in to the full git status: the staged · modified · untracked summary and the changed-file list. No file content, no diff — the changed-file list (paths + status codes), never file bodies; git status alone drives the exact pulse-plus-requery loop kolu fails on, so it is the proof.

To paint ahead/behind, git.getStatus’s local output grew a branch tracking header (name · upstream · ahead · behind) and the working-tree section counts — read off the same git status the file list already reads, so it costs no extra git call (simple-git already computes both and the code just stopped discarding them). The same change models the result as a discriminated union on mode and drops the always-null base from the local arm; removing a field a 0.3 viewer’s schema still requires is a breaking reshape, so the workspace contract bumps 0.3 → 1.0 (a major, not a minor) and the gate marks 0.3 and 1.0 mutually skew in both directions. This is the one place R4.7 extends the surface rather than purely consuming it — and it stays in kolu-git + @kolu/terminal-workspace, touching no @kolu/surface* API, so it needs no drishti gate.

The consume loop is RepoWatchSet: per distinct repo across the fleet, subscribe to the {seq} pulse and re-query getStatus on each, keyed by repo root so repo-mates share one subscription and the last to leave tears it down. It runs over the real unix-socket / stdioLink link, so R4.7 answers one question: does the {seq} source-arm stream + requery survive a real link, end to end? Proven by an integration test over a real served socket (a working-tree change pulses → the re-query reflects it) and by raw-PTY capture — frame N+1 ≠ frame N on a touch / git addnever directLink (the in-process path that masked the bug).

So the shape is sound over a real wire. But the consumer here is raw for await iteration over a mirrorRemoteSurface handle — close kin to directLink, and not kolu’s consumer. kolu’s browser (and the #1510 prototype that stuck) reads through surfaceClient.streams.use() + a Solid reconcile store — a path R4.7 never touches. So two unknowns remain, both R4.8’s: that consumer, and whether the surface should hand a raw {seq} pulse to a browser at all.

R4.8 — pulam-web: the browser twin → its own note

R4.8 grew its own UI and a layered plan, so it moved to a dedicated note: pulam-web. The short of it — a drishti-shaped Node browser ↔ ssh app reading pulam’s surface over websocketLink + surfaceClient + Solid reconcile, kolu’s exact browser-consumption leg minus kolu (Node + Vite, matching kolu’s own stack). It lands cheapest-first: R-pulamweb-1 graduates the reactive stream consumer in drishti ✅; R-pulamweb-2 stands up the whole framework (provision · fan-out · mirror · re-serve) rendering only a terminal list#1524; R-pulamweb-3 layers the agent dashboard (every agent sorted by what needs you) ✅ #1535; R-pulamweb-4 adds the live git status + drill-in — do next. Full plan, UI mockup, and verified reuse map live in pulam-web.

History