feat: detect and route PR merge conflicts - #9
Merged
Conversation
* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup Three related fixes to the shared wake-drain and Pi supervision-branch dispatch machinery so a routine success is never main-blocking and a mixed queue can safely split between actors. 1. Successful routine results no longer create main-blocking wake rows. fm-startup-network.sh only enqueues a check: startup-network wake when the deferred result is actionable (state is not "done", or the report carries a bootstrap-diagnostics actionable prefix); a clean success stays durable in the report file without ever waking the agent. 2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes presentation and --ack-through to the current actor (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original whole-queue cutoff behavior, unaffected. A branch actor (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision branch's own bash tool calls) is scoped to an explicit eligible-row snapshot instead of a cutoff comparison, so it can never remove a row it was not granted - the fix for the swallow risk that used to force an all-or-nothing whole-queue fallback to main. .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the single owner of eligibility: a check-kind row (merge-confirmation polls, Relay mentions, credential/auth failures) is now excluded rather than vetoing the whole scan for a non-heartbeat wake, while a heartbeat review keeps its original all-or-nothing rule unchanged. writeEligibleRowsSnapshot publishes the exact eligible sequence numbers before every branch prompt; fm-primary-pi-watch.ts's offer still refuses a check-kind trigger outright so a main-only close is never itself routed to the branch. 3. A repeat identical merged-PR-poll result for an already-notified task is absorbed instead of enqueued again. A poll's own retirement state is scoped to one registration and cannot see a prior registration's outcome, so a task re-registered after its merge was already surfaced would otherwise wake main a second time for the same event. bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives across re-registrations to catch that case; the first notification for a task still reaches main unchanged. Regression tests colocated in tests/fm-startup-network.test.sh, tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow property), tests/fm-pi-branch-extension.test.sh, and tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and docs/pi-supervision-branch.md updated for the new contracts. * no-mistakes(review): Bind merge deduplication to canonical PR identity * no-mistakes(review): Serialize wake row ownership across main and branch * no-mistakes(review): Bind branch grants and deduplicate within actor claims * no-mistakes(review): Fallback main-owned wake claims to main delivery * no-mistakes(review): Clarify silent startup success guidance * no-mistakes(review): Release residual branch grants after settled prompts * no-mistakes(review): Reject truncated wake rows as corrupted * no-mistakes(document): Document per-actor routing and silent startup success * no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test * no-mistakes: apply CI fixes
* Hide branch outcome tool in Pi Calm * no-mistakes(review): Preserve stock outcomes rendering and document tool audit * no-mistakes(review): Document branch read tool audit disposition * no-mistakes(review): Match stock outcomes output sanitization * no-mistakes(document): Document Calm custom-tool visibility
Detect newly conflicted open PRs from registry-derived repos, dedupe by head SHA, poll lazy GitHub mergeability safely, and arm through the standard registered check path.
…errun, record pruning
Conflict detection already worked; four review rounds patched the same coverage-disclosure hole. A gap is now keyed by a stable target with a frozen opened_at, cause is metadata that never resets age, and wake lines ack by identity rather than by scanning the rendered text.
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.
Intent
Build automatic detection and routing of pull-request merge conflicts across every repository this fleet works in. The captain asked for this after conflicts hit several PRs twice in one day; it is a proven repeated need.
A registered check polls open GitHub pull requests of every repository this home works in, detects genuinely conflicted ones, and wakes firstmate with one routed actionable line. Repositories are derived from data/projects.md project clones plus this firstmate checkout's origin remote; owning teams come from data/secondmates.md. Do not hardcode a repository list.
Hard requirements still in force:
Coverage is a separate ledger from conflicts (Sol diagnosis 2026-08-25). A conflict is a positive observation keyed by repo + number + head. A coverage gap is an absence of trustworthy observation keyed by a stable target defined BEFORE any GitHub read: repo:<owner/repo> for a valid slug, project: when a clone or origin cannot resolve to a valid repo, and source:projects-registry or source:firstmate-origin when discovery fails before a repo can be named. Never persist or interpolate an unvalidated raw origin slug.
Typed outcomes are observed vs unobserved with cause discovery | invalid-origin | budget | github | truncated. Sweep completeness is derived from those outcomes: complete only when every expected target was observed AND discovery itself completed. One continuous gap record per target keeps opened_at and disclosed across cause flaps; changing cause never resets age and never opens a second gap. Disclose once after FM_PR_CONFLICT_UNREAD_GRACE_SECS (0 = first sweep) as coverage-hole target=... latest-cause=...; local identity refusal, budget cut, and truncated envelopes must not be worded as GitHub reads failing. Recovery then a later outage is a new gap. Typed notification queue acks by identity (conflict key / coverage disclosed), never by substring-scanning the rendered line. Prefix remains pr-conflict:.
PR transport is GraphQL --jq to a JSON array, not TSV. Sanitize tabs and newlines only at format time so titles keep a literal backslash.
This rebuild started from validated pipeline head 46b6866b (GraphQL batching already landed). Do not revert to c382a2d. Do not patch the four parked coverage findings one by one; the coverage subsystem was rebuilt as a ledger instead of hole-kinds.
Delivery: keep the fork-targeted route. The pipeline must open the PR against bingb0t5/firstmate base main. This home cannot merge the upstream kunchenguid PR 3010 and its workflows need an admin approval we do not have, so that is not delivery. Do not hand-create a fork PR and do not forge an attestation.
PR body must state: detection is a safety net not a cure (conflicts happen because PRs wait unmerged while main moves); round-2 REST-per-PR created the budget cascade; arm/shim duplication with fm-tool-update-check.sh is a noted follow-up, not this PR.
Tests must exercise behavior through the executable and must never assert implementation-source bytes. One sentence per line in tracked Markdown. No agent name as a commit co-author.
Delivery for this run: this checkout's origin is bingb0t5/firstmate. Open the PR against bingb0t5/firstmate base main, not kunchenguid. Leave kunchenguid#3044 open as an unmergeable contribution. GitHub origin parsing is the Sol structural parser in bin/fm-repo-slug-lib.sh (typed fm_repo_slug_parse), not another regex. Missing jq/gh-axi are source:runtime-jq and source:runtime-gh-axi with cause dependency-missing. PR body must state detection is a safety net not a cure; round-2 REST-per-PR created the budget cascade; arm/shim duplication with fm-tool-update-check.sh is a noted follow-up, not this PR.
CEO overview
hasNextPage) is unobserved, not clean.bin/fm-pr-conflict-watch.sh armand never resolves conflicts. Arm/shim duplication withfm-tool-update-check.shis a noted follow-up, not this PR.Decision needed
No decision required. Remaining CI redness was missing PR-communication headings in this description, not a product choice.
Module-boundary decision
New module:
bin/fm-pr-conflict-watch.shplustests/fm-pr-conflict-watch.test.sh, with GitHub identity parsing owned bybin/fm-repo-slug-lib.sh(shared with bearings).bin/fm-project-origin-lib.shstays forge-agnostic.Validation
PR must be raised via no-mistakes, Lint, Test coverage guard, Repo invariants, Stock macOS Bash snapshot compatibility, and the portable/Herdr behavior jobs that have finished on this head.pr-communicationandverify-source-of-truthwere failing on this description; remaining behavior jobs may still be in progress on the current head.PR_LIMITclean PRs stay silent; a conflict beyond the page emitslatest-cause=truncatedand persists the coverage gap; returning to a complete page recovers silently. Detection does not prevent conflicts.Evidence: PR-limit completeness and recovery transcript
Source: PR-limit completeness and recovery transcript
Exactly three PRs produced no wake. Adding a fourth conflicted PR beyond the limit emittedpr-conflict: coverage-hole target=repo:acme/alpha ... latest-cause=truncatedand persisted the disclosed coverage gap. Returning to three PRs silently cleared the ledger.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
bin/fm-pr-conflict-watch.sh:932- Intent requires that “one GraphQL read per repository carries the open set” and that a target is observed only after a trustworthy observation. This query requests onlyfirst:$PR_LIMITwithoutpageInfo, thenevaluate_repomarks the repository observed even when exactly the limit was returned. With the default limit of 30, a repository with 31 open PRs and a conflict at position 31 produces no conflict and no coverage hole. Return completeness metadata from this GraphQL boundary and treathasNextPageas unobserved, or otherwise fetch the complete bounded open set without REST-per-PR reads.🔧 Fix: Handle truncated GraphQL PR sets as coverage gaps
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
tests/fm-pr-conflict-watch.test.shExecutedbin/fm-pr-conflict-watch.shthrough its end-user check path withFM_PR_CONFLICT_PR_LIMIT=3: exactly three clean PRs, four PRs with a conflict beyond the page, then recovery to three complete rows; captured stdout and the persisted.pr-conflict-watchcoverage ledger.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.