cli-agent-runner 0.1.38__tar.gz → 0.1.39__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.38 → cli_agent_runner-0.1.39}/CHANGELOG.md +14 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/PKG-INFO +1 -1
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_docgen.py +0 -6
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_emit.py +7 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/agent_runtime.py +35 -15
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/config.py +30 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/claude.toml +1 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/runner.py +7 -2
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/commands.md +21 -4
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/configuration.md +5 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/long-running-agents.md +1 -1
- cli_agent_runner-0.1.39/docs/migrations/0.1.39.md +74 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/plugins.md +4 -2
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/runbook.md +11 -3
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/thesis.md +4 -3
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_grace_kill_emission.py +63 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_architecture.py +3 -16
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_catalogs.py +0 -1
- cli_agent_runner-0.1.39/tests/invariants/test_doc_claims_match_ssot.py +102 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime_grace.py +74 -7
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_observation.py +3 -2
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config.py +45 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_docgen.py +19 -13
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_presets.py +20 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.gitignore +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/LICENSE +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/README.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/README.zh.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/build.sh +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/README.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/architecture.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/events.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_vcs_state.py +0 -0
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
- Grace-kill (`max_grace_after_result_s`) is no longer defeated by long-lived helper subprocesses (e.g. claude's persistent Bash-tool shell-snapshot). `[runtime] grace_kill_ignore_patterns` lists regexes for cmdlines to exclude from the liveness count; the claude preset ships a matching default.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `[runtime] grace_kill_ignore_patterns: list[str]` — regex patterns; matching child cmdlines are excluded from the grace-kill liveness check.
|
|
15
|
+
- `round_grace_extended` event payload gains `ignored_children` — cmdlines filtered by `grace_kill_ignore_patterns`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Docs: `commands.md` documents `monitor --mode/--port` and `init --preset`; the Chinese verb list and `[monitor]` default values now point to the generated tables instead of restating them; runbook upgrade examples use a version placeholder.
|
|
19
|
+
|
|
20
|
+
### Internal
|
|
21
|
+
- New invariant `test_doc_claims_match_ssot` gates documented counts (detectors / defenses / verbs) and config value-sets (`dirty_action` / `context_injection_mode` / transient classification) against their code SSOT — count/enum doc drift now fails CI at the introducing commit.
|
|
22
|
+
- Removed the unused `alert-kinds` docgen renderer; de-duplicated redundant defense-count and alert-kind guards to one canonical tripwire each.
|
|
23
|
+
|
|
10
24
|
## [0.1.38] - 2026-05-24
|
|
11
25
|
|
|
12
26
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.39
|
|
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
|
|
@@ -110,11 +110,6 @@ def render_defenses_table() -> str:
|
|
|
110
110
|
return "\n".join(lines)
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def render_alert_kinds_list() -> str:
|
|
114
|
-
"""Flat bullet list of all known alert kinds, alphabetised."""
|
|
115
|
-
return "\n".join(f"- `{k}`" for k in sorted(KNOWN_ALERT_KINDS))
|
|
116
|
-
|
|
117
|
-
|
|
118
113
|
def render_detector_list() -> str:
|
|
119
114
|
"""Bullet list of detectors; auto-stop kinds flagged inline."""
|
|
120
115
|
lines: list[str] = []
|
|
@@ -155,7 +150,6 @@ def render_verb_table() -> str:
|
|
|
155
150
|
|
|
156
151
|
RENDERERS: dict[str, Callable[[], str]] = {
|
|
157
152
|
"defenses-table": render_defenses_table,
|
|
158
|
-
"alert-kinds": render_alert_kinds_list,
|
|
159
153
|
"detector-list": render_detector_list,
|
|
160
154
|
"event-kinds": render_event_kinds_list,
|
|
161
155
|
"config-schema": render_config_schema_table,
|
|
@@ -258,10 +258,16 @@ def emit_round_grace_extended(
|
|
|
258
258
|
round_num: int,
|
|
259
259
|
grace_s: int,
|
|
260
260
|
live_children: list[str],
|
|
261
|
+
ignored_children: list[str] | None = None,
|
|
261
262
|
) -> None:
|
|
262
263
|
"""Emit when the grace-after-result timer expired but the agent still had
|
|
263
264
|
live worker processes (e.g. a backgrounded build), so the round was NOT
|
|
264
265
|
killed; it continues until it finishes or hits round_timeout_s.
|
|
266
|
+
|
|
267
|
+
ignored_children: cmdlines that matched a grace_kill_ignore_patterns entry
|
|
268
|
+
and were excluded from the liveness count — useful for verifying
|
|
269
|
+
patterns are firing and for noticing when an upstream CLI changes
|
|
270
|
+
its helper path.
|
|
265
271
|
"""
|
|
266
272
|
from agent_runner.events import ROUND_GRACE_EXTENDED, emit
|
|
267
273
|
|
|
@@ -271,6 +277,7 @@ def emit_round_grace_extended(
|
|
|
271
277
|
round_num=round_num,
|
|
272
278
|
grace_s=grace_s,
|
|
273
279
|
live_children=live_children,
|
|
280
|
+
ignored_children=ignored_children or [],
|
|
274
281
|
)
|
|
275
282
|
|
|
276
283
|
|
|
@@ -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.39'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 39)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -11,6 +11,7 @@ Defenses encoded here:
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
|
+
import re
|
|
14
15
|
import signal
|
|
15
16
|
import subprocess # noqa: TID251 — sanctioned subprocess caller
|
|
16
17
|
import time
|
|
@@ -57,18 +58,26 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
|
|
|
57
58
|
pass
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
def _live_children(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
def _live_children(
|
|
62
|
+
proc: subprocess.Popen,
|
|
63
|
+
*,
|
|
64
|
+
ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
65
|
+
max_n: int = 5,
|
|
66
|
+
max_len: int = 120,
|
|
67
|
+
) -> tuple[list[str], list[str]]:
|
|
68
|
+
"""Cmdlines of live (non-zombie) descendants of ``proc``, split into
|
|
69
|
+
``(live, ignored)``: ``live`` is what counts toward the grace-kill
|
|
70
|
+
liveness check; ``ignored`` matched an ``ignore_patterns`` entry and is
|
|
71
|
+
excluded (e.g. claude's persistent shell-snapshot helper). Both lists
|
|
72
|
+
are bounded by ``max_n``/``max_len`` to keep events small. ``ignore_patterns
|
|
73
|
+
is None`` → no filtering, ``ignored`` is empty, ``live`` matches 0.1.38.
|
|
66
74
|
"""
|
|
67
75
|
try:
|
|
68
76
|
parent = psutil.Process(proc.pid)
|
|
69
77
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
70
|
-
return []
|
|
71
|
-
|
|
78
|
+
return [], []
|
|
79
|
+
live: list[str] = []
|
|
80
|
+
ignored: list[str] = []
|
|
72
81
|
for child in parent.children(recursive=True):
|
|
73
82
|
try:
|
|
74
83
|
if child.status() == psutil.STATUS_ZOMBIE:
|
|
@@ -76,10 +85,16 @@ def _live_children(proc: subprocess.Popen, *, max_n: int = 5, max_len: int = 120
|
|
|
76
85
|
line = " ".join(child.cmdline()) or child.name()
|
|
77
86
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
78
87
|
continue
|
|
79
|
-
|
|
80
|
-
if
|
|
88
|
+
short = line[:max_len]
|
|
89
|
+
if ignore_patterns and any(p.search(line) for p in ignore_patterns):
|
|
90
|
+
if len(ignored) < max_n:
|
|
91
|
+
ignored.append(short)
|
|
92
|
+
else:
|
|
93
|
+
if len(live) < max_n:
|
|
94
|
+
live.append(short)
|
|
95
|
+
if len(live) >= max_n and len(ignored) >= max_n:
|
|
81
96
|
break
|
|
82
|
-
return
|
|
97
|
+
return live, ignored
|
|
83
98
|
|
|
84
99
|
|
|
85
100
|
# Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
|
|
@@ -99,7 +114,8 @@ def run(
|
|
|
99
114
|
max_grace_after_result_s: int = 0,
|
|
100
115
|
progress_callback: Callable[[dict], None] | None = None,
|
|
101
116
|
progress_interval_s: int = 0,
|
|
102
|
-
on_grace_extended: Callable[[list[str]], None] | None = None,
|
|
117
|
+
on_grace_extended: Callable[[list[str], list[str]], None] | None = None,
|
|
118
|
+
grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
103
119
|
) -> RunResult:
|
|
104
120
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
105
121
|
|
|
@@ -116,6 +132,10 @@ def run(
|
|
|
116
132
|
progress_interval_s seconds with a dict of log stats (log_size_kb,
|
|
117
133
|
last_write_age_s, wall_age_s). Keeps agent_runtime event-free; callers
|
|
118
134
|
build the callback to emit events.
|
|
135
|
+
|
|
136
|
+
grace_kill_ignore_patterns: pre-compiled regex patterns; child cmdlines
|
|
137
|
+
matching any pattern (re.search) are excluded from the liveness count
|
|
138
|
+
(persistent helpers that aren't real workers). None = no filtering.
|
|
119
139
|
"""
|
|
120
140
|
argv = _build_argv(command, prompt_arg_template, prompt)
|
|
121
141
|
env = {**os.environ, **env_extra}
|
|
@@ -157,13 +177,13 @@ def run(
|
|
|
157
177
|
except OSError:
|
|
158
178
|
pass # log not flushed yet; check next tick
|
|
159
179
|
if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
|
|
160
|
-
|
|
161
|
-
if
|
|
180
|
+
live, ignored = _live_children(proc, ignore_patterns=grace_kill_ignore_patterns)
|
|
181
|
+
if live:
|
|
162
182
|
# Busy: a backgrounded worker is still running. Don't
|
|
163
183
|
# reap — defer to the wall-clock ceiling. Signal once.
|
|
164
184
|
if not grace_extended_emitted:
|
|
165
185
|
if on_grace_extended is not None:
|
|
166
|
-
on_grace_extended(
|
|
186
|
+
on_grace_extended(live, ignored)
|
|
167
187
|
grace_extended_emitted = True
|
|
168
188
|
else:
|
|
169
189
|
_kill_pgroup(proc)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
import tomllib
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from pathlib import Path
|
|
@@ -40,6 +41,12 @@ class RuntimeConfig:
|
|
|
40
41
|
fresh_eyes_every_n: int | None = None # None = disabled
|
|
41
42
|
dry_run: bool = False
|
|
42
43
|
max_grace_after_result_s: int = 0 # 0 = disabled
|
|
44
|
+
grace_kill_ignore_patterns: list[str] = field(default_factory=list)
|
|
45
|
+
"""Regex patterns (re.search) tested against each child process's joined
|
|
46
|
+
cmdline. Matching children are excluded from the grace-kill liveness
|
|
47
|
+
check — for persistent helper subprocesses (e.g. claude's shell-snapshot
|
|
48
|
+
bash) that would otherwise defeat max_grace_after_result_s. Empty list
|
|
49
|
+
= no filtering (0.1.38 behavior preserved)."""
|
|
43
50
|
|
|
44
51
|
|
|
45
52
|
@dataclass(frozen=True)
|
|
@@ -221,6 +228,25 @@ def _validate_remote_failure_tolerance(value: Any) -> int:
|
|
|
221
228
|
return v
|
|
222
229
|
|
|
223
230
|
|
|
231
|
+
def _validate_regex_list(value: Any, *, field: str) -> list[str]:
|
|
232
|
+
"""Validate a list of regex pattern strings (each must compile). Returns the
|
|
233
|
+
raw strings unchanged; callers compile when they need ``re.Pattern`` objects."""
|
|
234
|
+
if not isinstance(value, list):
|
|
235
|
+
raise ValueError(f"{field}: expected a list of regex strings, got {type(value).__name__}")
|
|
236
|
+
out: list[str] = []
|
|
237
|
+
for p in value:
|
|
238
|
+
if not isinstance(p, str):
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"{field}: each pattern must be a string, got {type(p).__name__}: {p!r}"
|
|
241
|
+
)
|
|
242
|
+
try:
|
|
243
|
+
re.compile(p)
|
|
244
|
+
except re.error as e:
|
|
245
|
+
raise ValueError(f"{field}: invalid regex {p!r}: {e}") from e
|
|
246
|
+
out.append(p)
|
|
247
|
+
return out
|
|
248
|
+
|
|
249
|
+
|
|
224
250
|
_PHASE_OVERRIDE_ALLOWED_FIELDS = frozenset(
|
|
225
251
|
{
|
|
226
252
|
"round_timeout_s",
|
|
@@ -392,6 +418,10 @@ def load_config(toml_path: Path) -> Config:
|
|
|
392
418
|
runtime_d.get("max_grace_after_result_s", 0),
|
|
393
419
|
field="runtime.max_grace_after_result_s",
|
|
394
420
|
),
|
|
421
|
+
grace_kill_ignore_patterns=_validate_regex_list(
|
|
422
|
+
runtime_d.get("grace_kill_ignore_patterns", []),
|
|
423
|
+
field="runtime.grace_kill_ignore_patterns",
|
|
424
|
+
),
|
|
395
425
|
)
|
|
396
426
|
prompt_d = raw.get("prompt", {})
|
|
397
427
|
mode = prompt_d.get("context_injection_mode", "prepend")
|
|
@@ -10,6 +10,7 @@ import hashlib
|
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
12
|
import random
|
|
13
|
+
import re
|
|
13
14
|
import sys
|
|
14
15
|
import time
|
|
15
16
|
import traceback as tb_mod
|
|
@@ -466,12 +467,15 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
466
467
|
**stats,
|
|
467
468
|
)
|
|
468
469
|
|
|
469
|
-
|
|
470
|
+
grace_kill_ignore_patterns = [re.compile(p) for p in cfg.runtime.grace_kill_ignore_patterns]
|
|
471
|
+
|
|
472
|
+
def _grace_extended_emit(live: list[str], ignored: list[str]) -> None:
|
|
470
473
|
api.emit_round_grace_extended(
|
|
471
474
|
log_dir,
|
|
472
475
|
round_num=round_num,
|
|
473
476
|
grace_s=cfg.runtime.max_grace_after_result_s,
|
|
474
|
-
live_children=
|
|
477
|
+
live_children=live,
|
|
478
|
+
ignored_children=ignored,
|
|
475
479
|
)
|
|
476
480
|
|
|
477
481
|
result = agent_runtime.run(
|
|
@@ -485,6 +489,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
485
489
|
progress_callback=_progress_emit,
|
|
486
490
|
progress_interval_s=cfg.monitor.round_progress_interval_s,
|
|
487
491
|
on_grace_extended=_grace_extended_emit,
|
|
492
|
+
grace_kill_ignore_patterns=grace_kill_ignore_patterns,
|
|
488
493
|
)
|
|
489
494
|
events.emit(
|
|
490
495
|
log_dir,
|
|
@@ -34,8 +34,15 @@ are shared between `peek`, `watch`, and `monitor`.
|
|
|
34
34
|
Scaffold a new project: writes `agent-runner.toml`, `prompts/main.md`, and
|
|
35
35
|
appends `logs/` to `.gitignore`. By default also creates a git commit.
|
|
36
36
|
|
|
37
|
+
Flags:
|
|
38
|
+
|
|
39
|
+
- `--preset {claude,aider,gemini}` — agent CLI preset to scaffold (default: `claude`)
|
|
40
|
+
- `--force` — overwrite an existing `agent-runner.toml`
|
|
41
|
+
- `--no-commit` — skip the initial git commit
|
|
42
|
+
|
|
37
43
|
```bash
|
|
38
|
-
agent-runner init # default: commit
|
|
44
|
+
agent-runner init # default: claude preset, commit
|
|
45
|
+
agent-runner init --preset aider # aider preset
|
|
39
46
|
agent-runner init --no-commit # skip the commit
|
|
40
47
|
agent-runner init --force # overwrite an existing toml
|
|
41
48
|
```
|
|
@@ -133,7 +140,7 @@ agent-runner events --kind transient_error_backoff_capped --tail
|
|
|
133
140
|
|
|
134
141
|
`peek` in a clear-and-refresh loop. Default 2s interval. Stop with Ctrl-C.
|
|
135
142
|
|
|
136
|
-
### `agent-runner monitor [--host SSH-ALIAS] [--interval N] [--json]`
|
|
143
|
+
### `agent-runner monitor [--host SSH-ALIAS] [--interval N] [--mode MODE] [--port PORT] [--json]`
|
|
137
144
|
|
|
138
145
|
Anomaly-detection daemon. Runs the 12 detectors against the live state on every
|
|
139
146
|
poll. Without `--host`, watches local logs at default 30s interval. With
|
|
@@ -143,15 +150,25 @@ When OAuth-fail or disk-critical detectors fire, monitor automatically issues a
|
|
|
143
150
|
graceful stop (locally via `api.stop`; remotely via `ssh <host> 'agent-runner stop'`).
|
|
144
151
|
Override with `[monitor]` config block (see configuration.md).
|
|
145
152
|
|
|
153
|
+
Flags:
|
|
154
|
+
|
|
155
|
+
- `--mode {anomaly,narrate,events,http}` — output mode (default: `anomaly`). `narrate`
|
|
156
|
+
streams a human-readable narrative; `events` streams raw event JSON; `http` serves
|
|
157
|
+
a local progress page.
|
|
158
|
+
- `--port PORT` — HTTP port for `--mode http` (default: `8765`, local-only).
|
|
159
|
+
- `--host SSH-ALIAS` — watch a remote agent-runner via ssh (anomaly mode only).
|
|
160
|
+
|
|
146
161
|
```bash
|
|
147
|
-
agent-runner monitor # local
|
|
162
|
+
agent-runner monitor # local anomaly mode
|
|
148
163
|
agent-runner monitor --host pi # remote
|
|
164
|
+
agent-runner monitor --mode narrate # streaming narrative
|
|
165
|
+
agent-runner monitor --mode http --port 9000 # HTTP progress page on port 9000
|
|
149
166
|
agent-runner monitor --json | jq -c # pipe alerts to a downstream consumer
|
|
150
167
|
```
|
|
151
168
|
|
|
152
169
|
## 中文摘要
|
|
153
170
|
|
|
154
|
-
16
|
|
171
|
+
16 个动词,完整列表见上方动词表(自动生成)。
|
|
155
172
|
|
|
156
173
|
观察类(peek/watch/monitor)三视角对称,全部共用 `--round / --log / --events / --select / --json` 下钻参数。
|
|
157
174
|
|
|
@@ -47,6 +47,7 @@ running with newly-set `dirty_action = "auto_commit"` is undefined).
|
|
|
47
47
|
| `fresh_eyes_every_n` | `int | None` | None |
|
|
48
48
|
| `dry_run` | `bool` | False |
|
|
49
49
|
| `max_grace_after_result_s` | `int` | 0 |
|
|
50
|
+
| `grace_kill_ignore_patterns` | `list[str]` | [] |
|
|
50
51
|
|
|
51
52
|
### `[prompt]`
|
|
52
53
|
|
|
@@ -200,6 +201,10 @@ Unconfigured phases (and configs without `[phases]`) keep using the global
|
|
|
200
201
|
|
|
201
202
|
## `[monitor]` (optional, defaults shown)
|
|
202
203
|
|
|
204
|
+
> Authoritative field-level defaults are in the generated schema table above
|
|
205
|
+
> (`[monitor]` section). The snippet below shows only the fields most commonly
|
|
206
|
+
> customised, with operational notes.
|
|
207
|
+
|
|
203
208
|
```toml
|
|
204
209
|
[monitor]
|
|
205
210
|
auto_stop_on = ["oauth_fail", "disk_critical"]
|
|
@@ -140,7 +140,7 @@ token breakdown + cost (where the underlying CLI exposes it).
|
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
Use as input to a cost-tracking detector or external billing reconciler.
|
|
143
|
-
See `docs/migrations/0.1.28.md` for the current
|
|
143
|
+
See `docs/migrations/0.1.28.md` for the current payload schema
|
|
144
144
|
(includes `cache_creation_tokens`, `tool_call_count`, `phase`, `success`)
|
|
145
145
|
plus a consumer dispatcher sketch. Aggregation (rollups, budget warnings)
|
|
146
146
|
is the consumer's responsibility — agent-runner emits raw per-round
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Migrating to 0.1.39
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install --upgrade cli-agent-runner==0.1.39
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**Claude users running 0.1.38**: add one line to `[runtime]` to unblock
|
|
10
|
+
grace-kill against claude's persistent shell-snapshot helper (see below).
|
|
11
|
+
New `agent-runner init --preset=claude` scaffolds get this automatically.
|
|
12
|
+
|
|
13
|
+
**Everyone else**: no action.
|
|
14
|
+
|
|
15
|
+
## Persistent-helper exclusion (the live fix)
|
|
16
|
+
|
|
17
|
+
0.1.38's grace-kill liveness check was correctly conservative — it refused to
|
|
18
|
+
reap a round with live worker children — but claude's `-p` mode keeps a
|
|
19
|
+
persistent Bash-tool shell-snapshot subprocess alive for the whole session.
|
|
20
|
+
That subprocess is not doing work; it's idle infrastructure. 0.1.38 saw it as
|
|
21
|
+
a live worker and deferred every post-result hang to `round_timeout_s` instead
|
|
22
|
+
of reaping at `max_grace_after_result_s`. This is the "persistent-helper
|
|
23
|
+
caveat" 0.1.38's migration doc flagged.
|
|
24
|
+
|
|
25
|
+
0.1.39 adds `[runtime] grace_kill_ignore_patterns` — a list of regex patterns;
|
|
26
|
+
child cmdlines matching any pattern (via `re.search`) are excluded from the
|
|
27
|
+
liveness count. `presets/claude.toml` ships a default pattern matching
|
|
28
|
+
claude's shell-snapshot.
|
|
29
|
+
|
|
30
|
+
### Existing claude operators — one line
|
|
31
|
+
|
|
32
|
+
Add to your `[runtime]` block:
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[runtime]
|
|
36
|
+
grace_kill_ignore_patterns = ['\.claude/shell-snapshots/snapshot-bash-']
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or run `agent-runner init --preset=claude` in a scratch directory and diff
|
|
40
|
+
the generated `agent-runner.toml` against yours.
|
|
41
|
+
|
|
42
|
+
After the change, post-result hangs are reaped at `max_grace_after_result_s`.
|
|
43
|
+
Without it, they continue to defer to `round_timeout_s` (the 0.1.38 behavior).
|
|
44
|
+
|
|
45
|
+
### Verifying the pattern is firing
|
|
46
|
+
|
|
47
|
+
The `round_grace_extended` event payload gains `ignored_children` listing
|
|
48
|
+
cmdlines that matched a pattern. Use it to:
|
|
49
|
+
|
|
50
|
+
- confirm the shell-snapshot is being filtered (`ignored_children` non-empty)
|
|
51
|
+
- catch the day claude renames its helper (`live_children` shows a new
|
|
52
|
+
unfiltered persistent process)
|
|
53
|
+
|
|
54
|
+
### Other presets
|
|
55
|
+
|
|
56
|
+
`aider.toml` and `gemini.toml` ship no default patterns. Add operator-specific
|
|
57
|
+
patterns to your own `agent-runner.toml` if needed.
|
|
58
|
+
|
|
59
|
+
## SSOT consistency hardening (also in 0.1.39)
|
|
60
|
+
|
|
61
|
+
A new invariant `test_doc_claims_match_ssot` gates documented counts
|
|
62
|
+
(detectors / defenses / verbs) and config value-sets against code SSOT.
|
|
63
|
+
`commands.md` documents `monitor --mode/--port` and `init --preset`.
|
|
64
|
+
Redundant count guards collapsed to one canonical tripwire each. The unused
|
|
65
|
+
`alert-kinds` docgen renderer was removed. No action required.
|
|
66
|
+
|
|
67
|
+
## What did NOT change
|
|
68
|
+
|
|
69
|
+
- The 0.1.38 grace-kill liveness semantics (still process-group-based;
|
|
70
|
+
patterns are an exclusion filter on top).
|
|
71
|
+
- `round_grace_kill` (still fires only when the post-filter live set is empty).
|
|
72
|
+
- `round_timeout_s` (still the hard ceiling).
|
|
73
|
+
- `max_grace_after_result_s` (knob unchanged).
|
|
74
|
+
- For non-claude deployments: zero behavior change.
|
|
@@ -170,6 +170,8 @@ Any exception raised by a hook is caught by the runner and emitted as a built-in
|
|
|
170
170
|
}
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
(Fields emitted by the `HOOK_FAILED` path in `runner.py` + `_summarize_error` in `hooks.py`.)
|
|
174
|
+
|
|
173
175
|
The round itself continues — a broken plugin must not crash the supervisor.
|
|
174
176
|
|
|
175
177
|
### What `plugin_context_enrichers()` surfaces
|
|
@@ -293,7 +295,7 @@ event with `classification` ∈ {`rate_limit_account`, `rate_limit_model`,
|
|
|
293
295
|
|
|
294
296
|
Per round (regardless of error state), also emits `agent_usage_recorded`
|
|
295
297
|
with token/cost/duration data extracted from the claude result event —
|
|
296
|
-
see `docs/migrations/0.1.28.md` for the full
|
|
298
|
+
see `docs/migrations/0.1.28.md` for the full payload schema. The
|
|
297
299
|
supervisor reads `transient_error_detected` on the next dispatch cycle
|
|
298
300
|
and applies the configured `transient_error_action` (default `back_off`;
|
|
299
301
|
`rate_limit_action` retained as a deprecated alias).
|
|
@@ -309,7 +311,7 @@ to ship equivalent detectors for other agent CLIs — the bundled
|
|
|
309
311
|
## Custom monitor detectors (§3.3)
|
|
310
312
|
|
|
311
313
|
0.1.5 adds a fourth extension point — plugin authors can ship custom monitor
|
|
312
|
-
detectors that run alongside the
|
|
314
|
+
detectors that run alongside the 12 builtins on every monitor poll.
|
|
313
315
|
|
|
314
316
|
### Group + Protocol
|
|
315
317
|
|
|
@@ -153,7 +153,7 @@ RestartSec=5
|
|
|
153
153
|
|
|
154
154
|
### Path 1 — systemd --user service (installed via `agent-runner install`)
|
|
155
155
|
|
|
156
|
-
agent-runner upgrade --target
|
|
156
|
+
agent-runner upgrade --target <version>
|
|
157
157
|
|
|
158
158
|
Does stop → pip → smoke → start, with auto-rollback on smoke failure.
|
|
159
159
|
|
|
@@ -167,7 +167,7 @@ package-only upgrade (pip + smoke + rollback), then prints the restart command.
|
|
|
167
167
|
It never runs `sudo` and never starts a service it didn't install. Restart your
|
|
168
168
|
supervisor yourself:
|
|
169
169
|
|
|
170
|
-
python3 -m pip install --user --break-system-packages --upgrade cli-agent-runner
|
|
170
|
+
python3 -m pip install --user --break-system-packages --upgrade cli-agent-runner==<version>
|
|
171
171
|
agent-runner --version
|
|
172
172
|
sudo systemctl restart <your-unit>
|
|
173
173
|
|
|
@@ -177,7 +177,7 @@ supervisor yourself:
|
|
|
177
177
|
Use `--no-restart` to force package-only mode even on a systemd --user host
|
|
178
178
|
(upgrade the package now, restart later):
|
|
179
179
|
|
|
180
|
-
agent-runner upgrade --target
|
|
180
|
+
agent-runner upgrade --target <version> --no-restart
|
|
181
181
|
|
|
182
182
|
### Manual rollback
|
|
183
183
|
|
|
@@ -584,6 +584,14 @@ work past `type=result`. Check the `live_children` field in the event to identif
|
|
|
584
584
|
the process; consider restructuring the agent to emit `type=result` only when
|
|
585
585
|
truly done.
|
|
586
586
|
|
|
587
|
+
**Persistent-helper exclusion (0.1.39+):** when an agent CLI keeps long-lived
|
|
588
|
+
helper subprocesses alive past `type=result` (claude does this with a Bash-tool
|
|
589
|
+
shell-snapshot), they would otherwise count as "live workers" and defer every
|
|
590
|
+
post-result hang to `round_timeout_s`. Set `[runtime] grace_kill_ignore_patterns
|
|
591
|
+
= [<regex>, ...]` to exclude them; the `claude` preset ships a default. The
|
|
592
|
+
`round_grace_extended` event's `ignored_children` field shows which cmdlines
|
|
593
|
+
matched a pattern.
|
|
594
|
+
|
|
587
595
|
### Disk pressure
|
|
588
596
|
|
|
589
597
|
**Symptom:** `[WARN] disk_warning` at >90%; `[CRIT] disk_critical` at >95% (auto-stops).
|
|
@@ -54,9 +54,10 @@ would produce constant false positives across diverse workloads.
|
|
|
54
54
|
The `anomaly_repetitive_active` detector (added 0.1.32) is the live example:
|
|
55
55
|
it fires when the claude plugin emits `anomaly_repetitive_tool` events
|
|
56
56
|
above a fixed threshold within a window — a specific signature, not N-σ.
|
|
57
|
-
`max_grace_after_result_s` (0.1.31) is another:
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
`max_grace_after_result_s` (0.1.31, refined 0.1.38) is another: a fixed
|
|
58
|
+
grace after the `result` event, the subprocess is killed only if its
|
|
59
|
+
process group has no live worker left — specific signature, not "is this
|
|
60
|
+
subprocess behaving unusually".
|
|
60
61
|
|
|
61
62
|
> **Example**: A 2026-05-18 proposal requested a "cost spike detector" that
|
|
62
63
|
> fires when this round's cost is N× the rolling 7-day average. Rejected.
|
{cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_grace_kill_emission.py
RENAMED
|
@@ -79,6 +79,28 @@ def test_grace_kill_emits_round_grace_kill_event(tmp_path: Path) -> None:
|
|
|
79
79
|
assert len(timeout_events) == 0
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _make_grace_config_with_patterns(
|
|
83
|
+
work_dir: Path, script_path: Path, grace_s: int, patterns: list[str]
|
|
84
|
+
) -> Config:
|
|
85
|
+
log_dir = work_dir / "logs"
|
|
86
|
+
log_dir.mkdir(exist_ok=True)
|
|
87
|
+
prompt = work_dir / "p.md"
|
|
88
|
+
prompt.write_text("Test prompt. " * 50)
|
|
89
|
+
return Config(
|
|
90
|
+
agent=AgentConfig(command=[str(script_path)], prompt_arg_template=[]),
|
|
91
|
+
runtime=RuntimeConfig(
|
|
92
|
+
work_dir=work_dir,
|
|
93
|
+
log_dir=log_dir,
|
|
94
|
+
round_timeout_s=10,
|
|
95
|
+
max_grace_after_result_s=grace_s,
|
|
96
|
+
grace_kill_ignore_patterns=patterns,
|
|
97
|
+
),
|
|
98
|
+
prompt=PromptConfig(file=prompt, inject_context=False),
|
|
99
|
+
vcs=VcsConfig(),
|
|
100
|
+
phases=PhasesConfig(),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
82
104
|
def test_round_grace_extended_emitted_when_worker_alive(tmp_path: Path) -> None:
|
|
83
105
|
"""Full runner flow: subprocess emits result then backgrounds a long child;
|
|
84
106
|
round_grace_extended event fires (not round_grace_kill); wall timeout reaps."""
|
|
@@ -109,3 +131,44 @@ def test_round_grace_extended_emitted_when_worker_alive(tmp_path: Path) -> None:
|
|
|
109
131
|
# round_grace_kill must NOT appear (round was busy, not idle)
|
|
110
132
|
grace_kill_events = [e for e in events if e.get("event") == "round_grace_kill"]
|
|
111
133
|
assert len(grace_kill_events) == 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_round_grace_extended_carries_ignored_children(tmp_path: Path) -> None:
|
|
137
|
+
"""With grace_kill_ignore_patterns set, persistent helpers appear under
|
|
138
|
+
ignored_children, not live_children — even when a real worker is also alive."""
|
|
139
|
+
_init_git(tmp_path)
|
|
140
|
+
|
|
141
|
+
script = tmp_path / "agent.sh"
|
|
142
|
+
# Emit result, then background both a snapshot-like helper and a 'real' sleep.
|
|
143
|
+
# exec -a renames the subprocess's argv[0] so the pattern can match it.
|
|
144
|
+
script.write_text(
|
|
145
|
+
"#!/bin/bash\n"
|
|
146
|
+
'echo \'{"type":"result","is_error":false}\'\n'
|
|
147
|
+
"exec -a snapshot-bash-test sleep 30 &\n"
|
|
148
|
+
"sleep 30 &\n"
|
|
149
|
+
"wait\n",
|
|
150
|
+
encoding="utf-8",
|
|
151
|
+
)
|
|
152
|
+
script.chmod(0o755)
|
|
153
|
+
|
|
154
|
+
cfg = _make_grace_config_with_patterns(
|
|
155
|
+
tmp_path, script, grace_s=1, patterns=["snapshot-bash-test"]
|
|
156
|
+
)
|
|
157
|
+
result = run_one_round(cfg)
|
|
158
|
+
|
|
159
|
+
assert result.killed_for_grace is False # real worker kept it alive
|
|
160
|
+
assert result.timed_out is True # wall-clock reaped it
|
|
161
|
+
|
|
162
|
+
events_list = read_events_for_current_month(cfg.runtime.log_dir)
|
|
163
|
+
extended_events = [e for e in events_list if e.get("event") == "round_grace_extended"]
|
|
164
|
+
assert len(extended_events) == 1
|
|
165
|
+
ev = extended_events[0]
|
|
166
|
+
|
|
167
|
+
# The plain sleep goes to live_children (real worker)
|
|
168
|
+
assert any("sleep" in c for c in ev["live_children"])
|
|
169
|
+
# The exec -a snapshot-bash-test process goes to ignored_children
|
|
170
|
+
assert any("snapshot-bash-test" in c for c in ev["ignored_children"])
|
|
171
|
+
|
|
172
|
+
# round_grace_kill must NOT appear (real worker still alive)
|
|
173
|
+
grace_kill_events = [e for e in events_list if e.get("event") == "round_grace_kill"]
|
|
174
|
+
assert len(grace_kill_events) == 0
|
|
@@ -118,21 +118,8 @@ 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_well_formed() -> None:
|
|
122
122
|
from agent_runner.monitor import KNOWN_ALERT_KINDS
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"hung",
|
|
127
|
-
"orphan_chain",
|
|
128
|
-
"disk_warning",
|
|
129
|
-
"disk_critical",
|
|
130
|
-
"mem_pressure",
|
|
131
|
-
"smoke_fail_rate",
|
|
132
|
-
"oauth_fail",
|
|
133
|
-
"network_fail",
|
|
134
|
-
"rate_limit_active",
|
|
135
|
-
"anomaly_repetitive_active",
|
|
136
|
-
"supervisor_stale",
|
|
137
|
-
}
|
|
138
|
-
assert KNOWN_ALERT_KINDS == expected
|
|
124
|
+
assert len(KNOWN_ALERT_KINDS) == 12
|
|
125
|
+
assert all(re.fullmatch(r"[a-z][a-z0-9_]*", k) for k in KNOWN_ALERT_KINDS)
|
|
@@ -26,7 +26,6 @@ def test_given_defenses_catalog_when_loaded_then_each_entry_has_required_fields(
|
|
|
26
26
|
phases=None,
|
|
27
27
|
)
|
|
28
28
|
cat = catalog(cfg)
|
|
29
|
-
assert len(cat) == 11
|
|
30
29
|
for d in cat:
|
|
31
30
|
assert d.name and isinstance(d.name, str)
|
|
32
31
|
assert d.current_state in {"active", "degraded", "off"}
|