Skip to content

feat: detect and route PR merge conflicts - #9

Merged
bingb0t5 merged 28 commits into
mainfrom
fm/fm-pr-conflict-watch-fork
Aug 25, 2026
Merged

feat: detect and route PR merge conflicts#9
bingb0t5 merged 28 commits into
mainfrom
fm/fm-pr-conflict-watch-fork

Conversation

@bingb0t5

@bingb0t5 bingb0t5 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

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:

  1. Dedupe by repository, PR number, AND head SHA. Same conflict on the same head stays silent after the first wake; a force-updated head that conflicts again is a new event. Keys are recorded only for conflicts that reached the printed line, so line-cap omissions wake later instead of being lost.
  2. GitHub lazy mergeability: UNKNOWN is never treated as clean or conflicted. Reread only those PRs until the state settles or stays unknown.
  3. Print nothing when there is nothing new; one line when firstmate should wake; finish inside FM_CHECK_TIMEOUT.
  4. Cheap: bound API calls, use gh-axi as the fleet standard. gh-axi has no --json flag and answers with an axi envelope, not raw JSON. One GraphQL read per repository carries the open set with mergeability; do not REST-read each PR (that round-2 approach exhausted the sweep budget and created the coverage cascade). A truncated envelope is a failed read, not a short list. A GraphQL null repository is a failed read, not an empty one.
  5. Include drafts; mark draft=yes rather than skipping them.
  6. Register with bin/fm-check-register.sh. Detection and routing only: never resolve conflicts, force-push, rebase, or call GitHub's update-branch.

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

  • What is changing: A registered fleet-wide check polls open GitHub pull requests, detects merge conflicts (including drafts), and wakes firstmate with one routed line. Unobserved repositories are tracked in a separate coverage ledger. GitHub origins are parsed by a typed structural parser. A GraphQL page that is not complete (hasNextPage) is unobserved, not clean.
  • Why it matters: Conflicts were hitting several PRs twice in one day because nobody noticed until a human looked. Detection is a safety net, not a cure: conflicts happen because PRs wait unmerged while main moves. Round-2 REST-per-PR reads created the budget cascade; this change keeps one GraphQL read per repository.
  • Customer or business impact: None outside the captain's own automated crew. No product, pricing, or externally visible behavior changes.
  • Risk and rollout: Medium. Conflict detection, coverage ledgers, origin parsing, and truncated-page handling are substantial, but the watcher is opt-in via bin/fm-pr-conflict-watch.sh arm and never resolves conflicts. Arm/shim duplication with fm-tool-update-check.sh is 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.sh plus tests/fm-pr-conflict-watch.test.sh, with GitHub identity parsing owned by bin/fm-repo-slug-lib.sh (shared with bearings). bin/fm-project-origin-lib.sh stays forge-agnostic.

Validation

  • Checks passed: Local no-mistakes intent, rebase, review (truncated open-set finding auto-fixed), test, document, lint, and push passed. On GitHub: 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.
  • Checks not run: None skipped. pr-communication and verify-source-of-truth were failing on this description; remaining behavior jobs may still be in progress on the current head.
  • Evidence and limitations: Focused executable watcher suite plus an end-to-end transcript: exactly PR_LIMIT clean PRs stay silent; a conflict beyond the page emits latest-cause=truncated and 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 emitted pr-conflict: coverage-hole target=repo:acme/alpha ... latest-cause=truncated and persisted the disclosed coverage gap. Returning to three PRs silently cleared the ledger.

SCENARIO 1: exactly PR_LIMIT=3 open PRs, complete page
watcher stdout: <empty - repository observed, no wake>

SCENARIO 2: fourth PR conflicts beyond the bounded page
watcher stdout: pr-conflict: coverage-hole target=repo:acme/alpha repo=acme/alpha unaccounted-for=0s latest-cause=truncated
persisted record coverage: [{"target_id":"repo:acme/alpha","repo":"acme/alpha","opened_at":1787652947,"last_attempt_at":1787652947,"latest_cause":"truncated","disclosed":1}]

SCENARIO 3: open set returns to three complete rows
watcher stdout: <empty - coverage recovered silently>
persisted record coverage: []

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 only first:$PR_LIMIT without pageInfo, then evaluate_repo marks 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 treat hasNextPage as 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.sh
  • Executed bin/fm-pr-conflict-watch.sh through its end-user check path with FM_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-watch coverage ledger.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

kunchenguid and others added 28 commits August 25, 2026 16:48
* 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.
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.
@bingb0t5
bingb0t5 merged commit 15a930f into main Aug 25, 2026
16 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants