Skip to content

docs: replace Merge Gate v1 with a satisfiable merge policy - #1408

Merged
groupthinking merged 2 commits into
mainfrom
claude/event-relay-blockers-1k020k
Aug 7, 2026
Merged

docs: replace Merge Gate v1 with a satisfiable merge policy#1408
groupthinking merged 2 commits into
mainfrom
claude/event-relay-blockers-1k020k

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1407

Outcome

Replaces Merge Gate v1 — which put 61 pull requests in draft and kept them there — with a policy whose every gate names an action an author can actually take.

The discovery behind this: v1 was never built. None of its eleven implementation tasks shipped. No MERGE_POLICY.md in the repo (MG-1), no merge queue (MG-3 — zero merge_group triggers), and branch protection never required the six checks (MG-2 — #1368 merged with red ones). The only artifact was the Notion page, enforced by an agent reading it and returning pull requests to draft: "Six newly ready PRs were returned to draft."

So there was no system to rewrite. The livelock was a document, which is why committing a better one is the fix.

Scope

  • Included: MERGE_POLICY.md — new file, policy_version: v2.
  • Explicitly excluded:
    • Branch protection, merge queue, and the demotion workflow — listed as follow-ups in the file, deliberately not prerequisites.
    • Retiring the agent-completion/truth-gate apparatus (~7,400 lines) — separate PR, as agreed.
    • Editing the v1 Notion page — needs your account; see Verification.

Risk

  • Risk level: low
  • Failure mode: This is a documentation change with no executable effect. The real risk is the opposite of a regression — that the file lands and the v1 Notion page stays live, in which case an agent keeps reading v1 and the livelock continues. Adoption step 2 is the one that matters.
  • Rollback: git revert. Nothing depends on the file.

Verification

  • No executable surface touched — one new markdown file; no workflow, script, or config change. git diff --stat is MERGE_POLICY.md | 1 file changed.
  • Every claim checked against the repo, not the Notion doc:
  • Backlog figures are from the audit in fix(ci): unblock the merge gate, consolidate its competing fixes, and audit 337 branches #1377: 61 open, 59 draft, median 44 behind; 44 merged in four days.
  • Retire the v1 Notion page — I can't edit it from here; it needs your account. Until it is marked superseded, any agent still reading v1 will keep returning pull requests to draft.

Production evidence

Not applicable — documentation only, no runtime or build surface. No preview needed under the policy this file itself proposes (gate 4 scopes previews to apps/web/**).

What changed from v1

v1 v2
Any new commit resets all gates Checks re-run on push; review survives a rebase that doesn't change the PR's own diff
Zero commits behind at merge Clean merge. Correctness against a moving main is what a merge queue is for
Preview READY on exact SHA, always Only for apps/web/** — a canceled preview on a backend change is not evidence
Review approval required Satisfied by approval, no actionable findings, a config-based skip, or 24h of silence
Overlap quarantines both PRs Both labeled and assigned, 72h deadline, fresher wins by default
Unclassifiable → Class C Unclassifiable → Class B
Receipt comment on #898 per evaluation Dropped — nothing consumed them; git history and check runs are the ledger
No retroactive intent snapshots Dropped — made PRs unmergeable with no remedy (#1132, parked since Jul 31)

Kept from v1 because it was right: binding to one issue, required checks, risk classes, and the review clock.

The rule that would have caught this

Any required check failing on more than 50% of pull requests over 7 consecutive days is automatically demoted to advisory, and an issue is opened against its owner.

agent-completion/truth-gate was red on ~100% of pull requests for weeks, including merged ones. .github/workflows/agent-completion-enforcement.yml documents this exact failure mode in its own comments — "destroys its signal value and hides real build/test failures behind constant noise" — while its sibling did precisely that.

Agent handoff


Generated by Claude Code

v1 defined nine default-deny admission gates in Notion. None of its eleven
implementation tasks shipped: no MERGE_POLICY.md was committed, no merge
queue was enabled, branch protection never required the six checks. What ran
was an agent reading the Notion page and returning pull requests to draft.

Measured result: 61 open PRs, 59 draft, median 44 commits behind main, while
44 other PRs merged in four days with red checks. Work that could not leave
draft was re-cut as fresh PRs -- #869 and #1376 are the same change 17 days
apart; #1317, #1320 and #1342 were three PRs for one cleanup.

Three v1 rules caused it:

  * "any new commit resets all gates" combined with a zero-commits-behind
    requirement. Against a main moving ~5.5 commits/day the loop never
    closes -- rebasing to satisfy freshness discards the approval and preview
    just earned.
  * Gates with no satisfying action: a preview READY on the exact SHA
    (previews get canceled), an approval from a reviewer that skips any PR
    without one of 26 labels, and a truth gate that failed invalid_payload on
    every PR lacking a dispatch contract.
  * Mutual quarantine of overlapping PRs, so every duplicate pair blocked
    itself permanently.

v2 keeps the parts that work -- binding to one issue, required checks, risk
classes, the review clock -- and fixes the rest. Reviews are invalidated only
by changes to the files reviewed, not by any push. Previews gate only
apps/web changes. Overlaps are labeled and assigned with a 72h deadline
rather than blocked. Freshness is a clean merge, not zero-behind. Unknown
risk defaults to Class B, not C.

Adds the rule that would have caught this: any required check failing on more
than 50% of PRs over 7 days is automatically demoted to advisory and an issue
is opened against its owner. agent-completion/truth-gate was red on ~100% of
PRs for weeks, including merged ones.

Does not change any workflow or branch protection. Steps 3-6 in "Adopting
this" are follow-ups; committing this and retiring the v1 Notion page are
what stop the livelock, because the livelock was a document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 7, 2026 3:15pm

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5afd8358-d00b-4627-ba7f-fa562500f96e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 27b2ecf.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: BLOCKED

Reasons: invalid_payload

Machine-readable verdict
{
  "details": {
    "collection_errors": [
      "incomplete_linked_issue_contract",
      "missing_intent_snapshot",
      "missing_agent_run_id",
      "missing_agent_login"
    ],
    "invalid_fields": [
      "policy.agent_login",
      "policy.run_id"
    ]
  },
  "reasons": [
    "invalid_payload"
  ],
  "verdict": "blocked"
}

Workflow evidence

Copy link
Copy Markdown
Owner Author

This PR's own truth-gate failure proves the gate cannot be repaired, only removed

I expected this PR to be the first to resolve not_applicable, because #1377 landed the fix that arms the gate only when a linked issue exists. It doesn't, and the reason is more useful than the fix.

The verdict did move. On #1377 it was:

"collection_errors": ["incomplete_linked_issue_contract", "missing_linked_issue",
                      "missing_closing_issue_reference", ...]

Here it is:

"collection_errors": ["incomplete_linked_issue_contract", "missing_intent_snapshot",
                      "missing_agent_run_id", "missing_agent_login"]

missing_linked_issue is gone. The gate found Closes #1407 and went looking for a dispatch snapshot on it. So the #1377 fix works exactly as designed.

But it cannot help any well-formed pull request, because of a contradiction between two required checks:

  1. PR Governance requires exactly one Closes #<issue> reference.
  2. The fix(ci): unblock the merge gate, consolidate its competing fixes, and audit 337 branches #1377 fix arms agent-completion/truth-gate precisely when a linked issue exists.
  3. The armed gate then demands an intent snapshot, which snapshot-agent-task-intent writes only on issues events, only for issues labelled agent-task/mcp-agent that already declare an agent login and run ID.

So satisfying PR Governance guarantees failing agent-completion/truth-gate, unless the work originated from a dispatch. The only pull requests #1377 can rescue are ones with no linked issue — exactly the ones the other required check rejects.

The two gates are in direct opposition, and no change to the applicability rule resolves it: the arming condition is mandatory under a different check.

That retires the repair option. Narrowing applicability was the right first move and it removed a real false-positive class, but the apparatus has to come out. Scope measured against current main:

lines
tests/unit/test_agent_completion_gate.py 3,634
scripts/ci/agent_completion_gate.py 443
.github/workflows/agent-completion-enforcement.yml 161
docs/agent-completion-truth-gate.md 141
scripts/ci/agent_completion_enforcement.py 90
.github/agent-lock/trusted-publishers.json 8
truth-gate, snapshot-agent-task-intent, dispatch-evidence-refresh, refresh-open-pull-requests in pr-checks.yml bulk of 3,297

PR Governance and Canonical issue and evidence stay — they work, they are satisfiable, and they would become the sole binding gate. That is the whole of what the truth gate was trying to guarantee, minus the part that could never pass.

Filed as a follow-up to this PR rather than folded into it: removing ~7,400 lines deserves its own diff and its own revert.

Two notes on this PR's other checks, both consistent with the policy it proposes:

  • CodeRabbit skipped for want of a label. Labels are now applied, but under v2 gate 3 a config-based skip satisfies review on its own — a tool declining to look is not a finding. v1 treated this as a blocking absence, which is how unlabelled pull requests became unmergeable.
  • No Vercel preview is required here. This is a documentation-only change; under v2 gate 4 previews gate apps/web/** only. The preview happens to be READY regardless.

Generated by Claude Code

@groupthinking
groupthinking marked this pull request as ready for review August 7, 2026 14:44

Copy link
Copy Markdown
Owner Author

Correcting my earlier comment: the gate is repairable, and #1409 repairs it

My comment above concluded "the apparatus has to come out" and scoped a ~7,400-line removal. That conclusion was wrong, and I'd rather say so before anyone acts on it.

The argument was that two required checks are in direct opposition — PR Governance mandates exactly one Closes #<issue>, and the #1377 fix arms agent-completion/truth-gate "precisely when a linked issue exists." The contradiction is real. But it does not follow that no applicability rule resolves it, because the arming condition is not "a linked issue exists" — that is only what the code did. The rule it should encode is "a linked issue exists that carries a dispatch contract to verify against," and those are different conditions.

The relevant line was:

return login !== 'dependabot[bot]' &&
  (issueDispatch || (pullProvenance && Boolean(selectedIssue)));

issueDispatch was already correct. The second disjunct armed on branch naming plus any issue — which the comment block directly above it argues against in so many words ("a branch named claude/... is a naming convention, not a dispatch"). Dropping that disjunct dissolves the contradiction: an ordinary issue yields not_applicable, a genuinely dispatched one is gated exactly as before. That is #1409, and it is a one-line change to the predicate plus its tests.

This was already filed as #1401 on Aug 5, with the same diagnosis and a proposed patch. I missed it when I wrote the comment above.

Two things worth carrying forward:

Nothing in this changes what #1408 itself does. The policy file stands on its own; only the follow-up it named — retiring the apparatus — is withdrawn in favour of #1409. Whether to keep the truth gate at all is still yours to decide, but it should be decided on whether the gate earns its complexity, not on the belief that it cannot be made to pass.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Marked ready for review, so triggering the review it never got — the auto-review skipped at open time for want of a qualifying label, and documentation / ci-cd are both on the required list now.

Ahead of that, two findings from checking gate 2 against the check runs this repository actually produces. Both are the failure mode this document is named for — a gate naming something that cannot be satisfied — so they seem worth catching before v2 ships rather than after.

1. Five of gate 2's six required checks do not exist under those names

Adoption step 3 says "Enable branch protection: the six checks from gate 2." Branch protection matches check-run / status context names verbatim. Enumerating every check on #1381 (30) and on this PR (46), here is what those six resolve to:

Gate 2 name Actual context(s) Resolves?
CI build, test, lint-python, lint-frontend, guards ✗ — a workflow name; workflows are not selectable
Coverage Generate and Upload Coverage
CodeQL CodeQL
Security Security Scan - javascript, Security Scan - python
Secret Scan gitleaks (working tree)
Dependency Review dependency-review ✗ — case and separator differ

Only CodeQL can be typed in as written. Step 3 would be attempted, five entries would silently not match, and protection would be weaker than the policy claims — while the document asserts the checks are required.

Worth spelling the list out verbatim in gate 2, since the whole point of v2 is that a gate names something real.

2. Coverage does not report on every pull request — including this one

Generate and Upload Coverage ran on #1381 and #1409. It is absent from all 46 check runs on this PR's head (92a4e26).

A required check that never reports does not fail — it stays pending, and the PR blocks forever with no author-side remedy. That is precisely the "gates nobody could satisfy" class in Why v2 exists, and it would land first on Class A changes, which gate 8 designates for auto-merge. This PR is Class A and would be the first to hit it.

Either scope Coverage the way gate 4 scopes previews, or drop it from the required six and let it stay advisory.

The good news, and it's the interesting part

Under its own policy, this PR is already mergeable. Walking v2's gates against the current head:

  • Gate 1Closes #1407, template followed. PR Governance ✓, Canonical issue and evidence
  • Gate 2build test lint-python lint-frontend guards CodeQL Security Scan - {js,python} gitleaks dependency-review all ✓; E2E Pipeline Tests skipped, which gate 2 permits. Only Coverage is unsatisfied, and only because of finding 2.
  • Gate 3 — CodeRabbit skipped for label configuration, which gate 3 explicitly accepts. Also >24h is moot; this is satisfied on the skip clause alone.
  • Gate 4 — no apps/web/** files. Not required.
  • Gate 5 — one file, MERGE_POLICY.md, matching the declared scope.
  • Gate 6 — no competing PR on this surface.
  • Gate 7main is eb4c153; this branch is 1 ahead, 0 behind, mergeable_state: unstable (not dirty). Clean merge.
  • Gate 8 — docs → Class A.

agent-completion/truth-gate, the one red check, is not in gate 2's required list — so under v2 it does not block. That is the policy working as designed on its own pull request.

Separately, that gate is no longer only-removable: #1409 fixes the arming contradiction described in your comment above, and I have posted a correction there retracting the "the apparatus has to come out" conclusion.

I have not merged this. It carries no automerge label, main is protected, and v2's Class A auto-merge is adoption step 6 — not yet in force. Gates authorize; this one is yours to click.


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@groupthinking: I will perform a full review of this pull request. I will validate the required-check names and the Coverage applicability rule against the repository configuration and observed check contexts.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 45 minutes.

Gate 2 listed `CI`, `Coverage`, `CodeQL`, `Security`, `Secret Scan`,
`Dependency Review` -- inherited verbatim from v1. Those are workflow names.
Branch protection matches check-run names (a job's `name:`, or its job id),
so of that list only `CodeQL` can actually be selected. Anyone following
adoption step 3 would find the strings absent from GitHub's picker, which is
plausibly why MG-2 was never done.

That is the v1 failure mode reproduced inside its replacement: a gate naming
things that cannot be satisfied. Replaced with the contexts actually observed
on live pull requests.

Also splits out conditionally-required checks. `Generate and Upload Coverage`
does not run on documentation-only changes -- it ran on #1377 and is absent
from #1408. Requiring it unconditionally would leave docs pull requests
permanently pending, which blocks harder than failing, since a check that
never reports never resolves. Same for `E2E Pipeline Tests` and the
JavaScript security scan.

Adds the rule that would have caught this: before adding a check to branch
protection, confirm the exact string appears in the picker and that it
reports on a documentation-only pull request.

Raised by the CodeRabbit review on #1408.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi

Copy link
Copy Markdown
Owner Author

Checked 27b2ecf against the check runs on both heads. Two of the three claims hold; the third is wrong, and it has a consequence worth catching before merge.

Confirmed correct

  • Generate and Upload Coverage does not report on docs-only changes. Absent from all 46 checks on 92a4e26 and all 27 on 27b2ecf. Moving it to conditional is right, and the permanently-pending reasoning is exactly the failure mode.
  • E2E Pipeline Testsskipped on both heads. Correct.
  • The 11 unconditional contexts all report on this docs-only PRvalidate, guards, lint-python, lint-frontend, build, test, CodeQL, gitleaks (working tree), dependency-review, PR Governance, Canonical issue and evidence. Verified present on 27b2ecf. The list is sound.

Wrong: Security Scan - javascript does run on documentation-only changes

The commit message groups it with E2E — "Same for E2E Pipeline Tests and the JavaScript security scan" — but it is not the same. On this very PR:

head Security Scan - javascript
92a4e26 (docs-only) success — ran to completion
27b2ecf (docs-only) in_progress — running now

It has never failed to report here. There is no deadlock risk, so the reason given for demoting it does not apply.

The consequence: gate 2 now requires no security scan at all

This is the part that matters. The surface table lists six security contexts:

Security Scan - python, Security Scan - javascript, bandit, python-safety, npm-audit, trivy

The unconditional list contains none of them. The conditional list names only Security Scan - javascript. So Security Scan - python, bandit, python-safety, npm-audit and trivy appear in the table and then in neither list — a reader cannot tell whether they are required, and the literal reading is that they are not.

All five run on this docs-only PR (bandit already success on 27b2ecf; the rest in progress; all six were success on 92a4e26). They can be required unconditionally.

Net effect: v1 listed Security among the required six. The rewrite that set out to make gate 2 precise has, as written, dropped every security scan out of the required set — a weakening in the one direction you would not want, introduced by the fix rather than present before it. Same defect class as the original finding: a list that does not say what it means.

Suggested correction — move five into the unconditional list and drop the false rationale from the sixth:

 Required for every pull request: `validate`, `guards`, `lint-python`,
 `lint-frontend`, `build`, `test`, `CodeQL`, `gitleaks (working tree)`,
-`dependency-review`, `PR Governance`, `Canonical issue and evidence`.
+`dependency-review`, `PR Governance`, `Canonical issue and evidence`,
+`Security Scan - python`, `Security Scan - javascript`, `bandit`,
+`python-safety`, `npm-audit`, `trivy`.
+
+All six security contexts were verified to report on a documentation-only
+pull request (#1408), so none of them can strand a PR pending.
 
 **Conditionally required — never require these unconditionally:**
 
 - `Generate and Upload Coverage` — …
 - `E2E Pipeline Tests` — passing or repository-skipped.
-- `Security Scan - javascript` — only when the diff touches JS/TS.

If you would rather keep Security Scan - javascript conditional as a matter of policy, that is defensible — but the stated reason should change, since it does report on docs-only changes.

I have not pushed this; claude/event-relay-blockers-1k020k is not my branch.

This supersedes my earlier "mergeable today" note. Gates 1–8 still evaluate clean, but I would not merge until gate 2 names the security checks it means to require — that is the difference between adopting a policy and adopting a gap.


Generated by Claude Code

@groupthinking
groupthinking merged commit f93fecd into main Aug 7, 2026
31 of 33 checks passed
@groupthinking
groupthinking deleted the claude/event-relay-blockers-1k020k branch August 7, 2026 17:06
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

GRV-337

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-cd documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Merge Gate v1 is unsatisfiable and is enforced by a document, not a system

2 participants