cli-agent-runner 0.1.42__tar.gz → 0.2.1__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.1}/CHANGELOG.md +22 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/PKG-INFO +2 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/README.md +1 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/README.zh.md +1 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/__init__.py +4 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/agent_runtime.py +47 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/api.py +6 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/api_types.py +13 -0
- cli_agent_runner-0.2.1/agent_runner/builtin_plugins/default_dirty_handler.py +83 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/config.py +15 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/events.py +1 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/hooks.py +67 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/presets/claude.toml +2 -1
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/runner.py +27 -45
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/vcs_state.py +15 -17
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/architecture.md +26 -2
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/configuration.md +21 -2
- cli_agent_runner-0.2.1/docs/migrations/0.2.0.md +166 -0
- cli_agent_runner-0.2.1/docs/migrations/0.2.1.md +77 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/plugins.md +122 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/runbook.md +27 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/pyproject.toml +3 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/_test_helpers.py +64 -5
- cli_agent_runner-0.2.1/tests/integration/test_dirty_handler_seam.py +74 -0
- cli_agent_runner-0.2.1/tests/integration/test_prompt_delivery_stdin.py +103 -0
- cli_agent_runner-0.2.1/tests/unit/test_agent_runtime_signal_name.py +21 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_types.py +27 -0
- cli_agent_runner-0.2.1/tests/unit/test_catalogs.py +10 -0
- cli_agent_runner-0.2.1/tests/unit/test_config_prompt_delivery.py +33 -0
- cli_agent_runner-0.2.1/tests/unit/test_default_dirty_handler.py +77 -0
- cli_agent_runner-0.2.1/tests/unit/test_dirty_handlers.py +49 -0
- cli_agent_runner-0.2.1/tests/unit/test_exit_cause.py +15 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_init_entry_points.py +3 -2
- cli_agent_runner-0.2.1/tests/unit/test_network_blip_signal_guard.py +47 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_presets.py +19 -3
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_vcs_state.py +27 -4
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.gitignore +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/LICENSE +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/_emit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/build.sh +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/README.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/commands.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/events.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.40.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/migrations/0.1.42.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/docs/thesis.md +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/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.1}/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.1}/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.1}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.1}/tests/unit/test_substrate.py +0 -0
|
@@ -5,6 +5,26 @@ 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.1] - 2026-07-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `[agent] prompt_delivery = "argv" | "stdin"` (default `argv`). `stdin` pipes the prompt to the agent's stdin instead of placing it in argv, closing a `pkill -f <token>` self-kill footgun (a background worker matching on a token from the prompt could match the agent's own argv). The `claude` preset now defaults to `stdin`; existing configs are unaffected.
|
|
12
|
+
- `agent_exit` gains `exit_cause` (`clean` / `error` / `signal:<NAME>` / `timeout`) so monitors can distinguish a clean exit from a signal-kill without a heuristic.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- A signal-killed round is no longer misclassified as `agent_network_blip` when its log happens to contain a network-error string; signal deaths are terminations, not network blips.
|
|
16
|
+
|
|
17
|
+
See `docs/migrations/0.2.1.md`.
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2026-07-02
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- 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.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `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`.
|
|
26
|
+
- `dirty_auto_committed` event (emitted when a handler auto-commits).
|
|
27
|
+
|
|
8
28
|
## [0.1.42] - 2026-06-25
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -866,7 +886,8 @@ Initial public release on PyPI as `cli-agent-runner`.
|
|
|
866
886
|
- Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
|
|
867
887
|
gated by a manual approval on the `pypi` GitHub environment.
|
|
868
888
|
|
|
869
|
-
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.
|
|
889
|
+
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.0...HEAD
|
|
890
|
+
[0.2.0]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.42...v0.2.0
|
|
870
891
|
[0.1.26]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.25...v0.1.26
|
|
871
892
|
[0.1.25]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.24...v0.1.25
|
|
872
893
|
[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.1
|
|
3
|
+
Version: 0.2.1
|
|
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.1
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -14,6 +14,7 @@ import os
|
|
|
14
14
|
import re
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess # noqa: TID251 — sanctioned subprocess caller
|
|
17
|
+
import threading
|
|
17
18
|
import time
|
|
18
19
|
from collections.abc import Callable
|
|
19
20
|
from dataclasses import dataclass, field
|
|
@@ -24,6 +25,25 @@ import psutil
|
|
|
24
25
|
REAP_GRACE_S = 5
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
def signal_name(exit_code: int) -> str | None:
|
|
29
|
+
"""Signal name for a signal death, else None.
|
|
30
|
+
|
|
31
|
+
Handles both the Python-negative form (``-15``) and the shell ``128+N``
|
|
32
|
+
form (``143``). Returns None for a non-signal exit — including a legitimate
|
|
33
|
+
``> 128`` code (e.g. 200) that is not a valid signal number.
|
|
34
|
+
"""
|
|
35
|
+
if exit_code < 0:
|
|
36
|
+
n = -exit_code
|
|
37
|
+
elif exit_code > 128:
|
|
38
|
+
n = exit_code - 128
|
|
39
|
+
else:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
return signal.Signals(n).name
|
|
43
|
+
except ValueError:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
27
47
|
@dataclass(frozen=True)
|
|
28
48
|
class RunResult:
|
|
29
49
|
exit_code: int
|
|
@@ -116,6 +136,7 @@ def run(
|
|
|
116
136
|
command: list[str],
|
|
117
137
|
prompt_arg_template: list[str],
|
|
118
138
|
prompt: str,
|
|
139
|
+
prompt_delivery: str = "argv",
|
|
119
140
|
timeout_s: int,
|
|
120
141
|
log_path: Path,
|
|
121
142
|
env_extra: dict[str, str],
|
|
@@ -145,7 +166,16 @@ def run(
|
|
|
145
166
|
matching any pattern (re.search) are excluded from the liveness count
|
|
146
167
|
(persistent helpers that aren't real workers). None = no filtering.
|
|
147
168
|
"""
|
|
148
|
-
|
|
169
|
+
stdin_mode = prompt_delivery == "stdin"
|
|
170
|
+
# Defense-in-depth: config validation already rejects {prompt} in the
|
|
171
|
+
# template for stdin mode, but run() must be safe even if called
|
|
172
|
+
# directly with a mismatched template. In stdin mode, never substitute
|
|
173
|
+
# {prompt} into argv — build it verbatim so the prompt cannot reach argv.
|
|
174
|
+
argv = (
|
|
175
|
+
list(command) + list(prompt_arg_template)
|
|
176
|
+
if stdin_mode
|
|
177
|
+
else _build_argv(command, prompt_arg_template, prompt)
|
|
178
|
+
)
|
|
149
179
|
env = {**os.environ, **env_extra}
|
|
150
180
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
151
181
|
log_file = log_path.open("w", encoding="utf-8")
|
|
@@ -154,11 +184,26 @@ def run(
|
|
|
154
184
|
proc = subprocess.Popen(
|
|
155
185
|
argv,
|
|
156
186
|
env=env,
|
|
157
|
-
stdin=subprocess.DEVNULL,
|
|
187
|
+
stdin=subprocess.PIPE if stdin_mode else subprocess.DEVNULL,
|
|
158
188
|
stdout=log_file,
|
|
159
189
|
stderr=subprocess.STDOUT,
|
|
160
190
|
start_new_session=True,
|
|
161
191
|
)
|
|
192
|
+
if stdin_mode and proc.stdin is not None:
|
|
193
|
+
# Write+close on a daemon thread so the wall-clock timeout loop below
|
|
194
|
+
# is never blocked by a write >64KB (OS pipe buffer) to an agent that
|
|
195
|
+
# doesn't drain stdin (R1128: no unbounded-hang path).
|
|
196
|
+
stdin_pipe = proc.stdin
|
|
197
|
+
stdin_data = prompt.encode("utf-8")
|
|
198
|
+
|
|
199
|
+
def _write_stdin():
|
|
200
|
+
try:
|
|
201
|
+
stdin_pipe.write(stdin_data)
|
|
202
|
+
stdin_pipe.close()
|
|
203
|
+
except (BrokenPipeError, OSError):
|
|
204
|
+
pass # agent exited before reading stdin; the poll loop handles exit
|
|
205
|
+
|
|
206
|
+
threading.Thread(target=_write_stdin, daemon=True).start()
|
|
162
207
|
result_seen_at: float | None = None
|
|
163
208
|
grace_extended_emitted = False
|
|
164
209
|
try:
|
|
@@ -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())
|
|
@@ -11,6 +11,7 @@ from typing import Any, Literal
|
|
|
11
11
|
_VALID_INJECTION_MODES: frozenset[str] = frozenset({"prepend", "file", "none"})
|
|
12
12
|
_VALID_DIRTY_ACTIONS: frozenset[str] = frozenset({"stash", "ignore", "auto_commit"})
|
|
13
13
|
_VALID_TRANSIENT_ERROR_ACTIONS: frozenset[str] = frozenset({"back_off", "skip", "stop"})
|
|
14
|
+
_VALID_PROMPT_DELIVERY: frozenset[str] = frozenset({"argv", "stdin"})
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ConfigError(ValueError):
|
|
@@ -23,6 +24,7 @@ class AgentConfig:
|
|
|
23
24
|
prompt_arg_template: list[str]
|
|
24
25
|
name: str | None = None
|
|
25
26
|
env: dict[str, str] = field(default_factory=dict)
|
|
27
|
+
prompt_delivery: Literal["argv", "stdin"] = "argv"
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@dataclass(frozen=True)
|
|
@@ -338,11 +340,23 @@ def load_config(toml_path: Path) -> Config:
|
|
|
338
340
|
raw = tomllib.load(f)
|
|
339
341
|
|
|
340
342
|
agent_d = raw.get("agent", {})
|
|
343
|
+
prompt_delivery = str(agent_d.get("prompt_delivery", "argv"))
|
|
344
|
+
prompt_arg_template = list(_require(agent_d, "prompt_arg_template"))
|
|
345
|
+
if prompt_delivery not in _VALID_PROMPT_DELIVERY:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f'invalid [agent] prompt_delivery {prompt_delivery!r}: use "argv" or "stdin"'
|
|
348
|
+
)
|
|
349
|
+
if prompt_delivery == "stdin" and any("{prompt}" in a for a in prompt_arg_template):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
"stdin delivery: remove {prompt} from [agent] prompt_arg_template "
|
|
352
|
+
"(the prompt is piped to stdin, not placed in argv)"
|
|
353
|
+
)
|
|
341
354
|
agent = AgentConfig(
|
|
342
355
|
command=list(_require(agent_d, "command")),
|
|
343
|
-
prompt_arg_template=
|
|
356
|
+
prompt_arg_template=prompt_arg_template,
|
|
344
357
|
name=agent_d.get("name"),
|
|
345
358
|
env={str(k): str(v) for k, v in agent_d.get("env", {}).items()},
|
|
359
|
+
prompt_delivery=prompt_delivery,
|
|
346
360
|
)
|
|
347
361
|
raw_work_dir = str(_require(raw, "runtime", "work_dir"))
|
|
348
362
|
work_dir = _expand_path(raw_work_dir, "").resolve()
|
|
@@ -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
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
command = ["claude", "--model", "claude-opus-4-7",
|
|
5
5
|
"--dangerously-skip-permissions",
|
|
6
6
|
"--verbose", "--output-format", "stream-json"]
|
|
7
|
-
prompt_arg_template = ["-p"
|
|
7
|
+
prompt_arg_template = ["-p"]
|
|
8
|
+
prompt_delivery = "stdin"
|
|
8
9
|
name = "claude"
|
|
9
10
|
|
|
10
11
|
[agent.env]
|
|
@@ -28,6 +28,7 @@ from agent_runner import (
|
|
|
28
28
|
startup_check,
|
|
29
29
|
vcs_state,
|
|
30
30
|
)
|
|
31
|
+
from agent_runner.agent_runtime import signal_name
|
|
31
32
|
from agent_runner.api import _primary_prompt_file, resolve_runtime_for_phase
|
|
32
33
|
from agent_runner.api import assemble_prompt as _api_assemble_prompt
|
|
33
34
|
from agent_runner.api_types import RoundResult, TransientErrorState
|
|
@@ -217,6 +218,20 @@ def _round_context_for_prompt(
|
|
|
217
218
|
return ctx
|
|
218
219
|
|
|
219
220
|
|
|
221
|
+
def _exit_cause(result: agent_runtime.RunResult) -> str:
|
|
222
|
+
"""Classify a round's exit for the ``agent_exit`` event.
|
|
223
|
+
|
|
224
|
+
Precedence: timeout > signal:<NAME> > clean > error. Timeout wins even
|
|
225
|
+
when agent-runner signal-killed the process to enforce the timeout.
|
|
226
|
+
"""
|
|
227
|
+
if result.timed_out:
|
|
228
|
+
return "timeout"
|
|
229
|
+
s = signal_name(result.exit_code)
|
|
230
|
+
if s is not None:
|
|
231
|
+
return f"signal:{s}"
|
|
232
|
+
return "clean" if result.exit_code == 0 else "error"
|
|
233
|
+
|
|
234
|
+
|
|
220
235
|
def _scan_round_log_for_network_blip(
|
|
221
236
|
*,
|
|
222
237
|
log_dir: Path,
|
|
@@ -233,6 +248,8 @@ def _scan_round_log_for_network_blip(
|
|
|
233
248
|
# Skip the I/O on the success path.
|
|
234
249
|
if result.exit_code == 0 and not result.timed_out:
|
|
235
250
|
return
|
|
251
|
+
if signal_name(result.exit_code) is not None:
|
|
252
|
+
return # signal death is a termination, not a network blip
|
|
236
253
|
try:
|
|
237
254
|
text = log_path.read_text(encoding="utf-8", errors="replace")
|
|
238
255
|
except FileNotFoundError:
|
|
@@ -423,6 +440,10 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
423
440
|
dry_run=cfg.runtime.dry_run,
|
|
424
441
|
anomaly_repetitive_window=cfg.monitor.anomaly_repetitive_window,
|
|
425
442
|
anomaly_repetitive_threshold=cfg.monitor.anomaly_repetitive_threshold,
|
|
443
|
+
vcs=hooks.VcsHookView(
|
|
444
|
+
dirty_action=cfg.vcs.dirty_action,
|
|
445
|
+
stash_idempotency_s=cfg.vcs.stash_idempotency_s,
|
|
446
|
+
),
|
|
426
447
|
)
|
|
427
448
|
_run_pre_round_hooks(
|
|
428
449
|
hook_ctx,
|
|
@@ -482,6 +503,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
482
503
|
command=cfg.agent.command,
|
|
483
504
|
prompt_arg_template=cfg.agent.prompt_arg_template,
|
|
484
505
|
prompt=prompt,
|
|
506
|
+
prompt_delivery=cfg.agent.prompt_delivery,
|
|
485
507
|
timeout_s=timeout_s,
|
|
486
508
|
log_path=log_path,
|
|
487
509
|
env_extra={**framework_env, **dict(cfg.agent.env)},
|
|
@@ -498,6 +520,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
498
520
|
exit_code=result.exit_code,
|
|
499
521
|
duration_s=result.duration_s,
|
|
500
522
|
timed_out=result.timed_out,
|
|
523
|
+
exit_cause=_exit_cause(result),
|
|
501
524
|
)
|
|
502
525
|
|
|
503
526
|
_scan_round_log_for_network_blip(
|
|
@@ -512,54 +535,12 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
512
535
|
if dirty:
|
|
513
536
|
events.emit(log_dir, "dirty_detected", round_num=round_num, files=dirty[:20])
|
|
514
537
|
|
|
515
|
-
|
|
516
|
-
action = cfg.vcs.dirty_action
|
|
538
|
+
dirty_outcome = None
|
|
517
539
|
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
|
-
)
|
|
540
|
+
dirty_outcome = hooks.dispatch_dirty(hook_ctx, dirty, log_dir=log_dir)
|
|
561
541
|
elif not dirty:
|
|
562
542
|
context_store.clear_orphan_state(log_dir)
|
|
543
|
+
stashed = bool(dirty_outcome and dirty_outcome.kind == "stashed")
|
|
563
544
|
|
|
564
545
|
if result.killed_for_grace:
|
|
565
546
|
api.emit_round_grace_kill(
|
|
@@ -609,6 +590,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
609
590
|
log_path=log_path,
|
|
610
591
|
dirty_files=dirty,
|
|
611
592
|
stashed=stashed,
|
|
593
|
+
dirty_outcome=dirty_outcome,
|
|
612
594
|
killed_for_grace=result.killed_for_grace,
|
|
613
595
|
)
|
|
614
596
|
_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()
|