Skip to content

fix(git): a merge driver declared after a branch was cut never fires on it - #3309

Merged
noahgift merged 1 commit into
mainfrom
PMAT-3308-union-attrs-override
Sep 15, 2026
Merged

noahgift merged 1 commit into
mainfrom
PMAT-3308-union-attrs-override

Conversation

@noahgift

@noahgift noahgift commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

The blocker

Seven open PRs could not be updated onto main today — gh pr update-branch refused each with "Cannot update PR branch due to conflicts". Two of them (#3005, #3271) conflicted only on docs/audits/impl-estimates.jsonl, a file that is strictly append-only and for which #3256 (768e740b1) already declared the correct resolution:

docs/audits/*.jsonl merge=union

It still conflicted.

Mechanism

A .gitattributes merge-driver declaration is retroactively inert. Git reads merge attributes from the side being merged into. A branch cut before the rule landed carries a .gitattributes without it, so the driver is never consulted.

Measured on agent/T-2 (#3005):

$ git show origin/agent/T-2:.gitattributes
docs/roadmaps/roadmap.yaml merge=roadmap      # no jsonl rule

$ git merge-base --is-ancestor 768e740b1 origin/agent/T-2 ; echo $?
1                                              # branch predates the declaration

$ git merge origin/main
CONFLICT (content): Merge conflict in docs/audits/impl-estimates.jsonl

The blobs were never the problem — git merge-file --union on the three stages returns rc=0, 50 rows, 0 markers, every line valid JSON. The attribute simply never bound. This is the "declared fix that cannot fire" class: the repo has had the right declaration since #3256 and every older PR kept conflicting anyway, with no signal that it was not in play.

Fix

ci_resolve_dirty.sh already owns this shape for roadmap.yaml: it registers the driver per invocation, never in the shared config, so nothing leaks into a .git shared by worktrees. Bind union the same way, via core.attributesFile.

core.attributesFile is the lowest-priority attribute source, so it applies only where the branch has no rule of its own — a branch that already carries the declaration is untouched. The temp file dies with the process.

Evidence

Verified on #3271, whose merge had failed minutes earlier:

merge rc=0
Auto-merging Makefile
Auto-merging docs/audits/impl-estimates.jsonl
Auto-merging docs/roadmaps/roadmap.yaml

49 rows, 0 invalid, 0 duplicate. check_roadmap_ids_unique.sh and check_roadmap_sorted.sh both PASS.

Row 9 carries both polarities — the fixture must CONFLICT without the override and merge WITH it — because an override that is never needed cannot be distinguished from one that does not work.

rows
selftest, unmodified 9/9, 0 failed
mutation: override file written empty 8/9 — row 9 red
restored 9/9

bashrs lint: 0 errors.

Drained under this diagnosis

PRs State
#3005, #3271, #3278, #3281 pushed out of CONFLICTING
#3205, #3238, #3246, #3249, #3259, #3265, #3270 updated onto main
#3134, #3245, #3248 genuine ci.yml content edits — not this class

Closes #3308

🤖 Generated with Claude Code

…on it

Seven open PRs could not be updated onto main today. Two of them conflicted
only on docs/audits/impl-estimates.jsonl -- a file that is strictly append-only
and for which #3256 (768e740) already declared `docs/audits/*.jsonl
merge=union`.

Git reads merge attributes from the side being merged INTO. A branch cut before
the rule landed carries a .gitattributes without it, so the driver is never
consulted: the declaration is retroactively inert. agent/T-2 is the measured
case -- its .gitattributes lists only the roadmap rule, 768e740 is not an
ancestor, and the merge conflicts. The blobs were never the problem:
`git merge-file --union` on the three stages returns rc=0, 50 rows, 0 markers.

This script already solves that shape for roadmap.yaml by registering the driver
per invocation rather than in the shared config. Bind union the same way, with
core.attributesFile -- the LOWEST priority source, so it applies only where the
branch has no rule of its own and a branch that already carries the declaration
is untouched. Nothing shared is written; the temp file dies with the process.

Verified on #3271, whose merge had failed minutes earlier: rc=0, all three of
Makefile, impl-estimates.jsonl and roadmap.yaml auto-merged, 49 rows with 0
invalid and 0 duplicate, both roadmap guards PASS.

Row 9 carries both polarities -- the fixture must CONFLICT without the override
and merge WITH it -- because an override that is never needed cannot be told
from one that does not work. Mutation-verified: blanking the override file turns
row 9 red (8/9), restoring it green (9/9).

Closes #3308

Pmat-Ticket: PMAT-3308

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3309 head=bf2d24f4b58ece95d916a6ff3d732f28ab5d4c38 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

@noahgift
noahgift added this pull request to the merge queue Sep 15, 2026
Merged via the queue into main with commit 03dfa8f Sep 15, 2026
19 of 20 checks passed
@noahgift
noahgift deleted the PMAT-3308-union-attrs-override branch September 15, 2026 16:41
@noahgift noahgift mentioned this pull request Sep 16, 2026
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.

A .gitattributes merge driver is retroactively inert: every PR older than the declaration still conflicts

1 participant