cli-agent-runner 0.2.21__tar.gz → 0.2.23__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 (373) hide show
  1. cli_agent_runner-0.2.23/.github/dependabot.yml +6 -0
  2. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.github/workflows/ci.yml +4 -4
  3. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.github/workflows/release.yml +11 -7
  4. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.vulture-whitelist.py +18 -0
  5. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/BACKLOG.md +0 -27
  6. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/CHANGELOG.md +28 -0
  7. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/PKG-INFO +2 -2
  8. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/README.md +1 -1
  9. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/SECURITY.md +17 -0
  10. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_emit/__init__.py +2 -0
  11. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_emit/serve.py +20 -0
  12. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_monitor_state.py +26 -6
  13. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_version.py +2 -2
  14. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/agent_runtime.py +52 -23
  15. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/api.py +36 -6
  16. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/api_types.py +5 -0
  17. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/__init__.py +2 -0
  18. cli_agent_runner-0.2.23/agent_runner/cli/doctor_cmd.py +77 -0
  19. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/serve_cmd.py +11 -0
  20. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/config/models.py +11 -0
  21. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/config/parsers.py +3 -0
  22. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/defenses.py +6 -2
  23. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/events.py +1 -0
  24. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/lifecycle.py +27 -12
  25. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/phase_select.py +63 -0
  26. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/presets/claude.toml +1 -1
  27. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/presets/codewhale.toml +3 -0
  28. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/presets/pi.toml +3 -0
  29. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/runner.py +5 -0
  30. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/startup_check.py +127 -49
  31. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/architecture.md +1 -0
  32. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/commands.md +29 -4
  33. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/configuration.md +16 -0
  34. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/memory-efficiency.md +48 -0
  35. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/recipes/codewhale.md +3 -0
  36. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/recipes/container-pi.md +16 -0
  37. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/runbook.md +6 -4
  38. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/contract/test_public_api_surface.py +1 -0
  39. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_serve_loop.py +63 -2
  40. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_architecture.py +11 -2
  41. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_import_footprint.py +5 -0
  42. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_container.py +118 -8
  43. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_grace.py +102 -0
  44. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_service.py +154 -0
  45. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config.py +28 -0
  46. cli_agent_runner-0.2.23/tests/unit/test_doctor_cmd.py +86 -0
  47. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_events.py +4 -0
  48. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_lifecycle.py +31 -0
  49. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_assembly.py +29 -0
  50. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_phase_select.py +115 -0
  51. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_startup_check.py +114 -0
  52. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.codecov.yml +0 -0
  53. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  54. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  55. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  56. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  57. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/.gitignore +0 -0
  58. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/CODE_OF_CONDUCT.md +0 -0
  59. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/CONTRIBUTING.md +0 -0
  60. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/LICENSE +0 -0
  61. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/README.zh.md +0 -0
  62. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/__init__.py +0 -0
  63. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_docgen.py +0 -0
  64. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_emit/agent.py +0 -0
  65. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_emit/memory.py +0 -0
  66. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_emit/rounds.py +0 -0
  67. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_monitor_detectors.py +0 -0
  68. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_monitor_registry.py +0 -0
  69. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_plugin_scan.py +0 -0
  70. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_redact.py +0 -0
  71. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_registry.py +0 -0
  72. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_resolve.py +0 -0
  73. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_round_outcome.py +0 -0
  74. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_round_support.py +0 -0
  75. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_serve_policy.py +0 -0
  76. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_substrate.py +0 -0
  77. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/_throttle.py +0 -0
  78. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/__init__.py +0 -0
  79. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/_constants.py +0 -0
  80. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  81. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/codewhale.py +0 -0
  82. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  83. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/gemini.py +0 -0
  84. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/kimi.py +0 -0
  85. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/pi.py +0 -0
  86. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/__main__.py +0 -0
  87. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/_serve_cgroup.py +0 -0
  88. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/_serve_round.py +0 -0
  89. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/common.py +0 -0
  90. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/events_cmd.py +0 -0
  91. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/init_cmd.py +0 -0
  92. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/install_cmd.py +0 -0
  93. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/migrate_cmd.py +0 -0
  94. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/monitor_cmd.py +0 -0
  95. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/peek_cmd.py +0 -0
  96. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/round_cmd.py +0 -0
  97. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/service_cmd.py +0 -0
  98. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/cli/upgrade_cmd.py +0 -0
  99. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/clock.py +0 -0
  100. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/config/__init__.py +0 -0
  101. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/config/errors.py +0 -0
  102. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/config/loader.py +0 -0
  103. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/config/validators.py +0 -0
  104. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/context_store.py +0 -0
  105. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/hooks.py +0 -0
  106. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/host_health.py +0 -0
  107. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/http_progress.py +0 -0
  108. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/metrics.py +0 -0
  109. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/migrations.py +0 -0
  110. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/monitor.py +0 -0
  111. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/presets/__init__.py +0 -0
  112. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/presets/aider.toml +0 -0
  113. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/presets/gemini.toml +0 -0
  114. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/presets/kimi.toml +0 -0
  115. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/prompt_loader.py +0 -0
  116. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/remote_relay.py +0 -0
  117. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/round_log.py +0 -0
  118. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/round_view.py +0 -0
  119. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/scaffold.py +0 -0
  120. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/schedule.py +0 -0
  121. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/service_unit.py +0 -0
  122. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/agent_runner/vcs_state.py +0 -0
  123. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/build.sh +0 -0
  124. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/deploy/Dockerfile +0 -0
  125. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/deploy/docker-compose.yml +0 -0
  126. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/README.md +0 -0
  127. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/events.md +0 -0
  128. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/long-running-agents.md +0 -0
  129. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/migrations/0.2.md +0 -0
  130. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/plugins.md +0 -0
  131. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/quickstart.md +0 -0
  132. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/recipes/aider.md +0 -0
  133. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/recipes/deploy-all-in-one.md +0 -0
  134. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/recipes/kimi.md +0 -0
  135. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/recipes/pi.md +0 -0
  136. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/docs/thesis.md +0 -0
  137. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/pyproject.toml +0 -0
  138. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/__init__.py +0 -0
  139. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/_clock.py +0 -0
  140. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/_test_helpers.py +0 -0
  141. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/conftest.py +0 -0
  142. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/contract/__init__.py +0 -0
  143. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/e2e/__init__.py +0 -0
  144. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/e2e/conftest.py +0 -0
  145. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  146. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_install_systemd.py +0 -0
  147. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  148. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  149. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  150. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  151. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  152. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/generate_vulture_whitelist.py +0 -0
  153. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/__init__.py +0 -0
  154. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_auto_stop_e2e.py +0 -0
  155. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_bounded_run.py +0 -0
  156. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_context_enricher_namespacing.py +0 -0
  157. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_dirty_handler_seam.py +0 -0
  158. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_fresh_eyes_signal.py +0 -0
  159. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_grace_kill_emission.py +0 -0
  160. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_install_dry_run.py +0 -0
  161. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_monitor_seeded.py +0 -0
  162. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_plugin_detector_loaded.py +0 -0
  163. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_plugin_owned_paths.py +0 -0
  164. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_plugin_real_flow.py +0 -0
  165. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  166. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  167. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_scaffold_presets.py +0 -0
  168. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  169. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_spawn_round_wedged.py +0 -0
  170. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_substrate_fingerprint.py +0 -0
  171. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/integration/test_transient_error_backoff.py +0 -0
  172. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/__init__.py +0 -0
  173. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/_docs.py +0 -0
  174. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/_event_scan.py +0 -0
  175. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_atomic_write_enforced.py +0 -0
  176. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_authored_facts_declared.py +0 -0
  177. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  178. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_bdd_test_conventions.py +0 -0
  179. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_catalogs.py +0 -0
  180. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_classification_ssot.py +0 -0
  181. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_config_error_consistency.py +0 -0
  182. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_config_value_set_ssot.py +0 -0
  183. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_container_stop_bound.py +0 -0
  184. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  185. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  186. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  187. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_cli_claims.py +0 -0
  188. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_operator_surface.py +0 -0
  189. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  190. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  191. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  192. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_docs_generated.py +0 -0
  193. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_docs_index_complete.py +0 -0
  194. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_entry_points_resolve.py +0 -0
  195. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_event_int_coercion.py +0 -0
  196. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_event_kind_registry.py +0 -0
  197. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_event_kinds_ssot.py +0 -0
  198. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_events_doc_contract.py +0 -0
  199. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  200. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_hook_contract_docs.py +0 -0
  201. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_layer_2_loop_size.py +0 -0
  202. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  203. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_module_boundaries.py +0 -0
  204. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_module_sizes.py +0 -0
  205. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  206. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_no_ai_signatures.py +0 -0
  207. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_no_internal_refs.py +0 -0
  208. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_no_price_logic.py +0 -0
  209. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  210. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_no_raw_time.py +0 -0
  211. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  212. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_no_version_refs.py +0 -0
  213. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_peek_schema_version.py +0 -0
  214. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_phase_select_stateless.py +0 -0
  215. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_plugins_config_stable.py +0 -0
  216. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  217. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  218. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_round_alloc_growth.py +0 -0
  219. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  220. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_round_num_single_source.py +0 -0
  221. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_round_result_stable.py +0 -0
  222. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  223. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  224. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  225. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_upstream_schema_canary.py +0 -0
  226. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  227. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/literate/__init__.py +0 -0
  228. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/literate/parser.py +0 -0
  229. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/literate/test_parser.py +0 -0
  230. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/literate/test_quickstart.py +0 -0
  231. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/__init__.py +0 -0
  232. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime.py +0 -0
  233. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  234. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_progress.py +0 -0
  235. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_reap.py +0 -0
  236. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  237. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_assemble_prompt.py +0 -0
  238. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_events_stream.py +0 -0
  239. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_install.py +0 -0
  240. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_observation.py +0 -0
  241. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_read_round_num.py +0 -0
  242. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_resolve_phase.py +0 -0
  243. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_api_types.py +0 -0
  244. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_auto_stop_gating.py +0 -0
  245. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_catalogs.py +0 -0
  246. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cgroup_probe.py +0 -0
  247. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_claude_error_detector.py +0 -0
  248. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_claude_foreign_schema.py +0 -0
  249. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cli.py +0 -0
  250. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cli_common.py +0 -0
  251. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cli_init_install.py +0 -0
  252. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cli_main_boundary.py +0 -0
  253. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cli_monitor_http.py +0 -0
  254. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  255. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_cli_upgrade.py +0 -0
  256. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_codewhale_code_precedence.py +0 -0
  257. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_codewhale_plugin.py +0 -0
  258. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_coerce_int_poison.py +0 -0
  259. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_fresh_eyes.py +0 -0
  260. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_max_rounds.py +0 -0
  261. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_nested_strictness.py +0 -0
  262. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_prompt_delivery.py +0 -0
  263. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_schedule.py +0 -0
  264. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_stop_file.py +0 -0
  265. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_strictness.py +0 -0
  266. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  267. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_config_transient_error_action.py +0 -0
  268. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_context_store.py +0 -0
  269. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_control_char_config.py +0 -0
  270. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_default_dirty_handler.py +0 -0
  271. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_defenses.py +0 -0
  272. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_detect_hung_latch.py +0 -0
  273. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_detector_isolation.py +0 -0
  274. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_detector_protocol.py +0 -0
  275. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_dirty_handlers.py +0 -0
  276. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_docgen.py +0 -0
  277. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_effective_throttle_view.py +0 -0
  278. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_emit_config_migrated.py +0 -0
  279. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_emit_schedule.py +0 -0
  280. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  281. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_events_cmd.py +0 -0
  282. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_events_cmd_rollover.py +0 -0
  283. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_events_cmd_tail.py +0 -0
  284. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_events_jsonl_decode.py +0 -0
  285. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_example_dirty_handler.py +0 -0
  286. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_example_plugin.py +0 -0
  287. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_exec_prefix.py +0 -0
  288. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_exit_cause.py +0 -0
  289. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_exit_classifier.py +0 -0
  290. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  291. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_gemini_plugin.py +0 -0
  292. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_git_timeout.py +0 -0
  293. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_hook_failure_isolation.py +0 -0
  294. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_hook_redact_order.py +0 -0
  295. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_hooks.py +0 -0
  296. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_host_health.py +0 -0
  297. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_http_progress.py +0 -0
  298. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_http_progress_epoch.py +0 -0
  299. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_init_entry_points.py +0 -0
  300. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  301. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_kimi_plugin.py +0 -0
  302. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_metrics.py +0 -0
  303. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  304. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_metrics_swap.py +0 -0
  305. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_migrate_cmd.py +0 -0
  306. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_migrations.py +0 -0
  307. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_cmd_severity.py +0 -0
  308. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_dedup_identity.py +0 -0
  309. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  310. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  311. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  312. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  313. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detectors.py +0 -0
  314. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_event_tail.py +0 -0
  315. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_loop.py +0 -0
  316. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_on_alert.py +0 -0
  317. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_remote.py +0 -0
  318. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  319. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_schedule_pause.py +0 -0
  320. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_network_blip_signal_guard.py +0 -0
  321. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_open_round_log.py +0 -0
  322. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  323. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_outer_round_ceiling.py +0 -0
  324. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_peek_argparse.py +0 -0
  325. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_peek_project_isolation.py +0 -0
  326. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_peek_schedule.py +0 -0
  327. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_peek_select.py +0 -0
  328. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_phase_agent_launch.py +0 -0
  329. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_phase_profiles.py +0 -0
  330. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_pi_plugin.py +0 -0
  331. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_plugin_constants.py +0 -0
  332. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_plugin_scan_parity.py +0 -0
  333. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_presets.py +0 -0
  334. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_primary_prompt_file_home.py +0 -0
  335. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_prompt_loader.py +0 -0
  336. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_read_json_hardening.py +0 -0
  337. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_redact.py +0 -0
  338. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_resolve.py +0 -0
  339. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_round_cmd.py +0 -0
  340. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_round_cmd_sigterm.py +0 -0
  341. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_round_log_helpers.py +0 -0
  342. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_round_outcome.py +0 -0
  343. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  344. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_round_view.py +0 -0
  345. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_runner.py +0 -0
  346. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  347. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_runner_throttle.py +0 -0
  348. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_scaffold.py +0 -0
  349. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_schedule.py +0 -0
  350. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_cmd_bounded.py +0 -0
  351. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_config_broken.py +0 -0
  352. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_crash_loop.py +0 -0
  353. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_mem_gate.py +0 -0
  354. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_phase_schedule.py +0 -0
  355. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_policy.py +0 -0
  356. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round.py +0 -0
  357. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  358. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_log.py +0 -0
  359. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_oom_killed.py +0 -0
  360. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_terminate.py +0 -0
  361. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_schedule_gate.py +0 -0
  362. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_sentinel.py +0 -0
  363. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_serve_startup_hooks.py +0 -0
  364. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_service_mode.py +0 -0
  365. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_service_unit.py +0 -0
  366. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_startup_battery_exit.py +0 -0
  367. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_substrate.py +0 -0
  368. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  369. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  370. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_throttle_epoch_bound.py +0 -0
  371. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_throttle_plugin_classification.py +0 -0
  372. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_upgrade_migrate.py +0 -0
  373. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.23}/tests/unit/test_vcs_state.py +0 -0
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -18,7 +18,7 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  timeout-minutes: 2
20
20
  steps:
21
- - uses: actions/checkout@v5
21
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
22
22
  with:
23
23
  # PR: scan only new commits the PR introduces (base..head).
24
24
  # Push to main: scan the just-pushed commits (before..after).
@@ -74,10 +74,10 @@ jobs:
74
74
  # granularity AND because coverage requires extra pytest flags not in
75
75
  # the local `test` task. CONTRIBUTING.md states the parity intentionally.
76
76
  steps:
77
- - uses: actions/checkout@v5
77
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
78
78
  with:
79
79
  fetch-depth: 0
80
- - uses: actions/setup-python@v6
80
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
81
81
  with:
82
82
  python-version: ${{ matrix.python }}
83
83
  cache: pip
@@ -108,7 +108,7 @@ jobs:
108
108
  run: pytest tests/literate/ -q
109
109
  - name: upload coverage
110
110
  if: matrix.python == '3.12' && matrix.os == 'ubuntu-latest'
111
- uses: codecov/codecov-action@v6
111
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
112
112
  with:
113
113
  files: ./coverage.xml
114
114
  fail_ci_if_error: false
@@ -26,10 +26,10 @@ jobs:
26
26
  runs-on: ubuntu-latest
27
27
  timeout-minutes: 10
28
28
  steps:
29
- - uses: actions/checkout@v5
29
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
30
30
  with:
31
31
  fetch-depth: 0
32
- - uses: actions/setup-python@v6
32
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
33
33
  with:
34
34
  python-version: "3.12"
35
35
  - name: install build
@@ -38,7 +38,7 @@ jobs:
38
38
  run: python -m build
39
39
  - name: list dist artifacts
40
40
  run: ls -la dist/
41
- - uses: actions/upload-artifact@v7
41
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
42
42
  with:
43
43
  name: dist
44
44
  path: dist/
@@ -53,11 +53,15 @@ jobs:
53
53
  permissions:
54
54
  id-token: write
55
55
  steps:
56
- - uses: actions/download-artifact@v8
56
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
57
57
  with:
58
58
  name: dist
59
59
  path: dist/
60
- - uses: pypa/gh-action-pypi-publish@release/v1
60
+ # SHA-pinned to release/v1's composite. NOTE: this pins the composite source;
61
+ # the image it pulls is still referenced by a moving ghcr tag, so a full pin
62
+ # would require the ghcr digest — deferred (needs read:packages + release-time
63
+ # validation). dependabot (github-actions) bumps this SHA.
64
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
61
65
  with:
62
66
  attestations: true
63
67
 
@@ -68,8 +72,8 @@ jobs:
68
72
  permissions:
69
73
  contents: write
70
74
  steps:
71
- - uses: actions/checkout@v5
72
- - uses: actions/download-artifact@v8
75
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
76
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
73
77
  with:
74
78
  name: dist
75
79
  path: dist/
@@ -110,6 +110,7 @@ pid
110
110
  uptime_s
111
111
  unit_file
112
112
  rate_limit
113
+ system_managed
113
114
 
114
115
  # agent_runner.api_types.SystemMetrics
115
116
  mem_total_mb
@@ -127,6 +128,11 @@ agent
127
128
  since_round
128
129
  phase
129
130
 
131
+ # agent_runner.cli.doctor_cmd.DoctorReport
132
+ checks
133
+ overlaps
134
+ plan
135
+
130
136
  # agent_runner.config.models.AgentConfig
131
137
  command
132
138
  prompt_arg_template
@@ -134,6 +140,7 @@ name
134
140
  env
135
141
  prompt_delivery
136
142
  exec_prefix
143
+ terminal_marker
137
144
 
138
145
  # agent_runner.config.models.Config
139
146
  agent
@@ -295,6 +302,12 @@ resume_timezone
295
302
  skipped
296
303
  active_window
297
304
 
305
+ # agent_runner.phase_select.WindowOverlap
306
+ phase_a
307
+ phase_b
308
+ window_a
309
+ window_b
310
+
298
311
  # agent_runner.round_log.PruneOutcome
299
312
  deleted
300
313
  deferred
@@ -318,6 +331,11 @@ reason
318
331
  how_to_fix
319
332
  permanent
320
333
 
334
+ # agent_runner.startup_check.CheckSpec
335
+ kind
336
+ scope
337
+ run
338
+
321
339
  # agent_runner.vcs_state.StashRef
322
340
  sha
323
341
  message
@@ -53,18 +53,6 @@ kind name is the version discriminator; a breaking change ships as
53
53
  `round_start_v2`). Reaching this failure requires a contract violation — but the
54
54
  suite would not report it.
55
55
 
56
- ## `remote_failure_tolerance_s` does not cover `subprocess.TimeoutExpired` — NEEDS_DESIGN
57
-
58
- The tolerance window covers ssh exit code 255 only. `monitor.run_remote_command`
59
- raises `MonitorRemoteError` on rc=255, and `api.monitor_loop`'s retry loop
60
- catches exactly that. A hung (not failed) ssh connection instead trips the
61
- `subprocess.run(timeout=...)` guard and raises `subprocess.TimeoutExpired`,
62
- which bypasses the window and kills the monitor.
63
-
64
- The rc=255-only scope is intentional and pinned by the monitor-signals design,
65
- and the documentation matches the code exactly — there is no drift here. Widening
66
- it changes a documented recovery path, so it is a design decision, not debt.
67
-
68
56
  ## `anomaly_repetitive_*` default — NEEDS_DESIGN
69
57
 
70
58
  `monitor.anomaly_repetitive_window` and `anomaly_repetitive_threshold` both
@@ -89,21 +77,6 @@ Deferred because closing it is a naming decision, not cleanup: no event kind
89
77
  carries the meaning "stashed but ref lost" (`orphan_stash_failed` would be wrong
90
78
  — the stash exists). Left as-is until that kind is designed.
91
79
 
92
- ## `_monitor_loop_iter`'s three fail-open guards are hand-duplicated
93
-
94
- `api._monitor_loop_iter` wraps the startup `monitor_started` emit, the
95
- per-tick poll, and `on_alert`'s per-alert dispatch each in its own
96
- `try/except Exception: warnings.warn(...)` block so a crash in any one of the
97
- three never ends monitor supervision. The three guards are structurally
98
- identical (catch, format a `type(e).__name__: {e}` warning, keep the loop
99
- alive) but hand-copied rather than sharing one implementation; `on_alert`'s
100
- own `emit()` closure already shows the pattern for factoring a shared guard.
101
-
102
- Collapsing them to one shared guard is behavior-preserving — it must not
103
- change which failures warn vs which propagate, nor the specific fallback
104
- each site takes on failure (sleep-and-retry after a poll failure,
105
- `verdict = "failed"` after an `on_alert` failure). Slated for 0.2.19.
106
-
107
80
  ## `oom_kill_delta` is named differently on its two events — NEEDS_DESIGN, DEFERRED to 0.3
108
81
 
109
82
  The same cgroup `memory.events.oom_kill` delta is emitted under two
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.23] - 2026-09-12
9
+
10
+ ### Added
11
+ - `[agent] terminal_marker` — per-preset grace-kill result marker (default = Claude's token; pi/codewhale set their own; kimi/aider have no marker and run to the wall-clock ceiling).
12
+ - `doctor` — read-only pre-flight (never launches the agent): startup checks, phase-window overlap warnings, and an N-round phase-plan preview.
13
+ - `phase_window_overlap` event — emitted at serve boot when two agent-overriding phases' windows collide in the same timezone.
14
+
15
+ ### Changed
16
+ - Startup-check dispatch and the catalog's kind list now derive from a single descriptor table (no behavior change).
17
+ - The lock-holder pid create_time verify now shares one `lifecycle` helper with `serve.pid` (no behavior change).
18
+ - Default `claude` preset model updated `claude-opus-4-7` → `claude-opus-5`.
19
+
20
+ ### Fixed
21
+ - `peek` no longer prints spurious "cannot coerce None" warnings on a freshly-initialized project; absent metric samples fall back to defaults silently (a genuinely malformed value still warns).
22
+
23
+ ## [0.2.22] - 2026-09-11
24
+
25
+ ### Fixed
26
+ - Container safety: agent-runner now refuses to boot when `runtime.log_dir` resolves inside a containerized `work_dir` — a contained agent could otherwise rewrite the pid/lock/cidfile control files the supervisor trusts. The best-effort container stop also validates the recovered container id (hex, length-bounded) and passes an end-of-options separator, and the injected `--cidfile` moves to a host-private directory, so a poisoned cidfile can no longer stop an unrelated container.
27
+ - `kill` / `restart --force` verify the lock-holder's start time before signaling, so a recycled pid is never sent a signal.
28
+ - System mode (`install --system`): `restart` / `uninstall` recognize a system unit and refuse with the exact `sudo systemctl …` command instead of a misleading one, and `uninstall` no longer aborts with an uncaught error on a host without a systemd user bus.
29
+
30
+ ### Security
31
+ - Release and CI GitHub Actions are pinned to commit SHAs, with Dependabot tracking updates.
32
+
33
+ ### Notes
34
+ - Not breaking: no `agent-runner migrate` step, no systemd unit change. `status --json` gains a `system_managed` field; existing fields unchanged. The one newly-rejected configuration is a containerized agent whose `runtime.log_dir` sits inside `work_dir` — move `log_dir` outside `work_dir` (the default already is).
35
+
8
36
  ## [0.2.21] - 2026-09-11
9
37
 
10
38
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.21
3
+ Version: 0.2.23
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
@@ -82,7 +82,7 @@ agent-runner monitor # live anomaly detection
82
82
 
83
83
  Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
84
84
 
85
- ## 16 verbs
85
+ ## 17 verbs
86
86
 
87
87
  | Lifecycle | Observation |
88
88
  |---|---|
@@ -44,7 +44,7 @@ agent-runner monitor # live anomaly detection
44
44
 
45
45
  Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
46
46
 
47
- ## 16 verbs
47
+ ## 17 verbs
48
48
 
49
49
  | Lifecycle | Observation |
50
50
  |---|---|
@@ -57,6 +57,23 @@ a narrower safety net than the host-level reaping a bare agent process gets,
57
57
  not a wider one — keep running it as a dedicated unprivileged user with no
58
58
  passwordless sudo and tight egress limits regardless.
59
59
 
60
+ **Residual: `agent-runner.toml` lives inside the mount.** The startup check
61
+ above stops the agent's pid and lock files from sitting inside a mounted
62
+ `work_dir` (the injected cidfile is separately placed in a host-private temp
63
+ dir outside any mount, so it's protected independently of `log_dir`) — but
64
+ `agent-runner.toml` itself is scaffolded at the `work_dir` root, and the
65
+ container recipes in
66
+ [docs/recipes/container-pi.md](docs/recipes/container-pi.md) bind-mount all
67
+ of `work_dir` read-write. Each round re-reads the config, so a containerized
68
+ agent that can write its own mount can rewrite `[agent] command` /
69
+ `exec_prefix` — or `runtime.stop_file`, if it's pointed inside the mount —
70
+ and have it run on the host next round. This is consistent with the threat
71
+ model above: agent-runner bounds a *misbehaving* agent, not a *hostile* one,
72
+ and containment is the container/VM's job, not agent-runner's. An operator
73
+ who needs to contain a not-fully-trusted agent should keep the config (and
74
+ `runtime.stop_file`) outside any bind-mount, or otherwise treat the
75
+ container boundary as their own responsibility to shape.
76
+
60
77
  ## Scope
61
78
 
62
79
  In scope: the `agent_runner` Python package, its CLI, its bundled systemd
@@ -57,6 +57,7 @@ from agent_runner._emit.serve import ( # noqa: F401 — public re-export
57
57
  emit_max_rounds_reached,
58
58
  emit_mem_loop,
59
59
  emit_mem_loop_persistent,
60
+ emit_phase_window_overlap,
60
61
  emit_rate_limit_stop,
61
62
  emit_schedule_paused,
62
63
  emit_schedule_phase_skipped,
@@ -78,6 +79,7 @@ __all__ = [
78
79
  "emit_mem_loop",
79
80
  "emit_mem_loop_persistent",
80
81
  "emit_mem_pressure_deferred_to_cgroup",
82
+ "emit_phase_window_overlap",
81
83
  "emit_rate_limit_stop",
82
84
  "emit_round_cgroup_memory",
83
85
  "emit_round_container_orphan_risk",
@@ -191,3 +191,23 @@ def emit_schedule_resumed(log_dir: Path, *, paused_for_s: int) -> None:
191
191
  from agent_runner.events import SCHEDULE_RESUMED, emit
192
192
 
193
193
  emit(log_dir, SCHEDULE_RESUMED, paused_for_s=paused_for_s)
194
+
195
+
196
+ def emit_phase_window_overlap(
197
+ log_dir: Path, *, phase_a: str, phase_b: str, window_a: str, window_b: str
198
+ ) -> None:
199
+ """Emit phase_window_overlap: two agent-overriding phases' own run_windows
200
+ collide (a rotation footgun; a later release promotes this exact detection
201
+ to a hard error). Called once at serve boot -- the collision is a property of the
202
+ static config, not of any one round, so (like emit_max_rounds_reached's
203
+ cgroup-probe sibling) this must not be called from the per-round path."""
204
+ from agent_runner.events import PHASE_WINDOW_OVERLAP, emit
205
+
206
+ emit(
207
+ log_dir,
208
+ PHASE_WINDOW_OVERLAP,
209
+ phase_a=phase_a,
210
+ phase_b=phase_b,
211
+ window_a=window_a,
212
+ window_b=window_b,
213
+ )
@@ -154,21 +154,41 @@ def _latest_metric_dict(metrics: list[dict[str, Any]]) -> dict[str, Any]:
154
154
  return metrics[-1] if metrics else {}
155
155
 
156
156
 
157
+ def _metric_int(latest: dict[str, Any], key: str, default: int) -> int:
158
+ """``_coerce_int`` for a metric field, silent on absence.
159
+
160
+ A fresh project has no metric events yet, so ``latest`` is ``{}`` and
161
+ every field reads as None -- "no sample yet", not malformed data. Only a
162
+ present-but-non-numeric value should still warn (delegated to
163
+ ``_coerce_int``, which owns that contract)."""
164
+ from agent_runner._throttle import _coerce_int
165
+
166
+ value = latest.get(key)
167
+ return default if value is None else _coerce_int(value, default)
168
+
169
+
170
+ def _metric_float(latest: dict[str, Any], key: str, default: float) -> float:
171
+ """Float sibling of ``_metric_int`` -- see its docstring."""
172
+ from agent_runner._throttle import _coerce_float
173
+
174
+ value = latest.get(key)
175
+ return default if value is None else _coerce_float(value, default)
176
+
177
+
157
178
  def assemble_project_state(source: LocalSource, *, project: str) -> ProjectState:
158
179
  metrics = parse_events_from_jsonl_files(source.metrics_files())
159
180
  status = read_json(source.status_path()) or {}
160
181
  orphan = read_json(source.orphan_path())
161
182
  latest = _latest_metric_dict(metrics)
162
- from agent_runner._throttle import _coerce_float, _coerce_int
163
183
 
164
184
  system = SystemMetrics(
165
- mem_total_mb=_coerce_int(latest.get("mem_total_mb"), 0),
166
- mem_available_mb=_coerce_int(latest.get("mem_available_mb"), 0),
167
- disk_used_pct=_coerce_float(latest.get("disk_used_pct"), 0.0),
168
- disk_free_gb=_coerce_float(latest.get("disk_free_gb"), 0.0),
185
+ mem_total_mb=_metric_int(latest, "mem_total_mb", 0),
186
+ mem_available_mb=_metric_int(latest, "mem_available_mb", 0),
187
+ disk_used_pct=_metric_float(latest, "disk_used_pct", 0.0),
188
+ disk_free_gb=_metric_float(latest, "disk_free_gb", 0.0),
169
189
  load_1m=latest.get("load_1m"),
170
190
  cpu_pct=latest.get("cpu_pct"),
171
- agent_process_count=_coerce_int(latest.get("agent_process_count"), 0),
191
+ agent_process_count=_metric_int(latest, "agent_process_count", 0),
172
192
  )
173
193
  return ProjectState(
174
194
  project=project,
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.21'
22
- __version_tuple__ = version_tuple = (0, 2, 21)
21
+ __version__ = version = '0.2.23'
22
+ __version_tuple__ = version_tuple = (0, 2, 23)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -14,6 +14,7 @@ import re
14
14
  import shutil
15
15
  import signal
16
16
  import subprocess # noqa: TID251 — sanctioned subprocess caller
17
+ import tempfile
17
18
  import threading
18
19
  from collections.abc import Callable
19
20
  from dataclasses import dataclass
@@ -246,11 +247,6 @@ def _live_children(
246
247
  return live, ignored
247
248
 
248
249
 
249
- # Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
250
- # A future CLI variant emitting `{"type": "result", ...}` (with space) would
251
- # bypass this scan; revisit if that happens.
252
- _RESULT_MARKER = b'"type":"result"'
253
-
254
250
  # Decoupled from the 0.2s poll tick: the marker scan only needs to notice the
255
251
  # result within max_grace_after_result_s (seconds, integer), so scanning more
256
252
  # often than this buys nothing but re-read cost on a growing round log.
@@ -290,6 +286,14 @@ def resolve_exec_target(command0: str, work_dir: Path, env_path: str | None = No
290
286
  # ladder) is out of scope here -- left for a future release.
291
287
  _CONTAINER_RUNTIMES = frozenset({"docker", "podman"})
292
288
 
289
+ # A real docker/podman container id is hex, 12-64 chars (short or full form).
290
+ # The cidfile is a trust-bearing control input -- validated before it's ever
291
+ # handed to a `stop` subprocess argv.
292
+ _CONTAINER_ID_RE = re.compile(r"^[0-9a-fA-F]{12,64}$")
293
+ # A real container id is <=64 hex chars; cap the read so a poisoned giant
294
+ # file can't OOM a constrained host.
295
+ _CIDFILE_READ_CAP = 128
296
+
293
297
  # docker/podman GLOBAL flags (before the subcommand) that take a separate
294
298
  # value token -- skipped in pairs so `docker -H unix:///var/run/docker.sock
295
299
  # run ...` / `docker --context foo run ...` still resolve to `run`. Not
@@ -507,14 +511,18 @@ def _best_effort_container_stop(
507
511
  if cidfile is None:
508
512
  return None, None
509
513
  try:
510
- cid = cidfile.read_text(encoding="utf-8").strip()
514
+ with cidfile.open("rb") as fh:
515
+ raw = fh.read(_CIDFILE_READ_CAP + 1)
511
516
  except OSError:
512
517
  return None, None
513
- if not cid:
514
- return None, None
518
+ if len(raw) > _CIDFILE_READ_CAP:
519
+ return None, None # oversize — not a real container id
520
+ cid = raw.decode("utf-8", "replace").strip()
521
+ if not _CONTAINER_ID_RE.match(cid):
522
+ return None, None # not a hex id — detection already fired the loud warn + event
515
523
  try:
516
524
  result = subprocess.run(
517
- [runtime, "stop", "-t", str(REAP_GRACE_S), cid],
525
+ [runtime, "stop", "-t", str(REAP_GRACE_S), "--", cid],
518
526
  stdout=subprocess.DEVNULL,
519
527
  stderr=subprocess.PIPE,
520
528
  timeout=_CONTAINER_STOP_TIMEOUT_S,
@@ -560,6 +568,7 @@ def run(
560
568
  log_path: Path,
561
569
  env_extra: dict[str, str],
562
570
  max_grace_after_result_s: int = 0,
571
+ terminal_marker: str = '"type":"result"', # mirrors config.DEFAULT_TERMINAL_MARKER
563
572
  progress_callback: Callable[[dict], None] | None = None,
564
573
  progress_interval_s: int = 0,
565
574
  on_grace_extended: Callable[[list[dict], list[dict]], None] | None = None,
@@ -575,13 +584,19 @@ def run(
575
584
  already-absolute cfg.runtime.work_dir. CLIs with no --cwd flag of their
576
585
  own (e.g. pi) depend on this.
577
586
 
578
- max_grace_after_result_s: when > 0, start a countdown after the first
579
- type=result event is detected in the log. After it elapses, reap the
587
+ max_grace_after_result_s: when > 0, start a countdown after
588
+ ``terminal_marker`` is first detected in the log. After it elapses, reap the
580
589
  process group only if the agent has no live worker processes left (a
581
590
  genuine hang). If a worker is still running (e.g. a backgrounded build),
582
591
  do not reap — invoke ``on_grace_extended`` once and keep waiting until the
583
592
  round finishes or hits the wall-clock ``timeout_s`` ceiling. 0 = disabled.
584
593
 
594
+ terminal_marker: exact compact byte substring the grace-kill scan looks
595
+ for (default matches claude CLI's no-whitespace JSONL ``{"type":"result",
596
+ ...}`` output). An empty string disables the marker scan outright, even
597
+ with max_grace_after_result_s > 0 (honest opt-out, not an
598
+ empty-substring-matches-everything trap).
599
+
585
600
  progress_callback: when not None and progress_interval_s > 0, called every
586
601
  progress_interval_s seconds with a dict of log stats (log_size_kb,
587
602
  last_write_age_s, wall_age_s). Keeps agent_runtime event-free; callers
@@ -606,6 +621,12 @@ def run(
606
621
  attempt where covered; it just isn't reported).
607
622
  """
608
623
  stdin_mode = prompt_delivery == "stdin"
624
+ marker_bytes = terminal_marker.encode("utf-8")
625
+ # Raising work (mkdir can fail on disk-full/perms/removed log_dir) must
626
+ # happen BEFORE the container-detection block below creates the
627
+ # host-private cidfile tempdir -- otherwise a raise here would skip the
628
+ # try/finally that cleans that tempdir up, leaking it once per round.
629
+ log_path.parent.mkdir(parents=True, exist_ok=True)
609
630
  # Container-orphan defense: detect BEFORE building argv (spawn_command may
610
631
  # gain an injected --cidfile) -- see _detect_container_run's module-level
611
632
  # note. A non-container command's argv is byte-identical to before:
@@ -638,8 +659,13 @@ def run(
638
659
  # no-silent-orphan floor.
639
660
  pass
640
661
  else:
641
- container_cidfile = log_path.with_name(log_path.name + ".cid")
642
- container_cidfile.unlink(missing_ok=True) # docker/podman refuse an existing one
662
+ # A supervisor control file must not live where the container can
663
+ # write it; put our injected cidfile in a host-private 0700 dir
664
+ # outside any mount. Docker writes the file host-side; the
665
+ # container never needs it.
666
+ container_cid_dir = Path(tempfile.mkdtemp(prefix="agent-runner-cid-"))
667
+ # Not created here — docker/podman refuse an existing cidfile.
668
+ container_cidfile = container_cid_dir / "round.cid"
643
669
  container_cidfile_is_ours = True
644
670
  spawn_command = _inject_cidfile(command, run_idx, container_cidfile)
645
671
 
@@ -669,7 +695,6 @@ def run(
669
695
  # PWD pinned last — it mirrors cwd= (a correctness pin, not a knob), so
670
696
  # an [agent.env] PWD cannot silently diverge from where the child runs.
671
697
  env = {**os.environ, **env_extra, "PWD": str(work_dir)}
672
- log_path.parent.mkdir(parents=True, exist_ok=True)
673
698
  proc: subprocess.Popen | None = None
674
699
  log_file = None
675
700
  try:
@@ -727,7 +752,7 @@ def run(
727
752
  exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
728
753
  )
729
754
  # Grace kill: result emitted but subprocess still running.
730
- if max_grace_after_result_s > 0:
755
+ if max_grace_after_result_s > 0 and marker_bytes:
731
756
  if result_seen_at is None and now - last_result_scan >= _RESULT_SCAN_INTERVAL_S:
732
757
  last_result_scan = now
733
758
  try:
@@ -736,12 +761,16 @@ def run(
736
761
  chunk = f.read()
737
762
  result_scan_offset += len(chunk)
738
763
  haystack = result_scan_carry + chunk
739
- if _RESULT_MARKER in haystack:
764
+ if marker_bytes in haystack:
740
765
  result_seen_at = now
741
766
  else:
742
767
  # Keep the tail so a marker split across two read
743
768
  # chunks (this scan vs. the next) still re-forms.
744
- result_scan_carry = haystack[-(len(_RESULT_MARKER) - 1) :]
769
+ # A 1-byte marker can't be split, so carry nothing
770
+ # -- haystack[-0:] is the WHOLE buffer, not empty,
771
+ # so the zero case needs its own branch.
772
+ _carry = len(marker_bytes) - 1
773
+ result_scan_carry = haystack[-_carry:] if _carry else b""
745
774
  except OSError:
746
775
  pass # log not flushed yet; retry next interval
747
776
  if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
@@ -805,10 +834,10 @@ def run(
805
834
  finally:
806
835
  if log_file is not None:
807
836
  log_file.close()
808
- # Cleanup, not correctness: our own injected cidfile is a tiny,
809
- # round-scoped tmp file -- delete it once the round is fully done
810
- # (read-back for the best-effort stop, if any, already happened
811
- # above). Only ours: an operator-supplied --cidfile is never ours to
812
- # delete.
837
+ # Cleanup, not correctness: our own injected cidfile lives in a tiny,
838
+ # round-scoped private tempdir -- remove that whole dir once the round
839
+ # is fully done (read-back for the best-effort stop, if any, already
840
+ # happened above). Only ours: an operator-supplied --cidfile is never
841
+ # ours to delete.
813
842
  if container_cidfile_is_ours and container_cidfile is not None:
814
- container_cidfile.unlink(missing_ok=True)
843
+ shutil.rmtree(container_cidfile.parent, ignore_errors=True)
@@ -193,6 +193,11 @@ def init(
193
193
  _SYSTEM_UNITS_DIR = Path("/etc/systemd/system")
194
194
 
195
195
 
196
+ def _system_unit_exists(project: str) -> bool:
197
+ """A --system install's root-owned unit; detect_service_mode can't see it (user-scope only)."""
198
+ return (_SYSTEM_UNITS_DIR / serve_unit_filename(project)).exists()
199
+
200
+
196
201
  def _install_system(
197
202
  cfg: Config, project: str, *, config_path: Path, with_monitor: bool
198
203
  ) -> InstallResult:
@@ -283,6 +288,15 @@ def uninstall(work_dir: Path | None = None) -> bool:
283
288
  if work_dir is None:
284
289
  work_dir = Path.cwd()
285
290
  project = _project_name(work_dir)
291
+ if _system_unit_exists(project): # root-owned -- don't silently no-op the user-scope dir
292
+ units = [serve_unit_filename(project)]
293
+ if (_SYSTEM_UNITS_DIR / monitor_unit_filename(project)).exists():
294
+ units.append(monitor_unit_filename(project))
295
+ remedy = " && ".join(
296
+ f"sudo systemctl disable --now {u} && sudo rm {_SYSTEM_UNITS_DIR / u}" for u in units
297
+ )
298
+ print(f"{project} is managed by a system unit; run: {remedy}")
299
+ return False
286
300
  units_dir = lifecycle._user_systemd_dir()
287
301
  serve = units_dir / serve_unit_filename(project)
288
302
  monitor = units_dir / monitor_unit_filename(project)
@@ -295,7 +309,10 @@ def uninstall(work_dir: Path | None = None) -> bool:
295
309
  lifecycle.stop_unit_draining(p.name, clock=SYSTEM_CLOCK, confirm_s=_PID_SIGNAL_GRACE_S)
296
310
  _systemctl_user("disable", p.name)
297
311
  p.unlink(missing_ok=True)
298
- _systemctl_user("daemon-reload")
312
+ try:
313
+ _systemctl_user("daemon-reload")
314
+ except (subprocess.CalledProcessError, FileNotFoundError):
315
+ pass # no user bus on this host -- units above (if any) already gone
299
316
  return True
300
317
 
301
318
 
@@ -348,7 +365,10 @@ def _round_holder_pid(log_dir: Path) -> int | None:
348
365
  pid = data.get("pid")
349
366
  if not lifecycle._valid_pid(pid):
350
367
  return None
351
- return pid if pid_alive(pid) else None
368
+ if not pid_alive(pid):
369
+ return None
370
+ recorded = data.get("create_time")
371
+ return pid if lifecycle.create_time_matches(pid, recorded) else None
352
372
 
353
373
 
354
374
  def _terminate_round_pid(pid: int) -> None:
@@ -435,6 +455,11 @@ def restart(project: str | Path, *, force: bool = False) -> ServiceStatus:
435
455
  # SYSTEMD_USER unit, so restarting a PID_FILE/NONE service would stop it and
436
456
  # never bring it back — the half-execution this fix eliminates.
437
457
  pname = _resolve_project(project)
458
+ if _system_unit_exists(pname): # root-owned -- below's message would mislead
459
+ raise RuntimeError(
460
+ f"{pname} is managed by a system unit; run: "
461
+ f"sudo systemctl restart {serve_unit_filename(pname)}"
462
+ )
438
463
  log_dir = _log_dir_for_project(project)
439
464
  mode = detect_service_mode(pname, log_dir=log_dir)
440
465
  if mode != ServiceMode.SYSTEMD_USER:
@@ -463,14 +488,18 @@ def status(project: str | Path) -> ServiceStatus:
463
488
  pname = _resolve_project(project)
464
489
  log_dir = _log_dir_for_project(project)
465
490
  mode = detect_service_mode(pname, log_dir=log_dir)
466
- if mode == ServiceMode.PID_FILE:
467
- pid = PIDFile(log_dir / "serve.pid").read()
468
- return ServiceStatus(mode=mode, active=pid is not None and pid_alive(pid), pid=pid)
469
491
  if mode == ServiceMode.SYSTEMD_USER:
470
492
  unit = lifecycle._user_systemd_dir() / serve_unit_filename(pname)
471
493
  active = _systemd_active(serve_unit_filename(pname), log_dir)
472
494
  return ServiceStatus(mode=mode, active=active, unit_file=unit)
473
- return ServiceStatus(mode=ServiceMode.NONE, active=False)
495
+ # detect_service_mode only sees the user-scope unit dir; label a --system
496
+ # install found here (PID_FILE/NONE) via system_managed, not unit_file.
497
+ system_managed = _system_unit_exists(pname)
498
+ if mode == ServiceMode.PID_FILE:
499
+ pid = PIDFile(log_dir / "serve.pid").read()
500
+ active = pid is not None and pid_alive(pid)
501
+ return ServiceStatus(mode=mode, active=active, pid=pid, system_managed=system_managed)
502
+ return ServiceStatus(mode=ServiceMode.NONE, active=False, system_managed=system_managed)
474
503
 
475
504
 
476
505
  def _resolve_target(project: str | Path | None) -> tuple[str, Path]:
@@ -816,6 +845,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
816
845
  emit_mem_loop,
817
846
  emit_mem_loop_persistent,
818
847
  emit_mem_pressure_deferred_to_cgroup,
848
+ emit_phase_window_overlap,
819
849
  emit_rate_limit_stop,
820
850
  emit_round_cgroup_memory,
821
851
  emit_round_container_orphan_risk,
@@ -62,6 +62,11 @@ class ServiceStatus:
62
62
  uptime_s: float | None = None
63
63
  unit_file: Path | None = None
64
64
  rate_limit: RateLimitState | None = None
65
+ # True when a `--system` install's root-owned unit owns this project but
66
+ # detect_service_mode fell through to PID_FILE/NONE (it only sees the
67
+ # user-scope unit dir) -- distinct from `unit_file`, which stays the
68
+ # live, user-owned, CLI-restartable systemd_user unit's path.
69
+ system_managed: bool = False
65
70
 
66
71
 
67
72
  @dataclass(frozen=True)
@@ -17,6 +17,7 @@ from pathlib import Path
17
17
 
18
18
  from agent_runner import __version__
19
19
  from agent_runner.cli import (
20
+ doctor_cmd,
20
21
  events_cmd,
21
22
  init_cmd,
22
23
  install_cmd,
@@ -71,6 +72,7 @@ def _build_parser() -> argparse.ArgumentParser:
71
72
  round_cmd.add_parser(sub, parent)
72
73
  upgrade_cmd.add_parser(sub, parent)
73
74
  migrate_cmd.add_parser(sub, parent)
75
+ doctor_cmd.add_parser(sub, parent)
74
76
 
75
77
  return parser
76
78