Overview
froot had three one-shot starter modules — scan_starter, review_starter, a11y_review_starter — each run after the worker boots to submit its loops' long-lived Temporal workflows. They were 98% identical: connect, loop over the repos, start a workflow with ALLOW_DUPLICATE_FAILED_ONLY, print. They differed only in the gate, the workflow, and the params.
This collapses them into one froot.starter that walks the loop registry once and branches on disposition: acting loops in FROOT_LOOPS get a ScanWorkflow; enabled advisory loops get their own review workflow. The decision is a pure, tested function; the Temporal I/O is thin glue. Net −226 lines, and adding a loop no longer means a new starter module.
The pure unit of work
A _Start is one workflow to submit: the registered workflow type name (started by name, so the starter imports no workflow classes), the params, the deterministic id, and a label. An _Advisory is the bespoke start-wiring for one advisory loop — its workflow type, id namer, params class, and its own enable flag and interval. The advisory roots are still their own (the per-PR engine is not unified here), so this is the one seam that stays per-loop.
src/froot/starter.py · 243 lines
⋯ 46 lines hidden (lines 1–46)
⋯ 150 lines hidden (lines 94–243)
The advisory wiring is one small table, keyed by loop — the two emit-signal loops and how each starts.
src/froot/starter.py · 243 lines
⋯ 94 lines hidden (lines 1–94)
⋯ 122 lines hidden (lines 122–243)
The decision: one pass over the registry
plans() is the whole decision, pure: given the repos, the configured FROOT_LOOPS, the scan interval, and the advisory wiring, it returns every workflow to start. One pass over the registered specs, branched on disposition — an acting loop is started iff it is in FROOT_LOOPS; an advisory loop iff its wiring is enabled.
src/froot/starter.py · 243 lines
⋯ 122 lines hidden (lines 1–122)
⋯ 65 lines hidden (lines 179–243)
The I/O: connect, then dispatch by name
_start() reads the settings, builds the plan, and — only if there is something to start — connects and submits each plan. It keeps every old starter's contract: ALLOW_DUPLICATE_FAILED_ONLY and a caught WorkflowAlreadyStartedError, so a running loop is left untouched and a terminated one can restart.
src/froot/starter.py · 243 lines
⋯ 186 lines hidden (lines 1–186)
The tests the old starters never had
The starters were untested I/O glue. Factoring the decision into a pure plans() makes the gating and routing testable without Temporal — the discriminating behaviour. The defaults start exactly a dependency scan and a determinism review; an advisory loop in FROOT_LOOPS spawns no scan; each loop's interval is its own, not swapped.