cli-agent-runner 0.2.22__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.22 → cli_agent_runner-0.2.23}/.vulture-whitelist.py +17 -0
  2. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/CHANGELOG.md +15 -0
  3. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/PKG-INFO +2 -2
  4. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/README.md +1 -1
  5. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_emit/__init__.py +2 -0
  6. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_emit/serve.py +20 -0
  7. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_monitor_state.py +26 -6
  8. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_version.py +2 -2
  9. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/agent_runtime.py +17 -10
  10. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/api.py +2 -10
  11. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/__init__.py +2 -0
  12. cli_agent_runner-0.2.23/agent_runner/cli/doctor_cmd.py +77 -0
  13. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/serve_cmd.py +11 -0
  14. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/config/models.py +11 -0
  15. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/config/parsers.py +3 -0
  16. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/events.py +1 -0
  17. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/lifecycle.py +27 -12
  18. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/phase_select.py +63 -0
  19. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/presets/claude.toml +1 -1
  20. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/presets/codewhale.toml +3 -0
  21. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/presets/pi.toml +3 -0
  22. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/runner.py +5 -6
  23. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/startup_check.py +93 -83
  24. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/architecture.md +1 -0
  25. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/commands.md +13 -2
  26. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/configuration.md +16 -0
  27. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/memory-efficiency.md +48 -0
  28. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/recipes/codewhale.md +3 -0
  29. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/runbook.md +1 -1
  30. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/contract/test_public_api_surface.py +1 -0
  31. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_serve_loop.py +63 -2
  32. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_architecture.py +11 -2
  33. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_import_footprint.py +5 -0
  34. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_grace.py +102 -0
  35. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config.py +28 -0
  36. cli_agent_runner-0.2.23/tests/unit/test_doctor_cmd.py +86 -0
  37. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_events.py +4 -0
  38. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_lifecycle.py +31 -0
  39. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_assembly.py +29 -0
  40. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_phase_select.py +115 -0
  41. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_startup_check.py +63 -0
  42. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.codecov.yml +0 -0
  43. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  44. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  45. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  46. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  47. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.github/dependabot.yml +0 -0
  48. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.github/workflows/ci.yml +0 -0
  49. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.github/workflows/release.yml +0 -0
  50. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/.gitignore +0 -0
  51. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/BACKLOG.md +0 -0
  52. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/CODE_OF_CONDUCT.md +0 -0
  53. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/CONTRIBUTING.md +0 -0
  54. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/LICENSE +0 -0
  55. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/README.zh.md +0 -0
  56. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/SECURITY.md +0 -0
  57. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/__init__.py +0 -0
  58. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_docgen.py +0 -0
  59. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_emit/agent.py +0 -0
  60. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_emit/memory.py +0 -0
  61. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_emit/rounds.py +0 -0
  62. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_monitor_detectors.py +0 -0
  63. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_monitor_registry.py +0 -0
  64. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_plugin_scan.py +0 -0
  65. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_redact.py +0 -0
  66. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_registry.py +0 -0
  67. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_resolve.py +0 -0
  68. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_round_outcome.py +0 -0
  69. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_round_support.py +0 -0
  70. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_serve_policy.py +0 -0
  71. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_substrate.py +0 -0
  72. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/_throttle.py +0 -0
  73. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/api_types.py +0 -0
  74. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/__init__.py +0 -0
  75. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/_constants.py +0 -0
  76. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  77. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/codewhale.py +0 -0
  78. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  79. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/gemini.py +0 -0
  80. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/kimi.py +0 -0
  81. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/builtin_plugins/pi.py +0 -0
  82. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/__main__.py +0 -0
  83. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/_serve_cgroup.py +0 -0
  84. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/_serve_round.py +0 -0
  85. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/common.py +0 -0
  86. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/events_cmd.py +0 -0
  87. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/init_cmd.py +0 -0
  88. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/install_cmd.py +0 -0
  89. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/migrate_cmd.py +0 -0
  90. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/monitor_cmd.py +0 -0
  91. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/peek_cmd.py +0 -0
  92. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/round_cmd.py +0 -0
  93. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/service_cmd.py +0 -0
  94. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/cli/upgrade_cmd.py +0 -0
  95. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/clock.py +0 -0
  96. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/config/__init__.py +0 -0
  97. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/config/errors.py +0 -0
  98. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/config/loader.py +0 -0
  99. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/config/validators.py +0 -0
  100. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/context_store.py +0 -0
  101. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/defenses.py +0 -0
  102. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/hooks.py +0 -0
  103. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/host_health.py +0 -0
  104. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/http_progress.py +0 -0
  105. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/metrics.py +0 -0
  106. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/migrations.py +0 -0
  107. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/monitor.py +0 -0
  108. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/presets/__init__.py +0 -0
  109. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/presets/aider.toml +0 -0
  110. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/presets/gemini.toml +0 -0
  111. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/presets/kimi.toml +0 -0
  112. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/prompt_loader.py +0 -0
  113. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/remote_relay.py +0 -0
  114. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/round_log.py +0 -0
  115. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/round_view.py +0 -0
  116. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/scaffold.py +0 -0
  117. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/schedule.py +0 -0
  118. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/service_unit.py +0 -0
  119. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/agent_runner/vcs_state.py +0 -0
  120. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/build.sh +0 -0
  121. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/deploy/Dockerfile +0 -0
  122. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/deploy/docker-compose.yml +0 -0
  123. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/README.md +0 -0
  124. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/events.md +0 -0
  125. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/long-running-agents.md +0 -0
  126. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/migrations/0.2.md +0 -0
  127. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/plugins.md +0 -0
  128. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/quickstart.md +0 -0
  129. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/recipes/aider.md +0 -0
  130. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/recipes/container-pi.md +0 -0
  131. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/recipes/deploy-all-in-one.md +0 -0
  132. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/recipes/kimi.md +0 -0
  133. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/recipes/pi.md +0 -0
  134. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/docs/thesis.md +0 -0
  135. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/pyproject.toml +0 -0
  136. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/__init__.py +0 -0
  137. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/_clock.py +0 -0
  138. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/_test_helpers.py +0 -0
  139. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/conftest.py +0 -0
  140. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/contract/__init__.py +0 -0
  141. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/e2e/__init__.py +0 -0
  142. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/e2e/conftest.py +0 -0
  143. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  144. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_install_systemd.py +0 -0
  145. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  146. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  147. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  148. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  149. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  150. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/generate_vulture_whitelist.py +0 -0
  151. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/__init__.py +0 -0
  152. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_auto_stop_e2e.py +0 -0
  153. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_bounded_run.py +0 -0
  154. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_context_enricher_namespacing.py +0 -0
  155. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_dirty_handler_seam.py +0 -0
  156. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_fresh_eyes_signal.py +0 -0
  157. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_grace_kill_emission.py +0 -0
  158. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_install_dry_run.py +0 -0
  159. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_monitor_seeded.py +0 -0
  160. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_plugin_detector_loaded.py +0 -0
  161. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_plugin_owned_paths.py +0 -0
  162. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_plugin_real_flow.py +0 -0
  163. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  164. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  165. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_scaffold_presets.py +0 -0
  166. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  167. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_spawn_round_wedged.py +0 -0
  168. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_substrate_fingerprint.py +0 -0
  169. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/integration/test_transient_error_backoff.py +0 -0
  170. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/__init__.py +0 -0
  171. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/_docs.py +0 -0
  172. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/_event_scan.py +0 -0
  173. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_atomic_write_enforced.py +0 -0
  174. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_authored_facts_declared.py +0 -0
  175. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  176. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_bdd_test_conventions.py +0 -0
  177. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_catalogs.py +0 -0
  178. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_classification_ssot.py +0 -0
  179. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_config_error_consistency.py +0 -0
  180. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_config_value_set_ssot.py +0 -0
  181. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_container_stop_bound.py +0 -0
  182. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  183. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  184. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  185. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_cli_claims.py +0 -0
  186. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_operator_surface.py +0 -0
  187. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  188. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  189. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  190. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_docs_generated.py +0 -0
  191. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_docs_index_complete.py +0 -0
  192. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_entry_points_resolve.py +0 -0
  193. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_event_int_coercion.py +0 -0
  194. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_event_kind_registry.py +0 -0
  195. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_event_kinds_ssot.py +0 -0
  196. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_events_doc_contract.py +0 -0
  197. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  198. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_hook_contract_docs.py +0 -0
  199. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_layer_2_loop_size.py +0 -0
  200. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  201. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_module_boundaries.py +0 -0
  202. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_module_sizes.py +0 -0
  203. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  204. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_no_ai_signatures.py +0 -0
  205. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_no_internal_refs.py +0 -0
  206. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_no_price_logic.py +0 -0
  207. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  208. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_no_raw_time.py +0 -0
  209. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  210. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_no_version_refs.py +0 -0
  211. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_peek_schema_version.py +0 -0
  212. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_phase_select_stateless.py +0 -0
  213. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_plugins_config_stable.py +0 -0
  214. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  215. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  216. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_round_alloc_growth.py +0 -0
  217. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  218. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_round_num_single_source.py +0 -0
  219. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_round_result_stable.py +0 -0
  220. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  221. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  222. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  223. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_upstream_schema_canary.py +0 -0
  224. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  225. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/literate/__init__.py +0 -0
  226. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/literate/parser.py +0 -0
  227. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/literate/test_parser.py +0 -0
  228. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/literate/test_quickstart.py +0 -0
  229. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/__init__.py +0 -0
  230. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime.py +0 -0
  231. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_container.py +0 -0
  232. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  233. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_progress.py +0 -0
  234. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_reap.py +0 -0
  235. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  236. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_assemble_prompt.py +0 -0
  237. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_events_stream.py +0 -0
  238. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_install.py +0 -0
  239. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_observation.py +0 -0
  240. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_read_round_num.py +0 -0
  241. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_resolve_phase.py +0 -0
  242. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_service.py +0 -0
  243. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_api_types.py +0 -0
  244. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_auto_stop_gating.py +0 -0
  245. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_catalogs.py +0 -0
  246. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cgroup_probe.py +0 -0
  247. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_claude_error_detector.py +0 -0
  248. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_claude_foreign_schema.py +0 -0
  249. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cli.py +0 -0
  250. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cli_common.py +0 -0
  251. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cli_init_install.py +0 -0
  252. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cli_main_boundary.py +0 -0
  253. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cli_monitor_http.py +0 -0
  254. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  255. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_cli_upgrade.py +0 -0
  256. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_codewhale_code_precedence.py +0 -0
  257. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_codewhale_plugin.py +0 -0
  258. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_coerce_int_poison.py +0 -0
  259. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_fresh_eyes.py +0 -0
  260. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_max_rounds.py +0 -0
  261. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_nested_strictness.py +0 -0
  262. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_prompt_delivery.py +0 -0
  263. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_schedule.py +0 -0
  264. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_stop_file.py +0 -0
  265. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_strictness.py +0 -0
  266. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  267. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_config_transient_error_action.py +0 -0
  268. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_context_store.py +0 -0
  269. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_control_char_config.py +0 -0
  270. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_default_dirty_handler.py +0 -0
  271. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_defenses.py +0 -0
  272. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_detect_hung_latch.py +0 -0
  273. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_detector_isolation.py +0 -0
  274. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_detector_protocol.py +0 -0
  275. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_dirty_handlers.py +0 -0
  276. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_docgen.py +0 -0
  277. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_effective_throttle_view.py +0 -0
  278. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_emit_config_migrated.py +0 -0
  279. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_emit_schedule.py +0 -0
  280. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  281. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_events_cmd.py +0 -0
  282. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_events_cmd_rollover.py +0 -0
  283. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_events_cmd_tail.py +0 -0
  284. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_events_jsonl_decode.py +0 -0
  285. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_example_dirty_handler.py +0 -0
  286. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_example_plugin.py +0 -0
  287. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_exec_prefix.py +0 -0
  288. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_exit_cause.py +0 -0
  289. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_exit_classifier.py +0 -0
  290. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  291. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_gemini_plugin.py +0 -0
  292. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_git_timeout.py +0 -0
  293. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_hook_failure_isolation.py +0 -0
  294. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_hook_redact_order.py +0 -0
  295. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_hooks.py +0 -0
  296. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_host_health.py +0 -0
  297. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_http_progress.py +0 -0
  298. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_http_progress_epoch.py +0 -0
  299. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_init_entry_points.py +0 -0
  300. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  301. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_kimi_plugin.py +0 -0
  302. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_metrics.py +0 -0
  303. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  304. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_metrics_swap.py +0 -0
  305. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_migrate_cmd.py +0 -0
  306. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_migrations.py +0 -0
  307. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_cmd_severity.py +0 -0
  308. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_dedup_identity.py +0 -0
  309. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  310. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  311. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  312. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  313. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_detectors.py +0 -0
  314. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_event_tail.py +0 -0
  315. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_loop.py +0 -0
  316. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_on_alert.py +0 -0
  317. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_remote.py +0 -0
  318. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  319. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_monitor_schedule_pause.py +0 -0
  320. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_network_blip_signal_guard.py +0 -0
  321. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_open_round_log.py +0 -0
  322. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  323. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_outer_round_ceiling.py +0 -0
  324. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_peek_argparse.py +0 -0
  325. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_peek_project_isolation.py +0 -0
  326. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_peek_schedule.py +0 -0
  327. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_peek_select.py +0 -0
  328. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_phase_agent_launch.py +0 -0
  329. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_phase_profiles.py +0 -0
  330. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_pi_plugin.py +0 -0
  331. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_plugin_constants.py +0 -0
  332. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_plugin_scan_parity.py +0 -0
  333. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_presets.py +0 -0
  334. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_primary_prompt_file_home.py +0 -0
  335. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_prompt_loader.py +0 -0
  336. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_read_json_hardening.py +0 -0
  337. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_redact.py +0 -0
  338. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_resolve.py +0 -0
  339. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_round_cmd.py +0 -0
  340. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_round_cmd_sigterm.py +0 -0
  341. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_round_log_helpers.py +0 -0
  342. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_round_outcome.py +0 -0
  343. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  344. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_round_view.py +0 -0
  345. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_runner.py +0 -0
  346. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  347. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_runner_throttle.py +0 -0
  348. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_scaffold.py +0 -0
  349. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_schedule.py +0 -0
  350. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_cmd_bounded.py +0 -0
  351. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_config_broken.py +0 -0
  352. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_crash_loop.py +0 -0
  353. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_mem_gate.py +0 -0
  354. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_phase_schedule.py +0 -0
  355. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_policy.py +0 -0
  356. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round.py +0 -0
  357. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  358. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_log.py +0 -0
  359. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_oom_killed.py +0 -0
  360. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_round_terminate.py +0 -0
  361. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_schedule_gate.py +0 -0
  362. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_sentinel.py +0 -0
  363. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_serve_startup_hooks.py +0 -0
  364. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_service_mode.py +0 -0
  365. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_service_unit.py +0 -0
  366. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_startup_battery_exit.py +0 -0
  367. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_substrate.py +0 -0
  368. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  369. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  370. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_throttle_epoch_bound.py +0 -0
  371. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_throttle_plugin_classification.py +0 -0
  372. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_upgrade_migrate.py +0 -0
  373. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.23}/tests/unit/test_vcs_state.py +0 -0
@@ -128,6 +128,11 @@ agent
128
128
  since_round
129
129
  phase
130
130
 
131
+ # agent_runner.cli.doctor_cmd.DoctorReport
132
+ checks
133
+ overlaps
134
+ plan
135
+
131
136
  # agent_runner.config.models.AgentConfig
132
137
  command
133
138
  prompt_arg_template
@@ -135,6 +140,7 @@ name
135
140
  env
136
141
  prompt_delivery
137
142
  exec_prefix
143
+ terminal_marker
138
144
 
139
145
  # agent_runner.config.models.Config
140
146
  agent
@@ -296,6 +302,12 @@ resume_timezone
296
302
  skipped
297
303
  active_window
298
304
 
305
+ # agent_runner.phase_select.WindowOverlap
306
+ phase_a
307
+ phase_b
308
+ window_a
309
+ window_b
310
+
299
311
  # agent_runner.round_log.PruneOutcome
300
312
  deleted
301
313
  deferred
@@ -319,6 +331,11 @@ reason
319
331
  how_to_fix
320
332
  permanent
321
333
 
334
+ # agent_runner.startup_check.CheckSpec
335
+ kind
336
+ scope
337
+ run
338
+
322
339
  # agent_runner.vcs_state.StashRef
323
340
  sha
324
341
  message
@@ -5,6 +5,21 @@ 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
+
8
23
  ## [0.2.22] - 2026-09-11
9
24
 
10
25
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.22
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,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.22'
22
- __version_tuple__ = version_tuple = (0, 2, 22)
21
+ __version__ = version = '0.2.23'
22
+ __version_tuple__ = version_tuple = (0, 2, 23)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -247,11 +247,6 @@ def _live_children(
247
247
  return live, ignored
248
248
 
249
249
 
250
- # Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
251
- # A future CLI variant emitting `{"type": "result", ...}` (with space) would
252
- # bypass this scan; revisit if that happens.
253
- _RESULT_MARKER = b'"type":"result"'
254
-
255
250
  # Decoupled from the 0.2s poll tick: the marker scan only needs to notice the
256
251
  # result within max_grace_after_result_s (seconds, integer), so scanning more
257
252
  # often than this buys nothing but re-read cost on a growing round log.
@@ -573,6 +568,7 @@ def run(
573
568
  log_path: Path,
574
569
  env_extra: dict[str, str],
575
570
  max_grace_after_result_s: int = 0,
571
+ terminal_marker: str = '"type":"result"', # mirrors config.DEFAULT_TERMINAL_MARKER
576
572
  progress_callback: Callable[[dict], None] | None = None,
577
573
  progress_interval_s: int = 0,
578
574
  on_grace_extended: Callable[[list[dict], list[dict]], None] | None = None,
@@ -588,13 +584,19 @@ def run(
588
584
  already-absolute cfg.runtime.work_dir. CLIs with no --cwd flag of their
589
585
  own (e.g. pi) depend on this.
590
586
 
591
- max_grace_after_result_s: when > 0, start a countdown after the first
592
- 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
593
589
  process group only if the agent has no live worker processes left (a
594
590
  genuine hang). If a worker is still running (e.g. a backgrounded build),
595
591
  do not reap — invoke ``on_grace_extended`` once and keep waiting until the
596
592
  round finishes or hits the wall-clock ``timeout_s`` ceiling. 0 = disabled.
597
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
+
598
600
  progress_callback: when not None and progress_interval_s > 0, called every
599
601
  progress_interval_s seconds with a dict of log stats (log_size_kb,
600
602
  last_write_age_s, wall_age_s). Keeps agent_runtime event-free; callers
@@ -619,6 +621,7 @@ def run(
619
621
  attempt where covered; it just isn't reported).
620
622
  """
621
623
  stdin_mode = prompt_delivery == "stdin"
624
+ marker_bytes = terminal_marker.encode("utf-8")
622
625
  # Raising work (mkdir can fail on disk-full/perms/removed log_dir) must
623
626
  # happen BEFORE the container-detection block below creates the
624
627
  # host-private cidfile tempdir -- otherwise a raise here would skip the
@@ -749,7 +752,7 @@ def run(
749
752
  exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
750
753
  )
751
754
  # Grace kill: result emitted but subprocess still running.
752
- if max_grace_after_result_s > 0:
755
+ if max_grace_after_result_s > 0 and marker_bytes:
753
756
  if result_seen_at is None and now - last_result_scan >= _RESULT_SCAN_INTERVAL_S:
754
757
  last_result_scan = now
755
758
  try:
@@ -758,12 +761,16 @@ def run(
758
761
  chunk = f.read()
759
762
  result_scan_offset += len(chunk)
760
763
  haystack = result_scan_carry + chunk
761
- if _RESULT_MARKER in haystack:
764
+ if marker_bytes in haystack:
762
765
  result_seen_at = now
763
766
  else:
764
767
  # Keep the tail so a marker split across two read
765
768
  # chunks (this scan vs. the next) still re-forms.
766
- 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""
767
774
  except OSError:
768
775
  pass # log not flushed yet; retry next interval
769
776
  if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
@@ -357,8 +357,6 @@ def _round_holder_pid(log_dir: Path) -> int | None:
357
357
  alive. This is the ONLY way ``kill()`` can reach an in-flight round: the
358
358
  round is ``start_new_session=True`` (its own session, its own pgid), so it
359
359
  sits outside whatever process group serve itself belongs to."""
360
- import psutil
361
-
362
360
  from agent_runner.context_store import read_json
363
361
 
364
362
  data = read_json(log_dir / "agent-runner.lock.holder")
@@ -370,14 +368,7 @@ def _round_holder_pid(log_dir: Path) -> int | None:
370
368
  if not pid_alive(pid):
371
369
  return None
372
370
  recorded = data.get("create_time")
373
- if recorded is None:
374
- return pid # legacy sidecar without a token: best-effort, mirrors serve.pid's read
375
- try:
376
- if abs(psutil.Process(pid).create_time() - recorded) < 1.0:
377
- return pid
378
- except psutil.Error:
379
- return None
380
- return None # recorded create_time != live process: a recycled pid, do not signal it
371
+ return pid if lifecycle.create_time_matches(pid, recorded) else None
381
372
 
382
373
 
383
374
  def _terminate_round_pid(pid: int) -> None:
@@ -854,6 +845,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
854
845
  emit_mem_loop,
855
846
  emit_mem_loop_persistent,
856
847
  emit_mem_pressure_deferred_to_cgroup,
848
+ emit_phase_window_overlap,
857
849
  emit_rate_limit_stop,
858
850
  emit_round_cgroup_memory,
859
851
  emit_round_container_orphan_risk,
@@ -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
 
@@ -0,0 +1,77 @@
1
+ """doctor subcommand — never launches the configured agent; runs the same
2
+ read-only-ish boot checks serve does (it may create/probe the log dir to
3
+ verify it's writable), then reports window overlaps + phase plan."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from agent_runner import phase_select, startup_check
10
+ from agent_runner.cli.common import cfg_from_args, emit
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class DoctorReport:
15
+ checks: list[startup_check.CheckResult]
16
+ overlaps: list[phase_select.WindowOverlap]
17
+ plan: list[dict]
18
+
19
+
20
+ def add_parser(sub, parent) -> None:
21
+ p = sub.add_parser("doctor", parents=[parent], help="Read-only pre-flight: checks + phase plan")
22
+ p.add_argument("--rounds", type=int, default=3, help="Phase-plan preview depth (default 3)")
23
+ p.set_defaults(func=cmd_doctor)
24
+
25
+
26
+ def _plan(cfg, rounds: int) -> list[dict]:
27
+ out = []
28
+ for r in range(1, rounds + 1):
29
+ sel = phase_select.select_phase(cfg, r)
30
+ out.append(
31
+ {
32
+ "round": r,
33
+ "phase": sel.phase,
34
+ "paused": sel.paused,
35
+ "resume_phase": sel.resume_phase,
36
+ "skipped": sel.skipped,
37
+ "active_window": sel.active_window,
38
+ }
39
+ )
40
+ return out
41
+
42
+
43
+ def cmd_doctor(args) -> int:
44
+ cfg = cfg_from_args(args)
45
+ report = DoctorReport(
46
+ checks=startup_check.run_battery(cfg),
47
+ overlaps=phase_select.find_phase_window_overlaps(cfg),
48
+ plan=_plan(cfg, args.rounds),
49
+ )
50
+ json_mode = getattr(args, "json", False)
51
+ if json_mode:
52
+ emit(report, json_mode=True)
53
+ else:
54
+ print(_format(report))
55
+ return 0
56
+
57
+
58
+ def _format(report: DoctorReport) -> str:
59
+ # Hand-formatted rather than common._pretty: that helper repr()s nested
60
+ # lists/dicts, which would dump report.plan's per-round dicts as Python
61
+ # reprs instead of the readable "round N: phase" lines below.
62
+ lines = ["checks:"]
63
+ for c in report.checks:
64
+ mark = "ok" if c.ok else "FAIL"
65
+ detail = ""
66
+ if not c.ok:
67
+ detail = f" — {c.reason}" + (f" (fix: {c.how_to_fix})" if c.how_to_fix else "")
68
+ lines.append(f" [{mark}] {c.name}{detail}")
69
+ if report.overlaps:
70
+ lines.append("window overlaps:")
71
+ for o in report.overlaps:
72
+ lines.append(f" {o.phase_a} ({o.window_a}) vs {o.phase_b} ({o.window_b})")
73
+ lines.append("phase plan:")
74
+ for step in report.plan:
75
+ where = "paused" if step["paused"] else (step["phase"] or "base")
76
+ lines.append(f" round {step['round']}: {where}")
77
+ return "\n".join(lines)
@@ -35,6 +35,7 @@ from agent_runner.api import (
35
35
  emit_config_broken,
36
36
  emit_fresh_eyes_round_triggered,
37
37
  emit_max_rounds_reached,
38
+ emit_phase_window_overlap,
38
39
  emit_rate_limit_stop,
39
40
  emit_round_logs_prune_deferred,
40
41
  emit_round_substrate_after,
@@ -615,6 +616,16 @@ def cmd(args) -> int:
615
616
  # Emitted exactly once per serve lifetime -- the per-round defer decision below
616
617
  # reuses this bool rather than re-probing/re-emitting every round.
617
618
  cgroup_probe_defer = _probe_and_emit_cgroup_defer(log_dir)
619
+ # Static config check -- phase-window collisions can't change mid-lifetime, so
620
+ # (like the cgroup probe above) this runs once at boot, not once per round.
621
+ for ov in phase_select.find_phase_window_overlaps(cfg):
622
+ emit_phase_window_overlap(
623
+ log_dir,
624
+ phase_a=ov.phase_a,
625
+ phase_b=ov.phase_b,
626
+ window_a=ov.window_a,
627
+ window_b=ov.window_b,
628
+ )
618
629
  rounds_completed = 0
619
630
  # Three independent consecutive-failure counters, one per breaker: b12
620
631
  # crash-loop (unknown short crashes), mem-loop (mem-terminated
@@ -22,6 +22,12 @@ _VALID_DIRTY_ACTIONS: frozenset[str] = frozenset({"stash", "ignore", "auto_commi
22
22
  _VALID_TRANSIENT_ERROR_ACTIONS: frozenset[str] = frozenset({"back_off", "skip", "stop"})
23
23
  _VALID_PROMPT_DELIVERY: frozenset[str] = frozenset({"argv", "stdin"})
24
24
 
25
+ # claude's own compact JSONL terminal token — the default `[agent] terminal_marker`
26
+ # so existing configs are unaffected. Single source for the field default and the
27
+ # TOML-parse fallback (parsers.py); `agent_runtime.run`'s own kwarg default mirrors
28
+ # this literal but is decoupled to avoid an agent_runtime -> config import.
29
+ DEFAULT_TERMINAL_MARKER = '"type":"result"'
30
+
25
31
 
26
32
  @dataclass(frozen=True)
27
33
  class AgentConfig:
@@ -31,6 +37,11 @@ class AgentConfig:
31
37
  env: dict[str, str] = field(default_factory=dict)
32
38
  prompt_delivery: Literal["argv", "stdin"] = "argv"
33
39
  exec_prefix: list[str] = field(default_factory=list)
40
+ terminal_marker: str = DEFAULT_TERMINAL_MARKER
41
+ """Byte substring the grace-kill scan looks for in the round log to detect
42
+ the agent's terminal record (see ``agent_runtime.run``'s marker scan).
43
+ Defaults to claude's own JSONL token so existing configs are unaffected;
44
+ an empty string opts out of the marker scan entirely (wall-clock-ceiling-only)."""
34
45
 
35
46
  @property
36
47
  def binary(self) -> str | None:
@@ -32,6 +32,7 @@ from agent_runner.config.models import (
32
32
  _VALID_PROMPT_DELIVERY,
33
33
  _VALID_TRANSIENT_ERROR_ACTIONS,
34
34
  _VCS_ALLOWED_FIELDS,
35
+ DEFAULT_TERMINAL_MARKER,
35
36
  AgentConfig,
36
37
  MonitorConfig,
37
38
  MonitorHostHealthConfig,
@@ -109,6 +110,7 @@ def _parse_agent(
109
110
  exec_prefix = _require_str_list(
110
111
  agent_d.get("exec_prefix", []), field=f"{field_prefix} exec_prefix"
111
112
  )
113
+ terminal_marker = str(agent_d.get("terminal_marker", DEFAULT_TERMINAL_MARKER))
112
114
  return AgentConfig(
113
115
  command=command,
114
116
  prompt_arg_template=prompt_arg_template,
@@ -116,6 +118,7 @@ def _parse_agent(
116
118
  env={str(k): str(v) for k, v in env_d.items()},
117
119
  prompt_delivery=prompt_delivery, # type: ignore[arg-type] # narrowed above
118
120
  exec_prefix=exec_prefix,
121
+ terminal_marker=terminal_marker,
119
122
  )
120
123
 
121
124
 
@@ -60,6 +60,7 @@ ORPHAN_IDEMPOTENT_SKIP = "orphan_idempotent_skip"
60
60
  ORPHAN_STASH_FAILED = "orphan_stash_failed"
61
61
  ORPHAN_STASHED = "orphan_stashed"
62
62
  PACKAGE_UPGRADED = "package_upgraded"
63
+ PHASE_WINDOW_OVERLAP = "phase_window_overlap"
63
64
  PROMPT_OVERWRITTEN = "prompt_overwritten"
64
65
  ROUND_CGROUP_MEMORY = "round_cgroup_memory"
65
66
  ROUND_CONTAINER_ORPHAN_RISK = "round_container_orphan_risk"
@@ -89,6 +89,29 @@ def _valid_pid(value: object) -> bool:
89
89
  return isinstance(value, int) and not isinstance(value, bool) and value > 1
90
90
 
91
91
 
92
+ def create_time_of(pid: int) -> float | None:
93
+ """The process start-time identity token for ``pid``, or None if it can't be
94
+ read (no such process / no permission). Recorded when writing a pid so a
95
+ later read can tell the same process from a recycled PID."""
96
+ try:
97
+ return psutil.Process(pid).create_time()
98
+ except psutil.Error:
99
+ return None
100
+
101
+
102
+ def create_time_matches(pid: int, recorded: float | None) -> bool:
103
+ """True iff ``pid`` still names the process whose start-time was ``recorded``.
104
+ ``recorded is None`` (a legacy sidecar without a token) -> True, best-effort
105
+ unverified, matching the historical behaviour. A live start-time differing by
106
+ >= 1s, or an unreadable process, -> False (a recycled PID: do not signal it)."""
107
+ if recorded is None:
108
+ return True
109
+ try:
110
+ return abs(psutil.Process(pid).create_time() - recorded) < 1.0
111
+ except psutil.Error:
112
+ return False
113
+
114
+
92
115
  @dataclass(frozen=True)
93
116
  class PIDFile:
94
117
  path: Path
@@ -99,10 +122,9 @@ class PIDFile:
99
122
  atomically (tmp + rename) so a concurrent ``stop``/``kill`` read never sees a
100
123
  torn file."""
101
124
  payload: dict[str, object] = {"pid": pid}
102
- try:
103
- payload["create_time"] = psutil.Process(pid).create_time()
104
- except psutil.Error:
105
- pass # token is best-effort; a read without it falls back to unverified
125
+ ct = create_time_of(pid)
126
+ if ct is not None:
127
+ payload["create_time"] = ct
106
128
  atomic_write_json(self.path, payload)
107
129
 
108
130
  def read(self) -> int | None:
@@ -124,14 +146,7 @@ class PIDFile:
124
146
  return None
125
147
  pid = data["pid"]
126
148
  recorded = data.get("create_time")
127
- if recorded is None:
128
- return pid # write couldn't capture a token; best-effort unverified
129
- try:
130
- if abs(psutil.Process(pid).create_time() - recorded) < 1.0:
131
- return pid
132
- except psutil.Error:
133
- pass
134
- return None # process gone, or a different (recycled) process now holds the PID
149
+ return pid if create_time_matches(pid, recorded) else None
135
150
 
136
151
  def unlink(self) -> None:
137
152
  self.path.unlink(missing_ok=True)
@@ -12,6 +12,7 @@ the module never reads throttle/event state itself, which is what keeps it pure.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import itertools
15
16
  from dataclasses import dataclass
16
17
  from datetime import datetime
17
18
 
@@ -49,6 +50,68 @@ class Selection:
49
50
  active_window: str | None
50
51
 
51
52
 
53
+ @dataclass(frozen=True)
54
+ class WindowOverlap:
55
+ """Two agent-overriding phases whose own run-windows collide in the same
56
+ timezone — a rotation footgun (serve alternates between them round-to-round).
57
+ """
58
+
59
+ phase_a: str
60
+ phase_b: str
61
+ window_a: str
62
+ window_b: str
63
+
64
+
65
+ def _windows_collide(a: schedule.Window, b: schedule.Window) -> bool:
66
+ """Two windows collide iff some (weekday, minute) is inside BOTH. Delegates
67
+ to ``Window.contains`` — the scheduler's own day-shift/midnight-wrap
68
+ semantics — instead of re-deriving interval/day math, so this can't drift
69
+ from what actually decides whether a round runs (a wrapped window's
70
+ past-midnight tail is attributed to its START day, not the calendar day
71
+ the tail's clock-time falls on — re-deriving that independently is exactly
72
+ what produced the false positive/negative this replaced). Config-time,
73
+ once per boot, over a handful of windows; ``any`` short-circuits, so the
74
+ O(7*1440) scan per pair is negligible.
75
+ """
76
+ return any(a.contains(d, m) and b.contains(d, m) for d in range(7) for m in range(1440))
77
+
78
+
79
+ def find_phase_window_overlaps(cfg) -> list[WindowOverlap]:
80
+ """Pure config check: pairs of phases that BOTH override ``agent`` AND BOTH
81
+ define their own non-empty ``run_windows`` AND share an effective timezone AND
82
+ whose windows intersect. Timezone-mismatched pairs are skipped (no cross-tz
83
+ math in a warning). ``pause_windows`` are ignored for now — a pause that carves
84
+ out the overlap can over-warn, acceptable for a warning; the eventual
85
+ hard-error form must model pauses before rejecting.
86
+ """
87
+ phases = cfg.phases
88
+ if phases is None:
89
+ return []
90
+ candidates = []
91
+ for p in phases.list or []:
92
+ ov = phases.overrides.get(p)
93
+ if ov and ov.agent and ov.schedule is not None and ov.schedule.run_windows:
94
+ sched = cfg.profile_for(p).schedule # effective tz (inherits global), own windows
95
+ candidates.append((p, sched))
96
+ overlaps: list[WindowOverlap] = []
97
+ for (pa, sa), (pb, sb) in itertools.combinations(candidates, 2):
98
+ if sa.timezone != sb.timezone:
99
+ continue
100
+ hit = next(
101
+ (
102
+ (wa, wb)
103
+ for wa in sa.run_windows
104
+ for wb in sb.run_windows
105
+ if _windows_collide(wa, wb)
106
+ ),
107
+ None,
108
+ )
109
+ if hit is not None:
110
+ wa, wb = hit
111
+ overlaps.append(WindowOverlap(pa, pb, wa.label, wb.label))
112
+ return overlaps
113
+
114
+
52
115
  def candidate_phases(cfg, round_num: int) -> list[str | None]:
53
116
  """Phases to consider this round, in preference order.
54
117
 
@@ -1,7 +1,7 @@
1
1
  # agent-runner.toml — generated by `agent-runner init --preset claude`.
2
2
 
3
3
  [agent]
4
- command = ["claude", "--model", "claude-opus-4-7",
4
+ command = ["claude", "--model", "claude-opus-5",
5
5
  "--dangerously-skip-permissions",
6
6
  "--verbose", "--output-format", "stream-json"]
7
7
  prompt_arg_template = ["-p"]
@@ -11,6 +11,9 @@
11
11
  command = ["codewhale", "exec", "--auto", "--output-format", "stream-json"]
12
12
  prompt_arg_template = ["{prompt}"]
13
13
  name = "codewhale"
14
+ # Grace-kill result-detection token: codewhale's terminal JSONL record (vs.
15
+ # claude's default "type":"result").
16
+ terminal_marker = "\"type\":\"metadata\""
14
17
  # [agent.env] omitted — DeepSeek key is ambient (env or codewhale keyring).
15
18
 
16
19
  [runtime]
@@ -19,6 +19,9 @@
19
19
  command = ["pi", "-p", "-na", "--mode", "json", "--model", "PROVIDER/MODEL"]
20
20
  prompt_arg_template = ["{prompt}"]
21
21
  name = "pi"
22
+ # Grace-kill result-detection token: pi's terminal JSONL record (vs. claude's
23
+ # default "type":"result").
24
+ terminal_marker = "\"type\":\"agent_end\""
22
25
 
23
26
  [agent.env]
24
27
  PI_OFFLINE = "1"