Harden PR-loop tick: deterministic loop-tick.sh with single verdict, in-flight detection, spawn lock - #82
Merged
Conversation
… spawn lock Collapses the multi-step PR-loop tick (census, poll, merge, feedback-check) into one script that computes a single machine-readable verdict (action=none/advance/feedback) instead of leaving that arithmetic to be re-derived from a prompt on every firing — the tick used to drift under smaller/cheaper models (fabricated step output, double-spawned orchestrators). loop-census.sh now reports in_flight=N (a branch exists but no PR yet), and loop-tick.sh adds a self-healing spawn lock (.claude/state/loop-advance.lock) so a second tick can't double-spawn an orchestrator for an issue already mid-flight. Fixes #81. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0135JVnX98DjPxTRzQZRy82q
… (issue #81 re-review) Re-review of the loop-tick hardening (10abb0d) found the spawn lock's own INVARIANT backwards: it was retained precisely when advance_ready==lock_issue, i.e. the no-branch-yet state census reports for a crashed spawn — so a crashed orchestrator wedged the issue forever with only manual `rm` as recovery. - loop-tick.sh: the lock is now released when (a) an open PR exists, (b) a branch exists (in_flight — the pre-branch race is over), or (c) the lock is older than LOCK_TTL_SECONDS=900 with the issue STILL advance_ready and no branch — treated as a crashed spawn and cleared to allow re-advance. The 15-minute TTL comfortably exceeds a real orchestrator's branch-push latency. - loop-tick.sh: the whole read-check-write critical section around the lock is now serialized with `flock` on a dedicated .claude/state/loop-advance.flock, closing a TOCTOU race where two overlapping ticks could both observe "no lock" and both emit `action=advance issue=N` (atomic temp+mv only prevented a torn read, not two racing readers). - loop-census.sh: `git branch -a --list ... | head -1` under `set -euo pipefail` could abort the whole script on git's SIGPIPE (exit 141) when head closes the pipe early; `|| true` absorbs the non-fatal pipeline failure. Reproduced and verified against a repo with 5000 matching branches. - docs/USAGE.md: reconciled the spawn-lock description with the corrected invariant. Tests: loop-tick.test.sh gained a TTL self-heal scenario (crashed-spawn recovery) and a real concurrent-process scenario proving `flock` serializes two overlapping ticks; scenario 7's step-passthrough assertions now check a BODY line from each fake fixture (not just the header banner), so silently swallowing loop-census.sh's or pr-feedback.sh's output fails loudly. Added loop-census.test.sh: runs the REAL loop-census.sh against a real git repo with local/remote-tracking branches to directly cover in_flight detection, including the "issue-4 vs issue-42" prefix-collision guard and the "remotes/origin/" stripping/suffix-match on both branch-exists paths. Every new/changed assertion was mutation-checked (reverted to the buggy behavior) to confirm it fails loudly; all failure modes reproduced deterministically. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0135JVnX98DjPxTRzQZRy82q
robercano
approved these changes
Jul 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #81
What this does
Hardens the self-hosted PR-loop tick so the protocol lives in scripts, not model discipline (root cause of the 2026-07-08 incident: fabricated telemetry, missed approvals, and a duplicate orchestrator for issue #78).
Deliverables
.claude/scripts/loop-tick.sh(new) — single-entry tick. Runsloop-census.sh->notify-poll.sh->merge-ready.sh->pr-feedback.shin order with full outputs preserved, then emits exactly one machine-readable verdict as the last line:action=none|action=advance issue=N|action=feedback pr=N. Precedence: feedback (lowest-numbered PR with unaddressed CHANGES_REQUESTED) > advance (censusadvance_ready) > none. Calls the existing scripts — no duplicated merge/poll logic. The driver's job collapses to: run one script, spawn the orchestrator only when the verdict says so.loop-census.sh— prints an explicitin_flight=Nline when afeat/issue-N-*branch exists but has no open PR, so a driver never misreads an in-progress orchestration as stuck. Existing output keys unchanged.flock-guarded state file (.claude/state/loop-advance.lock, gitignored) recording the in-flight issue + UTC timestamp when ADVANCE is emitted. A second ADVANCE for the same issue is refused by script logic. Self-healing invariant (documented in a comment): the lock clears once an open PR exists for N, or the branch appears (in_flight), or — crashed-spawn recovery — the lock ages pastLOCK_TTL_SECONDS=900(15 min) while still advance_ready with no branch.flockcloses the concurrent-tick TOCTOU race.docs/GETTING_STARTED.md/docs/USAGE.md: drive tick sessions on Sonnet, reserve Fable/Opus for scoping/planning/filing, and referencesloop-tick.sh.Tests
loop-tick.test.sh— verdict precedence, lowest-PR tiebreak, lock write/refuse, TTL self-heal, real concurrent-tick flock serialization, and full-output passthrough of all four steps.loop-census.test.sh(new) — runs the real census against a throwaway git repo: in_flight detection,origin/prefix stripping, andissue-4vsissue-42/43prefix-collision guard.GATES_FILE=.claude/self/gates.json): build, lint, test_affected all pass.Review
Two adversarial lenses, consensus=all: correctness (opus) and tests (sonnet) both APPROVE after one reject/fix round (fixed a lock deadlock, a TOCTOU race, a SIGPIPE footgun, and two vacuous-test gaps).
Notes for the owner
loop-census.shuntracked;docs/*modified) was seeded onto this branch and is included here. Merge caveat: because those files are still uncommitted in your local main working tree,git pull/ff-only after merge will complain they "would be overwritten" —git checkout -- docs/... && rmthe local untrackedloop-census.sh(or stash) so the merged versions take.flockand GNUdate -d(the latter already used bynotify-poll.sh). Downstream consumer projects don't run these scripts.🤖 Generated with Claude Code