cli-agent-runner 0.1.35__tar.gz → 0.1.36__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.35 → cli_agent_runner-0.1.36}/CHANGELOG.md +9 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/PKG-INFO +5 -5
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/README.md +4 -4
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/README.zh.md +5 -4
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/api.py +1 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/config.py +14 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/monitor.py +43 -2
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/architecture.md +4 -2
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/commands.md +1 -1
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/configuration.md +2 -0
- cli_agent_runner-0.1.36/docs/migrations/0.1.36.md +73 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/runbook.md +23 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_architecture.py +2 -1
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_observation.py +15 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_service.py +22 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_config.py +23 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_docgen.py +2 -2
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_monitor_assembly.py +1 -0
- cli_agent_runner-0.1.36/tests/unit/test_monitor_detect_supervisor_stale.py +38 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_monitor_detectors.py +36 -1
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.gitignore +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/LICENSE +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/_emit.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/build.sh +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/README.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/events.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/plugins.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/docs/thesis.md +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/tests/unit/test_vcs_state.py +0 -0
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.36] - 2026-05-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- New monitor detector `supervisor_stale` (notify) — alerts when the supervisor stops emitting events (stuck between rounds or dead), a blind spot the event stream and `detect_hung` cannot catch. Default ON; threshold derives from `round_timeout_s * 1.5`. Detector count 11 → 12.
|
|
14
|
+
- `[monitor] supervisor_stale_threshold_s` config — override the derived staleness threshold (positive = seconds; 0 = disable; unset = derived).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `docs/runbook.md` documents the liveness-monitoring architecture: run `monitor --host` from a separate machine to detect supervisor silent-death AND host death (a same-host monitor dies with its host).
|
|
18
|
+
|
|
10
19
|
## [0.1.35] - 2026-05-20
|
|
11
20
|
|
|
12
21
|
### Removed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.36
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -49,7 +49,7 @@ full disks, runaway memory.
|
|
|
49
49
|
|
|
50
50
|
```
|
|
51
51
|
┌──────────────────────────────────────────┐
|
|
52
|
-
│ Layer 3: The Witness (monitor) │
|
|
52
|
+
│ Layer 3: The Witness (monitor) │ 12 detectors + auto-stop
|
|
53
53
|
├──────────────────────────────────────────┤
|
|
54
54
|
│ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
|
|
55
55
|
├──────────────────────────────────────────┤
|
|
@@ -86,7 +86,7 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
|
86
86
|
|---|---|
|
|
87
87
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
88
88
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
89
|
-
| `restart` / `status` | `monitor` —
|
|
89
|
+
| `restart` / `status` | `monitor` — 12 detectors, alerts, auto-stop |
|
|
90
90
|
| `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
|
|
91
91
|
|
|
92
92
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
@@ -106,11 +106,11 @@ guards it. Highlights:
|
|
|
106
106
|
|
|
107
107
|
Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
108
108
|
|
|
109
|
-
## Monitor:
|
|
109
|
+
## Monitor: 12 detectors
|
|
110
110
|
|
|
111
111
|
Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
|
|
112
112
|
`mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
|
|
113
|
-
`anomaly_repetitive_active`.
|
|
113
|
+
`anomaly_repetitive_active`, `supervisor_stale`.
|
|
114
114
|
|
|
115
115
|
**Auto-stop the service** (continuing is harmful):
|
|
116
116
|
- `oauth_fail` — burning API quota on auth-rejected rounds
|
|
@@ -12,7 +12,7 @@ full disks, runaway memory.
|
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
┌──────────────────────────────────────────┐
|
|
15
|
-
│ Layer 3: The Witness (monitor) │
|
|
15
|
+
│ Layer 3: The Witness (monitor) │ 12 detectors + auto-stop
|
|
16
16
|
├──────────────────────────────────────────┤
|
|
17
17
|
│ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
|
|
18
18
|
├──────────────────────────────────────────┤
|
|
@@ -49,7 +49,7 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
|
49
49
|
|---|---|
|
|
50
50
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
51
51
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
52
|
-
| `restart` / `status` | `monitor` —
|
|
52
|
+
| `restart` / `status` | `monitor` — 12 detectors, alerts, auto-stop |
|
|
53
53
|
| `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
|
|
54
54
|
|
|
55
55
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
@@ -69,11 +69,11 @@ guards it. Highlights:
|
|
|
69
69
|
|
|
70
70
|
Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
71
71
|
|
|
72
|
-
## Monitor:
|
|
72
|
+
## Monitor: 12 detectors
|
|
73
73
|
|
|
74
74
|
Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
|
|
75
75
|
`mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
|
|
76
|
-
`anomaly_repetitive_active`.
|
|
76
|
+
`anomaly_repetitive_active`, `supervisor_stale`.
|
|
77
77
|
|
|
78
78
|
**Auto-stop the service** (continuing is harmful):
|
|
79
79
|
- `oauth_fail` — burning API quota on auth-rejected rounds
|
|
@@ -20,7 +20,7 @@ supervisor 重启 —— 这是核心模式。中间穿插 11 条防御,避开
|
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
┌──────────────────────────────────────────┐
|
|
23
|
-
│ Layer 3:Witness(monitor) │
|
|
23
|
+
│ Layer 3:Witness(monitor) │ 12 个检测器 + 自动停服
|
|
24
24
|
├──────────────────────────────────────────┤
|
|
25
25
|
│ Layer 2:Loop(serve,~120 LOC 薄壳) │ 捕获信号,循环拉起 round
|
|
26
26
|
├──────────────────────────────────────────┤
|
|
@@ -63,7 +63,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
|
|
|
63
63
|
|---|---|
|
|
64
64
|
| `init` / `install` / `uninstall` | `peek` —— 项目状态快照 |
|
|
65
65
|
| `start` / `stop` / `kill` / `cancel` | `watch` —— peek 在刷新循环里 |
|
|
66
|
-
| `restart` / `status` | `monitor` ——
|
|
66
|
+
| `restart` / `status` | `monitor` —— 12 个检测器 + 告警 + 自动停服 |
|
|
67
67
|
| `round` / `serve` / `upgrade` | `events` —— 查询 / 流式订阅 events.jsonl |
|
|
68
68
|
|
|
69
69
|
**停服三动词**有清晰的语义分层:
|
|
@@ -95,11 +95,12 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
|
|
|
95
95
|
|
|
96
96
|
完整列表 + 历史出处:[`docs/architecture.md`](docs/architecture.md)。
|
|
97
97
|
|
|
98
|
-
## Monitor:
|
|
98
|
+
## Monitor:12 个检测器
|
|
99
99
|
|
|
100
100
|
**只告警**(warning 级,服务继续跑):
|
|
101
101
|
`timeout_rate` / `hung` / `orphan_chain` / `disk_warning` /
|
|
102
|
-
`mem_pressure` / `smoke_fail_rate` / `network_fail`
|
|
102
|
+
`mem_pressure` / `smoke_fail_rate` / `network_fail` / `rate_limit_active` /
|
|
103
|
+
`anomaly_repetitive_active` / `supervisor_stale`
|
|
103
104
|
|
|
104
105
|
**自动停服**(critical 级,继续是 net negative):
|
|
105
106
|
|
|
@@ -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.36'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 36)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -452,6 +452,7 @@ def _poll_once(project: str | Path, *, host: str | None) -> list[monitor.Alert]:
|
|
|
452
452
|
metrics=metrics,
|
|
453
453
|
log_tails=log_tails,
|
|
454
454
|
round_timeout_s=cfg.runtime.round_timeout_s,
|
|
455
|
+
supervisor_stale_threshold_s=cfg.monitor.supervisor_stale_threshold_s,
|
|
455
456
|
auth_fail_patterns=cfg.monitor.auth_fail_patterns,
|
|
456
457
|
auth_fail_hint=cfg.monitor.auth_fail_hint,
|
|
457
458
|
phases_overrides=cfg.phases.overrides if cfg.phases.overrides else None,
|
|
@@ -141,6 +141,12 @@ class MonitorConfig:
|
|
|
141
141
|
anomaly_repetitive_threshold: int = 0 # 0 = disabled
|
|
142
142
|
host_health: MonitorHostHealthConfig = field(default_factory=MonitorHostHealthConfig)
|
|
143
143
|
round_progress_interval_s: int = 0 # 0 = disabled; >0 = emit round_progress every N seconds
|
|
144
|
+
supervisor_stale_threshold_s: int | None = None
|
|
145
|
+
"""Staleness deadline for the supervisor_stale detector (seconds).
|
|
146
|
+
|
|
147
|
+
None (unset) → derived default round_timeout_s * 1.5.
|
|
148
|
+
Positive int → explicit threshold. 0 → disable the detector.
|
|
149
|
+
"""
|
|
144
150
|
|
|
145
151
|
|
|
146
152
|
@dataclass(frozen=True)
|
|
@@ -467,6 +473,14 @@ def load_config(toml_path: Path) -> Config:
|
|
|
467
473
|
monitor_d.get("round_progress_interval_s", 0),
|
|
468
474
|
field="monitor.round_progress_interval_s",
|
|
469
475
|
),
|
|
476
|
+
supervisor_stale_threshold_s=(
|
|
477
|
+
None
|
|
478
|
+
if monitor_d.get("supervisor_stale_threshold_s") is None
|
|
479
|
+
else _require_non_negative_int(
|
|
480
|
+
monitor_d["supervisor_stale_threshold_s"],
|
|
481
|
+
field="monitor.supervisor_stale_threshold_s",
|
|
482
|
+
)
|
|
483
|
+
),
|
|
470
484
|
)
|
|
471
485
|
plugins_raw = dict(raw.get("plugins") or {}) # copy so we can pop
|
|
472
486
|
disable = list(plugins_raw.pop("disable", []))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Monitor — anomaly detectors over events + metrics + log tails.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
12 built-in detectors. Two trigger ``auto_action="stop_service"``:
|
|
4
4
|
* oauth_fail — auth pattern in short-exit logs (retrying burns API quota)
|
|
5
5
|
* disk_critical — disk_used_pct > 95% (writing more risks corruption)
|
|
6
6
|
|
|
@@ -54,6 +54,7 @@ KNOWN_ALERT_KINDS: frozenset[str] = frozenset(
|
|
|
54
54
|
"network_fail",
|
|
55
55
|
"rate_limit_active",
|
|
56
56
|
"anomaly_repetitive_active",
|
|
57
|
+
"supervisor_stale",
|
|
57
58
|
}
|
|
58
59
|
)
|
|
59
60
|
|
|
@@ -429,6 +430,39 @@ def detect_anomaly_repetitive_active(
|
|
|
429
430
|
)
|
|
430
431
|
|
|
431
432
|
|
|
433
|
+
def detect_supervisor_stale(
|
|
434
|
+
events: list[dict[str, Any]],
|
|
435
|
+
*,
|
|
436
|
+
now: datetime,
|
|
437
|
+
stale_threshold_s: int,
|
|
438
|
+
) -> Alert | None:
|
|
439
|
+
"""Alert when the most recent event is older than ``stale_threshold_s``.
|
|
440
|
+
|
|
441
|
+
Catches supervisor "silent-death": stuck between rounds (after round_end,
|
|
442
|
+
before the next round_start) emitting no events. The event stream cannot
|
|
443
|
+
distinguish that from a normal idle gap — only a deadline check can.
|
|
444
|
+
|
|
445
|
+
``stale_threshold_s <= 0`` disables the check (caller resolves the
|
|
446
|
+
sentinel). Empty event list → no alert: that is "never started", not
|
|
447
|
+
silent-death, and there is no baseline to measure staleness against.
|
|
448
|
+
"""
|
|
449
|
+
if stale_threshold_s <= 0 or not events:
|
|
450
|
+
return None
|
|
451
|
+
last_ts_str = max((e["ts"] for e in events if "ts" in e), default=None)
|
|
452
|
+
if last_ts_str is None:
|
|
453
|
+
return None
|
|
454
|
+
age_s = (now - parse_iso_ms(last_ts_str)).total_seconds()
|
|
455
|
+
if age_s <= stale_threshold_s:
|
|
456
|
+
return None
|
|
457
|
+
return _alert(
|
|
458
|
+
"supervisor_stale",
|
|
459
|
+
"warning",
|
|
460
|
+
f"No events for {int(age_s)}s (threshold {stale_threshold_s}s) — "
|
|
461
|
+
f"supervisor may be stuck or dead. Last event: {last_ts_str}.",
|
|
462
|
+
{"age_s": int(age_s), "threshold_s": stale_threshold_s, "last_ts": last_ts_str},
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
432
466
|
# ---------------------------------------------------------------------------
|
|
433
467
|
# State-tree assembly (Task 3.2)
|
|
434
468
|
# ---------------------------------------------------------------------------
|
|
@@ -535,6 +569,7 @@ def run_all_detectors(
|
|
|
535
569
|
metrics: list[dict[str, Any]],
|
|
536
570
|
log_tails: dict[int, str],
|
|
537
571
|
round_timeout_s: int = 1800,
|
|
572
|
+
supervisor_stale_threshold_s: int | None = None,
|
|
538
573
|
now: datetime | None = None,
|
|
539
574
|
auth_fail_patterns: list[str] | None = None,
|
|
540
575
|
auth_fail_hint: str | None = None,
|
|
@@ -543,12 +578,17 @@ def run_all_detectors(
|
|
|
543
578
|
disk_warning_pct: float = 90.0,
|
|
544
579
|
disk_critical_pct: float = 95.0,
|
|
545
580
|
) -> list[Alert]:
|
|
546
|
-
"""Run all
|
|
581
|
+
"""Run all 12 detectors; returns alerts (empty = healthy)."""
|
|
547
582
|
if now is None:
|
|
548
583
|
now = datetime.now(UTC)
|
|
549
584
|
compiled_auth_pats = (
|
|
550
585
|
[re.compile(p, re.IGNORECASE) for p in auth_fail_patterns] if auth_fail_patterns else None
|
|
551
586
|
)
|
|
587
|
+
effective_stale_s = (
|
|
588
|
+
int(round_timeout_s * 1.5)
|
|
589
|
+
if supervisor_stale_threshold_s is None
|
|
590
|
+
else supervisor_stale_threshold_s
|
|
591
|
+
)
|
|
552
592
|
candidates = [
|
|
553
593
|
detect_timeout_rate(events),
|
|
554
594
|
detect_hung(
|
|
@@ -568,6 +608,7 @@ def run_all_detectors(
|
|
|
568
608
|
detect_network_fail(events, log_tails),
|
|
569
609
|
detect_rate_limit_active(events, now=now.timestamp()),
|
|
570
610
|
detect_anomaly_repetitive_active(events),
|
|
611
|
+
detect_supervisor_stale(events, now=now, stale_threshold_s=effective_stale_s),
|
|
571
612
|
]
|
|
572
613
|
return [a for a in candidates if a is not None]
|
|
573
614
|
|
|
@@ -65,13 +65,14 @@ surfacing everywhere.
|
|
|
65
65
|
| `event_kind_registry` | Prevent events.emit() typos / unregistered kinds slipping past CI | `tests/invariants/test_event_kind_registry.py` |
|
|
66
66
|
<!-- /gen:defenses-table -->
|
|
67
67
|
|
|
68
|
-
## Monitor:
|
|
68
|
+
## Monitor: 12 detectors
|
|
69
69
|
|
|
70
70
|
Three categories by `auto_action`:
|
|
71
71
|
|
|
72
72
|
**Notify only** (severity `warning`):
|
|
73
73
|
`timeout_rate`, `hung`, `orphan_chain`, `disk_warning`, `mem_pressure`,
|
|
74
|
-
`smoke_fail_rate`, `network_fail
|
|
74
|
+
`smoke_fail_rate`, `network_fail`, `rate_limit_active`,
|
|
75
|
+
`anomaly_repetitive_active`, `supervisor_stale`.
|
|
75
76
|
|
|
76
77
|
**Auto-stop service** (severity `critical`, `auto_action="stop_service"`):
|
|
77
78
|
`oauth_fail`, `disk_critical`. Continuing in either state is harmful (burning
|
|
@@ -88,6 +89,7 @@ API quota / writing to a near-full disk).
|
|
|
88
89
|
- `orphan_chain`
|
|
89
90
|
- `rate_limit_active`
|
|
90
91
|
- `smoke_fail_rate`
|
|
92
|
+
- `supervisor_stale`
|
|
91
93
|
- `timeout_rate`
|
|
92
94
|
<!-- /gen:detector-list -->
|
|
93
95
|
|
|
@@ -117,7 +117,7 @@ agent-runner events --kind transient_error_backoff_capped --tail
|
|
|
117
117
|
|
|
118
118
|
### `agent-runner monitor [--host SSH-ALIAS] [--interval N] [--json]`
|
|
119
119
|
|
|
120
|
-
Anomaly-detection daemon. Runs the
|
|
120
|
+
Anomaly-detection daemon. Runs the 12 detectors against the live state on every
|
|
121
121
|
poll. Without `--host`, watches local logs at default 30s interval. With
|
|
122
122
|
`--host`, watches a remote agent-runner over plain ssh at default 60s interval.
|
|
123
123
|
|
|
@@ -80,6 +80,7 @@ running with newly-set `dirty_action = "auto_commit"` is undefined).
|
|
|
80
80
|
| `anomaly_repetitive_threshold` | `int` | 0 |
|
|
81
81
|
| `host_health` | `MonitorHostHealthConfig` | MonitorHostHealthConfig(mem_avail_min_mb=200, disk_warning_pct=90.0, disk_critical_pct=95.0) |
|
|
82
82
|
| `round_progress_interval_s` | `int` | 0 |
|
|
83
|
+
| `supervisor_stale_threshold_s` | `int | None` | None |
|
|
83
84
|
<!-- /gen:config-schema -->
|
|
84
85
|
|
|
85
86
|
### `vcs.dirty_action`
|
|
@@ -203,6 +204,7 @@ Unconfigured phases (and configs without `[phases]`) keep using the global
|
|
|
203
204
|
[monitor]
|
|
204
205
|
auto_stop_on = ["oauth_fail", "disk_critical"]
|
|
205
206
|
round_progress_interval_s = 0 # 0 = disabled; set >0 to emit round_progress heartbeat events
|
|
207
|
+
# supervisor_stale_threshold_s = 2700 # unset = round_timeout_s * 1.5; 0 = disable
|
|
206
208
|
|
|
207
209
|
[monitor.host_health]
|
|
208
210
|
mem_avail_min_mb = 200 # mem_pressure fires when mem_available_mb < this
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Migrating to 0.1.36
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install --upgrade cli-agent-runner==0.1.36
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No action required. The new `supervisor_stale` detector is ON by default with
|
|
10
|
+
a derived threshold and is notify-only — it never stops your service.
|
|
11
|
+
|
|
12
|
+
## What changed
|
|
13
|
+
|
|
14
|
+
0.1.36 adds a 12th monitor detector, `supervisor_stale`, that closes a
|
|
15
|
+
liveness blind spot: a supervisor that hangs *between* rounds (after a round
|
|
16
|
+
ends, before the next one starts) emits no events. The event stream cannot
|
|
17
|
+
tell a permanent silence from a normal idle gap, and `detect_hung` only
|
|
18
|
+
covers a round that *started* and then hung mid-execution. `supervisor_stale`
|
|
19
|
+
watches the age of the most recent event and alerts when it exceeds a
|
|
20
|
+
staleness deadline.
|
|
21
|
+
|
|
22
|
+
## Default behavior (no action needed)
|
|
23
|
+
|
|
24
|
+
- ON by default.
|
|
25
|
+
- Threshold derives from `round_timeout_s * 1.5` — comfortably above the
|
|
26
|
+
longest legitimate inter-event gap (a round running to full timeout, plus
|
|
27
|
+
restart delay), so it does not false-positive on healthy systems.
|
|
28
|
+
- Notify-only: it emits an alert, never an auto-stop. A stuck or dead
|
|
29
|
+
supervisor cannot honor an auto-stop anyway; the alert is for a human or an
|
|
30
|
+
external watchdog.
|
|
31
|
+
|
|
32
|
+
## Tuning (optional)
|
|
33
|
+
|
|
34
|
+
Set `[monitor] supervisor_stale_threshold_s` when the derived default does not
|
|
35
|
+
fit your project's cadence:
|
|
36
|
+
|
|
37
|
+
```toml
|
|
38
|
+
[monitor]
|
|
39
|
+
supervisor_stale_threshold_s = 3600 # explicit seconds
|
|
40
|
+
# supervisor_stale_threshold_s = 0 # disable the detector
|
|
41
|
+
# (unset) # derive round_timeout_s * 1.5
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **Very short rounds with occasional long legitimate gaps** (e.g. 2-minute
|
|
45
|
+
rounds plus a periodic maintenance pause): set a value higher than derived.
|
|
46
|
+
- **Phase overrides that raise `round_timeout_s`** for some phase: the derived
|
|
47
|
+
threshold uses the *base* `round_timeout_s`, so a round in a longer-timeout
|
|
48
|
+
phase can exceed `base * 1.5`. Set
|
|
49
|
+
`supervisor_stale_threshold_s >= max_phase_timeout * 1.5`.
|
|
50
|
+
|
|
51
|
+
## The liveness architecture (important)
|
|
52
|
+
|
|
53
|
+
A monitor on the *same host* as the supervisor dies when that host dies — it
|
|
54
|
+
cannot report its own host's death. For true liveness coverage, run the
|
|
55
|
+
monitor from a **separate machine**:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# On your laptop / a second host, not on the supervised host:
|
|
59
|
+
agent-runner monitor --host pi
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That catches both failure modes: a stuck supervisor on a live host
|
|
63
|
+
(`supervisor_stale`, events frozen) and a dead host or severed network (SSH
|
|
64
|
+
poll fails → `monitor_remote_giveup`).
|
|
65
|
+
|
|
66
|
+
## What did NOT change
|
|
67
|
+
|
|
68
|
+
- The existing 11 detectors are unchanged.
|
|
69
|
+
- `detect_hung` still covers the in-round hang case.
|
|
70
|
+
- `round_timeout_s` is unchanged; the staleness threshold derives from it but
|
|
71
|
+
does not modify it.
|
|
72
|
+
- No new core event kind: `supervisor_stale` is a monitor *alert kind*, surfaced
|
|
73
|
+
through the existing monitor alert path.
|
|
@@ -253,6 +253,29 @@ API. Power profile:
|
|
|
253
253
|
real state change. Verify the detector logic and thresholds before enabling
|
|
254
254
|
`auto_stop` on a production remote.
|
|
255
255
|
|
|
256
|
+
### Liveness monitoring: run monitor from a separate machine
|
|
257
|
+
|
|
258
|
+
`agent-runner monitor` detects anomalies including `supervisor_stale` — the
|
|
259
|
+
supervisor stopped emitting events because it is stuck between rounds or dead.
|
|
260
|
+
But a monitor running on the *same host* as the supervisor dies when that host
|
|
261
|
+
dies, so it cannot report its own host's death.
|
|
262
|
+
|
|
263
|
+
For true liveness coverage, run the monitor from a **separate machine**:
|
|
264
|
+
|
|
265
|
+
# On your laptop / a second host, NOT on the supervised host:
|
|
266
|
+
agent-runner monitor --host pi
|
|
267
|
+
|
|
268
|
+
This catches both failure modes:
|
|
269
|
+
|
|
270
|
+
- Supervisor stuck on a live host → `supervisor_stale` alert (events frozen).
|
|
271
|
+
- Host itself dead / network gone → SSH poll fails → `monitor_remote_giveup`.
|
|
272
|
+
|
|
273
|
+
The `supervisor_stale` threshold defaults to `round_timeout_s * 1.5`. Override
|
|
274
|
+
with `[monitor] supervisor_stale_threshold_s = N` for projects whose legitimate
|
|
275
|
+
cadence — very short rounds with occasional long legitimate gaps, or phase
|
|
276
|
+
overrides that raise `round_timeout_s` — does not fit the derived default. Set
|
|
277
|
+
to `0` to disable the detector entirely.
|
|
278
|
+
|
|
256
279
|
## Live event stream (machine-readable)
|
|
257
280
|
|
|
258
281
|
For machine consumption (parity comparisons, custom dashboards, automation
|
|
@@ -118,7 +118,7 @@ def test_given_api_types_when_inspected_then_all_frozen_dataclasses() -> None:
|
|
|
118
118
|
assert cls.__dataclass_params__.frozen, f"{name} not frozen"
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def
|
|
121
|
+
def test_given_known_alert_kinds_when_inspected_then_matches_twelve_detectors() -> None:
|
|
122
122
|
from agent_runner.monitor import KNOWN_ALERT_KINDS
|
|
123
123
|
|
|
124
124
|
expected = {
|
|
@@ -133,5 +133,6 @@ def test_given_known_alert_kinds_when_inspected_then_matches_eleven_detectors()
|
|
|
133
133
|
"network_fail",
|
|
134
134
|
"rate_limit_active",
|
|
135
135
|
"anomaly_repetitive_active",
|
|
136
|
+
"supervisor_stale",
|
|
136
137
|
}
|
|
137
138
|
assert KNOWN_ALERT_KINDS == expected
|
|
@@ -107,9 +107,24 @@ def test_given_no_alerts_when_poll_once_then_returns_empty(
|
|
|
107
107
|
tmp_git_repo: Path,
|
|
108
108
|
monkeypatch: pytest.MonkeyPatch,
|
|
109
109
|
) -> None:
|
|
110
|
+
import dataclasses
|
|
111
|
+
|
|
110
112
|
monkeypatch.setenv("HOME", str(tmp_git_repo))
|
|
111
113
|
api.init(tmp_git_repo, force=False, commit=False)
|
|
112
114
|
_seed_logs(tmp_git_repo)
|
|
115
|
+
|
|
116
|
+
# Disable supervisor_stale: seeded events use a fixed old timestamp; the
|
|
117
|
+
# detector would otherwise fire because now >> seed ts.
|
|
118
|
+
real_load = load_config
|
|
119
|
+
|
|
120
|
+
def patched_load(path):
|
|
121
|
+
cfg = real_load(path)
|
|
122
|
+
return dataclasses.replace(
|
|
123
|
+
cfg,
|
|
124
|
+
monitor=dataclasses.replace(cfg.monitor, supervisor_stale_threshold_s=0),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
monkeypatch.setattr("agent_runner.api.load_config", patched_load)
|
|
113
128
|
alerts = api._poll_once(tmp_git_repo, host=None)
|
|
114
129
|
assert alerts == []
|
|
115
130
|
|
|
@@ -205,3 +205,25 @@ def test_given_per_phase_override_when_poll_once_then_forwards_phases_overrides_
|
|
|
205
205
|
"phases_overrides kwarg missing from run_all_detectors call"
|
|
206
206
|
)
|
|
207
207
|
assert call_kwargs["phases_overrides"] == {"dev": PhaseOverride(round_timeout_s=3600)}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_poll_once_forwards_supervisor_stale_threshold(
|
|
211
|
+
tmp_git_repo: Path,
|
|
212
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""_poll_once must forward cfg.monitor.supervisor_stale_threshold_s."""
|
|
215
|
+
api.init(tmp_git_repo, force=False, commit=False)
|
|
216
|
+
|
|
217
|
+
captured: list[dict] = []
|
|
218
|
+
|
|
219
|
+
def capturing_rad(**kwargs):
|
|
220
|
+
captured.append(kwargs)
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
monkeypatch.setattr("agent_runner.monitor.run_all_detectors", capturing_rad)
|
|
224
|
+
|
|
225
|
+
api._poll_once(tmp_git_repo, host=None)
|
|
226
|
+
|
|
227
|
+
assert captured, "run_all_detectors was never called"
|
|
228
|
+
call_kwargs = captured[0]
|
|
229
|
+
assert "supervisor_stale_threshold_s" in call_kwargs
|
|
@@ -1554,3 +1554,26 @@ def test_given_high_disk_critical_when_disk_used_below_then_warning_still_fires(
|
|
|
1554
1554
|
alert = detect_disk_warning(metrics, threshold_pct=90.0, critical_pct=98.0)
|
|
1555
1555
|
assert alert is not None
|
|
1556
1556
|
assert alert.detector == "disk_warning"
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def test_given_no_supervisor_stale_field_then_default_none(tmp_path: Path) -> None:
|
|
1560
|
+
toml = _write_toml(
|
|
1561
|
+
tmp_path,
|
|
1562
|
+
'[agent]\ncommand = ["true"]\nprompt_arg_template = ["{prompt}"]\n'
|
|
1563
|
+
'[runtime]\nwork_dir = "."\nlog_dir = "/tmp/logs"\n'
|
|
1564
|
+
'[prompt]\nfile = "p.md"\n',
|
|
1565
|
+
)
|
|
1566
|
+
cfg = load_config(toml)
|
|
1567
|
+
assert cfg.monitor.supervisor_stale_threshold_s is None
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def test_given_supervisor_stale_threshold_set_then_loaded(tmp_path: Path) -> None:
|
|
1571
|
+
toml = _write_toml(
|
|
1572
|
+
tmp_path,
|
|
1573
|
+
'[agent]\ncommand = ["true"]\nprompt_arg_template = ["{prompt}"]\n'
|
|
1574
|
+
'[runtime]\nwork_dir = "."\nlog_dir = "/tmp/logs"\n'
|
|
1575
|
+
'[prompt]\nfile = "p.md"\n'
|
|
1576
|
+
"[monitor]\nsupervisor_stale_threshold_s = 600\n",
|
|
1577
|
+
)
|
|
1578
|
+
cfg = load_config(toml)
|
|
1579
|
+
assert cfg.monitor.supervisor_stale_threshold_s == 600
|
|
@@ -108,9 +108,9 @@ def test_given_render_alert_kinds_list_when_called_then_returns_bullet_list() ->
|
|
|
108
108
|
from agent_runner._docgen import render_alert_kinds_list
|
|
109
109
|
|
|
110
110
|
md = render_alert_kinds_list()
|
|
111
|
-
# Bullet list, alphabetised,
|
|
111
|
+
# Bullet list, alphabetised, 12 entries
|
|
112
112
|
bullets = [line for line in md.splitlines() if line.startswith("- ")]
|
|
113
|
-
assert len(bullets) ==
|
|
113
|
+
assert len(bullets) == 12
|
|
114
114
|
assert any("oauth_fail" in line for line in bullets)
|
|
115
115
|
assert any("disk_critical" in line for line in bullets)
|
|
116
116
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from agent_runner.monitor import detect_supervisor_stale
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _ev(ts: str, event: str = "round_end", **fields) -> dict:
|
|
9
|
+
return {"event": event, "ts": ts, **fields}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
NOW = datetime(2026, 5, 21, 12, 0, 0, tzinfo=UTC)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_given_last_event_older_than_threshold_then_alerts() -> None:
|
|
16
|
+
# Last event 4000s before NOW, threshold 2700s -> stale.
|
|
17
|
+
events = [_ev("2026-05-21T10:53:20.000Z", round_num=5)]
|
|
18
|
+
alert = detect_supervisor_stale(events, now=NOW, stale_threshold_s=2700)
|
|
19
|
+
assert alert is not None
|
|
20
|
+
assert alert.detector == "supervisor_stale"
|
|
21
|
+
assert alert.severity == "warning"
|
|
22
|
+
assert alert.auto_action == "none"
|
|
23
|
+
assert "2700" in alert.message
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_given_last_event_within_threshold_then_no_alert() -> None:
|
|
27
|
+
# Last event 100s before NOW, threshold 2700s -> healthy.
|
|
28
|
+
events = [_ev("2026-05-21T11:58:20.000Z", round_num=5)]
|
|
29
|
+
assert detect_supervisor_stale(events, now=NOW, stale_threshold_s=2700) is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_given_empty_events_then_no_alert() -> None:
|
|
33
|
+
assert detect_supervisor_stale([], now=NOW, stale_threshold_s=2700) is None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_given_threshold_zero_then_disabled_no_alert() -> None:
|
|
37
|
+
events = [_ev("2026-05-21T00:00:00.000Z", round_num=1)] # very old
|
|
38
|
+
assert detect_supervisor_stale(events, now=NOW, stale_threshold_s=0) is None
|
|
@@ -24,7 +24,7 @@ def _ev(event: str, **fields) -> dict:
|
|
|
24
24
|
return {"event": event, "ts": "2026-05-12T10:00:00.000Z", **fields}
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def test_given_known_alert_kinds_when_inspected_then_contains_all_twelve() -> None:
|
|
28
28
|
expected = {
|
|
29
29
|
"timeout_rate",
|
|
30
30
|
"hung",
|
|
@@ -37,6 +37,7 @@ def test_given_known_alert_kinds_when_inspected_then_contains_all_eleven() -> No
|
|
|
37
37
|
"network_fail",
|
|
38
38
|
"rate_limit_active",
|
|
39
39
|
"anomaly_repetitive_active",
|
|
40
|
+
"supervisor_stale",
|
|
40
41
|
}
|
|
41
42
|
assert expected == KNOWN_ALERT_KINDS
|
|
42
43
|
|
|
@@ -308,3 +309,37 @@ def test_given_phase_not_in_override_when_detect_hung_then_uses_global() -> None
|
|
|
308
309
|
phases_overrides={"warmup": PhaseOverride(round_timeout_s=300)},
|
|
309
310
|
)
|
|
310
311
|
assert out is None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_given_no_stale_threshold_then_derives_from_round_timeout() -> None:
|
|
315
|
+
# round_timeout_s=1000 -> derived 1500s. Last event 1800s ago -> stale.
|
|
316
|
+
from agent_runner.monitor import run_all_detectors
|
|
317
|
+
|
|
318
|
+
events = [_ev("round_end", round_num=1)] # ts 2026-05-12T10:00:00.000Z
|
|
319
|
+
now = datetime(2026, 5, 12, 10, 30, 0, tzinfo=UTC) # 1800s later
|
|
320
|
+
alerts = run_all_detectors(
|
|
321
|
+
events=events,
|
|
322
|
+
metrics=[],
|
|
323
|
+
log_tails={},
|
|
324
|
+
round_timeout_s=1000,
|
|
325
|
+
now=now,
|
|
326
|
+
)
|
|
327
|
+
assert any(a.detector == "supervisor_stale" for a in alerts)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_given_explicit_stale_threshold_then_used_over_derived() -> None:
|
|
331
|
+
# Explicit 3600s threshold; last event 1800s ago -> NOT stale even though
|
|
332
|
+
# derived (round_timeout 1000 * 1.5 = 1500) would have fired.
|
|
333
|
+
from agent_runner.monitor import run_all_detectors
|
|
334
|
+
|
|
335
|
+
events = [_ev("round_end", round_num=1)]
|
|
336
|
+
now = datetime(2026, 5, 12, 10, 30, 0, tzinfo=UTC) # 1800s later
|
|
337
|
+
alerts = run_all_detectors(
|
|
338
|
+
events=events,
|
|
339
|
+
metrics=[],
|
|
340
|
+
log_tails={},
|
|
341
|
+
round_timeout_s=1000,
|
|
342
|
+
supervisor_stale_threshold_s=3600,
|
|
343
|
+
now=now,
|
|
344
|
+
)
|
|
345
|
+
assert not any(a.detector == "supervisor_stale" for a in alerts)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_agent_runner-0.1.35 → cli_agent_runner-0.1.36}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|