Add a pre-call USD belt on top of the post-charge token/round meter (§8),
so no future live run can loop past its run budget. Belt-and-braces above
the SDK's per-call max_budget_usd cap.
- budget.py: optional run-total `max_cost_usd` on BudgetMeter (fail-fast on
non-positive, §10) + `guard_before_call(spent_usd)` raising the same
structured stop event (BudgetKind widened with "cost_usd"; limit/observed
→ float). Reaching the cap exactly does not stop; crossing it does
(mirrors the token cap).
- loop.py: `_guarded_complete` helper reads the client's accumulated
total_cost_usd (0.0 for scripted clients) and guards BEFORE every
client.complete; all three call sites routed through it — one detach point.
- sdk_client.py: total_cost_usd already exposed/accumulated — untouched.
- tests/test_budget.py: meter-level cap tests + load-bearing loop-wiring
test (counting client; detach the guard → unguarded loop runs to the round
cap → kind "rounds" not "cost_usd" → red).
457→462 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
22 src files), run_s10.py/runs/ byte-untouched. README test-count sync ×2 +
budget.py belt note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
IR schema now refuses non-finite numbers (allow_inf_nan=False on quantity/
unit_cost/claimed_saving_nok) and non-finite or negative assumption-band
endpoints; json.loads accepts the bare Infinity literal, so the bundle seam
is tested directly. ModelMapContract rejects empty-string model ids
(min_length=1). check_turn_safety_net documented as a deliberately
unreachable belt under the range-bound debate loop.
18 new tests; detach-proven (re-allow inf/nan -> 5 red, drop min_length ->
2 red). Full gate: 365 passed, ruff/format/mypy clean; golden untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Transkript-analyse av den stoppede live-kjøringen (10 kall, 162 250 tokens,
$0.331506): konfig-lekkasjen (setting_sources=None laster ALLE filsystem-
settings) injiserte operatørens Claude-konfig i hvert kall — ~10-15k uncachede
tokens, en påtvunget bekreftelses-preamble som gjorde ren-JSON-svar umulige,
og en checker kapret av lekkede instrukser (debatt konvergerte aldri).
- persist_stop_artifacts: stopp-event verbatim + usage/kost persisteres ALLTID
ved BudgetExceeded (delt usage-shape med fullført-run-stien)
- build_call_options: setting_sources=[] (SDK isolation mode, verifisert mot
installert 0.2.110-kilde), system_prompt=None → tom system-prompt; detach-
bevis via monkeypatchet query
- _generation_prompt: krever ONLY the raw JSON object (fence-innpakning ga
4 fullpris parse-retries)
187/187 uten nøkkel · ruff + mypy --strict rene · tre detach-bevis RØDE → grønn
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
Spec §3 steps 2–5 + §8, TDD-ed offline (scripted, honesty-marked stand-in):
- budget.py: BudgetMeter over TerminationContract — provider-reported usage
only (missing usage fails closed), structured BudgetExceeded stop event.
- loop.py: ModelClient protocol; blind parse-retry generation (never silent
repair); round-capped debate with turn safety net and mandated VERDICT
line; opt-in-reject checker gate (explicit REJECT overrides a validated
outcome, validator rejection stands); most-recent-reason-verbatim informed
refinement under max_attempts; validator_decision stamped BEFORE override,
checker_decision as its own result field (§9, never conflated).
- 45 new tests (121 total, no API key); four detach proofs run RED and
reverted green: checker override, informed block, surfaced checker output,
stamp-before-override.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS