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:

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

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:

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:

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.

: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:

– 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>:

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 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