cli-agent-runner 0.2.0__tar.gz → 0.2.1__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.2.0 → cli_agent_runner-0.2.1}/CHANGELOG.md +11 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/PKG-INFO +1 -1
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/agent_runtime.py +47 -2
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/config.py +15 -1
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/claude.toml +2 -1
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/runner.py +19 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/configuration.md +15 -0
- cli_agent_runner-0.2.1/docs/migrations/0.2.1.md +77 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/runbook.md +6 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/_test_helpers.py +46 -0
- cli_agent_runner-0.2.1/tests/integration/test_prompt_delivery_stdin.py +103 -0
- cli_agent_runner-0.2.1/tests/unit/test_agent_runtime_signal_name.py +21 -0
- cli_agent_runner-0.2.1/tests/unit/test_config_prompt_delivery.py +33 -0
- cli_agent_runner-0.2.1/tests/unit/test_exit_cause.py +15 -0
- cli_agent_runner-0.2.1/tests/unit/test_network_blip_signal_guard.py +47 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_presets.py +19 -3
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.gitignore +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/LICENSE +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/README.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/README.zh.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_emit.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/build.sh +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/README.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/architecture.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/events.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.40.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.42.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.2.0.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_vcs_state.py +0 -0
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.1] - 2026-07-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `[agent] prompt_delivery = "argv" | "stdin"` (default `argv`). `stdin` pipes the prompt to the agent's stdin instead of placing it in argv, closing a `pkill -f <token>` self-kill footgun (a background worker matching on a token from the prompt could match the agent's own argv). The `claude` preset now defaults to `stdin`; existing configs are unaffected.
|
|
12
|
+
- `agent_exit` gains `exit_cause` (`clean` / `error` / `signal:<NAME>` / `timeout`) so monitors can distinguish a clean exit from a signal-kill without a heuristic.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- A signal-killed round is no longer misclassified as `agent_network_blip` when its log happens to contain a network-error string; signal deaths are terminations, not network blips.
|
|
16
|
+
|
|
17
|
+
See `docs/migrations/0.2.1.md`.
|
|
18
|
+
|
|
8
19
|
## [0.2.0] - 2026-07-02
|
|
9
20
|
|
|
10
21
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -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.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -14,6 +14,7 @@ import os
|
|
|
14
14
|
import re
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess # noqa: TID251 — sanctioned subprocess caller
|
|
17
|
+
import threading
|
|
17
18
|
import time
|
|
18
19
|
from collections.abc import Callable
|
|
19
20
|
from dataclasses import dataclass, field
|
|
@@ -24,6 +25,25 @@ import psutil
|
|
|
24
25
|
REAP_GRACE_S = 5
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
def signal_name(exit_code: int) -> str | None:
|
|
29
|
+
"""Signal name for a signal death, else None.
|
|
30
|
+
|
|
31
|
+
Handles both the Python-negative form (``-15``) and the shell ``128+N``
|
|
32
|
+
form (``143``). Returns None for a non-signal exit — including a legitimate
|
|
33
|
+
``> 128`` code (e.g. 200) that is not a valid signal number.
|
|
34
|
+
"""
|
|
35
|
+
if exit_code < 0:
|
|
36
|
+
n = -exit_code
|
|
37
|
+
elif exit_code > 128:
|
|
38
|
+
n = exit_code - 128
|
|
39
|
+
else:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
return signal.Signals(n).name
|
|
43
|
+
except ValueError:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
27
47
|
@dataclass(frozen=True)
|
|
28
48
|
class RunResult:
|
|
29
49
|
exit_code: int
|
|
@@ -116,6 +136,7 @@ def run(
|
|
|
116
136
|
command: list[str],
|
|
117
137
|
prompt_arg_template: list[str],
|
|
118
138
|
prompt: str,
|
|
139
|
+
prompt_delivery: str = "argv",
|
|
119
140
|
timeout_s: int,
|
|
120
141
|
log_path: Path,
|
|
121
142
|
env_extra: dict[str, str],
|
|
@@ -145,7 +166,16 @@ def run(
|
|
|
145
166
|
matching any pattern (re.search) are excluded from the liveness count
|
|
146
167
|
(persistent helpers that aren't real workers). None = no filtering.
|
|
147
168
|
"""
|
|
148
|
-
|
|
169
|
+
stdin_mode = prompt_delivery == "stdin"
|
|
170
|
+
# Defense-in-depth: config validation already rejects {prompt} in the
|
|
171
|
+
# template for stdin mode, but run() must be safe even if called
|
|
172
|
+
# directly with a mismatched template. In stdin mode, never substitute
|
|
173
|
+
# {prompt} into argv — build it verbatim so the prompt cannot reach argv.
|
|
174
|
+
argv = (
|
|
175
|
+
list(command) + list(prompt_arg_template)
|
|
176
|
+
if stdin_mode
|
|
177
|
+
else _build_argv(command, prompt_arg_template, prompt)
|
|
178
|
+
)
|
|
149
179
|
env = {**os.environ, **env_extra}
|
|
150
180
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
151
181
|
log_file = log_path.open("w", encoding="utf-8")
|
|
@@ -154,11 +184,26 @@ def run(
|
|
|
154
184
|
proc = subprocess.Popen(
|
|
155
185
|
argv,
|
|
156
186
|
env=env,
|
|
157
|
-
stdin=subprocess.DEVNULL,
|
|
187
|
+
stdin=subprocess.PIPE if stdin_mode else subprocess.DEVNULL,
|
|
158
188
|
stdout=log_file,
|
|
159
189
|
stderr=subprocess.STDOUT,
|
|
160
190
|
start_new_session=True,
|
|
161
191
|
)
|
|
192
|
+
if stdin_mode and proc.stdin is not None:
|
|
193
|
+
# Write+close on a daemon thread so the wall-clock timeout loop below
|
|
194
|
+
# is never blocked by a write >64KB (OS pipe buffer) to an agent that
|
|
195
|
+
# doesn't drain stdin (R1128: no unbounded-hang path).
|
|
196
|
+
stdin_pipe = proc.stdin
|
|
197
|
+
stdin_data = prompt.encode("utf-8")
|
|
198
|
+
|
|
199
|
+
def _write_stdin():
|
|
200
|
+
try:
|
|
201
|
+
stdin_pipe.write(stdin_data)
|
|
202
|
+
stdin_pipe.close()
|
|
203
|
+
except (BrokenPipeError, OSError):
|
|
204
|
+
pass # agent exited before reading stdin; the poll loop handles exit
|
|
205
|
+
|
|
206
|
+
threading.Thread(target=_write_stdin, daemon=True).start()
|
|
162
207
|
result_seen_at: float | None = None
|
|
163
208
|
grace_extended_emitted = False
|
|
164
209
|
try:
|
|
@@ -11,6 +11,7 @@ from typing import Any, Literal
|
|
|
11
11
|
_VALID_INJECTION_MODES: frozenset[str] = frozenset({"prepend", "file", "none"})
|
|
12
12
|
_VALID_DIRTY_ACTIONS: frozenset[str] = frozenset({"stash", "ignore", "auto_commit"})
|
|
13
13
|
_VALID_TRANSIENT_ERROR_ACTIONS: frozenset[str] = frozenset({"back_off", "skip", "stop"})
|
|
14
|
+
_VALID_PROMPT_DELIVERY: frozenset[str] = frozenset({"argv", "stdin"})
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ConfigError(ValueError):
|
|
@@ -23,6 +24,7 @@ class AgentConfig:
|
|
|
23
24
|
prompt_arg_template: list[str]
|
|
24
25
|
name: str | None = None
|
|
25
26
|
env: dict[str, str] = field(default_factory=dict)
|
|
27
|
+
prompt_delivery: Literal["argv", "stdin"] = "argv"
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@dataclass(frozen=True)
|
|
@@ -338,11 +340,23 @@ def load_config(toml_path: Path) -> Config:
|
|
|
338
340
|
raw = tomllib.load(f)
|
|
339
341
|
|
|
340
342
|
agent_d = raw.get("agent", {})
|
|
343
|
+
prompt_delivery = str(agent_d.get("prompt_delivery", "argv"))
|
|
344
|
+
prompt_arg_template = list(_require(agent_d, "prompt_arg_template"))
|
|
345
|
+
if prompt_delivery not in _VALID_PROMPT_DELIVERY:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f'invalid [agent] prompt_delivery {prompt_delivery!r}: use "argv" or "stdin"'
|
|
348
|
+
)
|
|
349
|
+
if prompt_delivery == "stdin" and any("{prompt}" in a for a in prompt_arg_template):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
"stdin delivery: remove {prompt} from [agent] prompt_arg_template "
|
|
352
|
+
"(the prompt is piped to stdin, not placed in argv)"
|
|
353
|
+
)
|
|
341
354
|
agent = AgentConfig(
|
|
342
355
|
command=list(_require(agent_d, "command")),
|
|
343
|
-
prompt_arg_template=
|
|
356
|
+
prompt_arg_template=prompt_arg_template,
|
|
344
357
|
name=agent_d.get("name"),
|
|
345
358
|
env={str(k): str(v) for k, v in agent_d.get("env", {}).items()},
|
|
359
|
+
prompt_delivery=prompt_delivery,
|
|
346
360
|
)
|
|
347
361
|
raw_work_dir = str(_require(raw, "runtime", "work_dir"))
|
|
348
362
|
work_dir = _expand_path(raw_work_dir, "").resolve()
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
command = ["claude", "--model", "claude-opus-4-7",
|
|
5
5
|
"--dangerously-skip-permissions",
|
|
6
6
|
"--verbose", "--output-format", "stream-json"]
|
|
7
|
-
prompt_arg_template = ["-p"
|
|
7
|
+
prompt_arg_template = ["-p"]
|
|
8
|
+
prompt_delivery = "stdin"
|
|
8
9
|
name = "claude"
|
|
9
10
|
|
|
10
11
|
[agent.env]
|
|
@@ -28,6 +28,7 @@ from agent_runner import (
|
|
|
28
28
|
startup_check,
|
|
29
29
|
vcs_state,
|
|
30
30
|
)
|
|
31
|
+
from agent_runner.agent_runtime import signal_name
|
|
31
32
|
from agent_runner.api import _primary_prompt_file, resolve_runtime_for_phase
|
|
32
33
|
from agent_runner.api import assemble_prompt as _api_assemble_prompt
|
|
33
34
|
from agent_runner.api_types import RoundResult, TransientErrorState
|
|
@@ -217,6 +218,20 @@ def _round_context_for_prompt(
|
|
|
217
218
|
return ctx
|
|
218
219
|
|
|
219
220
|
|
|
221
|
+
def _exit_cause(result: agent_runtime.RunResult) -> str:
|
|
222
|
+
"""Classify a round's exit for the ``agent_exit`` event.
|
|
223
|
+
|
|
224
|
+
Precedence: timeout > signal:<NAME> > clean > error. Timeout wins even
|
|
225
|
+
when agent-runner signal-killed the process to enforce the timeout.
|
|
226
|
+
"""
|
|
227
|
+
if result.timed_out:
|
|
228
|
+
return "timeout"
|
|
229
|
+
s = signal_name(result.exit_code)
|
|
230
|
+
if s is not None:
|
|
231
|
+
return f"signal:{s}"
|
|
232
|
+
return "clean" if result.exit_code == 0 else "error"
|
|
233
|
+
|
|
234
|
+
|
|
220
235
|
def _scan_round_log_for_network_blip(
|
|
221
236
|
*,
|
|
222
237
|
log_dir: Path,
|
|
@@ -233,6 +248,8 @@ def _scan_round_log_for_network_blip(
|
|
|
233
248
|
# Skip the I/O on the success path.
|
|
234
249
|
if result.exit_code == 0 and not result.timed_out:
|
|
235
250
|
return
|
|
251
|
+
if signal_name(result.exit_code) is not None:
|
|
252
|
+
return # signal death is a termination, not a network blip
|
|
236
253
|
try:
|
|
237
254
|
text = log_path.read_text(encoding="utf-8", errors="replace")
|
|
238
255
|
except FileNotFoundError:
|
|
@@ -486,6 +503,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
486
503
|
command=cfg.agent.command,
|
|
487
504
|
prompt_arg_template=cfg.agent.prompt_arg_template,
|
|
488
505
|
prompt=prompt,
|
|
506
|
+
prompt_delivery=cfg.agent.prompt_delivery,
|
|
489
507
|
timeout_s=timeout_s,
|
|
490
508
|
log_path=log_path,
|
|
491
509
|
env_extra={**framework_env, **dict(cfg.agent.env)},
|
|
@@ -502,6 +520,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
502
520
|
exit_code=result.exit_code,
|
|
503
521
|
duration_s=result.duration_s,
|
|
504
522
|
timed_out=result.timed_out,
|
|
523
|
+
exit_cause=_exit_cause(result),
|
|
505
524
|
)
|
|
506
525
|
|
|
507
526
|
_scan_round_log_for_network_blip(
|
|
@@ -28,6 +28,7 @@ running with newly-set `dirty_action = "auto_commit"` is undefined).
|
|
|
28
28
|
| `prompt_arg_template` | `list[str]` | — |
|
|
29
29
|
| `name` | `str | None` | None |
|
|
30
30
|
| `env` | `dict[str, str]` | {} |
|
|
31
|
+
| `prompt_delivery` | `Literal['argv', 'stdin']` | 'argv' |
|
|
31
32
|
|
|
32
33
|
### `[runtime]`
|
|
33
34
|
|
|
@@ -84,6 +85,20 @@ running with newly-set `dirty_action = "auto_commit"` is undefined).
|
|
|
84
85
|
| `supervisor_stale_threshold_s` | `int | None` | None |
|
|
85
86
|
<!-- /gen:config-schema -->
|
|
86
87
|
|
|
88
|
+
### `agent.prompt_delivery`
|
|
89
|
+
|
|
90
|
+
Type: string, one of `"argv"`, `"stdin"`
|
|
91
|
+
Default: `"argv"`
|
|
92
|
+
|
|
93
|
+
Controls how the assembled prompt reaches the agent subprocess. `"argv"`
|
|
94
|
+
(default, unchanged behavior) substitutes the prompt into
|
|
95
|
+
`prompt_arg_template` and passes it as a command-line argument. `"stdin"`
|
|
96
|
+
writes the prompt to the subprocess's stdin instead — it never appears in
|
|
97
|
+
argv, which avoids a `pkill -f <token>` self-kill if the agent's own cleanup
|
|
98
|
+
commands happen to match a token drawn from its prompt. The `claude` preset
|
|
99
|
+
defaults to `"stdin"`; existing configs are unchanged. See
|
|
100
|
+
`docs/migrations/0.2.1.md` to adopt on an existing config.
|
|
101
|
+
|
|
87
102
|
### `vcs.dirty_action`
|
|
88
103
|
|
|
89
104
|
Type: string, one of `"stash"`, `"ignore"`, `"auto_commit"`
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Migration guide: 0.2.1
|
|
2
|
+
|
|
3
|
+
## TL;DR — zero migration for existing users
|
|
4
|
+
|
|
5
|
+
`[agent] prompt_delivery` defaults to `"argv"`, the pre-0.2.1 behavior. No TOML
|
|
6
|
+
changes are required to keep your current setup working exactly as before.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## New: `[agent] prompt_delivery`
|
|
11
|
+
|
|
12
|
+
The prompt can now reach the agent two ways:
|
|
13
|
+
|
|
14
|
+
| `prompt_delivery` | Behavior |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `"argv"` (default) | Unchanged: the prompt is substituted into `prompt_arg_template` and passed as a command-line argument, as in every prior release. |
|
|
17
|
+
| `"stdin"` | The prompt is written to the agent subprocess's stdin instead. It never appears in the process's argv. |
|
|
18
|
+
|
|
19
|
+
### The `pkill -f` footgun this fixes
|
|
20
|
+
|
|
21
|
+
A round's agent sometimes needs to reap background helper processes it
|
|
22
|
+
started earlier in the same session (e.g. a long build or a watcher). A
|
|
23
|
+
tempting one-liner is `pkill -f <some-token>` — kill anything whose command
|
|
24
|
+
line matches a token. If that token also happens to appear in the *agent's
|
|
25
|
+
own* prompt (a file path, a project name, a phrase the agent echoed back),
|
|
26
|
+
and the prompt was placed in the agent's own argv, `pkill -f` can match and
|
|
27
|
+
kill the agent process itself. This was reported from a production
|
|
28
|
+
deployment: an agent's own cleanup command silently killed its own round.
|
|
29
|
+
|
|
30
|
+
`prompt_delivery = "stdin"` closes this: with the prompt off argv, a
|
|
31
|
+
`pkill -f <token>` run by the agent can no longer self-match on prompt
|
|
32
|
+
content.
|
|
33
|
+
|
|
34
|
+
**New `claude` scaffolds** (`agent-runner init --preset claude`) already
|
|
35
|
+
default to `prompt_delivery = "stdin"`.
|
|
36
|
+
|
|
37
|
+
**To adopt on an existing config**, two edits are required together:
|
|
38
|
+
|
|
39
|
+
```toml
|
|
40
|
+
[agent]
|
|
41
|
+
prompt_delivery = "stdin"
|
|
42
|
+
prompt_arg_template = ["-p"] # remove {prompt} — it is no longer substituted into argv
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Both changes are required: `prompt_delivery = "stdin"` alone does not remove
|
|
46
|
+
`{prompt}` from `prompt_arg_template` for you, and config load rejects the
|
|
47
|
+
combination of `prompt_delivery = "stdin"` with a `{prompt}` placeholder
|
|
48
|
+
still present (`ConfigError` at startup) — the prompt is piped to stdin, not
|
|
49
|
+
placed in argv.
|
|
50
|
+
|
|
51
|
+
The right-hand side of `prompt_arg_template` (flags like `-p`) is unaffected;
|
|
52
|
+
only the `{prompt}` substitution changes.
|
|
53
|
+
|
|
54
|
+
Regardless of `prompt_delivery`, agents should reap background workers via
|
|
55
|
+
process-group-aware means rather than `pkill -f <pattern>` — see
|
|
56
|
+
`docs/runbook.md`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## New: `agent_exit.exit_cause`
|
|
61
|
+
|
|
62
|
+
The `agent_exit` event gains an `exit_cause` field: one of `clean`, `error`,
|
|
63
|
+
`signal:<NAME>` (e.g. `signal:SIGTERM`), or `timeout`. Monitor/consumer code
|
|
64
|
+
that previously inferred a signal-kill from `exit_code` heuristics can read
|
|
65
|
+
`exit_cause` directly instead. Additive field; existing consumers reading
|
|
66
|
+
only `exit_code` are unaffected.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Fixed: signal-killed rounds no longer misclassified as network blips
|
|
71
|
+
|
|
72
|
+
A round terminated by a signal (e.g. a self-SIGTERM from a background
|
|
73
|
+
process) whose log happened to contain a network-error-looking string was
|
|
74
|
+
previously misclassified as `agent_network_blip`, which could feed
|
|
75
|
+
retry/back-off logic. Signal deaths are now recognized as terminations and
|
|
76
|
+
excluded from the network-blip scan. No config change; this only affects
|
|
77
|
+
which event a signal-killed round emits.
|
|
@@ -624,6 +624,12 @@ post-result hang to `round_timeout_s`. Set `[runtime] grace_kill_ignore_patterns
|
|
|
624
624
|
`round_grace_extended` event's `ignored_children` field shows which cmdlines
|
|
625
625
|
matched a pattern.
|
|
626
626
|
|
|
627
|
+
**Reaping background workers:** if an agent needs to clean up its own
|
|
628
|
+
backgrounded helper processes, use process-group-aware means (its own
|
|
629
|
+
`bg`/job-control tooling, or killing by recorded PID/pgid) — not
|
|
630
|
+
`pkill -f <pattern>`, which matches on command-line substrings and can
|
|
631
|
+
self-match the agent's own process or hit unrelated siblings.
|
|
632
|
+
|
|
627
633
|
### Disk pressure
|
|
628
634
|
|
|
629
635
|
**Symptom:** `[WARN] disk_warning` at >90%; `[CRIT] disk_critical` at >95% (auto-stops).
|
|
@@ -8,6 +8,8 @@ the test suite. After: one factory.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
12
|
+
import tomllib
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Any
|
|
13
15
|
|
|
@@ -104,6 +106,50 @@ def make_toml_with_sections(
|
|
|
104
106
|
return toml
|
|
105
107
|
|
|
106
108
|
|
|
109
|
+
def _format_toml_value(v: Any) -> str:
|
|
110
|
+
"""Serialize a Python scalar/list back to a TOML literal (str/list only;
|
|
111
|
+
sufficient for the [agent] overrides write_min_config supports)."""
|
|
112
|
+
if isinstance(v, list):
|
|
113
|
+
return "[" + ", ".join(_format_toml_value(x) for x in v) + "]"
|
|
114
|
+
if isinstance(v, str):
|
|
115
|
+
return json.dumps(v) # TOML basic-string quoting matches JSON's
|
|
116
|
+
if isinstance(v, bool): # must precede the int check: bool is an int subclass
|
|
117
|
+
return "true" if v else "false"
|
|
118
|
+
return str(v)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def write_min_config(tmp_path: Path, *, agent_extra: str = "") -> Path:
|
|
122
|
+
"""Write a minimal agent-runner.toml, letting callers override [agent] fields.
|
|
123
|
+
|
|
124
|
+
agent_extra: raw TOML lines parsed as a fragment and merged into the
|
|
125
|
+
default [agent] table (``command = ["true"]``, ``prompt_arg_template =
|
|
126
|
+
["{prompt}"]``) — keys in agent_extra replace the corresponding default
|
|
127
|
+
(avoids emitting duplicate TOML keys, which tomllib rejects). e.g.
|
|
128
|
+
``'prompt_delivery = "stdin"\\nprompt_arg_template = ["-p"]\\n'``.
|
|
129
|
+
"""
|
|
130
|
+
log_dir = tmp_path / "logs"
|
|
131
|
+
log_dir.mkdir(exist_ok=True)
|
|
132
|
+
prompt_file = tmp_path / "prompt.md"
|
|
133
|
+
prompt_file.write_text(_VALID_PROMPT)
|
|
134
|
+
|
|
135
|
+
agent: dict[str, Any] = {"command": ["true"], "prompt_arg_template": ["{prompt}"]}
|
|
136
|
+
if agent_extra:
|
|
137
|
+
agent.update(tomllib.loads(agent_extra))
|
|
138
|
+
|
|
139
|
+
agent_lines = "\n".join(f"{k} = {_format_toml_value(v)}" for k, v in agent.items())
|
|
140
|
+
toml = tmp_path / "agent-runner.toml"
|
|
141
|
+
toml.write_text(
|
|
142
|
+
"[agent]\n"
|
|
143
|
+
f"{agent_lines}\n"
|
|
144
|
+
"[runtime]\n"
|
|
145
|
+
f'work_dir = "{tmp_path}"\n'
|
|
146
|
+
f'log_dir = "{log_dir}"\n'
|
|
147
|
+
"[prompt]\n"
|
|
148
|
+
f'file = "{prompt_file}"\n'
|
|
149
|
+
)
|
|
150
|
+
return toml
|
|
151
|
+
|
|
152
|
+
|
|
107
153
|
def read_events_for_current_month(log_dir: Path) -> list[dict]:
|
|
108
154
|
"""Read all events from the current month's events-YYYY-MM.jsonl."""
|
|
109
155
|
import json
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import textwrap
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from agent_runner import agent_runtime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _fake_agent(tmp_path: Path) -> Path:
|
|
9
|
+
# Echoes its own argv and whatever it reads from stdin, into stdout.
|
|
10
|
+
p = tmp_path / "fake_agent.py"
|
|
11
|
+
p.write_text(
|
|
12
|
+
textwrap.dedent("""
|
|
13
|
+
import sys
|
|
14
|
+
sys.stdout.write("ARGV:" + "\\x1f".join(sys.argv) + "\\n")
|
|
15
|
+
sys.stdout.write("STDIN:" + sys.stdin.read() + "\\n")
|
|
16
|
+
""")
|
|
17
|
+
)
|
|
18
|
+
return p
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_stdin_mode_keeps_prompt_out_of_argv(tmp_path):
|
|
22
|
+
fake = _fake_agent(tmp_path)
|
|
23
|
+
log_path = tmp_path / "round.log"
|
|
24
|
+
marker = "PYTEST_MARKER_TOKEN_XYZ"
|
|
25
|
+
res = agent_runtime.run(
|
|
26
|
+
command=[sys.executable, str(fake)],
|
|
27
|
+
prompt_arg_template=["-p"],
|
|
28
|
+
prompt=f"do the thing {marker}",
|
|
29
|
+
prompt_delivery="stdin",
|
|
30
|
+
timeout_s=15,
|
|
31
|
+
log_path=log_path,
|
|
32
|
+
env_extra={},
|
|
33
|
+
)
|
|
34
|
+
out = log_path.read_text()
|
|
35
|
+
assert res.exit_code == 0
|
|
36
|
+
argv_line = next(line for line in out.splitlines() if line.startswith("ARGV:"))
|
|
37
|
+
stdin_line = next(line for line in out.splitlines() if line.startswith("STDIN:"))
|
|
38
|
+
assert marker not in argv_line # prompt NOT in argv
|
|
39
|
+
assert marker in stdin_line # prompt received on stdin
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_argv_mode_unchanged(tmp_path):
|
|
43
|
+
fake = _fake_agent(tmp_path)
|
|
44
|
+
log_path = tmp_path / "round.log"
|
|
45
|
+
marker = "ARGV_MARKER_TOKEN"
|
|
46
|
+
agent_runtime.run(
|
|
47
|
+
command=[sys.executable, str(fake)],
|
|
48
|
+
prompt_arg_template=["-p", "{prompt}"],
|
|
49
|
+
prompt=f"hello {marker}",
|
|
50
|
+
prompt_delivery="argv",
|
|
51
|
+
timeout_s=15,
|
|
52
|
+
log_path=log_path,
|
|
53
|
+
env_extra={},
|
|
54
|
+
)
|
|
55
|
+
argv_line = next(line for line in log_path.read_text().splitlines() if line.startswith("ARGV:"))
|
|
56
|
+
assert marker in argv_line # default argv behavior intact
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_stdin_mode_delivers_large_prompt_without_hanging(tmp_path):
|
|
60
|
+
# Prompt exceeds the OS pipe buffer (~64KB); if the write were still
|
|
61
|
+
# blocking on the main thread before the poll loop, an agent that reads
|
|
62
|
+
# stdin only after some delay (or a full-pipe write) could hang run()
|
|
63
|
+
# with no timeout_s protection. The write now happens on a daemon
|
|
64
|
+
# thread, so run() reaches the poll loop immediately regardless.
|
|
65
|
+
fake = _fake_agent(tmp_path)
|
|
66
|
+
log_path = tmp_path / "round.log"
|
|
67
|
+
marker = "LARGE_PROMPT_MARKER"
|
|
68
|
+
big_prompt = "x" * 100_000 + marker
|
|
69
|
+
res = agent_runtime.run(
|
|
70
|
+
command=[sys.executable, str(fake)],
|
|
71
|
+
prompt_arg_template=["-p"],
|
|
72
|
+
prompt=big_prompt,
|
|
73
|
+
prompt_delivery="stdin",
|
|
74
|
+
timeout_s=15,
|
|
75
|
+
log_path=log_path,
|
|
76
|
+
env_extra={},
|
|
77
|
+
)
|
|
78
|
+
assert res.exit_code == 0
|
|
79
|
+
assert not res.timed_out
|
|
80
|
+
stdin_line = next(
|
|
81
|
+
line for line in log_path.read_text().splitlines() if line.startswith("STDIN:")
|
|
82
|
+
)
|
|
83
|
+
assert marker in stdin_line # fully delivered despite exceeding pipe buffer
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_stdin_mode_guards_against_prompt_in_argv_template(tmp_path):
|
|
87
|
+
# Config validation already rejects {prompt} in the template for stdin
|
|
88
|
+
# mode, but run() itself must never substitute {prompt} into argv in
|
|
89
|
+
# stdin mode, even if called directly with a mismatched template.
|
|
90
|
+
fake = _fake_agent(tmp_path)
|
|
91
|
+
log_path = tmp_path / "round.log"
|
|
92
|
+
marker = "SECRET_MARKER"
|
|
93
|
+
agent_runtime.run(
|
|
94
|
+
command=[sys.executable, str(fake)],
|
|
95
|
+
prompt_arg_template=["-p", "{prompt}"],
|
|
96
|
+
prompt=marker,
|
|
97
|
+
prompt_delivery="stdin",
|
|
98
|
+
timeout_s=15,
|
|
99
|
+
log_path=log_path,
|
|
100
|
+
env_extra={},
|
|
101
|
+
)
|
|
102
|
+
argv_line = next(line for line in log_path.read_text().splitlines() if line.startswith("ARGV:"))
|
|
103
|
+
assert marker not in argv_line # runtime guard holds even with a bad template
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from agent_runner.agent_runtime import signal_name
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_negative_form_is_signal():
|
|
5
|
+
assert signal_name(-15) == "SIGTERM"
|
|
6
|
+
assert signal_name(-9) == "SIGKILL"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_shell_128_plus_n_form_is_signal():
|
|
10
|
+
assert signal_name(143) == "SIGTERM"
|
|
11
|
+
assert signal_name(137) == "SIGKILL"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_clean_and_plain_error_are_not_signals():
|
|
15
|
+
assert signal_name(0) is None
|
|
16
|
+
assert signal_name(1) is None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_high_nonsignal_exit_is_not_a_signal_and_does_not_crash():
|
|
20
|
+
# 200 -> 72 is not a valid signal number; must return None, not raise.
|
|
21
|
+
assert signal_name(200) is None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from agent_runner.config import load_config
|
|
4
|
+
from tests._test_helpers import write_min_config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _cfg(tmp_path, agent_extra):
|
|
8
|
+
return write_min_config(tmp_path, agent_extra=agent_extra)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_default_is_argv(tmp_path):
|
|
12
|
+
cfg = load_config(_cfg(tmp_path, agent_extra=""))
|
|
13
|
+
assert cfg.agent.prompt_delivery == "argv"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_stdin_accepted_when_template_has_no_prompt(tmp_path):
|
|
17
|
+
cfg = load_config(_cfg(tmp_path, 'prompt_delivery = "stdin"\nprompt_arg_template = ["-p"]\n'))
|
|
18
|
+
assert cfg.agent.prompt_delivery == "stdin"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_stdin_with_prompt_token_rejected(tmp_path):
|
|
22
|
+
with pytest.raises(ValueError, match="stdin"):
|
|
23
|
+
load_config(
|
|
24
|
+
_cfg(
|
|
25
|
+
tmp_path,
|
|
26
|
+
'prompt_delivery = "stdin"\nprompt_arg_template = ["-p", "{prompt}"]\n',
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_invalid_value_rejected(tmp_path):
|
|
32
|
+
with pytest.raises(ValueError):
|
|
33
|
+
load_config(_cfg(tmp_path, 'prompt_delivery = "file"\n'))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from agent_runner.agent_runtime import RunResult
|
|
2
|
+
from agent_runner.runner import _exit_cause # small pure helper we add
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _rr(exit_code, timed_out=False):
|
|
6
|
+
return RunResult(exit_code=exit_code, duration_s=1.0, timed_out=timed_out, pid=1)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_exit_cause_mapping():
|
|
10
|
+
assert _exit_cause(_rr(0)) == "clean"
|
|
11
|
+
assert _exit_cause(_rr(1)) == "error"
|
|
12
|
+
assert _exit_cause(_rr(143)) == "signal:SIGTERM"
|
|
13
|
+
assert _exit_cause(_rr(-15)) == "signal:SIGTERM"
|
|
14
|
+
# timeout wins even though agent-runner signal-killed it to enforce the timeout
|
|
15
|
+
assert _exit_cause(_rr(-15, timed_out=True)) == "timeout"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from agent_runner.agent_runtime import RunResult
|
|
2
|
+
from agent_runner.runner import _scan_round_log_for_network_blip
|
|
3
|
+
from tests._test_helpers import read_events_for_current_month # existing helper
|
|
4
|
+
|
|
5
|
+
# NOTE: "temporary failure in name resolution" does NOT match NETWORK_PATTERNS
|
|
6
|
+
# in agent_runner/monitor.py (verified) — the regex has no "resolution"/"failure"
|
|
7
|
+
# alternative. "DNS lookup failed" does match via the bare "dns" alternative, so
|
|
8
|
+
# it is used here to keep the RED/GREEN honest.
|
|
9
|
+
DNS_LOG_LINE = "... DNS lookup failed ...\n"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _blip_kinds(log_dir):
|
|
13
|
+
try:
|
|
14
|
+
events = read_events_for_current_month(log_dir)
|
|
15
|
+
except FileNotFoundError:
|
|
16
|
+
return []
|
|
17
|
+
return [e.get("event") for e in events]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_signal_death_with_network_string_is_not_a_blip(tmp_path):
|
|
21
|
+
log_dir = tmp_path / "logs"
|
|
22
|
+
log_dir.mkdir()
|
|
23
|
+
log_path = tmp_path / "round.log"
|
|
24
|
+
log_path.write_text(DNS_LOG_LINE)
|
|
25
|
+
_scan_round_log_for_network_blip(
|
|
26
|
+
log_dir=log_dir,
|
|
27
|
+
log_path=log_path,
|
|
28
|
+
result=RunResult(exit_code=143, duration_s=1.0, timed_out=False, pid=1),
|
|
29
|
+
round_num=1,
|
|
30
|
+
phase=None,
|
|
31
|
+
)
|
|
32
|
+
assert "agent_network_blip" not in _blip_kinds(log_dir)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_plain_error_with_network_string_still_blips(tmp_path):
|
|
36
|
+
log_dir = tmp_path / "logs"
|
|
37
|
+
log_dir.mkdir()
|
|
38
|
+
log_path = tmp_path / "round.log"
|
|
39
|
+
log_path.write_text(DNS_LOG_LINE)
|
|
40
|
+
_scan_round_log_for_network_blip(
|
|
41
|
+
log_dir=log_dir,
|
|
42
|
+
log_path=log_path,
|
|
43
|
+
result=RunResult(exit_code=1, duration_s=1.0, timed_out=False, pid=1),
|
|
44
|
+
round_num=1,
|
|
45
|
+
phase=None,
|
|
46
|
+
)
|
|
47
|
+
assert "agent_network_blip" in _blip_kinds(log_dir)
|
|
@@ -48,12 +48,28 @@ def test_given_preset_when_substituted_and_parsed_then_valid_toml(name: str) ->
|
|
|
48
48
|
|
|
49
49
|
@pytest.mark.parametrize("name", PRESET_NAMES)
|
|
50
50
|
def test_given_preset_when_parsed_then_prompt_arg_template_contains_prompt(name: str) -> None:
|
|
51
|
+
"""argv-delivery presets must place {prompt} in the template; stdin-delivery
|
|
52
|
+
presets (e.g. claude) deliberately omit it — the prompt travels on stdin
|
|
53
|
+
instead, so it never lands in process argv."""
|
|
51
54
|
text = _preset_text(name).replace("{project}", "test-project")
|
|
52
55
|
parsed = tomllib.loads(text)
|
|
53
56
|
template = parsed["agent"]["prompt_arg_template"]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
if parsed["agent"].get("prompt_delivery", "argv") == "stdin":
|
|
58
|
+
assert not any("{prompt}" in arg for arg in template), (
|
|
59
|
+
f"{name}.toml: stdin delivery but prompt_arg_template still has {{prompt}}"
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
assert any("{prompt}" in arg for arg in template), (
|
|
63
|
+
f"{name}.toml: prompt_arg_template lacks {{prompt}}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_given_claude_preset_when_parsed_then_uses_stdin_delivery() -> None:
|
|
68
|
+
"""claude preset opts into stdin prompt delivery: -p with no {prompt} arg."""
|
|
69
|
+
text = _preset_text("claude").replace("{project}", "test-project")
|
|
70
|
+
parsed = tomllib.loads(text)
|
|
71
|
+
assert parsed["agent"]["prompt_arg_template"] == ["-p"]
|
|
72
|
+
assert parsed["agent"]["prompt_delivery"] == "stdin"
|
|
57
73
|
|
|
58
74
|
|
|
59
75
|
def test_given_claude_preset_when_parsed_then_includes_disable_autoupdater() -> None:
|
|
File without changes
|