cli-agent-runner 0.1.30__tar.gz → 0.1.32__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.30 → cli_agent_runner-0.1.32}/CHANGELOG.md +20 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/PKG-INFO +6 -5
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/README.md +5 -4
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/_emit.py +75 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/agent_runtime.py +60 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/api.py +6 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/api_types.py +1 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/builtin_plugins/claude_rate_limit.py +71 -8
- cli_agent_runner-0.1.32/agent_runner/cli/peek_cmd.py +156 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/config.py +49 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/events.py +3 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/hooks.py +14 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/monitor.py +63 -7
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/runner.py +22 -1
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/architecture.md +4 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/commands.md +1 -1
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/configuration.md +33 -3
- cli_agent_runner-0.1.32/docs/migrations/0.1.31.md +89 -0
- cli_agent_runner-0.1.32/docs/migrations/0.1.32.md +102 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/runbook.md +5 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/_test_helpers.py +6 -0
- cli_agent_runner-0.1.32/tests/integration/test_grace_kill_emission.py +79 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_architecture.py +2 -1
- cli_agent_runner-0.1.32/tests/unit/test_agent_runtime_grace.py +72 -0
- cli_agent_runner-0.1.32/tests/unit/test_agent_runtime_progress.py +70 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_claude_error_detector.py +69 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_config.py +144 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_docgen.py +2 -2
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_hooks.py +51 -0
- cli_agent_runner-0.1.32/tests/unit/test_monitor_detect_anomaly_repetitive.py +71 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_monitor_detectors.py +2 -1
- cli_agent_runner-0.1.32/tests/unit/test_peek_select.py +76 -0
- cli_agent_runner-0.1.30/agent_runner/cli/peek_cmd.py +0 -81
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.githooks/commit-msg +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.gitignore +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/LICENSE +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/README.zh.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/build.sh +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/README.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/events.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/plugins.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/docs/thesis.md +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.30 → cli_agent_runner-0.1.32}/tests/unit/test_vcs_state.py +0 -0
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.32] - 2026-05-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `[monitor] round_progress_interval_s` — heartbeat `round_progress` event during a live round (log_size_kb, last_write_age_s, wall_age_s). Default 0 = disabled.
|
|
14
|
+
- `agent-runner peek --select events.<kind> [--window N]` — native selector for events.jsonl query (replaces `tail -F | grep | jq` patterns). Current-month only.
|
|
15
|
+
- `[monitor.host_health]` config section — `mem_avail_min_mb` / `disk_warning_pct` / `disk_critical_pct` thresholds for `mem_pressure` / `disk_warning` / `disk_critical` detectors. Defaults preserve prior behavior.
|
|
16
|
+
- New monitor detector `anomaly_repetitive_active` (notify) — alerts when `anomaly_repetitive_tool` events fire in recent rounds. Detector count 10 → 11.
|
|
17
|
+
|
|
18
|
+
See `docs/migrations/0.1.32.md`.
|
|
19
|
+
|
|
20
|
+
## [0.1.31] - 2026-05-18
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `[runtime] max_grace_after_result_s` — kill subprocess that emits `type=result` then sits silent for N seconds (0 = disabled). New `round_grace_kill` event distinguishes from regular `round_timeout_kill`.
|
|
24
|
+
- `[monitor] anomaly_repetitive_window` + `anomaly_repetitive_threshold` — claude plugin emits `anomaly_repetitive_tool` event when same (tool, target) appears ≥threshold times in window-sized tool stream. Specific scar pattern; both 0 = disabled.
|
|
25
|
+
- `[runtime] dry_run` boolean exposed via `HookContext.dry_run` for plugin authors to skip side-effect actions when set.
|
|
26
|
+
- Documentation: TOML changes require daemon restart (no hot-reload); phase rotation indexing clarification.
|
|
27
|
+
|
|
28
|
+
See `docs/migrations/0.1.31.md`.
|
|
29
|
+
|
|
10
30
|
## [0.1.30] - 2026-05-18
|
|
11
31
|
|
|
12
32
|
### 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.32
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -49,7 +49,7 @@ full disks, runaway memory.
|
|
|
49
49
|
|
|
50
50
|
```
|
|
51
51
|
┌──────────────────────────────────────────┐
|
|
52
|
-
│ Layer 3: The Witness (monitor) │
|
|
52
|
+
│ Layer 3: The Witness (monitor) │ 11 detectors + auto-stop
|
|
53
53
|
├──────────────────────────────────────────┤
|
|
54
54
|
│ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
|
|
55
55
|
├──────────────────────────────────────────┤
|
|
@@ -86,7 +86,7 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
|
86
86
|
|---|---|
|
|
87
87
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
88
88
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
89
|
-
| `restart` / `status` | `monitor` —
|
|
89
|
+
| `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
|
|
90
90
|
| `round` / `serve` | |
|
|
91
91
|
|
|
92
92
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
@@ -106,10 +106,11 @@ guards it. Highlights:
|
|
|
106
106
|
|
|
107
107
|
Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
108
108
|
|
|
109
|
-
## Monitor:
|
|
109
|
+
## Monitor: 11 detectors
|
|
110
110
|
|
|
111
111
|
Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
|
|
112
|
-
`mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active
|
|
112
|
+
`mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
|
|
113
|
+
`anomaly_repetitive_active`.
|
|
113
114
|
|
|
114
115
|
**Auto-stop the service** (continuing is harmful):
|
|
115
116
|
- `oauth_fail` — burning API quota on auth-rejected rounds
|
|
@@ -12,7 +12,7 @@ full disks, runaway memory.
|
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
┌──────────────────────────────────────────┐
|
|
15
|
-
│ Layer 3: The Witness (monitor) │
|
|
15
|
+
│ Layer 3: The Witness (monitor) │ 11 detectors + auto-stop
|
|
16
16
|
├──────────────────────────────────────────┤
|
|
17
17
|
│ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
|
|
18
18
|
├──────────────────────────────────────────┤
|
|
@@ -49,7 +49,7 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
|
49
49
|
|---|---|
|
|
50
50
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
51
51
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
52
|
-
| `restart` / `status` | `monitor` —
|
|
52
|
+
| `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
|
|
53
53
|
| `round` / `serve` | |
|
|
54
54
|
|
|
55
55
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
@@ -69,10 +69,11 @@ guards it. Highlights:
|
|
|
69
69
|
|
|
70
70
|
Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
|
|
71
71
|
|
|
72
|
-
## Monitor:
|
|
72
|
+
## Monitor: 11 detectors
|
|
73
73
|
|
|
74
74
|
Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
|
|
75
|
-
`mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active
|
|
75
|
+
`mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
|
|
76
|
+
`anomaly_repetitive_active`.
|
|
76
77
|
|
|
77
78
|
**Auto-stop the service** (continuing is harmful):
|
|
78
79
|
- `oauth_fail` — burning API quota on auth-rejected rounds
|
|
@@ -15,9 +15,12 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"emit_agent_usage_recorded",
|
|
18
|
+
"emit_anomaly_repetitive_tool",
|
|
18
19
|
"emit_fresh_eyes_round_triggered",
|
|
19
20
|
"emit_max_rounds_reached",
|
|
20
21
|
"emit_rate_limit_stop",
|
|
22
|
+
"emit_round_grace_kill",
|
|
23
|
+
"emit_round_progress",
|
|
21
24
|
"emit_round_substrate_after",
|
|
22
25
|
"emit_round_substrate_before",
|
|
23
26
|
"emit_stop_file_detected",
|
|
@@ -199,6 +202,78 @@ def emit_agent_usage_recorded(
|
|
|
199
202
|
)
|
|
200
203
|
|
|
201
204
|
|
|
205
|
+
def emit_round_progress(
|
|
206
|
+
log_dir: Path,
|
|
207
|
+
*,
|
|
208
|
+
round_num: int,
|
|
209
|
+
log_size_kb: int,
|
|
210
|
+
last_write_age_s: int,
|
|
211
|
+
wall_age_s: int,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Mid-round heartbeat event when round_progress_interval_s > 0.
|
|
214
|
+
|
|
215
|
+
Emitted periodically during a live round to surface visibility on long
|
|
216
|
+
rounds: log_size_kb shows writing activity; last_write_age_s and wall_age_s
|
|
217
|
+
together distinguish "agent thinking" from "agent stuck".
|
|
218
|
+
"""
|
|
219
|
+
from agent_runner.events import ROUND_PROGRESS, emit
|
|
220
|
+
|
|
221
|
+
emit(
|
|
222
|
+
log_dir,
|
|
223
|
+
ROUND_PROGRESS,
|
|
224
|
+
round_num=round_num,
|
|
225
|
+
log_size_kb=log_size_kb,
|
|
226
|
+
last_write_age_s=last_write_age_s,
|
|
227
|
+
wall_age_s=wall_age_s,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def emit_round_grace_kill(
|
|
232
|
+
log_dir: Path,
|
|
233
|
+
*,
|
|
234
|
+
round_num: int,
|
|
235
|
+
grace_s: int,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Emit when subprocess killed because grace-after-result timer expired.
|
|
238
|
+
|
|
239
|
+
Subprocess emitted type=result in JSONL log then sat silent for longer
|
|
240
|
+
than max_grace_after_result_s seconds. Distinguishes from round_timeout_kill
|
|
241
|
+
(wall-clock exceeded without result event).
|
|
242
|
+
"""
|
|
243
|
+
from agent_runner.events import ROUND_GRACE_KILL, emit
|
|
244
|
+
|
|
245
|
+
emit(log_dir, ROUND_GRACE_KILL, round_num=round_num, grace_s=grace_s)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def emit_anomaly_repetitive_tool(
|
|
249
|
+
log_dir: Path,
|
|
250
|
+
*,
|
|
251
|
+
round_num: int,
|
|
252
|
+
tool_name: str,
|
|
253
|
+
target: str | None,
|
|
254
|
+
count: int,
|
|
255
|
+
window: int,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Emit when claude plugin detects the same (tool, target) tuple repeated
|
|
258
|
+
>= threshold times in a sliding window of tool-call events.
|
|
259
|
+
|
|
260
|
+
Claude-only (gemini JSONL stats summary does not expose per-tool events).
|
|
261
|
+
Default OFF: both anomaly_repetitive_window and anomaly_repetitive_threshold
|
|
262
|
+
must be > 0 in [monitor] config to activate.
|
|
263
|
+
"""
|
|
264
|
+
from agent_runner.events import ANOMALY_REPETITIVE_TOOL, emit
|
|
265
|
+
|
|
266
|
+
emit(
|
|
267
|
+
log_dir,
|
|
268
|
+
ANOMALY_REPETITIVE_TOOL,
|
|
269
|
+
round_num=round_num,
|
|
270
|
+
tool_name=tool_name,
|
|
271
|
+
target=target,
|
|
272
|
+
count=count,
|
|
273
|
+
window=window,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
202
277
|
def emit_transient_error_backoff_capped(
|
|
203
278
|
log_dir: Path,
|
|
204
279
|
*,
|
|
@@ -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.32'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 32)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -27,6 +27,7 @@ class RunResult:
|
|
|
27
27
|
duration_s: float
|
|
28
28
|
timed_out: bool
|
|
29
29
|
pid: int
|
|
30
|
+
killed_for_grace: bool = False
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
|
|
@@ -53,6 +54,12 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
|
|
|
53
54
|
pass
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
# Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
|
|
58
|
+
# A future CLI variant emitting `{"type": "result", ...}` (with space) would
|
|
59
|
+
# bypass this scan; revisit if that happens.
|
|
60
|
+
_RESULT_MARKER = b'"type":"result"'
|
|
61
|
+
|
|
62
|
+
|
|
56
63
|
def run(
|
|
57
64
|
*,
|
|
58
65
|
command: list[str],
|
|
@@ -61,16 +68,29 @@ def run(
|
|
|
61
68
|
timeout_s: int,
|
|
62
69
|
log_path: Path,
|
|
63
70
|
env_extra: dict[str, str],
|
|
71
|
+
max_grace_after_result_s: int = 0,
|
|
72
|
+
progress_callback: Callable[[dict], None] | None = None,
|
|
73
|
+
progress_interval_s: int = 0,
|
|
64
74
|
) -> RunResult:
|
|
65
75
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
66
76
|
|
|
67
77
|
Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
|
|
78
|
+
|
|
79
|
+
max_grace_after_result_s: when > 0, start a countdown after the first
|
|
80
|
+
type=result event is detected in the log; kill if subprocess is still
|
|
81
|
+
running after this many seconds (HUNG defense). 0 = disabled.
|
|
82
|
+
|
|
83
|
+
progress_callback: when not None and progress_interval_s > 0, called every
|
|
84
|
+
progress_interval_s seconds with a dict of log stats (log_size_kb,
|
|
85
|
+
last_write_age_s, wall_age_s). Keeps agent_runtime event-free; callers
|
|
86
|
+
build the callback to emit events.
|
|
68
87
|
"""
|
|
69
88
|
argv = _build_argv(command, prompt_arg_template, prompt)
|
|
70
89
|
env = {**os.environ, **env_extra}
|
|
71
90
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
91
|
log_file = log_path.open("w", encoding="utf-8")
|
|
73
92
|
start = time.time()
|
|
93
|
+
last_progress_at = start
|
|
74
94
|
proc = subprocess.Popen(
|
|
75
95
|
argv,
|
|
76
96
|
env=env,
|
|
@@ -79,6 +99,7 @@ def run(
|
|
|
79
99
|
stderr=subprocess.STDOUT,
|
|
80
100
|
start_new_session=True,
|
|
81
101
|
)
|
|
102
|
+
result_seen_at: float | None = None
|
|
82
103
|
try:
|
|
83
104
|
while True:
|
|
84
105
|
ret = proc.poll()
|
|
@@ -93,6 +114,45 @@ def run(
|
|
|
93
114
|
return RunResult(
|
|
94
115
|
exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
|
|
95
116
|
)
|
|
117
|
+
# Grace kill: result emitted but subprocess still running
|
|
118
|
+
if max_grace_after_result_s > 0:
|
|
119
|
+
if result_seen_at is None:
|
|
120
|
+
# Cheap check: byte-scan log for marker substring
|
|
121
|
+
try:
|
|
122
|
+
with log_path.open("rb") as f:
|
|
123
|
+
if _RESULT_MARKER in f.read():
|
|
124
|
+
result_seen_at = now
|
|
125
|
+
except OSError:
|
|
126
|
+
pass # log not flushed yet; check next tick
|
|
127
|
+
if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
|
|
128
|
+
_kill_pgroup(proc)
|
|
129
|
+
duration = time.time() - start
|
|
130
|
+
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
131
|
+
return RunResult(
|
|
132
|
+
exit_code=exit_code,
|
|
133
|
+
duration_s=duration,
|
|
134
|
+
timed_out=True,
|
|
135
|
+
pid=proc.pid,
|
|
136
|
+
killed_for_grace=True,
|
|
137
|
+
)
|
|
138
|
+
# Progress heartbeat: call back if interval elapsed
|
|
139
|
+
if progress_callback is not None and progress_interval_s > 0:
|
|
140
|
+
if now - last_progress_at >= progress_interval_s:
|
|
141
|
+
try:
|
|
142
|
+
st = log_path.stat()
|
|
143
|
+
log_size_kb = st.st_size // 1024
|
|
144
|
+
last_write_age_s = max(0, int(now - st.st_mtime))
|
|
145
|
+
except OSError:
|
|
146
|
+
log_size_kb = 0
|
|
147
|
+
last_write_age_s = 0
|
|
148
|
+
progress_callback(
|
|
149
|
+
{
|
|
150
|
+
"log_size_kb": log_size_kb,
|
|
151
|
+
"last_write_age_s": last_write_age_s,
|
|
152
|
+
"wall_age_s": int(now - start),
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
last_progress_at = now
|
|
96
156
|
time.sleep(0.2)
|
|
97
157
|
finally:
|
|
98
158
|
log_file.close()
|
|
@@ -455,6 +455,9 @@ def _poll_once(project: str | Path, *, host: str | None) -> list[monitor.Alert]:
|
|
|
455
455
|
auth_fail_patterns=cfg.monitor.auth_fail_patterns,
|
|
456
456
|
auth_fail_hint=cfg.monitor.auth_fail_hint,
|
|
457
457
|
phases_overrides=cfg.phases.overrides if cfg.phases.overrides else None,
|
|
458
|
+
mem_avail_min_mb=cfg.monitor.host_health.mem_avail_min_mb,
|
|
459
|
+
disk_warning_pct=cfg.monitor.host_health.disk_warning_pct,
|
|
460
|
+
disk_critical_pct=cfg.monitor.host_health.disk_critical_pct,
|
|
458
461
|
)
|
|
459
462
|
if not monitor._PLUGIN_DETECTORS:
|
|
460
463
|
return builtin # skip ProjectState assembly when no plugins to feed
|
|
@@ -725,9 +728,12 @@ def check_self_terminated_sentinel(log_dir: Path) -> bool:
|
|
|
725
728
|
# Preserves the public import surface: `from agent_runner.api import emit_*` continues to work.
|
|
726
729
|
from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-export
|
|
727
730
|
emit_agent_usage_recorded,
|
|
731
|
+
emit_anomaly_repetitive_tool,
|
|
728
732
|
emit_fresh_eyes_round_triggered,
|
|
729
733
|
emit_max_rounds_reached,
|
|
730
734
|
emit_rate_limit_stop,
|
|
735
|
+
emit_round_grace_kill,
|
|
736
|
+
emit_round_progress,
|
|
731
737
|
emit_round_substrate_after,
|
|
732
738
|
emit_round_substrate_before,
|
|
733
739
|
emit_stop_file_detected,
|
|
@@ -23,6 +23,7 @@ from typing import Any
|
|
|
23
23
|
|
|
24
24
|
from agent_runner.api import (
|
|
25
25
|
emit_agent_usage_recorded,
|
|
26
|
+
emit_anomaly_repetitive_tool,
|
|
26
27
|
emit_transient_error_detected,
|
|
27
28
|
)
|
|
28
29
|
from agent_runner.builtin_plugins._constants import (
|
|
@@ -45,7 +46,11 @@ class ClaudeErrorDetector:
|
|
|
45
46
|
log_path = ctx.agent_log_path
|
|
46
47
|
if log_path is None or not log_path.exists():
|
|
47
48
|
return
|
|
48
|
-
parsed = _parse_claude_log(
|
|
49
|
+
parsed = _parse_claude_log(
|
|
50
|
+
log_path,
|
|
51
|
+
anomaly_window=ctx.anomaly_repetitive_window,
|
|
52
|
+
anomaly_threshold=ctx.anomaly_repetitive_threshold,
|
|
53
|
+
)
|
|
49
54
|
|
|
50
55
|
if parsed.get("transient_error"):
|
|
51
56
|
te = parsed["transient_error"]
|
|
@@ -60,18 +65,68 @@ class ClaudeErrorDetector:
|
|
|
60
65
|
**parsed["usage"],
|
|
61
66
|
)
|
|
62
67
|
|
|
68
|
+
if parsed.get("anomaly"):
|
|
69
|
+
emit_anomaly_repetitive_tool(ctx.log_dir, round_num=ctx.round_num, **parsed["anomaly"])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_tool_target(tool_input: Any) -> str | None:
|
|
73
|
+
"""Best-effort primary-input extraction for repetition detection."""
|
|
74
|
+
if not isinstance(tool_input, dict):
|
|
75
|
+
return None
|
|
76
|
+
for key in ("file_path", "path", "pattern", "command"):
|
|
77
|
+
v = tool_input.get(key)
|
|
78
|
+
if isinstance(v, str):
|
|
79
|
+
return v[:100] # truncate long commands
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _detect_repetitive_tool(
|
|
84
|
+
tool_calls: list[tuple[str, str | None]],
|
|
85
|
+
*,
|
|
86
|
+
window: int,
|
|
87
|
+
threshold: int,
|
|
88
|
+
) -> dict | None:
|
|
89
|
+
"""Slide a window of size ``window`` over tool_calls; return anomaly dict
|
|
90
|
+
when any (tool_name, target) tuple appears >= threshold times."""
|
|
91
|
+
if window <= 0 or threshold <= 0 or len(tool_calls) < threshold:
|
|
92
|
+
return None
|
|
93
|
+
from collections import Counter
|
|
63
94
|
|
|
64
|
-
|
|
95
|
+
n = len(tool_calls)
|
|
96
|
+
for start in range(max(0, n - window), n - threshold + 1):
|
|
97
|
+
window_slice = tool_calls[start : start + window]
|
|
98
|
+
if not window_slice:
|
|
99
|
+
continue
|
|
100
|
+
counts = Counter(window_slice)
|
|
101
|
+
most_common_tuple, most_common_count = counts.most_common(1)[0]
|
|
102
|
+
if most_common_count >= threshold:
|
|
103
|
+
return {
|
|
104
|
+
"tool_name": most_common_tuple[0],
|
|
105
|
+
"target": most_common_tuple[1],
|
|
106
|
+
"count": most_common_count,
|
|
107
|
+
"window": window,
|
|
108
|
+
}
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _parse_claude_log(
|
|
113
|
+
log_path: Path,
|
|
114
|
+
*,
|
|
115
|
+
anomaly_window: int = 0,
|
|
116
|
+
anomaly_threshold: int = 0,
|
|
117
|
+
) -> dict[str, Any]:
|
|
65
118
|
"""Scan last _TAIL_LINES for rate_limit/result/assistant events.
|
|
66
119
|
|
|
67
|
-
Returns dict with optional 'transient_error' and '
|
|
120
|
+
Returns dict with optional 'transient_error', 'usage', and 'anomaly' keys.
|
|
121
|
+
anomaly_window/anomaly_threshold: when both > 0, slide a window over
|
|
122
|
+
(tool_name, target) tuples; populate 'anomaly' if threshold reached.
|
|
68
123
|
"""
|
|
69
124
|
with log_path.open("r", encoding="utf-8", errors="replace") as f:
|
|
70
125
|
tail = deque(f, maxlen=_TAIL_LINES)
|
|
71
126
|
rate_limit_info: dict | None = None
|
|
72
127
|
result_event: dict | None = None
|
|
73
128
|
assistant_model: str | None = None
|
|
74
|
-
|
|
129
|
+
tool_calls: list[tuple[str, str | None]] = []
|
|
75
130
|
for line in tail:
|
|
76
131
|
line = line.strip()
|
|
77
132
|
if not line:
|
|
@@ -94,9 +149,11 @@ def _parse_claude_log(log_path: Path) -> dict[str, Any]:
|
|
|
94
149
|
assistant_model = str(model_val)
|
|
95
150
|
content = msg.get("content", []) if isinstance(msg, dict) else []
|
|
96
151
|
if isinstance(content, list):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
152
|
+
for c in content:
|
|
153
|
+
if isinstance(c, dict) and c.get("type") == "tool_use":
|
|
154
|
+
tool_calls.append(
|
|
155
|
+
(str(c.get("name", "?")), _extract_tool_target(c.get("input", {})))
|
|
156
|
+
)
|
|
100
157
|
|
|
101
158
|
out: dict[str, Any] = {}
|
|
102
159
|
|
|
@@ -106,11 +163,17 @@ def _parse_claude_log(log_path: Path) -> dict[str, Any]:
|
|
|
106
163
|
|
|
107
164
|
if result_event is not None:
|
|
108
165
|
usage_payload = _extract_usage(
|
|
109
|
-
result_event, model=assistant_model, tool_call_count=
|
|
166
|
+
result_event, model=assistant_model, tool_call_count=len(tool_calls)
|
|
110
167
|
)
|
|
111
168
|
if usage_payload is not None:
|
|
112
169
|
out["usage"] = usage_payload
|
|
113
170
|
|
|
171
|
+
anomaly = _detect_repetitive_tool(
|
|
172
|
+
tool_calls, window=anomaly_window, threshold=anomaly_threshold
|
|
173
|
+
)
|
|
174
|
+
if anomaly is not None:
|
|
175
|
+
out["anomaly"] = anomaly
|
|
176
|
+
|
|
114
177
|
return out
|
|
115
178
|
|
|
116
179
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""peek and watch subcommands — snapshot + auto-refresh."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from agent_runner import api
|
|
13
|
+
from agent_runner.cli.common import emit, fail, work_dir_from_args
|
|
14
|
+
from agent_runner.config import load_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _round_arg(s: str) -> int | str:
|
|
18
|
+
if s == "latest":
|
|
19
|
+
return s
|
|
20
|
+
try:
|
|
21
|
+
return int(s)
|
|
22
|
+
except ValueError as e:
|
|
23
|
+
raise argparse.ArgumentTypeError(f"--round expects int or 'latest', got {s!r}") from e
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _positive_int(s: str) -> int:
|
|
27
|
+
try:
|
|
28
|
+
n = int(s)
|
|
29
|
+
except ValueError as e:
|
|
30
|
+
raise argparse.ArgumentTypeError(f"expects positive int, got {s!r}") from e
|
|
31
|
+
if n <= 0:
|
|
32
|
+
raise argparse.ArgumentTypeError(f"expects positive int (> 0), got {n}")
|
|
33
|
+
return n
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def add_parser(sub, parent) -> None:
|
|
37
|
+
for verb, fn in (("peek", cmd_peek), ("watch", cmd_watch)):
|
|
38
|
+
p = sub.add_parser(
|
|
39
|
+
verb, parents=[parent], help=f"{verb} project state with optional drill-down"
|
|
40
|
+
)
|
|
41
|
+
p.add_argument(
|
|
42
|
+
"--round",
|
|
43
|
+
type=_round_arg,
|
|
44
|
+
default=None,
|
|
45
|
+
metavar="N",
|
|
46
|
+
help="Drill into round N (int or 'latest')",
|
|
47
|
+
)
|
|
48
|
+
p.add_argument("--log", action="store_true", help="Include current round's log tail")
|
|
49
|
+
p.add_argument(
|
|
50
|
+
"--events", type=int, default=None, metavar="N", help="Include last N events"
|
|
51
|
+
)
|
|
52
|
+
p.add_argument(
|
|
53
|
+
"--select",
|
|
54
|
+
type=str,
|
|
55
|
+
default=None,
|
|
56
|
+
help=(
|
|
57
|
+
"Selector: 'events.<kind>' queries events.jsonl (current month); "
|
|
58
|
+
"dot-path (e.g. system.disk_used_pct) extracts a subtree from peek state."
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
p.add_argument(
|
|
62
|
+
"--window",
|
|
63
|
+
type=_positive_int,
|
|
64
|
+
default=10,
|
|
65
|
+
metavar="N",
|
|
66
|
+
help="Max entries returned by --select events.<kind> (default 10, must be > 0).",
|
|
67
|
+
)
|
|
68
|
+
if verb == "watch":
|
|
69
|
+
p.add_argument(
|
|
70
|
+
"--interval",
|
|
71
|
+
type=int,
|
|
72
|
+
default=2,
|
|
73
|
+
metavar="SECONDS",
|
|
74
|
+
help="Refresh interval (default 2)",
|
|
75
|
+
)
|
|
76
|
+
p.set_defaults(func=fn)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run_events_select(
|
|
80
|
+
log_dir: Path, *, kind: str, window: int, month_tag: str | None = None
|
|
81
|
+
) -> list[dict]:
|
|
82
|
+
"""Read current-month events.jsonl and return last ``window`` events of ``kind``.
|
|
83
|
+
|
|
84
|
+
Only the current month's file is scanned (documented limitation).
|
|
85
|
+
Returns an empty list when the file is missing or no matching events found.
|
|
86
|
+
"""
|
|
87
|
+
if month_tag is None:
|
|
88
|
+
month_tag = datetime.now(UTC).strftime("%Y-%m")
|
|
89
|
+
events_file = log_dir / f"events-{month_tag}.jsonl"
|
|
90
|
+
if not events_file.exists():
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
matches: list[dict] = []
|
|
94
|
+
with events_file.open("r", encoding="utf-8") as f:
|
|
95
|
+
for line in f:
|
|
96
|
+
line = line.strip()
|
|
97
|
+
if not line:
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
evt = json.loads(line)
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
continue
|
|
103
|
+
if evt.get("event") == kind:
|
|
104
|
+
matches.append(evt)
|
|
105
|
+
|
|
106
|
+
return matches[-window:]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_peek(args) -> int:
|
|
110
|
+
select = args.select
|
|
111
|
+
# Intercept events.<kind> selector before passing to regular peek logic.
|
|
112
|
+
if select is not None and select.startswith("events."):
|
|
113
|
+
kind = select[len("events.") :]
|
|
114
|
+
if not kind:
|
|
115
|
+
print("Error: --select events.<kind> requires a non-empty kind", file=sys.stderr)
|
|
116
|
+
return 2
|
|
117
|
+
try:
|
|
118
|
+
cfg = load_config(work_dir_from_args(args) / "agent-runner.toml")
|
|
119
|
+
except FileNotFoundError as e:
|
|
120
|
+
return fail(f"config not found: {e}")
|
|
121
|
+
log_dir = cfg.runtime.log_dir
|
|
122
|
+
window = getattr(args, "window", 10) or 10
|
|
123
|
+
matches = _run_events_select(log_dir, kind=kind, window=window)
|
|
124
|
+
if getattr(args, "json", False):
|
|
125
|
+
print(json.dumps(matches, default=str))
|
|
126
|
+
else:
|
|
127
|
+
for m in matches:
|
|
128
|
+
print(json.dumps(m, default=str))
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
result = api.peek(
|
|
133
|
+
work_dir_from_args(args),
|
|
134
|
+
round=args.round,
|
|
135
|
+
log=args.log,
|
|
136
|
+
events=args.events,
|
|
137
|
+
select=select,
|
|
138
|
+
)
|
|
139
|
+
except KeyError as e:
|
|
140
|
+
return fail(str(e))
|
|
141
|
+
except FileNotFoundError as e:
|
|
142
|
+
return fail(f"config not found: {e}")
|
|
143
|
+
emit(result, json_mode=getattr(args, "json", False))
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cmd_watch(args) -> int:
|
|
148
|
+
while True:
|
|
149
|
+
sys.stdout.write("\x1b[2J\x1b[H")
|
|
150
|
+
rc = cmd_peek(args)
|
|
151
|
+
if rc != 0:
|
|
152
|
+
return rc
|
|
153
|
+
try:
|
|
154
|
+
time.sleep(args.interval)
|
|
155
|
+
except KeyboardInterrupt:
|
|
156
|
+
return 0
|