What changed, and why
froot runs durable maintenance loops on Temporal. The worker image already shipped four workflows: the dependency-patch scan loop and its per-bump driver, plus a determinism-review loop and its per-PR driver. But only the scan loop had a committed go-live Job. The review loop was built, registered, and tested in the image — then never started. Nothing in this k8s stack ran python -m froot.review_starter, so the workflow that took real effort to build sat dormant after every install.
This PR closes that gap. It adds manage/start-review.yaml — the one-shot Job that kicks the review loop, a near-exact sibling of start-scan.yaml — and threads it through install.sh and the README so go-live now starts both loops. The rest is doc accuracy: the worker comments, the README, and the top-level CLAUDE.md infra inventory had all drifted (single npm loop, an omitted stack, an unverifiable log claim). No worker code changes — this is deployment wiring plus making the docs match what the image actually does.
The star: the review go-live Job
This is the only new file. It is a one-shot Job that runs the worker image's python -m froot.review_starter, which submits one long-lived ReviewWorkflow per repo in FROOT_REPOS. The header comment is the contract: each tick re-derives the repo's open PRs, leaves an advisory determinism comment where a Temporal workflow risks a replay hazard, then continues-as-new with no cursor. It self-skips when FROOT_REVIEW_ENABLED is off (it defaults on, so a plain apply goes live).
The whole file — short by design. Read the header contract, then the env + resources.
infra/k8s/froot/manage/start-review.yaml · 54 lines
Sibling check: how it differs from start-scan
The claim that this is "the sibling of start-scan.yaml" should be verifiable, not taken on faith. Place the two side by side. The Job spec is identical — backoffLimit: 3, ttlSecondsAfterFinished: 300, restartPolicy: Never, the same tiny 25m/32Mi request, the same Temporal env triple, the same froot-config mount. Four things differ, and only four: the Job/container name, the component label, the module the command runs, and the header prose. That is exactly the surface you want a reviewer to confirm.
The existing scan starter, for direct comparison — same shape, scan_starter instead of review_starter.
infra/k8s/froot/manage/start-scan.yaml · 51 lines
⋯ 31 lines hidden (lines 1–31)
| Field | start-scan.yaml | start-review.yaml |
|---|---|---|
| Job / container name | froot-start-scan / scan-starter | froot-start-review / review-starter |
component label | scan-starter | review-starter |
command module | froot.scan_starter | froot.review_starter |
| workflow id (from header) | froot-scan-<owner>-<name> | froot-review-<owner>-<name> |
| resources | 25m/32Mi req, 200m/192Mi lim | identical |
Threading both loops through install + README
With the Job in place, go-live has to start it. install.sh previously printed a single "kick the scan loop" hint; now it points at both starters and the worker reads FROOT_REPOS too. The ConfigMap comment is corrected: it is no longer "the scan starter reads these" but "the scan + review starter Jobs read these, and the worker reads FROOT_REPOS too" — because the dashboard needs the repo list to frame its read-model.
The ConfigMap-comment fix, and the closing hint now kicking both loops.
infra/k8s/froot/install.sh · 63 lines
⋯ 38 lines hidden (lines 1–38)
⋯ 11 lines hidden (lines 46–56)
The README's install and operate sections move in lockstep: step 3 now applies and waits on both Jobs, the layout table lists start-review.yaml, and a new operate paragraph documents the review loop's own go-live Job, default-on toggle, and where its ReviewWorkflows surface in the Temporal UI. The opening line also broadens the scope from "dependency-patch loops" to "code-maintenance loops" spanning npm and Python (uv), with the determinism reviewer as the second, advisory loop.
The install block — both Jobs applied and waited on at go-live.
infra/k8s/froot/README.md · 116 lines
⋯ 43 lines hidden (lines 1–43)
⋯ 57 lines hidden (lines 60–116)
Doc accuracy: node-agnostic resources + an honest log claim
Two worker-manifest comments were quietly wrong, and this PR makes them honest.
First, the resource rationale used to reason about a two-node cluster ("the original node's request budget is ~98% reserved... while the second node has fresh headroom"). The cluster is single-node now, so that framing is stale. The new comment is node-agnostic: the request budget is tight, and with no metrics-server reserved != used, so the request is pinned tiny to schedule and the limit gives burst room. Same 50m/64Mi request — only the reasoning changed.
The resource comment, rewritten node-agnostic; the numbers are unchanged.
infra/k8s/froot/manifests/10-worker.yaml · 116 lines
⋯ 85 lines hidden (lines 1–85)
⋯ 23 lines hidden (lines 94–116)
Second, the telemetry comment used to assert "structured outcome logs go to stdout (free filelog)." That over-claims: it states a delivery the worker does not yet guarantee. The corrected comment states only what is verifiable — each outcome emits a structured loop_outcome line at INFO — and is explicit that stdout delivery follows once INFO logging is configured in the worker. The README and the operate hint carry the same softened wording.
The judge-model comment now names both judges, and the telemetry claim is downgraded to what's verifiable.
infra/k8s/froot/manifests/10-worker.yaml · 116 lines
⋯ 66 lines hidden (lines 1–66)
⋯ 38 lines hidden (lines 79–116)
Closing the inventory gap: CLAUDE.md
The top-level CLAUDE.md keeps a one-line-per-stack infra inventory: observability, temporal, tailscale. The froot stack was missing — an omission, since it is a deployed, install-script-managed workload like the others. This PR appends it: a froot bullet that names the worker, the four-workflow surface (dependency-patch for npm + uv plus the advisory determinism-review loop), the read-only dashboard, and that it points outward at GitHub repos and rides the Temporal cluster, Ollama egress, and ClickStack — deployed via its own install.sh, not Terraform.
The inventory line — the froot clause is the appended tail (the rest is unchanged context).