cli-agent-runner 0.1.36__tar.gz → 0.1.38__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.36 → cli_agent_runner-0.1.38}/CHANGELOG.md +23 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/PKG-INFO +1 -1
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_emit.py +35 -6
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/agent_runtime.py +57 -15
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/api.py +1 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/upgrade_cmd.py +147 -20
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/events.py +2 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/runner.py +10 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/architecture.md +2 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/commands.md +19 -1
- cli_agent_runner-0.1.38/docs/migrations/0.1.37.md +65 -0
- cli_agent_runner-0.1.38/docs/migrations/0.1.38.md +53 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/runbook.md +50 -23
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_grace_kill_emission.py +33 -1
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_catalogs.py +14 -0
- cli_agent_runner-0.1.38/tests/unit/test_agent_runtime_grace.py +159 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_upgrade.py +235 -0
- cli_agent_runner-0.1.36/tests/unit/test_agent_runtime_grace.py +0 -72
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.gitignore +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/LICENSE +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/README.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/README.zh.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/build.sh +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/README.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/configuration.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/events.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/plugins.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/thesis.md +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_vcs_state.py +0 -0
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.38] - 2026-05-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Grace-kill (`max_grace_after_result_s`) no longer reaps a round that emitted `type=result` while a backgrounded child process (e.g. a long build) is still running. It now reaps only when the agent's process group has no live worker processes left (a genuine hang); otherwise it waits for the round to finish or for the `round_timeout_s` ceiling.
|
|
14
|
+
- Corrected `round_grace_kill`'s description: the kill is gated on the process group being idle (no live workers), not on log silence.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- New event `round_grace_extended` — emitted once when grace elapsed after `type=result` but a live worker process kept the round busy; carries the worker cmdlines.
|
|
18
|
+
- `round_grace_kill` now carries `live_children` (cmdlines observed at kill time; empty for a genuine idle hang).
|
|
19
|
+
|
|
20
|
+
## [0.1.37] - 2026-05-22
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- `upgrade` no longer crashes when run from a directory without `agent-runner.toml` — it upgrades the package and falls back to package-only mode.
|
|
24
|
+
- `upgrade` handles PEP 668 externally-managed environments (Debian 12 etc.): retries pip with `--break-system-packages` (and `--user` for user-site installs) when not in a venv.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `upgrade` only stop/start-orchestrates the `systemd --user` service it installed. For a self-managed service (e.g. a systemd system unit) it does package-only upgrade + smoke and prints the restart command to run yourself — no more silent no-op, and no more `agent-runner start` suggestion (which could spawn a conflicting second supervisor).
|
|
28
|
+
- New `--no-restart` flag forces package-only upgrade.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- New event `package_upgraded` (on-disk package changed; restart deferred to the operator), distinct from `service_upgraded` (the live service is now on the new version).
|
|
32
|
+
|
|
10
33
|
## [0.1.36] - 2026-05-21
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.38
|
|
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
|
|
@@ -19,6 +19,7 @@ __all__ = [
|
|
|
19
19
|
"emit_fresh_eyes_round_triggered",
|
|
20
20
|
"emit_max_rounds_reached",
|
|
21
21
|
"emit_rate_limit_stop",
|
|
22
|
+
"emit_round_grace_extended",
|
|
22
23
|
"emit_round_grace_kill",
|
|
23
24
|
"emit_round_progress",
|
|
24
25
|
"emit_round_substrate_after",
|
|
@@ -233,16 +234,44 @@ def emit_round_grace_kill(
|
|
|
233
234
|
*,
|
|
234
235
|
round_num: int,
|
|
235
236
|
grace_s: int,
|
|
237
|
+
live_children: list[str] | None = None,
|
|
236
238
|
) -> None:
|
|
237
|
-
"""Emit when subprocess killed because grace-after-result timer
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(wall-clock exceeded without result event).
|
|
239
|
+
"""Emit when the subprocess was killed because the grace-after-result timer
|
|
240
|
+
expired AND the agent's process group had no live worker processes left
|
|
241
|
+
(a genuine hang). Distinct from round_grace_extended (grace elapsed but a
|
|
242
|
+
worker was still running) and round_timeout_kill (wall-clock exceeded).
|
|
242
243
|
"""
|
|
243
244
|
from agent_runner.events import ROUND_GRACE_KILL, emit
|
|
244
245
|
|
|
245
|
-
emit(
|
|
246
|
+
emit(
|
|
247
|
+
log_dir,
|
|
248
|
+
ROUND_GRACE_KILL,
|
|
249
|
+
round_num=round_num,
|
|
250
|
+
grace_s=grace_s,
|
|
251
|
+
live_children=live_children or [],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def emit_round_grace_extended(
|
|
256
|
+
log_dir: Path,
|
|
257
|
+
*,
|
|
258
|
+
round_num: int,
|
|
259
|
+
grace_s: int,
|
|
260
|
+
live_children: list[str],
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Emit when the grace-after-result timer expired but the agent still had
|
|
263
|
+
live worker processes (e.g. a backgrounded build), so the round was NOT
|
|
264
|
+
killed; it continues until it finishes or hits round_timeout_s.
|
|
265
|
+
"""
|
|
266
|
+
from agent_runner.events import ROUND_GRACE_EXTENDED, emit
|
|
267
|
+
|
|
268
|
+
emit(
|
|
269
|
+
log_dir,
|
|
270
|
+
ROUND_GRACE_EXTENDED,
|
|
271
|
+
round_num=round_num,
|
|
272
|
+
grace_s=grace_s,
|
|
273
|
+
live_children=live_children,
|
|
274
|
+
)
|
|
246
275
|
|
|
247
276
|
|
|
248
277
|
def emit_anomaly_repetitive_tool(
|
|
@@ -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, 1,
|
|
21
|
+
__version__ = version = '0.1.38'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 38)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -15,9 +15,11 @@ import signal
|
|
|
15
15
|
import subprocess # noqa: TID251 — sanctioned subprocess caller
|
|
16
16
|
import time
|
|
17
17
|
from collections.abc import Callable
|
|
18
|
-
from dataclasses import dataclass
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
import psutil
|
|
22
|
+
|
|
21
23
|
REAP_GRACE_S = 5
|
|
22
24
|
|
|
23
25
|
|
|
@@ -28,6 +30,7 @@ class RunResult:
|
|
|
28
30
|
timed_out: bool
|
|
29
31
|
pid: int
|
|
30
32
|
killed_for_grace: bool = False
|
|
33
|
+
grace_kill_children: list[str] = field(default_factory=list)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
|
|
@@ -54,6 +57,31 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
|
|
|
54
57
|
pass
|
|
55
58
|
|
|
56
59
|
|
|
60
|
+
def _live_children(proc: subprocess.Popen, *, max_n: int = 5, max_len: int = 120) -> list[str]:
|
|
61
|
+
"""Cmdlines of live (non-zombie) descendant processes of ``proc``.
|
|
62
|
+
|
|
63
|
+
Empty when ``proc`` has no live workers (a stuck agent that emitted
|
|
64
|
+
type=result then hung). Non-empty when the round backgrounded work (e.g. a
|
|
65
|
+
build) still running. Bounded so the resulting event stays small.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
parent = psutil.Process(proc.pid)
|
|
69
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
70
|
+
return []
|
|
71
|
+
out: list[str] = []
|
|
72
|
+
for child in parent.children(recursive=True):
|
|
73
|
+
try:
|
|
74
|
+
if child.status() == psutil.STATUS_ZOMBIE:
|
|
75
|
+
continue
|
|
76
|
+
line = " ".join(child.cmdline()) or child.name()
|
|
77
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
78
|
+
continue
|
|
79
|
+
out.append(line[:max_len])
|
|
80
|
+
if len(out) >= max_n:
|
|
81
|
+
break
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
57
85
|
# Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
|
|
58
86
|
# A future CLI variant emitting `{"type": "result", ...}` (with space) would
|
|
59
87
|
# bypass this scan; revisit if that happens.
|
|
@@ -71,14 +99,18 @@ def run(
|
|
|
71
99
|
max_grace_after_result_s: int = 0,
|
|
72
100
|
progress_callback: Callable[[dict], None] | None = None,
|
|
73
101
|
progress_interval_s: int = 0,
|
|
102
|
+
on_grace_extended: Callable[[list[str]], None] | None = None,
|
|
74
103
|
) -> RunResult:
|
|
75
104
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
76
105
|
|
|
77
106
|
Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
|
|
78
107
|
|
|
79
108
|
max_grace_after_result_s: when > 0, start a countdown after the first
|
|
80
|
-
type=result event is detected in the log
|
|
81
|
-
|
|
109
|
+
type=result event is detected in the log. After it elapses, reap the
|
|
110
|
+
process group only if the agent has no live worker processes left (a
|
|
111
|
+
genuine hang). If a worker is still running (e.g. a backgrounded build),
|
|
112
|
+
do not reap — invoke ``on_grace_extended`` once and keep waiting until the
|
|
113
|
+
round finishes or hits the wall-clock ``timeout_s`` ceiling. 0 = disabled.
|
|
82
114
|
|
|
83
115
|
progress_callback: when not None and progress_interval_s > 0, called every
|
|
84
116
|
progress_interval_s seconds with a dict of log stats (log_size_kb,
|
|
@@ -100,6 +132,7 @@ def run(
|
|
|
100
132
|
start_new_session=True,
|
|
101
133
|
)
|
|
102
134
|
result_seen_at: float | None = None
|
|
135
|
+
grace_extended_emitted = False
|
|
103
136
|
try:
|
|
104
137
|
while True:
|
|
105
138
|
ret = proc.poll()
|
|
@@ -114,10 +147,9 @@ def run(
|
|
|
114
147
|
return RunResult(
|
|
115
148
|
exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
|
|
116
149
|
)
|
|
117
|
-
# Grace kill: result emitted but subprocess still running
|
|
150
|
+
# Grace kill: result emitted but subprocess still running.
|
|
118
151
|
if max_grace_after_result_s > 0:
|
|
119
152
|
if result_seen_at is None:
|
|
120
|
-
# Cheap check: byte-scan log for marker substring
|
|
121
153
|
try:
|
|
122
154
|
with log_path.open("rb") as f:
|
|
123
155
|
if _RESULT_MARKER in f.read():
|
|
@@ -125,16 +157,26 @@ def run(
|
|
|
125
157
|
except OSError:
|
|
126
158
|
pass # log not flushed yet; check next tick
|
|
127
159
|
if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
children = _live_children(proc)
|
|
161
|
+
if children:
|
|
162
|
+
# Busy: a backgrounded worker is still running. Don't
|
|
163
|
+
# reap — defer to the wall-clock ceiling. Signal once.
|
|
164
|
+
if not grace_extended_emitted:
|
|
165
|
+
if on_grace_extended is not None:
|
|
166
|
+
on_grace_extended(children)
|
|
167
|
+
grace_extended_emitted = True
|
|
168
|
+
else:
|
|
169
|
+
_kill_pgroup(proc)
|
|
170
|
+
duration = time.time() - start
|
|
171
|
+
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
172
|
+
return RunResult(
|
|
173
|
+
exit_code=exit_code,
|
|
174
|
+
duration_s=duration,
|
|
175
|
+
timed_out=True,
|
|
176
|
+
pid=proc.pid,
|
|
177
|
+
killed_for_grace=True,
|
|
178
|
+
grace_kill_children=[],
|
|
179
|
+
)
|
|
138
180
|
# Progress heartbeat: call back if interval elapsed
|
|
139
181
|
if progress_callback is not None and progress_interval_s > 0:
|
|
140
182
|
if now - last_progress_at >= progress_interval_s:
|
|
@@ -733,6 +733,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
733
733
|
emit_fresh_eyes_round_triggered,
|
|
734
734
|
emit_max_rounds_reached,
|
|
735
735
|
emit_rate_limit_stop,
|
|
736
|
+
emit_round_grace_extended,
|
|
736
737
|
emit_round_grace_kill,
|
|
737
738
|
emit_round_progress,
|
|
738
739
|
emit_round_substrate_after,
|
|
@@ -18,7 +18,9 @@ import sys
|
|
|
18
18
|
import time
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
import agent_runner
|
|
21
22
|
from agent_runner import __version__, api, events
|
|
23
|
+
from agent_runner.api_types import ServiceMode
|
|
22
24
|
from agent_runner.cli.common import cfg_from_args, fail, info
|
|
23
25
|
from agent_runner.config import Config
|
|
24
26
|
|
|
@@ -28,8 +30,8 @@ def add_parser(sub, parent) -> None:
|
|
|
28
30
|
"upgrade",
|
|
29
31
|
parents=[parent],
|
|
30
32
|
help=(
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
+
"Package upgrade with service-mode gate: orchestrated stop/start"
|
|
34
|
+
" for systemd --user; package-only otherwise"
|
|
33
35
|
),
|
|
34
36
|
)
|
|
35
37
|
p.add_argument(
|
|
@@ -40,24 +42,69 @@ def add_parser(sub, parent) -> None:
|
|
|
40
42
|
help="Pin a specific version (e.g. 0.1.13). Default: latest from PyPI. "
|
|
41
43
|
"Use to roll back: `--target <previous-version>`.",
|
|
42
44
|
)
|
|
45
|
+
p.add_argument(
|
|
46
|
+
"--no-restart",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Upgrade the package + smoke only; do not stop/start the service "
|
|
49
|
+
"(you restart it yourself).",
|
|
50
|
+
)
|
|
43
51
|
p.set_defaults(func=cmd)
|
|
44
52
|
|
|
45
53
|
|
|
46
54
|
def cmd(args) -> int:
|
|
47
|
-
cfg =
|
|
48
|
-
return _run_upgrade(
|
|
55
|
+
cfg = _try_load_cfg(args)
|
|
56
|
+
return _run_upgrade(
|
|
57
|
+
cfg,
|
|
58
|
+
target=args.target,
|
|
59
|
+
cfg_path=args.config,
|
|
60
|
+
no_restart=getattr(args, "no_restart", False),
|
|
61
|
+
)
|
|
49
62
|
|
|
50
63
|
|
|
51
|
-
def
|
|
52
|
-
"""
|
|
64
|
+
def _try_load_cfg(args) -> Config | None:
|
|
65
|
+
"""Load the project config if present; None when absent (package-only)."""
|
|
66
|
+
try:
|
|
67
|
+
return cfg_from_args(args)
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _pip_env_flags() -> list[str]:
|
|
73
|
+
"""Extra pip flags for the current install under PEP 668.
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
Inside a venv: none (pip is unrestricted). Otherwise (system/user
|
|
76
|
+
interpreter on an externally-managed distro) the caller retries with these.
|
|
77
|
+
``--user`` is added only when agent_runner lives in user-site, matching
|
|
78
|
+
where the existing install actually is.
|
|
79
|
+
"""
|
|
80
|
+
import sys
|
|
81
|
+
|
|
82
|
+
if sys.prefix != sys.base_prefix: # inside a venv → no PEP 668
|
|
83
|
+
return []
|
|
84
|
+
import site
|
|
85
|
+
|
|
86
|
+
flags = ["--break-system-packages"]
|
|
87
|
+
user_site = site.getusersitepackages()
|
|
88
|
+
if str(Path(agent_runner.__file__)).startswith(str(Path(user_site))):
|
|
89
|
+
flags.insert(0, "--user")
|
|
90
|
+
return flags
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _pip_install(spec: str, *, force_reinstall: bool = False) -> subprocess.CompletedProcess:
|
|
94
|
+
"""pip install --upgrade <spec>, retrying once with PEP668 flags on an
|
|
95
|
+
externally-managed environment. Returns CompletedProcess (rc check by caller).
|
|
56
96
|
"""
|
|
57
|
-
|
|
97
|
+
base = [sys.executable, "-m", "pip", "install", "--upgrade", spec]
|
|
58
98
|
if force_reinstall:
|
|
59
|
-
|
|
60
|
-
|
|
99
|
+
base.insert(4, "--force-reinstall")
|
|
100
|
+
r = subprocess.run(base, capture_output=True, text=True, check=False)
|
|
101
|
+
if r.returncode == 0 or "externally-managed-environment" not in (r.stderr or ""):
|
|
102
|
+
return r
|
|
103
|
+
extra = _pip_env_flags()
|
|
104
|
+
if not extra:
|
|
105
|
+
return r
|
|
106
|
+
info(f"externally-managed env detected; retrying pip with {' '.join(extra)}")
|
|
107
|
+
return subprocess.run(base + extra, capture_output=True, text=True, check=False)
|
|
61
108
|
|
|
62
109
|
|
|
63
110
|
def _smoke_version() -> tuple[int, str]:
|
|
@@ -93,18 +140,40 @@ def _smoke_peek(cfg_path: Path) -> tuple[int, str]:
|
|
|
93
140
|
return 0, ""
|
|
94
141
|
|
|
95
142
|
|
|
96
|
-
def _run_upgrade(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
143
|
+
def _run_upgrade(
|
|
144
|
+
cfg: Config | None,
|
|
145
|
+
*,
|
|
146
|
+
target: str | None,
|
|
147
|
+
cfg_path: Path,
|
|
148
|
+
no_restart: bool = False,
|
|
149
|
+
) -> int:
|
|
150
|
+
"""Dispatch: full orchestration for the systemd --user service we installed;
|
|
151
|
+
package-only everywhere else."""
|
|
101
152
|
if target is not None and not target.strip():
|
|
102
153
|
return fail("--target must be a non-empty version string (e.g. 0.1.13)")
|
|
154
|
+
from_version = __version__
|
|
155
|
+
if _orchestrate_capable(cfg, no_restart):
|
|
156
|
+
return _orchestrated_upgrade(
|
|
157
|
+
cfg, target=target, cfg_path=cfg_path, from_version=from_version
|
|
158
|
+
)
|
|
159
|
+
return _package_only_upgrade(cfg, target=target, from_version=from_version)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _orchestrate_capable(cfg: Config | None, no_restart: bool) -> bool:
|
|
163
|
+
if cfg is None or no_restart:
|
|
164
|
+
return False
|
|
165
|
+
pname = api._resolve_project(cfg.runtime.work_dir)
|
|
166
|
+
return api.detect_service_mode(pname, log_dir=cfg.runtime.log_dir) == ServiceMode.SYSTEMD_USER
|
|
103
167
|
|
|
168
|
+
|
|
169
|
+
def _orchestrated_upgrade(
|
|
170
|
+
cfg: Config, *, target: str | None, cfg_path: Path, from_version: str
|
|
171
|
+
) -> int:
|
|
172
|
+
"""Full stop → pip → smoke(--version + peek) → start → emit service_upgraded,
|
|
173
|
+
with auto-rollback on smoke failure. Only reached for the systemd --user
|
|
174
|
+
service agent-runner installed (api.start works there)."""
|
|
104
175
|
log_dir = cfg.runtime.log_dir
|
|
105
176
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
-
|
|
107
|
-
from_version = __version__
|
|
108
177
|
t0 = time.monotonic()
|
|
109
178
|
|
|
110
179
|
info("stopping service...")
|
|
@@ -155,14 +224,13 @@ def _run_upgrade(cfg: Config, *, target: str | None, cfg_path: Path) -> int:
|
|
|
155
224
|
started_at=t0,
|
|
156
225
|
cfg_path=cfg_path,
|
|
157
226
|
)
|
|
158
|
-
|
|
159
227
|
info(f"smoke OK (now at {to_version})")
|
|
160
228
|
|
|
161
229
|
info("starting service...")
|
|
162
230
|
t_start = time.monotonic()
|
|
163
231
|
try:
|
|
164
232
|
api.start(cfg.runtime.work_dir)
|
|
165
|
-
except Exception as e: # noqa: BLE001 — new version installed but service stopped
|
|
233
|
+
except Exception as e: # noqa: BLE001 — new version installed but service stopped
|
|
166
234
|
return _rollback_failed(
|
|
167
235
|
log_dir,
|
|
168
236
|
to_version,
|
|
@@ -183,6 +251,65 @@ def _run_upgrade(cfg: Config, *, target: str | None, cfg_path: Path) -> int:
|
|
|
183
251
|
return 0
|
|
184
252
|
|
|
185
253
|
|
|
254
|
+
def _package_only_upgrade(cfg: Config | None, *, target: str | None, from_version: str) -> int:
|
|
255
|
+
"""Upgrade the on-disk package + smoke (--version), with pip-level rollback.
|
|
256
|
+
Never touches the service — the operator restarts it. Used for any deployment
|
|
257
|
+
not managed as a systemd --user service (system unit, foreground, none, no
|
|
258
|
+
config, or --no-restart)."""
|
|
259
|
+
spec = "cli-agent-runner" if target is None else f"cli-agent-runner=={target}"
|
|
260
|
+
info(f"package-only upgrade (service not managed by agent-runner); installing {spec}...")
|
|
261
|
+
pip_result = _pip_install(spec)
|
|
262
|
+
if pip_result.returncode != 0:
|
|
263
|
+
return fail(
|
|
264
|
+
f"pip install failed (rc={pip_result.returncode}): "
|
|
265
|
+
f"{pip_result.stderr.strip()[:200]}; "
|
|
266
|
+
f"package unchanged, your service keeps running the current version"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
rc_v, version_or_err = _smoke_version()
|
|
270
|
+
if rc_v != 0:
|
|
271
|
+
attempted = target or "latest"
|
|
272
|
+
info(f"smoke failed at {attempted} ({version_or_err}); reinstalling {from_version}...")
|
|
273
|
+
rb = _pip_install(f"cli-agent-runner=={from_version}", force_reinstall=True)
|
|
274
|
+
if rb.returncode != 0:
|
|
275
|
+
return fail(
|
|
276
|
+
f"package smoke failed AND rollback reinstall failed (rc={rb.returncode}): "
|
|
277
|
+
f"{rb.stderr.strip()[:200]}; run: "
|
|
278
|
+
f"pip install --force-reinstall cli-agent-runner=={from_version}"
|
|
279
|
+
)
|
|
280
|
+
return fail(
|
|
281
|
+
f"package smoke failed at {attempted}; reinstalled {from_version}; service untouched"
|
|
282
|
+
)
|
|
283
|
+
to_version = version_or_err
|
|
284
|
+
|
|
285
|
+
if cfg is not None:
|
|
286
|
+
log_dir = cfg.runtime.log_dir
|
|
287
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
events.emit(
|
|
289
|
+
log_dir,
|
|
290
|
+
events.PACKAGE_UPGRADED,
|
|
291
|
+
from_version=from_version,
|
|
292
|
+
to_version=to_version,
|
|
293
|
+
restart_deferred=True,
|
|
294
|
+
)
|
|
295
|
+
info(f"package upgraded {from_version} → {to_version}. Restart your supervisor to load it:")
|
|
296
|
+
info(_restart_hint(cfg))
|
|
297
|
+
return 0
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _restart_hint(cfg: Config | None) -> str:
|
|
301
|
+
"""Mode-correct restart command. Never suggests `agent-runner start`
|
|
302
|
+
(which would spawn a conflicting supervisor on a system-unit host)."""
|
|
303
|
+
if cfg is not None:
|
|
304
|
+
pname = api._resolve_project(cfg.runtime.work_dir)
|
|
305
|
+
if api.detect_service_mode(pname, log_dir=cfg.runtime.log_dir) == ServiceMode.SYSTEMD_USER:
|
|
306
|
+
return f" systemctl --user restart {api.serve_unit_filename(pname)}"
|
|
307
|
+
return (
|
|
308
|
+
" sudo systemctl restart <your-unit> # if run by a systemd system unit\n"
|
|
309
|
+
" (agent-runner can't know a service it didn't install; substitute your unit name)"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
186
313
|
def _rollback(
|
|
187
314
|
cfg: Config,
|
|
188
315
|
log_dir: Path,
|
|
@@ -46,8 +46,10 @@ MONITOR_STARTED = "monitor_started"
|
|
|
46
46
|
ORPHAN_IDEMPOTENT_SKIP = "orphan_idempotent_skip"
|
|
47
47
|
ORPHAN_STASH_FAILED = "orphan_stash_failed"
|
|
48
48
|
ORPHAN_STASHED = "orphan_stashed"
|
|
49
|
+
PACKAGE_UPGRADED = "package_upgraded"
|
|
49
50
|
PROMPT_OVERWRITTEN = "prompt_overwritten"
|
|
50
51
|
ROUND_END = "round_end"
|
|
52
|
+
ROUND_GRACE_EXTENDED = "round_grace_extended"
|
|
51
53
|
ROUND_GRACE_KILL = "round_grace_kill"
|
|
52
54
|
ROUND_PROGRESS = "round_progress"
|
|
53
55
|
ROUND_START = "round_start"
|
|
@@ -466,6 +466,14 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
466
466
|
**stats,
|
|
467
467
|
)
|
|
468
468
|
|
|
469
|
+
def _grace_extended_emit(children: list[str]) -> None:
|
|
470
|
+
api.emit_round_grace_extended(
|
|
471
|
+
log_dir,
|
|
472
|
+
round_num=round_num,
|
|
473
|
+
grace_s=cfg.runtime.max_grace_after_result_s,
|
|
474
|
+
live_children=children,
|
|
475
|
+
)
|
|
476
|
+
|
|
469
477
|
result = agent_runtime.run(
|
|
470
478
|
command=cfg.agent.command,
|
|
471
479
|
prompt_arg_template=cfg.agent.prompt_arg_template,
|
|
@@ -476,6 +484,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
476
484
|
max_grace_after_result_s=cfg.runtime.max_grace_after_result_s,
|
|
477
485
|
progress_callback=_progress_emit,
|
|
478
486
|
progress_interval_s=cfg.monitor.round_progress_interval_s,
|
|
487
|
+
on_grace_extended=_grace_extended_emit,
|
|
479
488
|
)
|
|
480
489
|
events.emit(
|
|
481
490
|
log_dir,
|
|
@@ -549,6 +558,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
549
558
|
log_dir,
|
|
550
559
|
round_num=round_num,
|
|
551
560
|
grace_s=cfg.runtime.max_grace_after_result_s,
|
|
561
|
+
live_children=result.grace_kill_children,
|
|
552
562
|
)
|
|
553
563
|
elif result.timed_out:
|
|
554
564
|
events.emit(
|
|
@@ -165,8 +165,10 @@ hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"
|
|
|
165
165
|
- `orphan_idempotent_skip`
|
|
166
166
|
- `orphan_stash_failed`
|
|
167
167
|
- `orphan_stashed`
|
|
168
|
+
- `package_upgraded`
|
|
168
169
|
- `prompt_overwritten`
|
|
169
170
|
- `round_end`
|
|
171
|
+
- `round_grace_extended`
|
|
170
172
|
- `round_grace_kill`
|
|
171
173
|
- `round_progress`
|
|
172
174
|
- `round_start`
|
|
@@ -24,7 +24,7 @@ are shared between `peek`, `watch`, and `monitor`.
|
|
|
24
24
|
| `monitor` | Anomaly detection, narrate/events stream, or HTTP progress page |
|
|
25
25
|
| `serve` | Long-running supervisor loop |
|
|
26
26
|
| `round` | Run one round and exit |
|
|
27
|
-
| `upgrade` |
|
|
27
|
+
| `upgrade` | Package upgrade with service-mode gate: orchestrated stop/start for systemd --user; package-only otherwise |
|
|
28
28
|
<!-- /gen:verb-table -->
|
|
29
29
|
|
|
30
30
|
## Lifecycle
|
|
@@ -76,6 +76,24 @@ Long-running supervisor loop. Traps SIGTERM (graceful stop), SIGINT (graceful),
|
|
|
76
76
|
SIGUSR1 (cancel — forwards SIGINT to current round). Writes `serve.pid` and
|
|
77
77
|
`round.pid`. `--once` runs a single round then exits (debug).
|
|
78
78
|
|
|
79
|
+
### `agent-runner upgrade [--target VERSION] [--no-restart] [--config PATH]`
|
|
80
|
+
|
|
81
|
+
Upgrade the agent-runner package. Behavior depends on the detected service mode:
|
|
82
|
+
|
|
83
|
+
- **systemd --user service** (installed via `agent-runner install`): full
|
|
84
|
+
orchestrated flow — stop → pip install → smoke (`--version` + `peek`) →
|
|
85
|
+
start → emit `service_upgraded`. Auto-rollback on smoke failure.
|
|
86
|
+
- **Anything else** (system unit, foreground, no config): package-only —
|
|
87
|
+
PEP 668-aware pip + `--version` smoke + pip-level rollback, emits
|
|
88
|
+
`package_upgraded`, prints the restart command. Never touches your running
|
|
89
|
+
service, never runs `sudo`.
|
|
90
|
+
|
|
91
|
+
`--config` is optional: when omitted (or the file is absent), `upgrade` falls
|
|
92
|
+
back to package-only mode automatically.
|
|
93
|
+
|
|
94
|
+
`--no-restart` forces package-only even on a systemd --user host (upgrade the
|
|
95
|
+
package now, restart your service yourself).
|
|
96
|
+
|
|
79
97
|
## Observation
|
|
80
98
|
|
|
81
99
|
### `agent-runner peek [flags]`
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Migrating to 0.1.37
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install --upgrade cli-agent-runner==0.1.37
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
If you run agent-runner as a `systemd --user` service (the kind `agent-runner
|
|
10
|
+
install` creates), `agent-runner upgrade` works end-to-end as before. If you run
|
|
11
|
+
it any other way (a systemd **system** unit, a foreground process, etc.), see
|
|
12
|
+
"Upgrading a self-managed service" below.
|
|
13
|
+
|
|
14
|
+
## What was fixed
|
|
15
|
+
|
|
16
|
+
- `upgrade` no longer crashes when run from a directory without
|
|
17
|
+
`agent-runner.toml`.
|
|
18
|
+
- `upgrade` handles Debian 12 / PEP 668 `externally-managed-environment`:
|
|
19
|
+
outside a venv it retries pip with `--break-system-packages` (plus `--user`
|
|
20
|
+
when the install lives in user-site).
|
|
21
|
+
- `upgrade` no longer silently no-ops on a non-user-managed service, and never
|
|
22
|
+
suggests `agent-runner start` (which would start a second, conflicting
|
|
23
|
+
supervisor next to your real one).
|
|
24
|
+
|
|
25
|
+
## How `upgrade` behaves now
|
|
26
|
+
|
|
27
|
+
- **systemd --user service** → full flow: stop → pip → smoke → start →
|
|
28
|
+
`service_upgraded`, with auto-rollback on smoke failure. Unchanged.
|
|
29
|
+
- **Anything else** (system unit / foreground / no config / `--no-restart`) →
|
|
30
|
+
**package-only**: PEP668-aware pip + `--version` smoke + pip-level rollback,
|
|
31
|
+
then it emits `package_upgraded` and prints the restart command for you to run.
|
|
32
|
+
It does not touch your running service.
|
|
33
|
+
|
|
34
|
+
## Upgrading a self-managed service (e.g. systemd system unit)
|
|
35
|
+
|
|
36
|
+
agent-runner never runs `sudo` and does not manage a unit it did not create.
|
|
37
|
+
The canonical recipe:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 1. Upgrade the package (PEP 668: --break-system-packages only touches ~/.local)
|
|
41
|
+
python3 -m pip install --user --break-system-packages --upgrade cli-agent-runner==0.1.37
|
|
42
|
+
# (or: agent-runner upgrade --target 0.1.37 --no-restart — does the pip + smoke for you)
|
|
43
|
+
|
|
44
|
+
# 2. Verify
|
|
45
|
+
agent-runner --version # → 0.1.37
|
|
46
|
+
python3 -c "import your_plugin" # if you use a plugin
|
|
47
|
+
|
|
48
|
+
# 3. Restart your supervisor so the long-running process loads the new code
|
|
49
|
+
sudo systemctl restart <your-unit>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Do **not** run `agent-runner start` on a system-unit host — it would spawn a
|
|
53
|
+
second supervisor alongside the one systemd manages.
|
|
54
|
+
|
|
55
|
+
## `package_upgraded` vs `service_upgraded`
|
|
56
|
+
|
|
57
|
+
- `service_upgraded` — the live service is now running the new version (emitted
|
|
58
|
+
only in the orchestrated systemd --user flow).
|
|
59
|
+
- `package_upgraded` — the on-disk package changed but the running supervisor
|
|
60
|
+
still runs the old code until you restart it (`restart_deferred: true`).
|
|
61
|
+
|
|
62
|
+
## What did NOT change
|
|
63
|
+
|
|
64
|
+
- The systemd --user happy path (the 0.1.13 flow) and its auto-rollback events.
|
|
65
|
+
- Smoke design (fresh-subprocess `--version`, plus `peek` in the orchestrated flow).
|