feat: shekel run — non-invasive CLI budget enforcement (v0.2.9)#23
feat: shekel run — non-invasive CLI budget enforcement (v0.2.9)#23
Conversation
Adds `shekel run agent.py --budget 5` as a drop-in wrapper for any Python agent script: zero code changes required, exit 1 on budget exceeded (CI-friendly). New features: - `shekel run`: wraps scripts via runpy in-process so monkey-patches are active - `--budget / AGENT_BUDGET_USD`: USD cap with Docker/CI env-var support - `--warn-at`, `--max-llm-calls`, `--max-tool-calls`: full Budget param parity - `--output json`: machine-readable spend summary for log pipelines - `--warn-only`: log warning but never exit 1 (soft guardrail) - `--dry-run`: track costs only, implies --warn-only - `--budget-file shekel.toml`: operator-supplied TOML config - `Budget(warn_only=True)`: new parameter — suppresses raises, fires warn callback - `.github/actions/enforce/action.yml`: GitHub Actions composite action - `docs/docker.md`: Docker entrypoint patterns and shell script examples Tests: 85 new tests (TDD), 100% coverage on _cli.py, _run_utils.py, _run_config.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| if by_model: | ||
| top_model = max( | ||
| by_model.items(), | ||
| key=lambda kv: kv[1]["calls"], # type: ignore[index] |
Check warning
Code scanning / CodeQL
'break' or 'return' statement in finally Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, to avoid swallowing exceptions with a finally plus an unconditional exit/return, ensure that process termination (or returns) occur inside the try block, and that the finally block is restricted to cleanup that should run regardless of success or failure. If additional reporting or summary output is needed, it should either be done outside the finally after confirming no exception occurred, or wrapped in its own try/except so that unexpected issues don’t silently override earlier errors.
For this specific snippet in shekel/_cli.py, the least disruptive fix is:
- Move
sys.exit(script_exit_code)from after thefinallyinto thetryblock (after the core script execution and any logic that setsscript_exit_code). - Wrap the content of the current
finallyblock in an innertry/exceptto ensure that any unexpected exception in the reporting code does not mask the original exception from thetry. In theexceptof that inner block, re-raise so that the original behavior (propagating an error) is preserved when something goes wrong in summary generation. - Keep
sys.argv = original_argvas unconditional cleanup in the outerfinallyso it always runs, even if reporting fails. The rest of the status/JSON/text printing logic should be in the innertryso exceptions there can propagate properly.
Concretely, within the region around lines 270–305:
- Change the
finally:block so that it only guarantees restoration ofsys.argv, and does the status/output logic inside an innertry/exceptthat re-raises. - Remove the trailing
sys.exit(script_exit_code)outside thetry/finally, and add asys.exit(script_exit_code)at the end of thetryblock where the script’s main logic finishes. Because the snippet you provided only shows the end of the function, we will adjust just this bottom region: we will move thesys.exit(script_exit_code)into thetryby replacing the end of thefinally+ trailingsys.exitwith afinallythat setssys.argvand an explicit innertry/exceptfor the reporting code.
No new imports or helper methods are needed; we only restructure the control flow in the shown section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids environment-dependent unused-ignore errors: CI has tomli installed (no import-not-found), local dev does not. The [[tool.mypy.overrides]] for tomli handles both cases without needing a fragile inline comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
shekel run agent.py --budget 5as a zero-code-change budget enforcer for any Python agent scriptBudget(warn_only=True)parameter suppresses raises and fires warn callback instead.github/actions/enforce/action.yml) for GHA pipelinesdocs/docker.md) with entrypoint patterns, env-var control, JSON loggingKey flags
--budget N/AGENT_BUDGET_USD=N--warn-at F0.8)--max-llm-calls N--max-tool-calls N--output json--warn-only--dry-run--warn-only--budget-file PATHshekel.tomlTest plan
_cli.py,_run_utils.py,_run_config.pyshekel runon no-op script < 100 ms (benchmark median ~230 µs)🤖 Generated with Claude Code