phase: 131_read_line_ranges
Build and Push Containers / build-and-push-app (push) Successful in 2m30s
Build and Push Containers / build-and-push-db (push) Successful in 11s

All 44 progress lines are pure dots, final line at 100%, exit 0. Everything is verified. Final report:

**Phase 131 — final verification pass (all 4 tasks already complete in working tree)**

- **Verified implemented:** `read(offset, limit)` schema + teaching descriptions; rewritten truncation notice; full 6-row dedupe matrix + range validation + `lines: a-b of N` header + slice cap in `_execute_tool`; wire-level `offset`/`limit` on `ChatToolEvent` (absent keys dropped → non-range frames byte-identical); mock-LLM `read the lines` flow; handbook repro (wording + line numbers derived from the real PDF at test time)
- **Tests:** `uv run pytest --no-cov` → 3097 passed, exit 0; `uv run pytest --cov=app` → **99% on app/** (>90%); `uv run pytest tests/e2e/test_read_line_ranges.py -v --no-cov` → 2/2 in isolation (47.6 s); `tests/integration/test_read_line_ranges_handbook.py` → 2/2 (3.5 s); 10-file targeted E2E batch pinning pre-phase surfaces (grep/ls/plain-read/ALREADY_IN_CONTEXT/truncation) → 31/31; `ruff check .` clean; `pyright` 0 errors
- **Completion criteria:** all 5 green — (1) exact range lines + `lines:` header, plain reads byte-identical (existing pins pass unchanged) ✅ (2) truncated-then-range succeeds end-to-end on the real handbook; other re-reads get teaching refusals ✅ (3) find-traps answered from real PDF text, no hardcoded wording ✅ (4) coverage + isolated E2E ✅ (5) no behavior change in completed phases ✅
- **Docs:** README + AGENTS reviewed in full. Fixed 3 inconsistencies: README missing `BOR_HISTORY_MAX_TURNS`/`BOR_HISTORY_MAX_CHARS` rows (pre-existing phase-74 gap — env-tunable, unit-pinned, undocumented); `.env.example` missing the same two vars (added); `.env.example` stale `BOR_READ_MAX_CHARS` comment (still said "grep-pointer notice (phase 95)" — updated to the phase-131 workflow notice). AGENTS.md verified consistent — no changes needed. All 67 `.env.example` vars documented, all defaults match `app/config.py`, pages/auth tables match live routes
- **Note:** PLAN.md §7.1 still states the retired 46rem chat column (superseded by phase 100, which AGENTS.md rule 5 records correctly) — PLAN.md is off-limits for this pass, flagged only
- **Next pending phase:** `132_image_upload_feedback`
This commit is contained in:
2026-09-28 11:49:55 -04:00
parent c88c65641a
commit 101156bada
39 changed files with 3521 additions and 108 deletions
+42 -1
View File
@@ -303,6 +303,20 @@ def get_llm() -> LLMClient:
return _llm
def _whole_line_number(value: Any) -> bool:
"""A ``read`` range argument's whole-line-number guard (phase 131,
task 04).
The same validation the agent's ``_read_range_args`` applies
server-side (``app/rag/agent.py``): an ``int`` (bools excluded —
the house guard) and ``>= 1``. A value that fails is a REFUSED
call (the ``BAD_RANGE_ARGS`` teaching), and its ``tool`` frame
carries no range — the refusal surfaces as the round's result, not
as the call's arguments.
"""
return isinstance(value, int) and not isinstance(value, bool) and value >= 1
def sse_event(payload: dict[str, Any]) -> str:
"""Serialize one SSE frame: ``data: <json>\\n\\n`` (PLAN §4)."""
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
@@ -905,8 +919,35 @@ async def chat(
"pattern" if piece.name == "grep" else "path"
)
argument = argument if isinstance(argument, str) else None
# Phase 131 (task 04): a range ``read``'s line
# range rides the SAME frame — the additive
# ``offset`` / ``limit`` fields (the wire's
# assertion surface for the owner's
# grep → read(offset/limit) dance). Only a
# well-formed range (``_whole_line_number``,
# the agent's own guard) lands on the frame;
# absent → omitted on serialization, so every
# non-range frame stays byte-identical to
# pre-phase (the ChatToolEvent serializer).
offset = (
piece.arguments.get("offset")
if piece.name == "read"
else None
)
offset = offset if _whole_line_number(offset) else None
limit = (
piece.arguments.get("limit")
if piece.name == "read"
else None
)
limit = limit if _whole_line_number(limit) else None
yield sse_event(
ChatToolEvent(name=piece.name, argument=argument).model_dump()
ChatToolEvent(
name=piece.name,
argument=argument,
offset=offset,
limit=limit,
).model_dump()
)
continue
if isinstance(piece, RetryPiece):