docs(contributing): squash-merge, and why --merge reds main - #176
Conversation
Landing a PR with `gh pr merge --merge` breaks two gates, and it has now happened three times in a row on main: c3db780 (#172, mine), 3e5072d (#174), and the trailer half on da0a9d8 (#157). check-role-discipline.py inspects every commit in the pushed range and wants each to name its row/<ROW-ID> branch or its PR as (#N). GitHub's squash-merge writes (#N) into the subject for free. A --merge landing leaves the content commit with the subject it had on the branch, which names neither, so POL-PR-REQUIRED reports "reached main without a reviewed row/* PR" about a commit that came from exactly such a PR. check-commit-trailers.py reads the same range, and GitHub's generated "Merge pull request #N from ..." message carries no FOLLOWING_AGENTS_PROTOCOL paragraph and no trailers, so POL-COMMIT-TRAILERS fails on the merge commit itself. Both gates are scoped over github.event.before..github.sha and each run's before is the previous run's sha, so no later run re-covers a range that already went red. That is the part worth writing down: the failure is not repairable after the push except by rewriting published history or waiving it, and it is invisible at PR time because the PR is checked against its own base. Documented in CONTRIBUTING.md rather than .agents/workflow.md because that file is 7 bytes under its 12288-byte procedure budget, and every paragraph of its non-generated prose is load-bearing; buying room by trimming it would cost more than this note is worth. CONTRIBUTING.md is also where a contributor with merge rights actually looks. This is documentation, not a guard. The permanent fix is a repository setting - allow squash only, disable merge commits - which needs admin and is left to the owner. No record row, no capability claim, no code. Every tree-scoped gate on main was already green before this change; main's red runs come entirely from the diff-scoped range gates described above. Gates: check-policy, check-protocol-consistency, check-agent-record, check-readme-structure, check-public-doc-tables, check-role-discipline, check-doc-checkpoint and check-commit-trailers over the range - all OK on a worktree pinned at 80b0880. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude-Code:claude-opus-5 [Claude Code]
test_agent_role.py::test_landed_detached_commit_remains_strict_without_ pending_evidence failed on PR #176 and would fail on many PRs that have nothing to do with what it asserts. The test's subject is main()'s DECISION: a violation on a commit that has landed, with no --pending-pr-head evidence, is strict (return 1) rather than a REPORT (return 0). But it obtained that violation by relying on the REAL HEAD to be one, and that coupling is live under CI. On a pull_request event GitHub checks out the SYNTHETIC merge commit for refs/pull/N/merge. inspect() sees two parents, so arrives_via_row_pr() switches to the merged_messages branch and scans the PR's own commit bodies with PR_REFERENCE = \(#\d+\)|#\d+, which matches a bare #123 anywhere. Any PR whose commit message cites an issue or PR number therefore stops being a violation, main() returns 0, and the assertion fails with 0 != 1. #176's message quotes (#157) and (#174) while explaining these very gates, which is how it tripped. Fixed by feeding main() a fixed violation through inspect/enforced rather than depending on HEAD's message, plus a regression case pinning that a message mentioning (#157) and #174 cannot flip the decision again. Proven on a reconstructed synthetic merge, the exact shape CI checks out (git merge --no-ff of the PR head into the base, detached): RED old test on that checkout: AssertionError: 0 != 1, 41 tests, FAILED (failures=1) -- the same failure CI reported GREEN new test on the SAME checkout: 42 tests, OK Not changed: PR_REFERENCE matching a bare #123 anywhere in a body also means a direct push whose message merely mentions an issue satisfies POL-PR-REQUIRED. That is a real hole, but tightening a policy gate's strictness is a separate reviewed decision, not a CI repair. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude-Code:claude-opus-5 [Claude Code]
|
Pushed a second commit: The Under a Proven on a reconstructed synthetic merge — the exact shape CI checks out:
Left alone deliberately: |
e39979a to
0d5a938
Compare
Why
mainhas been red onagent-recordanddocumentation-checkpointfor three consecutive landings, and every one of them is the merge method rather than the content:da0a9d8dPOL-COMMIT-TRAILERS(squash message lost its trailers) +POL-DOC-*c3db7804/b97fc1d4POL-PR-REQUIREDon the content commit +POL-COMMIT-TRAILERSon the merge commit — mine, landed with--merge3e5072d4POL-PR-REQUIREDon the content commit — same causegh pr merge --mergebreaks two gates that read the commits a push actually lands onmain:check-role-discipline.pyinspects every commit in the pushed range and wants each to name itsrow/<ROW-ID>branch or its PR as(#N). Squash-merge writes(#N)into the subject for free;--mergeleaves the content commit with its branch subject, which names neither, soPOL-PR-REQUIREDreports "reached main without a reviewed row/ PR"* about a commit that came from exactly such a PR.check-commit-trailers.pyreads the same range, and GitHub's generatedMerge pull request #N from ...message carries noFOLLOWING_AGENTS_PROTOCOLparagraph and no trailers.The part worth writing down is that neither is repairable after the push. Both gates are scoped over
github.event.before..github.sha, and each run'sbeforeis the previous run'ssha, so no later run re-covers a range that already went red. It is also invisible at PR time, because a PR is checked against its own base.What this does and does not fix
Merging this squashed makes the next
mainrun green, because every tree-scoped gate onmainis already passing — verified at80b08801:check-agent-record(ENGINE=144),check-gate-commands --check,audit-live-rows --check(203 live rows, 0 abandoned ACTIVE),check-role-discipline,check-policy,check-protocol-consistency,check-now-current,check-state-order,check-public-doc-tables, and thetest_policy_contract/test_policy_waivers/test_check_commit_trailerssuite (50 tests). Main's redness is entirely the diff-scoped range gates above.This is documentation, not a guard. The permanent fix is a repository setting — allow squash only, disable merge commits — which needs admin rights I do not have. Recommend flipping it; this note then just explains the reason.
Two notes for anyone reading those failed logs:
INTEGRATION FAILED:lines inagent-recordare expected stdout fromtest_agent_gates.py's negative cases (16/16 pass). LikewiseROW-THAT-IS-NOT-THEREis fixture output fromtest_check_gate_commands.py. Neither is a failure, and both make the log read worse than it is.ACTIVErows toSPIKE. That premise did not reproduce —audit-live-rows --checkreports0 abandoned ACTIVEat that PR's own branch point — and the demotions covered Qwen3, DeepSeek-V2/V4, Laguna and Kimi-Linear, which have token-exact gates. Closed with the evidence.Placement
In
CONTRIBUTING.mdrather than.agents/workflow.md: that file sits 7 bytes under its 12288-byte procedure budget and every paragraph of its non-generated prose is load-bearing, so buying room by trimming it would cost more than this note is worth.CONTRIBUTING.mdis also where a contributor with merge rights actually looks.Gates
check-policy,check-protocol-consistency,check-agent-record,check-readme-structure,check-public-doc-tables,check-role-discipline, pluscheck-commit-trailersandcheck-doc-checkpointover the commit range — all OK on a worktree pinned at80b08801. No record row, no capability claim, no code touched.Please squash-merge this one.