The factory view
flywheel factory on a terminal opens the interactive factory: a live view of the floor laid out
like k9s (issue #583). flywheel factory --plain redraws the plain floor instead, and
flywheel factory --once (or --json) prints it once and exits; neither changes with this layout.
The screen
From top to bottom:
- The header (Ctrl-E hides it). The left column is the factory’s context:
repo <dir> · <integration branch>, led byctx <name> ·once the view knows the ledger’s fleet name:--ctx, a switch, or:ctxlisting it (see Contexts)factory running, orfactory FROZEN since HH:MM until HH:MM by <session>: <reason>whileflywheel suspendholds it (nountilwhen it lasts untilflywheel resume)- one
paused <model> until HH:MMline per model a rate limit pauses lead <session> · workers <busy>/<max> · landed today <n> · $<cost>, the cost (like the middle column’s spend) compact rather than cut:$7.25under $100, then$711,$1.2k,$3.4Mhealth <age> agofor the latest health event, orhealth noneflywheel <version>(devfor a local build)
The middle column is the last 24 hours’ numbers (see Metrics):
throughput <n>/24hwith a sparkline of the landings per hour,wip <n>,first-pass <n>%,andons <n>,spend 24h $<n>andworkers <busy>/<max> busy. The live view computes them at most every 30 seconds, so a key press never waits for them. The column shows only while the whole key menu still fits beside it; a narrow terminal drops it first.The right column is the key menu of the screen shown:
<:> view </> filter <enter> why <l> log <?> help <q> quit <ctrl-e> header .... It takes four rows, or five when four do not fit, and as many columns as fit beside the context column (which keeps at most three fifths of the width, its long lines cut with~). A hint is never cut: when five rows still do not fit, whole hints are dropped from the end and<?> helpstays, since help lists every key. Each view lists only the keys valid there, so the workers view has no<enter> why. A frame too short to keep five table rows below the header drops the header on its own. - The title bar:
── Units(all)[3] ──, the view, its filter and its row count, then the sort when one is set (── Units(/build)[2] ↑stage ──); in a unit’s detail the tab and the unit,── Why T3 ──,── Explain T3 ──,── Brief T3 ──,── Log T3 ──,── Checkpoints T3 ──,── Findings T3 ──or── Events T3 ──; in another drill-down── Learning L-02 ──,── Checkpoint T3/2 ──or── Views ──;── Help ──in help. The search view’s title names its text:── Search "disk full"(all)[12] ──. The metrics views name their window:── Pulse 24h ──,── Metrics 7d(all)[24] ──,── Metric lead time 24h ──. - The table, the drill-down’s lines or the help text. The cursor row starts with
>. - The flash line: the latest message (an unknown view, a toggle, a reload). The next key clears it, or it clears itself after five seconds.
- The crumbs (Ctrl-G hides them): the navigation path as tags, e.g.
<units> <T3> <log>. While a prompt is open the last line is the prompt,:or/followed by what you typed.
Hiding the header or the crumbs gives their lines to the table.
Keys
| Key | Where | What it does |
|---|---|---|
j / Down, k / Up |
table, drill-down, help | move the cursor, or scroll |
g / Home, G / End |
table | first row, last row |
| PgDn, PgUp | table, drill-down, help | a page down or up |
: |
table | the view prompt: a view by name or alias (see Views), s <text> to search, q to quit |
/ |
table | the filter prompt; the table filters as you type, Enter keeps it (see Filters) |
| Enter | units, andon, tree | the unit’s detail, at its why tab (see Unit detail) |
| Enter | learnings, checkpoints, search | the learning in full, the checkpoint’s changed paths, the result’s unit at the match |
| Enter | ctx | switch the whole view to that fleet ledger (see Contexts) |
l |
units, andon, tree | the unit’s detail at its log tab |
w d y l c F e |
unit detail | switch tab: why, explain, brief, log, checkpoints, findings, events |
J (Shift) |
unit detail | open the detail of the unit’s first need that has not landed |
f |
drill-down | fullscreen: hide the header and the crumbs and give their lines to the drill-down; again to leave |
G (Shift) |
drill-down | the last line; in the log tab, follow the unit again |
w t |
log tab | wrap long lines (instead of cutting them); show or hide the timestamps. In the log tab w wraps, so the why tab is Esc then Enter away |
1 2 3 |
pulse, metrics | the window: 24h, 7d, 30d |
arrows, h j k l |
pulse | move between the panels |
| Enter | pulse, metrics | the metric’s drill-down: the panel’s first metric, the row’s metric (see Metric drill-down) |
M W (Shift) |
metrics | split the metric under the cursor by model, by worker; the same key again, or Esc, returns (see Split by model or worker) |
h u |
metric drill-down | the chart, the units behind the number |
| Enter | metric drill-down, units | the unit’s detail at its why; Esc comes back to the units |
| Esc | everywhere | leave the drill-down, help or prompt; in a table, clear the filter, else go back to the previous view |
- |
table | swap to the previous view, and back |
[ / ] |
table | step back / forward through the : commands entered |
N A S C (Shift) |
table | sort by the first column, age, stage (or state), cost; again flips the direction |
v i r x |
units, unit detail | validate, inspect, resume, withdraw the marked units or the one shown, after a y/N (see Actions) |
| space | units | mark or unmark the row; Esc clears the marks first |
Z R (Shift) |
units, unit detail | suspend, resume the factory, after a y/N |
:result |
table | the last action’s whole output |
| Ctrl-A | everywhere but a prompt | list every view and alias in the drill-down pane |
| Backspace | prompt | delete the last character |
? |
table, drill-down, help | show or leave help |
q |
table, drill-down, help | quit (in a prompt it is typed) |
| Ctrl-C | everywhere | quit |
| Ctrl-E | everywhere | show or hide the header |
| Ctrl-G | everywhere | show or hide the crumbs |
| Ctrl-W | everywhere | wide: show every cell whole; normally a cell before a row’s last is cut to 40 characters with ~ |
| Ctrl-R | everywhere | reload now: read the whole ledger afresh instead of waiting for the next refresh |
The terminal delivers Ctrl-A to Ctrl-Z as control bytes; Ctrl-C, Ctrl-H (Backspace), Ctrl-I (Tab)
and Ctrl-J / Ctrl-M (Enter) keep their usual meaning. Ctrl-Space and Ctrl-\ are decoded too,
for the keys to come.
Views
| View | Alias | Rows |
|---|---|---|
units |
u |
every unit: stage, attempt, session, model, steps, age, state, and last its why (see Unit detail) |
workers |
w |
the worker lines and the staffed roles |
andon |
a |
what stopped or holds the line, worst first, each with its next step (see Andon) |
events |
e |
the recent events, newest first |
lines |
l |
the product lines |
tree |
t |
the needs tree of every unit not landed, like k9s xray: each unit no other open unit needs is a root, its needs are its children (├─, └─), each node task stage; a landed need is a leaf, a cycle is cut and marked cycle |
health |
h |
the recent health events: time, age, running, stalled, rate-limited, andon, paused models |
learnings |
lr |
the learnings, newest first: id, title, severity, task, dismissed |
checkpoints |
c |
refs/flywheel/checkpoints/<task>/<attempt>: task, attempt, sha, files, age (read from git as the view opens and on Ctrl-R) |
pulse |
p |
the metrics dashboard: six panels (see Pulse) |
metrics |
m |
every metric: family, metric, value, trend, change, definition (see Metrics table) |
search |
s <text> |
the search results (see Search) |
ctx |
fleet |
every factory ledger on the machine, and Enter switches to one (see Contexts) |
An unknown name flashes unknown view :x (Ctrl-A lists them).
Contexts
:ctx (or :fleet) is k9s’s context list for the fleet (issue #585): every ledger
flywheel fleet status lists, idle worktree ledgers folded the same way, as NAME, KIND, STATE
(running, paused, SUSPENDED or error: <why>), RUNNING, ANDON, PAUSED (the models a rate
limit pauses), HEALTH, LAST and PATH. The ledger the view shows is marked (*) in NAME. The table
filters and sorts like the others. It reads every ledger, so the view reads the fleet only while it
is shown, at most once per refresh interval (Ctrl-R reads it again). With no registry, or an empty
one, it shows one line: flywheel fleet add <path> registers a root.
Enter on a row switches the whole view to that ledger: the units view, fresh crumbs, the old
ledger’s views and caches dropped, and the header’s first context line starts with
ctx <name> (and the kind when it is not a root, ctx repo/wt (git-worktree)). :ctx again
stars the new one. A ledger that fails to load (its directory or its .flywheel gone) flashes
ctx <name>: <why> and the view stays where it was. An idle fold stands for several ledgers, so
Enter on it only flashes how to list them.
flywheel factory --ctx <name> starts in that ledger, named as flywheel fleet status names it
(--dir is then ignored); --once and --json render it. An unknown name is a usage error (exit 2)
listing the known names.
Headless frames
flywheel factory --keys SEQ --frames draws the interactive view without a terminal: the same
screens, keys and default skin as the live view, at --width (default 100) by --height (default
30) and at --now when given (which also fixes the zone times are shown in). It presses SEQ token by
token and prints one JSON array, [{"key": "", "frame": "<ANSI text>"}, {"key": "j", ...}]: the
first frame, then one after each token. A token is one character (j, /, :), a named key
(<enter>, <esc>, <tab>, <up>, <down>, <left>, <right>, <pgup>, <pgdn>, <home>,
<end>, <backspace>, <delete>, <space>, <ctrl-a> … <ctrl-z>) or a quoted run typed one rune
at a time and drawn once ("andon"). An unknown token is a usage error (exit 2), and --keys
without --frames is one too.
flywheel factory --keys 'j <enter> : "andon" <enter>' --frames --width 100 --height 30 --now 2026-09-27T09:30:00Z
The live demo is built this way: scripts/demo-web.sh runs a real session with the
offline sim adapter, renders the tour with --frames and writes docs/demo/demo.json;
--check fails when the committed file no longer matches what the CLI draws.
History
Every view you open with : goes on a stack, and the crumbs show it: <units> <workers> <andon>.
Esc goes back one level (after clearing the view’s filter, if it has one), - swaps the current
view with the previous one, and [ / ] re-run the previous / next : command you entered. A view
keeps its cursor, filter and sort: coming back to it restores them.
Filters
/text: a case-insensitive regular expression over the row’s cells. An invalid expression matches literally and the flash line saysliteral match./!text: the inverse, the rows the expression does not match./-f text: fuzzy, the rows holding every character oftextin order.
Sort
Shift-N sorts by the first column (METRIC in the metrics table), Shift-A by age, Shift-S by stage (or state where there is no
stage), Shift-C by cost where the view has a cost column (none has yet; the flash says so). The
same key again flips the direction; the title bar shows it (↑stage, ↓stage). The sort is
stable, so rows with equal keys keep their order. Ages sort as durations and numbers as numbers.
Sorting by the column under a column cursor (k9s Shift-O with Shift-Left/Right) is not there: the
terminal decoder does not report Shift-Left/Right. The tree keeps its order.
Unit detail
Enter on a unit opens its detail. The first line is its why (below), then the tab’s lines. A single
key switches the tab, and the crumbs name it (<units> <T3> <brief>):
| Key | Tab | Lines |
|---|---|---|
w |
why | the why and the timeline (the tab Enter opens) |
d |
explain | flywheel explain of the unit |
y |
brief | the brief’s text, the file its latest planned, amended or dispatched event names |
l |
log | the worker’s run: one line per tool call of its latest attempt (see The log tab) |
c |
checkpoints | its refs/flywheel/checkpoints/<task>/<attempt> and their changed paths (read from git as the tab opens and on Ctrl-R) |
F |
findings | its review findings, OPEN or closed, each followed by the responses to it |
e |
events | its events as the ledger records them |
Shift-J opens the detail of the unit’s first need that has not landed; Esc returns to the list, the cursor where it was.
Why
One or two plain sentences, from the ledger’s facts only, the same on every machine (times in
UTC). They name the state, the reason and the next step as flywheel recover would:
- planned:
blocked: needs k1, which has not landed., orqueued: planned and its needs are met; next: flywheel run k2.(or that the factory is frozen) - dispatched:
queued: attempt 2 dispatched 3m ago, waiting for the worker to start. - running:
building: attempt 2, 14 steps, running for 23m.;stalledorsilentwhen the floor says so or nothing happened for longer than the worker’s stall timeout, with the timeout - finished:
rate-limited(the model and its reset),capped(the output cap and the peak reasoning),suspended(frozen by suspend at a time; resumes at the thaw or on flywheel resume),failed(the finish reason),validation failed(the failing gates and the first failing command),needs correction(the open blocking findings),finished, awaiting validation,validated, awaiting inspectionorvalidated, awaiting the review panel passed, awaiting landing,landed as <commit> (PR #n),withdrawn: <note>,lost
The units table shows it as its last column, WHY, cut to the frame; the detail has it whole.
Timeline
Every event of the unit in order, and a ·· <length> <what> row for each gap of more than five
minutes between two of them:
attempt N running: an attempt was dispatched or started and had not finishedwaiting for the rate-limit reset at HH:MM UTC: the gap follows a rate-limited finishfrozen by suspend: a factory suspension overlapped the gapwaiting for validation: the gap follows a finishwaiting for inspection: the gap follows a passing reading (owns checked or a gate passed)idle: anything else
The last line sums it up: total from the first event to the last, touch the attempts’ run time
(each dispatch or start to its finish or loss) and the flow efficiency, touch over total.
Search
:s <text> (or :search <text>) searches, case-insensitively, the ledger’s events, the run logs
(.flywheel/runs/*.jsonl), the reports (.flywheel/runs/*.report.md) and the briefs
(.flywheel/briefs/*.txt), and shows one row per matching line: SOURCE (event, log,
report, brief), TASK, ATTEMPT, LINE (an event’s line in the ledger) and an excerpt, the match
with about 40 characters on each side. It stops at 500 results and the title says
500+ (narrow the search). Enter on an event opens the unit’s log at the match; on any other
result, the unit’s detail at its explain tab. The search runs when you enter it and on Ctrl-R, never on every
refresh.
Metrics
The metrics views read the factory’s lean metrics (metrics.md) over a window: 1
the last 24 hours (1-hour buckets, the default), 2 the last 7 days, 3 the last 30 days (1-day
buckets). A trend arrow compares a value with the same metric over the previous window of equal
length: ↑ higher, ↓ lower, → equal. Every number opens its chart, and the chart the exact
units behind it.
Pulse
:pulse (:p) is six panels in a grid: three columns at 110 cells or wider, two at 72 or wider,
one below.
| Panel | Lines |
|---|---|
| FLOW | landed, wip, lead p50/p90 |
| QUALITY | first-pass yield, rework, gate fail rate, findings per reviewed unit |
| RELIABILITY | andons, MTTR, frozen, paused |
| COST | spend, cost per landed unit, tokens per step |
| CAPACITY | utilization, idle |
| BY MODEL | a bar per model: its cost per accepted unit (– when none of its units was accepted) and its spend; a narrow panel drops the spend, never cutting an amount |
Each line is the value, the sparkline of its per-bucket series (landed, wip, andons and spend
have one) and its trend arrow. The panel under the cursor is marked ▶; the arrows or h j
k l move it, and Enter opens the drill-down of the panel’s metric: lead time for FLOW, the
first line’s metric for QUALITY, RELIABILITY and CAPACITY, cost per landed unit for COST, the
by-model scoreboard for BY MODEL.
Metrics table
:metrics (:m) lists every metric: FAMILY, METRIC, VALUE, TREND (the sparkline, where the
metric has a series, and the arrow), Δ PREV (the change from the previous window, = when none)
and DEFINITION (its one line from metrics.md). Shift-N sorts by METRIC; / filters.
Enter opens the metric’s drill-down.
Split by model or worker
Shift-M on a metric splits the window by model, Shift-W by worker; the title names the split,
── Metrics 24h · spend by model(all)[2] ──. Each row is MODEL (or WORKER), SPEND, LANDED,
FIRST-PASS and PER LANDED:
- by model, from the metrics’ by-model scoreboard: landed is the model’s accepted units, first-pass its accepted share of the inspected ones
- by worker, from the dispatched events’ worker (
-for an older dispatch that names none): spend is its attempts’ finishes in the window, landed the units landed in the window whose latest dispatch was its, first-pass those landed on their first attempt
– marks a share or cost with nothing to divide by. The other key switches the split; the same
key again, or Esc, returns to every metric with the cursor on the metric split.
Metric drill-down
The value, its trend and change, and its definition, then one of two parts; the crumbs name it,
<units> <pulse> <lead time> <units>:
h, the chart (the part it opens at): a histogram with the p50 and p90 marked for lead, queue and touch time and MTTR; a control chart (mean and ±2σ, units in landing order) for cycle time; the WIP over time as a cumulative flow, one band per stage (queued, running, finished, passed; seestage_series); bars for a breakdown (gate fail rate per gate, andons by kind, pause per model, cost per unit by model, utilization per worker) and for a split of the units (first pass or corrected, severity, landed or not); else the series as one wide sparkline.u, the units behind the number (its evidence): TASK, VALUE (the unit’s part, e.g.lead 9h12m,corrected x2,stalled 09-02 13:58, open,$3.40) and GROUP, worst first.jkmove the cursor; Enter opens that unit’s detail at its why and timeline, and Esc from it comes back to the same row.
Instant keys
A key never waits for the ledger. The view redraws at once from the data it last read; reading
runs in the background, every refresh interval, and on Ctrl-R, and the frame redraws when it ends.
A key that needs other data (another view, a unit’s tab, a search, a metrics window) starts a
read too, and until it arrives the drill-down says loading…. Only one read runs at a time: a
refresh or Ctrl-R asked for while one runs starts right after it. Only the first frame waits for
its data.
Colours
With colour on, each row of the units, andon and tree tables is drawn whole in the colour of its state (a landed unit by its stage, else by its STATE, else by its STAGE):
| Colour (dark skin) | States |
|---|---|
| cyan | running, exploring, long-step |
| green | passed, done, validated |
| yellow | waiting, planned, dispatched, finished, blocked, needs-correction, rate-limited, stacked |
| red | failed, stalled, silent, capped, provider-error, mismatch, no-writes, lost |
| magenta | suspended, frozen |
| dim | landed, withdrawn |
The header’s factory running takes the running colour and factory FROZEN the frozen one; the
key menu’s <key>s and the crumbs have their own colours. The cursor row is in reverse video.
Changed rows
Like k9s’s MODIFIED and NEW markers, a row of the units, andon, tree, workers or lines table whose cells changed since the previous read, or that was not there before, is drawn bold for two refreshes. Its age and its WHY move with the clock, so they alone do not count as a change. The first read marks nothing.
Skins
factory.skin in .flywheel/config.json picks the colours: dark (the default), light (darker
colours for a light terminal), or none for no colour at all (the cursor’s reverse video
included). Any other value is refused when the config is read.
{ "factory": { "skin": "light" } }
flywheel factory --plain, --once and --json do not use the skin.
Andon
:andon (:a) lists what stopped or holds the line, worst first (entries of one severity
newest first):
- UNIT: the unit, or a factory-wide entry:
model/<model>(a rate limit pauses it),health(the controller stopped recording),factory(suspended),staffing/<role>,group:<id> - SEVERITY:
highfor failed, stalled, silent, git-write and permission-denied;mediumfor rate-limited, a paused model, stale health and no-plan;lowfor the rest - SIGNAL: the condition as the floor names it (
stalled,capped,paused until 13:00, …); the row takes its colour - SINCE: the clock time it began, so a refresh leaves it unchanged
- WHAT HAPPENED: the unit’s why on one line, or what the factory-wide entry means
- NEXT:
flywheel recover’s next command for the unit (its action and reason when it has no command), decided from the ledger alone: the view never runs recover’s world checks (worktrees, leases, run files, git), which take minutes on a large ledger, so it assumes the worktree matches the ledger. Where only those checks can decide — an attempt still in flight (lost or live), afailed-dirtyunit’s uncommitted changes — NEXT readsflywheel recover; astackedunit’s isflywheel rebase <task>. A paused model’s iswait for the reset, then flywheel supervise --resume-limited, stale health’sflywheel controller, a suspended factory’sflywheel resume --session <s>.
UNIT leads the row: Enter and l open that unit, and it keys the changed-row marks.
The log tab
The log tab is the worker’s run: the latest attempt’s run file (.flywheel/runs/<task>.<attempt>.jsonl)
read through the adapter its dispatched event names. The first line is the dispatch
(10:00:00 dispatched T1 attempt r1 on claude-opus-5-5 (claude)), then one line per tool call:
--:--:-- #3 edit internal/flywheel/tui.go +12 −4
--:--:-- #4 bash go test ./internal/flywheel/ rc=1
--:--:-- ↳ --- FAIL: TestTUIRunLog (0.00s)
10:05:00 ended stop $0.42
the step number, the tool and its target (the file, or the command), +N −M for an edit whose
input names its old and new text, rc=N once a command’s result arrives, and under a failed
command its first failing test or compile line. A run stream carries no time per line, so a step’s
clock reads --:--:--; the last line is how the run ended, at the finished event’s time. A torn
last line waits for the next read, and the file is read again only when it grows. The unit’s
ledger events are the events tab (e).
A running unit’s log follows it: the last line stays in sight as lines arrive. Scrolling up (k,
Up, PgUp) pauses that and the title says paused; G to follow; G follows again. A unit that
does not run opens its log at the first line. w wraps long lines instead of cutting them, t
hides or shows the timestamps, and f (as in every drill-down) goes fullscreen.
Actions
In the units view and a unit’s detail, a key acts on the marked units, else on the unit under the cursor (the detail’s unit in a detail). Each runs the flywheel binary itself as a subprocess with explicit arguments, so the CLI’s own rules (T3, T4, the locks) decide; the view never writes the ledger.
| Key | Runs |
|---|---|
v |
flywheel validate <task> --dir <dir> |
i |
asks p pass, r rework, s scrap or e escalate, then flywheel inspect <task> --verdict <v> --session $FLYWHEEL_SESSION --dir <dir> |
r |
flywheel run <task> --resume --dir <dir> |
x |
flywheel log --task <task> --kind withdrawn --note "withdrawn from flywheel factory" --dir <dir> |
Z |
flywheel suspend --dir <dir>, with --session $FLYWHEEL_SESSION when set |
R |
flywheel resume --dir <dir>, with --session $FLYWHEEL_SESSION when set |
S stays sort by stage, so suspend is Z. i with FLYWHEEL_SESSION unset flashes set
FLYWHEEL_SESSION to your own session to inspect and does nothing: an inspection is recorded as
your session, never a worker’s.
Every action asks first on the flash line, validate T1 T2? y/N, with the command it will run on
the line below. y runs it; any other key cancels and flashes cancelled. While it runs the flash
reads running: flywheel validate T1 --dir . and every action key says busy: validate T1: one
action at a time, and a key never waits for it. When it ends the flash reads validate T1: exit 0,
or validate T1: exit 5 — <its last line>, and the next fetch shows the new state. :result
shows the whole output (Esc goes back). With several targets the commands run one after another
and stop at the first that fails.
Marks. space marks or unmarks the row under the cursor (a ● beside it) and moves down;
Esc in the units view clears the marks before it goes back. The marks clear once an action runs.
Read-only. flywheel factory --readonly turns every action off: each action key flashes
read-only: started with --readonly. Marks and views still work, and the header says
read-only.
Hotkeys. .flywheel/hotkeys.json binds a free key to a view command, read once when the view
starts:
{"hotkeys": {"K": ":pulse", "<ctrl-y>": ":andon", "L": ":s rate limit"}}
A key is one --keys token: a single character or a named key such as <ctrl-y>. A key already
bound, a command that opens no view, or a file that does not parse is skipped with one flash
naming it; the rest load. A hotkey works in any table view, as if its command were typed after :.
Coming next
- filtering by column, and sorting by the column under a cursor