Unchained Labs

localflow

Four terminals open. Two of them are waiting for you.

You have several Claude Code sessions running right now. One is working, some are blocked on a question you have not noticed, and one of them has quietly spent more than the rest of the week combined. localflow reads the registry and the transcripts already on your disk and puts all of it on one board.

  • alpha
  • 62 tests passing
  • zero runtime deps
  • reads only your own disk
  • MIT
The dashboard itself, driven by a real server over the DevTools Protocol against a machine built for the reel — the board, a card, another tool's sessions, the detail drawer, the fan-out a session actually performed, the reroute and spawn dialogs, what graphlint and preflight say about that graph, burn rate and the five-hour block, spend over time, the freshwater estimate, and the machines it can start work on.

The board

Four lanes, and deliberately no fifth

Every Claude Code session on the machine, live. The lane mapping is dull on purpose, because every interesting version of it involves guessing.

The localflow board: four lanes of live sessions with tokens, cost and cache share, including cards from Codex and Gemini
Fourteen sessions on one machine, from three different tools. The bar under each card is the share of input tokens served from cache — the single biggest lever on what a session costs.
Lane What it means
running The registry says busy. The model is working.
queued Idle with prompts enqueued and not yet started.
waiting on you Idle with an empty queue. It wants an answer — and a session blocked on a question is invisible from the terminal you are not looking at.
ended Gone from the registry.

There is no "done" lane. Nothing on your disk records whether a session achieved what it was asked to do — only that it stopped. A board that rendered "ended" as "went well" would be inventing the one fact you most want, which is the same defect authsweep calls a false clean, wearing a green tick.

$ localflow board

  {hi}localflow{/hi}  7 session(s) · 1.9M out · 738.9M cached in · $593
             98% of input tokens came from cache

  running (2)

    {hi}Build whitespace agent workflow tooling in unchained-labs{/hi}
      graph-claude-46 · opus-5 · 660k out · $184 · 99% cached · 38s
      ~/dev/graph-claude · last: Bash · 23 tool errors

   (3)

    {hi}create a new repo for socials where we will have private docs …{/hi}
      wardn-22 · opus-5 · 839k out · $328 · 97% cached · 2d
      ~/dev/bench2clanker · claude/bench2baller-mt167r · last: Bash

Nothing is sent anywhere. No daemon, no config, no account — the data is already on your disk and localflow reads it with claude agents --json, which is a supported, TTY-free command, plus the transcripts Claude Code writes anyway.

Cost

Two corrections, both invisible unless you go looking

The board prices every session from the token counts in its own transcript. Getting that right needed two fixes, and neither would have announced itself.

Usage is re-emitted as a message streams

The same usage object appears up to ten times per message, byte identical, once per streamed update. Summing every line inflated output tokens by 2.25x on a real 17MB transcript, which would have made every figure on the board wrong by that factor. Counting once per message id is exact rather than approximate — every duplicate was identical.

Cache writes are billed by how long they live

A 1-hour cache write costs 2x the input rate; a 5-minute one costs 1.25x. Claude Code writes 1-hour entries, so a single 1.25x multiplier under-prices a real session by about a third.

This is measured, not recalled. claude -p --output-format json reports total_cost_usd for the run it just did, which makes it an oracle: pick multipliers, price the reported tokens, and see whether the arithmetic lands on the number the CLI printed.

  531 input + 22188 cache-read + 3026 cache-write(1h) + 51 output, haiku

  1.25x -> $0.0067873000
  2.00x -> $0.0090568000
  CLI   -> $0.0090568000

The test suite re-runs that check against a captured fixture on every commit, so a rate change fails the build instead of quietly changing your bill. Where no price is known for a model the card reads cost unknown, never $0.00 — the first is a fact and the second is a lie.

The graph that ran

Lint the run you already paid for

graphlint lints the workflow you wrote. That is the easy half: a spec is a statement of intent, and intent is not what your bill is made of.

Every fan-out Claude Code performs is recorded — which agents were issued together, how wide the group got, which came back with an error. So the graph can be reconstructed afterwards and handed to exactly the same tools.

localflow graph f60740f7 > observed.graph.json
graphlint check observed.graph.json     # lint the run you already paid for
preflight estimate observed.graph.json  # price the next one like it

Agent calls sharing an assistant message ran concurrently; calls in separate messages ran one after another. The grouping is the graph. Each observed sequence is emitted as a barrier whose reason begins observed: — a measurement, not an opinion about whether the barrier was needed. That question belongs to graphlint, which now has real graphs to ask it about instead of only the ones people wrote down.

Shape, not intent. A transcript records the calls, not the reasoning behind them. localflow can see that a barrier happened; it cannot see whether one was needed, and it does not pretend to. It adds only the observations that need the measured numbers to be sayable at all — three verifiers whose prompts are 100% alike, a fan-out where children failed, a session running at a 12% cache hit rate — and does not duplicate graphlint's rule set, because two rule sets eventually disagree.

And the number preflight said it could not measure

preflight's calibrate is careful about one refusal: it does not invent a cache hit rate, because usage rows do not report cache reads. True of the rows it had. A Claude Code transcript reports cache_read_input_tokens and a cache_creation object split by TTL, so here it is a measurement — and it is the assumption a cost model is most sensitive to, since cache reads are ~98% of input tokens on a real session and bill at a tenth of the rate.

$ localflow calibrate > preflight.json

  measured across 11 session(s), 900 model call(s)

  cacheHitRate          97.9%   measured, not assumed

  reported, not written:
    input per call       332,433   p10–p90 27,648–1,070,519
    output per call        2,413

Only the rate is written, and the reason is the interesting part. preflight's worker profile means one unit of work and defaults to 8k input. An interactive session measures 332k per call, because by call two hundred the context is the conversation. Both numbers are correct and they are not the same quantity — writing the second into the first would be a fortyfold error wearing the authority of a measurement, which is precisely the failure preflight's own refusals exist to prevent.

The session detail drawer, showing tool calls and the fan-out graph the session performed
The detail drawer, scrolled to the fan-out the session actually performed.

Orchestrating

Four verbs, and one deliberate absence

Off by default. localflow watches; it steers only when you ask it to, with --allow-actions.

Verb What it runs
spawn claude --bg -p <prompt> — a background agent, in a directory you allowed
reprompt claude --resume <id> -p <prompt> — another turn on the same session
reroute claude --resume <id> --fork-session --model <m> — the same conversation, a different model. The original is left exactly as it was.
stop SIGINT to the session's pid, which is what Ctrl-C sends

The fifth verb is missing on purpose. A board like this obviously wants to inject a prompt into a session that is mid-turn. Each live session has a Unix socket under /run/user/<uid>/cc-socks/, and driving it would mean reverse-engineering an undocumented protocol that can change in any release. So reprompt on a busy session refuses and tells you to fork instead. A tool that steers your agents through a private channel is a tool that silently stops steering them one Tuesday.

Dragging a card asks for the action that would put it in that lane. Most moves have no such action — you cannot drag a session into running, because what makes a session run is having something to do — and the lane refuses in red and says why, rather than snapping the card back and pretending nothing happened.

Safety

This can start Claude sessions, so it is fussy about who asks

A page on the open internet can point your browser at 127.0.0.1. Four things stop that page reaching this server, and CI asserts every one of them on each commit.

Actions are off

Without --allow-actions every mutating route answers 403. Watching is the default; steering is opted into.

Host is checked

DNS rebinding resolves an attacker's hostname to 127.0.0.1, and the give-away is that the browser still sends their name in Host.

Origin is checked

A cross-site POST carries the originating site. A fetch from our own page carries ours; curl sends none. Anything else is refused.

Loopback by default

Binding anywhere else has to be spelled out, and prints a warning saying who can now read your transcripts.

Limits

What it does not do

It cannot tell you whether a session succeeded. That is not recorded anywhere it can read, so it does not guess.

It reads Claude Code only. Not Cursor, not Aider, not the API directly. The board is built on claude agents --json and the transcript layout — the first is a supported command, the second is not a published schema. It was derived from a real installation and a future version could move it.

Tool errors are not failures. A tool_result with is_error is routine: a grep that matched nothing, a command that exited 1. The count is shown; no card is marked broken for it.

History is capped. Ten ended sessions by default. A machine with a year of transcripts should not turn the board into an archive.