chore: release v0.8.0 — ranged file reads and an honest truncation marker (#31) - #32
Conversation
…o continue (#31) github_read_file and github_read_pr_file take start_line (1-based, default 1) and end_line (inclusive, 0 = to the end), sliced locally from the whole file the contents API returns (still --method GET with the ref as a query param). The 20000-char cap now cuts at a LINE boundary, never mid-line, and ends with one footer line naming the file's total line count and the next start_line: … [truncated: showed lines 1-487 of 870 (20000-char cap); continue with start_line=488] A range returned in full ends with "… [showed lines A-B of N; continue with start_line=B+1]" or "…; end of file]". A single line longer than the whole cap is the one mid-line cut and says so ("truncated mid-line: line A of N alone is L chars …"). The default read of a file that fits is byte-identical, as before. Bad ranges and a start_line past EOF are Error strings, checked before any gh call where possible. Tool descriptions state the cap, the marker, and to page with start_line/end_line instead of guessing or reading the base version. Release v0.8.0 (new tool parameters): manifest, pyproject, uv.lock project line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSRkzcPyaDrKSY7geFuGr3
|
Live check against real GitHub (branch head Following the continuation markers until |
There was a problem hiding this comment.
QA panel review — PASS
code-review-structural · head 6563b6132047 · formal
The PR adds line-range reads with an honest truncation marker to the GitHub read tools; all four LLM finders came back clean on the diff itself. The single surviving finding is a pre-existing nit in api.py (the diff does not touch that file) where fetch_issues inherits a state validator that accepts "merged" — a PR-only state — and forwards it to gh issue list rather than rejecting it with the documented message. Verification confirmed the finding at head SHA with no severity change. No panel disagreement; no coverage gaps.
Findings
| Severity | Location | Finding | Verified | |
|---|---|---|---|---|
| ⚪ | nit | api.py:40 |
fetch_issues accepts state="merged" via the shared _norm_state validator, which is valid for PRs but not for issues; the value passes validation and is forward… | confirmed |
findings JSON (machine-readable)
[
{
"file": "api.py",
"line": 40,
"severity": "nit",
"category": "api-contract",
"claim": "fetch_issues accepts state=\"merged\" via the shared _norm_state validator, which is valid for PRs but not for issues; the value passes validation and is forwarded to `gh issue list --state merged`, producing a generic gh stderr error instead of the documented validation message. (protopatch)",
"evidence": "_norm_state (line 40-43) accepts (\"open\", \"closed\", \"merged\", \"all\"). fetch_issues (line 52-56) uses it as its sole state validator, so state=\"merged\" passes the check and reaches `gh issue list --state merged` \u2014 a command gh does not support (issues have no \"merged\" state). The resulting error is a generic gh stderr message classified by check_gh_error, not the clear \"state must be one of \u2026\" message the docstring promises. Fix: replace the _norm_state call in fetch_issues with an inline check against (\"open\", \"closed\", \"all\"), or parameterize _norm_state to accept the valid set. fetch_prs can keep using the four-state set.",
"source": "protopatch",
"verdict": "confirmed",
"note": "Re-read api.py at head SHA: _norm_state returns s if s in (\"open\",\"closed\",\"merged\",\"all\"); fetch_issues calls it and only errors when it returns None, so state=\"merged\" passes and reaches `gh issue list --state merged`. Pre-existing (PR diff does not touch api.py); nit severity is appropriate."
}
]1 finding(s) excluded from the verdict by in-diff confinement (file not among this PR's changed paths):
api.py(nit) — fetch_issues accepts state="merged" via the shared _norm_state validator, which is valid for PRs but not for issues; the value passes validation and is forwarde
Why
github_read_fileandgithub_read_pr_filecut every read at 20,000 chars (out[:20000] + "\n… (truncated at 20000 chars)") and took no offset, so the rest of a large file couldn't be read through them. On the QA review panel this became the main limit on large PRs (#31):find_removed_behaviorran 974 s and ended mid-investigation ("The PR head file is truncated at 20000 chars. Let me read the base version to see the unchanged tail…"). It never produced a findings array or aFINDER_STATUSline.graph/snapshot_op.py(870 lines) andgraph/snapshot_import.pyare both over the cap.graph/snapshot_op.pyis truncated mid-statement at line 539 → SyntaxError". The file is intact and parses. The old cut landed mid-line, and the marker said nothing about how much was left or how to continue.What changed
Ranged reads. Both tools take
start_line: int = 1andend_line: int = 0: 1-based and inclusive, with 0 meaning "to the end of the file" (still subject to the cap). The contents API always returns the whole file, so the range is sliced locally. The request is still--method GETwith-f ref=…(#29), and the range never reaches GitHub.One footer line that says how to continue (
read_tools._render_file). It always names the file's total line count:… [truncated: showed lines 1-487 of 870 (20000-char cap); continue with start_line=488]…; continue with start_line=488, end_line=800]… [showed lines 100-200 of 870; continue with start_line=201]… [showed lines 541-870 of 870; end of file]… [truncated mid-line: line 1 of 3 alone is 25000 chars (20000-char cap), showed its first 20000; continue with start_line=2]The footer is always the last line, on its own line. The shown text, not counting the newline before the footer, stays within the cap. That is the same rule as the unchanged whole-file read, so a file of exactly 20000 chars is still returned untouched.
Small files are byte-identical. The default read (no range) of a file that fits comes back exactly as before, with no header and no footer. An explicit range always gets a footer.
Bad input is an
Error:string, never a raise.start_line < 1,end_line < 0, andend_line < start_lineare rejected before anyghcall. Astart_linepast EOF says so and gives the line count, e.g.Error: start_line=871 is past the end of big.py — it has 870 line(s).github_read_pr_filenames the PR head in that message and puts no content header over it.Tool descriptions (what the model sees) state the 20000-char cap, that a cut lands on a line boundary with a marker (with the marker shown), and to page with
start_line/end_linerather than guessing or reading the base version. A test pins the docstring's plain-literal20000to_MAX_FILE_CHARS. The paging contract is documented in PROTO.md §5 and summarized in the README.The finder-prompt note from the issue's item 3 is deliberately not in this PR. The reviewer runs an operator override of
code-review-structural.yaml, so the paging guidance lives in these tool descriptions, which every lane already sees.Tests
tests/test_read_tools.py, with +35 tests against an 870-line synthetic file where 487 whole lines fit the cap:"", one char, CRLF, blank lines, unicode, exactly 20000 chars, a 487-line file. Checked through both tools, andread_pr_filekeeps its header.end_linepast EOF (clamps),end_line=0. Checked through both tools.end_line.start_linepast EOF (both tools) and a ranged read of an empty file.run_ghnever called.start_line/end_linewith defaults 1/0 in the args schema.-f ref=made gh send POST, a 404 #29) extended: a ranged read on both tools is still a GET carrying the ref, and no range parameter appears in theghargv.test_long_content_is_truncatednow asserts the new mid-line marker.Teeth check: I reverted the body to the pre-#31 behavior, a raw char cut (
"".join(lines[a-1:last])[:cap]), and 5 tests failed: the line-boundary marker test, the at-vs-over-the-cap test, the paging test, the cut-inside-a-range test, and the randomized invariants.Gates: these match CI.
uv run --no-project --python 3.12 --with-requirements requirements-dev.txt pytest -q --basetemp=…: 313 passed (278 on main before this change).uvx ruff@0.15.10 check .: clean.uvx ruff@0.15.10 format --check .: clean.Release
This is a v0.8.0 minor release, because the tools gain new parameters.
protoagent.plugin.yamlandpyproject.tomlare bumped together (tests/test_version.py). Inuv.lockonly the project's own version line was edited; I did not re-lock.Fixes #31
🤖 Generated with Claude Code
https://claude.ai/code/session_01LSRkzcPyaDrKSY7geFuGr3