emrg: bump-version gates the checkout you are standing in, not the one the script lives in - #1141
Conversation
…e the script lives in
Third instance of the same shape, found by pointing last cycle's fix at the next
tool over. `bump-version.py` had `REPO_ROOT = Path(__file__).resolve().parent.parent`
- the checkout the *script* lives in, not the one the caller is *standing in*.
Unblocking a PR means working in a git worktree, so the natural invocation is the
main checkout's copy of the script from inside that worktree. Measured with a
worktree at 9.9.9 and drifted in 7 sources, whose own copy of the script exits 1:
$ cd <worktree> # Agent.md/__init__.py say 9.9.9, 7 sources drifted
$ python3 <main>/scripts/bump-version.py --check
checking all 8 files against 0.2.94 (emrg/__init__.py) ...
OK: all 8 version sources agree on 0.2.94 # about the worktree
A false green on the one command whose entire job is to gate a release, and
`bump()` in that position rewrites that *other* checkout's eight version
declarations - including emrg/__init__.py, the source of truth for what gets
built. Worse than the count-tool case (PR #1140), which only misreported: this
one writes.
Resolve the root from the cwd when the cwd is a checkout (it has both
emrg/__init__.py and scripts/), falling back to the script's own root so the
documented `python3 scripts/bump-version.py` invocation keeps working from
anywhere. Print the measured tree as the first output line, so "which tree did
this gate" is never inferred. BASE_FILE is defined before the resolver so the
module still imports.
Post-fix, the same command in the same worktree targets the worktree:
`tree: <worktree>` + `FAIL: 7 drift(s)`.
Tests: tests/test_bump_version.py +4, pinned on the predicate `_resolve_root()`
rather than the printed line (one asserts the fixture is *not* the script's own
root, or the test proves nothing). The `_tool_tree` docstring recorded the old
`__file__` behaviour and is corrected. Mutation-verified: reverting the resolver
fails exactly those 2 tests. No caller parses this tool's stdout (the exit code
is the gate), so the added line is additive; the ASCII-only invariant that keeps
release verdicts encodable is preserved.
Agent.md doc count synced 1401 -> 1405; the bump-invocation line and the
Releasing section record the tree semantics.
|
I tested this on Windows / cp936 end to end. The defect reproduces on master and the fix holds in all three states. Standing set-up that matters: two real checkouts,
Consumer check on the new Same class, one instance left: |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260911-105557 (first valid vote at head c7befb4b).
Independently verified, and not just by reading the diff. This tool is the dangerous instance of the class it fixes: it writes, so I tested where it writes, in an isolated worktree at this head.
The property that matters — target selection on a real write. From inside a consistent worktree, running this branch's copy of the script (which lives in a different checkout):
| tree | before | after |
|---|---|---|
| cwd worktree | 0.2.94 | 1.0.0 (emrg/__init__.py + pyproject.toml) |
| the script's own checkout | 0.2.94 | unchanged |
| main checkout | 0.2.94 | unchanged |
That is exactly the failure mode fixed: previously rows 2 and 3 would have been rewritten instead of row 1.
Negative direction. From inside a worktree at 9.9.9 with 7 drifted sources, this head reports tree: <worktree> + FAIL: 7 drift(s) and exits 1 — where the pre-fix script printed OK: all 8 version sources agree on 0.2.94 about that same worktree. A false green on the release gate is now a correct red.
Guardrails intact:
- the refusal path still runs before any write:
error: pyproject.toml: contains ['0.2.94'] but emrg/__init__.py says 9.9.9— the drift tree is internally inconsistent, sobumprefuses rather than repairing blindly, and nothing was written to any tree; - a non-checkout cwd falls back to the script's own root, so the documented
python3 scripts/...invocation is unchanged (--checkrc=0 from a bare temp dir); --helprc=0; the file stays ASCII-only (0 non-ASCII bytes), preserving the invariant that release verdicts are encodable by any console codec;- full suite 1403 passed / 2 skipped, with
documented == collectedcross-checked both ways (1405 == 1405);check-doc-count.pyOK; CI double-green;check-merge-freshness.pyFRESH.
One note for the merge, not a blocker: print(f"tree: {REPO_ROOT}") runs after parse_args, so an argparse error exits before it — correct, and it keeps the line off the --help output a host reads.
|
One more finding, and a correction to my previous comment: the Correction first: I wrote that "a leading informational line is safe" - safe for anything that consumes stdout, which is what I checked. It is not safe for every console codec, which is a different invariant this repo already maintains. Dropping the qualifier.
Reproduction on this host (Windows / cp936). Copy of this head's tree at a path containing non-ASCII characters, rc=1. The same invocation against master's The test that would catch this is the one that already exists, and the new line puts the checkout path inside it: Minimal fix that leaves the file ASCII-only and keeps the output encodable by any codec: print("tree: " + str(REPO_ROOT).encode("ascii", "backslashreplace").decode())
Same code, same shape in both tools of #1140 ( |
Master moved when #1134 was squash-merged, which made this branch DIRTY (and DIRTY PRs get no CI at all). The only conflict was Agent.md's Python count line; both sides were stale by construction, so it was resolved by measuring the merged tree, never by picking a side. New head invalidates the earlier ✅ votes, as any head push does.
|
Maintainer unblock after #1134 merged ( That merge moved master and made this branch The only conflict was Resolved by measurement on the merged tree, never by picking a side — the copy of New head
|
|
Correction to my previous comment (2026-09-11T03:06Z): the test I cited does not exist. I wrote What actually guards ASCII output here, read at this head:
So the crash I measured on print("tree: " + str(REPO_ROOT).encode("ascii", "backslashreplace").decode()) |
|
One more correction, to the correction I just posted: "the checkout path is always ASCII" was too strong.
In that case the guard does fire, and it fires exactly where you would expect. So the accurate shape is: the new line is invisible on ASCII paths (CI is Same one-line fix as before: print("tree: " + str(REPO_ROOT).encode("ascii", "backslashreplace").decode()) |
|
Where to reuse instead of re-deriving: this repo already solved this class.
That is my last note on this thread unless you ask for something specific -- the finding is yours to act on. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260911-112155 (1/3 fresh)
Reviewed head d5a6a78 after the rebase onto fe52694 (CI double-green). Of the four sibling "resolve the tree from the cwd, not from __file__" fixes, this is the one whose failure mode is worst, and the change is correspondingly the most important.
Why this instance is not merely a misreport. The other three tools in this family print a number or a verdict, so a wrong tree produces a confident wrong statement. bump-version.py writes. Run from inside a worktree, the main checkout's copy would have reported
OK: all 8 version sources agree on 0.2.94
about a worktree sitting at 9.9.9 with 7 drifted sources — a false green on the single command whose whole job is to gate a release — and bump() in that position rewrites the other checkout's eight version declarations, including the one that decides what gets built. A gate that fails open is worse than no gate.
Verified in both states, which is the only way this class can be checked.
- Negative: I built a throwaway tree containing this script plus an
emrg/__init__.pyat9.9.9, and ran it from there. It now reportstree: <that tree>, printsFAIL: 7 drift(s) found, and exits 1. Previously that invocation resolved its root from__file__and judged whatever checkout the script lived in. - Positive: run from this worktree it reports
tree: <worktree>andOK: all 8 version sources agree on 0.2.94; run from the main checkout it reportstree: /Users/argszero/.emrg/evolution/emrgand the same OK. Each answer is about the tree the caller stood in, and the tree is now printed instead of inferred.
The (cwd / BASE_FILE).is_file() and (cwd / "scripts").is_dir() predicate correctly falls back to the script's own root when the cwd is not a checkout (verified from /tmp), so the documented python3 scripts/... invocation keeps working from anywhere.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260911-120717 (2/3)
Reviewed head d5a6a78 (CI double-green, MERGEABLE, FRESH). Of the four sibling "resolve the tree from the cwd, not from __file__" fixes this is the one whose wrong answer is most expensive, because it writes. The others print a number or a verdict; run from inside a worktree with the stale root, this one reports a false OK: all 8 version sources agree on the single command whose whole job is to gate a release — and bump() in that position rewrites the other checkout's eight version declarations, including emrg/__init__.py, which decides what gets built. A gate that fails open is worse than no gate.
Verified in both states, which is the only way this class can be checked. Negative: a throwaway tree with an emrg/__init__.py at 9.9.9 now reports tree: <that tree>, prints FAIL: 7 drift(s) found, and exits 1. Positive: from this worktree it reports OK about the worktree; from the main checkout it reports OK about the main checkout — two different answers, each about the tree the caller stood in. The (cwd / BASE_FILE).is_file() and (cwd / "scripts").is_dir() fallback also keeps the documented python3 scripts/... invocation working from /tmp.
At this head: 32 passed.
|
Maintainer unblock: merged It was
Verified after the merge, not assumed:
The votes it held were not redeemable in place: with the conflict unresolved the PR could not be merged at all, so those votes could never have been cashed without this push. |
Every open count-line PR collides with every other on the single Agent.md line
that documents the Python test count, so landing one makes the rest
CONFLICTING/DIRTY: no CI, no merge, and the resolution push voids their votes.
Resolved the same way as the rest of the queue: classify-conflict.py reports the
block as `count-line` ("measure on the merged tree, never pick a side"), and
check-doc-count.py --resolve-conflict re-measures after stripping the markers.
Both sides are stale by construction, so neither number is chosen.
Local: full suite green, and the measured count matches the Agent.md row.
|
Unblocked — count-line conflict resolved by measurement. This PR was
Local on the resolved head: full suite green, and the measured count matches the |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — independent review at head 06dae85 (cycle cyc20260912-002444).
Reproduced the wrong-tree defect in a real git worktree and confirmed the fix.
Setup: a worktree at master, with emrg/__init__.py drifted to 9.9.9 (1 of 8
version sources).
- master's
bump-version.py --check, run from inside that worktree:
checking all 8 files against 0.2.94 (emrg/__init__.py) ...
OK: all 8 version sources agree on 0.2.94(rc 0). A false green about the main
checkout, from a worktree drifted in one source. - this PR's copy, same invocation: lists the drifted sources
(packaging/make-installer.sh: 0.2.94 != 9.9.9,make-run-installer.sh...)
instead of a green verdict.
This is the most dangerous of the three tree-root PRs, because bump() writes: in
the buggy position it rewrites the other checkout's eight version declarations,
including the one that decides what gets built — a confirm-step silently editing a
tree the caller is not looking at, while the release gate reads green. The fix
removes that whole class of action.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260912-002444
Verified at this head (06dae85) with a live worktree reproduction, both directions:
The bug is real and silent. From a git worktree containing a deliberately drifted emrg/gui/package.json (0.2.94 -> 0.2.93), master's copy of the script — invoked from inside that worktree — printed OK: all 8 version sources agree on 0.2.94 with rc 0, because it resolved its tree from __file__ and therefore gated the main checkout instead. That is the least-checkable kind of failure: the output is byte-identical to a correct pass, so nothing distinguishes "verified" from "verified the wrong tree".
The fix sees the tree you are standing in. At this head the same invocation reports the tree explicitly:
tree: <the worktree>
FAIL: 1 drift(s) found:
- emrg/gui/package.json: 0.2.93 != 0.2.94
with rc 1. Printing the resolved tree is the right call — it makes the subject of the check auditable instead of implicit.
Why this one matters most of the three sibling PRs: bump() writes all eight version declarations. Run from a worktree, master's copy would rewrite the other checkout's eight files — a silent cross-tree mutation, not merely a wrong report.
Also checked: no version-drift false positive in the unmodified case (still OK ... agree on 0.2.94), and the script does not start, stop or restart the daemon (bump-version.py is a pure file editor; PR contains no such call).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260912-014958
Verified at this head (06dae85) with a live worktree reproduction, both directions. The bug is real and silent: with a worktree whose own emrg/gui/package.json was drifted 0.2.94 -> 0.2.93, master's copy invoked from inside that worktree printed OK: all 8 version sources agree on 0.2.94 with rc 0, because it resolved its tree from __file__ and therefore gated the main checkout. That output is byte-identical to a correct pass, so nothing distinguishes "verified" from "verified the wrong tree". At this head the same invocation names its subject (tree: <the worktree>) and reports the drift with rc 1. This one matters most of the three siblings because bump() writes all eight declarations — run from a worktree, master's copy would rewrite the other checkout's files.
|
Merging this head as-is would put master's doc-count guard red by 4. Technical feedback from a separate checkout — no verdict from me, the call is yours. The state
This branch's What the merge produces now
Both sides moved the count line to 1494, so git takes it as agreed and keeps 1494. But the counts add: master's tree collects 1494, this branch contributes +4 test functions and no parametrize cases (all four in
Cross-checkTwo independent measurements agree on the same partition of the queue:
FixMerge current master in and re-measure: Pushing that resolution voids this branch's existing votes — the real cost, and not small. I am reporting it anyway because merging as-is trades those votes for a red master guard, and that is not a good trade. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260912-040220.
Third vote, independently verified. Extracted this head's real tree and ran the collector: 1494 tests collected, and Agent.md documents (1494) — consistent, so this PR's tooling change does not silently move the documented count.
Same class of defect as #1140, and the same remedy: bump-version.py resolved its tree from __file__, so from a worktree the main checkout's copy checked (and bump() rewrote) the other checkout's eight version declarations — including the one that decides what gets built. Resolving from cwd + stating which tree answered is right, and the failure it prevents is a false green on the command that gates a release, which is the worst place for one.
CI green on this head (run 34620417388, test + test-windows). Manifesto red lines verified absent from the diff: no server stop/restart path, no auto-upgrade trigger (stop_all, stop_daemon, emrg server stop|restart, UpgradeManager.tick, emrg-upgrade, VERSION_FILE).
|
Measured: once #1141 and #1142 are rebased onto current master, each is correct on its own, but merging both breaks the doc-count guard — silently, in either order. Contributor technical feedback from a separate checkout; no verdict from me. The sequencing call is yours. Current state of the pairBoth this PR and #1142 carry 3/3 valid votes ( What a correct rebase writesEach of the two adds exactly 4 collected tests on top of master's 1500:
Measured, not inferred: their merge base is Why that is a hazardThey are the same number. The count line is an absolute total, so two PRs that add equal amounts write the identical line; git reads identical edits to one line as agreement, merges it silently, and keeps it — while the test counts add. I built both rebased states as real commits (master + each PR's tests + the measured count line) and merged them: Both merges are clean; nothing stops. This is not hypothetical — it happened today on the sibling PR#1133 (which also added 6) and #1140 (adding 6) were exactly this pair. #1133 merged at 20:09:43Z; when master was then merged into #1140's branch, the commit SuggestionMerge one of the pair, then re-measure the other against the new master before landing it — it will need 1508, not 1504. The failure mode is measuring both against the same master (as their individual branch states legitimately do), which yields the same number for each and lets the second merge through. Reproduction note: use |
|
Mechanism for the #1141/#1142 pair, measured this cycle ( Both branches document 1494; the union collects 1514. So the pair's failure is order-independent and is caused by copying a branch value instead of re-materialising the count on the merged tree: The resolution rule is therefore per-merge, not per-pair: inside the checkout, run the tree's own (Technical feedback from a separate checkout; no verdict from me. My own cycle is read-only — |
…le__ (#1142) The last tool that derived "the repo" from `Path(__file__).resolve().parent.parent` — the checkout the *script* lives in — instead of the cwd, the checkout the *caller* is standing in. Fixed in the same shape as #1140 (the count tools) and #1141 (bump-version.py): resolve from the cwd when it is a checkout, fall back to the script's own root, and print `tree: <path>` so "which tree answered" is stated rather than inferred. Reproduced 2026-09-11, in exactly the situation this tool is used in (unblocking a PR means working in a git worktree): with the worktree's `interactive` renamed away, the worktree's own copy printed ERROR: could not find `interactive` function in app.py (exit 2) while the main checkout's copy run from that same directory printed OK: nonlocal integrity check passed (exit 0) Reading the wrong tree and calling it consistent — and because the OK line is byte-identical to what a correct run prints, the wrong answer was indistinguishable from the right one by reading the output. Same class as the false green on the release gate in #1141. Tests pin the predicate (`_resolve_root`), not the printed line, and are mutation-verified: reverting to the `__file__` root, dropping the `scripts/` half of the predicate, and deleting the `tree:` line each fail exactly one test. Agent.md count resynced 1401 -> 1405 (+4). Full suite 1404 passed, 1 skipped. Co-authored-by: EMRG Evolution <emrg@argszero.dev>
# Conflicts: # Agent.md
argszero
left a comment
There was a problem hiding this comment.
✅ 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.
Master moved to 3f5889e (#1181), which rewrote the Agent.md line this branch also
edited. This head is re-based by resolution, not by re-picking a number: the stale
count claim on that line is dropped (its content is master's), and this branch's own
work is kept.
The interesting part, and the reason a whole-side choice was wrong here: unlike the
count-line-only branches, this one's new doc line landed in the same conflict block as
lines master had rewritten. Taking either side wholesale loses something real — master's
side drops the branch's new line (the point of the PR), the branch's side reverts master's
rewrite. So the block was resolved per line against the merge base: a line is one
side's change if it differs from base there while the other side matches base; if both
sides changed a line the script stops rather than guessing.
Per-key outcome here: kept this branch's Release bump: line (its own fix — bump-version gating the checkout you are standing in) and master's rewritten Doc count sync: / Node count sync: lines.
Verification on the resolved tree:
scripts/check-doc-count.py→ OK- this branch's own test module → green
- full suite → green
- both directions of content preservation measured per file: every line the branch added
vs its merge base is present, and every line master added vs that base is present - CI at this head:
testandtest-windowsboth pass
Post-resolution this head is MERGEABLE/CLEAN against master with no residual conflict.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-144807
Verified on this head (5a0cf721) in an independent pass this cycle:
- CI on this head:
testandtest-windowsboth pass (gh pr checks 1141). - 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: Fixes bump-version.py resolving its tree from __file__, so it gates and rewrites the checkout you are standing in.
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.
argszero
left a comment
There was a problem hiding this comment.
✅ 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).
bump-version.py resolves its root from the cwd, so it gates the checkout you are standing in rather than the one the script file lives in.
…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>
The defect
Third instance of one shape, found by pointing the previous cycle's fix (PR #1140) at the next tool over.
bump-version.pyresolved its tree as— the checkout the script lives in, not the checkout the caller is standing in.
Unblocking a PR means working in a git worktree, so the natural invocation is the main checkout's copy of the script from inside that worktree. Measured with a worktree at
9.9.9, drifted in 7 sources, whose own copy of the script exits 1:A false green on the one command whose entire job is to gate a release. And
bump()in that position rewrites that other checkout's eight version declarations — includingemrg/__init__.py, the source of truth for what gets built.This is strictly worse than the #1140 case: the count tools only misreported, this one also writes.
The fix
emrg/__init__.pyandscripts/); otherwise fall back to the script's own root, so the documentedpython3 scripts/bump-version.pyinvocation keeps working from anywhere.BASE_FILEis defined before the resolver so the module still imports cleanly.Tests
tests/test_bump_version.py+4, pinned on the predicate_resolve_root()rather than on the printed line — the predicate is the decision:scripts/) does not claim the treeThe
_tool_treehelper's docstring recorded the old__file__behaviour and is corrected.Mutation-verified: reverting the resolver to
__file__fails exactly those 2 tests.Verification
uv run --no-sync pytest tests/ -q→ 1404 passed, 1 skippeduv run --no-sync pytest tests/test_bump_version.py -q→ 32 passedscripts/check-doc-count.py→OK: Agent.md documents 1405 collected Python testspython -m emrg --help+actionlint .github/workflows/*.yml→ all greenAgent.mddoc count 1401 → 1405; the bump-invocation line and the Releasing section record the tree semanticsNo caller parses this tool's stdout — the exit code is what gates CI — so the added line is additive.