Skip to content

The board

The daemon serves the board at http://127.0.0.1:4000/. It is one page with three full-page views behind a hotkey row, 1–3, and a settings sheet on ⌘,. Everything on it is a view over fibers the daemon already polls, plus the host-local ledgers — the board stores nothing of its own.

Key View What it answers
1 Desk What needs doing, and what is running right now
2 Chronicle What a stretch of weeks was about
3 Board What the work produced
⌘, Settings Every operator file, on any host in the fleet

?view=chronicle (or shelf) deep-links a view.

The tab named Board, and the board

Hotkey 3 is titled Board for the reader. Internally it is the shelf — the view id, the storage keys and the module names all say shelf, and ?view=shelf is what deep-links it. This page uses "the board" for the whole surface at :4000 and "the Board tab" for the third view.

Desk — the kanban

Three surfaces: the Now board of cards that need something, a Pinned strip of perennial roles, and Resting, where snoozed work and standing roles between runs wait.

The Desk with fictional workshop tasks: lunch options and speaker bios in Drafts, the participant guide and venue access In Flight, and the programme and venue decision Awaiting Review.

The cards use fictional workshop data. Each card shows its fiber's outcome, so the venue decision is readable beside the guide that uses it.

Where a card lands is two independent decisions: which column it belongs to, and which horizon it sits on.

Column

The browser computes column membership with classifyFiber (ui/src/board/KanbanRules.ts). That function decides membership alone, and it evaluates in this order:

Column Condition
Cycles tagged cycle — checked first, unconditionally
Tempered closed + tempered: true
Discarded closed + tempered: false
Awaiting review closed, tempered absent
In flight live tmux worker with a shuttle block — liveness wins over everything below
Pinned resting kind: pinned (open or active)
Scheduled active + kind: standing — drawn in Resting, wearing its next launch
In flight active, other kinds
Drafts anything left, including open

The cycle branch comes first on purpose. A cycle is an annotation on the calendar rather than work, so it leaves classification before any lifecycle question is asked — otherwise a stray "Autumn 2026" would sit in Drafts forever.

Apart from that one tag, the classifier reads only status, tempered, kind, and tmux liveness. Neither the daemon nor the board reads tags for anything else.

Horizon

Column says which lane; horizon says desk or Resting. It is computed by effectiveHorizon from two frontmatter keys:

  • horizon: stashed takes a card off the Now board and puts it in Resting. (The wire format and the API still say stashed everywhere; "Resting" is what the human is told, because that is what the surface means — deliberately paused work, not a bin of failures.)
  • A due: day that is today or already past overrides a stored stashed and pulls the card back onto the desk. That override is what makes snooze a return ticket rather than a black hole. A future due: alone changes nothing: the card keeps its place and simply wears the date.

Snooze is the gesture that writes both. Drag a card and a drag horizon appears under the tab strip — a slim row of upcoming days, plus a chip per upcoming cycle. Drop on a day for due: + horizon: stashed; drop on a cycle chip to land on that cycle's start (clamped to tomorrow if it is already running); drop on today to put it back on the desk; drop into Resting to stash it dateless.

Gestures

Two gestures carry different meanings. Drag-and-drop advances the card's state. Modal buttons give you another worker on the same run.

Drag-to-tempered acts by kind: on a standing role it accepts and re-arms, on a pinned role it accepts and re-parks to the strip, on a oneshot it writes the terminus. The outcome stays: the last run's digest is the card's headline until the next run writes its own.

Beyond the columns the Desk offers a fiber and file viewer, Stash and Capture dialogs, and Attach. Each card's panel folds a drawer under its title: a message box with New session and Resume, the next launch's agent, effort, session and kind, the card's due day and parent, and Temper / Discard.

Images pasted or dropped into the message box wait there as thumbnails, each with a × to remove it: PNG, JPEG, GIF or WebP, at most 10 MB each, 8 per send and 25 MB in total. New session, Resume and Meeting first store them on the host that owns the fiber (under its data directory's attachments/), then send the message followed by one [Image: <path>] line per image, so the worker opens each by path. If the upload fails nothing is sent and the box says why. While a send is in flight every verb waits and the thumbnails are frozen. A paste that carries text stays a text paste, even when the copying app put an image beside it.

Open a worker

The Aloft, Waiting, and Needs you controls open the worker's conversation. For terminal workers, the board can open Kitty; shuttle attach <fiber> works from other terminals too. Claude sessions with Remote Control can open in the browser or Claude app, using your browser's preference in Settings. Codex app workers use their native desktop link, with remote-access guidance on mobile. See Opening conversations for the choices, prerequisites, and quick-access terminal setup.

Chronicle — where the time went

The activity stream bucketed per minute, joined to fibers through the session and commit ledgers. See Telemetry for what feeds it and what happens when a ledger is absent.

Chronicle draws fibers as multi-day lifelines across calendar days, under a strip of cycle bands. Activity is inked on each lifeline, one mark per civil day; ahead of today a row carries only hollow marks for what is due and what is armed.

Chronicle with the same fictional workshop tasks: venue research, programme review, access checks, and guide preparation across September and October, under the Plan a small workshop cycle.

No fill, just marks on a line per fiber, so months of fibers stack without drowning each other. The era strip is the same cycle data that fences the Desk's Cycles column.

Everything on the page is joined through the ledgers. A minute or a commit that does not resolve to a fiber the board carries is not drawn at all, so work started outside shuttle is invisible here — and nothing is ever attributed by reading a slug: prefix out of a commit subject or a directory name.

The record is fetched on demand. The Desk's cards refresh every 15 s, and Chronicle redraws its rows from them, but its temporal feeds — activity, the session ledger, the commit ledger — are fetched when the view opens, again at most every five minutes while it stays open, and whenever you ask with the refresh control. Days already past are fetched once per open; scrolling back fetches only the days newly in view. Nothing on the board asks for temporal data while Chronicle is closed.

Board — what the work produced

Hotkey 3. Every file a worker pushed with shuttle send-file <path> [path...] in the last 30 days, laid out on a canvas as cards that render their own contents: the report renders inside its frame, the plot draws, the page is the thing itself rather than a link to it. A list of filenames is an index of work; a wall of rendered pages is the work, and you find the one you want by recognising it.

  • A card is two layers. The face — name, fiber, age, kind — is synchronous and is the card's resting state, never a skeleton. The body — the iframe, the image, the page — mounts when the card nears the viewport and is taken down again when the canvas carries more live bodies than it can afford.
  • Cards are handles, never factories. Every gesture rearranges the canvas; nothing on a card makes another card. Drag the header to move it, the corner to resize, the ✶ to hold it in place, the body to make the frame live.
  • Nothing overlaps, except a pile — one fiber's work gathered by the fiber lens.
  • Reading happens in the Reader. The ↗ sends a file to one overlay window with its own tab strip, because shuttle runs as a dock web-app where every window.open would otherwise become a separate window.

The feed is /api/v1/sent-files/all (with a /composite sibling that fans in every host a hub aggregates — the fleet), which reads the host's event stream. A host with no event stream shows an empty canvas — see Telemetry.

The board is optional, and the bundle is its own artifact

A fetched daemon already has it: CI builds the bundle and copies it into the release's own priv/, so a downloaded daemon serves the board with no Node anywhere in sight.

A checkout serves ui/dist from the checkout, and the repo does not ship that. Build it with:

make ui        # npm ci when the lockfile changed, then npm run build

A fresh clone builds it fine — no private checkout needed (see Sharp edges). make build, make restart and make all all include this step; SKIP_UI=1 leaves the bundle alone, which is how a host that takes its bundle from elsewhere is built.

SHUTTLE_UI_DIST overrides both, pointing the daemon at any built bundle on disk.

Without the bundle the root URL 404s with a hint, and the API stays fully usable. If you change any /api/v1/* route, rebuild the bundle — a stale bundle against a changed route table fails silently as a 404.

Settings

⌘, opens the settings sheet, and so does a bare ,: the board's own idiom is bare keys, and a phone has no ⌘. The ⚙︎ closing the tab strip does the same with a pointer, pinned to the right edge on a phone so it never scrolls out of reach. Esc closes it. It is an overlay rather than a fourth tab — the three tabs are windows onto the work, and configuration is not work.

Settings opens to Conversations, where you choose the default Aloft action for Claude sessions. The conversation-opening preference belongs to this browser and saves immediately. Right-click Aloft to choose another supported opening route for that session without changing your default.

Host settings sit separately under Worker hosts. The host picker selects which machine the remaining configuration addresses. The board is reachable from a phone and from a second hub, so the machine you are configuring is usually not the one you are sitting at; every read and write on the sheet carries the chosen host's origin and is owner-routed to the daemon that owns the file. Configuring a remote needs that remote's daemon to be recent enough to serve the config routes; an older one says so.

Section What it holds
Conversations The default for opening Claude conversations in this browser; worker execution stays unchanged
Notes & tasks stores.json — the store list this daemon is configured with, and the symlinked substores it reaches through them
Project folders projects.json — the checkouts Stash and Capture offer; adding one initializes its .felt/
Worker agents The merged registry, each record marked with the layer it came from, with a default-effort select per agent (written as an overrides entry), over agents.json
Connected hosts remotes.json as rows — how each remote is reached, whether it answered, what build it is running — plus the supervised tunnel jobs derived from it
Access & listening Whether this is a personal or shared machine, and who can reach its daemon
Daemon status Build, CLI contract, poll health, running workers, and the boot quarantine

Host configuration sections offer an advanced text editor for their files. That is what makes the sheet hold all the configuration rather than all of it there is a widget for, and it is the only safe way to touch remotes.json: a structured round trip drops every key the model does not know about, and that file carries several (auth, ssh_flags, tunnel.label, per-entry timeouts) that shuttle remotes add has no flag for. A save is refused unless the tool that really reads the file accepts it first, and the refusal is that tool's own sentence — see the API reference.

Two things the sheet will not do. It will not edit ~/.shuttle/host: the daemon freezes its host id at boot, so a file rewritten under a live daemon would leave the CLI and the dispatcher disagreeing about what this machine is called, and that is the worst failure this system has. And when SHUTTLE_STORES or SHUTTLE_PROJECTS is set in a daemon's environment — which overrides the file's contents outright — the section says so and turns editing off, rather than letting you carefully fix a setting that has no effect.

A card that never appears at all is usually a dispatch question rather than a board question — see Diagnosing a missing card.