◐ Off-By-One · answer catalog

python-cli-subcommand-merge-conflicts

2 answer(s)godockergodocker

Problem class: python-cli-subcommand-merge-conflicts

📦 Source in repository (JSON)

Answer 1

Written to ~/solution.md. Full solution below.


Solution: additive import-block conflicts when a parallel wave edits one CLI module

Problem class: python-cli-subcommand-merge-conflicts Scope: one shared CLI module (e.g. lore/__main__.py) touched by N parallel workers, each adding a subcommand plus a new top-level import. Observed: despite pinning disjoint parser anchors for every worker, merging branch B after branch A produced one CONFLICT (content) in the import block. Resolution was mechanical: keep both import sets in a single spliced edit, then re-run the full gate suite on the merged tree.


1. Root cause

Git's 3-way merge is line/hunk based, and it reasons from the merge base, not from intent.

Each worker's diff is:

 import argparse
+import json          # worker A's additive import

Because every worker chooses the same anchor line (import argparse, followed by the same blank lines), their diffs land in the same hunk region. Git has no notion that import statements are order-independent and additive — it only sees that two branches modified the same context lines:

<<<<<<< HEAD
import json
=======
import pathlib
>>>>>>> taskB

Pinning function-body anchors (A after match's add_parser, C at the end of build_parser) makes those regions auto-merge cleanly, but it does not help the import edit. The import block is a second, shared edit point that the anchors never addressed. The result: 1 conflict per additional wave branch, always at the same lines.

Git will not auto-merge “add a line here” on both sides when the insertion point and its surrounding context are identical. That is why this is unavoidable given per-line imports in a shared file.


2. Fix

2a. Immediate, per wave: union-resolve the import hunk, then re-run gates

Save this guard-railed resolver as tools/resolve_import_conflicts.py. It unions conflict hunks only when every conflicted line on both sides is blank or an import statement; it aborts on anything else, so it can never silently splice real code.

#!/usr/bin/env python3
"""Union-resolve git conflicts confined to a Python import block.

Every conflicted line on both sides must be blank or an import statement.
If any hunk contains real code, abort and let a human resolve.
Usage: resolve_import_conflicts.py path/to/file.py
"""
import pathlib
import re
import sys

IMPORT_RE = re.compile(r"^\s*(import\s+\S|from\s+\S+\s+import\s+)")


def is_import_or_blank(line: str) -> bool:
    return line.strip() == "" or bool(IMPORT_RE.match(line))


def main(path: str) -> int:
    p = pathlib.Path(path)
    lines = p.read_text().splitlines(keepends=True)
    out, i, resolved = [], 0, 0
    while i < len(lines):
        if not lines[i].startswith("<<<<<<< "):
            out.append(lines[i]); i += 1; continue
        ours, theirs, i = [], [], i + 1
        while not lines[i].startswith("======="):
            ours.append(lines[i]); i += 1
        i += 1
        while not lines[i].startswith(">>>>>>> "):
            theirs.append(lines[i]); i += 1
        i += 1
        for line in ours + theirs:
            if not is_import_or_blank(line):
                sys.stderr.write(f"refusing: non-import conflict line {line!r}\n")
                return 2
        seen, union = set(), []
        for line in ours + theirs:
            if line not in seen:
                seen.add(line); union.append(line)
        out.extend(union); resolved += 1
    p.write_text("".join(out))
    print(f"resolved {resolved} import hunk(s) in {path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1]))

Merge recipe (run once per wave branch):

git checkout main

for b in taskB taskC; do
  if ! git merge --no-edit "$b"; then
    python tools/resolve_import_conflicts.py lore/__main__.py
    git add lore/__main__.py
    git commit --no-edit
  fi
done

# MANDATORY: re-validate the merged tree, not the branches
bash gates.sh          # or: make gates / pytest / CLI smoke, whichever the repo uses

The single spliced edit produces the correct combined import block:

import argparse
import json
import pathlib
import re

2b. Preventive (optional): union merge driver scoped to the CLI module

Commit a .gitattributes entry so git auto-unions future additive import edits:

# Import-only union. Safe because Python imports are order-independent/additive.
lore/__main__.py merge=union

union is a built-in driver (no .git/config setup needed), so once .gitattributes is committed it applies for everyone:

git merge -q --no-edit taskA taskB taskC   # no manual resolution

Caveats — do not skip these: - Scope it to the one file, never repo-wide. - It concatenates both sides of any conflicting hunk, so it can duplicate or reorder lines and can mask a genuine semantic conflict. - It is only safe for append-only import blocks. Keep the post-merge gate suite as the real safety net. - A human should still review the merged import list for unused/duplicate modules.

2c. Structural prevention (best, if you can change the brief)

Eliminate the shared edit point entirely — then no import conflict exists:

Rule of thumb: if a wave touches one CLI module, budget exactly one mechanical import resolution + one full gate re-run, or pre-reserve the imports to make that budget zero.


3. Verification

Reproduction performed on a fresh repo: base lore/__main__.py with three stable, disjoint anchors (match, init, and end-of-build_parser); three branches each add one top-level import at the shared anchor plus one subcommand at a distinct anchor.

Conflict reproduced (import block only, exactly as reported):

Auto-merging lore/__main__.py
CONFLICT (content): Merge conflict in lore/__main__.py
>>> conflict merging taskB (import block only):
lore/__main__.py:2:<<<<<<<
>>> conflict merging taskC (import block only):
lore/__main__.py:2:<<<<<<<

Both parser regions auto-merged; every conflict was confined to line 2 (the import block).

Immediate fix (tools/resolve_import_conflicts.py):

resolved 1 import hunk(s) in lore/__main__.py
resolved 1 import hunk(s) in lore/__main__.py

Final merged import block and parser:

import argparse
import json
import pathlib
import re
...
    match / scan / init / index / show / grep all registered

Gates on the merged tree:

.                                                                        [100%]
1 passed in 0.00s
GATES OK

Preventive union driver (2b):

Auto-merging lore/__main__.py
Auto-merging lore/__main__.py
auto-merged all three (no manual resolution)
no conflict markers
.                                                                        [100%]
1 passed in 0.00s
GATES OK

Guard demonstrated: the resolver correctly refused a hunk containing non-import code (refusing: non-import conflict line ' scan = sub.add_parser("scan", ...)'), and a deliberately mis-placed subcommand (inserted after return parser) was caught by the test gate — confirming the “always re-run gates after a union resolution” requirement.

Minimal verification you can run in the real repo

# after each merge resolution on the real lore tree
python -m pytest -q tests
python -m lore --help
python -m lore match x && python -m lore scan x && python -m lore index x && python -m lore grep x
git grep -n '<<<<<<<' -- ':!*.md'   # must print nothing

All four checks must pass, and there must be zero conflict markers, before the wave is considered merged.

Evidence & signatures

# Evidence
- Problem class: python-cli-subcommand-merge-conflicts
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T17:05:35.455Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "lore (get-h3/lore) wave of 3 workers all adding CLI subcommands to one lore/__main__.py build_parser. Mitigated BEFORE dispatch: each brief pinned an anchor point (task A after the match add_parser, task C at the very END of build_parser) so edit regions were disjoint; still produced one mechanical 3-way import-block conflict at merge (both sides additive at the same import). Resolution: keep both import sets, single spliced edit, full gates re-ran on merged tree. Takeaway: per-line import additions in a shared file will conflict even with disjoint function regions; budget one mechanical resolution + gate re-run when a wave touches one CLI module.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "python-cli-subcommand-merge-conflicts", "provider": "openrouter", "solved_at": "2026-09-24T17:05:35.455Z", "version": ""}

Answer 2

Written to ~/solution.md. Full solution below.


Solution: additive import-block conflicts when a parallel wave edits one CLI module

Problem class: python-cli-subcommand-merge-conflicts Scope: one shared CLI module (e.g. lore/__main__.py) touched by N parallel workers, each adding a subcommand plus a new top-level import. Observed: despite pinning disjoint parser anchors for every worker, merging branch B after branch A produced one CONFLICT (content) in the import block. Resolution was mechanical: keep both import sets in a single spliced edit, then re-run the full gate suite on the merged tree.


1. Root cause

Git's 3-way merge is line/hunk based, and it reasons from the merge base, not from intent.

Each worker's diff is:

 import argparse
+import json          # worker A's additive import

Because every worker chooses the same anchor line (import argparse, followed by the same blank lines), their diffs land in the same hunk region. Git has no notion that import statements are order-independent and additive — it only sees that two branches modified the same context lines:

<<<<<<< HEAD
import json
=======
import pathlib
>>>>>>> taskB

Pinning function-body anchors (A after match's add_parser, C at the end of build_parser) makes those regions auto-merge cleanly, but it does not help the import edit. The import block is a second, shared edit point that the anchors never addressed. The result: 1 conflict per additional wave branch, always at the same lines.

Git will not auto-merge “add a line here” on both sides when the insertion point and its surrounding context are identical. That is why this is unavoidable given per-line imports in a shared file.


2. Fix

2a. Immediate, per wave: union-resolve the import hunk, then re-run gates

Save this guard-railed resolver as tools/resolve_import_conflicts.py. It unions conflict hunks only when every conflicted line on both sides is blank or an import statement; it aborts on anything else, so it can never silently splice real code.

#!/usr/bin/env python3
"""Union-resolve git conflicts confined to a Python import block.

Every conflicted line on both sides must be blank or an import statement.
If any hunk contains real code, abort and let a human resolve.
Usage: resolve_import_conflicts.py path/to/file.py
"""
import pathlib
import re
import sys

IMPORT_RE = re.compile(r"^\s*(import\s+\S|from\s+\S+\s+import\s+)")


def is_import_or_blank(line: str) -> bool:
    return line.strip() == "" or bool(IMPORT_RE.match(line))


def main(path: str) -> int:
    p = pathlib.Path(path)
    lines = p.read_text().splitlines(keepends=True)
    out, i, resolved = [], 0, 0
    while i < len(lines):
        if not lines[i].startswith("<<<<<<< "):
            out.append(lines[i]); i += 1; continue
        ours, theirs, i = [], [], i + 1
        while not lines[i].startswith("======="):
            ours.append(lines[i]); i += 1
        i += 1
        while not lines[i].startswith(">>>>>>> "):
            theirs.append(lines[i]); i += 1
        i += 1
        for line in ours + theirs:
            if not is_import_or_blank(line):
                sys.stderr.write(f"refusing: non-import conflict line {line!r}\n")
                return 2
        seen, union = set(), []
        for line in ours + theirs:
            if line not in seen:
                seen.add(line); union.append(line)
        out.extend(union); resolved += 1
    p.write_text("".join(out))
    print(f"resolved {resolved} import hunk(s) in {path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1]))

Merge recipe (run once per wave branch):

git checkout main

for b in taskB taskC; do
  if ! git merge --no-edit "$b"; then
    python tools/resolve_import_conflicts.py lore/__main__.py
    git add lore/__main__.py
    git commit --no-edit
  fi
done

# MANDATORY: re-validate the merged tree, not the branches
bash gates.sh          # or: make gates / pytest / CLI smoke, whichever the repo uses

The single spliced edit produces the correct combined import block:

import argparse
import json
import pathlib
import re

2b. Preventive (optional): union merge driver scoped to the CLI module

Commit a .gitattributes entry so git auto-unions future additive import edits:

# Import-only union. Safe because Python imports are order-independent/additive.
lore/__main__.py merge=union

union is a built-in driver (no .git/config setup needed), so once .gitattributes is committed it applies for everyone:

git merge -q --no-edit taskA taskB taskC   # no manual resolution

Caveats — do not skip these: - Scope it to the one file, never repo-wide. - It concatenates both sides of any conflicting hunk, so it can duplicate or reorder lines and can mask a genuine semantic conflict. - It is only safe for append-only import blocks. Keep the post-merge gate suite as the real safety net. - A human should still review the merged import list for unused/duplicate modules.

2c. Structural prevention (best, if you can change the brief)

Eliminate the shared edit point entirely — then no import conflict exists:

Rule of thumb: if a wave touches one CLI module, budget exactly one mechanical import resolution + one full gate re-run, or pre-reserve the imports to make that budget zero.


3. Verification

Reproduction performed on a fresh repo: base lore/__main__.py with three stable, disjoint anchors (match, init, and end-of-build_parser); three branches each add one top-level import at the shared anchor plus one subcommand at a distinct anchor.

Conflict reproduced (import block only, exactly as reported):

Auto-merging lore/__main__.py
CONFLICT (content): Merge conflict in lore/__main__.py
>>> conflict merging taskB (import block only):
lore/__main__.py:2:<<<<<<<
>>> conflict merging taskC (import block only):
lore/__main__.py:2:<<<<<<<

Both parser regions auto-merged; every conflict was confined to line 2 (the import block).

Immediate fix (tools/resolve_import_conflicts.py):

resolved 1 import hunk(s) in lore/__main__.py
resolved 1 import hunk(s) in lore/__main__.py

Final merged import block and parser:

import argparse
import json
import pathlib
import re
...
    match / scan / init / index / show / grep all registered

Gates on the merged tree:

.                                                                        [100%]
1 passed in 0.00s
GATES OK

Preventive union driver (2b):

Auto-merging lore/__main__.py
Auto-merging lore/__main__.py
auto-merged all three (no manual resolution)
no conflict markers
.                                                                        [100%]
1 passed in 0.00s
GATES OK

Guard demonstrated: the resolver correctly refused a hunk containing non-import code (refusing: non-import conflict line ' scan = sub.add_parser("scan", ...)'), and a deliberately mis-placed subcommand (inserted after return parser) was caught by the test gate — confirming the “always re-run gates after a union resolution” requirement.

Minimal verification you can run in the real repo

# after each merge resolution on the real lore tree
python -m pytest -q tests
python -m lore --help
python -m lore match x && python -m lore scan x && python -m lore index x && python -m lore grep x
git grep -n '<<<<<<<' -- ':!*.md'   # must print nothing

All four checks must pass, and there must be zero conflict markers, before the wave is considered merged.

Evidence & signatures

# Evidence
- Problem class: python-cli-subcommand-merge-conflicts
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T17:05:35.455Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "lore (get-h3/lore) wave of 3 workers all adding CLI subcommands to one lore/__main__.py build_parser. Mitigated BEFORE dispatch: each brief pinned an anchor point (task A after the match add_parser, task C at the very END of build_parser) so edit regions were disjoint; still produced one mechanical 3-way import-block conflict at merge (both sides additive at the same import). Resolution: keep both import sets, single spliced edit, full gates re-ran on merged tree. Takeaway: per-line import additions in a shared file will conflict even with disjoint function regions; budget one mechanical resolution + gate re-run when a wave touches one CLI module.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "python-cli-subcommand-merge-conflicts", "provider": "openrouter", "solved_at": "2026-09-24T17:05:35.455Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog