f9b1cee476
#849 found get_affected_flows returning ~247k tokens inside a workflow documented as "5 tool calls, 800 tokens total". PR #853 capped that one tool. Measuring all 30 registered tools against a real 5.6k-node graph found the same class of bug in ten more places, several of them on the default path: list_communities 206,858 tokens with DEFAULT arguments get_community 134,781 default / 535,618 with members get_architecture_overview 625,012 in standard mode refactor dead_code 47,312 / suggest 38,246 detect_changes 46,089 for a ONE-file diff get_surprising_connections 1,287,174 at top_n=10**6 get_hub_nodes 555,848 / get_bridge_nodes 317,202 get_review_context 4,720,622 on a whole-repo diff PR #853's own fix was also only half a fix: standard mode carries a full steps list per flow (~980 tokens each), so its 50-flow cap still produced ~49k tokens, and max_flows=0 disabled the limit entirely. Every list-returning tool now follows one contract, the one #853 established: `total` (or a per-list `*_total`) always reports the untruncated count, `truncated` marks the cut, and the summary line says how many of how many are shown. Bounds are validated the way query.py validates max_results - booleans rejected, values below 1 rejected. detail_level="minimal" was added to the analysis and refactor tools, projecting to the same compact field sets their siblings use. Where a count alone cannot bound a response, a shared budget does: get_flow and get_affected_flows spend a step budget, get_review_context and detect_changes spend a source-line budget. Ceilings that depend on payload size depend on detail_level, mirroring query.py capping minimal-mode results at five. Two behaviour changes are deliberate and their #853 tests are updated in this commit: get_affected_flows standard mode now caps at 25 flows (minimal at 500), and max_flows=0 keeps its "no caller limit" meaning while still obeying the ceiling - an escape hatch that returns a quarter of a million tokens is the bug, not a feature. Default behaviour stays backward compatible in shape: an existing caller passing nothing still gets a valid response, just bounded. tests/test_token_budget.py records the measured budget table as reviewable data and pins it three ways: per-tool default and worst-case token ceilings, exact truncated list lengths against the imported ceiling constants, and the ceiling constants themselves. Removing a cap, raising a ceiling, or adding an unbounded field fails it. The fixture graph builds once per module and the whole file runs in ~7s, offline. Reported, not fixed: code_review_graph/tools/query.py is owned elsewhere and four of its tools remain unbounded - get_impact_radius (3.4M tokens; changed_nodes and edges ignore max_results, which is not even exposed on the MCP signature), find_large_functions (737k), traverse_graph (385k) and semantic_search_nodes, whose limit/token_budget are neither validated nor capped. Their default budgets are still asserted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fHfGDiZedoxjpKzanHri3