cli-agent-runner 0.1.42__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.42 → cli_agent_runner-0.2.0}/CHANGELOG.md +11 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/PKG-INFO +2 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/README.md +1 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/README.zh.md +1 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/__init__.py +4 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/api.py +6 -0
- {cli_agent_runner-0.1.42 → 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.42 → cli_agent_runner-0.2.0}/agent_runner/events.py +1 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/hooks.py +67 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/runner.py +8 -45
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/vcs_state.py +15 -17
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/architecture.md +26 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/configuration.md +6 -2
- cli_agent_runner-0.2.0/docs/migrations/0.2.0.md +166 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/plugins.md +122 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/runbook.md +21 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/pyproject.toml +3 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/_test_helpers.py +18 -5
- cli_agent_runner-0.2.0/tests/integration/test_dirty_handler_seam.py +74 -0
- {cli_agent_runner-0.1.42 → 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.2.0/tests/unit/test_dirty_handlers.py +49 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_init_entry_points.py +3 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_vcs_state.py +27 -4
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.gitignore +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/LICENSE +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_emit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/build.sh +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/README.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/commands.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/events.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.40.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.42.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/thesis.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.42 → 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.42 → 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.42 → 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.42 → cli_agent_runner-0.2.0}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_substrate.py +0 -0
|
@@ -5,6 +5,15 @@ 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
|
+
|
|
8
17
|
## [0.1.42] - 2026-06-25
|
|
9
18
|
|
|
10
19
|
### Added
|
|
@@ -866,7 +875,8 @@ Initial public release on PyPI as `cli-agent-runner`.
|
|
|
866
875
|
- Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
|
|
867
876
|
gated by a manual approval on the `pypi` GitHub environment.
|
|
868
877
|
|
|
869
|
-
[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
|
|
870
880
|
[0.1.26]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.25...v0.1.26
|
|
871
881
|
[0.1.25]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.24...v0.1.25
|
|
872
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
|
|
@@ -109,7 +109,7 @@ Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
|
109
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):
|
|
@@ -72,7 +72,7 @@ Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
|
72
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):
|
|
@@ -99,7 +99,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
|
|
|
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:
|
|
@@ -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
|
|
@@ -799,6 +799,12 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
799
799
|
emit_transient_error_recovered,
|
|
800
800
|
)
|
|
801
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
|
+
|
|
802
808
|
|
|
803
809
|
def narrate_events(log_dir: Path, *, poll_interval_s: float = 0.5) -> Iterator[str]:
|
|
804
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())
|
|
@@ -34,6 +34,7 @@ AGENT_SPAWN = "agent_spawn"
|
|
|
34
34
|
AGENT_USAGE_RECORDED = "agent_usage_recorded"
|
|
35
35
|
CONFIG_BROKEN = "config_broken"
|
|
36
36
|
CRASH_LOOP = "crash_loop"
|
|
37
|
+
DIRTY_AUTO_COMMITTED = "dirty_auto_committed"
|
|
37
38
|
DIRTY_COMMIT_FAILED = "dirty_commit_failed"
|
|
38
39
|
DIRTY_DETECTED = "dirty_detected"
|
|
39
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
|
|
|
@@ -423,6 +423,10 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
423
423
|
dry_run=cfg.runtime.dry_run,
|
|
424
424
|
anomaly_repetitive_window=cfg.monitor.anomaly_repetitive_window,
|
|
425
425
|
anomaly_repetitive_threshold=cfg.monitor.anomaly_repetitive_threshold,
|
|
426
|
+
vcs=hooks.VcsHookView(
|
|
427
|
+
dirty_action=cfg.vcs.dirty_action,
|
|
428
|
+
stash_idempotency_s=cfg.vcs.stash_idempotency_s,
|
|
429
|
+
),
|
|
426
430
|
)
|
|
427
431
|
_run_pre_round_hooks(
|
|
428
432
|
hook_ctx,
|
|
@@ -512,54 +516,12 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
512
516
|
if dirty:
|
|
513
517
|
events.emit(log_dir, "dirty_detected", round_num=round_num, files=dirty[:20])
|
|
514
518
|
|
|
515
|
-
|
|
516
|
-
action = cfg.vcs.dirty_action
|
|
519
|
+
dirty_outcome = None
|
|
517
520
|
if dirty and not result.timed_out and result.exit_code == 0:
|
|
518
|
-
|
|
519
|
-
ref = vcs_state.stash_orphan(
|
|
520
|
-
cfg.runtime.work_dir,
|
|
521
|
-
round_num=round_num,
|
|
522
|
-
phase=phase,
|
|
523
|
-
idempotency_s=cfg.vcs.stash_idempotency_s,
|
|
524
|
-
log_dir=cfg.runtime.log_dir,
|
|
525
|
-
)
|
|
526
|
-
if ref is not None:
|
|
527
|
-
context_store.write_orphan_state(
|
|
528
|
-
log_dir,
|
|
529
|
-
context_store.OrphanState(
|
|
530
|
-
round_num=round_num,
|
|
531
|
-
files=dirty,
|
|
532
|
-
stashed_ref=ref.sha,
|
|
533
|
-
stash_message=ref.message,
|
|
534
|
-
timestamp=now_iso_ms(),
|
|
535
|
-
phase=phase,
|
|
536
|
-
),
|
|
537
|
-
)
|
|
538
|
-
events.emit(
|
|
539
|
-
log_dir,
|
|
540
|
-
"orphan_stashed",
|
|
541
|
-
round_num=round_num,
|
|
542
|
-
ref=ref.sha,
|
|
543
|
-
reason="clean_exit_with_dirty_tree",
|
|
544
|
-
)
|
|
545
|
-
stashed = True
|
|
546
|
-
elif action == "ignore":
|
|
547
|
-
# Leave tree dirty for next round; dirty_detected already emitted
|
|
548
|
-
pass
|
|
549
|
-
elif action == "auto_commit":
|
|
550
|
-
err = vcs_state.try_auto_commit(
|
|
551
|
-
cfg.runtime.work_dir, round_num, phase, log_dir=cfg.runtime.log_dir
|
|
552
|
-
)
|
|
553
|
-
if err is not None:
|
|
554
|
-
events.emit(
|
|
555
|
-
log_dir,
|
|
556
|
-
events.DIRTY_COMMIT_FAILED,
|
|
557
|
-
round_num=round_num,
|
|
558
|
-
phase=phase,
|
|
559
|
-
reason=err,
|
|
560
|
-
)
|
|
521
|
+
dirty_outcome = hooks.dispatch_dirty(hook_ctx, dirty, log_dir=log_dir)
|
|
561
522
|
elif not dirty:
|
|
562
523
|
context_store.clear_orphan_state(log_dir)
|
|
524
|
+
stashed = bool(dirty_outcome and dirty_outcome.kind == "stashed")
|
|
563
525
|
|
|
564
526
|
if result.killed_for_grace:
|
|
565
527
|
api.emit_round_grace_kill(
|
|
@@ -609,6 +571,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
609
571
|
log_path=log_path,
|
|
610
572
|
dirty_files=dirty,
|
|
611
573
|
stashed=stashed,
|
|
574
|
+
dirty_outcome=dirty_outcome,
|
|
612
575
|
killed_for_grace=result.killed_for_grace,
|
|
613
576
|
)
|
|
614
577
|
_run_post_round_hooks(hook_ctx, round_result, log_dir)
|
|
@@ -72,6 +72,10 @@ def _matches_owned_path(path: str) -> bool:
|
|
|
72
72
|
return False
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
class AutoCommitError(RuntimeError):
|
|
76
|
+
"""git add/commit failed during try_auto_commit (reason capped at 200 chars)."""
|
|
77
|
+
|
|
78
|
+
|
|
75
79
|
@dataclass(frozen=True)
|
|
76
80
|
class StashRef:
|
|
77
81
|
sha: str # full commit SHA — IMMUTABLE under concurrent stash
|
|
@@ -322,20 +326,13 @@ def try_auto_commit(
|
|
|
322
326
|
phase: str | None,
|
|
323
327
|
*,
|
|
324
328
|
log_dir: Path | None = None,
|
|
325
|
-
) -> str
|
|
326
|
-
"""Auto-commit dirty tree with hardcoded subject
|
|
327
|
-
|
|
328
|
-
Subject: ``agent-runner auto-commit: R<N> <phase>`` (phase part omitted if None).
|
|
329
|
-
Uses ``git -c commit.gpgsign=false`` to skip GPG; honors pre-commit hooks
|
|
330
|
-
(no ``--no-verify``). DOES NOT push — local commit only.
|
|
329
|
+
) -> str:
|
|
330
|
+
"""Auto-commit the dirty tree with a hardcoded subject; return the commit SHA.
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
the exclusion, this is a no-op (returns None, leaves HEAD untouched).
|
|
337
|
-
|
|
338
|
-
Callers (runner.py) emit ``dirty_commit_failed`` event when return value is not None.
|
|
332
|
+
Returns "" when nothing remained staged after excluding log_dir (no-op;
|
|
333
|
+
HEAD untouched). Raises AutoCommitError on git failure. DOES NOT push.
|
|
334
|
+
Subject: ``agent-runner auto-commit: R<N> <phase>``. Uses
|
|
335
|
+
``git -c commit.gpgsign=false``; honors pre-commit hooks (no --no-verify).
|
|
339
336
|
"""
|
|
340
337
|
phase_part = f" {phase}" if phase else ""
|
|
341
338
|
subject = f"agent-runner auto-commit: R{round_num}{phase_part}"
|
|
@@ -343,13 +340,13 @@ def try_auto_commit(
|
|
|
343
340
|
exclude = _log_dir_exclude_pathspec(work_dir, log_dir)
|
|
344
341
|
add_result = _git(work_dir, "add", "-A", *exclude)
|
|
345
342
|
if add_result.returncode != 0:
|
|
346
|
-
|
|
343
|
+
raise AutoCommitError((add_result.stderr or "git add failed")[:200])
|
|
347
344
|
|
|
348
345
|
# Only the exclusion can leave nothing staged (a zero-work round that churned
|
|
349
346
|
# only log_dir); without it the tree was dirty so there is always something to
|
|
350
347
|
# commit. Skip the extra git call on the common (no-exclusion) path.
|
|
351
348
|
if exclude and _git(work_dir, "diff", "--cached", "--quiet").returncode == 0:
|
|
352
|
-
return
|
|
349
|
+
return ""
|
|
353
350
|
|
|
354
351
|
commit_result = _git(
|
|
355
352
|
work_dir,
|
|
@@ -359,6 +356,7 @@ def try_auto_commit(
|
|
|
359
356
|
pre_flags=("-c", "commit.gpgsign=false"),
|
|
360
357
|
)
|
|
361
358
|
if commit_result.returncode != 0:
|
|
362
|
-
|
|
359
|
+
raise AutoCommitError((commit_result.stderr or "git commit failed")[:200])
|
|
363
360
|
|
|
364
|
-
|
|
361
|
+
head = _git(work_dir, "rev-parse", "HEAD")
|
|
362
|
+
return head.stdout.strip()
|
|
@@ -72,8 +72,8 @@ Three categories by `auto_action`:
|
|
|
72
72
|
|
|
73
73
|
**Notify only** (severity `warning`):
|
|
74
74
|
`timeout_rate`, `hung`, `orphan_chain`, `disk_warning`, `mem_pressure`,
|
|
75
|
-
`
|
|
76
|
-
`
|
|
75
|
+
`network_fail`, `rate_limit_active`, `anomaly_repetitive_active`,
|
|
76
|
+
`supervisor_stale`.
|
|
77
77
|
|
|
78
78
|
**Auto-stop service** (severity `critical`, `auto_action="stop_service"`):
|
|
79
79
|
`oauth_fail`, `disk_critical`. Continuing in either state is harmful (burning
|
|
@@ -142,6 +142,29 @@ Setting `inject_context = false` does NOT disable PreRoundHooks. Setting
|
|
|
142
142
|
If you want neither injection: set both. If you want to disable a specific plugin
|
|
143
143
|
hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"]`.
|
|
144
144
|
|
|
145
|
+
## Dirty-handler seam (0.2.0+)
|
|
146
|
+
|
|
147
|
+
After a round exits cleanly with a dirty working tree, the runner dispatches a
|
|
148
|
+
priority-ordered chain of registered `DirtyHandler` plugins. The first to return
|
|
149
|
+
a non-`None` `DirtyOutcome` wins; remaining handlers are skipped.
|
|
150
|
+
|
|
151
|
+
This makes dirty-tree policy fully pluggable. The `default_dirty_handler` plugin
|
|
152
|
+
(bundled, default-on, priority 1000) implements the `stash` / `ignore` /
|
|
153
|
+
`auto_commit` policy from `[vcs] dirty_action` — so the external behavior is
|
|
154
|
+
identical to pre-0.2.0 unless a consumer plugin intervenes first.
|
|
155
|
+
|
|
156
|
+
**Lifecycle-hook family** (4 groups, run in this order per round):
|
|
157
|
+
|
|
158
|
+
1. `serve_startup_hooks` — once at `agent-runner serve` boot, before the loop
|
|
159
|
+
2. `pre_round_hooks` — after lock acquire, before context is written
|
|
160
|
+
3. `post_round_hooks` (+ `context_enrichers`) — after agent exits, before `round_end`
|
|
161
|
+
4. `dirty_handler_hooks` — after post_round, if the tree is dirty on a clean exit
|
|
162
|
+
|
|
163
|
+
The stash *mechanism* (SHA lock, idempotency guard, `stash_orphan` /
|
|
164
|
+
`try_auto_commit` primitives) stays in core. The stash *policy* is plugin-provided.
|
|
165
|
+
|
|
166
|
+
See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
|
|
167
|
+
|
|
145
168
|
## Known event kinds
|
|
146
169
|
|
|
147
170
|
<!-- gen:event-kinds -->
|
|
@@ -153,6 +176,7 @@ hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"
|
|
|
153
176
|
- `anomaly_repetitive_tool`
|
|
154
177
|
- `config_broken`
|
|
155
178
|
- `crash_loop`
|
|
179
|
+
- `dirty_auto_committed`
|
|
156
180
|
- `dirty_commit_failed`
|
|
157
181
|
- `dirty_detected`
|
|
158
182
|
- `fresh_eyes_round_triggered`
|
|
@@ -90,13 +90,17 @@ Type: string, one of `"stash"`, `"ignore"`, `"auto_commit"`
|
|
|
90
90
|
Default: `"stash"`
|
|
91
91
|
|
|
92
92
|
Controls supervisor behavior when round subprocess exits with a dirty
|
|
93
|
-
working tree
|
|
93
|
+
working tree. This config is read by the bundled `default_dirty_handler` plugin
|
|
94
|
+
(0.2.0+, default-on). Existing behavior and TOML are unchanged.
|
|
94
95
|
|
|
95
96
|
| Value | Behavior |
|
|
96
97
|
|---|---|
|
|
97
98
|
| `"stash"` | Auto-stash dirty tree with ORPHAN-prefix message. `dirty_detected` + `orphan_stashed` events emitted. |
|
|
98
99
|
| `"ignore"` | Emit `dirty_detected` event only. Working tree left dirty for next round. |
|
|
99
|
-
| `"auto_commit"` | Supervisor commits with subject `agent-runner auto-commit: R<N> <phase>`. No push. On failure, emits `dirty_commit_failed`, leaves tree dirty. |
|
|
100
|
+
| `"auto_commit"` | Supervisor commits with subject `agent-runner auto-commit: R<N> <phase>`. No push. On success, emits `dirty_auto_committed`. On failure, emits `dirty_commit_failed`, leaves tree dirty. |
|
|
101
|
+
|
|
102
|
+
To replace this policy entirely, disable the bundled plugin and register your own
|
|
103
|
+
`DirtyHandler` — see `docs/plugins.md` and `docs/migrations/0.2.0.md`.
|
|
100
104
|
|
|
101
105
|
## `[agent.env]` (optional)
|
|
102
106
|
|