cli-agent-runner 0.2.12__tar.gz → 0.2.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.vulture-whitelist.py +2 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/CHANGELOG.md +18 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/PKG-INFO +1 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/_emit.py +8 -4
- cli_agent_runner-0.2.13/agent_runner/_resolve.py +120 -0
- cli_agent_runner-0.2.13/agent_runner/_serve_policy.py +189 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/_throttle.py +117 -45
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/agent_runtime.py +21 -5
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/api.py +162 -87
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/api_types.py +7 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/_constants.py +3 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/claude_rate_limit.py +2 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/gemini.py +53 -20
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/pi.py +3 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/__init__.py +12 -2
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/common.py +29 -5
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/events_cmd.py +7 -21
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/install_cmd.py +15 -5
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/migrate_cmd.py +3 -1
- cli_agent_runner-0.2.13/agent_runner/cli/round_cmd.py +86 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/serve_cmd.py +27 -5
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/upgrade_cmd.py +2 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/clock.py +20 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/config.py +143 -29
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/context_store.py +21 -4
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/events.py +50 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/hooks.py +8 -3
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/http_progress.py +12 -4
- cli_agent_runner-0.2.13/agent_runner/lifecycle.py +192 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/migrations.py +254 -26
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/monitor.py +83 -66
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/runner.py +48 -10
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/scaffold.py +2 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/service_unit.py +48 -19
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/vcs_state.py +6 -2
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/architecture.md +1 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/commands.md +13 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/configuration.md +4 -4
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/migrations/0.2.md +55 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/plugins.md +6 -4
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/quickstart.md +7 -8
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/runbook.md +12 -8
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/pyproject.toml +2 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/_test_helpers.py +20 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/contract/test_public_api_surface.py +6 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/e2e/conftest.py +1 -1
- cli_agent_runner-0.2.13/tests/integration/test_auto_stop_e2e.py +188 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_plugin_real_flow.py +1 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_serve_loop.py +1 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/_event_scan.py +64 -16
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_architecture.py +4 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_authored_facts_declared.py +3 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_catalogs.py +3 -0
- cli_agent_runner-0.2.13/tests/invariants/test_config_error_consistency.py +135 -0
- cli_agent_runner-0.2.13/tests/invariants/test_dataclass_fields_have_producers.py +210 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_cli_claims.py +13 -3
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_docs_index_complete.py +3 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_event_kind_registry.py +3 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_event_kinds_ssot.py +3 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_hook_contract_docs.py +3 -1
- cli_agent_runner-0.2.13/tests/invariants/test_migrations_registry_ssot.py +22 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_module_boundaries.py +1 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_module_sizes.py +3 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_no_ai_signatures.py +9 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +3 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_no_raw_time.py +28 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_no_vacuous_invariants.py +16 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_repo_constants_patched_in_tests.py +3 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_set_diff_for_auto_tool_classification.py +3 -1
- cli_agent_runner-0.2.13/tests/invariants/test_timeout_budget_invariant.py +47 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/literate/parser.py +6 -3
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/literate/test_parser.py +8 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/literate/test_quickstart.py +10 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime.py +27 -11
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_grace.py +48 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_reap.py +50 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_api_install.py +75 -0
- cli_agent_runner-0.2.13/tests/unit/test_api_service.py +583 -0
- cli_agent_runner-0.2.13/tests/unit/test_auto_stop_gating.py +157 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_cli.py +1 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_cli_main_boundary.py +16 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config.py +187 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_strictness.py +3 -3
- cli_agent_runner-0.2.13/tests/unit/test_control_char_config.py +129 -0
- cli_agent_runner-0.2.13/tests/unit/test_detect_hung_latch.py +69 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_events_cmd.py +47 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_events_cmd_tail.py +38 -1
- cli_agent_runner-0.2.13/tests/unit/test_events_jsonl_decode.py +126 -0
- cli_agent_runner-0.2.13/tests/unit/test_exit_classifier.py +89 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_gemini_plugin.py +44 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_migrate_cmd.py +89 -1
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_migrations.py +163 -7
- cli_agent_runner-0.2.13/tests/unit/test_monitor_detect_rate_limit.py +87 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_remote.py +59 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_outer_round_ceiling.py +13 -9
- cli_agent_runner-0.2.13/tests/unit/test_read_json_hardening.py +85 -0
- cli_agent_runner-0.2.13/tests/unit/test_resolve.py +125 -0
- cli_agent_runner-0.2.13/tests/unit/test_round_cmd.py +124 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_runner.py +61 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_runner_throttle.py +90 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_config_broken.py +30 -2
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_startup_hooks.py +7 -4
- cli_agent_runner-0.2.13/tests/unit/test_service_mode.py +103 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_service_unit.py +79 -15
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_startup_battery_exit.py +40 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_upgrade_migrate.py +26 -0
- cli_agent_runner-0.2.12/agent_runner/_serve_policy.py +0 -89
- cli_agent_runner-0.2.12/agent_runner/cli/round_cmd.py +0 -54
- cli_agent_runner-0.2.12/agent_runner/lifecycle.py +0 -108
- cli_agent_runner-0.2.12/tests/invariants/test_config_error_consistency.py +0 -74
- cli_agent_runner-0.2.12/tests/unit/test_api_service.py +0 -314
- cli_agent_runner-0.2.12/tests/unit/test_auto_stop_gating.py +0 -87
- cli_agent_runner-0.2.12/tests/unit/test_detect_hung_latch.py +0 -31
- cli_agent_runner-0.2.12/tests/unit/test_monitor_detect_rate_limit.py +0 -47
- cli_agent_runner-0.2.12/tests/unit/test_round_cmd.py +0 -57
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/.gitignore +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/LICENSE +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/README.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/README.zh.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/build.sh +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/README.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/events.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/_clock.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.12 → cli_agent_runner-0.2.13}/tests/unit/test_vcs_state.py +0 -0
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.13] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Breaking
|
|
11
|
+
- Config validation now rejects unknown keys in base `[agent]`/`[runtime]`/`[vcs]`/`[monitor]`, a table given as a scalar, unknown per-phase `[phases.<name>.prompt]` keys, and an argv `prompt_arg_template` missing `{prompt}`. **Run `agent-runner migrate` before `upgrade`**, as in 0.2.12.
|
|
12
|
+
- systemd `serve` units grow `TimeoutStopSec` (now `round_timeout_s + 210`, up from `+60`) — re-render after upgrading.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Round-child exit codes are classified by permanence, not a type whitelist: a config break stays 78, a self-healing environmental failure (lock contention, a git-status timeout, an unwritable log dir) now reliably reaches retryable 76, and everything else still falls to 1 so the crash-loop breaker bounds a genuine bug; serve itself still never exits 76.
|
|
16
|
+
- `kill` reaps the whole round-and-agent process group; `stop`/`restart`/`uninstall` are drain-aware so a mid-round stop no longer leaves a systemd serve stopped or raises; service-mode detection checks systemd `is-active` instead of guessing; and `monitor_auto_stop_triggered` fires only after a confirmed stop.
|
|
17
|
+
- `install --system` rejects control characters in `work_dir`/`log_dir`/config path, closing a root-injection path through the rendered unit.
|
|
18
|
+
- `read_json`/orphan-state reads survive a non-UTF-8 byte or a directory instead of crashing serve; a git-status timeout mid-round no longer drops the round's own bookkeeping (`dirty_check_failed`).
|
|
19
|
+
- Throttle readers (serve's loop-top gate, the skip path, `peek`, monitor) converge on one reset source; the back-off sleep now wakes early on an NTP clock jump.
|
|
20
|
+
|
|
21
|
+
### Notes
|
|
22
|
+
- New event kind: `dirty_check_failed`. `peek --json` schema is unchanged this release.
|
|
23
|
+
|
|
24
|
+
See `docs/migrations/0.2.md`.
|
|
25
|
+
|
|
8
26
|
## [0.2.12] - 2026-08-31
|
|
9
27
|
|
|
10
28
|
### Breaking
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -54,7 +54,8 @@ def emit_max_rounds_reached(log_dir: Path, *, rounds_completed: int, max_rounds:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def emit_config_broken(log_dir: Path, *, reason: str) -> None:
|
|
57
|
-
"""Emit config_broken (serve
|
|
57
|
+
"""Emit config_broken (serve gave up on a permanent, non-self-healing failure —
|
|
58
|
+
a startup-battery check, or any other ConfigError-classified round exit)."""
|
|
58
59
|
from agent_runner.events import CONFIG_BROKEN, emit
|
|
59
60
|
|
|
60
61
|
emit(log_dir, CONFIG_BROKEN, reason=reason)
|
|
@@ -70,7 +71,7 @@ def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path
|
|
|
70
71
|
from agent_runner.events import CRASH_LOOP, emit
|
|
71
72
|
|
|
72
73
|
try:
|
|
73
|
-
reason = redact_secrets(log_path.read_text(errors="replace")[-2000:])
|
|
74
|
+
reason = redact_secrets(log_path.read_text(encoding="utf-8", errors="replace")[-2000:])
|
|
74
75
|
except OSError:
|
|
75
76
|
reason = ""
|
|
76
77
|
emit(log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, reason=reason)
|
|
@@ -315,8 +316,11 @@ def emit_agent_usage_recorded(
|
|
|
315
316
|
- ``models_breakdown``: only populated when a round used multiple models
|
|
316
317
|
(gemini multi-model rounds). None for claude (always single-model).
|
|
317
318
|
- ``cache_creation_tokens``: claude only — ``usage.cache_creation_input_tokens``,
|
|
318
|
-
independent count from ``cached_tokens`` (cache_read).
|
|
319
|
-
|
|
319
|
+
independent count from ``cached_tokens`` (cache_read). Priced differently
|
|
320
|
+
from fresh input by the provider; the framework records the count only and
|
|
321
|
+
never interprets cost (no price tables here — see ``cost_usd`` above for the
|
|
322
|
+
one cost figure the framework passes through verbatim). Gemini has no
|
|
323
|
+
equivalent → 0.
|
|
320
324
|
- ``tool_call_count``: number of tool invocations the agent made in the round.
|
|
321
325
|
Claude: count of ``tool_use`` content blocks across all assistant events.
|
|
322
326
|
Gemini: ``stats.tool_calls``.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Internal identity/location resolver (Group C, 0.2.13 "close the seams").
|
|
2
|
+
|
|
3
|
+
Project name, config path, unit filename and log_dir were each derived
|
|
4
|
+
independently at several call sites and could disagree when ``work_dir`` and
|
|
5
|
+
the TOML's own directory differ. This module is the single source for all
|
|
6
|
+
four; the divergent call sites route through it instead of re-deriving. It
|
|
7
|
+
also carries ``guard_against_clobber``, ``api.install``'s same-basename
|
|
8
|
+
sibling-unit guard — a natural fit since it works entirely off unit-file
|
|
9
|
+
identity (``WorkingDirectory=``) rather than any install-specific state.
|
|
10
|
+
|
|
11
|
+
INTERNAL ONLY -- not a public/documented contract. Exposing a resolver as a
|
|
12
|
+
public contract is deferred to 0.3.
|
|
13
|
+
|
|
14
|
+
Leaf module: imports only ``agent_runner.config`` and
|
|
15
|
+
``agent_runner.service_unit`` (both leaves themselves), so anything in the
|
|
16
|
+
package can import this without risking a cycle.
|
|
17
|
+
|
|
18
|
+
Lenient/strict split (spec-review correction): validating the project
|
|
19
|
+
name on the serve/round/init path would newly reject any ``work_dir`` whose
|
|
20
|
+
basename has a space or CJK characters -- common on macOS dev boxes -- an
|
|
21
|
+
unflagged BREAK, since only lifecycle/observe verbs validate today. So
|
|
22
|
+
``strict=True`` applies ONLY where the name is interpolated into a unit
|
|
23
|
+
filename / ssh / systemd identity (api.py's lifecycle verbs, install,
|
|
24
|
+
lifecycle.py, install_cmd.py); ``strict=False`` for descriptive uses
|
|
25
|
+
(``hook_ctx.project`` in runner.py, the scaffolded toml's ``{project}`` in
|
|
26
|
+
scaffold.py) that must keep accepting any basename.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from agent_runner.config import load_config
|
|
35
|
+
from agent_runner.service_unit import serve_unit_filename
|
|
36
|
+
|
|
37
|
+
_PROJECT_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def project_name(work_dir: Path, *, strict: bool) -> str:
|
|
41
|
+
"""Derive the project name from ``work_dir``'s basename.
|
|
42
|
+
|
|
43
|
+
strict=True rejects anything but ``[A-Za-z0-9._-]+`` -- required wherever
|
|
44
|
+
the name is interpolated into a unit filename / ssh / systemd identity.
|
|
45
|
+
strict=False accepts any non-empty basename (spaces, CJK, etc.) -- for
|
|
46
|
+
descriptive uses where the name is never shelled out or used as a
|
|
47
|
+
filesystem/systemd token by itself.
|
|
48
|
+
"""
|
|
49
|
+
name = work_dir.resolve().name or "default"
|
|
50
|
+
if strict and not _PROJECT_NAME_RE.match(name):
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"invalid project name {name!r}: must match [A-Za-z0-9._-]+. "
|
|
53
|
+
"The project name is the basename of work_dir and is interpolated into "
|
|
54
|
+
"ssh remote commands and systemd unit filenames; shell metacharacters "
|
|
55
|
+
"and path separators are rejected."
|
|
56
|
+
)
|
|
57
|
+
return name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def config_path(args) -> Path:
|
|
61
|
+
"""Resolve the config TOML path CLI ``args`` refer to (``--config``,
|
|
62
|
+
default ``./agent-runner.toml``).
|
|
63
|
+
|
|
64
|
+
Single source for "given argparse args, which agent-runner.toml" so every
|
|
65
|
+
CLI bootstrap step (which runs before a ``Config`` exists to read
|
|
66
|
+
``runtime.work_dir`` back from) agrees on the same file, instead of each
|
|
67
|
+
reconstructing its own getattr/default dance.
|
|
68
|
+
"""
|
|
69
|
+
cfg = getattr(args, "config", None)
|
|
70
|
+
return Path(cfg).resolve() if cfg is not None else Path("agent-runner.toml").resolve()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def unit_filename(project: str) -> str:
|
|
74
|
+
"""Thin wrap of the existing ``service_unit.serve_unit_filename`` -- the
|
|
75
|
+
single name every unit-filename call site should go through."""
|
|
76
|
+
return serve_unit_filename(project)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def log_dir(work_dir: Path) -> Path:
|
|
80
|
+
"""Return the configured log_dir from ``<work_dir>/agent-runner.toml``.
|
|
81
|
+
|
|
82
|
+
Falls back to the conventional ``~/.agent-runner/<project>/logs`` only
|
|
83
|
+
when the toml is missing there, keeping lifecycle/observe verbs aligned
|
|
84
|
+
with where ``serve_cmd.py`` actually writes ``serve.pid``.
|
|
85
|
+
"""
|
|
86
|
+
cfg_path = work_dir / "agent-runner.toml"
|
|
87
|
+
if cfg_path.exists():
|
|
88
|
+
return load_config(cfg_path).runtime.log_dir
|
|
89
|
+
return Path.home() / ".agent-runner" / project_name(work_dir, strict=True) / "logs"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _unit_owner(serve_path: Path) -> Path | None:
|
|
93
|
+
"""Parse an existing unit's ``WorkingDirectory=`` line, or None if the file
|
|
94
|
+
doesn't exist / can't be read / has no such line."""
|
|
95
|
+
try:
|
|
96
|
+
text = serve_path.read_text()
|
|
97
|
+
except OSError:
|
|
98
|
+
return None
|
|
99
|
+
for line in text.splitlines():
|
|
100
|
+
if line.startswith("WorkingDirectory="):
|
|
101
|
+
return Path(line.removeprefix("WorkingDirectory="))
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def guard_against_clobber(serve_path: Path, work_dir: Path, *, force: bool) -> None:
|
|
106
|
+
"""Refuse to overwrite a same-basename sibling project's unit.
|
|
107
|
+
|
|
108
|
+
The unit filename is derived from the project name (work_dir's basename)
|
|
109
|
+
alone, so two unrelated projects sharing a basename — or the same project
|
|
110
|
+
moved to a new path — would silently clobber each other's install.
|
|
111
|
+
force=True is the explicit override for the moved-repo case (``api.install``).
|
|
112
|
+
"""
|
|
113
|
+
owner = _unit_owner(serve_path)
|
|
114
|
+
if owner is None or owner == work_dir or force:
|
|
115
|
+
return
|
|
116
|
+
raise FileExistsError(
|
|
117
|
+
f"{serve_path} already manages a different project at {owner} "
|
|
118
|
+
f"(this is {work_dir}); pass force=True to overwrite "
|
|
119
|
+
"(e.g. after moving/renaming that project's directory)."
|
|
120
|
+
)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Pure serve restart policy — no I/O, no clock, no events read.
|
|
2
|
+
|
|
3
|
+
Extracted from api.py (0.2.12 Group G): serve_cmd imports the decision + exit
|
|
4
|
+
codes from here directly (see the architecture allowlist); api.py re-exports
|
|
5
|
+
them for external back-compat. Kept dependency-free so the serve loop stays a
|
|
6
|
+
thin dispatcher and the policy is unit-testable in isolation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from agent_runner.config import ConfigError
|
|
14
|
+
|
|
15
|
+
# Exit code for a permanent (no-retry) startup-battery failure. A broken config
|
|
16
|
+
# does not self-heal between rounds, so serve STOPS rather than respawning it
|
|
17
|
+
# forever. 78 = EX_CONFIG (sysexits) — avoids argparse's 2 and the generic 1.
|
|
18
|
+
PERMANENT_CONFIG_EXIT = 78
|
|
19
|
+
|
|
20
|
+
# Exit code serve returns when the crash-loop breaker trips (a give-up stop, like
|
|
21
|
+
# config_broken). Distinct from 78 so `systemctl status` / SuccessExitStatus can
|
|
22
|
+
# tell the two "needs intervention" stops apart. 75 = EX_TEMPFAIL (sysexits).
|
|
23
|
+
# The systemd unit lists both in RestartPreventExitStatus so a deliberate stop
|
|
24
|
+
# stays stopped (and visibly failed) while an unexpected supervisor crash restarts.
|
|
25
|
+
CRASH_LOOP_EXIT = 75
|
|
26
|
+
|
|
27
|
+
# Exit code for an ENVIRONMENTAL startup-battery failure (ENOSPC, mount hiccup,
|
|
28
|
+
# an unclassified check): recoverable, unlike a permanent config break. NOT in
|
|
29
|
+
# the unit's RestartPreventExitStatus (stays "78 75") so 76 restarts; treated
|
|
30
|
+
# exactly like an active throttle (post_round_decision), so serve keeps
|
|
31
|
+
# retrying at a fixed doubled delay with the crash-loop breaker disarmed,
|
|
32
|
+
# staying alive until the environment heals. StartLimit bounds only repeated
|
|
33
|
+
# serve-PROCESS death, not this round-level path. 76 = EX_PROTOCOL (sysexits).
|
|
34
|
+
ENV_BATTERY_EXIT = 76
|
|
35
|
+
|
|
36
|
+
# Crash-loop circuit breaker (b12). The serve loop escalates the restart delay
|
|
37
|
+
# on consecutive UNKNOWN short crashes (non-zero exit, short duration, no
|
|
38
|
+
# classified transient) and STOPS after CRASH_LOOP_THRESHOLD of them — the Run 6
|
|
39
|
+
# ~100-empty-rounds scar. Recoverable-slow failures (rate limit / 5h quota / 5xx
|
|
40
|
+
# / timeout) are already handled by the transient-error throttle and never reach
|
|
41
|
+
# this path. A clean (exit 0), long, or classified-transient round resets the run.
|
|
42
|
+
CRASH_LOOP_THRESHOLD = 5
|
|
43
|
+
CRASH_LOOP_SHORT_EXIT_S = 60 # mirrors monitor.SHORT_EXIT_THRESHOLD_S
|
|
44
|
+
CRASH_LOOP_MAX_DELAY_S = 1800 # cap the escalating restart delay (30 min)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EnvironmentalError(Exception):
|
|
48
|
+
"""Marks a round-child failure as ENVIRONMENTAL (recoverable) rather than a
|
|
49
|
+
permanent config break — classify_round_exit maps it to ENV_BATTERY_EXIT
|
|
50
|
+
(76), same treatment as an active throttle: serve retries at a flat back-off
|
|
51
|
+
with the crash-loop breaker disarmed.
|
|
52
|
+
|
|
53
|
+
A raise site elsewhere in the package (runner.LockHeldError,
|
|
54
|
+
vcs_state.GitTimeout) mixes this in alongside its own base class instead of
|
|
55
|
+
_serve_policy importing every module that raises one — this leaf module
|
|
56
|
+
sits BELOW runner.py/vcs_state.py in the import graph (both reach here via
|
|
57
|
+
api.py already), so importing upward would cycle. classify_round_exit
|
|
58
|
+
matches this marker, not the concrete class or its OTHER base.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# The action strings below are the restart-action enum, not events.py kinds:
|
|
63
|
+
# "continue" has no constant, and a constant cannot sit inside Literal[...].
|
|
64
|
+
def post_round_decision(
|
|
65
|
+
*,
|
|
66
|
+
returncode: int,
|
|
67
|
+
duration_s: float,
|
|
68
|
+
throttle_active: bool,
|
|
69
|
+
consecutive: int,
|
|
70
|
+
restart_delay_s: int,
|
|
71
|
+
) -> tuple[Literal["config_broken", "crash_loop", "continue"], int, int]:
|
|
72
|
+
"""Restart policy after one round — keeps the serve loop a thin dispatcher.
|
|
73
|
+
|
|
74
|
+
Returns ``(action, delay_s, consecutive)`` where action is:
|
|
75
|
+
- ``"config_broken"`` — permanent startup failure (b18): stop.
|
|
76
|
+
- ``"crash_loop"`` — CRASH_LOOP_THRESHOLD consecutive unknown short crashes
|
|
77
|
+
(b12): stop. An unknown short crash is a non-zero, fast exit with no
|
|
78
|
+
classified transient (rate-limit/5xx/timeout are handled by the throttle).
|
|
79
|
+
- ``"continue"`` — sleep ``delay_s`` then run the next round.
|
|
80
|
+
|
|
81
|
+
A clean (exit 0), long, or transient round resets ``consecutive`` to 0; an
|
|
82
|
+
unknown short crash escalates the delay (restart × 2ⁿ, capped) until the stop.
|
|
83
|
+
An ``ENV_BATTERY_EXIT`` (76, environmental startup-battery failure) is treated
|
|
84
|
+
exactly like an active throttle: it takes the flat ``restart_delay_s * 2``
|
|
85
|
+
delay below (never escalating) and resets ``consecutive`` to 0, so it never
|
|
86
|
+
counts toward the crash-loop breaker — a short environmental outage (ENOSPC,
|
|
87
|
+
mount hiccup) never trips ``crash_loop``. This always returns ``"continue"``,
|
|
88
|
+
so serve itself never exits 76 and systemd's StartLimit window never engages
|
|
89
|
+
for this path; a *persistent* environmental failure keeps serve alive
|
|
90
|
+
retrying at the doubled delay until the environment heals. StartLimit bounds
|
|
91
|
+
only repeated serve-PROCESS death, independent of this round-level path.
|
|
92
|
+
"""
|
|
93
|
+
if returncode == PERMANENT_CONFIG_EXIT:
|
|
94
|
+
return ("config_broken", 0, consecutive)
|
|
95
|
+
throttle_active = throttle_active or returncode == ENV_BATTERY_EXIT
|
|
96
|
+
unknown_short_crash = (
|
|
97
|
+
returncode != 0 and duration_s < CRASH_LOOP_SHORT_EXIT_S and not throttle_active
|
|
98
|
+
)
|
|
99
|
+
if unknown_short_crash:
|
|
100
|
+
consecutive += 1
|
|
101
|
+
if consecutive >= CRASH_LOOP_THRESHOLD:
|
|
102
|
+
return ("crash_loop", 0, consecutive)
|
|
103
|
+
delay = min(restart_delay_s * 2**consecutive, CRASH_LOOP_MAX_DELAY_S)
|
|
104
|
+
return ("continue", delay, consecutive)
|
|
105
|
+
delay = restart_delay_s if returncode == 0 else restart_delay_s * 2
|
|
106
|
+
return ("continue", delay, 0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def classify_round_exit(exc: BaseException) -> int:
|
|
110
|
+
"""The round-child exit-code CLASSIFIER (Group A) — permanence, not exception
|
|
111
|
+
identity, decides serve's response. Replaces the old exception *whitelist*
|
|
112
|
+
(``cli/__init__.py`` mapped only ``ConfigError``->78, ``round_cmd.py`` only
|
|
113
|
+
``KeyboardInterrupt``->130; ~10 other classes fell through to Python's own
|
|
114
|
+
uncaught-exception exit 1 — neither retry-76 nor stay-stopped-78/75).
|
|
115
|
+
|
|
116
|
+
- ``ConfigError`` (won't self-heal) -> ``PERMANENT_CONFIG_EXIT`` (78).
|
|
117
|
+
- ``EnvironmentalError``-marked (self-heals) -> ``ENV_BATTERY_EXIT`` (76).
|
|
118
|
+
- ``KeyboardInterrupt`` (SIGTERM/SIGINT) -> 130, the shell's convention.
|
|
119
|
+
- ``SystemExit`` passes its own code through unchanged (an int code; a
|
|
120
|
+
non-int/absent code — argparse-style ``sys.exit("msg")`` or a bare
|
|
121
|
+
``sys.exit()`` — falls to 1, same as unclassified).
|
|
122
|
+
- Anything else, INCLUDING a totally unclassified traceback, falls to 1 so
|
|
123
|
+
the crash-loop breaker still bounds a genuine supervisor bug. Do NOT
|
|
124
|
+
default unclassified -> 76: ``post_round_decision`` resets its counter on
|
|
125
|
+
76, so an arbitrary bug would loop forever at the doubled delay with no
|
|
126
|
+
breaker and no detector. The battery's own "unclassified -> environmental"
|
|
127
|
+
rule (``startup_check.py``) governs *authored probes*, not arbitrary round
|
|
128
|
+
tracebacks — a different trust boundary.
|
|
129
|
+
|
|
130
|
+
Never returns ``CRASH_LOOP_EXIT`` (75) — that verdict belongs exclusively to
|
|
131
|
+
serve's own ``post_round_decision``, counting consecutive unknown short
|
|
132
|
+
crashes; a round child never claims it directly.
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(exc, SystemExit):
|
|
135
|
+
return exc.code if isinstance(exc.code, int) else 1
|
|
136
|
+
if isinstance(exc, KeyboardInterrupt):
|
|
137
|
+
return 130
|
|
138
|
+
if isinstance(exc, ConfigError):
|
|
139
|
+
return PERMANENT_CONFIG_EXIT
|
|
140
|
+
if isinstance(exc, EnvironmentalError):
|
|
141
|
+
return ENV_BATTERY_EXIT
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Round-timeout budget (Group C, seam 3 — 0.2.13). TimeoutStopSec
|
|
147
|
+
# (service_unit.py, systemd's SIGKILL deadline after `systemctl stop`) and the
|
|
148
|
+
# outer round-wall ceiling (api.outer_round_ceiling_s, the in-process watchdog
|
|
149
|
+
# around the round subprocess) were derived independently and could drift —
|
|
150
|
+
# TimeoutStopSec only added a flat 60s to the inner round timeout, with no
|
|
151
|
+
# margin for the outer ceiling's own reap/git-commit/hook slack, let alone the
|
|
152
|
+
# SIGTERM-to-round-child grace on top of that. One function computes both so
|
|
153
|
+
# they cannot disagree.
|
|
154
|
+
#
|
|
155
|
+
# service_unit.py must not import api.py (cycle: api.py already imports
|
|
156
|
+
# service_unit.render_serve_unit), so this lives here — a dependency-free leaf
|
|
157
|
+
# both service_unit.py and api.py can import. Its margin constants are
|
|
158
|
+
# LITERAL mirrors of the real sources of truth (agent_runtime.REAP_GRACE_S,
|
|
159
|
+
# vcs_state.GIT_COMMIT_TIMEOUT_S, api._ROUND_TERM_GRACE_S) rather than
|
|
160
|
+
# imports, same as service_unit.py's literal "78 75" RestartPreventExitStatus
|
|
161
|
+
# mirroring PERMANENT_CONFIG_EXIT/CRASH_LOOP_EXIT — pinned by
|
|
162
|
+
# test_timeout_budget_invariant.py so a change to any of the three can't
|
|
163
|
+
# silently drift this budget out of sync.
|
|
164
|
+
_REAP_GRACE_S = 5
|
|
165
|
+
_GIT_COMMIT_TIMEOUT_S = 120
|
|
166
|
+
_HOOK_ALLOWANCE_S = 60 # slack for post-round hooks between the inner wall and the ceiling
|
|
167
|
+
_ROUND_TERM_GRACE_S = 15 # mirrors api._ROUND_TERM_GRACE_S / serve_cmd._ROUND_TERM_GRACE_S
|
|
168
|
+
_STOP_GRACE_MARGIN_S = 10 # pad above _ROUND_TERM_GRACE_S for systemd stop-request overhead
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def timeout_budget(round_timeout_s: int) -> tuple[int, int]:
|
|
172
|
+
"""Single source for the round-timeout safety budget.
|
|
173
|
+
|
|
174
|
+
Returns ``(timeout_stop_sec, outer_ceiling_s)``:
|
|
175
|
+
|
|
176
|
+
- ``outer_ceiling_s`` — the in-process outer wall-clock ceiling for the
|
|
177
|
+
round subprocess (``api.outer_round_ceiling_s``): ``round_timeout_s``
|
|
178
|
+
plus reap grace + git-commit ceiling + hook allowance, so it only trips
|
|
179
|
+
when the round supervisor itself is wedged, never during its own
|
|
180
|
+
bounded post-round cleanup.
|
|
181
|
+
- ``timeout_stop_sec`` — systemd's ``TimeoutStopSec``
|
|
182
|
+
(``service_unit.py``): must clear ``outer_ceiling_s`` by enough for a
|
|
183
|
+
SIGTERM to reach and drain the round (``_ROUND_TERM_GRACE_S``) plus a
|
|
184
|
+
stop-request overhead pad, so `systemctl stop` never SIGKILLs a round
|
|
185
|
+
that is draining normally.
|
|
186
|
+
"""
|
|
187
|
+
outer_ceiling_s = round_timeout_s + _REAP_GRACE_S + _GIT_COMMIT_TIMEOUT_S + _HOOK_ALLOWANCE_S
|
|
188
|
+
timeout_stop_sec = outer_ceiling_s + _ROUND_TERM_GRACE_S + _STOP_GRACE_MARGIN_S
|
|
189
|
+
return timeout_stop_sec, outer_ceiling_s
|