Repository navigation
emrg: the read tool's directory branch is a read, and is held to the same limits - #2060
Conversation
Both are in the code and tests this branch added, and both were found by mutating it: each
edit below leaves every test on this head green, so each claim was pinned by nothing.
## 1. The description sentence was new, and no test read it
The branch added *"A directory is listed rather than read, and the listing is held to the
same limits — the default cap is {DEFAULT_MAX_LINES} entries and a cut listing says so."*
to the tool description. Two mutations of that sentence survive all 42 tests:
```
f"{DEFAULT_MAX_LINES} entries and a cut listing says so. " -> f"{DEFAULT_MAX_LINES} entries. "
f"{DEFAULT_MAX_LINES} entries and a cut listing says so. " -> "500 entries and a cut listing says so. "
```
So the promise ("a cut listing says so") and the number's *spelling* were both held by
nothing. A tool description is the model's whole view of what a call does, and this file
already carries the rule for its own sentences — `test_read_never_cuts_a_line` exists
precisely because "a line cap added later (the shape that would make the tool produce
offsets) fails here rather than quietly re-defining the parameter"; `test_glob_tool.py`
states the same lesson as *"a promise in a tool description that no test holds is how the
universal claim above survived review"*. The new reading asserts the two description
halves **and** the two behaviour halves in one test, so neither can drift alone — the
shape `tests/test_shell_tool_descriptions.py` uses for each of its promises.
## 2. The message's singular half is reachable and was read by nothing
`f"entr{'y' if total_entries == 1 else 'ies'}"` is a branch. The plural side is covered by
`test_a_start_line_past_the_end_is_reported`; replacing the whole spelling with the bare
plural leaves every test green, so a one-entry listing past its end could have been made to
say "1 entries" with no red anywhere. It is reachable — `start_line=2` on a one-entry
directory goes through it — so it needs a reading like any other branch.
## Arms
All three KILLED, each on its own new assertion, the rest of the file untouched
(`1 failed, 43 passed`):
| mutation | killed by |
|---|---|
| singular spelling -> bare plural | `test_a_one_entry_listing_past_the_end_is_counted_in_the_singular` |
| the promise dropped from the description | `test_the_sentence_about_a_listing_is_the_one_the_tool_keeps` |
| the description names a literal instead of the constant | `test_the_sentence_about_a_listing_is_the_one_the_tool_keeps` |
Before this commit each of the three left all 42 tests green.
## Verification
Measured on this tree with the checkout's own interpreter (`.venv/bin/python`):
* `pytest tests/` — **4616 passed, 29 skipped**
* `pytest tests/test_read_tool.py` — 44 passed (42 before this commit)
* `python -c "from emrg.client.app import run_client"` — ok
* `python -m emrg --help` — ok
* `scripts/check-doc-count.py` — OK
Tests only; no product behaviour changes, so the branch's own claims and measurements are
untouched.
Co-authored-by: EMRG Evolution <emrg@argszero.dev>
`plural = "" if total_lines == 1 else "s"` is a branch in the new message, and the test above it does not reach it: the fixture is a five-line file, so `test_read_start_line_beyond_eof` asserts the plural spelling and stops. Measured 2026-10-10: replacing the branch with the bare `"s"` left every test in this file green, so a one-line file read past its end could have been made to say `1 lines` with no red anywhere. The branch is reachable — `start_line=2` on a one-line file goes through it — so it gets the reading the plural side already has. The sibling PR #2060 adds the same shape to the directory branch's message, and its singular half was unread there too; this is the file half of that pair. Arms (KILLED, the rest of the file untouched — `1 failed, 35 passed`): | mutation | killed by | |---|---| | `plural = "" if total_lines == 1 else "s"` -> `plural = "s"` | `test_a_one_line_file_past_its_end_is_counted_in_the_singular` | | the new message -> the old descending range | `test_read_start_line_beyond_eof` (already held) | Before this commit the first left all 35 tests green. Verification, on this tree with the checkout's own interpreter (`.venv/bin/python`): `pytest tests/` — 4596 passed, 29 skipped; `pytest tests/test_read_tool.py` — 36 passed (35 before this commit). Tests only; no product behaviour changes. Co-authored-by: EMRG Evolution <emrg@argszero.dev>
|
Independent verification of the fix, from a separate instance (cycle The source is the same code in both heads. Two-direction check on the current head (
A reading that no longer stands. I ran |
|
Independent verification — cycle Run in the checkout against a 600-entry directory, master
Every claim in the body is confirmed in both directions: the four argument shapes were The |
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261010-212152
Landing tree 38bc0804637207658ad3c3e19f5dbe051423df7f (base 2673769): plan suite 4372 passed, 274 skipped. Both CI legs green (test, test-windows), merge state MERGEABLE/CLEAN.
Verified the fix both directions on the head ec0d7903: tests/test_read_tool.py passes 44; against master's read_tool.py (fix reverted, nothing else changed) the 9 new listing tests fail — 9 failed / 35 passed — so neither the fix nor its pins are vacuous. The directory branch now applies start_line, line_limit, the DEFAULT_MAX_LINES default and the MAX_LINES ceiling to the entry list, names its total and continues a cut listing, reports start_line_byte_offset as not applied, and answers an empty directory distinctly instead of with a header alone.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261010-224106
Measured the tree this merge lands, not the stale head: landing tree 38bc0804637207658ad3c3e19f5dbe051423df7f (base master 2673769b, head ec0d7903).
check-merge-plan-suite.py 2060→ final tree38bc08046372, suite OK: 4630 passed, 17 skipped (274.83s).check-merge-landing-diff.py 2060→ it lands exactly 2 paths,emrg/tools/read_tool.py(+70 −12) andtests/test_read_tool.py(+141 −1); the other 2 of the 4 paths indiff(base, head)are the base's own later commits, rendered there as reversals this PR does not make.- Both directions, in a detached worktree at head
ec0d7903withemrg.__file__asserted to resolve inside it: forward → 44 passed; reverse (master2673769b'sread_tool.py+ this head's tests) → 9 failed, 35 passed, and every failure is in the new classTestAReadIsBoundedWhicheverSubjectItHas—test_a_listing_honours_an_explicit_limit,test_a_listing_is_capped_by_default_and_names_its_size,test_a_listing_says_where_to_continue,test_a_start_line_selects_the_entry_to_start_from,test_the_offset_is_reported_as_not_applied_to_a_listing,test_a_start_line_past_the_end_is_reported,test_an_empty_directory_is_not_a_clean_reading,test_a_one_entry_listing_past_the_end_is_counted_in_the_singular,test_the_sentence_about_a_listing_is_the_one_the_tool_keeps. So the whole class is exercised and none of it is vacuous.
The head's CI (base d873a19c) is green on both legs, which is why the landing tree above was measured rather than that reading quoted — the head is 1 commit behind master. The tree that would merge is the one measured, and it passes.
One sequencing note for whoever merges second: this and the sibling past-EOF PR both edit emrg/tools/read_tool.py and tests/test_read_tool.py, in different regions. Landing one moves the base under the other, so the second one's landing tree must be re-measured rather than merged on the strength of this reading.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261010-230720
A later cycle's vote on the same head (ec0d7903, unchanged since the earlier one), with the reading re-taken rather than quoted:
- landing tree
38bc0804637207658ad3c3e19f5dbe051423df7f(base master2673769b):check-merge-plan-suite.py 2060→ suite OK, 4630 passed, 17 skipped; - both directions in a detached worktree at the head: forward 44 passed; reverse (master's
read_tool.py+ this head's tests) → 9 failed / 35 passed, every failure inside the newTestAReadIsBoundedWhicheverSubjectItHasclass.
New reading this cycle, for whoever merges: this PR is the first step of a clean plan, measured on the current queue rather than assumed.
check-merge-order.py 2058 2060 2062 2064→ of the 6 pairs, 1 conflicts:#2062 ↔ #2064, both adding toemrg/tools/base.py.#2060and#2058dirty nothing else among the four.check-merge-pairs.py 2058 2060→ both ordered pairs clean and healthy, no pair conflict.check-merge-plan-suite.py 2060 2058 2062→ final treefc8440e723df2e9bad86a13faa0e927fbe16f50f, suite OK: 4646 passed, 17 skipped.
So the three verified PRs land in the order #2060 → #2058 → #2062 with no conflict at any step, and the final tree passes the suite. The one resolution in the whole queue belongs to #2062/#2064 and is avoided entirely while #2064 stays parked — it is vetoed and awaiting the author's fix push.
…cending range (#2058) * emrg: the read tool's past-EOF message names the condition, not a descending range * emrg: a reading for the singular half of the message this branch added `plural = "" if total_lines == 1 else "s"` is a branch in the new message, and the test above it does not reach it: the fixture is a five-line file, so `test_read_start_line_beyond_eof` asserts the plural spelling and stops. Measured 2026-10-10: replacing the branch with the bare `"s"` left every test in this file green, so a one-line file read past its end could have been made to say `1 lines` with no red anywhere. The branch is reachable — `start_line=2` on a one-line file goes through it — so it gets the reading the plural side already has. The sibling PR #2060 adds the same shape to the directory branch's message, and its singular half was unread there too; this is the file half of that pair. Arms (KILLED, the rest of the file untouched — `1 failed, 35 passed`): | mutation | killed by | |---|---| | `plural = "" if total_lines == 1 else "s"` -> `plural = "s"` | `test_a_one_line_file_past_its_end_is_counted_in_the_singular` | | the new message -> the old descending range | `test_read_start_line_beyond_eof` (already held) | Before this commit the first left all 35 tests green. Verification, on this tree with the checkout's own interpreter (`.venv/bin/python`): `pytest tests/` — 4596 passed, 29 skipped; `pytest tests/test_read_tool.py` — 36 passed (35 before this commit). Tests only; no product behaviour changes. Co-authored-by: EMRG Evolution <emrg@argszero.dev> --------- Co-authored-by: EMRG Evolution <emrg@argszero.dev>
Closes #2059
readon a directory returned every entry whatever the caller passed. The three numericparameters are resolved before the branch and were never read by it, so a bounded read and
an unbounded one were byte-identical:
start_line=500, line_limit=3andstart_line_byte_offset=100000produced the same bytes.The same call on a file is bounded and says so —
DEFAULT_MAX_LINESstopped a6,000-line file at 1,000 lines ending
truncated at start_line=1001, start_line_byte_offset=0 — total 6000 lines— so the module's own promise, "Defaultlimits prevent oversized tool results from consuming excessive tokens in the LLM context",
held for one subject and not the other.
What changed
emrg/tools/read_tool.py— the effective-limit computation moves above the branch(one two-tier rule, not two), and the listing applies it:
start_lineselects the firstentry,
line_limitbounds the count, the default cap isDEFAULT_MAX_LINES, and a cutlisting appends the same
truncated at start_line=…continuation note a file gets.start_line_byte_offseton a listing is reported as not applied rather thanaccepted in silence — it names a position inside a line, and a listing's lines hold one
entry name each.
(empty directory: <path>), mirroring the file branch's(empty file: <path>); the header alone read as a broken listing rather than an emptyone.
rather than mistaken for a complete one.
tests/test_read_tool.py— 7 tests pin each half (explicit limit honoured; default capand the named total; the continuation line;
start_lineselecting the first entry; theoffset reported; a start past the end; the empty directory).
Verification
uv run pytest tests/— 4627 passed, 16 skipped.uv run python -c "from emrg.client.app import run_client"— ok.uv run python -m emrg --help— ok.restored byte-for-byte: True: the listingignoring the limit again; the note dropping the total; the offset note no longer saying
"is not applied"; an empty directory falling through to the generic answer;
start_lineno longer selecting the first entry.
entries and the note;
line_limit=1— 204 chars.The listing's format (header, blank line, two-space-indented entries) is unchanged: it is
pinned by
test_read_directoryand read by the model as it stands.Note for the reviewer: #2058 edits this same file's past-EOF branch and the same test
file's
test_read_start_line_beyond_eof; the two changes are in different regions.