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.
Files changed (247) hide show
  1. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/CHANGELOG.md +11 -0
  2. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/PKG-INFO +1 -1
  3. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_version.py +2 -2
  4. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/agent_runtime.py +47 -2
  5. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/config.py +15 -1
  6. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/claude.toml +2 -1
  7. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/runner.py +19 -0
  8. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/configuration.md +15 -0
  9. cli_agent_runner-0.2.1/docs/migrations/0.2.1.md +77 -0
  10. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/runbook.md +6 -0
  11. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/_test_helpers.py +46 -0
  12. cli_agent_runner-0.2.1/tests/integration/test_prompt_delivery_stdin.py +103 -0
  13. cli_agent_runner-0.2.1/tests/unit/test_agent_runtime_signal_name.py +21 -0
  14. cli_agent_runner-0.2.1/tests/unit/test_config_prompt_delivery.py +33 -0
  15. cli_agent_runner-0.2.1/tests/unit/test_exit_cause.py +15 -0
  16. cli_agent_runner-0.2.1/tests/unit/test_network_blip_signal_guard.py +47 -0
  17. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_presets.py +19 -3
  18. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.codecov.yml +0 -0
  19. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  21. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  22. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/workflows/ci.yml +0 -0
  24. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.github/workflows/release.yml +0 -0
  25. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.gitignore +0 -0
  26. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/.vulture-whitelist.py +0 -0
  27. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/CODE_OF_CONDUCT.md +0 -0
  28. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/CONTRIBUTING.md +0 -0
  29. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/LICENSE +0 -0
  30. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/README.md +0 -0
  31. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/README.zh.md +0 -0
  32. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/SECURITY.md +0 -0
  33. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/__init__.py +0 -0
  34. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_docgen.py +0 -0
  35. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_emit.py +0 -0
  36. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_redact.py +0 -0
  37. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_registry.py +0 -0
  38. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_substrate.py +0 -0
  39. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/_throttle.py +0 -0
  40. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/api.py +0 -0
  41. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/api_types.py +0 -0
  42. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/__init__.py +0 -0
  43. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/_constants.py +0 -0
  44. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  45. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/codewhale.py +0 -0
  46. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  47. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/builtin_plugins/gemini.py +0 -0
  48. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/__init__.py +0 -0
  49. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/__main__.py +0 -0
  50. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/common.py +0 -0
  51. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/events_cmd.py +0 -0
  52. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/init_cmd.py +0 -0
  53. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/install_cmd.py +0 -0
  54. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/monitor_cmd.py +0 -0
  55. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/peek_cmd.py +0 -0
  56. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/round_cmd.py +0 -0
  57. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/serve_cmd.py +0 -0
  58. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/service_cmd.py +0 -0
  59. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/cli/upgrade_cmd.py +0 -0
  60. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/context_store.py +0 -0
  61. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/defenses.py +0 -0
  62. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/detector_helpers.py +0 -0
  63. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/events.py +0 -0
  64. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/hooks.py +0 -0
  65. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/http_progress.py +0 -0
  66. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/lifecycle.py +0 -0
  67. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/metrics.py +0 -0
  68. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/monitor.py +0 -0
  69. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/__init__.py +0 -0
  70. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/aider.toml +0 -0
  71. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/codewhale.toml +0 -0
  72. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/presets/gemini.toml +0 -0
  73. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/prompt_loader.py +0 -0
  74. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/round_log.py +0 -0
  75. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/round_view.py +0 -0
  76. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/scaffold.py +0 -0
  77. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/service_unit.py +0 -0
  78. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/startup_check.py +0 -0
  79. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/agent_runner/vcs_state.py +0 -0
  80. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/build.sh +0 -0
  81. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/example-agent-runner.toml +0 -0
  82. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/launchd.plist.tmpl +0 -0
  83. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/run-loop.sh +0 -0
  84. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/deploy/systemd.service.tmpl +0 -0
  85. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/README.md +0 -0
  86. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/architecture.md +0 -0
  87. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/commands.md +0 -0
  88. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/events.md +0 -0
  89. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/long-running-agents.md +0 -0
  90. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/marketing/README.md +0 -0
  91. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/marketing/promo-cn.html +0 -0
  92. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.16.md +0 -0
  93. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.17.md +0 -0
  94. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.19.md +0 -0
  95. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.20.md +0 -0
  96. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.21.md +0 -0
  97. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.22.md +0 -0
  98. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.23.md +0 -0
  99. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.24.md +0 -0
  100. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.25.md +0 -0
  101. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.26.md +0 -0
  102. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.27.md +0 -0
  103. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.28.md +0 -0
  104. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.29.md +0 -0
  105. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.30.md +0 -0
  106. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.31.md +0 -0
  107. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.32.md +0 -0
  108. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.33.md +0 -0
  109. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.34.md +0 -0
  110. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.35.md +0 -0
  111. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.36.md +0 -0
  112. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.37.md +0 -0
  113. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.38.md +0 -0
  114. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.39.md +0 -0
  115. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.40.md +0 -0
  116. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.1.42.md +0 -0
  117. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/migrations/0.2.0.md +0 -0
  118. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/plugins.md +0 -0
  119. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/quickstart.md +0 -0
  120. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/recipes/aider.md +0 -0
  121. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/recipes/codewhale.md +0 -0
  122. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/docs/thesis.md +0 -0
  123. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/pyproject.toml +0 -0
  124. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/__init__.py +0 -0
  125. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/conftest.py +0 -0
  126. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/contract/__init__.py +0 -0
  127. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/contract/test_public_api_surface.py +0 -0
  128. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/__init__.py +0 -0
  129. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/conftest.py +0 -0
  130. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  131. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_install_systemd.py +0 -0
  132. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  133. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  134. {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
  135. {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
  136. {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
  137. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/__init__.py +0 -0
  138. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_bounded_run.py +0 -0
  139. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_context_enricher_namespacing.py +0 -0
  140. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_dirty_handler_seam.py +0 -0
  141. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_fresh_eyes_signal.py +0 -0
  142. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_grace_kill_emission.py +0 -0
  143. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_install_dry_run.py +0 -0
  144. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_monitor_seeded.py +0 -0
  145. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_detector_loaded.py +0 -0
  146. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_owned_paths.py +0 -0
  147. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_plugin_real_flow.py +0 -0
  148. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  149. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_scaffold_presets.py +0 -0
  150. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_serve_loop.py +0 -0
  151. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_substrate_fingerprint.py +0 -0
  152. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/integration/test_transient_error_backoff.py +0 -0
  153. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/__init__.py +0 -0
  154. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_architecture.py +0 -0
  155. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_atomic_write_enforced.py +0 -0
  156. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_catalogs.py +0 -0
  157. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_classification_ssot.py +0 -0
  158. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  159. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_docs_generated.py +0 -0
  160. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_entry_points_resolve.py +0 -0
  161. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_event_kind_registry.py +0 -0
  162. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_event_kinds_ssot.py +0 -0
  163. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_events_doc_contract.py +0 -0
  164. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_layer_2_loop_size.py +0 -0
  165. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_module_boundaries.py +0 -0
  166. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_module_sizes.py +0 -0
  167. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_no_ai_signatures.py +0 -0
  168. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  169. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_peek_schema_version.py +0 -0
  170. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  171. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_round_result_stable.py +0 -0
  172. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  173. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/invariants/test_upstream_schema_canary.py +0 -0
  174. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/__init__.py +0 -0
  175. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/parser.py +0 -0
  176. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/test_parser.py +0 -0
  177. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/literate/test_quickstart.py +0 -0
  178. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/__init__.py +0 -0
  179. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime.py +0 -0
  180. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime_grace.py +0 -0
  181. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_agent_runtime_progress.py +0 -0
  182. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_assemble_prompt.py +0 -0
  183. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_events_stream.py +0 -0
  184. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_install.py +0 -0
  185. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_observation.py +0 -0
  186. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_read_round_num.py +0 -0
  187. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_resolve_phase.py +0 -0
  188. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_service.py +0 -0
  189. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_api_types.py +0 -0
  190. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_auto_stop_gating.py +0 -0
  191. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_catalogs.py +0 -0
  192. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_claude_error_detector.py +0 -0
  193. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli.py +0 -0
  194. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_common.py +0 -0
  195. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_init_install.py +0 -0
  196. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_monitor_http.py +0 -0
  197. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  198. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_cli_upgrade.py +0 -0
  199. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_codewhale_plugin.py +0 -0
  200. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config.py +0 -0
  201. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_fresh_eyes.py +0 -0
  202. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_max_rounds.py +0 -0
  203. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_stop_file.py +0 -0
  204. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  205. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_config_transient_error_action.py +0 -0
  206. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_context_store.py +0 -0
  207. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_default_dirty_handler.py +0 -0
  208. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_defenses.py +0 -0
  209. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_detector_helpers.py +0 -0
  210. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_detector_protocol.py +0 -0
  211. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_dirty_handlers.py +0 -0
  212. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_docgen.py +0 -0
  213. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_events.py +0 -0
  214. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_events_cmd.py +0 -0
  215. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  216. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_gemini_plugin.py +0 -0
  217. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_hook_failure_isolation.py +0 -0
  218. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_hooks.py +0 -0
  219. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_http_progress.py +0 -0
  220. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_init_entry_points.py +0 -0
  221. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_lifecycle.py +0 -0
  222. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_metrics.py +0 -0
  223. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_assembly.py +0 -0
  224. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  225. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  226. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  227. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_detectors.py +0 -0
  228. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_monitor_remote.py +0 -0
  229. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_peek_argparse.py +0 -0
  230. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_peek_select.py +0 -0
  231. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_prompt_loader.py +0 -0
  232. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_redact.py +0 -0
  233. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_round_log_helpers.py +0 -0
  234. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_round_view.py +0 -0
  235. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_runner.py +0 -0
  236. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_runner_throttle.py +0 -0
  237. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_scaffold.py +0 -0
  238. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_cmd_bounded.py +0 -0
  239. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_config_broken.py +0 -0
  240. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_crash_loop.py +0 -0
  241. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_round_log.py +0 -0
  242. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_sentinel.py +0 -0
  243. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_serve_startup_hooks.py +0 -0
  244. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_service_unit.py +0 -0
  245. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_startup_check.py +0 -0
  246. {cli_agent_runner-0.2.0 → cli_agent_runner-0.2.1}/tests/unit/test_substrate.py +0 -0
  247. {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.0
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.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
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
- argv = _build_argv(command, prompt_arg_template, prompt)
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=list(_require(agent_d, "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", "{prompt}"]
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
- assert any("{prompt}" in arg for arg in template), (
55
- f"{name}.toml: prompt_arg_template lacks {{prompt}}"
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: