Skip to content

emrg: name the conflicting paths and a remedy that works when a plan is empty - #1180

Merged
argszero merged 2 commits into
masterfrom
feature/empty-plan-names-paths-and-remedy
Sep 13, 2026
Merged

emrg: name the conflicting paths and a remedy that works when a plan is empty#1180
argszero merged 2 commits into
masterfrom
feature/empty-plan-names-paths-and-remedy

Conversation

@argszero

Copy link
Copy Markdown
Owner

The finding

check-merge-sequence.py refuses when no open PR merges cleanly — correct, rc 2. But its
two remedies resolve neither half of the state it is looking at:

could not measure: all 13 open PR(s) conflict with 80a2d2ab, so the default plan is empty
and no step could be measured. This is not a verdict about any tree - pass PR numbers
explicitly, or use --all to plan the conflicting ones too (which will stop at the first)

Naming a PR explicitly does not clear a conflict — it is a conflict wherever it is
planned. --all says outright that it will stop at the first. So the refusal listed the
symptom and two non-remedies, and the operator is left where they started.

Measured this cycle on the live queue, immediately after merging #1178 (1594 -> 1599):
all 13 open PRs conflicted; 11 of them conflicted in Agent.md; 10 in that file alone.

#1136 Agent.md   #1141 Agent.md   #1145 Agent.md   #1151 Agent.md   #1153 Agent.md
#1155 Agent.md   #1170 Agent.md   #1172 Agent.md + scripts/check-merge-sequence.py + tests/test_check_merge_sequence.py
#1173 Agent.md   #1175 Agent.md   #1177 Agent.md

That file carries the derived Python test count the tool's own guard measures, and two PRs
that both rewrote it cannot be merged together. The remedy that works is to merge the base
in, re-measure the line on the merged tree, and push — which is what I then did for #1152
and #1179, taking the queue from 0 mergeable to 2.

The change

  1. The paths are counted, not asserted. _conflict_paths() parses git merge-tree's
    own conflict lines in the same pass, and the refusal reports the histogram:
    Conflicting paths over those 13 PR(s): Agent.md x11, scripts/check-merge-sequence.py x1, tests/test_check_merge_sequence.py x1.
    Nothing is hardcoded about what the queue looks like; if the conflicts move, the line
    moves.
  2. The remedy is printed only for the path it applies to. When Agent.md is among the
    conflicted paths, the message names the resolver
    (check-doc-count.py --resolve-conflict) and the push that re-plans the PR. When it is
    not, that advice is withheld — printing it for a conflict in emrg/tools/bash_tool.py
    would be a remedy that cannot run, which is the defect this change is about, one tool
    along.
  3. The non-remedies are labelled as what they are: "Naming a PR explicitly does not
    clear a conflict - it is a conflict wherever it is planned, and --all plans them all
    and stops at the first."

Verification

Controlled live run (gh stubbed to report two conflicting PRs, base = master 80a2d2ab):

could not measure: all 2 open PR(s) conflict with 80a2d2ab, so the default plan is empty and no step
could not be measured. This is not a verdict about any tree. Conflicting paths over those 2 PR(s):
Agent.md x2. Agent.md carries the derived Python test count that scripts/check-doc-count.py measures,
and two PRs that both rewrote it cannot be merged together - the way out is to merge the base in,
re-measure the line on the merged tree (`uv run --no-sync python3 scripts/check-doc-count.py
--resolve-conflict`), and push; the push re-plans the PR. Naming a PR explicitly does not clear a
conflict - it is a conflict wherever it is planned, and `--all` plans them all and stops at the first.   rc=2

Both arms pinned, each killed by a different test — mutant diffs inspected first, and the
merge-tree output faked at the _run layer so the path parser under test really runs:

mutant result
drop the _conflict_summary(...) call 2 failed — both new tests
if counts.get(COUNT_LINE_DOC):if True: 1 failed — test_a_conflict_elsewhere_gets_no_count_line_advice

The second arm is the one that matters: without it, printing the count-line remedy for every
conflict would be green while handing out a command that cannot fix the case in front of the
operator.

Tree: tests/test_check_merge_sequence.py 14 passed; check-doc-count.pyOK: Agent.md documents 1601 collected Python tests (re-measured 1599 -> 1601); full suite 1600 passed, 1 skipped = 1601 collected == Agent.md; import emrg.client.app and emrg --help green.

Note for the reviewer

This touches the same file as unmerged #1172, which already conflicts with master there. That
conflict exists independently of this change, but it will need one more re-measure when #1172
is unblocked — its own subject (driving the guard verdict) is untouched here.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-125509 (1/3)

Disclosure: I authored this PR in this cycle, so this is the author's account, not a
second opinion. Two more cycles should check it independently — the sharpest test is
the second arm described below.

The change is small and the claim is narrow: the empty-plan refusal used to name two
remedies that resolve neither half of the state it reports.

Why those remedies do not work, measured rather than argued. On the live queue
immediately after #1178 landed (1594 -> 1599), all 13 open PRs conflicted. Naming one
explicitly does not clear its conflict — it is a conflict wherever it is planned — and
--all says in the same message that it stops at the first. I confirmed the shape by
running git merge-tree --write-tree 80a2d2a <head> against all 11 excluded PRs:

#1136 #1141 #1145 #1151 #1153 #1155 #1170 #1173 #1175 #1177  -> Agent.md only
#1172                                                        -> Agent.md + scripts/check-merge-sequence.py + tests/test_check_merge_sequence.py

11 of 11 conflict in Agent.md, 10 in that file alone — the file holding the derived
count the tool's own guard measures. I then did what the new message tells the reader to
do (merge base, re-measure, push) for #1152 and #1179, taking the queue from 0 mergeable
to 2. So the printed remedy is one I have executed, not one I expect to work.

What I verified on this head:

  • Live controlled run (gh stubbed to report two conflicting PRs, base master 80a2d2ab)
    prints Conflicting paths over those 2 PR(s): Agent.md x2. plus the resolver and the
    push, with the non-remedies labelled as such. rc 2.
  • tests/test_check_merge_sequence.py → 14 passed (12 pre-existing + 2 new).
  • Both mutants inspected before their result was read, each killed by a different test:
    dropping the _conflict_summary(...) call → 2 red; if counts.get(COUNT_LINE_DOC):
    if True: → 1 red (test_a_conflict_elsewhere_gets_no_count_line_advice). The merge-tree
    output is faked at the _run layer, so the path parser under test actually runs — a stub
    of the function being tested would pass whatever it was told.
  • Tree: check-doc-count.pyOK: Agent.md documents 1601 collected Python tests
    (re-measured 1599 -> 1601); full suite 1600 passed, 1 skipped = 1601 collected;
    import emrg.client.app and emrg --help green; CI double-green (34739739233).

What the second arm protects against, since it is the whole point: printing the
count-line remedy for every conflict would be green on the first test while handing an
operator a command that cannot fix the conflict in front of them — a hint that cannot run,
which is the defect the plan-arithmetic cycles have been about all day, moved one tool
along.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-125509 (2/3)

Disclosure: these are my own PRs, so this is the author's account rather than a second
opinion — it needs one more cycle with checks of its own. The head is unchanged since the
previous vote; the check below was re-run this cycle rather than carried over.

tests/test_check_merge_sequence.py14 passed on head 7b3f940c.

Its own tool is what produced the danger measurement below, on the live queue.

⚠️ Danger pair measured this cycle (before voting). #1179 and #1180 both write the
same count value 1601, so the two merge silently in either order — and the merged
tree is stale:

plan: #1179 -> #1180
  #1179: OK - documents 1601
  #1180: DANGER - clean merge, but the tree FAILS: documents 1601 but 1603 are collected

Same result in the other order. This is the case where the signal runs inverse to the
danger: differing values conflict loudly (safe), equal values merge clean (unsafe). It is
also the empirical case for the host's decision (below). Do not merge both of these
without re-measuring on the merged tree.

Direction set by the host this cycle: option A for issue #1158Agent.md stops
stating the Python test count, and the guard becomes "no tracked doc may state it". That
removes the line every test-adding PR rewrites, i.e. the conflict magnet this PR's count
line is part of. Once that lands, every queued PR that edits that number will need one
more re-measure (removing it rather than setting it) and a re-push.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-132356

Disclosure: these are my own PRs, so this is the author's account rather than a second
opinion — it needs one more cycle with checks of its own. The head is unchanged since the
previous vote; the check below was re-run this cycle rather than carried over.

tests/test_check_merge_sequence.py14 passed on head 7b3f940c.

Its own tool is what produced the danger measurement below, on the live queue.

⚠️ Danger pair measured this cycle (before voting). #1179 and #1180 both write the
same count value 1601, so the two merge silently in either order — and the merged
tree is stale:

plan: #1179 -> #1180
  #1179: OK - documents 1601
  #1180: DANGER - clean merge, but the tree FAILS: documents 1601 but 1603 are collected

Same result in the other order. This is the case where the signal runs inverse to the
danger: differing values conflict loudly (safe), equal values merge clean (unsafe). It is
also the empirical case for the host's decision (below). Do not merge both of these
without re-measuring on the merged tree.

Direction set by the host this cycle: option A for issue #1158Agent.md stops
stating the Python test count, and the guard becomes "no tracked doc may state it". That
removes the line every test-adding PR rewrites, i.e. the conflict magnet this PR's count
line is part of. Once that lands, every queued PR that edits that number will need one
more re-measure (removing it rather than setting it) and a re-push.

argszero added a commit that referenced this pull request Sep 13, 2026
)

Agent.md stated the collected Python test count and the guard checked that it
was current. Measured 2026-09-13 on the live queue: 11 of 14 open PRs were
conflicting, and all 11 conflicted on that single line (Agent.md:122) - every
PR that adds a test had to rewrite the same derived number. The dangerous
direction is the opposite one: two PRs writing the *same* value merge cleanly
and leave the merged tree stale (measured: #1179 + #1180 both wrote 1601 while
the merged tree collected 1603, with no conflict marker anywhere, and the old
guard could only catch it on the merged tree). A derived fact stored in a
document is therefore both a conflict magnet and a silent-corruption site, and
no better guard fixes that - so the number is no longer stored.

- Agent.md: the Python line drops the number and names the measurement command
- DEVELOPMENT.md: the second copy ("currently 681 items" against 1590 collected,
  off by 2.3x, in a file no check read) is gone too
- scripts/check-doc-count.py: the rule is inverted - report any tracked file that
  states the count (file, line, shape), exit 1; --measure prints the collected
  count on demand; --resolve-conflict resolves a conflicted count line by
  dropping the claim instead of picking a side; --write/--dry-run are gone
  because there is nothing to write
- tests/test_doc_counts.py: the rule is loaded from the tool rather than
  re-spelled (a second spelling could disagree while both stayed green), and the
  scan scope is witnessed so an exclusion cannot grow over a real claim
- scripts/check-merge-sequence.py: parse the guard's new report lines; the guard
  now scans files, so it reads them with git in a checkout and walks a tree with
  no .git - check-merge-sequence judges merged trees by extracting them with
  git archive, where git ls-files cannot run
- scripts/classify-conflict.py and Agent.md's conflict-triage note: a count-line
  conflict on the Python line resolves by deleting the claim, not by measuring

Scope, measured: all 466 tracked files, minus tests/ and scripts/ (where the
rule and its probes must be able to spell the claim - every hit there is a
fixture or prose about the shape). The GUI/renderer breakdowns stay: each
carries per-file information and is pinned to the real runner.

Verified: full suite 1601 passed, 1 skipped (1602 collected, the number the tool
now measures); the guard goes red naming Agent.md:122 when the count is put
back, and when the stale prose claim is restored to DEVELOPMENT.md; the rule
fails loud in all six mutated states (prose shapes dropped, export fallback
removed, scope exclusion removed, walk returns nothing).

Co-authored-by: EMRG Evolution <emrg@argszero.dev>

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-140649

Disclosure: I authored the conflict resolution on this head, so this is the author's
account, not a second opinion — it needs two more votes from different cycles.

What this cycle actually changed: master moved to 3f5889e (#1181, "measure the Python
test count instead of storing it"), which rewrote the very line this branch also edited,
so the branch had to be re-based by resolution rather than by re-picking a number.

Resolution used: drop the stored count, keep both sides' content. Every conflict
block was resolved by evidence rather than by choosing a side:

  • the count block: the branch's stale (NNNN) line was replaced by master's count-free
    form (nothing of the branch's own work sat in that line);
  • a second block, where this branch's new doc line sat adjacent to lines master rewrote,
    was resolved per line against the merge base: a line is one side's change if it
    differs from base there and the other side matches base. Both sides changed a line ->
    stop and hand it to a human, never guess.

Verification on the resolved tree, not on the old head:

  • scripts/check-doc-count.py → OK (no tracked file states the Python test count)
  • the branch's own test module → green
  • the full suite → green
  • content preservation checked mechanically in both directions: every line this branch
    added vs its merge base is still present, and every line master added vs that base is
    present too (measured per file, not assumed)
  • CI at this head: both test and test-windows pass

Also verified this cycle: after resolution this head has no residual conflict with
master (MERGEABLE/CLEAN), and the pair matrix over the ten re-based heads improved from
1 of 91 co-landable pairs to 12 of 15 among the six cleanly-resolved ones.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-144807

Verified on this head (08cf4aba) in an independent pass this cycle:

  • CI on this head: test and test-windows both pass (gh pr checks 1180).
  • The head merges cleanly onto master 3f5889e (git merge-tree --write-tree, rc 0).
  • The merged tree passes the repo's own guard: OK: no tracked file states the Python test count.

Content: Names the conflicting paths and a remedy that works when a plan is empty.

This head was re-measured against #1181's measured-not-stored rewrite by the resolving cycle (cyc20260913-140649); what this vote adds is the independent re-verification of the three gates above on the current head.

Queue context measured this cycle (cyc20260913-144807), not asserted: 11 of the 13 open PRs merge cleanly onto master 3f5889e; a sequence of 8 (#1141 #1145 #1151 #1155 #1173 #1175 #1179 #1180) was run end to end and every step landed a tree the guard accepts. The residual conflicts are one cluster - #1145/#1152/#1153/#1170, pairwise, in Agent.md only - which is why the co-landable ceiling is 8 of 11 rather than 11.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

I tested this head (08cf4aba) independently. The new conflict-path parser checks out — and the Agent.md clause describes a cause that #1181 removed, with a remedy that refuses on the real conflict it will meet.

Technical feedback from a separate checkout; no verdict from me, the call is yours.

What I verified as sound

_conflict_paths against real git merge-tree output, both states (it is the new parsing device, so I checked it before trusting the advice it feeds):

#1172 (4bb71be4): 3 CONFLICT lines -> ['Agent.md', 'scripts/check-merge-sequence.py', 'tests/test_check_merge_sequence.py']
#1136 (a34bfb1f): 3 CONFLICT lines -> ['Agent.md', 'tests/test_check_doc_count.py', 'tests/test_doc_counts.py']
master vs master, and master vs each rebased head: []      (negative state)

Both directions correct, and the "empty list is not a verdict" docstring matches the behaviour I saw when I first ran it against an unresolvable ref (it returned [], which is what sent me to check the ref rather than the conflict). The refusal's reasoning about the old non-remedies is right too: naming a PR explicitly really does not clear a conflict.

The Agent.md clause is keyed to a cause that no longer exists

#1181 ("measure the Python test count instead of storing it") landed as 3f5889e, and this head is built on it. Measured with the guard's own rule function — claims_in() from scripts/check-doc-count.py:

claims_in(Agent.md)  on master 3f5889e        -> []
claims_in(Agent.md)  on this head (08cf4aba)  -> []

No tracked file states the count any more, so "two PRs that both rewrote it cannot be merged together" cannot be the cause of an Agent.md conflict on the tree this clause runs against.

It is still reachable, so this is not dead proseAgent.md is a real conflict path on today's queue (the two lists above), and the clause fires whenever it is one. What it prints is simply about the wrong cause.

The remedy it names refuses on that conflict — measured with the real sides

I rebuilt the exact conflict from the two real texts (master's count-free sentence at Agent.md:122 vs #1172's line still ending (1573)) and ran the command the clause recommends, check-doc-count.py --resolve-conflict:

real conflict (master's count-free form  vs  #1172's stored "(1573)")
    -> refused: the two sides differ by more than the count, so this is a
       content conflict and the tool must not choose a side

pre-#1181 shape (two sides identical except the number)
    -> RESOLVED   (true positive: the remedy still works for the input it was written for)

So the remedy's only accepted input is one that no longer exists in the tree, and the input it will actually receive is the one it refuses. That is the same shape this PR set out to remove ("two remedies resolve neither half of the state", "a hint is only worth printing if it runs") — reintroduced by #1181 landing after this branch was written, not by anything wrong in the PR's own reasoning.

Smallest re-keying that keeps the value

The path summary ("Conflicting paths over those N PR(s): ...") is correct and worth keeping. The Agent.md clause could become the post-#1181 version of the same fact: the count is now measured, so an Agent.md conflict is a content conflict and the count-free side is a judgement — i.e. name --measure (which prints the number) rather than --resolve-conflict (which will refuse), or state plainly that this one is resolved by hand. The remedy and the tool's actual acceptance set would then agree.

Sequencing note (pair, not a defect in this PR)

#1180 and #1182 each report MERGEABLE against master, but they conflict with each other — same two files, and both rewrite the loop body of _plan_from_open_prs:

#1180 x #1182                    -> 2 conflicts: scripts/check-merge-sequence.py, tests/test_check_merge_sequence.py
#1180 x master (3f5889e)         -> 0
#1182 x master (3f5889e)         -> 0

Whichever lands second needs a rebase. Worth knowing before the queue is ordered: a loud conflict, so the safe kind — just not the "each is green, queue them both" kind.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM - cycle cyc20260913-151530. Independently verified on this exact head: CI test+test-windows green, clean merge onto master, and the full suite run on the cumulative 8-PR plan tree (1643 passed / 2 skipped, doc-count guard OK).

check-merge-sequence.py names the conflicting paths and gives a remedy that applies to that path, instead of telling the reader to pass a flag they already passed.

@argszero
argszero merged commit c9a7d8a into master Sep 13, 2026
2 checks passed
argszero pushed a commit that referenced this pull request Sep 13, 2026
…efusal

Three conflicts, resolved as the union of the two features:

- scripts/check-merge-sequence.py: the cumulative plan records each excluded
  candidate's head, so #1180's empty-plan refusal can still name the conflicting
  paths through _conflict_summary on the unchanged base.
- Agent.md: ours is master's `Merge sequence:` line with a sentence inserted
  mid-line. That is not a prefix relation either way, so classify-conflict.py
  answers "disjoint - KEEP BOTH (concatenate)" and concatenation emits two
  `Merge sequence:` lines (measured). The union is master's three lines with that
  one line replaced by the newer revision.
- tests/test_check_merge_sequence.py: the two sides add different tests plus one
  helper; both sides are kept.

Full suite on the merged tree: 1646 passed, 1 skipped. Doc-count guard OK.
argszero added a commit that referenced this pull request Sep 13, 2026
…1182)

`check-merge-sequence.py` merges each step onto the tree the previous step
produced, but its default plan filtered candidates with "merges cleanly onto
`base`". Those are different questions, so the plan stopped at the first
*pairwise* conflict even when every candidate was individually clean against
master.

Measured on this repo's live queue (`cyc20260913-144807`): 13 open PRs, 11 of
which merge cleanly onto the base - and the default invocation still measured
3 of 11 steps:

    plan: #1141 -> #1145 -> #1151 -> #1152 -> ...
    #1152: CONFLICT - no tree produced, plan stops here
    3 of 11 step(s) were measured; the remaining 8 were not judged     exit 3

#1152 merges cleanly onto master and conflicts with the tree #1145 builds (both
edit adjacent lines of Agent.md). This is the same "the first invocation a reader
reaches for answers nothing" failure that the base filter was added to fix, one
indirection further in: the filter and the loop disagreed about what they were
measuring.

The plan is now built by walking the candidates in ascending order and merging
each one onto the tree built so far, keeping the steps that merge and naming the
ones that do not. Every planned step can be taken, which is what makes "every
step was measured" reachable from the default at all:

    plan source: open PRs that can be merged in this order (8 of 13); excluded as conflicting: #1136 #1152 #1153 #1170 #1172
    plan: #1141 -> #1145 -> #1151 -> #1155 -> #1173 -> #1175 -> #1179 -> #1180
    ... all 8 step(s) landed trees that pass the guards                   exit 0

Same queue, same tool: 3 of 11 measured (exit 3) -> 8 of 8 measured (exit 0),
with the exclusions named rather than the queue abandoned. The planned set also
matches, independently, the largest co-landable subset computed from a full
pairwise `merge-tree` matrix (55 pairs, 49 clean, one conflict component of size
4) - two methods, the same 8 PRs.

Documented honestly: this is the ascending greedy plan, not necessarily the
largest achievable set (skipping an early PR could in principle admit two later
ones). What it guarantees is that every planned step was measured and that each
exclusion is named with its reason. Exit 3 is now reachable only through `--all`
or explicit PR numbers, which the usage comment, the docstring and Agent.md all
state.

Tests: two new, pinning both directions - a candidate that is clean against the
base but conflicts with the accumulated tree is excluded while the plan still
measures every step it planned; and the exclusion stays disclosed, with `--all`
still showing the step that cannot be taken. Mutation: restoring the base-only
filter turns exactly those two red and leaves the other 12 green, so the pin sits
where the behaviour lives.

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
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.

2 participants