cli-agent-runner 0.3.5__tar.gz → 0.3.6__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 (416) hide show
  1. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/CHANGELOG.md +6 -0
  2. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/PKG-INFO +1 -1
  3. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_lifecycle.py +16 -7
  4. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_notify.py +15 -6
  5. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_sandbox.py +93 -13
  6. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_procwait.py +38 -15
  7. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_version.py +2 -2
  8. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/agent_runtime.py +11 -5
  9. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_round.py +39 -8
  10. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/serve_cmd.py +20 -6
  11. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/models.py +8 -7
  12. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/commands.md +4 -0
  13. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/memory-efficiency.md +28 -0
  14. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/runbook.md +4 -0
  15. cli_agent_runner-0.3.6/tests/integration/test_reap_property.py +314 -0
  16. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_architecture.py +1 -1
  17. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_timeout_budget_invariant.py +14 -3
  18. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_reap.py +27 -0
  19. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_notify.py +16 -0
  20. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_sandbox_wire.py +252 -1
  21. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_procwait.py +14 -0
  22. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_spawn_seam.py +61 -1
  23. cli_agent_runner-0.3.5/tests/integration/test_reap_property.py +0 -153
  24. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.codecov.yml +0 -0
  25. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  26. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  27. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  28. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/dependabot.yml +0 -0
  30. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/workflows/ci.yml +0 -0
  31. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/workflows/release.yml +0 -0
  32. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.gitignore +0 -0
  33. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.vulture-whitelist.py +0 -0
  34. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/BACKLOG.md +0 -0
  35. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/CODE_OF_CONDUCT.md +0 -0
  36. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/CONTRIBUTING.md +0 -0
  37. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/LICENSE +0 -0
  38. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/README.md +0 -0
  39. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/README.zh.md +0 -0
  40. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/SECURITY.md +0 -0
  41. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/__init__.py +0 -0
  42. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_docgen.py +0 -0
  43. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/__init__.py +0 -0
  44. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/agent.py +0 -0
  45. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/memory.py +0 -0
  46. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/plugins.py +0 -0
  47. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/rounds.py +0 -0
  48. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/serve.py +0 -0
  49. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_install.py +0 -0
  50. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_monitor_detectors.py +0 -0
  51. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_monitor_registry.py +0 -0
  52. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_monitor_state.py +0 -0
  53. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_observe.py +0 -0
  54. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_checksum.py +0 -0
  55. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_manifest.py +0 -0
  56. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_scan.py +0 -0
  57. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_redact.py +0 -0
  58. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_registry.py +0 -0
  59. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_resolve.py +0 -0
  60. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_round_outcome.py +0 -0
  61. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_round_support.py +0 -0
  62. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_sandbox_probe.py +0 -0
  63. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_serve_policy.py +0 -0
  64. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_substrate.py +0 -0
  65. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_throttle.py +0 -0
  66. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/api.py +0 -0
  67. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/api_types.py +0 -0
  68. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/__init__.py +0 -0
  69. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/_constants.py +0 -0
  70. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  71. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/codewhale.py +0 -0
  72. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  73. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/gemini.py +0 -0
  74. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/kimi.py +0 -0
  75. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/pi.py +0 -0
  76. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/__init__.py +0 -0
  77. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/__main__.py +0 -0
  78. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_cgroup.py +0 -0
  79. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/common.py +0 -0
  80. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/doctor_cmd.py +0 -0
  81. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/events_cmd.py +0 -0
  82. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/init_cmd.py +0 -0
  83. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/install_cmd.py +0 -0
  84. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/migrate_cmd.py +0 -0
  85. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/monitor_cmd.py +0 -0
  86. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/peek_cmd.py +0 -0
  87. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/round_cmd.py +0 -0
  88. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/service_cmd.py +0 -0
  89. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/upgrade_cmd.py +0 -0
  90. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/clock.py +0 -0
  91. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/__init__.py +0 -0
  92. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/errors.py +0 -0
  93. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/loader.py +0 -0
  94. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/parsers.py +0 -0
  95. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/validators.py +0 -0
  96. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/context_store.py +0 -0
  97. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/defenses.py +0 -0
  98. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/events.py +0 -0
  99. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/hooks.py +0 -0
  100. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/host_health.py +0 -0
  101. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/http_progress.py +0 -0
  102. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/lifecycle.py +0 -0
  103. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/metrics.py +0 -0
  104. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/migrations.py +0 -0
  105. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/monitor.py +0 -0
  106. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/phase_select.py +0 -0
  107. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/__init__.py +0 -0
  108. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/aider.toml +0 -0
  109. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/claude.toml +0 -0
  110. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/codewhale.toml +0 -0
  111. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/gemini.toml +0 -0
  112. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/kimi.toml +0 -0
  113. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/pi.toml +0 -0
  114. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/prompt_loader.py +0 -0
  115. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/remote_relay.py +0 -0
  116. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/round_log.py +0 -0
  117. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/round_view.py +0 -0
  118. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/runner.py +0 -0
  119. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/scaffold.py +0 -0
  120. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/schedule.py +0 -0
  121. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/service_unit.py +0 -0
  122. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/startup_check.py +0 -0
  123. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/vcs_state.py +0 -0
  124. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/build.sh +0 -0
  125. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/deploy/Dockerfile +0 -0
  126. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/deploy/docker-compose.yml +0 -0
  127. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/README.md +0 -0
  128. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/architecture.md +0 -0
  129. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/configuration.md +0 -0
  130. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/events.md +0 -0
  131. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/long-running-agents.md +0 -0
  132. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/migrations/0.2.md +0 -0
  133. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/migrations/0.3.md +0 -0
  134. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/plugins.md +0 -0
  135. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/quickstart.md +0 -0
  136. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/aider.md +0 -0
  137. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/codewhale.md +0 -0
  138. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/container-pi.md +0 -0
  139. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/deploy-all-in-one.md +0 -0
  140. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/kimi.md +0 -0
  141. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/pi.md +0 -0
  142. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/thesis.md +0 -0
  143. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/pyproject.toml +0 -0
  144. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/__init__.py +0 -0
  145. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/_clock.py +0 -0
  146. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/_test_helpers.py +0 -0
  147. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/conftest.py +0 -0
  148. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/contract/__init__.py +0 -0
  149. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/contract/test_public_api_surface.py +0 -0
  150. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/__init__.py +0 -0
  151. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/conftest.py +0 -0
  152. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  153. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_install_systemd.py +0 -0
  154. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  155. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  156. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  157. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  158. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  159. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/generate_vulture_whitelist.py +0 -0
  160. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/__init__.py +0 -0
  161. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_auto_stop_e2e.py +0 -0
  162. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_bounded_run.py +0 -0
  163. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_context_enricher_namespacing.py +0 -0
  164. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_cooperative_grace_property.py +0 -0
  165. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_dirty_handler_seam.py +0 -0
  166. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_fresh_eyes_signal.py +0 -0
  167. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_grace_kill_emission.py +0 -0
  168. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_install_dry_run.py +0 -0
  169. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_monitor_seeded.py +0 -0
  170. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_detector_loaded.py +0 -0
  171. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_owned_paths.py +0 -0
  172. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_real_flow.py +0 -0
  173. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  174. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  175. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_scaffold_presets.py +0 -0
  176. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_serve_loop.py +0 -0
  177. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  178. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_wedged.py +0 -0
  179. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_substrate_fingerprint.py +0 -0
  180. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_transient_error_backoff.py +0 -0
  181. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/__init__.py +0 -0
  182. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/_docs.py +0 -0
  183. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/_event_scan.py +0 -0
  184. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_atomic_write_enforced.py +0 -0
  185. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_authored_facts_declared.py +0 -0
  186. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  187. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_bdd_test_conventions.py +0 -0
  188. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
  189. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_catalogs.py +0 -0
  190. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_classification_ssot.py +0 -0
  191. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_config_error_consistency.py +0 -0
  192. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_config_value_set_ssot.py +0 -0
  193. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_container_stop_bound.py +0 -0
  194. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  195. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  196. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  197. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_cli_claims.py +0 -0
  198. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_operator_surface.py +0 -0
  199. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  200. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  201. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  202. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_generated.py +0 -0
  203. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_index_complete.py +0 -0
  204. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_entry_points_resolve.py +0 -0
  205. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_event_int_coercion.py +0 -0
  206. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kind_registry.py +0 -0
  207. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kinds_ssot.py +0 -0
  208. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_events_doc_contract.py +0 -0
  209. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  210. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_hook_contract_docs.py +0 -0
  211. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_import_footprint.py +0 -0
  212. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
  213. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_layer_2_loop_size.py +0 -0
  214. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  215. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_module_boundaries.py +0 -0
  216. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_module_sizes.py +0 -0
  217. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  218. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_ai_signatures.py +0 -0
  219. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_asyncio_select.py +0 -0
  220. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_internal_refs.py +0 -0
  221. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_price_logic.py +0 -0
  222. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  223. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_raw_time.py +0 -0
  224. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  225. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_version_refs.py +0 -0
  226. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_peek_schema_version.py +0 -0
  227. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_phase_select_stateless.py +0 -0
  228. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_plugins_config_stable.py +0 -0
  229. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_procwait_shield.py +0 -0
  230. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  231. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  232. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_alloc_growth.py +0 -0
  233. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  234. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_num_single_source.py +0 -0
  235. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_result_stable.py +0 -0
  236. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  237. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  238. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_upstream_schema_canary.py +0 -0
  239. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  240. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/linux/__init__.py +0 -0
  241. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/linux/test_plugin_sandbox_kill.py +0 -0
  242. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/__init__.py +0 -0
  243. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/parser.py +0 -0
  244. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/test_parser.py +0 -0
  245. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/test_quickstart.py +0 -0
  246. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/__init__.py +0 -0
  247. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime.py +0 -0
  248. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_container.py +0 -0
  249. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_grace.py +0 -0
  250. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  251. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_progress.py +0 -0
  252. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  253. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_assemble_prompt.py +0 -0
  254. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_events_stream.py +0 -0
  255. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_install.py +0 -0
  256. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_observation.py +0 -0
  257. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_read_round_num.py +0 -0
  258. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_resolve_phase.py +0 -0
  259. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_service.py +0 -0
  260. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_types.py +0 -0
  261. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_apply_reap_grace_env.py +0 -0
  262. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_auto_stop_gating.py +0 -0
  263. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_builtin_provenance.py +0 -0
  264. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_catalogs.py +0 -0
  265. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cgroup_probe.py +0 -0
  266. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_claude_error_detector.py +0 -0
  267. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_claude_foreign_schema.py +0 -0
  268. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli.py +0 -0
  269. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_common.py +0 -0
  270. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_init_install.py +0 -0
  271. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_main_boundary.py +0 -0
  272. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_monitor_http.py +0 -0
  273. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  274. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_upgrade.py +0 -0
  275. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_code_precedence.py +0 -0
  276. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_plugin.py +0 -0
  277. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_coerce_int_poison.py +0 -0
  278. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_collapse_spawn_decisions.py +0 -0
  279. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config.py +0 -0
  280. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_fresh_eyes.py +0 -0
  281. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_max_rounds.py +0 -0
  282. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_nested_strictness.py +0 -0
  283. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_prompt_delivery.py +0 -0
  284. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_schedule.py +0 -0
  285. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_stop_file.py +0 -0
  286. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_strictness.py +0 -0
  287. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  288. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_transient_error_action.py +0 -0
  289. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_context_store.py +0 -0
  290. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_control_char_config.py +0 -0
  291. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_default_dirty_handler.py +0 -0
  292. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_defenses.py +0 -0
  293. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_detect_hung_latch.py +0 -0
  294. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_detector_isolation.py +0 -0
  295. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_detector_protocol.py +0 -0
  296. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_dirty_handlers.py +0 -0
  297. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
  298. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_docgen.py +0 -0
  299. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_cmd.py +0 -0
  300. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_sandbox.py +0 -0
  301. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_effective_throttle_view.py +0 -0
  302. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_emit_config_migrated.py +0 -0
  303. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule.py +0 -0
  304. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  305. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events.py +0 -0
  306. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd.py +0 -0
  307. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_rollover.py +0 -0
  308. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_tail.py +0 -0
  309. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_jsonl_decode.py +0 -0
  310. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_example_dirty_handler.py +0 -0
  311. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_example_plugin.py +0 -0
  312. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_exec_prefix.py +0 -0
  313. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_exit_cause.py +0 -0
  314. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_exit_classifier.py +0 -0
  315. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  316. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_gate_serve_boot.py +0 -0
  317. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_gemini_plugin.py +0 -0
  318. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_git_timeout.py +0 -0
  319. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_hook_failure_isolation.py +0 -0
  320. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_hook_redact_order.py +0 -0
  321. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_hooks.py +0 -0
  322. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_host_health.py +0 -0
  323. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress.py +0 -0
  324. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress_epoch.py +0 -0
  325. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_init_entry_points.py +0 -0
  326. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  327. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_kill_pgroup_shielding.py +0 -0
  328. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_kimi_plugin.py +0 -0
  329. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_lifecycle.py +0 -0
  330. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_load_fail_closed.py +0 -0
  331. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_loader_split.py +0 -0
  332. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_manifest_spawn_hooks.py +0 -0
  333. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_metrics.py +0 -0
  334. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  335. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_swap.py +0 -0
  336. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_migrate_cmd.py +0 -0
  337. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_migrations.py +0 -0
  338. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_assembly.py +0 -0
  339. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_cmd_severity.py +0 -0
  340. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_identity.py +0 -0
  341. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  342. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  343. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  344. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  345. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detectors.py +0 -0
  346. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_event_tail.py +0 -0
  347. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_loop.py +0 -0
  348. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_on_alert.py +0 -0
  349. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_remote.py +0 -0
  350. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  351. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_schedule_pause.py +0 -0
  352. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_network_blip_signal_guard.py +0 -0
  353. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_open_round_log.py +0 -0
  354. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  355. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_outer_round_ceiling.py +0 -0
  356. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_argparse.py +0 -0
  357. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_project_isolation.py +0 -0
  358. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_sandbox_block.py +0 -0
  359. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_schedule.py +0 -0
  360. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_select.py +0 -0
  361. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_phase_agent_launch.py +0 -0
  362. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_phase_profiles.py +0 -0
  363. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_phase_select.py +0 -0
  364. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_pi_plugin.py +0 -0
  365. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_checksum.py +0 -0
  366. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_constants.py +0 -0
  367. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_manifest.py +0 -0
  368. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_name_mismatch.py +0 -0
  369. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_scan_parity.py +0 -0
  370. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugins_config_fields.py +0 -0
  371. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_presets.py +0 -0
  372. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_primary_prompt_file_home.py +0 -0
  373. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_prompt_loader.py +0 -0
  374. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_read_json_hardening.py +0 -0
  375. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_redact.py +0 -0
  376. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_resolve.py +0 -0
  377. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd.py +0 -0
  378. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd_sigterm.py +0 -0
  379. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_log_helpers.py +0 -0
  380. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome.py +0 -0
  381. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  382. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_view.py +0 -0
  383. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_runner.py +0 -0
  384. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  385. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_runner_throttle.py +0 -0
  386. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_sandbox_probe.py +0 -0
  387. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_scaffold.py +0 -0
  388. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_schedule.py +0 -0
  389. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_cmd_bounded.py +0 -0
  390. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_config_broken.py +0 -0
  391. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_crash_loop.py +0 -0
  392. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_mem_gate.py +0 -0
  393. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_phase_schedule.py +0 -0
  394. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_policy.py +0 -0
  395. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round.py +0 -0
  396. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  397. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_log.py +0 -0
  398. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_oom_killed.py +0 -0
  399. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_terminate.py +0 -0
  400. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_wait_exit.py +0 -0
  401. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_schedule_gate.py +0 -0
  402. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_sentinel.py +0 -0
  403. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_startup_hooks.py +0 -0
  404. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_wakeup.py +0 -0
  405. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_service_mode.py +0 -0
  406. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_service_unit.py +0 -0
  407. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_startup_battery_exit.py +0 -0
  408. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_startup_check.py +0 -0
  409. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_substrate.py +0 -0
  410. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  411. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_terminate_round_wait_exit.py +0 -0
  412. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  413. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_epoch_bound.py +0 -0
  414. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_plugin_classification.py +0 -0
  415. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_upgrade_migrate.py +0 -0
  416. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_vcs_state.py +0 -0
@@ -5,6 +5,12 @@ 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.3.6] - 2026-09-15
9
+
10
+ ### Changed
11
+ - `agent-runner kill` now reaps a stuck round's detached descendants (a child that `setsid()`'d off its process group) the same way `serve stop` already does, so a forced stop never leaves an orphan behind.
12
+ - A stop (`SIGTERM` / `serve stop`) during a sandboxed third-party spawn hook, or during a plugin-requested `defer`, now takes effect immediately instead of after up to 30 s — and the confined hook's child process is always reaped, never left running. A background process the hook itself forks is not reaped; its abandoned output pipe is dropped after a bounded 2 s drain wait (on a normal hook exit, surfaced as `hook_failed`) rather than stalling the stop.
13
+
8
14
  ## [0.3.5] - 2026-09-15
9
15
 
10
16
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.3.5
3
+ Version: 0.3.6
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
@@ -181,17 +181,26 @@ def _terminate_round_pid(pid: int) -> None:
181
181
  """TERM-first -> grace -> SIGKILL a bare round-child pid.
182
182
 
183
183
  Mirrors ``serve_cmd._terminate_round``'s shape, but this runs from a
184
- SEPARATE CLI process (``kill()``) that only has the pid not serve's own
184
+ SEPARATE CLI process (``kill()``) that only has the pid -- not serve's own
185
185
  ``Popen`` handle. Never ``killpg``: the round is ``start_new_session=True``,
186
- so its own pgid holds only itself, and even a killpg on it cannot reach the
187
- agent (itself a separate session again, ``agent_runtime.py``) only the
188
- round's own SIGTERM -> KeyboardInterrupt handler (``round_cmd.py``) walks
189
- that link and reaps the agent pgroup via ``_kill_pgroup``. A plain SIGTERM
190
- here fires exactly that handler; SIGKILL is only the last-resort escalation
191
- for a round that doesn't even get to run its handler."""
186
+ so a plain SIGTERM here fires the round's own SIGTERM -> KeyboardInterrupt
187
+ handler (``round_cmd.py``), which walks the link to the agent pgroup and
188
+ reaps it via ``_kill_pgroup`` (itself snapshotting and reaping any stray).
189
+ SIGKILL is only the last-resort escalation for a round that never even runs
190
+ its handler -- and on THAT path the leader's own cooperative reap never
191
+ happened, so we snapshot the leader's descendants up front (while the leader
192
+ subtree is still resolvable) via a bare-pid shim and reap the strays here:
193
+ a descendant that ``setsid()``'d off the leader's pgroup would otherwise
194
+ survive the leader's death, orphaned. Symmetric with ``serve stop``."""
195
+ import types
196
+
197
+ from agent_runner import agent_runtime
198
+
199
+ stray = agent_runtime._snapshot_stray_descendants(types.SimpleNamespace(pid=pid))
192
200
  send_signal_to_pid(pid, signal.SIGTERM)
193
201
  if _await_pid_exit(pid, _ROUND_TERM_GRACE_S):
194
202
  send_signal_to_pid(pid, signal.SIGKILL)
203
+ agent_runtime._kill_stray_descendants(stray)
195
204
 
196
205
 
197
206
  def stop(project: str | Path) -> ServiceStatus:
@@ -78,11 +78,14 @@ def drain(fd: int) -> None:
78
78
  ``select()`` on it instantly ready -- an undrained fd busy-spins its
79
79
  caller at 100% CPU forever after the first wake, since a level-triggered
80
80
  fd stays readable until read. Advisory: the bytes carry no payload and are
81
- discarded. Used only by :meth:`Listener.wait` (its own timeout branch) --
82
- the pause/sleep waits (``_interruptible_sleep``/``_pause_poll``) are the
83
- sole consumers of a live ``Listener``. Nothing watches this fd mid-round
84
- (``_spawn_round`` only ever waits on the round leader's own exit fd), so
85
- there is no other caller that must drain it itself.
81
+ discarded. Two callers: :meth:`Listener.wait` self-drains its own fd on the
82
+ wake branch (the pause/sleep waits ``_interruptible_sleep``/``_pause_poll``
83
+ are the consumers of a live ``Listener``); and the sandboxed-hook trampoline
84
+ (``_plugin_sandbox._run_child_process``, P3) drains the CALLER-OWNED wake fd
85
+ it was handed on the ``"woken"`` branch of its exit wait, before re-checking
86
+ ``should_stop()`` -- that fd is the serve listener's, watched by
87
+ ``wait_exit`` for the duration of the hook, so the trampoline must drain it
88
+ itself rather than leave the next wait instantly ready.
86
89
  """
87
90
  while True:
88
91
  try:
@@ -179,7 +182,13 @@ class Listener:
179
182
  """
180
183
  deadline = clock.monotonic() + timeout_s
181
184
  remaining = max(0.0, deadline - clock.monotonic())
182
- ready, _, _ = select.select([self.fd], [], [], remaining)
185
+ try:
186
+ ready, _, _ = select.select([self.fd], [], [], remaining)
187
+ except ValueError:
188
+ # fd >= FD_SETSIZE (1024) makes select.select raise ValueError --
189
+ # degrade to a plain timed sleep instead of crashing.
190
+ clock.sleep(remaining)
191
+ return False
183
192
  if not ready:
184
193
  return False
185
194
 
@@ -17,12 +17,15 @@ from __future__ import annotations
17
17
  import json
18
18
  import subprocess
19
19
  import sys
20
+ from collections.abc import Callable
20
21
  from pathlib import Path
21
22
  from typing import Literal
22
23
 
23
- from agent_runner import hooks
24
+ from agent_runner import _notify, hooks
25
+ from agent_runner._procwait import wait_exit
24
26
  from agent_runner._registry import resolve_entry_target
25
27
  from agent_runner.api_types import DirtyOutcome, SpawnDecision
28
+ from agent_runner.clock import SYSTEM_CLOCK
26
29
 
27
30
  _TRAMPOLINE_TIMEOUT_S = 30.0 # wall-clock; not config-tunable this release
28
31
  _MAX_WIRE_BYTES = 64 * 1024
@@ -30,6 +33,17 @@ _MAX_STDERR_BYTES = 8 * 1024 # hard read cap; a runaway child can't OOM the sup
30
33
  _REF_CAP = 256
31
34
  _STDERR_CAP = 512
32
35
 
36
+ # Total wall budget for draining a confined child's pipes AFTER it is reaped. A
37
+ # process the hook itself fork()'d inherits the child's stdout/stderr and can hold
38
+ # them open past the child's own death, so the drain join is BOUNDED, not
39
+ # unbounded -- otherwise a single such pipe-holder stalls the stop forever. The
40
+ # pair of joins shares ONE absolute deadline (not _DRAIN_JOIN_S each), and
41
+ # _DRAIN_JOIN_S + 3 <= _lifecycle._PID_SIGNAL_GRACE_S leaves >=3 s of the
42
+ # stop-confirm window for the rest of the unwind (pinned by
43
+ # tests/invariants/test_timeout_budget_invariant.py; a literal mirror -- this
44
+ # module must never import _lifecycle in production).
45
+ _DRAIN_JOIN_S = 2
46
+
33
47
  # The child's environment is built default-DENY from this allowlist -- NOT copied
34
48
  # from the parent's, which holds the real agent secrets (ANTHROPIC_*, and every
35
49
  # *_API_KEY/*_TOKEN/*_SECRET agent_runtime injects). A confined plugin is Python
@@ -126,6 +140,13 @@ def _ctx_from_wire(data: dict) -> hooks.HookContext:
126
140
  # ---------------------------------------------------------------------------
127
141
 
128
142
 
143
+ class SpawnHookInterrupted(Exception): # noqa: N818 -- a control-flow signal, not an error
144
+ """Raised by ``_run_child_process`` when ``should_stop()`` fires on a wake
145
+ during a sandboxed spawn hook -- distinct from a plugin failure so the seam
146
+ returns ``None`` WITHOUT emitting ``hook_failed`` (a stop is not the plugin's
147
+ fault). Never raised on the dirty round path, which passes no ``wake_fd``."""
148
+
149
+
129
150
  def run_hook_sandboxed(
130
151
  hook_kind: Literal["spawn_hook", "dirty_handler"],
131
152
  module_path: str,
@@ -137,6 +158,8 @@ def run_hook_sandboxed(
137
158
  dirty_files: list[str] | None = None,
138
159
  view: hooks.SpawnView | None = None,
139
160
  timeout_s: float = _TRAMPOLINE_TIMEOUT_S,
161
+ wake_fd: int | None = None,
162
+ should_stop: Callable[[], bool] | None = None,
140
163
  ) -> SpawnDecision | DirtyOutcome | None:
141
164
  """Launch the confinement child for one third-party hook call and return its
142
165
  validated outcome. ``module_path``/``attr_path`` locate the plugin the child
@@ -172,6 +195,8 @@ def run_hook_sandboxed(
172
195
  ],
173
196
  json.dumps(payload).encode("utf-8"),
174
197
  timeout_s,
198
+ wake_fd=wake_fd,
199
+ should_stop=should_stop,
175
200
  )
176
201
  if returncode < 0:
177
202
  from agent_runner.api import emit_plugin_sandbox_kill
@@ -224,14 +249,50 @@ def _drain_capped(stream, cap: int, out: list[bytes]) -> None:
224
249
  out.append(bytes(data))
225
250
 
226
251
 
252
+ def _join_drain_threads(threads: tuple, budget_s: float) -> bool:
253
+ """Join the drain threads under ONE absolute deadline -- the whole set is
254
+ bounded by ``budget_s``, NOT ``budget_s`` each -- so a process the hook
255
+ ``fork()``'d that still holds the reaped child's output pipe open cannot stall
256
+ the join past ``budget_s``. Returns True iff every thread finished draining
257
+ within the budget (the normal-exit path raises on False; the ``BaseException``
258
+ cleanup path ignores it, since it is already unwinding)."""
259
+ join_deadline = SYSTEM_CLOCK.monotonic() + budget_s
260
+ for t in threads:
261
+ t.join(max(0.0, join_deadline - SYSTEM_CLOCK.monotonic()))
262
+ return not any(t.is_alive() for t in threads)
263
+
264
+
227
265
  def _run_child_process(
228
- argv: list[str], stdin_bytes: bytes, timeout_s: float
266
+ argv: list[str],
267
+ stdin_bytes: bytes,
268
+ timeout_s: float,
269
+ *,
270
+ wake_fd: int | None = None,
271
+ should_stop: Callable[[], bool] | None = None,
229
272
  ) -> tuple[int, bytes, bytes]:
230
273
  """Spawn the confined child with the minimal env and read BOUNDED stdout/stderr
231
274
  concurrently (a thread per stream), so a plugin printing without limit within
232
275
  the wall-timeout cannot exhaust supervisor memory. Concurrent draining also
233
- avoids a full-pipe deadlock. On timeout the child is killed and
234
- ``TimeoutExpired`` propagates (the caller isolates it as ``hook_failed``)."""
276
+ avoids a full-pipe deadlock.
277
+
278
+ The exit wait is the fd-driven ``_procwait.wait_exit`` (not a sleep-poll):
279
+ ``"exited"`` reaps and returns; ``"timeout"`` kills and raises
280
+ ``TimeoutExpired`` (the caller isolates it as ``hook_failed``); ``"woken"``
281
+ (only when ``wake_fd`` is set) drains the advisory byte and, if
282
+ ``should_stop()`` is now true, kills the child and raises
283
+ ``SpawnHookInterrupted`` -- otherwise a stray ring re-enters the wait on the
284
+ SAME ABSOLUTE deadline so it never shortens the hook's budget. The whole wait
285
+ is wrapped so the confined child NEVER outlives its parent: any BaseException
286
+ (a round ``KeyboardInterrupt``, a timeout, an interrupt) kills and reaps the
287
+ child before propagating.
288
+
289
+ A process the hook itself ``fork()``s is NOT reaped (that child has no process
290
+ group of its own -- see the Option-B follow-up), and it can inherit and hold
291
+ the child's stdout/stderr open past the child's own death; so the drain joins
292
+ are BOUNDED (``_DRAIN_JOIN_S``, one absolute deadline across the pair) at BOTH
293
+ the normal-exit tail and the ``BaseException`` cleanup -- such a pipe-holder can
294
+ never stall the parent. On the normal path a timed-out drain surfaces as a
295
+ ``RuntimeError`` the seam isolates as ``hook_failed``."""
235
296
  import threading
236
297
 
237
298
  proc = subprocess.Popen(
@@ -243,9 +304,17 @@ def _run_child_process(
243
304
  )
244
305
  out_box: list[bytes] = []
245
306
  err_box: list[bytes] = []
246
- # +1 so an over-cap stdout stays over the limit and _parse_dirty_stdout rejects it.
247
- t_out = threading.Thread(target=_drain_capped, args=(proc.stdout, _MAX_WIRE_BYTES + 1, out_box))
248
- t_err = threading.Thread(target=_drain_capped, args=(proc.stderr, _MAX_STDERR_BYTES, err_box))
307
+ # daemon=True is load-bearing, not cosmetic: nothing in agent_runner/ calls
308
+ # os._exit, so a non-daemon drain thread still blocked on a fork()'d
309
+ # pipe-holder's read() would be joined by threading._shutdown() at interpreter
310
+ # exit and re-hang the stop the bounded join below just rescued. Matches the
311
+ # repo's other pump threads (remote_relay._pump, agent_runtime._write_stdin).
312
+ t_out = threading.Thread(
313
+ target=_drain_capped, args=(proc.stdout, _MAX_WIRE_BYTES + 1, out_box), daemon=True
314
+ )
315
+ t_err = threading.Thread(
316
+ target=_drain_capped, args=(proc.stderr, _MAX_STDERR_BYTES, err_box), daemon=True
317
+ )
249
318
  t_out.start()
250
319
  t_err.start()
251
320
  try:
@@ -255,15 +324,26 @@ def _run_child_process(
255
324
  pass
256
325
  finally:
257
326
  proc.stdin.close()
258
- proc.wait(timeout=timeout_s)
259
- except subprocess.TimeoutExpired:
327
+ deadline = SYSTEM_CLOCK.monotonic() + timeout_s
328
+ while True:
329
+ outcome = wait_exit(proc, deadline=deadline, wake_fd=wake_fd)
330
+ if outcome == "exited":
331
+ proc.wait()
332
+ break
333
+ if outcome == "timeout":
334
+ raise subprocess.TimeoutExpired(proc.args, timeout_s)
335
+ _notify.drain(wake_fd)
336
+ if should_stop is not None and should_stop():
337
+ raise SpawnHookInterrupted(f"spawn hook interrupted for {argv[0]!r}")
338
+ except BaseException:
260
339
  proc.kill()
261
340
  proc.wait()
262
- t_out.join()
263
- t_err.join()
341
+ _join_drain_threads((t_out, t_err), _DRAIN_JOIN_S)
264
342
  raise
265
- t_out.join()
266
- t_err.join()
343
+ if not _join_drain_threads((t_out, t_err), _DRAIN_JOIN_S):
344
+ raise RuntimeError(
345
+ "drain timed out: a process the hook forked still holds its output pipe (not reaped)"
346
+ )
267
347
  return proc.returncode, out_box[0], err_box[0]
268
348
 
269
349
 
@@ -96,12 +96,13 @@ def wait_exit(
96
96
  *,
97
97
  deadline: float,
98
98
  clock: Clock = SYSTEM_CLOCK,
99
- ) -> Literal["exited", "timeout"]:
100
- """Block until ``proc`` exits or ``deadline`` (a ``clock.monotonic()``
101
- timestamp) is reached, whichever is soonest. Never calls
102
- ``os.waitpid``/``proc.wait``/``proc.poll`` on the fast path -- a caller
103
- that gets back ``"exited"`` must reap ``proc`` itself right after this
104
- returns (see the module docstring).
99
+ wake_fd: int | None = None,
100
+ ) -> Literal["exited", "timeout", "woken"]:
101
+ """Block until ``proc`` exits, ``deadline`` (a ``clock.monotonic()``
102
+ timestamp) is reached, or ``wake_fd`` becomes readable -- whichever is
103
+ soonest. Never calls ``os.waitpid``/``proc.wait``/``proc.poll`` on the
104
+ fast path -- a caller that gets back ``"exited"`` must reap ``proc``
105
+ itself right after this returns (see the module docstring).
105
106
 
106
107
  Opens and closes its own ``exit_fd(proc)`` registration internally, once
107
108
  per call, in a single ``select.select`` -- callers invoke this at most
@@ -110,16 +111,27 @@ def wait_exit(
110
111
  across calls.
111
112
 
112
113
  Returns ``"exited"`` (the caller should immediately reap, e.g.
113
- ``proc.wait()``) or ``"timeout"`` (deadline reached, proc still running,
114
- caller re-evaluates its own logic). When ``proc`` has already exited by
115
- the time ``deadline`` is reached, the exit fd is ready in the SAME
116
- ``select`` call that also times out -- the exit always wins that tie, so
117
- an already-dead proc is never mistakenly reported as ``"timeout"``.
114
+ ``proc.wait()``), ``"timeout"`` (deadline reached, proc still running,
115
+ caller re-evaluates its own logic), or ``"woken"`` (only possible when
116
+ ``wake_fd`` is given: it became readable before either of the other two).
117
+ When ``proc`` has already exited by the time ``deadline`` is reached, the
118
+ exit fd is ready in the SAME ``select`` call that also times out -- the
119
+ exit always wins that tie, so an already-dead proc is never mistakenly
120
+ reported as ``"timeout"``. Likewise, when both the exit fd and
121
+ ``wake_fd`` are ready in the same ``select`` call, the exit fd wins --
122
+ an already-dead proc is never mistakenly reported as ``"woken"``.
123
+ ``wake_fd`` is caller-owned: this function neither opens, closes, nor
124
+ drains it (the caller drains it after a ``"woken"`` return); ``_procwait``
125
+ never imports ``_notify``.
118
126
 
119
127
  Fallback (``exit_fd(proc)`` is ``None``): polls ``proc.poll()`` at a
120
128
  short, fixed cadence via ``clock.sleep``, with the same two return
121
- meanings, so a test driving this path with a fake clock still makes
122
- progress without blocking on real wall time.
129
+ meanings (``wake_fd`` is IGNORED on this path -- a documented latency
130
+ degrade, not a "woken" report), so a test driving this path with a fake
131
+ clock still makes progress without blocking on real wall time. The same
132
+ poll fallback is also used when the fast path's ``select.select`` raises
133
+ ``ValueError`` (a fd >= ``FD_SETSIZE``, 1024, is unselectable) instead of
134
+ crashing.
123
135
 
124
136
  Short-circuits to ``"exited"`` when ``proc.returncode`` is already set:
125
137
  this owner has already reaped it, so its pid is freed (and may be reused).
@@ -139,8 +151,19 @@ def wait_exit(
139
151
 
140
152
  try:
141
153
  remaining = max(0.0, deadline - clock.monotonic())
142
- ready, _, _ = select.select([fd], [], [], remaining)
143
- return "exited" if ready else "timeout"
154
+ rlist = [fd, wake_fd] if wake_fd is not None else [fd]
155
+ try:
156
+ ready, _, _ = select.select(rlist, [], [], remaining)
157
+ except ValueError:
158
+ # fd >= FD_SETSIZE (1024) makes select.select raise ValueError --
159
+ # degrade to the poll fallback (its documented latency) instead
160
+ # of crashing.
161
+ return _wait_exit_by_polling(proc, deadline=deadline, clock=clock)
162
+ if fd in ready:
163
+ return "exited"
164
+ if ready:
165
+ return "woken"
166
+ return "timeout"
144
167
  finally:
145
168
  os.close(fd)
146
169
 
@@ -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.3.5'
22
- __version_tuple__ = version_tuple = (0, 3, 5)
21
+ __version__ = version = '0.3.6'
22
+ __version_tuple__ = version_tuple = (0, 3, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -141,7 +141,7 @@ def _snapshot_stray_descendants(proc: subprocess.Popen) -> list[dict]:
141
141
  while the leader (and hence its subtree) is still alive -- both reap sites
142
142
  (``_kill_pgroup`` and ``cli/_serve_round._terminate_round``) open with it, so
143
143
  the capture-before-signal ordering lives in ONE place, not copy-pasted."""
144
- stray, _ignored = _live_children(proc)
144
+ stray, _ignored = _live_children(proc, max_n=None)
145
145
  _capture_descendant_pgids(stray)
146
146
  return stray
147
147
 
@@ -206,7 +206,7 @@ def _live_children(
206
206
  proc: subprocess.Popen,
207
207
  *,
208
208
  ignore_patterns: list[re.Pattern[str]] | None = None,
209
- max_n: int = 5,
209
+ max_n: int | None = 5,
210
210
  ) -> tuple[list[dict], list[dict]]:
211
211
  """Live (non-zombie) descendants of ``proc``, split into ``(live, ignored)``.
212
212
 
@@ -217,6 +217,12 @@ def _live_children(
217
217
  events-*.jsonl. Ignore-pattern MATCHING runs against the full cmdline
218
218
  (detection unchanged); only what we STORE is minimized.
219
219
 
220
+ ``max_n`` (default 5) bounds only the event-persisted lists this returns
221
+ to a caller that writes them to events-*.jsonl. The reap snapshot
222
+ (``_snapshot_stray_descendants``) passes ``max_n=None`` — a real subtree
223
+ can exceed 5 live descendants, and undercounting it here would leave the
224
+ excess unreaped, not merely under-logged.
225
+
220
226
  The stored name is read from the kernel-reported process name (comm —
221
227
  e.g. /proc/<pid>/comm on Linux), not from ``Path(argv[0]).name``: comm is
222
228
  derived from the exec()'d binary, not from argv[0], and is itself
@@ -251,11 +257,11 @@ def _live_children(
251
257
  matched = p.pattern
252
258
  break
253
259
  if matched is not None:
254
- if len(ignored) < max_n:
260
+ if max_n is None or len(ignored) < max_n:
255
261
  ignored.append({"name": name, "pid": pid, "matched": matched})
256
- elif len(live) < max_n:
262
+ elif max_n is None or len(live) < max_n:
257
263
  live.append({"name": name, "pid": pid})
258
- if len(live) >= max_n and len(ignored) >= max_n:
264
+ if max_n is not None and len(live) >= max_n and len(ignored) >= max_n:
259
265
  break
260
266
  return live, ignored
261
267
 
@@ -28,7 +28,7 @@ from typing import Literal
28
28
 
29
29
  from agent_runner import _resolve, events, hooks, host_health, metrics
30
30
  from agent_runner._notify import NULL_LISTENER, Listener, NullListener
31
- from agent_runner._plugin_sandbox import run_hook_sandboxed
31
+ from agent_runner._plugin_sandbox import SpawnHookInterrupted, run_hook_sandboxed
32
32
  from agent_runner._procwait import wait_exit
33
33
  from agent_runner._serve_policy import (
34
34
  _MEM_LOOP_PERSIST_THRESHOLD,
@@ -251,7 +251,9 @@ def _spawn_view(work_dir, profile) -> hooks.SpawnView:
251
251
  )
252
252
 
253
253
 
254
- def _run_one_spawn_hook(h, ctx, log_dir, view, *, sandbox, engaged) -> SpawnDecision | None:
254
+ def _run_one_spawn_hook(
255
+ h, ctx, log_dir, view, *, sandbox, engaged, listener=NULL_LISTENER, stop
256
+ ) -> SpawnDecision | None:
255
257
  """Run one spawn hook, isolating any failure exactly as ``dispatch_dirty``
256
258
  isolates a raising dirty handler: emit ``hook_failed`` and return None (the
257
259
  caller omits None from the collapse — treated as proceed).
@@ -265,7 +267,9 @@ def _run_one_spawn_hook(h, ctx, log_dir, view, *, sandbox, engaged) -> SpawnDeci
265
267
  in-process (unconfined — announced once at serve boot) and ``require`` refuses
266
268
  it (belt-and-braces; ``gate_serve_boot`` already aborts serve under
267
269
  require+can't-engage). The same ``view`` is passed on every path, so ``env``
268
- stays names-only (no secret value)."""
270
+ stays names-only (no secret value). A sandboxed hook interrupted by a stop
271
+ (``should_stop`` fires on the serve doorbell) returns None WITHOUT emitting
272
+ ``hook_failed`` -- a stop is not a plugin failure."""
269
273
  third_party = not hooks._SPAWN_HOOK_BUILTIN.get(id(h), False)
270
274
  try:
271
275
  from agent_runner._sandbox_probe import hook_route
@@ -279,9 +283,19 @@ def _run_one_spawn_hook(h, ctx, log_dir, view, *, sandbox, engaged) -> SpawnDeci
279
283
  if route == "trampoline":
280
284
  module_path, attr_path = hooks._SPAWN_HOOK_MODULE.get(id(h), ("", ""))
281
285
  return run_hook_sandboxed(
282
- "spawn_hook", module_path, attr_path, h.name, ctx, log_dir=log_dir, view=view
286
+ "spawn_hook",
287
+ module_path,
288
+ attr_path,
289
+ h.name,
290
+ ctx,
291
+ log_dir=log_dir,
292
+ view=view,
293
+ wake_fd=listener.fd,
294
+ should_stop=lambda: stop["requested"],
283
295
  )
284
296
  return h.before_spawn(ctx, view)
297
+ except SpawnHookInterrupted:
298
+ return None
285
299
  except Exception as exc: # noqa: BLE001 — isolate; omit from collapse (== proceed)
286
300
  events.emit(
287
301
  log_dir,
@@ -294,7 +308,15 @@ def _run_one_spawn_hook(h, ctx, log_dir, view, *, sandbox, engaged) -> SpawnDeci
294
308
 
295
309
 
296
310
  def _maybe_defer_for_spawn_hooks(
297
- cfg, log_dir, stop, *, phase, work_dir, engaged: bool, clock: Clock = SYSTEM_CLOCK
311
+ cfg,
312
+ log_dir,
313
+ stop,
314
+ *,
315
+ phase,
316
+ work_dir,
317
+ engaged: bool,
318
+ clock: Clock = SYSTEM_CLOCK,
319
+ listener: Listener | NullListener = NULL_LISTENER,
298
320
  ) -> bool:
299
321
  """The LAST serve-admission gate: run every registered SpawnHook over a
300
322
  read-only view of the resolved spawn and collapse their verdicts
@@ -321,8 +343,17 @@ def _maybe_defer_for_spawn_hooks(
321
343
  named: list[tuple[str, SpawnDecision]] = []
322
344
  for h in registered:
323
345
  decision = _run_one_spawn_hook(
324
- h, ctx, log_dir, view, sandbox=cfg.plugins.sandbox, engaged=engaged
346
+ h,
347
+ ctx,
348
+ log_dir,
349
+ view,
350
+ sandbox=cfg.plugins.sandbox,
351
+ engaged=engaged,
352
+ listener=listener,
353
+ stop=stop,
325
354
  )
355
+ if stop["requested"]:
356
+ return False # a stop landed during a hook -- serve_cmd's stop-check refuses the spawn
326
357
  if decision is None:
327
358
  continue # hook_failed already emitted; omit from collapse (== proceed)
328
359
  if decision.action != "proceed" and h.name not in allow:
@@ -351,13 +382,13 @@ def _maybe_defer_for_spawn_hooks(
351
382
  defer_s=collapsed.defer_s,
352
383
  reason=collapsed.reason,
353
384
  )
354
- if not _interruptible_sleep(collapsed.defer_s, stop, clock=clock):
385
+ if not _interruptible_sleep(collapsed.defer_s, stop, clock=clock, listener=listener):
355
386
  emit_round_resumed(log_dir, deferred_for_s=int(clock.monotonic() - started))
356
387
  return True
357
388
  emit_plugin_spawn_decision(
358
389
  log_dir, hook=winner, action="skip", defer_s=0, reason=collapsed.reason
359
390
  )
360
- _interruptible_sleep(_SPAWN_SKIP_REPOLL_S, stop, clock=clock)
391
+ _interruptible_sleep(_SPAWN_SKIP_REPOLL_S, stop, clock=clock, listener=listener)
361
392
  return True
362
393
 
363
394
 
@@ -456,7 +456,7 @@ def _round_throttle_gate(cfg, args, log_dir, stop) -> tuple[frozenset[str], int
456
456
  return frozenset(), None
457
457
 
458
458
 
459
- def _spawn_gate(cfg, log_dir, stop, phase, *, sandbox_engaged: bool):
459
+ def _spawn_gate(cfg, log_dir, stop, phase, *, sandbox_engaged: bool, listener=NULL_LISTENER):
460
460
  """The final admission gate: run the pre-spawn hooks for the resolved
461
461
  ``phase`` and translate a defer/skip into the ``_PAUSED_CONTINUE`` sentinel
462
462
  (caller re-admits next iteration); a proceed returns ``phase`` unchanged.
@@ -465,9 +465,17 @@ def _spawn_gate(cfg, log_dir, stop, phase, *, sandbox_engaged: bool):
465
465
  already paused, and it is the LAST gate (after memory / schedule / throttle
466
466
  have all resolved a concrete phase to spawn). ``sandbox_engaged`` is the
467
467
  boot probe verdict threaded from ``cmd()`` so the seam routes without
468
- re-probing per round."""
468
+ re-probing per round. ``listener`` forwards into the sandboxed spawn-hook
469
+ wait and any defer/skip sleep, so a stop lands immediately instead of
470
+ riding out the trampoline's 30s wall-timeout."""
469
471
  if _maybe_defer_for_spawn_hooks(
470
- cfg, log_dir, stop, phase=phase, work_dir=cfg.runtime.work_dir, engaged=sandbox_engaged
472
+ cfg,
473
+ log_dir,
474
+ stop,
475
+ phase=phase,
476
+ work_dir=cfg.runtime.work_dir,
477
+ engaged=sandbox_engaged,
478
+ listener=listener,
471
479
  ):
472
480
  return _PAUSED_CONTINUE
473
481
  return phase
@@ -505,11 +513,15 @@ def _select_and_gate(
505
513
  if args.ignore_schedule:
506
514
  # rotation self-resolves in the round; no --phase, but the pre-spawn gate
507
515
  # still runs on the concrete (None) outcome.
508
- return _spawn_gate(cfg, log_dir, stop, None, sandbox_engaged=sandbox_engaged)
516
+ return _spawn_gate(
517
+ cfg, log_dir, stop, None, sandbox_engaged=sandbox_engaged, listener=listener
518
+ )
509
519
  if not _phase_aware(cfg):
510
520
  if _maybe_pause_for_schedule(cfg, log_dir, stop, listener=listener):
511
521
  return _PAUSED_CONTINUE
512
- return _spawn_gate(cfg, log_dir, stop, None, sandbox_engaged=sandbox_engaged)
522
+ return _spawn_gate(
523
+ cfg, log_dir, stop, None, sandbox_engaged=sandbox_engaged, listener=listener
524
+ )
513
525
  # Pass the clock explicitly (call-time lookup) so tests can monkeypatch
514
526
  # schedule.now_in_zone; a default arg would capture the original at import.
515
527
  sel = phase_select.select_phase(
@@ -548,7 +560,9 @@ def _select_and_gate(
548
560
  chosen=sel.phase,
549
561
  active_window=sel.active_window or "",
550
562
  )
551
- return _spawn_gate(cfg, log_dir, stop, sel.phase, sandbox_engaged=sandbox_engaged)
563
+ return _spawn_gate(
564
+ cfg, log_dir, stop, sel.phase, sandbox_engaged=sandbox_engaged, listener=listener
565
+ )
552
566
 
553
567
 
554
568
  def _prune_serve_round_logs(log_dir: Path, retention: int) -> None:
@@ -34,13 +34,14 @@ DEFAULT_SIGTERM_GRACE_S = 10
34
34
 
35
35
  # Ceiling for [agent] sigterm_grace_s: sits a STRICT ~3s BELOW _serve_policy's
36
36
  # _ROUND_TERM_GRACE_S (15, the supervisor's own wait for the round leader) --
37
- # NOT equal to it. The margin matters on the out-of-process `agent-runner kill`
38
- # path (_lifecycle._terminate_round_pid), which SIGKILLs only the leader pid
39
- # with no stray-reap: without it, a leader whose own killpg(SIGKILL) of its
40
- # agent fires at the same instant the supervisor SIGKILLs the leader could lose
41
- # the race, leaving the agent permanently orphaned. config/ cannot import
42
- # _serve_policy (a cycle: _serve_policy already imports agent_runner.config for
43
- # ConfigError), so this is a LITERAL mirror-with-margin, pinned by
37
+ # NOT equal to it. The margin buys the round LEADER's own cooperative
38
+ # _kill_pgroup (SIGTERM -> grace -> killpg of its agent) room to finish before
39
+ # any outside SIGKILL lands: on the out-of-process `agent-runner kill` path
40
+ # (_lifecycle._terminate_round_pid) the leader is SIGTERM'd first and only
41
+ # SIGKILLed after this same grace, so a flushing agent isn't cut off an instant
42
+ # before the leader's own escalation. config/ cannot import _serve_policy (a
43
+ # cycle: _serve_policy already imports agent_runner.config for ConfigError), so
44
+ # this is a LITERAL mirror-with-margin, pinned by
44
45
  # tests/invariants/test_timeout_budget_invariant.py against drift, the same
45
46
  # pattern _serve_policy uses for its own agent_runtime.REAP_GRACE_S /
46
47
  # vcs_state.GIT_COMMIT_TIMEOUT_S mirrors.
@@ -89,6 +89,10 @@ there:
89
89
  - `start` is idempotent.
90
90
  - `stop` waits up to `round_budget_s` for the current round.
91
91
  - `kill` (and `restart --force`) is for a stuck round only — 5s grace then SIGKILL.
92
+ - `agent-runner kill` does NOT honor `[agent] sigterm_grace_s`: under a systemd
93
+ `--user` unit it `systemctl kill`s the whole cgroup after a fixed 5 s, so a
94
+ cooperative agent (e.g. `gemini`) that needs its full grace to flush should be
95
+ stopped with `agent-runner serve stop` (drain-aware) rather than `kill`.
92
96
  - `restart` against a `--system`-installed unit refuses, printing the
93
97
  `sudo systemctl restart ...` command to run instead (it manages user-scope
94
98
  units only). `status` still reports it correctly — a `--system` install
@@ -496,6 +496,34 @@ No new internal wait — the grace window rides the v0.3.4 `wait_exit` deadline
496
496
 
497
497
  Dev-host-relative (macOS). The grace's real value shows on the constrained fleet (a `gemini` round flushing under memory pressure before an early-SIGTERM nudge) — that nudge is the 462 MB-constrained-host-gated v0.3.7 work; this release ships only the correct, configurable grace it will use. No new constrained-host claim.
498
498
 
499
+ ## 0.3.5 → 0.3.6 (2026-09-15)
500
+
501
+ Two closed lifecycle gaps, not a new subsystem: `kill` now reaps a stuck round's detached descendants the way `serve stop` already did, and the sandboxed spawn-hook trampoline wait now joins the same `wait_exit` fd primitive as the rest of `serve`, so a stop during a confined third-party hook (or a plugin `defer`) reacts immediately instead of riding out the old poll. Same macOS harness (§ methodology above).
502
+
503
+ ### 1. Import/startup RSS
504
+
505
+ | | 0.3.5 | 0.3.6 | Δ |
506
+ |---|---|---|---|
507
+ | RSS (avg of 5 cold runs) | 24.12 MB | 24.18 MB | +0.06 MB (flat, within noise) |
508
+ | RSS range | 24.03–24.19 MB* | 24.00–24.34 MB | |
509
+
510
+ \* one v0.3.5 run came in at 27.17 MB — the same first-invocation cold-page-cache outlier this page excludes elsewhere (see Caveats); dropped from the average/range as noise.
511
+
512
+ Flat, and no new import edge. `_plugin_sandbox`/`_procwait`/`_notify` are NOT on the bare `import agent_runner` graph, but they ARE on the `import agent_runner.cli` (serve/round) startup graph — frozen in `test_import_footprint.py`'s `EXPECTED_STARTUP_PKG_MODULES` allowlist since the v0.3.2 SpawnHook seam (`_serve_round` imports `run_hook_sandboxed` at module level) and the v0.3.4 doorbell. v0.3.6 adds no new module to that graph — only new NAMES within those already-imported modules (`wake_fd`/`should_stop`/`SpawnHookInterrupted`/`_DRAIN_JOIN_S`/`_join_drain_threads`) — so the allowlist is unchanged and `test_import_footprint.py` + `test_round_alloc_growth.py` are green. Base dependency stays `psutil>=5.9`. (The raw `sys.modules` count shows +1, `agent_runner._version` — a generated file a fresh throwaway worktree lacks, a measurement artifact, not a code change.)
513
+
514
+ ### 2. Efficiency
515
+
516
+ No headline RSS number here — the win is in wakeups and reaction latency, and it's conditional on workload. For the first-party plugin fleet, which runs no third-party spawn hooks, this release is flat, same as 0.3.5. The number that moves is scoped to whoever *does* run one:
517
+
518
+ - **Per engaged third-party spawn hook, the trampoline wait drops from ~20 wakeups/s to exactly 1.** The old confinement trampoline joined a stdlib `subprocess.Popen.wait()`, which sleep-polls internally (~20 Hz); the rewrite blocks the trampoline on the same `wait_exit` fd primitive the rest of `serve` already uses, so it wakes exactly once, on the hook's actual exit — asserted by `test_plugin_sandbox_wire.py`'s `calls["n"] == 1` property test.
519
+ - **A stop during a confined hook or a plugin `defer` now takes effect immediately instead of after up to 30 s**, and the confined hook's child process is always reaped, never left running — closing the two lifecycle gaps this release targets. Behavioral, not an RSS change.
520
+
521
+ The trampoline bounds its post-reap pipe drain to `_DRAIN_JOIN_S` (`agent_runner/_plugin_sandbox.py`): if a process the hook itself `fork()`'d keeps the reaped child's output pipe open, the drain thread stays blocked in `read()` and is abandoned (a daemon thread) rather than stalling the stop. That leaks at most one drain-thread pair per such invocation, bounded by the plugin's own process leak and negligible in RSS (an untouched thread stack); the thread dies when the fork()'d holder exits or when `serve` itself does. Reaping the hook's own `fork()`'d descendants is a recorded internal follow-up.
522
+
523
+ ### 3. Constrained-host (honesty)
524
+
525
+ Dev-host-relative (macOS). Neither closed gap is constrained-host-specific — both are plain process-lifecycle correctness fixes exercised on CI (Linux, where the sandbox trampoline actually engages) and dev. No new constrained-host claim; the 462 MB `memory.high` write stays 0.3.7, Pi-gated.
526
+
499
527
  ## Enforcement
500
528
 
501
529
  Four invariant tests keep these numbers from drifting silently: