cli-agent-runner 0.1.37__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.37 → cli_agent_runner-0.1.39}/CHANGELOG.md +24 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/PKG-INFO +1 -1
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_docgen.py +0 -6
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_emit.py +42 -6
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/agent_runtime.py +77 -15
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/api.py +1 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/config.py +30 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/events.py +1 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/claude.toml +1 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/runner.py +15 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/architecture.md +1 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/commands.md +21 -4
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/configuration.md +5 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/long-running-agents.md +1 -1
- cli_agent_runner-0.1.39/docs/migrations/0.1.38.md +53 -0
- cli_agent_runner-0.1.39/docs/migrations/0.1.39.md +74 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/plugins.md +4 -2
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/runbook.md +30 -3
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/thesis.md +4 -3
- cli_agent_runner-0.1.39/tests/integration/test_grace_kill_emission.py +174 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_architecture.py +3 -16
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_catalogs.py +7 -1
- cli_agent_runner-0.1.39/tests/invariants/test_doc_claims_match_ssot.py +102 -0
- cli_agent_runner-0.1.39/tests/unit/test_agent_runtime_grace.py +226 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_observation.py +3 -2
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config.py +45 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_docgen.py +19 -13
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_presets.py +20 -0
- cli_agent_runner-0.1.37/tests/integration/test_grace_kill_emission.py +0 -79
- cli_agent_runner-0.1.37/tests/unit/test_agent_runtime_grace.py +0 -72
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.gitignore +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/LICENSE +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/README.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/README.zh.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/build.sh +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/README.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/events.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.37 → 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.37 → 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.37 → 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.37 → cli_agent_runner-0.1.39}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_vcs_state.py +0 -0
|
@@ -7,6 +7,30 @@ 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
|
+
|
|
24
|
+
## [0.1.38] - 2026-05-24
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Grace-kill (`max_grace_after_result_s`) no longer reaps a round that emitted `type=result` while a backgrounded child process (e.g. a long build) is still running. It now reaps only when the agent's process group has no live worker processes left (a genuine hang); otherwise it waits for the round to finish or for the `round_timeout_s` ceiling.
|
|
28
|
+
- Corrected `round_grace_kill`'s description: the kill is gated on the process group being idle (no live workers), not on log silence.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- New event `round_grace_extended` — emitted once when grace elapsed after `type=result` but a live worker process kept the round busy; carries the worker cmdlines.
|
|
32
|
+
- `round_grace_kill` now carries `live_children` (cmdlines observed at kill time; empty for a genuine idle hang).
|
|
33
|
+
|
|
10
34
|
## [0.1.37] - 2026-05-22
|
|
11
35
|
|
|
12
36
|
### 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,
|
|
@@ -19,6 +19,7 @@ __all__ = [
|
|
|
19
19
|
"emit_fresh_eyes_round_triggered",
|
|
20
20
|
"emit_max_rounds_reached",
|
|
21
21
|
"emit_rate_limit_stop",
|
|
22
|
+
"emit_round_grace_extended",
|
|
22
23
|
"emit_round_grace_kill",
|
|
23
24
|
"emit_round_progress",
|
|
24
25
|
"emit_round_substrate_after",
|
|
@@ -233,16 +234,51 @@ def emit_round_grace_kill(
|
|
|
233
234
|
*,
|
|
234
235
|
round_num: int,
|
|
235
236
|
grace_s: int,
|
|
237
|
+
live_children: list[str] | None = None,
|
|
236
238
|
) -> None:
|
|
237
|
-
"""Emit when subprocess killed because grace-after-result timer
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(wall-clock exceeded without result event).
|
|
239
|
+
"""Emit when the subprocess was killed because the grace-after-result timer
|
|
240
|
+
expired AND the agent's process group had no live worker processes left
|
|
241
|
+
(a genuine hang). Distinct from round_grace_extended (grace elapsed but a
|
|
242
|
+
worker was still running) and round_timeout_kill (wall-clock exceeded).
|
|
242
243
|
"""
|
|
243
244
|
from agent_runner.events import ROUND_GRACE_KILL, emit
|
|
244
245
|
|
|
245
|
-
emit(
|
|
246
|
+
emit(
|
|
247
|
+
log_dir,
|
|
248
|
+
ROUND_GRACE_KILL,
|
|
249
|
+
round_num=round_num,
|
|
250
|
+
grace_s=grace_s,
|
|
251
|
+
live_children=live_children or [],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def emit_round_grace_extended(
|
|
256
|
+
log_dir: Path,
|
|
257
|
+
*,
|
|
258
|
+
round_num: int,
|
|
259
|
+
grace_s: int,
|
|
260
|
+
live_children: list[str],
|
|
261
|
+
ignored_children: list[str] | None = None,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Emit when the grace-after-result timer expired but the agent still had
|
|
264
|
+
live worker processes (e.g. a backgrounded build), so the round was NOT
|
|
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.
|
|
271
|
+
"""
|
|
272
|
+
from agent_runner.events import ROUND_GRACE_EXTENDED, emit
|
|
273
|
+
|
|
274
|
+
emit(
|
|
275
|
+
log_dir,
|
|
276
|
+
ROUND_GRACE_EXTENDED,
|
|
277
|
+
round_num=round_num,
|
|
278
|
+
grace_s=grace_s,
|
|
279
|
+
live_children=live_children,
|
|
280
|
+
ignored_children=ignored_children or [],
|
|
281
|
+
)
|
|
246
282
|
|
|
247
283
|
|
|
248
284
|
def emit_anomaly_repetitive_tool(
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.39'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 39)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -11,13 +11,16 @@ 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
|
|
17
18
|
from collections.abc import Callable
|
|
18
|
-
from dataclasses import dataclass
|
|
19
|
+
from dataclasses import dataclass, field
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
|
|
22
|
+
import psutil
|
|
23
|
+
|
|
21
24
|
REAP_GRACE_S = 5
|
|
22
25
|
|
|
23
26
|
|
|
@@ -28,6 +31,7 @@ class RunResult:
|
|
|
28
31
|
timed_out: bool
|
|
29
32
|
pid: int
|
|
30
33
|
killed_for_grace: bool = False
|
|
34
|
+
grace_kill_children: list[str] = field(default_factory=list)
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
|
|
@@ -54,6 +58,45 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
|
|
|
54
58
|
pass
|
|
55
59
|
|
|
56
60
|
|
|
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.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
parent = psutil.Process(proc.pid)
|
|
77
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
78
|
+
return [], []
|
|
79
|
+
live: list[str] = []
|
|
80
|
+
ignored: list[str] = []
|
|
81
|
+
for child in parent.children(recursive=True):
|
|
82
|
+
try:
|
|
83
|
+
if child.status() == psutil.STATUS_ZOMBIE:
|
|
84
|
+
continue
|
|
85
|
+
line = " ".join(child.cmdline()) or child.name()
|
|
86
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
87
|
+
continue
|
|
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:
|
|
96
|
+
break
|
|
97
|
+
return live, ignored
|
|
98
|
+
|
|
99
|
+
|
|
57
100
|
# Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
|
|
58
101
|
# A future CLI variant emitting `{"type": "result", ...}` (with space) would
|
|
59
102
|
# bypass this scan; revisit if that happens.
|
|
@@ -71,19 +114,28 @@ def run(
|
|
|
71
114
|
max_grace_after_result_s: int = 0,
|
|
72
115
|
progress_callback: Callable[[dict], None] | None = None,
|
|
73
116
|
progress_interval_s: int = 0,
|
|
117
|
+
on_grace_extended: Callable[[list[str], list[str]], None] | None = None,
|
|
118
|
+
grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
74
119
|
) -> RunResult:
|
|
75
120
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
76
121
|
|
|
77
122
|
Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
|
|
78
123
|
|
|
79
124
|
max_grace_after_result_s: when > 0, start a countdown after the first
|
|
80
|
-
type=result event is detected in the log
|
|
81
|
-
|
|
125
|
+
type=result event is detected in the log. After it elapses, reap the
|
|
126
|
+
process group only if the agent has no live worker processes left (a
|
|
127
|
+
genuine hang). If a worker is still running (e.g. a backgrounded build),
|
|
128
|
+
do not reap — invoke ``on_grace_extended`` once and keep waiting until the
|
|
129
|
+
round finishes or hits the wall-clock ``timeout_s`` ceiling. 0 = disabled.
|
|
82
130
|
|
|
83
131
|
progress_callback: when not None and progress_interval_s > 0, called every
|
|
84
132
|
progress_interval_s seconds with a dict of log stats (log_size_kb,
|
|
85
133
|
last_write_age_s, wall_age_s). Keeps agent_runtime event-free; callers
|
|
86
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.
|
|
87
139
|
"""
|
|
88
140
|
argv = _build_argv(command, prompt_arg_template, prompt)
|
|
89
141
|
env = {**os.environ, **env_extra}
|
|
@@ -100,6 +152,7 @@ def run(
|
|
|
100
152
|
start_new_session=True,
|
|
101
153
|
)
|
|
102
154
|
result_seen_at: float | None = None
|
|
155
|
+
grace_extended_emitted = False
|
|
103
156
|
try:
|
|
104
157
|
while True:
|
|
105
158
|
ret = proc.poll()
|
|
@@ -114,10 +167,9 @@ def run(
|
|
|
114
167
|
return RunResult(
|
|
115
168
|
exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
|
|
116
169
|
)
|
|
117
|
-
# Grace kill: result emitted but subprocess still running
|
|
170
|
+
# Grace kill: result emitted but subprocess still running.
|
|
118
171
|
if max_grace_after_result_s > 0:
|
|
119
172
|
if result_seen_at is None:
|
|
120
|
-
# Cheap check: byte-scan log for marker substring
|
|
121
173
|
try:
|
|
122
174
|
with log_path.open("rb") as f:
|
|
123
175
|
if _RESULT_MARKER in f.read():
|
|
@@ -125,16 +177,26 @@ def run(
|
|
|
125
177
|
except OSError:
|
|
126
178
|
pass # log not flushed yet; check next tick
|
|
127
179
|
if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
180
|
+
live, ignored = _live_children(proc, ignore_patterns=grace_kill_ignore_patterns)
|
|
181
|
+
if live:
|
|
182
|
+
# Busy: a backgrounded worker is still running. Don't
|
|
183
|
+
# reap — defer to the wall-clock ceiling. Signal once.
|
|
184
|
+
if not grace_extended_emitted:
|
|
185
|
+
if on_grace_extended is not None:
|
|
186
|
+
on_grace_extended(live, ignored)
|
|
187
|
+
grace_extended_emitted = True
|
|
188
|
+
else:
|
|
189
|
+
_kill_pgroup(proc)
|
|
190
|
+
duration = time.time() - start
|
|
191
|
+
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
192
|
+
return RunResult(
|
|
193
|
+
exit_code=exit_code,
|
|
194
|
+
duration_s=duration,
|
|
195
|
+
timed_out=True,
|
|
196
|
+
pid=proc.pid,
|
|
197
|
+
killed_for_grace=True,
|
|
198
|
+
grace_kill_children=[],
|
|
199
|
+
)
|
|
138
200
|
# Progress heartbeat: call back if interval elapsed
|
|
139
201
|
if progress_callback is not None and progress_interval_s > 0:
|
|
140
202
|
if now - last_progress_at >= progress_interval_s:
|
|
@@ -733,6 +733,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
733
733
|
emit_fresh_eyes_round_triggered,
|
|
734
734
|
emit_max_rounds_reached,
|
|
735
735
|
emit_rate_limit_stop,
|
|
736
|
+
emit_round_grace_extended,
|
|
736
737
|
emit_round_grace_kill,
|
|
737
738
|
emit_round_progress,
|
|
738
739
|
emit_round_substrate_after,
|
|
@@ -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")
|
|
@@ -49,6 +49,7 @@ ORPHAN_STASHED = "orphan_stashed"
|
|
|
49
49
|
PACKAGE_UPGRADED = "package_upgraded"
|
|
50
50
|
PROMPT_OVERWRITTEN = "prompt_overwritten"
|
|
51
51
|
ROUND_END = "round_end"
|
|
52
|
+
ROUND_GRACE_EXTENDED = "round_grace_extended"
|
|
52
53
|
ROUND_GRACE_KILL = "round_grace_kill"
|
|
53
54
|
ROUND_PROGRESS = "round_progress"
|
|
54
55
|
ROUND_START = "round_start"
|
|
@@ -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,6 +467,17 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
466
467
|
**stats,
|
|
467
468
|
)
|
|
468
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:
|
|
473
|
+
api.emit_round_grace_extended(
|
|
474
|
+
log_dir,
|
|
475
|
+
round_num=round_num,
|
|
476
|
+
grace_s=cfg.runtime.max_grace_after_result_s,
|
|
477
|
+
live_children=live,
|
|
478
|
+
ignored_children=ignored,
|
|
479
|
+
)
|
|
480
|
+
|
|
469
481
|
result = agent_runtime.run(
|
|
470
482
|
command=cfg.agent.command,
|
|
471
483
|
prompt_arg_template=cfg.agent.prompt_arg_template,
|
|
@@ -476,6 +488,8 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
476
488
|
max_grace_after_result_s=cfg.runtime.max_grace_after_result_s,
|
|
477
489
|
progress_callback=_progress_emit,
|
|
478
490
|
progress_interval_s=cfg.monitor.round_progress_interval_s,
|
|
491
|
+
on_grace_extended=_grace_extended_emit,
|
|
492
|
+
grace_kill_ignore_patterns=grace_kill_ignore_patterns,
|
|
479
493
|
)
|
|
480
494
|
events.emit(
|
|
481
495
|
log_dir,
|
|
@@ -549,6 +563,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
549
563
|
log_dir,
|
|
550
564
|
round_num=round_num,
|
|
551
565
|
grace_s=cfg.runtime.max_grace_after_result_s,
|
|
566
|
+
live_children=result.grace_kill_children,
|
|
552
567
|
)
|
|
553
568
|
elif result.timed_out:
|
|
554
569
|
events.emit(
|
|
@@ -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,53 @@
|
|
|
1
|
+
# Migrating to 0.1.38
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install --upgrade cli-agent-runner==0.1.38
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No action or config change. If you use `max_grace_after_result_s`, grace-kill
|
|
10
|
+
now distinguishes a hung agent from a still-busy one.
|
|
11
|
+
|
|
12
|
+
## What changed
|
|
13
|
+
|
|
14
|
+
Previously, grace-kill reaped the whole process group `max_grace_after_result_s`
|
|
15
|
+
seconds after the agent emitted `type=result`, with no awareness of child
|
|
16
|
+
processes. A round that backgrounded a long build and emitted `type=result`
|
|
17
|
+
("waiting for build…") could have the still-running build reaped.
|
|
18
|
+
|
|
19
|
+
Now, at grace expiry, agent-runner checks for live worker processes in the
|
|
20
|
+
agent's process group:
|
|
21
|
+
|
|
22
|
+
- **No live workers** → genuine hang → reaped (`round_grace_kill`, as before).
|
|
23
|
+
- **A live worker** (e.g. a build) → not reaped; agent-runner emits
|
|
24
|
+
`round_grace_extended` once and waits until the round finishes or hits the
|
|
25
|
+
`round_timeout_s` wall-clock ceiling.
|
|
26
|
+
|
|
27
|
+
The check re-runs each poll tick, so once a backgrounded worker exits, a still-
|
|
28
|
+
stuck agent is reaped promptly rather than waiting out `round_timeout_s`.
|
|
29
|
+
|
|
30
|
+
## Three distinct outcomes
|
|
31
|
+
|
|
32
|
+
- `round_grace_extended` — grace elapsed but a worker is still running (busy).
|
|
33
|
+
- `round_grace_kill` — grace elapsed and the process group is idle (hang).
|
|
34
|
+
- `round_timeout_kill` — wall-clock `round_timeout_s` exceeded (hard ceiling).
|
|
35
|
+
|
|
36
|
+
## Recommended agent contract
|
|
37
|
+
|
|
38
|
+
Treat the safety net as a net, not a crutch: emit `type=result` only when the
|
|
39
|
+
turn is truly done. Run long builds/tests foreground and commit before ending
|
|
40
|
+
the turn, rather than backgrounding work past `type=result`.
|
|
41
|
+
|
|
42
|
+
## Known limitation
|
|
43
|
+
|
|
44
|
+
The "live worker" check treats *any* live non-zombie descendant as busy. If
|
|
45
|
+
your agent keeps persistent helper subprocesses alive past `type=result` (e.g.
|
|
46
|
+
MCP servers), grace-kill will defer hangs to the `round_timeout_s` ceiling. This
|
|
47
|
+
never false-kills; the `round_grace_extended` events make it visible if it
|
|
48
|
+
happens.
|
|
49
|
+
|
|
50
|
+
## What did NOT change
|
|
51
|
+
|
|
52
|
+
- `max_grace_after_result_s` config, default, and `0 = disabled`.
|
|
53
|
+
- `round_timeout_s` wall-clock kill (still the hard ceiling and backstop).
|
|
@@ -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.
|