cli-agent-runner 0.3.4__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.4 → cli_agent_runner-0.3.6}/.vulture-whitelist.py +2 -0
  2. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/CHANGELOG.md +11 -0
  3. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/PKG-INFO +1 -1
  4. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_lifecycle.py +16 -7
  5. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_notify.py +15 -6
  6. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_manifest.py +20 -0
  7. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_sandbox.py +93 -13
  8. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_procwait.py +38 -15
  9. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_serve_policy.py +13 -2
  10. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_version.py +2 -2
  11. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/agent_runtime.py +27 -9
  12. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_round.py +45 -12
  13. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/common.py +8 -2
  14. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/doctor_cmd.py +15 -0
  15. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/serve_cmd.py +69 -6
  16. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/models.py +27 -0
  17. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/parsers.py +24 -0
  18. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/runner.py +24 -0
  19. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/commands.md +5 -1
  20. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/configuration.md +15 -0
  21. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/memory-efficiency.md +51 -0
  22. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/plugins.md +2 -2
  23. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/runbook.md +4 -0
  24. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_auto_stop_e2e.py +9 -1
  25. cli_agent_runner-0.3.6/tests/integration/test_cooperative_grace_property.py +142 -0
  26. cli_agent_runner-0.3.6/tests/integration/test_reap_property.py +314 -0
  27. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_run_one_round_with_fake_agent.py +26 -0
  28. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_wedged.py +14 -0
  29. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_architecture.py +2 -1
  30. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_peek_schema_version.py +1 -1
  31. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_timeout_budget_invariant.py +44 -2
  32. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_grace.py +82 -0
  33. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_reap.py +80 -0
  34. cli_agent_runner-0.3.6/tests/unit/test_apply_reap_grace_env.py +92 -0
  35. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config.py +44 -0
  36. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_cmd.py +9 -0
  37. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_kill_pgroup_shielding.py +24 -0
  38. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_notify.py +16 -0
  39. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_sandbox_block.py +26 -0
  40. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_manifest.py +44 -0
  41. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_sandbox_wire.py +252 -1
  42. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_procwait.py +14 -0
  43. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_runner.py +35 -0
  44. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_service_unit.py +4 -3
  45. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_spawn_seam.py +61 -1
  46. cli_agent_runner-0.3.4/tests/integration/test_reap_property.py +0 -153
  47. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.codecov.yml +0 -0
  48. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  49. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  50. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  51. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  52. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/dependabot.yml +0 -0
  53. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/workflows/ci.yml +0 -0
  54. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/workflows/release.yml +0 -0
  55. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.gitignore +0 -0
  56. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/BACKLOG.md +0 -0
  57. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/CODE_OF_CONDUCT.md +0 -0
  58. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/CONTRIBUTING.md +0 -0
  59. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/LICENSE +0 -0
  60. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/README.md +0 -0
  61. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/README.zh.md +0 -0
  62. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/SECURITY.md +0 -0
  63. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/__init__.py +0 -0
  64. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_docgen.py +0 -0
  65. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/__init__.py +0 -0
  66. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/agent.py +0 -0
  67. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/memory.py +0 -0
  68. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/plugins.py +0 -0
  69. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/rounds.py +0 -0
  70. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/serve.py +0 -0
  71. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_install.py +0 -0
  72. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_monitor_detectors.py +0 -0
  73. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_monitor_registry.py +0 -0
  74. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_monitor_state.py +0 -0
  75. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_observe.py +0 -0
  76. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_checksum.py +0 -0
  77. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_scan.py +0 -0
  78. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_redact.py +0 -0
  79. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_registry.py +0 -0
  80. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_resolve.py +0 -0
  81. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_round_outcome.py +0 -0
  82. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_round_support.py +0 -0
  83. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_sandbox_probe.py +0 -0
  84. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_substrate.py +0 -0
  85. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_throttle.py +0 -0
  86. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/api.py +0 -0
  87. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/api_types.py +0 -0
  88. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/__init__.py +0 -0
  89. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/_constants.py +0 -0
  90. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  91. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/codewhale.py +0 -0
  92. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  93. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/gemini.py +0 -0
  94. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/kimi.py +0 -0
  95. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/pi.py +0 -0
  96. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/__init__.py +0 -0
  97. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/__main__.py +0 -0
  98. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_cgroup.py +0 -0
  99. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/events_cmd.py +0 -0
  100. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/init_cmd.py +0 -0
  101. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/install_cmd.py +0 -0
  102. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/migrate_cmd.py +0 -0
  103. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/monitor_cmd.py +0 -0
  104. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/peek_cmd.py +0 -0
  105. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/round_cmd.py +0 -0
  106. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/service_cmd.py +0 -0
  107. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/upgrade_cmd.py +0 -0
  108. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/clock.py +0 -0
  109. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/__init__.py +0 -0
  110. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/errors.py +0 -0
  111. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/loader.py +0 -0
  112. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/validators.py +0 -0
  113. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/context_store.py +0 -0
  114. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/defenses.py +0 -0
  115. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/events.py +0 -0
  116. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/hooks.py +0 -0
  117. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/host_health.py +0 -0
  118. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/http_progress.py +0 -0
  119. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/lifecycle.py +0 -0
  120. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/metrics.py +0 -0
  121. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/migrations.py +0 -0
  122. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/monitor.py +0 -0
  123. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/phase_select.py +0 -0
  124. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/__init__.py +0 -0
  125. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/aider.toml +0 -0
  126. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/claude.toml +0 -0
  127. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/codewhale.toml +0 -0
  128. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/gemini.toml +0 -0
  129. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/kimi.toml +0 -0
  130. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/pi.toml +0 -0
  131. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/prompt_loader.py +0 -0
  132. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/remote_relay.py +0 -0
  133. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/round_log.py +0 -0
  134. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/round_view.py +0 -0
  135. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/scaffold.py +0 -0
  136. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/schedule.py +0 -0
  137. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/service_unit.py +0 -0
  138. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/startup_check.py +0 -0
  139. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/vcs_state.py +0 -0
  140. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/build.sh +0 -0
  141. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/deploy/Dockerfile +0 -0
  142. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/deploy/docker-compose.yml +0 -0
  143. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/README.md +0 -0
  144. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/architecture.md +0 -0
  145. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/events.md +0 -0
  146. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/long-running-agents.md +0 -0
  147. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/migrations/0.2.md +0 -0
  148. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/migrations/0.3.md +0 -0
  149. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/quickstart.md +0 -0
  150. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/aider.md +0 -0
  151. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/codewhale.md +0 -0
  152. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/container-pi.md +0 -0
  153. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/deploy-all-in-one.md +0 -0
  154. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/kimi.md +0 -0
  155. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/pi.md +0 -0
  156. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/thesis.md +0 -0
  157. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/pyproject.toml +0 -0
  158. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/__init__.py +0 -0
  159. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/_clock.py +0 -0
  160. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/_test_helpers.py +0 -0
  161. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/conftest.py +0 -0
  162. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/contract/__init__.py +0 -0
  163. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/contract/test_public_api_surface.py +0 -0
  164. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/__init__.py +0 -0
  165. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/conftest.py +0 -0
  166. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  167. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_install_systemd.py +0 -0
  168. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  169. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  170. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  171. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  172. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  173. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/generate_vulture_whitelist.py +0 -0
  174. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/__init__.py +0 -0
  175. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_bounded_run.py +0 -0
  176. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_context_enricher_namespacing.py +0 -0
  177. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_dirty_handler_seam.py +0 -0
  178. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_fresh_eyes_signal.py +0 -0
  179. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_grace_kill_emission.py +0 -0
  180. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_install_dry_run.py +0 -0
  181. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_monitor_seeded.py +0 -0
  182. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_detector_loaded.py +0 -0
  183. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_owned_paths.py +0 -0
  184. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_real_flow.py +0 -0
  185. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  186. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_scaffold_presets.py +0 -0
  187. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_serve_loop.py +0 -0
  188. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  189. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_substrate_fingerprint.py +0 -0
  190. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_transient_error_backoff.py +0 -0
  191. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/__init__.py +0 -0
  192. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/_docs.py +0 -0
  193. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/_event_scan.py +0 -0
  194. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_atomic_write_enforced.py +0 -0
  195. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_authored_facts_declared.py +0 -0
  196. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  197. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_bdd_test_conventions.py +0 -0
  198. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
  199. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_catalogs.py +0 -0
  200. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_classification_ssot.py +0 -0
  201. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_config_error_consistency.py +0 -0
  202. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_config_value_set_ssot.py +0 -0
  203. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_container_stop_bound.py +0 -0
  204. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  205. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  206. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  207. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_cli_claims.py +0 -0
  208. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_operator_surface.py +0 -0
  209. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  210. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  211. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  212. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_generated.py +0 -0
  213. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_index_complete.py +0 -0
  214. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_entry_points_resolve.py +0 -0
  215. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_event_int_coercion.py +0 -0
  216. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kind_registry.py +0 -0
  217. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kinds_ssot.py +0 -0
  218. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_events_doc_contract.py +0 -0
  219. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  220. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_hook_contract_docs.py +0 -0
  221. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_import_footprint.py +0 -0
  222. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
  223. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_layer_2_loop_size.py +0 -0
  224. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  225. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_module_boundaries.py +0 -0
  226. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_module_sizes.py +0 -0
  227. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  228. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_ai_signatures.py +0 -0
  229. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_asyncio_select.py +0 -0
  230. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_internal_refs.py +0 -0
  231. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_price_logic.py +0 -0
  232. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  233. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_raw_time.py +0 -0
  234. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  235. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_version_refs.py +0 -0
  236. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_phase_select_stateless.py +0 -0
  237. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_plugins_config_stable.py +0 -0
  238. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_procwait_shield.py +0 -0
  239. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  240. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  241. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_alloc_growth.py +0 -0
  242. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  243. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_num_single_source.py +0 -0
  244. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_result_stable.py +0 -0
  245. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  246. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  247. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_upstream_schema_canary.py +0 -0
  248. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  249. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/linux/__init__.py +0 -0
  250. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/linux/test_plugin_sandbox_kill.py +0 -0
  251. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/__init__.py +0 -0
  252. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/parser.py +0 -0
  253. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/test_parser.py +0 -0
  254. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/test_quickstart.py +0 -0
  255. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/__init__.py +0 -0
  256. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime.py +0 -0
  257. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_container.py +0 -0
  258. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  259. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_progress.py +0 -0
  260. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  261. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_assemble_prompt.py +0 -0
  262. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_events_stream.py +0 -0
  263. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_install.py +0 -0
  264. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_observation.py +0 -0
  265. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_read_round_num.py +0 -0
  266. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_resolve_phase.py +0 -0
  267. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_service.py +0 -0
  268. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_types.py +0 -0
  269. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_auto_stop_gating.py +0 -0
  270. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_builtin_provenance.py +0 -0
  271. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_catalogs.py +0 -0
  272. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cgroup_probe.py +0 -0
  273. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_claude_error_detector.py +0 -0
  274. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_claude_foreign_schema.py +0 -0
  275. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli.py +0 -0
  276. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_common.py +0 -0
  277. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_init_install.py +0 -0
  278. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_main_boundary.py +0 -0
  279. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_monitor_http.py +0 -0
  280. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  281. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_upgrade.py +0 -0
  282. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_code_precedence.py +0 -0
  283. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_plugin.py +0 -0
  284. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_coerce_int_poison.py +0 -0
  285. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_collapse_spawn_decisions.py +0 -0
  286. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_fresh_eyes.py +0 -0
  287. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_max_rounds.py +0 -0
  288. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_nested_strictness.py +0 -0
  289. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_prompt_delivery.py +0 -0
  290. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_schedule.py +0 -0
  291. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_stop_file.py +0 -0
  292. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_strictness.py +0 -0
  293. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  294. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_transient_error_action.py +0 -0
  295. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_context_store.py +0 -0
  296. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_control_char_config.py +0 -0
  297. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_default_dirty_handler.py +0 -0
  298. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_defenses.py +0 -0
  299. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_detect_hung_latch.py +0 -0
  300. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_detector_isolation.py +0 -0
  301. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_detector_protocol.py +0 -0
  302. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_dirty_handlers.py +0 -0
  303. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
  304. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_docgen.py +0 -0
  305. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_sandbox.py +0 -0
  306. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_effective_throttle_view.py +0 -0
  307. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_emit_config_migrated.py +0 -0
  308. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule.py +0 -0
  309. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  310. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events.py +0 -0
  311. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd.py +0 -0
  312. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_rollover.py +0 -0
  313. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_tail.py +0 -0
  314. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_jsonl_decode.py +0 -0
  315. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_example_dirty_handler.py +0 -0
  316. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_example_plugin.py +0 -0
  317. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_exec_prefix.py +0 -0
  318. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_exit_cause.py +0 -0
  319. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_exit_classifier.py +0 -0
  320. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  321. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_gate_serve_boot.py +0 -0
  322. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_gemini_plugin.py +0 -0
  323. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_git_timeout.py +0 -0
  324. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_hook_failure_isolation.py +0 -0
  325. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_hook_redact_order.py +0 -0
  326. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_hooks.py +0 -0
  327. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_host_health.py +0 -0
  328. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress.py +0 -0
  329. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress_epoch.py +0 -0
  330. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_init_entry_points.py +0 -0
  331. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  332. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_kimi_plugin.py +0 -0
  333. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_lifecycle.py +0 -0
  334. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_load_fail_closed.py +0 -0
  335. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_loader_split.py +0 -0
  336. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_manifest_spawn_hooks.py +0 -0
  337. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_metrics.py +0 -0
  338. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  339. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_swap.py +0 -0
  340. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_migrate_cmd.py +0 -0
  341. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_migrations.py +0 -0
  342. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_assembly.py +0 -0
  343. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_cmd_severity.py +0 -0
  344. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_identity.py +0 -0
  345. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  346. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  347. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  348. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  349. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detectors.py +0 -0
  350. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_event_tail.py +0 -0
  351. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_loop.py +0 -0
  352. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_on_alert.py +0 -0
  353. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_remote.py +0 -0
  354. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  355. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_schedule_pause.py +0 -0
  356. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_network_blip_signal_guard.py +0 -0
  357. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_open_round_log.py +0 -0
  358. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  359. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_outer_round_ceiling.py +0 -0
  360. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_argparse.py +0 -0
  361. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_project_isolation.py +0 -0
  362. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_schedule.py +0 -0
  363. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_select.py +0 -0
  364. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_phase_agent_launch.py +0 -0
  365. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_phase_profiles.py +0 -0
  366. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_phase_select.py +0 -0
  367. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_pi_plugin.py +0 -0
  368. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_checksum.py +0 -0
  369. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_constants.py +0 -0
  370. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_name_mismatch.py +0 -0
  371. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_scan_parity.py +0 -0
  372. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugins_config_fields.py +0 -0
  373. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_presets.py +0 -0
  374. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_primary_prompt_file_home.py +0 -0
  375. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_prompt_loader.py +0 -0
  376. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_read_json_hardening.py +0 -0
  377. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_redact.py +0 -0
  378. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_resolve.py +0 -0
  379. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd.py +0 -0
  380. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd_sigterm.py +0 -0
  381. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_log_helpers.py +0 -0
  382. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome.py +0 -0
  383. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  384. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_view.py +0 -0
  385. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  386. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_runner_throttle.py +0 -0
  387. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_sandbox_probe.py +0 -0
  388. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_scaffold.py +0 -0
  389. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_schedule.py +0 -0
  390. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_cmd_bounded.py +0 -0
  391. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_config_broken.py +0 -0
  392. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_crash_loop.py +0 -0
  393. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_mem_gate.py +0 -0
  394. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_phase_schedule.py +0 -0
  395. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_policy.py +0 -0
  396. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round.py +0 -0
  397. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  398. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_log.py +0 -0
  399. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_oom_killed.py +0 -0
  400. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_terminate.py +0 -0
  401. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_wait_exit.py +0 -0
  402. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_schedule_gate.py +0 -0
  403. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_sentinel.py +0 -0
  404. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_startup_hooks.py +0 -0
  405. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_wakeup.py +0 -0
  406. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_service_mode.py +0 -0
  407. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_startup_battery_exit.py +0 -0
  408. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_startup_check.py +0 -0
  409. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_substrate.py +0 -0
  410. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  411. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_terminate_round_wait_exit.py +0 -0
  412. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  413. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_epoch_bound.py +0 -0
  414. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_plugin_classification.py +0 -0
  415. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_upgrade_migrate.py +0 -0
  416. {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_vcs_state.py +0 -0
@@ -158,6 +158,7 @@ overlaps
158
158
  plan
159
159
  sandbox
160
160
  cgroup
161
+ sigterm_grace
161
162
 
162
163
  # agent_runner.config.models.AgentConfig
163
164
  command
@@ -167,6 +168,7 @@ env
167
168
  prompt_delivery
168
169
  exec_prefix
169
170
  terminal_marker
171
+ sigterm_grace_s
170
172
 
171
173
  # agent_runner.config.models.Config
172
174
  agent
@@ -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.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
+
14
+ ## [0.3.5] - 2026-09-15
15
+
16
+ ### Added
17
+ - `[agent] sigterm_grace_s` (default 10): the SIGTERM→SIGKILL grace a round gives its agent when the agent's preset declares itself cooperative (the built-in `gemini` preset does), so an agent that catches SIGTERM to flush or commit its work gets time to finish. A non-cooperative agent (e.g. `claude`) keeps the previous 5 s. Capped at 12 s, inside the supervisor's 15 s wait for the round; `peek --json` (schema `2.4`) and `doctor` report the resolved grace per agent.
18
+
8
19
  ## [0.3.4] - 2026-09-15
9
20
 
10
21
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.3.4
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
 
@@ -62,6 +62,26 @@ def cooperative_manifest_names() -> list[str]:
62
62
  return [m.name for m in _LOADED_MANIFESTS if m.sigterm_cooperative]
63
63
 
64
64
 
65
+ def is_cooperative_agent(agent_binary: str | None) -> bool:
66
+ """Whether the agent's preset declares itself SIGTERM-cooperative.
67
+ `None in [...]` is safely False, so no None-guard is needed."""
68
+ return agent_binary in cooperative_manifest_names()
69
+
70
+
71
+ def resolve_sigterm_grace_s(agent_binary: str | None, sigterm_grace_s: int) -> int:
72
+ """The SIGTERM->SIGKILL grace this round's agent actually gets:
73
+ `sigterm_grace_s` (config's [agent] sigterm_grace_s, boot-capped at
74
+ _ROUND_TERM_GRACE_S) when `agent_binary` names a manifest declaring
75
+ sigterm_cooperative=True, else agent_runtime.REAP_GRACE_S (5s)
76
+ unchanged. One function, two callers: cli.serve_cmd._apply_reap_grace_env
77
+ (what the agent actually gets) and doctor/peek (what an operator sees)."""
78
+ from agent_runner.agent_runtime import REAP_GRACE_S
79
+
80
+ if is_cooperative_agent(agent_binary):
81
+ return sigterm_grace_s
82
+ return REAP_GRACE_S
83
+
84
+
65
85
  def register_manifest(
66
86
  manifest: PluginManifest,
67
87
  *,
@@ -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
 
@@ -312,13 +312,24 @@ def timeout_budget(round_budget_s: int) -> tuple[int, int]:
312
312
  round subprocess (``api.outer_round_ceiling_s``): ``round_budget_s``
313
313
  plus reap grace + git-commit ceiling + hook allowance, so it only trips
314
314
  when the round supervisor itself is wedged, never during its own
315
- bounded post-round cleanup.
315
+ bounded post-round cleanup. The reap margin must cover the widest grace
316
+ ANY round can be configured with -- a cooperative agent's
317
+ ``sigterm_grace_s`` is boot-capped at ``_ROUND_TERM_GRACE_S``
318
+ (``config/models.py``'s mirror of this module's single source), not
319
+ the non-cooperative default ``_REAP_GRACE_S``, so the margin is
320
+ ``max(_REAP_GRACE_S, _ROUND_TERM_GRACE_S)`` -- else a long cooperative
321
+ grace could trip the outer wall-clock ceiling mid-grace.
316
322
  - ``timeout_stop_sec`` — systemd's ``TimeoutStopSec``
317
323
  (``service_unit.py``): must clear ``outer_ceiling_s`` by enough for a
318
324
  SIGTERM to reach and drain the round (``_ROUND_TERM_GRACE_S``) plus a
319
325
  stop-request overhead pad, so `systemctl stop` never SIGKILLs a round
320
326
  that is draining normally.
321
327
  """
322
- outer_ceiling_s = round_budget_s + _REAP_GRACE_S + _GIT_COMMIT_TIMEOUT_S + _HOOK_ALLOWANCE_S
328
+ outer_ceiling_s = (
329
+ round_budget_s
330
+ + max(_REAP_GRACE_S, _ROUND_TERM_GRACE_S)
331
+ + _GIT_COMMIT_TIMEOUT_S
332
+ + _HOOK_ALLOWANCE_S
333
+ )
323
334
  timeout_stop_sec = outer_ceiling_s + _ROUND_TERM_GRACE_S + _STOP_GRACE_MARGIN_S
324
335
  return timeout_stop_sec, outer_ceiling_s
@@ -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.4'
22
- __version_tuple__ = version_tuple = (0, 3, 4)
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
 
@@ -161,7 +161,9 @@ def _wait_exit_shielded(
161
161
  continue
162
162
 
163
163
 
164
- def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
164
+ def _kill_pgroup(
165
+ proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK, reap_grace_s: int = REAP_GRACE_S
166
+ ) -> None:
165
167
  """SIGTERM the pgroup, grace, then SIGKILL — the reap primitive shared by
166
168
  the round-timeout path and ``run``'s BaseException handler (which fires on
167
169
  a SIGTERM landing while the round is unwinding from a first one).
@@ -185,7 +187,7 @@ def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
185
187
  except OSError:
186
188
  pass
187
189
  deadline = (
188
- clock.monotonic() + REAP_GRACE_S
190
+ clock.monotonic() + reap_grace_s
189
191
  ) # monotonic: an NTP step must not stretch/skip the reap
190
192
  _wait_exit_shielded(proc, deadline=deadline, clock=clock)
191
193
  # "exited" or "timeout": either way, fall through to the SIGKILL last-resort
@@ -204,7 +206,7 @@ def _live_children(
204
206
  proc: subprocess.Popen,
205
207
  *,
206
208
  ignore_patterns: list[re.Pattern[str]] | None = None,
207
- max_n: int = 5,
209
+ max_n: int | None = 5,
208
210
  ) -> tuple[list[dict], list[dict]]:
209
211
  """Live (non-zombie) descendants of ``proc``, split into ``(live, ignored)``.
210
212
 
@@ -215,6 +217,12 @@ def _live_children(
215
217
  events-*.jsonl. Ignore-pattern MATCHING runs against the full cmdline
216
218
  (detection unchanged); only what we STORE is minimized.
217
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
+
218
226
  The stored name is read from the kernel-reported process name (comm —
219
227
  e.g. /proc/<pid>/comm on Linux), not from ``Path(argv[0]).name``: comm is
220
228
  derived from the exec()'d binary, not from argv[0], and is itself
@@ -249,11 +257,11 @@ def _live_children(
249
257
  matched = p.pattern
250
258
  break
251
259
  if matched is not None:
252
- if len(ignored) < max_n:
260
+ if max_n is None or len(ignored) < max_n:
253
261
  ignored.append({"name": name, "pid": pid, "matched": matched})
254
- elif len(live) < max_n:
262
+ elif max_n is None or len(live) < max_n:
255
263
  live.append({"name": name, "pid": pid})
256
- 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:
257
265
  break
258
266
  return live, ignored
259
267
 
@@ -574,6 +582,7 @@ def _terminate_agent(
574
582
  container_runtime: str | None,
575
583
  container_cidfile: Path | None,
576
584
  on_container_orphan_risk: Callable[[str, str | None, bool | None], None] | None,
585
+ reap_grace_s: int = REAP_GRACE_S,
577
586
  ) -> None:
578
587
  """Reap the agent pgroup (``_kill_pgroup``), then -- for a
579
588
  container-launching command only -- make a best-effort container `stop`
@@ -581,7 +590,7 @@ def _terminate_agent(
581
590
  hard-kill entry point ``run()`` uses for all three of its termination
582
591
  paths (R1128 wall-clock, grace-kill, and the BaseException reap), so the
583
592
  container handling isn't duplicated three times."""
584
- _kill_pgroup(proc, clock)
593
+ _kill_pgroup(proc, clock, reap_grace_s=reap_grace_s)
585
594
  if container_runtime is not None:
586
595
  cid, stop_ok = _best_effort_container_stop(container_runtime, container_cidfile)
587
596
  if on_container_orphan_risk is not None:
@@ -606,10 +615,16 @@ def run(
606
615
  grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
607
616
  on_container_orphan_risk: Callable[[str, str | None, bool | None], None] | None = None,
608
617
  clock: Clock = SYSTEM_CLOCK,
618
+ reap_grace_s: int = REAP_GRACE_S,
609
619
  ) -> RunResult:
610
620
  """Spawn the agent subprocess and wait for exit or timeout.
611
621
 
612
- Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
622
+ Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → reap_grace_s → SIGKILL.
623
+
624
+ reap_grace_s: the SIGTERM->SIGKILL grace passed straight through to every
625
+ ``_terminate_agent`` call site below (R1128 wall-clock, grace-kill, and
626
+ the BaseException reap) -- one deadline value, threaded end to end.
627
+ Defaults to REAP_GRACE_S so every existing caller is unchanged.
613
628
 
614
629
  work_dir: the agent child's working directory; callers pass the
615
630
  already-absolute cfg.runtime.work_dir. CLIs with no --cwd flag of their
@@ -776,6 +791,7 @@ def run(
776
791
  container_runtime=container_runtime,
777
792
  container_cidfile=container_cidfile,
778
793
  on_container_orphan_risk=_report_orphan_once,
794
+ reap_grace_s=reap_grace_s,
779
795
  )
780
796
  duration = clock.monotonic() - start
781
797
  exit_code = proc.returncode if proc.returncode is not None else -1
@@ -820,6 +836,7 @@ def run(
820
836
  container_runtime=container_runtime,
821
837
  container_cidfile=container_cidfile,
822
838
  on_container_orphan_risk=_report_orphan_once,
839
+ reap_grace_s=reap_grace_s,
823
840
  )
824
841
  duration = clock.monotonic() - start
825
842
  exit_code = proc.returncode if proc.returncode is not None else -1
@@ -860,6 +877,7 @@ def run(
860
877
  container_runtime=container_runtime,
861
878
  container_cidfile=container_cidfile,
862
879
  on_container_orphan_risk=_report_orphan_once,
880
+ reap_grace_s=reap_grace_s,
863
881
  )
864
882
  raise
865
883
  finally: