fab state machines — today vs. light mode

discussion mock · 2026-08-11 · sources: internal/status/status.go · _pipeline.md · change-lifecycle.md · adopted — implemented by 260811-3ol6

The adopted design changes neither state machine. Machine 1 (the stage graph) and Machine 2 (the per-stage state vocabulary and its event transitions) are byte-identical before and after. What changes is the execution locuswho runs each stage's behavior — routed by a single plan-size check that rides an existing seam: plan.md is co-generated inline at apply entry in both modes, and the resulting task count decides the lane. No entry heuristic, no guessing from the intake — the decision point is a fact, not a forecast. Validated against loom's archive: 21% of 972 completed changes (195/927 measurable; 137 with PRs) would have run light end-to-end, and only ~7% of light runs would see any rework — handled inline, no mode switch. The change-type entry heuristic from the first draft is rejected — loom shows 68% of type-eligible changes blow past the size threshold while 154 small feat/fix changes slip through it.

machine 1 · stage graph — UNCHANGED machine 2 · per-stage states — UNCHANGED NEW · one plan-time fork, no mid-run mode machinery loom-validated · 195 of 927 stayed light (21%)
01

Machine 1 — the stage pipeline

intake → apply → review → hydrate → ship → review-pr · forward via finish (auto-activates next) · one backward edge (review rework)

Six stages, one manual gate (intake, where all human judgment is frontloaded), one rework back-edge (review fail → reset apply, bounded at max_cycles, default 3), and two special paths: /fab-adopt enters late with apply: skipped, and a Copilot-timeout leaves review-pr deliberately active. Node color shows the execution locus — that's the only thing the adopted design touches.

TODAY every post-intake stage runs in a dispatched worker (native subagent / tmux pane / headless CLI)

Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ opens full size

Loading...
AFTER · COMBINED LIGHT + FULL two fully parallel lanes, zero crossings — the size check is the only decision, made once. Review appears in each lane but is the same fresh-worker dispatch in both — drawn twice for legibility, never inline. (Adopt entry and the Copilot-timeout edge are unchanged — omitted.)

Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ opens full size

Loading...
dispatched worker / subagent
inline — orchestrator's own context
main session (intake — both modes)
parked failure
stage / steptoday (all changes)after — light laneafter — full lane
intake main session main session main session
plan co-generation (apply entry) inside the apply worker inline this is where the lane decision reads its input inline new for full too — the worker receives the finished plan (the plan-exists seam already in the contract)
task execution (apply) worker native / pane / headless, resumable across rework inline orchestrator already holds the always-load layer + intake + memory worker co-gen skipped; dispatch prompt can carry the task list
review fresh worker fresh worker — kept reviewer independence is the pipeline's highest-value dispatch; tiny diff ⇒ cheap anyway fresh worker
hydrate worker inline often near-no-op for small changes (empty Affected Memory) worker
ship /git-pr subagent inline mechanical: commit, push, draft PR /git-pr subagent
review-pr /git-pr-review subagent inline eliminates the yield-seam hazard the synchronous-poll directive exists to fight; the 10-min Copilot poll is wall-clock either way /git-pr-review subagent
Why the graph is untouched. The bracket stays the pure sequencer: the same fab status finish / fail / reset choreography fires in the same order, the same artifacts (intake.md, plan.md, result contract) are produced, and the review-iteration counter invariant (finish apply is the only counted re-entry) holds verbatim. Inline stages run on the session model and skip fab resolve-agent — consistent with the existing rule that an undispatched stage never switches the session model.
02

Machine 2 — states within a stage

internal/status/status.go · defaultTransitions + review/review-pr overrides · validated against AllowedStates per stage

TODAY AFTER: IDENTICAL — zero new states, zero new transitions

Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ opens full size · ® = review / review-pr only

Loading...
eventfromtoscopeside effects
start pendingactiveall stages pre hook runs first — non-zero exit blocks the start
start failedactivereview, review-pr recovery re-entry from a parked failure
advance activereadyall whose schema allows ready (not ship / review-pr)
finish active, readydoneall stages auto-activates next pending stage (the only counted review re-entry); post hook after save; auto-logs review/review-pr "passed"
reset done, ready, skippedactiveall stages cascades all downstream stages → pending; preserves iterations
skip pending, activeskippedall except intake (schema forbids the target) forward-cascades downstream pending → skipped (adopt's entry mechanism)
fail activefailedreview, review-pr auto-logs review/review-pr "failed"
refresh not a transitionall stages pull-based recompute of derived fields (change type, plan counts, confidence) — the one fab status command dispatched workers may run

Per-stage nuances from AllowedStates: intake can only hold active / ready / done (never pending, skipped, or failed); ship and review-pr can never hold ready; only review and review-pr can hold failed.

After light mode: this machine is untouched. Light mode deliberately adds no state and no transition. The full existing event surface is how the light bracket keeps resumability for free — an interrupted light run resumes through the same progress map, and a promoted run continues from whatever stage state it reached. (An earlier idea worth not pursuing: a light-done or merged-stage state would fork every consumer of this table — pr-meta, display glyphs, preflight routing.)
03

The lane decision — a one-time fork, not a machine

proposed · orchestrator-level, orthogonal to both machines above · decided at PLAN time, once, with no mid-run mode changes

Decided once, immediately after the inline plan co-generation at apply entry — the earliest moment the strongest size signal (the actual task list) is a fact. No intake-keyword inference: the first draft's change-type heuristic was rejected on loom's data (68% of type-eligible changes outgrow the threshold; 154 small feat/fix changes escape it). --light / --full skip the check. There is no promotion valve — light rework stays inline under the same max_cycles budget, and exhaustion parks to the manual menu identically in both lanes. A parked light run re-enters however the user chooses, including --full.

Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ opens full size

Loading...
The loom evidence (972 archived changes, 927 with a recoverable task count). Rework risk is a clean gradient over plan size: 7–8% for ≤5-task changes, 11% at 6–8, 16% at 9–12, 29% at 13+. The ≤5 cut leaves 210 changes light-eligible at plan time; 195 (21% of all) finish with zero rework, 137 of them with recorded PRs. Only ~7% of light runs would see any rework at all — handled inline, bounded by the same cycle budget as full. Analysis: loom_light_analysis.py over fab/changes/archive/**/.status.yaml + .history.jsonl review-failed events.
Why no valve is safe. Every diff — light or full — passes the same independent review, so a misclassified "small" change can waste inline rework cycles but never ship unreviewed. The bounded cycle budget and the parked review: failed state are the backstop, and because Machines 1 and 2 are identical in both lanes, a parked light run re-enters cleanly as full. Scope discovered mid-rework (a plan revision adding tasks) rides the same backstop rather than any automatic promotion. V1 keeps the lane in the orchestrator's context only (no .status.yaml field); a weight event in .history.jsonl (see 04) covers both resume-consistency and measurement.
What the light lane gives up, explicitly. Inline stages consume the orchestrator's context window (the reason full stays the default for real features), run on the session model ignoring per-stage agent.profiles, and skip pane/headless adapters entirely. The reviewer stays independent in both lanes — the one dispatch whose value survives at every change size.
04

Adjacent optimizations the data surfaced

side effects of the plan-time decision — each independently adoptable

#optimizationwhy it falls out of this design
1 Full mode gets faster too Inline plan co-gen means the dispatched apply worker starts with the plan in hand (the plan-exists seam already in the apply contract) — its cold start does task execution only, and the dispatch prompt can carry the task list.
2 Light rework = worker continuation for free In the light lane the orchestrator is the apply author — it remembers what the reviewer rejected with zero SendMessage / pane-delivery machinery. The entire § Worker Continuation apparatus is the full lane's problem only.
3 Guard the inline co-gen worst case If the intake's affected scope is obviously large, skip inline co-gen and dispatch apply with co-gen exactly as today — graceful degradation to the shipped path bounds the orchestrator-context cost of planning a big change inline.
4 Threshold as a project knob Loom's risk gradient is smooth — no magic knee at 5. A config leaf (e.g. pipeline.light_max_tasks, default 5) lets a repo with different change anatomy cut bolder (6–8 tasks = another 21% coverage at 11% rework).
5 Stamp the lane into history A weight event in .history.jsonl makes realized savings measurable later — this page's loom analysis was only possible because review outcomes were logged. It doubles as the resume-consistency record.
6 Attack the last wall-clock: the Copilot poll For a light run, the external 10-minute review-pr poll now dominates end-to-end time. Requesting the Copilot review during ship (at PR creation) lets the wait overlap hydrate-index regen and PR bookkeeping instead of starting cold at review-pr entry. Independent of light mode — benefits every run.