Twinned from https://github.com/cfsh/ninefold/issues/543 by tools/beads/import_github.py's reconcile pass.
Aedan, in chat, right after #530 merged:
> "regarding 530 and yes this correctly closes 503 - given the issues /risks however that still seem to persist, i wonder if we can design a longer-term solution (per PRINCIPLES.md) - perhaps we have a single rebuild_f5 clone, and we run a scheduled script similar to reviewer - and it is looking for any new PRs that justify it running rebuild f5 (and invoking claude to help only on script failure - that claude invocation can file bugs if it wants PRs to fix stuff systemically). > > ninefold-devs can somehow 'ping' the scheduled script to run when they have new PR content worth merging perhaps (idk what the mechanism would be). > > We can consider this a thing to do post move to WSL if that would make the infra more unix like / better"
I concur, and the reason is sharper than "it would be tidier": #530 fixed the blast radius but not the ownership.
The worktree means a rebuild can no longer damage the session that runs it. That closes #503, #512 item 1, and the whole hand_back/start_ref/sweep family. What it does not touch is that *every dev session still runs the rebuild*, which is where the remaining risk lives:
main + every open PR after every merge and every PR it opens. Three dev sessions doing that is three identical integrations, ~11s and a force-push each, racing each other to f5/integration. The winner is whoever pushed last, and nothing records which.rerere's rr-cache lives in each clone's .git. I resolved two conflicts today (#519 vs #530 in rebuild_f5.py; #515 vs piece11 in design_docs/README.md) and both resolutions exist only in ninefold-c. A/B hit the same conflicts cold and resolve them again, possibly differently. That is a shared-state problem masquerading as a local one, and a single owner deletes it.None of these is a defect in #530. They are all consequences of *who runs it*, which #530 deliberately did not change.
ninefold-reviewer already solved this exact shape: a repo-wide job that must not run N times, owned by one scheduled process in its own clone, invoking Claude per unit of work rather than continuously. It has the operational lessons banked too — #352 (a dead dispatcher ran 38 hours undetected), #467 (a 45-minute lock silently suppressing work), #483 (logs growing unbounded), and poll.py --check's STALE-DISPATCH liveness probe. A second scheduled service should inherit those rather than rediscover them, and the STALE-DISPATCH pattern in particular should extend to cover it — a silently dead f5 rebuilder is exactly the "f5 goes stale and Aedan playtests the wrong tree" failure (#288, #400) that the tool exists to prevent.
The "invoke Claude only on failure" split is the important half of the idea. The happy path is deterministic and needs no model: fetch, merge, build, test, import, push. Claude earns its cost only where judgment is required — an unrecorded conflict — and that is also exactly where a systemic finding tends to be sitting. #465 (this script cannot converge on a delete/modify conflict) has been open for weeks precisely because it only bites mid-rebuild, when whoever hit it wanted to be doing something else.
Aedan flagged not knowing what this would be. Options, cheapest first:
gh pr list on a timer and diff the head shas. No ping at all — the service notices new content itself. Simplest, and it is what the reviewer's dispatcher already does. A 5-minute poll is well inside "Aedan might F5 now" latency.f5:dirty (or the service watches for any PR whose head sha it has not integrated). Uses GitHub as the queue, which is already the medium every session shares — same argument as CLAIM/RELEASE markers.Recommendation: poll and diff head shas, with no explicit ping. The set of open PR heads *is* the input; if it has not changed, there is nothing to rebuild, and a session cannot forget to ping. A label is a fallback if latency turns out to matter.
Agreed this is better after the move, and worth saying why rather than just deferring: the reviewer's dispatcher is a schtasks job with a Windows-path log, rebuild_f5.py shells to bash run_tests.sh because CreateProcess cannot run a shebang, and probe_gate/presubmit carry cp1252 workarounds. A second long-running service on Windows means a second copy of all of that. Under WSL it is a systemd timer or a cron entry and the shebang problem stops existing. Not a blocker — this could ship on Windows today — but the WSL move roughly halves the incidental work, so sequencing it after is right unless f5 staleness starts biting.
f5.py change? Aedan's side pulls origin/f5/integration and is agnostic about who built it — probably untouched, which is worth confirming rather than assuming.rebuild_f5.py as a manual command? Keep it runnable by hand (a session debugging its own branch's mergeability wants it) or make the service the only caller? Keeping it is nearly free now that the worktree makes it harmless.paused semantics unchanged? Should be yes — the label is about the frontier, not about who builds it.poll.py --check's STALE-DISPATCH to cover a second service, or give it its own signal? One probe reporting on both is fewer things to forget.Sized for /design rather than straight to implementation — it is a service boundary, an ownership change and a UX question, which is design 001's own bar for a doc. Related: #534 (per-clone disk, which this answers by removing the question), #465, #419 (Monitor-vs-cron, same "what wakes a loop" question one level over), #503/#518/#530 (what got us here), design_docs/001 §"no logic in shell".
Filed unclaimed.
None.
No comments.