cli-agent-runner 0.2.22__tar.gz → 0.2.24__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.24}/.vulture-whitelist.py +17 -0
  2. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/CHANGELOG.md +24 -0
  3. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/PKG-INFO +2 -2
  4. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/README.md +1 -1
  5. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_emit/__init__.py +2 -0
  6. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_emit/memory.py +7 -3
  7. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_emit/serve.py +20 -0
  8. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_monitor_state.py +26 -6
  9. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_version.py +2 -2
  10. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/agent_runtime.py +17 -10
  11. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/api.py +2 -10
  12. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/__init__.py +2 -0
  13. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/_serve_cgroup.py +41 -11
  14. cli_agent_runner-0.2.24/agent_runner/cli/doctor_cmd.py +84 -0
  15. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/serve_cmd.py +11 -0
  16. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/config/models.py +11 -0
  17. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/config/parsers.py +3 -0
  18. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/events.py +1 -0
  19. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/lifecycle.py +27 -12
  20. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/metrics.py +18 -2
  21. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/phase_select.py +63 -0
  22. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/presets/claude.toml +1 -1
  23. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/presets/codewhale.toml +3 -0
  24. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/presets/pi.toml +3 -0
  25. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/runner.py +5 -6
  26. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/startup_check.py +93 -83
  27. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/architecture.md +1 -0
  28. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/commands.md +13 -2
  29. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/configuration.md +16 -0
  30. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/memory-efficiency.md +91 -0
  31. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/recipes/codewhale.md +3 -0
  32. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/runbook.md +2 -2
  33. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/contract/test_public_api_surface.py +1 -0
  34. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_serve_loop.py +67 -3
  35. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_architecture.py +11 -2
  36. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_import_footprint.py +5 -0
  37. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_grace.py +108 -0
  38. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cgroup_probe.py +191 -7
  39. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config.py +28 -0
  40. cli_agent_runner-0.2.24/tests/unit/test_doctor_cmd.py +143 -0
  41. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_events.py +4 -0
  42. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_lifecycle.py +31 -0
  43. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_assembly.py +29 -0
  44. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_phase_select.py +115 -0
  45. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_startup_check.py +63 -0
  46. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.codecov.yml +0 -0
  47. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  48. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  49. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  50. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  51. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.github/dependabot.yml +0 -0
  52. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.github/workflows/ci.yml +0 -0
  53. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.github/workflows/release.yml +0 -0
  54. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/.gitignore +0 -0
  55. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/BACKLOG.md +0 -0
  56. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/CODE_OF_CONDUCT.md +0 -0
  57. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/CONTRIBUTING.md +0 -0
  58. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/LICENSE +0 -0
  59. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/README.zh.md +0 -0
  60. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/SECURITY.md +0 -0
  61. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/__init__.py +0 -0
  62. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_docgen.py +0 -0
  63. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_emit/agent.py +0 -0
  64. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_emit/rounds.py +0 -0
  65. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_monitor_detectors.py +0 -0
  66. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_monitor_registry.py +0 -0
  67. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_plugin_scan.py +0 -0
  68. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_redact.py +0 -0
  69. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_registry.py +0 -0
  70. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_resolve.py +0 -0
  71. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_round_outcome.py +0 -0
  72. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_round_support.py +0 -0
  73. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_serve_policy.py +0 -0
  74. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_substrate.py +0 -0
  75. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/_throttle.py +0 -0
  76. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/api_types.py +0 -0
  77. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/__init__.py +0 -0
  78. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/_constants.py +0 -0
  79. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  80. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/codewhale.py +0 -0
  81. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  82. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/gemini.py +0 -0
  83. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/kimi.py +0 -0
  84. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/pi.py +0 -0
  85. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/__main__.py +0 -0
  86. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/_serve_round.py +0 -0
  87. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/common.py +0 -0
  88. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/events_cmd.py +0 -0
  89. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/init_cmd.py +0 -0
  90. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/install_cmd.py +0 -0
  91. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/migrate_cmd.py +0 -0
  92. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/monitor_cmd.py +0 -0
  93. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/peek_cmd.py +0 -0
  94. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/round_cmd.py +0 -0
  95. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/service_cmd.py +0 -0
  96. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/cli/upgrade_cmd.py +0 -0
  97. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/clock.py +0 -0
  98. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/config/__init__.py +0 -0
  99. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/config/errors.py +0 -0
  100. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/config/loader.py +0 -0
  101. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/config/validators.py +0 -0
  102. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/context_store.py +0 -0
  103. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/defenses.py +0 -0
  104. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/hooks.py +0 -0
  105. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/host_health.py +0 -0
  106. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/http_progress.py +0 -0
  107. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/migrations.py +0 -0
  108. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/monitor.py +0 -0
  109. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/presets/__init__.py +0 -0
  110. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/presets/aider.toml +0 -0
  111. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/presets/gemini.toml +0 -0
  112. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/presets/kimi.toml +0 -0
  113. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/prompt_loader.py +0 -0
  114. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/remote_relay.py +0 -0
  115. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/round_log.py +0 -0
  116. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/round_view.py +0 -0
  117. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/scaffold.py +0 -0
  118. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/schedule.py +0 -0
  119. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/service_unit.py +0 -0
  120. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/agent_runner/vcs_state.py +0 -0
  121. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/build.sh +0 -0
  122. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/deploy/Dockerfile +0 -0
  123. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/deploy/docker-compose.yml +0 -0
  124. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/README.md +0 -0
  125. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/events.md +0 -0
  126. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/long-running-agents.md +0 -0
  127. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/migrations/0.2.md +0 -0
  128. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/plugins.md +0 -0
  129. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/quickstart.md +0 -0
  130. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/recipes/aider.md +0 -0
  131. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/recipes/container-pi.md +0 -0
  132. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/recipes/deploy-all-in-one.md +0 -0
  133. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/recipes/kimi.md +0 -0
  134. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/recipes/pi.md +0 -0
  135. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/docs/thesis.md +0 -0
  136. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/pyproject.toml +0 -0
  137. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/__init__.py +0 -0
  138. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/_clock.py +0 -0
  139. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/_test_helpers.py +0 -0
  140. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/conftest.py +0 -0
  141. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/contract/__init__.py +0 -0
  142. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/e2e/__init__.py +0 -0
  143. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/e2e/conftest.py +0 -0
  144. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  145. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_install_systemd.py +0 -0
  146. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  147. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  148. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  149. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  150. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  151. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/generate_vulture_whitelist.py +0 -0
  152. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/__init__.py +0 -0
  153. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_auto_stop_e2e.py +0 -0
  154. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_bounded_run.py +0 -0
  155. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_context_enricher_namespacing.py +0 -0
  156. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_dirty_handler_seam.py +0 -0
  157. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_fresh_eyes_signal.py +0 -0
  158. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_grace_kill_emission.py +0 -0
  159. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_install_dry_run.py +0 -0
  160. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_monitor_seeded.py +0 -0
  161. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_detector_loaded.py +0 -0
  162. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_owned_paths.py +0 -0
  163. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_real_flow.py +0 -0
  164. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  165. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  166. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_scaffold_presets.py +0 -0
  167. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  168. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_spawn_round_wedged.py +0 -0
  169. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_substrate_fingerprint.py +0 -0
  170. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/integration/test_transient_error_backoff.py +0 -0
  171. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/__init__.py +0 -0
  172. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/_docs.py +0 -0
  173. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/_event_scan.py +0 -0
  174. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_atomic_write_enforced.py +0 -0
  175. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_authored_facts_declared.py +0 -0
  176. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  177. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_bdd_test_conventions.py +0 -0
  178. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_catalogs.py +0 -0
  179. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_classification_ssot.py +0 -0
  180. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_config_error_consistency.py +0 -0
  181. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_config_value_set_ssot.py +0 -0
  182. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_container_stop_bound.py +0 -0
  183. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  184. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  185. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  186. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_cli_claims.py +0 -0
  187. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_operator_surface.py +0 -0
  188. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  189. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  190. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  191. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_docs_generated.py +0 -0
  192. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_docs_index_complete.py +0 -0
  193. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_entry_points_resolve.py +0 -0
  194. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_event_int_coercion.py +0 -0
  195. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_event_kind_registry.py +0 -0
  196. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_event_kinds_ssot.py +0 -0
  197. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_events_doc_contract.py +0 -0
  198. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  199. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_hook_contract_docs.py +0 -0
  200. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_layer_2_loop_size.py +0 -0
  201. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  202. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_module_boundaries.py +0 -0
  203. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_module_sizes.py +0 -0
  204. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  205. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_no_ai_signatures.py +0 -0
  206. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_no_internal_refs.py +0 -0
  207. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_no_price_logic.py +0 -0
  208. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  209. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_no_raw_time.py +0 -0
  210. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  211. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_no_version_refs.py +0 -0
  212. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_peek_schema_version.py +0 -0
  213. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_phase_select_stateless.py +0 -0
  214. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_plugins_config_stable.py +0 -0
  215. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  216. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  217. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_round_alloc_growth.py +0 -0
  218. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  219. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_round_num_single_source.py +0 -0
  220. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_round_result_stable.py +0 -0
  221. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  222. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  223. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  224. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_upstream_schema_canary.py +0 -0
  225. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  226. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/literate/__init__.py +0 -0
  227. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/literate/parser.py +0 -0
  228. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/literate/test_parser.py +0 -0
  229. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/literate/test_quickstart.py +0 -0
  230. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/__init__.py +0 -0
  231. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime.py +0 -0
  232. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_container.py +0 -0
  233. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  234. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_progress.py +0 -0
  235. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_reap.py +0 -0
  236. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  237. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_assemble_prompt.py +0 -0
  238. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_events_stream.py +0 -0
  239. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_install.py +0 -0
  240. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_observation.py +0 -0
  241. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_read_round_num.py +0 -0
  242. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_resolve_phase.py +0 -0
  243. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_service.py +0 -0
  244. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_api_types.py +0 -0
  245. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_auto_stop_gating.py +0 -0
  246. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_catalogs.py +0 -0
  247. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_claude_error_detector.py +0 -0
  248. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_claude_foreign_schema.py +0 -0
  249. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cli.py +0 -0
  250. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cli_common.py +0 -0
  251. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cli_init_install.py +0 -0
  252. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cli_main_boundary.py +0 -0
  253. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cli_monitor_http.py +0 -0
  254. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  255. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_cli_upgrade.py +0 -0
  256. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_codewhale_code_precedence.py +0 -0
  257. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_codewhale_plugin.py +0 -0
  258. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_coerce_int_poison.py +0 -0
  259. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_fresh_eyes.py +0 -0
  260. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_max_rounds.py +0 -0
  261. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_nested_strictness.py +0 -0
  262. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_prompt_delivery.py +0 -0
  263. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_schedule.py +0 -0
  264. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_stop_file.py +0 -0
  265. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_strictness.py +0 -0
  266. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  267. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_config_transient_error_action.py +0 -0
  268. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_context_store.py +0 -0
  269. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_control_char_config.py +0 -0
  270. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_default_dirty_handler.py +0 -0
  271. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_defenses.py +0 -0
  272. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_detect_hung_latch.py +0 -0
  273. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_detector_isolation.py +0 -0
  274. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_detector_protocol.py +0 -0
  275. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_dirty_handlers.py +0 -0
  276. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_docgen.py +0 -0
  277. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_effective_throttle_view.py +0 -0
  278. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_emit_config_migrated.py +0 -0
  279. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_emit_schedule.py +0 -0
  280. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  281. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd.py +0 -0
  282. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd_rollover.py +0 -0
  283. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd_tail.py +0 -0
  284. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_events_jsonl_decode.py +0 -0
  285. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_example_dirty_handler.py +0 -0
  286. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_example_plugin.py +0 -0
  287. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_exec_prefix.py +0 -0
  288. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_exit_cause.py +0 -0
  289. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_exit_classifier.py +0 -0
  290. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  291. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_gemini_plugin.py +0 -0
  292. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_git_timeout.py +0 -0
  293. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_hook_failure_isolation.py +0 -0
  294. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_hook_redact_order.py +0 -0
  295. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_hooks.py +0 -0
  296. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_host_health.py +0 -0
  297. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_http_progress.py +0 -0
  298. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_http_progress_epoch.py +0 -0
  299. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_init_entry_points.py +0 -0
  300. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  301. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_kimi_plugin.py +0 -0
  302. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_metrics.py +0 -0
  303. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  304. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_metrics_swap.py +0 -0
  305. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_migrate_cmd.py +0 -0
  306. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_migrations.py +0 -0
  307. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_cmd_severity.py +0 -0
  308. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_dedup_identity.py +0 -0
  309. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  310. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  311. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  312. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  313. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detectors.py +0 -0
  314. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_event_tail.py +0 -0
  315. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_loop.py +0 -0
  316. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_on_alert.py +0 -0
  317. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_remote.py +0 -0
  318. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  319. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_schedule_pause.py +0 -0
  320. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_network_blip_signal_guard.py +0 -0
  321. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_open_round_log.py +0 -0
  322. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  323. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_outer_round_ceiling.py +0 -0
  324. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_peek_argparse.py +0 -0
  325. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_peek_project_isolation.py +0 -0
  326. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_peek_schedule.py +0 -0
  327. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_peek_select.py +0 -0
  328. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_phase_agent_launch.py +0 -0
  329. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_phase_profiles.py +0 -0
  330. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_pi_plugin.py +0 -0
  331. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_plugin_constants.py +0 -0
  332. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_plugin_scan_parity.py +0 -0
  333. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_presets.py +0 -0
  334. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_primary_prompt_file_home.py +0 -0
  335. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_prompt_loader.py +0 -0
  336. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_read_json_hardening.py +0 -0
  337. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_redact.py +0 -0
  338. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_resolve.py +0 -0
  339. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_round_cmd.py +0 -0
  340. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_round_cmd_sigterm.py +0 -0
  341. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_round_log_helpers.py +0 -0
  342. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_round_outcome.py +0 -0
  343. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  344. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_round_view.py +0 -0
  345. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_runner.py +0 -0
  346. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  347. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_runner_throttle.py +0 -0
  348. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_scaffold.py +0 -0
  349. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_schedule.py +0 -0
  350. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_cmd_bounded.py +0 -0
  351. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_config_broken.py +0 -0
  352. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_crash_loop.py +0 -0
  353. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_mem_gate.py +0 -0
  354. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_phase_schedule.py +0 -0
  355. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_policy.py +0 -0
  356. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round.py +0 -0
  357. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  358. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_log.py +0 -0
  359. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_oom_killed.py +0 -0
  360. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_terminate.py +0 -0
  361. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_schedule_gate.py +0 -0
  362. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_sentinel.py +0 -0
  363. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_serve_startup_hooks.py +0 -0
  364. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_service_mode.py +0 -0
  365. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_service_unit.py +0 -0
  366. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_startup_battery_exit.py +0 -0
  367. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_substrate.py +0 -0
  368. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  369. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  370. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_epoch_bound.py +0 -0
  371. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_plugin_classification.py +0 -0
  372. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/tests/unit/test_upgrade_migrate.py +0 -0
  373. {cli_agent_runner-0.2.22 → cli_agent_runner-0.2.24}/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,30 @@ 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.24] - 2026-09-12
9
+
10
+ ### Added
11
+ - `host_cgroup_memory_limit` now advises adding `MemoryHigh` when a cgroup has `memory.max` set without `memory.high` (graceful pre-OOM throttling) — only for a bound on the supervisor's own cgroup, with the PSI-floor caveat when the mid-round floor isn't already deferring.
12
+
13
+ ### Changed
14
+ - `doctor`'s phase-plan preview now shows a paused phase's resume time.
15
+ - Trimmed the grace-kill test wall-time (~80s faster suite; behavior-identical).
16
+
17
+ ## [0.2.23] - 2026-09-12
18
+
19
+ ### Added
20
+ - `[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).
21
+ - `doctor` — read-only pre-flight (never launches the agent): startup checks, phase-window overlap warnings, and an N-round phase-plan preview.
22
+ - `phase_window_overlap` event — emitted at serve boot when two agent-overriding phases' windows collide in the same timezone.
23
+
24
+ ### Changed
25
+ - Startup-check dispatch and the catalog's kind list now derive from a single descriptor table (no behavior change).
26
+ - The lock-holder pid create_time verify now shares one `lifecycle` helper with `serve.pid` (no behavior change).
27
+ - Default `claude` preset model updated `claude-opus-4-7` → `claude-opus-5`.
28
+
29
+ ### Fixed
30
+ - `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).
31
+
8
32
  ## [0.2.22] - 2026-09-11
9
33
 
10
34
  ### 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.24
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",
@@ -113,7 +113,7 @@ def emit_host_cgroup_memory_limit(
113
113
  mid-round hard floor can defer to kernel cgroup-OOM -- see
114
114
  ``emit_mem_pressure_deferred_to_cgroup`` below.
115
115
 
116
- This also carries an optional startup swap-cap advisory
116
+ This also carries an optional startup advisory
117
117
  (``swap_total_bytes`` / ``swap_cap_pct`` / ``memory_high`` / ``advisory``)
118
118
  as FIELDS on this SAME event -- never a separate event kind, and never an
119
119
  auto-change to the operator's cgroup or unit. ``swap_total_bytes`` is the
@@ -123,8 +123,12 @@ def emit_host_cgroup_memory_limit(
123
123
  bytes (``metrics.cgroup_memory_high``) -- ``None`` means unset (the
124
124
  cgroup read the literal ``"max"``, or no finite value at all), never the
125
125
  raw ``"max"`` token, so a caller can't mistake "unset" for a real
126
- ceiling; ``advisory`` is the human-readable warning text when the swap
127
- cap looks implausibly tight, else ``None``."""
126
+ ceiling; ``advisory`` is the human-readable warning text, else ``None`` --
127
+ it covers two independent conditions, joined with ``"; "`` when both
128
+ fire: a ``memory.swap.max`` that looks implausibly tight against host
129
+ swap, and a ``memory.max`` set on this process's OWN cgroup without a
130
+ ``memory.high``, which recommends adding one (below ``memory.max``) for
131
+ graceful pre-OOM throttling instead of a hard kill."""
128
132
  from agent_runner.events import HOST_CGROUP_MEMORY_LIMIT, emit
129
133
 
130
134
  emit(
@@ -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.24'
22
+ __version_tuple__ = version_tuple = (0, 2, 24)
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
 
@@ -225,9 +225,18 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
225
225
  memory.swap.max is bounded but far below the host's own available swap
226
226
  (``_SWAP_CAP_ADVISORY_PCT``): the operator capped the cgroup's swap well
227
227
  under what the host has, so the mid-round floor may terminate a round
228
- the kernel would have contained on a wider cap. One line is also printed
229
- to stderr in that case. The event also carries ``memory_high`` -- the
230
- bounding ancestor's ``memory.high`` soft-throttle threshold
228
+ the kernel would have contained on a wider cap. A second, independent
229
+ advisory recommends ``memory.high`` when ``memory.max`` is set but
230
+ ``memory.high`` isn't -- ONLY when the bounding ancestor is the
231
+ operator's OWN leaf (``bounding_cgroup_path == cgroup_path``): since
232
+ ``memory.max`` is the MIN across every ancestor, "set" can also mean an
233
+ inherited parent slice or a container root, where "add MemoryHigh" is
234
+ noise the operator can't act on from their own unit. That hint also
235
+ appends a PSI-floor caveat, but only when the floor ISN'T already
236
+ deferring to kernel cgroup-OOM (a deferring host has no floor for the
237
+ throttle's PSI-full rise to trip). Both advisories join with ``"; "``
238
+ and print as one stderr line. The event also carries ``memory_high`` --
239
+ the bounding ancestor's ``memory.high`` soft-throttle threshold
231
240
  (``metrics.cgroup_memory_high``), ``None`` when unset -- for the same
232
241
  reason: an operator asking "is MemoryHigh even set" is exactly this
233
242
  release's field ask. Advisory only -- this never changes the operator's
@@ -247,13 +256,39 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
247
256
  swap_cap_pct = (
248
257
  round(100.0 * swap_max / swap_total, 1) if swap_max is not None and swap_total > 0 else None
249
258
  )
250
- advisory: str | None = None
259
+ advisories: list[str] = []
251
260
  if swap_cap_pct is not None and swap_cap_pct < _SWAP_CAP_ADVISORY_PCT:
252
- advisory = (
261
+ advisories.append(
253
262
  "cgroup memory.swap.max is far below host swap; the mid-round floor may "
254
263
  "terminate rounds the kernel would have contained -- consider bounding "
255
264
  "both memory.max and memory.swap.max, or set in_round_mem_terminate=false"
256
265
  )
266
+ defer = (
267
+ limits["memory_max"] is not None
268
+ and swap_max is not None
269
+ and limits["memory_max"] < metrics.mem_total_bytes()
270
+ and swap_max <= swap_total # 4th guard: a >> host-swap cap can't bind -> stay armed
271
+ )
272
+ own_scope = (
273
+ limits["memory_max"] is not None
274
+ and memory_high is None
275
+ and limits["bounding_cgroup_path"] == limits["cgroup_path"]
276
+ )
277
+ if own_scope:
278
+ hint = (
279
+ "memory.max is set on this cgroup without memory.high -- add "
280
+ "memory.high (below memory.max) for graceful throttling before an "
281
+ "OOM kill"
282
+ )
283
+ if not defer:
284
+ hint += (
285
+ "; bound memory.swap.max (at or below host swap) too, or set "
286
+ "in_round_mem_terminate=false, so the throttle's PSI-full rise "
287
+ "doesn't trip the mid-round floor"
288
+ )
289
+ advisories.append(hint)
290
+ advisory = "; ".join(advisories) or None
291
+ if advisory is not None:
257
292
  print(f"agent-runner: {advisory}", file=sys.stderr)
258
293
  emit_host_cgroup_memory_limit(
259
294
  log_dir,
@@ -265,9 +300,4 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
265
300
  memory_high=memory_high,
266
301
  advisory=advisory,
267
302
  )
268
- return (
269
- limits["memory_max"] is not None
270
- and swap_max is not None
271
- and limits["memory_max"] < metrics.mem_total_bytes()
272
- and swap_max <= swap_total # 4th guard: a >> host-swap cap can't bind -> stay armed
273
- )
303
+ return defer
@@ -0,0 +1,84 @@
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
+ "resume_at": sel.resume_at.isoformat() if sel.resume_at else None,
37
+ "resume_timezone": sel.resume_timezone,
38
+ "skipped": sel.skipped,
39
+ "active_window": sel.active_window,
40
+ }
41
+ )
42
+ return out
43
+
44
+
45
+ def cmd_doctor(args) -> int:
46
+ cfg = cfg_from_args(args)
47
+ report = DoctorReport(
48
+ checks=startup_check.run_battery(cfg),
49
+ overlaps=phase_select.find_phase_window_overlaps(cfg),
50
+ plan=_plan(cfg, args.rounds),
51
+ )
52
+ json_mode = getattr(args, "json", False)
53
+ if json_mode:
54
+ emit(report, json_mode=True)
55
+ else:
56
+ print(_format(report))
57
+ return 0
58
+
59
+
60
+ def _format(report: DoctorReport) -> str:
61
+ # Hand-formatted rather than common._pretty: that helper repr()s nested
62
+ # lists/dicts, which would dump report.plan's per-round dicts as Python
63
+ # reprs instead of the readable "round N: phase" lines below.
64
+ lines = ["checks:"]
65
+ for c in report.checks:
66
+ mark = "ok" if c.ok else "FAIL"
67
+ detail = ""
68
+ if not c.ok:
69
+ detail = f" — {c.reason}" + (f" (fix: {c.how_to_fix})" if c.how_to_fix else "")
70
+ lines.append(f" [{mark}] {c.name}{detail}")
71
+ if report.overlaps:
72
+ lines.append("window overlaps:")
73
+ for o in report.overlaps:
74
+ lines.append(f" {o.phase_a} ({o.window_a}) vs {o.phase_b} ({o.window_b})")
75
+ lines.append("phase plan:")
76
+ for step in report.plan:
77
+ if step["paused"]:
78
+ where = "paused"
79
+ if step["resume_at"] is not None:
80
+ where += f" -> resumes {step['resume_phase']} at {step['resume_at']}"
81
+ else:
82
+ where = step["phase"] or "base"
83
+ lines.append(f" round {step['round']}: {where}")
84
+ 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)