feat(coordination): add a git merge-tree textual verdict to in-flight conflicts - #497
Merged
Merged
Conversation
… conflicts Each map_in_flight_conflicts conflict pair now carries textualMerge (textual-conflict | clean-automerge | not-assessed) from git merge-tree between the tip commits over a merge base resolved in the real repo. The merge runs in a scratch bare repository with an alternates file, so the analyzed repository gets no objects and none of its merge drivers or attributes run. Missing or criss-cross bases, non-local PR heads, agent tasks, cross-repo pairs, and the 60-simulation cap are not-assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ayload budget Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e oracle Adversarial review fixes: - disable git lazy fetch on analyzed-repo reads (a partial clone ran a repository-chosen uploadpack command) - check merge attributes on paths both sides change and refuse merge.renormalize; forward rename settings (each gave a false clean) - not-assessed for submodule conflicts - 20-second simulation budget with per-spawn timeouts; command-free failure details - response-size loop, capped paths, conflictedFileCount - diff the resolved branch tip; scratch parent for race-free tests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- merge.default and branch.<name>.mergeOptions are not-assessed - merge attributes are checked on paths changed by either side, so a rename onto an attributed name is caught - replace refs and grafts are not-assessed; GIT_NO_REPLACE_OBJECTS on every read - a partial clone on git older than 2.45 is not-assessed (lazy fetch cannot be turned off there); config is read before any object read Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- stop dropping conflicts when node details alone exceed the budget; cap node details; cut conflicts before findings - cap merge-attribute paths by total characters (Windows argv) and cap paths in not-assessed details - report the time budget when a spawn is killed at the deadline Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- a merge driver named text, set, or unspecified is not-assessed (check-attr prints it like the default merge state) - run every read from the repository top level, so a subdirectory analysis root no longer hides merge attributes - parse git booleans (non-zero integers are true) - pass --no-ext-diff --no-textconv explicitly (gitPathArgs hides diff) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tory merge settings Round-4 verification fixes: - any repository-local merge.* key outside a harmless allowlist is not-assessed; any merge.<name>.* driver section named text, set, or unspecified is not-assessed (no .driver line needed) - forward diff.algorithm and diff.indentHeuristic - check-attr runs with core.precomposeunicode=false (NFD paths) - on a case-insensitive checkout, a case-variant attributes file or ancestor directory is not-assessed - a core.worktree that resolves to another git directory is not-assessed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-5 verification fixes: - the case-variant attributes check runs on every platform; a real merge on a case-insensitive filesystem ignores core.ignorecase - two changed ancestor directories that differ only by case are not-assessed - read config with -z so a newline in a value cannot forge an entry - list directories with --literal-pathspecs (names starting with ':') Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ig values Round-6 verification fixes: - a non-ASCII name in a changed directory or beside a changed path is not-assessed (filesystems fold Unicode case beyond toLowerCase) - git boolean parsing from -z output: no value is true, empty is false; an empty forwarded value is not-assessed - cap the ls-tree directory argv at 24,000 characters Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the merge oracle Round-7 verification fixes: - forward merge.conflictStyle (diff3 skips conflict refinement, so it changes whether a merge conflicts); only merge, diff3, zdiff3 - pull.twohead other than ort/recursive is not-assessed - the merge.* allowlist applies to every config scope (global includeIf gitdir can reach only the real repository) - a forwarded key with no value is not-assessed (git dies on it) - refuse NTFS-aliasing names (trailing dot or space, 8.3 short names) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NTFS cannot hold a path containing ':' and git for Windows refuses to check one out (core.protectNTFS), so the literal-pathspec case cannot occur there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…acle Round-8 verification fixes (systematic pass over git merge's config readers): - forward diff.algorithm as -X diff-algorithm (merge-tree ignores the config key); the test's ground truth is now a real git merge - compare pull.twohead exactly (case- and space-sensitive) - keys git merge parses strictly must hold a parseable value - a changed path with a .git component is not-assessed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rotection Round-9 verification fixes: - strict and forwarded config values are checked untrimmed (git does not trim quoted values), with git's ranges (merge.log >= 0, verbosity 0-5) - both tips and the merged tree must pass read-tree with core.protectNTFS and core.protectHFS, or the pair is not-assessed (.git., GIT~1, .git::$DATA, .gitmodules symlinks) - relax over-refusals: any merge.ff value, int unit suffixes, case-insensitive diff.algorithm Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…in the merge oracle Round-10 verification fixes: - read all attributes (check-attr -a) on changed paths; anything outside text/eol/crlf/whitespace/export-*/linguist-* is not-assessed (the diff attribute changes rename detection) - parse config integers like git: sign, k/m/g units, 32-bit ranges (merge.stat=3g, merge.log=2g now refused) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-11 verification fix: each side can remove a different line that cleared an attribute, so only the merged tree applies it (for example a required filter a real merge would run). check-attr -a --source=<merged tree> runs in the scratch repository with the same allowlist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… oracle Round-12 verification fix: with merge.directoryRenames, a directory rename places paths (e/y for an added d/y) outside the changed-path set every attribute and path check covers. diff-tree from each tip to the merged tree finds them; any unseen path is not-assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… new paths Round-13 verification fixes: - run the unseen-path and merged-tree attribute checks only for a clean merge; they exist to prevent a false clean, and a conflict verdict never claims clean (directory-rename and file/directory conflicts keep their textual-conflict verdict again) - the caveat states that local uncommitted and untracked files are not considered Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… oracle Round-14 verification fix: vendoring a submodule (its gitlink replaced by ordinary files with the same names) collides with the checked-out submodule's files in a real merge. A path whose mode changes to or from 160000 on either side is not-assessed; submodule bumps stay assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-15 verification fix: a path git accepts can exceed a checkout filesystem's limits (a 255-byte name, about 1,024 bytes on macOS), which fails a real merge's checkout. A changed path with a name over 255 bytes or a total over 1,000 bytes is not-assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…system allows Round-16 verification fix: git stores a symlink target of any length, but creating the link fails past the filesystem path limit (1,024 bytes on macOS), which fails a real merge. A changed symlink whose target is 1,000 bytes or longer (ls-tree -l size) is not-assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… oracle Round-17 verification fix: a changed file that differs from another changed path or a sibling entry only by letter case (README vs readme) overwrites it on a case-insensitive checkout, which fails a real merge. The caveat now states the verdict assumes each commit can already be checked out on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on in the merge oracle Round-18 verification fix: forwarding a local merge.renames=false, merge.directoryRenames, a rename limit, or a non-default diff.algorithm modeled the local repository, but a fresh clone or a hosted merge does not carry those settings and reports the conflicts they hide. Such a setting is now not-assessed; only git's defaults are assessed. The unused -X diff-algorithm path is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-19 verification fix: a real merge ignores a symlinked .gitattributes, but check-attr --source reads its target text as attribute lines, which can hide a rule (merge=binary) the merge applies. The directory listing now carries modes, and a .gitattributes with mode 120000 in the base or either tip is not-assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-20 verification fix: a real merge reads only a regular .gitattributes from disk, strips a UTF-8 byte-order mark, and reads past a NUL byte; check-attr --source reads a gitlink's object as attribute lines, keeps the BOM on line 1, and stops at a NUL. A .gitattributes with a mode other than a regular file, a BOM, or a NUL byte in the base or either tip is not-assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erge oracle Round-21 verification fix: a checkout attribute on a .gitattributes (for example ident) rewrites the file a real merge reads, which can push a line past git's 2,048-byte attribute line limit that check-attr --source still reads from the blob. The attribute allowlist now also applies to each .gitattributes in the base and both tips, and a line of 2,000 bytes or more is not-assessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-22 verification fix: check-attr --source run in the analyzed repository lets its local .git/info/attributes outrank a tree's .gitattributes, which can hide a rule (such as ident on .gitattributes) that a fresh clone applies. Every tree attribute read now runs with --git-dir=<scratch>, which has no info/attributes; the work-tree read stays in the analyzed repository, where it can only add restrictions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Status
LGTM. Four parallel adversarial reviews, then 22 verification rounds, closed every confirmed finding (1 security issue and 37 false cleans); the last verification round found no false clean and no repository-chosen command. CI is green on
caf879228, including Windows unit tests and Windows smoke.What was missing
map_in_flight_conflictsreports symbol-level hazards (WAW, RAW, shared-append, WAR). It could not say whether git will actually conflict. Two branches that edit different parts ofresolveCallSitewere a WAW, the same as two branches that rewrite the same lines. Those need different landing decisions. Ashared-appendpair was always called "safe to land in either order", even when git would conflict.What it does
Each conflict pair gets
textualMerge:textual-conflict, with the conflicted files named;clean-automerge, where the hazard is behavioral only;not-assessed, with a detail.The oracle (
merge-oracle.ts):git merge-tree --write-tree -z --no-messages --merge-base=<base>(the mode/stage lines flag submodule conflicts) between the two tip commits. The merge base is resolved in the real repository.Read-only and safe on an untrusted repository. Probes on git 2.50 showed two problems:
merge-tree --write-treewrites objects into.git/objects.$GIT_DIR/info/attributes, and--attr-sourcedoes not stop it.So the merge runs in a new, empty bare repository in the OS temp directory. It reads the real objects through
objects/info/alternates, and it is removed infinally. InheritedGIT_DIRandGIT_OBJECT_DIRECTORY-style variables are stripped.Never silently clean. These cases are
not-assessed:headRefOid) that is not present locally;Output changes:
textualConflictCountis added.Round-1 review fixes
cat-file -e/merge-baselazy-fetched and ran a repository-chosenremote.origin.uploadpackcommandGIT_NO_LAZY_FETCH=1on every analyzed-repo read in this tool (incl. its existinggit show/git diff). Repo-wide hardening filed as a separate task.-merge,binary,merge=binary(also viainfo/attributes)git check-attr(base, both tips, repo files) on paths both sides change; any non-defaultmergeattribute isnot-assessedmerge.renames=falsegit configand forwarded as-cvalues;merge.renormalizeisnot-assessednot-assessed(the scratch repo cannot see submodule commits)boundResponsehalves until it fitsmin(30 s, remaining); the rest isnot-assessedfile://URL, branch diff vs tipscratchParentoption,pathToFileURL, diffmergeBase..tipRound-2 verification fixes
merge.default=binary(or a custom driver)merge.defaultother thantextisnot-assessedf.txttof.bin(*.bin -merge), B editsf.txtGIT_NO_REPLACE_OBJECTS=1on every read; replace refs or grafts arenot-assessedbranch.<name>.mergeOptionsnot-assessedGIT_NO_LAZY_FETCHnot-assessedbefore any object readRound-3 verification fixes
text,set, orunspecified(check-attrprints it like the default state)merge.*.driverkeys are read; those names arenot-assesseddiffpaths are top-relative,check-attris cwd-relative)rev-parse --show-toplevelmerge.renormalize=2/remote.X.promisor=2not treated as truegitPathArgshidesdifffrom the hardened runner's driver flags--no-ext-diff --no-textconvpassed explicitlyRound-4 verification fixes
merge.text.name/merge.text.recursivewithout a.driverlinemerge.<name>.*section namedtext/set/unspecifiedisnot-assesseddiff.algorithm=patiencediff.algorithmanddiff.indentHeuristicforwarded; a test compares againstgit merge-treein the repository itself for patience, myers, and histogrammerge.*key outside an allowlist of output/tooling keys isnot-assessedcheck-attrruns withcore.precomposeunicode=false.GITATTRIBUTES,Sub/.gitattributesforsub/f.txtnot-assessedcore.worktreepointing into another repositorynot-assessedwhen the top level resolves to a different git directoryRound-5 verification fixes
core.ignorecase=false/unset on a case-insensitive filesystemsub/andSub/both ancestors of changed pathsnot-assessedcore.ignorecase falsegit config -zparsing:/Sub/.gitattributesread as pathspec magic byls-tree--literal-pathspecsRound-6 verification fixes
toLowerCasemisses (.gitattributeſ,ς/vsσ/,ß/vsss/)not-assessedmerge.renames ""(git reads empty as false; the oracle forwardedtrue)-zvalue parsing: no=is true, empty is false; an empty forwarded value isnot-assessedls-treedirectory argvRound-7 verification fixes
merge.conflictStyle=diff3/zdiff3(skips conflict refinement)merge,diff3,zdiff3); a test compares againstgit merge-treein the repository for all threepull.twohead=resolve/octopusort/recursiveisnot-assessedincludeIf "gitdir:"reaches only the real repositorymerge.*allowlist applies to every scopenot-assessedRound-8 verification fixes (systematic config-key pass)
-c diff.algorithmis ignored bymerge-tree(read only for porcelain merges), and the test's ground truth wasmerge-treetoo-X diff-algorithm=; the test's ground truth is now a realgit mergein a worktree, with a non-vacuity check that myers changes the resultpull.twohead=ORTor"ort "(git is case- and space-sensitive)merge.stat,commit.cleanup,core.bigFileThreshold, …) makesgit mergedienot-assessed.gitcomponent (a real merge refuses to check it out)not-assessedRound-9 verification fixes
merge.stat " true",commit.cleanup " strip",merge.renames " true"(git does not trim quoted values)merge.log=-1,merge.verbosity=6(git rejects; merge-ort asserts 0–5)sub/.git./x,sub/GIT~1/x,.git::$DATA/x, a.gitmodulessymlink (a real checkout refuses them)read-treewithcore.protectNTFSandcore.protectHFSin the scratch repositorymerge.ff=Only,merge.log=1k,diff.algorithm=Myers)Round-10 verification fixes
merge.stat=3g,merge.log=2g,core.bigFileThreshold=17179869184g(out of git's range)-diff(or a binary diff driver) changes rename detectioncheck-attr -aon changed paths; anything outsidetext,eol,crlf,whitespace,export-*,linguist-*isnot-assessedRound-11 verification fix
filter, so only the merged.gitattributesapplies a required filter a real merge runscheck-attr -a --source=<merged tree>in the scratch repository (no config, so no filter runs) with the same allowlistRound-12 verification fix
merge.directoryRenames=true, A movesd/toe/and B addsd/y; the merge writese/y, which no check saw (a required filter one/yfails the real merge)diff-treefrom each tip to the merged tree; a path outside the checked set isnot-assessedRound-13 verification fixes
p~<id>) intonot-assessedgit mergefailRound 13 found no false clean on commits.
Round-14 verification fix
160000on either side isnot-assessed; submodule bumps stay assessedRound-15 verification fix
not-assessedRound-16 verification fix
ls-tree -lsize) isnot-assessedRound-17 verification fix
README, B editsreadme(they overwrite each other on a case-insensitive checkout)not-assessedRound-18 verification fix
merge.renames=falseormerge.directoryRenames=falsewas forwarded, modeling the local repository; a fresh clone or hosted merge does not carry it and reports the conflictrenamesoff, a rename limit,merge.directoryRenamesother thanconflict, or a non-defaultdiff.algorithmisnot-assessed(the-X diff-algorithmpath is removed)Round-19 verification fix
d/.gitattributeswhose target text isf mergehides the rootd/f merge=binaryfromcheck-attr --source, while a real merge ignores the symlink and reports a binary conflict.gitattributeswith mode120000in the base or either tip isnot-assessedRound-20 verification fixes
.gitattributes(git strips it from a file on disk, not from a blob).gitattributesblob with a BOM in the base or either tip isnot-assessed.gitattributes(the blob parser stops there; the file parser reads on).gitattributesblob with a NUL byte isnot-assessed.gitattributesgitlink pointing at a blob (a real merge ignores it;check-attr --sourcereads it).gitattributesmode other than100644/100755isnot-assessedRound-21 verification fix
.gitattributes identexpands$Id$on checkout and pushes a line past git's 2,048-byte attribute line limit, so a real merge drops a rulecheck-attr --sourcestill reads.gitattributesitself in the base and both tips; a.gitattributesline of 2,000 bytes or more isnot-assessedRound-22 verification fix
.git/info/attributes(.gitattributes !ident) outranks a tree's.gitattributesincheck-attr --source, hiding a rule a fresh clone applies--git-dir=<scratch>, which has noinfo/attributes; the work-tree read stays in the analyzed repositoryProof
merge-oracle.test.ts(real git, 31 tests, including partial-clone lazy fetch, merge attributes, forwarded rename config,merge.renormalize, a submodule conflict, and a spent budget):clean-automerge; same-line edits givetextual-conflict;git statusdo not change, and no scratch directory is left behind;.gitattributesandinfo/attributesdoes not run, with a non-vacuity control that the same merge inside the repository does run it;not-assessed.interference-map.test.ts: 8 new tests cover the annotation, suggestions, headline, missing tip, agent task, cross-repo pair, a rejected simulation, the 60 cap, branch tip and PRheadRefOidplumbing, and a forged oid.2026-09-13-add-merge-tree-conflict-oracle.openspec validate --specs --strictpasses: 16 of 16.Notes
--merge-base(2.43 for-Xwhendiff.algorithmis set). Older git givesnot-assessedwith git's error.GIT_NO_LAZY_FETCHneeds git 2.45+; on older git a partial clone's lazy fetch is not disabled.refExists/resolveBaseRefinsrc/core/drift/git-diff.ts(used to resolve the base ref) still run withoutGIT_NO_LAZY_FETCH, and so do other tools' git reads. Central hardening inexecFileGitis filed as a separate task.openlore-merge-*temp directory (about 12 KB typical); there is no sweep.docs/mcp-tools.mdis updated for the longer tool description.🤖 Generated with Claude Code