Phase 5 — Hardening: size gates, retry, structured logging, quote-based resolution #14
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 5 hardening work. Six self-contained items implemented in the same branch via TDD.
Checklist
WUMING_MAX_DIFF_LINESenv var; post a plain comment and exit cleanly when exceededtruncate_large_hunks()indiff.py; replace oversized hunks with a single stub line before sending to agentsestimated_tokens = len(diff_text) // 4; emit a structured log event per agent call for observabilitywith_retry()async helper inretry.py; applied to allForgejoClientand backend HTTP calls; no new runtime depssrc/wuming/log.py;JSONFormatter(stdlib); replace allprint()calls; Woodpecker-friendlyquotefield onReviewComment;find_line_by_quote()indiff.py; agents supply verbatim line text;_resolve_positionstries quote-search before trusting the LLM line numberRationale
print()output is not machine-readableTask 1: PR size gate
Motivation: A very large PR (thousands of changed lines) exceeds the LLM context window, causing silent truncation or API errors. Better to refuse early with a clear message than to produce incomplete or garbled reviews.
New env var:
WUMING_MAX_DIFF_LINES(integer, default0= disabled). When the raw diff line count exceeds the configured value, WuMing posts a single plain PR comment explaining the PR is too large to review automatically, then returns without posting any inline review.Key files:
src/wuming/config.py— addmax_diff_lines: intfield; parseWUMING_MAX_DIFF_LINESsrc/wuming/forgejo.py— addpost_comment(body: str)method (Forgejo issues comments endpoint:POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments); distinct frompost_review()src/wuming/main.py— afterraw_diff = await forgejo.get_diff(), checklen(raw_diff.splitlines()) > config.max_diff_lines; if triggered, callawait forgejo.post_comment(...)and returnApproach: Add the gate as the very first check after fetching the diff, before any parsing or agent dispatch. The plain comment (not a code review) uses the issues API so it always appears even when no diff positions exist.
Task 2: Hunk size cap
Motivation: A single oversized hunk (e.g. 2000-line auto-generated file that slipped through skip-path filters) sends an enormous block of text to the LLM, consuming most or all of the available context and leaving no room for the actual review prompt. Truncating it with a clear stub preserves the diff structure while keeping the payload manageable.
Key file:
src/wuming/diff.pyNew function:
Any
Hunkwhoselinestuple exceedsmax_linesis replaced with a single+-prefixed stub line:The stub starts with
+sobuild_position_mapand_format_diff_texttreat it as a valid added line with a new-file line number. The function is pure — it returns a new list without mutating the input.Wire-up: Called in
src/wuming/main.pyimmediately afterfilter_diff(), before diffs are handed to the router.Task 3: Approximate token budget per agent call
Motivation: There is currently no visibility into how much of an LLM's context window each agent call consumes. Logging the estimate gives operators early warning when a diff is approaching context limits, without requiring a paid token-counting API call.
Formula:
estimated_tokens = len(diff_text) // 4(chars / 4 is the standard rough approximation for English-like code text).Key file:
src/wuming/agents/base.pyChange: In
BaseAgent.review(), afterdiff_text = _format_diff_text(matching), emit:This is a purely observational addition — no capping, no early exit. The log event uses the structured JSON logger added in Task 5 so it is machine-readable in Woodpecker.
Task 4: HTTP retry with exponential backoff
Motivation: Transient network failures, 503s from overloaded LLM APIs, and 429 rate-limit responses currently crash the entire review run. A simple retry loop with exponential backoff handles the common case without adding runtime dependencies.
New file:
src/wuming/retry.pyNew function:
Retry policy:
httpx2.TransportError(network-level)httpx2.HTTPStatusErrorwithstatus >= 500(server error)httpx2.HTTPStatusErrorwithstatus == 429(rate-limited)k:base_delay * 2**(k-1)seconds viaasyncio.sleepmax_attemptsexhausted, re-raises the last exceptionApplied to:
src/wuming/forgejo.py(get_diff,get_pr_head_sha,list_review_comment_bodies,post_review,post_comment)complete()method in each ofdeepseek.py,anthropic.py,ollama.pyNo new runtime dependencies — uses only
asyncioandhttpx2(already present).Task 5: Structured JSON logging
Motivation: All current log/status output uses
print(), which is plain text and invisible to Woodpecker's structured log parsing. Replacing it with newline-delimited JSON enables log aggregation, filtering by level, and per-agent metrics — without adding a third-party logging library.New file:
src/wuming/log.pyKey classes/functions:
Stdlib only:
datetime,json,logging.Migration: Replace every
print()andprint(..., file=sys.stderr)call inmain.py,router.py, andagents/base.pywithlogger.info/warning/error. Callsetup_logging()at the top of_main(). Each module declares a module-levellogger = logging.getLogger("wuming.<module>").Task 6: Quote-based line resolution
Motivation: LLMs frequently hallucinate line numbers, causing valid review comments to be silently dropped during position resolution. Asking the LLM to also quote the exact verbatim text of the line it is commenting on allows us to locate the correct diff position by text search, falling back to the numeric line only when the quote is absent.
Changes:
src/wuming/comments.pyquote: str | None = Noneas the last field onReviewCommentsrc/wuming/diff.pyStrips the leading
+/prefix from each non-deleted hunk line, compares toquote(exact, case-sensitive), and returns the new-file line number of the first match. Deleted (-) lines are skipped. ReturnsNoneif not found.src/wuming/agents/base.py_parse_response(): extractitem.get("quote")(optional, defaultNone)review(): module-level logger emits token budget (Task 3)Agent system prompts (
code.py,config_agent.py,docs.py,shell.py)"quote"field to the expected JSON schema: "the exact verbatim text of the line, without the leading +/- prefix; omit or set to null if uncertain"src/wuming/main.py—_resolve_positions()file_diffs: list[FileDiff]parametercomment.quoteis set: callfind_line_by_quote()first; use the returned line number for theposition_mapslookup; fall back tocomment.lineif the quote search returnsNone