cli-agent-runner 0.1.41__tar.gz → 0.2.0__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.1.41 → cli_agent_runner-0.2.0}/CHANGELOG.md +26 -1
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/PKG-INFO +6 -6
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/README.md +5 -5
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/README.zh.md +6 -6
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/__init__.py +4 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/_emit.py +23 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/api.py +62 -1
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/api_types.py +13 -0
- cli_agent_runner-0.2.0/agent_runner/builtin_plugins/default_dirty_handler.py +83 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/serve_cmd.py +26 -5
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/defenses.py +12 -2
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/events.py +3 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/hooks.py +67 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/monitor.py +0 -25
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/runner.py +9 -43
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/vcs_state.py +57 -11
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/architecture.md +33 -7
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/commands.md +1 -1
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/configuration.md +6 -2
- cli_agent_runner-0.2.0/docs/migrations/0.1.42.md +58 -0
- cli_agent_runner-0.2.0/docs/migrations/0.2.0.md +166 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/plugins.md +122 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/runbook.md +21 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/thesis.md +12 -7
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/pyproject.toml +3 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/_test_helpers.py +26 -8
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_bounded_run.py +10 -2
- cli_agent_runner-0.2.0/tests/integration/test_dirty_handler_seam.py +74 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_fresh_eyes_signal.py +2 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_substrate_fingerprint.py +5 -1
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_architecture.py +4 -1
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_types.py +27 -0
- cli_agent_runner-0.2.0/tests/unit/test_catalogs.py +10 -0
- cli_agent_runner-0.2.0/tests/unit/test_default_dirty_handler.py +77 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_defenses.py +2 -2
- cli_agent_runner-0.2.0/tests/unit/test_dirty_handlers.py +49 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_init_entry_points.py +3 -2
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detectors.py +1 -18
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_runner.py +2 -2
- cli_agent_runner-0.2.0/tests/unit/test_serve_config_broken.py +33 -0
- cli_agent_runner-0.2.0/tests/unit/test_serve_crash_loop.py +128 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_vcs_state.py +92 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.gitignore +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/LICENSE +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/build.sh +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/README.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/events.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/migrations/0.1.40.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.41 → cli_agent_runner-0.2.0}/tests/unit/test_substrate.py +0 -0
|
@@ -5,6 +5,30 @@ 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.0] - 2026-07-02
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Dirty-tree handling is now pluggable. The clean-exit-dirty policy moved out of core into a `dirty_handler` hook; `stash` / `ignore` / `auto_commit` now ship as the bundled, default-on `default_dirty_handler` plugin. **No behavior or config change for existing users** — `[vcs] dirty_action` works exactly as before.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `dirty_handler` plugin seam (4th lifecycle hook) + `DirtyOutcome` on `RoundResult`; `stash_orphan` / `try_auto_commit` are now public `api` primitives. Consumers can fully own dirty-tree policy — see `docs/migrations/0.2.0.md`.
|
|
15
|
+
- `dirty_auto_committed` event (emitted when a handler auto-commits).
|
|
16
|
+
|
|
17
|
+
## [0.1.42] - 2026-06-25
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `crash_loop` defense — serve stops after 5 consecutive *unknown* short crashes (non-zero exit, <60s, no classified transient), escalating the restart delay and recording the failure reason. Ends the respawn-forever crash loop; recoverable-slow failures (rate-limit / quota / 5xx / timeout) still ride the transient-error backoff unchanged.
|
|
21
|
+
- `config_broken` defense — a permanent startup-battery failure now halts serve (distinct no-retry exit code `78`) instead of respawning a broken config every round.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `vcs.dirty_action` no longer sweeps the runner's own `log_dir` bookkeeping when `log_dir` is inside `work_dir`: `auto_commit` excludes it from the commit (no more phantom `git_head` advance on a zero-work round) and `stash` excludes it from `git stash push -u` (logs no longer vanish). `.evolving/` and agent work are unaffected.
|
|
25
|
+
|
|
26
|
+
### Removed
|
|
27
|
+
- The inert `smoke_fail_rate` monitor alert (could never fire — superseded by the always-on `config_broken` stop). Monitor now ships 11 detectors.
|
|
28
|
+
|
|
29
|
+
### Docs
|
|
30
|
+
- `thesis.md`: the stuck-loop defense is described honestly as a notify-level, opt-in-to-auto-stop monitor detector (`anomaly_repetitive_active`), not a default hard-stop; fixed the `stuck_loop_detected` naming drift.
|
|
31
|
+
|
|
8
32
|
## [0.1.41] - 2026-06-07
|
|
9
33
|
|
|
10
34
|
### Added
|
|
@@ -851,7 +875,8 @@ Initial public release on PyPI as `cli-agent-runner`.
|
|
|
851
875
|
- Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
|
|
852
876
|
gated by a manual approval on the `pypi` GitHub environment.
|
|
853
877
|
|
|
854
|
-
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.
|
|
878
|
+
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.0...HEAD
|
|
879
|
+
[0.2.0]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.42...v0.2.0
|
|
855
880
|
[0.1.26]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.25...v0.1.26
|
|
856
881
|
[0.1.25]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.24...v0.1.25
|
|
857
882
|
[0.1.24]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.23...v0.1.24
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -49,7 +49,7 @@ full disks, runaway memory.
|
|
|
49
49
|
|
|
50
50
|
```
|
|
51
51
|
┌──────────────────────────────────────────┐
|
|
52
|
-
│ Layer 3: The Witness (monitor) │
|
|
52
|
+
│ Layer 3: The Witness (monitor) │ 11 detectors + auto-stop
|
|
53
53
|
├──────────────────────────────────────────┤
|
|
54
54
|
│ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
|
|
55
55
|
├──────────────────────────────────────────┤
|
|
@@ -86,14 +86,14 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
|
86
86
|
|---|---|
|
|
87
87
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
88
88
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
89
|
-
| `restart` / `status` | `monitor` —
|
|
89
|
+
| `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
|
|
90
90
|
| `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
|
|
91
91
|
|
|
92
92
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
93
93
|
|
|
94
94
|
## Defenses (built in)
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
12 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
97
97
|
Each carries the historical incident it codifies and the invariant test that
|
|
98
98
|
guards it. Highlights:
|
|
99
99
|
|
|
@@ -106,10 +106,10 @@ guards it. Highlights:
|
|
|
106
106
|
|
|
107
107
|
Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
108
108
|
|
|
109
|
-
## Monitor:
|
|
109
|
+
## Monitor: 11 detectors
|
|
110
110
|
|
|
111
111
|
Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
|
|
112
|
-
`mem_pressure`, `
|
|
112
|
+
`mem_pressure`, `network_fail`, `rate_limit_active`,
|
|
113
113
|
`anomaly_repetitive_active`, `supervisor_stale`.
|
|
114
114
|
|
|
115
115
|
**Auto-stop the service** (continuing is harmful):
|
|
@@ -12,7 +12,7 @@ full disks, runaway memory.
|
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
┌──────────────────────────────────────────┐
|
|
15
|
-
│ Layer 3: The Witness (monitor) │
|
|
15
|
+
│ Layer 3: The Witness (monitor) │ 11 detectors + auto-stop
|
|
16
16
|
├──────────────────────────────────────────┤
|
|
17
17
|
│ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
|
|
18
18
|
├──────────────────────────────────────────┤
|
|
@@ -49,14 +49,14 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
|
49
49
|
|---|---|
|
|
50
50
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
51
51
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
52
|
-
| `restart` / `status` | `monitor` —
|
|
52
|
+
| `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
|
|
53
53
|
| `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
|
|
54
54
|
|
|
55
55
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
56
56
|
|
|
57
57
|
## Defenses (built in)
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
12 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
60
60
|
Each carries the historical incident it codifies and the invariant test that
|
|
61
61
|
guards it. Highlights:
|
|
62
62
|
|
|
@@ -69,10 +69,10 @@ guards it. Highlights:
|
|
|
69
69
|
|
|
70
70
|
Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
71
71
|
|
|
72
|
-
## Monitor:
|
|
72
|
+
## Monitor: 11 detectors
|
|
73
73
|
|
|
74
74
|
Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
|
|
75
|
-
`mem_pressure`, `
|
|
75
|
+
`mem_pressure`, `network_fail`, `rate_limit_active`,
|
|
76
76
|
`anomaly_repetitive_active`, `supervisor_stale`.
|
|
77
77
|
|
|
78
78
|
**Auto-stop the service** (continuing is harmful):
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
把任意 CLI agent(Claude Code、自研 agent、任何长跑命令)包装成可被
|
|
8
8
|
systemd / launchd 拉起、能被远程观测的服务。**每轮跑完进程退出**,外层
|
|
9
|
-
supervisor 重启 —— 这是核心模式。中间穿插
|
|
9
|
+
supervisor 重启 —— 这是核心模式。中间穿插 12 条防御,避开 production 上
|
|
10
10
|
最容易翻车的几条路:
|
|
11
11
|
|
|
12
12
|
- 轮卡死、Tool 调用空转 → 硬墙 timeout
|
|
@@ -20,7 +20,7 @@ supervisor 重启 —— 这是核心模式。中间穿插 11 条防御,避开
|
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
┌──────────────────────────────────────────┐
|
|
23
|
-
│ Layer 3:Witness(monitor) │
|
|
23
|
+
│ Layer 3:Witness(monitor) │ 11 个检测器 + 自动停服
|
|
24
24
|
├──────────────────────────────────────────┤
|
|
25
25
|
│ Layer 2:Loop(serve,~120 LOC 薄壳) │ 捕获信号,循环拉起 round
|
|
26
26
|
├──────────────────────────────────────────┤
|
|
@@ -63,7 +63,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
|
|
|
63
63
|
|---|---|
|
|
64
64
|
| `init` / `install` / `uninstall` | `peek` —— 项目状态快照 |
|
|
65
65
|
| `start` / `stop` / `kill` / `cancel` | `watch` —— peek 在刷新循环里 |
|
|
66
|
-
| `restart` / `status` | `monitor` ——
|
|
66
|
+
| `restart` / `status` | `monitor` —— 11 个检测器 + 告警 + 自动停服 |
|
|
67
67
|
| `round` / `serve` / `upgrade` | `events` —— 查询 / 流式订阅 events.jsonl |
|
|
68
68
|
|
|
69
69
|
**停服三动词**有清晰的语义分层:
|
|
@@ -73,7 +73,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
|
|
|
73
73
|
|
|
74
74
|
动词参考:[`docs/commands.md`](docs/commands.md)。
|
|
75
75
|
|
|
76
|
-
## 内置防御(
|
|
76
|
+
## 内置防御(12 条)
|
|
77
77
|
|
|
78
78
|
防御以数据形式定义在 `agent_runner/defenses.py`,可通过
|
|
79
79
|
`agent-runner peek --select defenses` 直接拿到。每条防御自带:
|
|
@@ -95,11 +95,11 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
|
|
|
95
95
|
|
|
96
96
|
完整列表 + 历史出处:[`docs/architecture.md`](docs/architecture.md)。
|
|
97
97
|
|
|
98
|
-
## Monitor:
|
|
98
|
+
## Monitor:11 个检测器
|
|
99
99
|
|
|
100
100
|
**只告警**(warning 级,服务继续跑):
|
|
101
101
|
`timeout_rate` / `hung` / `orphan_chain` / `disk_warning` /
|
|
102
|
-
`mem_pressure` / `
|
|
102
|
+
`mem_pressure` / `network_fail` / `rate_limit_active` /
|
|
103
103
|
`anomaly_repetitive_active` / `supervisor_stale`
|
|
104
104
|
|
|
105
105
|
**自动停服**(critical 级,继续是 net negative):
|
|
@@ -14,6 +14,7 @@ _HOOK_GROUPS = (
|
|
|
14
14
|
"agent_runner.context_enrichers",
|
|
15
15
|
"agent_runner.post_round_hooks",
|
|
16
16
|
"agent_runner.serve_startup_hooks",
|
|
17
|
+
"agent_runner.dirty_handler_hooks",
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
# Tracks the names passed to the most recent ``apply_plugin_disable`` call.
|
|
@@ -111,6 +112,9 @@ def apply_plugin_disable(names: list[str]) -> None:
|
|
|
111
112
|
# Serve-startup hooks
|
|
112
113
|
_prune_by_name(hooks._SERVE_STARTUP_HOOKS, desired, found)
|
|
113
114
|
|
|
115
|
+
# Dirty handlers
|
|
116
|
+
_prune_by_name(hooks._DIRTY_HANDLERS, desired, found)
|
|
117
|
+
|
|
114
118
|
# Plugin event kinds
|
|
115
119
|
for name in list(events._PLUGIN_KINDS):
|
|
116
120
|
if name in desired:
|
|
@@ -45,6 +45,29 @@ def emit_max_rounds_reached(log_dir: Path, *, rounds_completed: int, max_rounds:
|
|
|
45
45
|
emit(log_dir, MAX_ROUNDS_REACHED, rounds_completed=rounds_completed, max_rounds=max_rounds)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def emit_config_broken(log_dir: Path, *, reason: str) -> None:
|
|
49
|
+
"""Emit config_broken (serve stopped on a permanent startup-battery failure)."""
|
|
50
|
+
from agent_runner.events import CONFIG_BROKEN, emit
|
|
51
|
+
|
|
52
|
+
emit(log_dir, CONFIG_BROKEN, reason=reason)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
|
|
56
|
+
"""Emit crash_loop (serve stopped after consecutive unknown short crashes).
|
|
57
|
+
|
|
58
|
+
Captures the failure reason — a redacted tail of the round log — so a
|
|
59
|
+
recurring unknown crash can later be classified into a transient bucket.
|
|
60
|
+
"""
|
|
61
|
+
from agent_runner._redact import redact_secrets
|
|
62
|
+
from agent_runner.events import CRASH_LOOP, emit
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
reason = redact_secrets(log_path.read_text(errors="replace")[-2000:])
|
|
66
|
+
except OSError:
|
|
67
|
+
reason = ""
|
|
68
|
+
emit(log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, reason=reason)
|
|
69
|
+
|
|
70
|
+
|
|
48
71
|
def emit_stop_file_detected(
|
|
49
72
|
log_dir: Path, *, stop_file: Path, content: str, rounds_completed: int
|
|
50
73
|
) -> None:
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -18,7 +18,7 @@ import sysconfig
|
|
|
18
18
|
import time
|
|
19
19
|
from collections.abc import Iterator
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Any
|
|
21
|
+
from typing import Any, Literal
|
|
22
22
|
|
|
23
23
|
from agent_runner import events, lifecycle
|
|
24
24
|
from agent_runner.api_types import (
|
|
@@ -45,6 +45,59 @@ from agent_runner.service_unit import (
|
|
|
45
45
|
serve_unit_filename,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
+
# Exit code for a permanent (no-retry) startup-battery failure. A broken config
|
|
49
|
+
# does not self-heal between rounds, so serve STOPS rather than respawning it
|
|
50
|
+
# forever. 78 = EX_CONFIG (sysexits) — avoids argparse's 2 and the generic 1.
|
|
51
|
+
# Lives here (not runner.py) so serve_cmd can import it from the sanctioned api
|
|
52
|
+
# facade without coupling to runner (runner imports api, not the reverse).
|
|
53
|
+
PERMANENT_CONFIG_EXIT = 78
|
|
54
|
+
|
|
55
|
+
# Crash-loop circuit breaker (b12). The serve loop escalates the restart delay
|
|
56
|
+
# on consecutive UNKNOWN short crashes (non-zero exit, short duration, no
|
|
57
|
+
# classified transient) and STOPS after CRASH_LOOP_THRESHOLD of them — the Run 6
|
|
58
|
+
# ~100-empty-rounds scar. Recoverable-slow failures (rate limit / 5h quota / 5xx
|
|
59
|
+
# / timeout) are already handled by the transient-error throttle and never reach
|
|
60
|
+
# this path. A clean (exit 0), long, or classified-transient round resets the run.
|
|
61
|
+
CRASH_LOOP_THRESHOLD = 5
|
|
62
|
+
CRASH_LOOP_SHORT_EXIT_S = 60 # mirrors monitor.SHORT_EXIT_THRESHOLD_S
|
|
63
|
+
CRASH_LOOP_MAX_DELAY_S = 1800 # cap the escalating restart delay (30 min)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def post_round_decision(
|
|
67
|
+
*,
|
|
68
|
+
returncode: int,
|
|
69
|
+
duration_s: float,
|
|
70
|
+
throttle_active: bool,
|
|
71
|
+
consecutive: int,
|
|
72
|
+
restart_delay_s: int,
|
|
73
|
+
) -> tuple[Literal["config_broken", "crash_loop", "continue"], int, int]:
|
|
74
|
+
"""Restart policy after one round — keeps the serve loop a thin dispatcher.
|
|
75
|
+
|
|
76
|
+
Returns ``(action, delay_s, consecutive)`` where action is:
|
|
77
|
+
- ``"config_broken"`` — permanent startup failure (b18): stop.
|
|
78
|
+
- ``"crash_loop"`` — CRASH_LOOP_THRESHOLD consecutive unknown short crashes
|
|
79
|
+
(b12): stop. An unknown short crash is a non-zero, fast exit with no
|
|
80
|
+
classified transient (rate-limit/5xx/timeout are handled by the throttle).
|
|
81
|
+
- ``"continue"`` — sleep ``delay_s`` then run the next round.
|
|
82
|
+
|
|
83
|
+
A clean (exit 0), long, or transient round resets ``consecutive`` to 0; an
|
|
84
|
+
unknown short crash escalates the delay (restart × 2ⁿ, capped) until the stop.
|
|
85
|
+
"""
|
|
86
|
+
if returncode == PERMANENT_CONFIG_EXIT:
|
|
87
|
+
return ("config_broken", 0, consecutive)
|
|
88
|
+
unknown_short_crash = (
|
|
89
|
+
returncode != 0 and duration_s < CRASH_LOOP_SHORT_EXIT_S and not throttle_active
|
|
90
|
+
)
|
|
91
|
+
if unknown_short_crash:
|
|
92
|
+
consecutive += 1
|
|
93
|
+
if consecutive >= CRASH_LOOP_THRESHOLD:
|
|
94
|
+
return ("crash_loop", 0, consecutive)
|
|
95
|
+
delay = min(restart_delay_s * 2**consecutive, CRASH_LOOP_MAX_DELAY_S)
|
|
96
|
+
return ("continue", delay, consecutive)
|
|
97
|
+
delay = restart_delay_s if returncode == 0 else restart_delay_s * 2
|
|
98
|
+
return ("continue", delay, 0)
|
|
99
|
+
|
|
100
|
+
|
|
48
101
|
_PROJECT_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
49
102
|
|
|
50
103
|
_LINGER_HINT = (
|
|
@@ -730,6 +783,8 @@ def check_self_terminated_sentinel(log_dir: Path) -> bool:
|
|
|
730
783
|
from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-export
|
|
731
784
|
emit_agent_usage_recorded,
|
|
732
785
|
emit_anomaly_repetitive_tool,
|
|
786
|
+
emit_config_broken,
|
|
787
|
+
emit_crash_loop,
|
|
733
788
|
emit_fresh_eyes_round_triggered,
|
|
734
789
|
emit_max_rounds_reached,
|
|
735
790
|
emit_rate_limit_stop,
|
|
@@ -744,6 +799,12 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
744
799
|
emit_transient_error_recovered,
|
|
745
800
|
)
|
|
746
801
|
|
|
802
|
+
# Re-export git primitives so external callers can reach them via the api facade.
|
|
803
|
+
from agent_runner.vcs_state import ( # noqa: E402,F401 — public primitives
|
|
804
|
+
stash_orphan,
|
|
805
|
+
try_auto_commit,
|
|
806
|
+
)
|
|
807
|
+
|
|
747
808
|
|
|
748
809
|
def narrate_events(log_dir: Path, *, poll_interval_s: float = 0.5) -> Iterator[str]:
|
|
749
810
|
"""Tail events-*.jsonl files in log_dir, yielding one formatted line per event.
|
|
@@ -28,6 +28,18 @@ class ServiceMode(StrEnum):
|
|
|
28
28
|
NONE = "none"
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class DirtyOutcome:
|
|
33
|
+
"""What a dirty tree resolved to, reported on RoundResult and via events.
|
|
34
|
+
|
|
35
|
+
``ref`` is the stash SHA (kind="stashed") or commit SHA (kind="committed");
|
|
36
|
+
None for "ignored".
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
kind: Literal["stashed", "committed", "ignored"]
|
|
40
|
+
ref: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
31
43
|
@dataclass(frozen=True)
|
|
32
44
|
class RateLimitState:
|
|
33
45
|
"""Public: surfaced via peek --json when supervisor is currently throttled."""
|
|
@@ -158,6 +170,7 @@ class RoundResult:
|
|
|
158
170
|
log_path: Path
|
|
159
171
|
dirty_files: list[str]
|
|
160
172
|
stashed: bool
|
|
173
|
+
dirty_outcome: DirtyOutcome | None = None
|
|
161
174
|
killed_for_grace: bool = False
|
|
162
175
|
|
|
163
176
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Bundled default dirty-tree handler — reproduces the pre-0.2.0 built-in
|
|
2
|
+
behavior (stash / ignore / auto_commit per [vcs] dirty_action) as a plugin.
|
|
3
|
+
Ships enabled; disable via [plugins] disable = ["default_dirty_handler"]."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from agent_runner import api, context_store, events, vcs_state
|
|
8
|
+
from agent_runner.api_types import DirtyOutcome
|
|
9
|
+
from agent_runner.events import now_iso_ms # match the helper runner.py uses
|
|
10
|
+
from agent_runner.hooks import HookContext, register_dirty_handler
|
|
11
|
+
from agent_runner.vcs_state import AutoCommitError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DefaultDirtyHandler:
|
|
15
|
+
name = "default_dirty_handler"
|
|
16
|
+
priority = 1000 # last-resort fallback
|
|
17
|
+
|
|
18
|
+
def handle_dirty(self, ctx: HookContext, dirty_files) -> DirtyOutcome | None:
|
|
19
|
+
action = ctx.vcs.dirty_action if ctx.vcs else "stash"
|
|
20
|
+
if action == "ignore":
|
|
21
|
+
return DirtyOutcome(kind="ignored")
|
|
22
|
+
if action == "auto_commit":
|
|
23
|
+
try:
|
|
24
|
+
sha = api.try_auto_commit(
|
|
25
|
+
ctx.work_dir, ctx.round_num, ctx.phase, log_dir=ctx.log_dir
|
|
26
|
+
)
|
|
27
|
+
except AutoCommitError as exc:
|
|
28
|
+
# Parity with runner.py: emit failure event, leave tree dirty
|
|
29
|
+
# (no stash fallback). Dirty tree carries into the next round.
|
|
30
|
+
events.emit(
|
|
31
|
+
ctx.log_dir,
|
|
32
|
+
events.DIRTY_COMMIT_FAILED,
|
|
33
|
+
round_num=ctx.round_num,
|
|
34
|
+
phase=ctx.phase,
|
|
35
|
+
reason=str(exc),
|
|
36
|
+
)
|
|
37
|
+
return DirtyOutcome(kind="ignored")
|
|
38
|
+
if not sha:
|
|
39
|
+
return DirtyOutcome(kind="ignored")
|
|
40
|
+
events.emit(
|
|
41
|
+
ctx.log_dir,
|
|
42
|
+
events.DIRTY_AUTO_COMMITTED,
|
|
43
|
+
round_num=ctx.round_num,
|
|
44
|
+
files=dirty_files[:20],
|
|
45
|
+
ref=sha,
|
|
46
|
+
)
|
|
47
|
+
return DirtyOutcome(kind="committed", ref=sha)
|
|
48
|
+
return self._stash(ctx, dirty_files)
|
|
49
|
+
|
|
50
|
+
def _stash(self, ctx: HookContext, dirty_files) -> DirtyOutcome:
|
|
51
|
+
# via vcs_state (not api): test_runner.py monkeypatches vcs_state.stash_orphan;
|
|
52
|
+
# the api re-export is a separate binding a patch wouldn't intercept.
|
|
53
|
+
ref = vcs_state.stash_orphan(
|
|
54
|
+
ctx.work_dir,
|
|
55
|
+
round_num=ctx.round_num,
|
|
56
|
+
phase=ctx.phase,
|
|
57
|
+
idempotency_s=ctx.vcs.stash_idempotency_s if ctx.vcs else 5,
|
|
58
|
+
log_dir=ctx.log_dir,
|
|
59
|
+
)
|
|
60
|
+
if ref is None:
|
|
61
|
+
return DirtyOutcome(kind="ignored")
|
|
62
|
+
context_store.write_orphan_state(
|
|
63
|
+
ctx.log_dir,
|
|
64
|
+
context_store.OrphanState(
|
|
65
|
+
round_num=ctx.round_num,
|
|
66
|
+
files=dirty_files,
|
|
67
|
+
stashed_ref=ref.sha,
|
|
68
|
+
stash_message=ref.message,
|
|
69
|
+
timestamp=now_iso_ms(),
|
|
70
|
+
phase=ctx.phase,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
events.emit(
|
|
74
|
+
ctx.log_dir,
|
|
75
|
+
events.ORPHAN_STASHED,
|
|
76
|
+
round_num=ctx.round_num,
|
|
77
|
+
ref=ref.sha,
|
|
78
|
+
reason="clean_exit_with_dirty_tree",
|
|
79
|
+
)
|
|
80
|
+
return DirtyOutcome(kind="stashed", ref=ref.sha)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
register_dirty_handler(DefaultDirtyHandler())
|
|
@@ -23,12 +23,15 @@ from agent_runner._throttle import _check_throttle_state
|
|
|
23
23
|
from agent_runner._throttle import reset_counters as _reset_counters
|
|
24
24
|
from agent_runner.api import (
|
|
25
25
|
check_self_terminated_sentinel,
|
|
26
|
+
emit_config_broken,
|
|
27
|
+
emit_crash_loop,
|
|
26
28
|
emit_fresh_eyes_round_triggered,
|
|
27
29
|
emit_max_rounds_reached,
|
|
28
30
|
emit_rate_limit_stop,
|
|
29
31
|
emit_round_substrate_after,
|
|
30
32
|
emit_round_substrate_before,
|
|
31
33
|
emit_stop_file_detected,
|
|
34
|
+
post_round_decision,
|
|
32
35
|
)
|
|
33
36
|
from agent_runner.cli.common import cfg_from_args
|
|
34
37
|
from agent_runner.hooks import run_serve_startup_hooks
|
|
@@ -135,6 +138,7 @@ def cmd(args) -> int:
|
|
|
135
138
|
stop_file = cfg.runtime.stop_file # cache: same pattern as effective_max_rounds
|
|
136
139
|
work_dir = cfg.runtime.work_dir
|
|
137
140
|
rounds_completed = 0
|
|
141
|
+
consecutive_crashes = 0 # b12: consecutive UNKNOWN short crashes (crash-loop breaker)
|
|
138
142
|
|
|
139
143
|
try:
|
|
140
144
|
pid_file.write(os.getpid())
|
|
@@ -197,6 +201,7 @@ def cmd(args) -> int:
|
|
|
197
201
|
every_n=cfg.runtime.fresh_eyes_every_n,
|
|
198
202
|
)
|
|
199
203
|
round_log_path = log_dir / f"round-{round_num}.log"
|
|
204
|
+
round_started = time.monotonic()
|
|
200
205
|
with round_log_path.open("w") as f:
|
|
201
206
|
r = subprocess.run(
|
|
202
207
|
[
|
|
@@ -211,6 +216,7 @@ def cmd(args) -> int:
|
|
|
211
216
|
stdout=f,
|
|
212
217
|
stderr=subprocess.STDOUT,
|
|
213
218
|
)
|
|
219
|
+
round_duration_s = time.monotonic() - round_started
|
|
214
220
|
atomic_relink(log_dir / ROUND_CURRENT_LINK, round_log_path)
|
|
215
221
|
git_head_after = compute_git_head(work_dir)
|
|
216
222
|
paths_hash_after = compute_paths_hash(work_dir, cfg.runtime.substrate_fingerprint_paths)
|
|
@@ -221,13 +227,28 @@ def cmd(args) -> int:
|
|
|
221
227
|
paths_hash=paths_hash_after,
|
|
222
228
|
)
|
|
223
229
|
rounds_completed += 1
|
|
230
|
+
# Restart policy (config_broken / crash_loop / continue) lives in the
|
|
231
|
+
# tested api.post_round_decision helper so this loop stays thin.
|
|
232
|
+
action, delay, consecutive_crashes = post_round_decision(
|
|
233
|
+
returncode=r.returncode,
|
|
234
|
+
duration_s=round_duration_s,
|
|
235
|
+
throttle_active=_check_throttle_state(log_dir) is not None,
|
|
236
|
+
consecutive=consecutive_crashes,
|
|
237
|
+
restart_delay_s=cfg.runtime.restart_delay_s,
|
|
238
|
+
)
|
|
239
|
+
if action == "config_broken":
|
|
240
|
+
emit_config_broken(log_dir, reason="startup battery permanent failure")
|
|
241
|
+
break
|
|
242
|
+
if action == "crash_loop":
|
|
243
|
+
emit_crash_loop(
|
|
244
|
+
log_dir,
|
|
245
|
+
consecutive=consecutive_crashes,
|
|
246
|
+
exit_code=r.returncode,
|
|
247
|
+
log_path=round_log_path,
|
|
248
|
+
)
|
|
249
|
+
break
|
|
224
250
|
if args.once or stop["requested"]:
|
|
225
251
|
break
|
|
226
|
-
delay = (
|
|
227
|
-
cfg.runtime.restart_delay_s
|
|
228
|
-
if r.returncode == 0
|
|
229
|
-
else cfg.runtime.restart_delay_s * 2
|
|
230
|
-
)
|
|
231
252
|
time.sleep(delay)
|
|
232
253
|
finally:
|
|
233
254
|
pid_file.unlink()
|
|
@@ -83,8 +83,18 @@ def catalog(cfg: Config) -> list[Defense]:
|
|
|
83
83
|
Defense(
|
|
84
84
|
name="startup_smoke_check",
|
|
85
85
|
value="6 checks (config / log_dir / agent_cli / git / prompt_file / prompt_smoke)",
|
|
86
|
-
codifies=
|
|
87
|
-
|
|
86
|
+
codifies=(
|
|
87
|
+
"R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; "
|
|
88
|
+
"now halts serve (config_broken) instead of respawning a broken config"
|
|
89
|
+
),
|
|
90
|
+
guarded_by=Path("tests/unit/test_serve_config_broken.py"),
|
|
91
|
+
current_state="active",
|
|
92
|
+
),
|
|
93
|
+
Defense(
|
|
94
|
+
name="crash_loop_breaker",
|
|
95
|
+
value="stop after 5 consecutive short crashes; exp-escalating delay",
|
|
96
|
+
codifies="Run 6 — crashing agent respawned ~100 empty rounds at a fixed 2x delay",
|
|
97
|
+
guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
|
|
88
98
|
current_state="active",
|
|
89
99
|
),
|
|
90
100
|
Defense(
|
|
@@ -32,6 +32,9 @@ ANOMALY_REPETITIVE_TOOL = "anomaly_repetitive_tool"
|
|
|
32
32
|
AGENT_NETWORK_BLIP = "agent_network_blip"
|
|
33
33
|
AGENT_SPAWN = "agent_spawn"
|
|
34
34
|
AGENT_USAGE_RECORDED = "agent_usage_recorded"
|
|
35
|
+
CONFIG_BROKEN = "config_broken"
|
|
36
|
+
CRASH_LOOP = "crash_loop"
|
|
37
|
+
DIRTY_AUTO_COMMITTED = "dirty_auto_committed"
|
|
35
38
|
DIRTY_COMMIT_FAILED = "dirty_commit_failed"
|
|
36
39
|
DIRTY_DETECTED = "dirty_detected"
|
|
37
40
|
FRESH_EYES_ROUND_TRIGGERED = "fresh_eyes_round_triggered"
|
|
@@ -30,6 +30,7 @@ Public API:
|
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
32
|
import sys
|
|
33
|
+
import traceback as tb_mod
|
|
33
34
|
from dataclasses import dataclass
|
|
34
35
|
from pathlib import Path
|
|
35
36
|
from typing import Any, Protocol, runtime_checkable
|
|
@@ -43,6 +44,14 @@ _TAIL_BYTES = 1024
|
|
|
43
44
|
_TRUNC_MARKER = "\n... [truncated] ...\n"
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class VcsHookView:
|
|
49
|
+
"""Minimal vcs config a dirty handler needs (keeps HookContext narrow)."""
|
|
50
|
+
|
|
51
|
+
dirty_action: str
|
|
52
|
+
stash_idempotency_s: int
|
|
53
|
+
|
|
54
|
+
|
|
46
55
|
@dataclass(frozen=True)
|
|
47
56
|
class HookContext:
|
|
48
57
|
"""Narrow per-round context passed to all hook callbacks.
|
|
@@ -86,6 +95,26 @@ class HookContext:
|
|
|
86
95
|
"""Count threshold for repetitive-tool anomaly (0 = disabled).
|
|
87
96
|
Populated from ``[monitor] anomaly_repetitive_threshold`` (0.1.31+).
|
|
88
97
|
"""
|
|
98
|
+
vcs: VcsHookView | None = None
|
|
99
|
+
"""Narrowed vcs config slice populated by the runner for all per-round hooks
|
|
100
|
+
(pre-round, enrichers, post-round, dirty-handler) (0.2.0+).
|
|
101
|
+
May be None only when a HookContext is constructed without it (e.g. tests).
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@runtime_checkable
|
|
106
|
+
class DirtyHandler(Protocol):
|
|
107
|
+
"""Resolves a clean-exit dirty tree. Runs ascending by ``priority``; the
|
|
108
|
+
first to return a DirtyOutcome wins. Return None to pass to the next."""
|
|
109
|
+
|
|
110
|
+
name: str
|
|
111
|
+
priority: int
|
|
112
|
+
|
|
113
|
+
def handle_dirty(
|
|
114
|
+
self,
|
|
115
|
+
ctx: HookContext,
|
|
116
|
+
dirty_files: list[str],
|
|
117
|
+
) -> Any | None: ...
|
|
89
118
|
|
|
90
119
|
|
|
91
120
|
@runtime_checkable
|
|
@@ -148,6 +177,7 @@ _PRE_ROUND_HOOKS: list[PreRoundHook] = []
|
|
|
148
177
|
_CONTEXT_ENRICHERS: list[ContextEnricher] = []
|
|
149
178
|
_POST_ROUND_HOOKS: list[PostRoundHook] = []
|
|
150
179
|
_SERVE_STARTUP_HOOKS: list[ServeStartupHook] = []
|
|
180
|
+
_DIRTY_HANDLERS: list[DirtyHandler] = []
|
|
151
181
|
|
|
152
182
|
|
|
153
183
|
def register_pre_round_hook(hook: PreRoundHook) -> None:
|
|
@@ -191,6 +221,43 @@ def plugin_context_enrichers() -> list[str]:
|
|
|
191
221
|
return sorted(e.name for e in _CONTEXT_ENRICHERS)
|
|
192
222
|
|
|
193
223
|
|
|
224
|
+
def register_dirty_handler(handler: DirtyHandler) -> None:
|
|
225
|
+
ensure_unique(handler.name, _DIRTY_HANDLERS, "dirty_handler")
|
|
226
|
+
_DIRTY_HANDLERS.append(handler)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def dirty_handlers() -> list[DirtyHandler]:
|
|
230
|
+
return list(_DIRTY_HANDLERS)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def dispatch_dirty(
|
|
234
|
+
ctx: HookContext,
|
|
235
|
+
dirty_files: list[str],
|
|
236
|
+
log_dir: Path,
|
|
237
|
+
) -> Any | None:
|
|
238
|
+
"""Run registered DirtyHandlers ascending by priority; first non-None wins.
|
|
239
|
+
|
|
240
|
+
A handler that raises is isolated — emits ``hook_failed`` with
|
|
241
|
+
``hook_kind="dirty_handler"`` and treated as pass (continue to next).
|
|
242
|
+
"""
|
|
243
|
+
ordered = sorted(_DIRTY_HANDLERS, key=lambda h: getattr(h, "priority", 0))
|
|
244
|
+
for h in ordered:
|
|
245
|
+
try:
|
|
246
|
+
outcome = h.handle_dirty(ctx, dirty_files)
|
|
247
|
+
except Exception as exc: # noqa: BLE001 — isolate; fall through to next
|
|
248
|
+
events.emit(
|
|
249
|
+
log_dir,
|
|
250
|
+
events.HOOK_FAILED,
|
|
251
|
+
hook_name=h.name,
|
|
252
|
+
hook_kind="dirty_handler",
|
|
253
|
+
**_summarize_error(exc, tb=tb_mod.format_exc()),
|
|
254
|
+
)
|
|
255
|
+
continue
|
|
256
|
+
if outcome is not None:
|
|
257
|
+
return outcome
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
194
261
|
def run_serve_startup_hooks(cfg: Any, log_dir: Path) -> bool:
|
|
195
262
|
"""Run all serve_startup_hooks. Returns True on success, False on abort.
|
|
196
263
|
|