cli-agent-runner 0.2.21__tar.gz → 0.2.22__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 (371) hide show
  1. cli_agent_runner-0.2.22/.github/dependabot.yml +6 -0
  2. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/workflows/ci.yml +4 -4
  3. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/workflows/release.yml +11 -7
  4. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.vulture-whitelist.py +1 -0
  5. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/BACKLOG.md +0 -27
  6. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/CHANGELOG.md +13 -0
  7. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/PKG-INFO +1 -1
  8. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/SECURITY.md +17 -0
  9. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_version.py +2 -2
  10. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/agent_runtime.py +35 -13
  11. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/api.py +44 -6
  12. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/api_types.py +5 -0
  13. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/defenses.py +6 -2
  14. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/runner.py +6 -0
  15. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/startup_check.py +75 -7
  16. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/commands.md +16 -2
  17. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/container-pi.md +16 -0
  18. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/runbook.md +5 -3
  19. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_container.py +118 -8
  20. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_service.py +154 -0
  21. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_startup_check.py +51 -0
  22. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.codecov.yml +0 -0
  23. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  24. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.gitignore +0 -0
  28. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/CODE_OF_CONDUCT.md +0 -0
  29. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/CONTRIBUTING.md +0 -0
  30. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/LICENSE +0 -0
  31. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/README.md +0 -0
  32. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/README.zh.md +0 -0
  33. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/__init__.py +0 -0
  34. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_docgen.py +0 -0
  35. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/__init__.py +0 -0
  36. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/agent.py +0 -0
  37. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/memory.py +0 -0
  38. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/rounds.py +0 -0
  39. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/serve.py +0 -0
  40. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_monitor_detectors.py +0 -0
  41. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_monitor_registry.py +0 -0
  42. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_monitor_state.py +0 -0
  43. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_plugin_scan.py +0 -0
  44. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_redact.py +0 -0
  45. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_registry.py +0 -0
  46. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_resolve.py +0 -0
  47. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_round_outcome.py +0 -0
  48. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_round_support.py +0 -0
  49. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_serve_policy.py +0 -0
  50. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_substrate.py +0 -0
  51. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_throttle.py +0 -0
  52. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/__init__.py +0 -0
  53. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/_constants.py +0 -0
  54. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  55. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/codewhale.py +0 -0
  56. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  57. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/gemini.py +0 -0
  58. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/kimi.py +0 -0
  59. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/pi.py +0 -0
  60. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/__init__.py +0 -0
  61. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/__main__.py +0 -0
  62. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_cgroup.py +0 -0
  63. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_round.py +0 -0
  64. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/common.py +0 -0
  65. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/events_cmd.py +0 -0
  66. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/init_cmd.py +0 -0
  67. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/install_cmd.py +0 -0
  68. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/migrate_cmd.py +0 -0
  69. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/monitor_cmd.py +0 -0
  70. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/peek_cmd.py +0 -0
  71. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/round_cmd.py +0 -0
  72. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/serve_cmd.py +0 -0
  73. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/service_cmd.py +0 -0
  74. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/upgrade_cmd.py +0 -0
  75. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/clock.py +0 -0
  76. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/__init__.py +0 -0
  77. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/errors.py +0 -0
  78. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/loader.py +0 -0
  79. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/models.py +0 -0
  80. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/parsers.py +0 -0
  81. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/validators.py +0 -0
  82. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/context_store.py +0 -0
  83. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/events.py +0 -0
  84. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/hooks.py +0 -0
  85. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/host_health.py +0 -0
  86. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/http_progress.py +0 -0
  87. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/lifecycle.py +0 -0
  88. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/metrics.py +0 -0
  89. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/migrations.py +0 -0
  90. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/monitor.py +0 -0
  91. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/phase_select.py +0 -0
  92. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/__init__.py +0 -0
  93. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/aider.toml +0 -0
  94. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/claude.toml +0 -0
  95. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/codewhale.toml +0 -0
  96. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/gemini.toml +0 -0
  97. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/kimi.toml +0 -0
  98. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/pi.toml +0 -0
  99. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/prompt_loader.py +0 -0
  100. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/remote_relay.py +0 -0
  101. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/round_log.py +0 -0
  102. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/round_view.py +0 -0
  103. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/scaffold.py +0 -0
  104. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/schedule.py +0 -0
  105. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/service_unit.py +0 -0
  106. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/vcs_state.py +0 -0
  107. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/build.sh +0 -0
  108. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/deploy/Dockerfile +0 -0
  109. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/deploy/docker-compose.yml +0 -0
  110. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/README.md +0 -0
  111. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/architecture.md +0 -0
  112. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/configuration.md +0 -0
  113. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/events.md +0 -0
  114. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/long-running-agents.md +0 -0
  115. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/memory-efficiency.md +0 -0
  116. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/migrations/0.2.md +0 -0
  117. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/plugins.md +0 -0
  118. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/quickstart.md +0 -0
  119. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/aider.md +0 -0
  120. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/codewhale.md +0 -0
  121. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/deploy-all-in-one.md +0 -0
  122. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/kimi.md +0 -0
  123. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/pi.md +0 -0
  124. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/thesis.md +0 -0
  125. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/pyproject.toml +0 -0
  126. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/__init__.py +0 -0
  127. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/_clock.py +0 -0
  128. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/_test_helpers.py +0 -0
  129. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/conftest.py +0 -0
  130. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/contract/__init__.py +0 -0
  131. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/contract/test_public_api_surface.py +0 -0
  132. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/__init__.py +0 -0
  133. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/conftest.py +0 -0
  134. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  135. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_install_systemd.py +0 -0
  136. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  137. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  138. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  139. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  140. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  141. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/generate_vulture_whitelist.py +0 -0
  142. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/__init__.py +0 -0
  143. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_auto_stop_e2e.py +0 -0
  144. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_bounded_run.py +0 -0
  145. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_context_enricher_namespacing.py +0 -0
  146. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_dirty_handler_seam.py +0 -0
  147. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_fresh_eyes_signal.py +0 -0
  148. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_grace_kill_emission.py +0 -0
  149. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_install_dry_run.py +0 -0
  150. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_monitor_seeded.py +0 -0
  151. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_detector_loaded.py +0 -0
  152. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_owned_paths.py +0 -0
  153. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_real_flow.py +0 -0
  154. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  155. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  156. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_scaffold_presets.py +0 -0
  157. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_serve_loop.py +0 -0
  158. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  159. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_wedged.py +0 -0
  160. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_substrate_fingerprint.py +0 -0
  161. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_transient_error_backoff.py +0 -0
  162. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/__init__.py +0 -0
  163. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/_docs.py +0 -0
  164. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/_event_scan.py +0 -0
  165. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_architecture.py +0 -0
  166. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_atomic_write_enforced.py +0 -0
  167. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_authored_facts_declared.py +0 -0
  168. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  169. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_bdd_test_conventions.py +0 -0
  170. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_catalogs.py +0 -0
  171. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_classification_ssot.py +0 -0
  172. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_config_error_consistency.py +0 -0
  173. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_config_value_set_ssot.py +0 -0
  174. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_container_stop_bound.py +0 -0
  175. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  176. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  177. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  178. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_cli_claims.py +0 -0
  179. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_operator_surface.py +0 -0
  180. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  181. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  182. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  183. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_generated.py +0 -0
  184. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_index_complete.py +0 -0
  185. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_entry_points_resolve.py +0 -0
  186. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_event_int_coercion.py +0 -0
  187. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kind_registry.py +0 -0
  188. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kinds_ssot.py +0 -0
  189. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_events_doc_contract.py +0 -0
  190. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  191. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_hook_contract_docs.py +0 -0
  192. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_import_footprint.py +0 -0
  193. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_layer_2_loop_size.py +0 -0
  194. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  195. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_module_boundaries.py +0 -0
  196. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_module_sizes.py +0 -0
  197. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  198. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_ai_signatures.py +0 -0
  199. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_internal_refs.py +0 -0
  200. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_price_logic.py +0 -0
  201. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  202. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_raw_time.py +0 -0
  203. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  204. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_version_refs.py +0 -0
  205. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_peek_schema_version.py +0 -0
  206. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_phase_select_stateless.py +0 -0
  207. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_plugins_config_stable.py +0 -0
  208. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  209. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  210. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_alloc_growth.py +0 -0
  211. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  212. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_num_single_source.py +0 -0
  213. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_result_stable.py +0 -0
  214. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  215. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  216. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  217. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_upstream_schema_canary.py +0 -0
  218. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  219. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/__init__.py +0 -0
  220. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/parser.py +0 -0
  221. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/test_parser.py +0 -0
  222. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/test_quickstart.py +0 -0
  223. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/__init__.py +0 -0
  224. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime.py +0 -0
  225. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_grace.py +0 -0
  226. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  227. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_progress.py +0 -0
  228. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_reap.py +0 -0
  229. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  230. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_assemble_prompt.py +0 -0
  231. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_events_stream.py +0 -0
  232. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_install.py +0 -0
  233. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_observation.py +0 -0
  234. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_read_round_num.py +0 -0
  235. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_resolve_phase.py +0 -0
  236. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_types.py +0 -0
  237. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_auto_stop_gating.py +0 -0
  238. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_catalogs.py +0 -0
  239. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cgroup_probe.py +0 -0
  240. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_claude_error_detector.py +0 -0
  241. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_claude_foreign_schema.py +0 -0
  242. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli.py +0 -0
  243. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_common.py +0 -0
  244. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_init_install.py +0 -0
  245. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_main_boundary.py +0 -0
  246. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_monitor_http.py +0 -0
  247. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  248. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_upgrade.py +0 -0
  249. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_code_precedence.py +0 -0
  250. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_plugin.py +0 -0
  251. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_coerce_int_poison.py +0 -0
  252. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config.py +0 -0
  253. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_fresh_eyes.py +0 -0
  254. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_max_rounds.py +0 -0
  255. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_nested_strictness.py +0 -0
  256. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_prompt_delivery.py +0 -0
  257. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_schedule.py +0 -0
  258. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_stop_file.py +0 -0
  259. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_strictness.py +0 -0
  260. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  261. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_transient_error_action.py +0 -0
  262. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_context_store.py +0 -0
  263. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_control_char_config.py +0 -0
  264. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_default_dirty_handler.py +0 -0
  265. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_defenses.py +0 -0
  266. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_detect_hung_latch.py +0 -0
  267. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_detector_isolation.py +0 -0
  268. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_detector_protocol.py +0 -0
  269. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_dirty_handlers.py +0 -0
  270. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_docgen.py +0 -0
  271. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_effective_throttle_view.py +0 -0
  272. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_emit_config_migrated.py +0 -0
  273. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule.py +0 -0
  274. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  275. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events.py +0 -0
  276. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd.py +0 -0
  277. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_rollover.py +0 -0
  278. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_tail.py +0 -0
  279. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_jsonl_decode.py +0 -0
  280. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_example_dirty_handler.py +0 -0
  281. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_example_plugin.py +0 -0
  282. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_exec_prefix.py +0 -0
  283. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_exit_cause.py +0 -0
  284. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_exit_classifier.py +0 -0
  285. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  286. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_gemini_plugin.py +0 -0
  287. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_git_timeout.py +0 -0
  288. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_hook_failure_isolation.py +0 -0
  289. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_hook_redact_order.py +0 -0
  290. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_hooks.py +0 -0
  291. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_host_health.py +0 -0
  292. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress.py +0 -0
  293. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress_epoch.py +0 -0
  294. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_init_entry_points.py +0 -0
  295. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  296. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_kimi_plugin.py +0 -0
  297. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_lifecycle.py +0 -0
  298. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_metrics.py +0 -0
  299. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  300. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_swap.py +0 -0
  301. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_migrate_cmd.py +0 -0
  302. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_migrations.py +0 -0
  303. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_assembly.py +0 -0
  304. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_cmd_severity.py +0 -0
  305. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_identity.py +0 -0
  306. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  307. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  308. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  309. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  310. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detectors.py +0 -0
  311. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_event_tail.py +0 -0
  312. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_loop.py +0 -0
  313. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_on_alert.py +0 -0
  314. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_remote.py +0 -0
  315. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  316. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_schedule_pause.py +0 -0
  317. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_network_blip_signal_guard.py +0 -0
  318. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_open_round_log.py +0 -0
  319. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  320. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_outer_round_ceiling.py +0 -0
  321. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_argparse.py +0 -0
  322. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_project_isolation.py +0 -0
  323. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_schedule.py +0 -0
  324. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_select.py +0 -0
  325. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_phase_agent_launch.py +0 -0
  326. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_phase_profiles.py +0 -0
  327. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_phase_select.py +0 -0
  328. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_pi_plugin.py +0 -0
  329. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_constants.py +0 -0
  330. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_scan_parity.py +0 -0
  331. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_presets.py +0 -0
  332. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_primary_prompt_file_home.py +0 -0
  333. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_prompt_loader.py +0 -0
  334. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_read_json_hardening.py +0 -0
  335. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_redact.py +0 -0
  336. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_resolve.py +0 -0
  337. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd.py +0 -0
  338. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd_sigterm.py +0 -0
  339. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_log_helpers.py +0 -0
  340. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome.py +0 -0
  341. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  342. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_view.py +0 -0
  343. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_runner.py +0 -0
  344. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  345. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_runner_throttle.py +0 -0
  346. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_scaffold.py +0 -0
  347. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_schedule.py +0 -0
  348. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_cmd_bounded.py +0 -0
  349. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_config_broken.py +0 -0
  350. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_crash_loop.py +0 -0
  351. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_mem_gate.py +0 -0
  352. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_phase_schedule.py +0 -0
  353. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_policy.py +0 -0
  354. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round.py +0 -0
  355. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  356. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_log.py +0 -0
  357. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_oom_killed.py +0 -0
  358. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_terminate.py +0 -0
  359. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_schedule_gate.py +0 -0
  360. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_sentinel.py +0 -0
  361. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_startup_hooks.py +0 -0
  362. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_service_mode.py +0 -0
  363. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_service_unit.py +0 -0
  364. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_startup_battery_exit.py +0 -0
  365. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_substrate.py +0 -0
  366. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  367. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  368. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_epoch_bound.py +0 -0
  369. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_plugin_classification.py +0 -0
  370. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_upgrade_migrate.py +0 -0
  371. {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_vcs_state.py +0 -0
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -18,7 +18,7 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  timeout-minutes: 2
20
20
  steps:
21
- - uses: actions/checkout@v5
21
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
22
22
  with:
23
23
  # PR: scan only new commits the PR introduces (base..head).
24
24
  # Push to main: scan the just-pushed commits (before..after).
@@ -74,10 +74,10 @@ jobs:
74
74
  # granularity AND because coverage requires extra pytest flags not in
75
75
  # the local `test` task. CONTRIBUTING.md states the parity intentionally.
76
76
  steps:
77
- - uses: actions/checkout@v5
77
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
78
78
  with:
79
79
  fetch-depth: 0
80
- - uses: actions/setup-python@v6
80
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
81
81
  with:
82
82
  python-version: ${{ matrix.python }}
83
83
  cache: pip
@@ -108,7 +108,7 @@ jobs:
108
108
  run: pytest tests/literate/ -q
109
109
  - name: upload coverage
110
110
  if: matrix.python == '3.12' && matrix.os == 'ubuntu-latest'
111
- uses: codecov/codecov-action@v6
111
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
112
112
  with:
113
113
  files: ./coverage.xml
114
114
  fail_ci_if_error: false
@@ -26,10 +26,10 @@ jobs:
26
26
  runs-on: ubuntu-latest
27
27
  timeout-minutes: 10
28
28
  steps:
29
- - uses: actions/checkout@v5
29
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
30
30
  with:
31
31
  fetch-depth: 0
32
- - uses: actions/setup-python@v6
32
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
33
33
  with:
34
34
  python-version: "3.12"
35
35
  - name: install build
@@ -38,7 +38,7 @@ jobs:
38
38
  run: python -m build
39
39
  - name: list dist artifacts
40
40
  run: ls -la dist/
41
- - uses: actions/upload-artifact@v7
41
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
42
42
  with:
43
43
  name: dist
44
44
  path: dist/
@@ -53,11 +53,15 @@ jobs:
53
53
  permissions:
54
54
  id-token: write
55
55
  steps:
56
- - uses: actions/download-artifact@v8
56
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
57
57
  with:
58
58
  name: dist
59
59
  path: dist/
60
- - uses: pypa/gh-action-pypi-publish@release/v1
60
+ # SHA-pinned to release/v1's composite. NOTE: this pins the composite source;
61
+ # the image it pulls is still referenced by a moving ghcr tag, so a full pin
62
+ # would require the ghcr digest — deferred (needs read:packages + release-time
63
+ # validation). dependabot (github-actions) bumps this SHA.
64
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
61
65
  with:
62
66
  attestations: true
63
67
 
@@ -68,8 +72,8 @@ jobs:
68
72
  permissions:
69
73
  contents: write
70
74
  steps:
71
- - uses: actions/checkout@v5
72
- - uses: actions/download-artifact@v8
75
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
76
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
73
77
  with:
74
78
  name: dist
75
79
  path: dist/
@@ -110,6 +110,7 @@ pid
110
110
  uptime_s
111
111
  unit_file
112
112
  rate_limit
113
+ system_managed
113
114
 
114
115
  # agent_runner.api_types.SystemMetrics
115
116
  mem_total_mb
@@ -53,18 +53,6 @@ kind name is the version discriminator; a breaking change ships as
53
53
  `round_start_v2`). Reaching this failure requires a contract violation — but the
54
54
  suite would not report it.
55
55
 
56
- ## `remote_failure_tolerance_s` does not cover `subprocess.TimeoutExpired` — NEEDS_DESIGN
57
-
58
- The tolerance window covers ssh exit code 255 only. `monitor.run_remote_command`
59
- raises `MonitorRemoteError` on rc=255, and `api.monitor_loop`'s retry loop
60
- catches exactly that. A hung (not failed) ssh connection instead trips the
61
- `subprocess.run(timeout=...)` guard and raises `subprocess.TimeoutExpired`,
62
- which bypasses the window and kills the monitor.
63
-
64
- The rc=255-only scope is intentional and pinned by the monitor-signals design,
65
- and the documentation matches the code exactly — there is no drift here. Widening
66
- it changes a documented recovery path, so it is a design decision, not debt.
67
-
68
56
  ## `anomaly_repetitive_*` default — NEEDS_DESIGN
69
57
 
70
58
  `monitor.anomaly_repetitive_window` and `anomaly_repetitive_threshold` both
@@ -89,21 +77,6 @@ Deferred because closing it is a naming decision, not cleanup: no event kind
89
77
  carries the meaning "stashed but ref lost" (`orphan_stash_failed` would be wrong
90
78
  — the stash exists). Left as-is until that kind is designed.
91
79
 
92
- ## `_monitor_loop_iter`'s three fail-open guards are hand-duplicated
93
-
94
- `api._monitor_loop_iter` wraps the startup `monitor_started` emit, the
95
- per-tick poll, and `on_alert`'s per-alert dispatch each in its own
96
- `try/except Exception: warnings.warn(...)` block so a crash in any one of the
97
- three never ends monitor supervision. The three guards are structurally
98
- identical (catch, format a `type(e).__name__: {e}` warning, keep the loop
99
- alive) but hand-copied rather than sharing one implementation; `on_alert`'s
100
- own `emit()` closure already shows the pattern for factoring a shared guard.
101
-
102
- Collapsing them to one shared guard is behavior-preserving — it must not
103
- change which failures warn vs which propagate, nor the specific fallback
104
- each site takes on failure (sleep-and-retry after a poll failure,
105
- `verdict = "failed"` after an `on_alert` failure). Slated for 0.2.19.
106
-
107
80
  ## `oom_kill_delta` is named differently on its two events — NEEDS_DESIGN, DEFERRED to 0.3
108
81
 
109
82
  The same cgroup `memory.events.oom_kill` delta is emitted under two
@@ -5,6 +5,19 @@ 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.22] - 2026-09-11
9
+
10
+ ### Fixed
11
+ - Container safety: agent-runner now refuses to boot when `runtime.log_dir` resolves inside a containerized `work_dir` — a contained agent could otherwise rewrite the pid/lock/cidfile control files the supervisor trusts. The best-effort container stop also validates the recovered container id (hex, length-bounded) and passes an end-of-options separator, and the injected `--cidfile` moves to a host-private directory, so a poisoned cidfile can no longer stop an unrelated container.
12
+ - `kill` / `restart --force` verify the lock-holder's start time before signaling, so a recycled pid is never sent a signal.
13
+ - System mode (`install --system`): `restart` / `uninstall` recognize a system unit and refuse with the exact `sudo systemctl …` command instead of a misleading one, and `uninstall` no longer aborts with an uncaught error on a host without a systemd user bus.
14
+
15
+ ### Security
16
+ - Release and CI GitHub Actions are pinned to commit SHAs, with Dependabot tracking updates.
17
+
18
+ ### Notes
19
+ - Not breaking: no `agent-runner migrate` step, no systemd unit change. `status --json` gains a `system_managed` field; existing fields unchanged. The one newly-rejected configuration is a containerized agent whose `runtime.log_dir` sits inside `work_dir` — move `log_dir` outside `work_dir` (the default already is).
20
+
8
21
  ## [0.2.21] - 2026-09-11
9
22
 
10
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.21
3
+ Version: 0.2.22
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
@@ -57,6 +57,23 @@ a narrower safety net than the host-level reaping a bare agent process gets,
57
57
  not a wider one — keep running it as a dedicated unprivileged user with no
58
58
  passwordless sudo and tight egress limits regardless.
59
59
 
60
+ **Residual: `agent-runner.toml` lives inside the mount.** The startup check
61
+ above stops the agent's pid and lock files from sitting inside a mounted
62
+ `work_dir` (the injected cidfile is separately placed in a host-private temp
63
+ dir outside any mount, so it's protected independently of `log_dir`) — but
64
+ `agent-runner.toml` itself is scaffolded at the `work_dir` root, and the
65
+ container recipes in
66
+ [docs/recipes/container-pi.md](docs/recipes/container-pi.md) bind-mount all
67
+ of `work_dir` read-write. Each round re-reads the config, so a containerized
68
+ agent that can write its own mount can rewrite `[agent] command` /
69
+ `exec_prefix` — or `runtime.stop_file`, if it's pointed inside the mount —
70
+ and have it run on the host next round. This is consistent with the threat
71
+ model above: agent-runner bounds a *misbehaving* agent, not a *hostile* one,
72
+ and containment is the container/VM's job, not agent-runner's. An operator
73
+ who needs to contain a not-fully-trusted agent should keep the config (and
74
+ `runtime.stop_file`) outside any bind-mount, or otherwise treat the
75
+ container boundary as their own responsibility to shape.
76
+
60
77
  ## Scope
61
78
 
62
79
  In scope: the `agent_runner` Python package, its CLI, its bundled systemd
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.21'
22
- __version_tuple__ = version_tuple = (0, 2, 21)
21
+ __version__ = version = '0.2.22'
22
+ __version_tuple__ = version_tuple = (0, 2, 22)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -14,6 +14,7 @@ import re
14
14
  import shutil
15
15
  import signal
16
16
  import subprocess # noqa: TID251 — sanctioned subprocess caller
17
+ import tempfile
17
18
  import threading
18
19
  from collections.abc import Callable
19
20
  from dataclasses import dataclass
@@ -290,6 +291,14 @@ def resolve_exec_target(command0: str, work_dir: Path, env_path: str | None = No
290
291
  # ladder) is out of scope here -- left for a future release.
291
292
  _CONTAINER_RUNTIMES = frozenset({"docker", "podman"})
292
293
 
294
+ # A real docker/podman container id is hex, 12-64 chars (short or full form).
295
+ # The cidfile is a trust-bearing control input -- validated before it's ever
296
+ # handed to a `stop` subprocess argv.
297
+ _CONTAINER_ID_RE = re.compile(r"^[0-9a-fA-F]{12,64}$")
298
+ # A real container id is <=64 hex chars; cap the read so a poisoned giant
299
+ # file can't OOM a constrained host.
300
+ _CIDFILE_READ_CAP = 128
301
+
293
302
  # docker/podman GLOBAL flags (before the subcommand) that take a separate
294
303
  # value token -- skipped in pairs so `docker -H unix:///var/run/docker.sock
295
304
  # run ...` / `docker --context foo run ...` still resolve to `run`. Not
@@ -507,14 +516,18 @@ def _best_effort_container_stop(
507
516
  if cidfile is None:
508
517
  return None, None
509
518
  try:
510
- cid = cidfile.read_text(encoding="utf-8").strip()
519
+ with cidfile.open("rb") as fh:
520
+ raw = fh.read(_CIDFILE_READ_CAP + 1)
511
521
  except OSError:
512
522
  return None, None
513
- if not cid:
514
- return None, None
523
+ if len(raw) > _CIDFILE_READ_CAP:
524
+ return None, None # oversize — not a real container id
525
+ cid = raw.decode("utf-8", "replace").strip()
526
+ if not _CONTAINER_ID_RE.match(cid):
527
+ return None, None # not a hex id — detection already fired the loud warn + event
515
528
  try:
516
529
  result = subprocess.run(
517
- [runtime, "stop", "-t", str(REAP_GRACE_S), cid],
530
+ [runtime, "stop", "-t", str(REAP_GRACE_S), "--", cid],
518
531
  stdout=subprocess.DEVNULL,
519
532
  stderr=subprocess.PIPE,
520
533
  timeout=_CONTAINER_STOP_TIMEOUT_S,
@@ -606,6 +619,11 @@ def run(
606
619
  attempt where covered; it just isn't reported).
607
620
  """
608
621
  stdin_mode = prompt_delivery == "stdin"
622
+ # Raising work (mkdir can fail on disk-full/perms/removed log_dir) must
623
+ # happen BEFORE the container-detection block below creates the
624
+ # host-private cidfile tempdir -- otherwise a raise here would skip the
625
+ # try/finally that cleans that tempdir up, leaking it once per round.
626
+ log_path.parent.mkdir(parents=True, exist_ok=True)
609
627
  # Container-orphan defense: detect BEFORE building argv (spawn_command may
610
628
  # gain an injected --cidfile) -- see _detect_container_run's module-level
611
629
  # note. A non-container command's argv is byte-identical to before:
@@ -638,8 +656,13 @@ def run(
638
656
  # no-silent-orphan floor.
639
657
  pass
640
658
  else:
641
- container_cidfile = log_path.with_name(log_path.name + ".cid")
642
- container_cidfile.unlink(missing_ok=True) # docker/podman refuse an existing one
659
+ # A supervisor control file must not live where the container can
660
+ # write it; put our injected cidfile in a host-private 0700 dir
661
+ # outside any mount. Docker writes the file host-side; the
662
+ # container never needs it.
663
+ container_cid_dir = Path(tempfile.mkdtemp(prefix="agent-runner-cid-"))
664
+ # Not created here — docker/podman refuse an existing cidfile.
665
+ container_cidfile = container_cid_dir / "round.cid"
643
666
  container_cidfile_is_ours = True
644
667
  spawn_command = _inject_cidfile(command, run_idx, container_cidfile)
645
668
 
@@ -669,7 +692,6 @@ def run(
669
692
  # PWD pinned last — it mirrors cwd= (a correctness pin, not a knob), so
670
693
  # an [agent.env] PWD cannot silently diverge from where the child runs.
671
694
  env = {**os.environ, **env_extra, "PWD": str(work_dir)}
672
- log_path.parent.mkdir(parents=True, exist_ok=True)
673
695
  proc: subprocess.Popen | None = None
674
696
  log_file = None
675
697
  try:
@@ -805,10 +827,10 @@ def run(
805
827
  finally:
806
828
  if log_file is not None:
807
829
  log_file.close()
808
- # Cleanup, not correctness: our own injected cidfile is a tiny,
809
- # round-scoped tmp file -- delete it once the round is fully done
810
- # (read-back for the best-effort stop, if any, already happened
811
- # above). Only ours: an operator-supplied --cidfile is never ours to
812
- # delete.
830
+ # Cleanup, not correctness: our own injected cidfile lives in a tiny,
831
+ # round-scoped private tempdir -- remove that whole dir once the round
832
+ # is fully done (read-back for the best-effort stop, if any, already
833
+ # happened above). Only ours: an operator-supplied --cidfile is never
834
+ # ours to delete.
813
835
  if container_cidfile_is_ours and container_cidfile is not None:
814
- container_cidfile.unlink(missing_ok=True)
836
+ shutil.rmtree(container_cidfile.parent, ignore_errors=True)
@@ -193,6 +193,11 @@ def init(
193
193
  _SYSTEM_UNITS_DIR = Path("/etc/systemd/system")
194
194
 
195
195
 
196
+ def _system_unit_exists(project: str) -> bool:
197
+ """A --system install's root-owned unit; detect_service_mode can't see it (user-scope only)."""
198
+ return (_SYSTEM_UNITS_DIR / serve_unit_filename(project)).exists()
199
+
200
+
196
201
  def _install_system(
197
202
  cfg: Config, project: str, *, config_path: Path, with_monitor: bool
198
203
  ) -> InstallResult:
@@ -283,6 +288,15 @@ def uninstall(work_dir: Path | None = None) -> bool:
283
288
  if work_dir is None:
284
289
  work_dir = Path.cwd()
285
290
  project = _project_name(work_dir)
291
+ if _system_unit_exists(project): # root-owned -- don't silently no-op the user-scope dir
292
+ units = [serve_unit_filename(project)]
293
+ if (_SYSTEM_UNITS_DIR / monitor_unit_filename(project)).exists():
294
+ units.append(monitor_unit_filename(project))
295
+ remedy = " && ".join(
296
+ f"sudo systemctl disable --now {u} && sudo rm {_SYSTEM_UNITS_DIR / u}" for u in units
297
+ )
298
+ print(f"{project} is managed by a system unit; run: {remedy}")
299
+ return False
286
300
  units_dir = lifecycle._user_systemd_dir()
287
301
  serve = units_dir / serve_unit_filename(project)
288
302
  monitor = units_dir / monitor_unit_filename(project)
@@ -295,7 +309,10 @@ def uninstall(work_dir: Path | None = None) -> bool:
295
309
  lifecycle.stop_unit_draining(p.name, clock=SYSTEM_CLOCK, confirm_s=_PID_SIGNAL_GRACE_S)
296
310
  _systemctl_user("disable", p.name)
297
311
  p.unlink(missing_ok=True)
298
- _systemctl_user("daemon-reload")
312
+ try:
313
+ _systemctl_user("daemon-reload")
314
+ except (subprocess.CalledProcessError, FileNotFoundError):
315
+ pass # no user bus on this host -- units above (if any) already gone
299
316
  return True
300
317
 
301
318
 
@@ -340,6 +357,8 @@ def _round_holder_pid(log_dir: Path) -> int | None:
340
357
  alive. This is the ONLY way ``kill()`` can reach an in-flight round: the
341
358
  round is ``start_new_session=True`` (its own session, its own pgid), so it
342
359
  sits outside whatever process group serve itself belongs to."""
360
+ import psutil
361
+
343
362
  from agent_runner.context_store import read_json
344
363
 
345
364
  data = read_json(log_dir / "agent-runner.lock.holder")
@@ -348,7 +367,17 @@ def _round_holder_pid(log_dir: Path) -> int | None:
348
367
  pid = data.get("pid")
349
368
  if not lifecycle._valid_pid(pid):
350
369
  return None
351
- return pid if pid_alive(pid) else None
370
+ if not pid_alive(pid):
371
+ return None
372
+ 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
352
381
 
353
382
 
354
383
  def _terminate_round_pid(pid: int) -> None:
@@ -435,6 +464,11 @@ def restart(project: str | Path, *, force: bool = False) -> ServiceStatus:
435
464
  # SYSTEMD_USER unit, so restarting a PID_FILE/NONE service would stop it and
436
465
  # never bring it back — the half-execution this fix eliminates.
437
466
  pname = _resolve_project(project)
467
+ if _system_unit_exists(pname): # root-owned -- below's message would mislead
468
+ raise RuntimeError(
469
+ f"{pname} is managed by a system unit; run: "
470
+ f"sudo systemctl restart {serve_unit_filename(pname)}"
471
+ )
438
472
  log_dir = _log_dir_for_project(project)
439
473
  mode = detect_service_mode(pname, log_dir=log_dir)
440
474
  if mode != ServiceMode.SYSTEMD_USER:
@@ -463,14 +497,18 @@ def status(project: str | Path) -> ServiceStatus:
463
497
  pname = _resolve_project(project)
464
498
  log_dir = _log_dir_for_project(project)
465
499
  mode = detect_service_mode(pname, log_dir=log_dir)
466
- if mode == ServiceMode.PID_FILE:
467
- pid = PIDFile(log_dir / "serve.pid").read()
468
- return ServiceStatus(mode=mode, active=pid is not None and pid_alive(pid), pid=pid)
469
500
  if mode == ServiceMode.SYSTEMD_USER:
470
501
  unit = lifecycle._user_systemd_dir() / serve_unit_filename(pname)
471
502
  active = _systemd_active(serve_unit_filename(pname), log_dir)
472
503
  return ServiceStatus(mode=mode, active=active, unit_file=unit)
473
- return ServiceStatus(mode=ServiceMode.NONE, active=False)
504
+ # detect_service_mode only sees the user-scope unit dir; label a --system
505
+ # install found here (PID_FILE/NONE) via system_managed, not unit_file.
506
+ system_managed = _system_unit_exists(pname)
507
+ if mode == ServiceMode.PID_FILE:
508
+ pid = PIDFile(log_dir / "serve.pid").read()
509
+ active = pid is not None and pid_alive(pid)
510
+ return ServiceStatus(mode=mode, active=active, pid=pid, system_managed=system_managed)
511
+ return ServiceStatus(mode=ServiceMode.NONE, active=False, system_managed=system_managed)
474
512
 
475
513
 
476
514
  def _resolve_target(project: str | Path | None) -> tuple[str, Path]:
@@ -62,6 +62,11 @@ class ServiceStatus:
62
62
  uptime_s: float | None = None
63
63
  unit_file: Path | None = None
64
64
  rate_limit: RateLimitState | None = None
65
+ # True when a `--system` install's root-owned unit owns this project but
66
+ # detect_service_mode fell through to PID_FILE/NONE (it only sees the
67
+ # user-scope unit dir) -- distinct from `unit_file`, which stays the
68
+ # live, user-owned, CLI-restartable systemd_user unit's path.
69
+ system_managed: bool = False
65
70
 
66
71
 
67
72
  @dataclass(frozen=True)
@@ -14,7 +14,7 @@ from dataclasses import dataclass
14
14
  from pathlib import Path
15
15
  from typing import Any
16
16
 
17
- from agent_runner import events
17
+ from agent_runner import events, startup_check
18
18
  from agent_runner.config import Config
19
19
 
20
20
 
@@ -29,6 +29,7 @@ class Defense:
29
29
 
30
30
  def catalog(cfg: Config) -> list[Defense]:
31
31
  """Return the defense catalog parameterised by current config."""
32
+ check_kinds = startup_check.all_check_kinds()
32
33
  return [
33
34
  Defense(
34
35
  name="round_timeout_s",
@@ -89,7 +90,10 @@ def catalog(cfg: Config) -> list[Defense]:
89
90
  ),
90
91
  Defense(
91
92
  name="startup_smoke_check",
92
- value="6 checks (config / log_dir / agent_cli / git / prompt_file / prompt_smoke)",
93
+ # Computed from startup_check.all_check_kinds() -- the battery's
94
+ # own SSOT -- so this count/list can't drift from the real boot
95
+ # battery the way the hand-written "6 checks" string once did.
96
+ value=f"{len(check_kinds)} checks ({' / '.join(check_kinds)})",
93
97
  codifies=(
94
98
  "R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; "
95
99
  "now halts serve (config_broken) instead of respawning a broken config"
@@ -78,6 +78,12 @@ def _write_holder_sidecar(lock_path: Path) -> None:
78
78
  "started_at": now_iso_ms(),
79
79
  "cmdline": _read_cmdline(os.getpid()),
80
80
  }
81
+ import psutil
82
+
83
+ try:
84
+ payload["create_time"] = psutil.Process(os.getpid()).create_time()
85
+ except psutil.Error:
86
+ pass
81
87
  _holder_sidecar(lock_path).write_text(json.dumps(payload), encoding="utf-8")
82
88
 
83
89
 
@@ -106,6 +106,33 @@ def _check_stdin_container_interactive(
106
106
  )
107
107
 
108
108
 
109
+ def _check_control_plane_outside_container(
110
+ agent: AgentConfig, work_dir: Path, log_dir: Path, name: str
111
+ ) -> CheckResult:
112
+ """The supervisor's per-round pid and lock-holder files live under
113
+ log_dir. When the agent runs in a container and log_dir resolves inside
114
+ work_dir -- which the container mounts -- the agent could forge them;
115
+ refuse at boot. (The injected cidfile is kept safe separately, in a
116
+ host-private temp dir outside any mount.) Enforced on work_dir (the mount
117
+ we can reason about); exec_prefix's other mounts are opaque, so the
118
+ general rule is documented, not code-enforced."""
119
+ if agent_runtime._detect_container_run(agent.spawn_command(work_dir)) is None:
120
+ return CheckResult(name, True)
121
+ if log_dir.is_relative_to(work_dir):
122
+ return CheckResult(
123
+ name,
124
+ False,
125
+ reason="runtime.log_dir is inside work_dir, which the container mounts — "
126
+ "the agent could forge the pid/lock control files agent-runner trusts",
127
+ how_to_fix="set runtime.log_dir OUTSIDE work_dir (the default "
128
+ "~/.agent-runner/<project>/logs already is); agent-runner's "
129
+ "per-round pid and lock files live under log_dir, so keeping "
130
+ "log_dir outside the mount keeps them out of the agent's reach",
131
+ permanent=True,
132
+ )
133
+ return CheckResult(name, True)
134
+
135
+
109
136
  def _check_work_dir_is_git(cfg: Config) -> CheckResult:
110
137
  from agent_runner.vcs_state import GitTimeout, is_git_repo
111
138
 
@@ -205,6 +232,11 @@ CHECKS: list[Callable[[Config], CheckResult]] = [
205
232
  ]
206
233
 
207
234
 
235
+ def _phase_qualified(base: str, phase: str | None) -> str:
236
+ """A check's name, suffixed `:<phase>` for an overriding phase profile."""
237
+ return base if phase is None else f"{base}:{phase}"
238
+
239
+
208
240
  def _agent_cli_checks(cfg: Config) -> list[CheckResult]:
209
241
  """Validate ``command[0]`` for EVERY profile the runner might launch — the
210
242
  base agent plus each phase's own agent — so a bad phase agent fails at boot
@@ -223,15 +255,24 @@ def _agent_cli_checks(cfg: Config) -> list[CheckResult]:
223
255
  results: list[CheckResult] = []
224
256
  for phase in [None, *overriding]:
225
257
  profile = cfg.profile_for(phase)
226
- name = "agent_cli_in_path" if phase is None else f"agent_cli_in_path:{phase}"
227
- results.append(_check_agent_target(profile.agent, cfg.runtime.work_dir, name))
228
- stdin_name = (
229
- "stdin_container_interactive"
230
- if phase is None
231
- else f"stdin_container_interactive:{phase}"
258
+ work_dir = cfg.runtime.work_dir
259
+ results.append(
260
+ _check_agent_target(
261
+ profile.agent, work_dir, _phase_qualified("agent_cli_in_path", phase)
262
+ )
263
+ )
264
+ results.append(
265
+ _check_stdin_container_interactive(
266
+ profile.agent, work_dir, _phase_qualified("stdin_container_interactive", phase)
267
+ )
232
268
  )
233
269
  results.append(
234
- _check_stdin_container_interactive(profile.agent, cfg.runtime.work_dir, stdin_name)
270
+ _check_control_plane_outside_container(
271
+ profile.agent,
272
+ work_dir,
273
+ cfg.runtime.log_dir,
274
+ _phase_qualified("control_plane_outside_container", phase),
275
+ )
235
276
  )
236
277
  return results
237
278
 
@@ -254,6 +295,33 @@ def _phase_prompt_checks(cfg: Config) -> list[CheckResult]:
254
295
  return results
255
296
 
256
297
 
298
+ def all_check_kinds() -> tuple[str, ...]:
299
+ """Static kind-names ``run_battery`` can emit, ``:<phase>`` suffix
300
+ stripped: the 5 base ``CHECKS`` names plus the 3 per-profile kinds
301
+ ``_agent_cli_checks`` adds (``agent_cli_in_path``,
302
+ ``stdin_container_interactive``, ``control_plane_outside_container``).
303
+ Per-phase prompt checks (``_phase_prompt_checks``) reuse
304
+ ``prompt_smoke_passes`` under a ``:<phase>`` suffix -- the same kind,
305
+ not a new one, so it is not listed twice.
306
+
307
+ Single source of truth for the defenses catalog's ``startup_smoke_check``
308
+ entry (see ``defenses.py``), so the human-readable count there is
309
+ computed from this instead of hand-copied and drifting; pinned against
310
+ the real battery by
311
+ ``test_all_check_kinds_should_match_battery_kinds_when_config_is_valid``.
312
+ """
313
+ return (
314
+ "config_loaded",
315
+ "log_dir_writable",
316
+ "work_dir_is_git_repo",
317
+ "prompt_file_exists",
318
+ "prompt_smoke_passes",
319
+ "agent_cli_in_path",
320
+ "stdin_container_interactive",
321
+ "control_plane_outside_container",
322
+ )
323
+
324
+
257
325
  def run_battery(cfg: Config) -> list[CheckResult]:
258
326
  """Run all checks. Returns empty list if escape hatch env is set."""
259
327
  if os.environ.get(ESCAPE_HATCH_ENV, "").lower() in ("1", "true", "yes", "on"):
@@ -65,11 +65,20 @@ Generate and install systemd user unit(s):
65
65
  - `--force`: overwrite a same-basename sibling project's unit (the clobber guard
66
66
  otherwise refuses, e.g. after moving/renaming a project directory).
67
67
 
68
- After writing, runs `systemctl --user daemon-reload`, `enable`, `start`.
68
+ For the default (non-`--system`) path, after writing the unit(s) it runs
69
+ `systemctl --user daemon-reload`, `enable`, `start`. The `--system` path runs
70
+ the system-level `daemon-reload` and `enable` only, as noted above — it does
71
+ not start the unit.
69
72
 
70
73
  ### `agent-runner uninstall`
71
74
 
72
- Stops and disables both units, then deletes the unit files and reloads systemd.
75
+ Stops and disables both user units, then deletes the unit files and reloads
76
+ systemd. A `--system`-installed unit is root-owned and out of reach here: this
77
+ refuses instead, printing the `sudo systemctl disable --now ...` command for
78
+ the serve unit (and the monitor unit too, if `--system --monitor` installed
79
+ one) to run in its place. Never raises for a missing user bus either (the
80
+ dietpi/RPi hosts `--system` is recommended for) — a failed `daemon-reload`
81
+ there is harmless once the units are already gone.
73
82
 
74
83
  ### `agent-runner start | stop | kill | restart | status`
75
84
 
@@ -79,6 +88,11 @@ there:
79
88
  - `start` is idempotent.
80
89
  - `stop` waits up to `round_timeout_s` for the current round.
81
90
  - `kill` (and `restart --force`) is for a stuck round only — 5s grace then SIGKILL.
91
+ - `restart` against a `--system`-installed unit refuses, printing the
92
+ `sudo systemctl restart ...` command to run instead (it manages user-scope
93
+ units only). `status` still reports it correctly — a `--system` install
94
+ sets `system_managed: true` on the result rather than misreporting it as a
95
+ `systemd_user`-mode service.
82
96
 
83
97
  ### `agent-runner round`
84
98
 
@@ -87,6 +87,22 @@ dirty-tree detector every round:
87
87
  -v /home/you/.pi-container-auth:/home/agent/.pi/agent:ro
88
88
  ```
89
89
 
90
+ ## Residual: the config file is inside the mount
91
+
92
+ `-v {work_dir}:/work:Z` above mounts all of `work_dir` read-write, and
93
+ `agent-runner.toml` is scaffolded at the `work_dir` root — so it's inside
94
+ that mount too. agent-runner's own startup check keeps its pid and lock
95
+ files out of a mounted `work_dir` (they live under `runtime.log_dir`,
96
+ required to sit outside it — the injected cidfile is separately placed in a
97
+ host-private temp dir outside any mount, protected independently of
98
+ `log_dir`), but it does not and cannot protect the config file the same way:
99
+ a containerized agent that can write
100
+ its own mount can rewrite `[agent] command` / `exec_prefix` and have it run
101
+ on the host next round. This is a residual of the supervisor-not-sandbox
102
+ model (see [SECURITY.md](../../SECURITY.md#containment)), not something this
103
+ recipe or agent-runner defends against — if you don't fully trust the agent,
104
+ keep the config (and `runtime.stop_file`, if set) outside any bind-mount.
105
+
90
106
  ## Footguns
91
107
 
92
108
  These aren't enforced by agent-runner (`exec_prefix` is opaque) — get them
@@ -729,11 +729,13 @@ ssh -L 8765:127.0.0.1:8765 <pi-host>
729
729
  ### Going truly 24×7 (systemd)
730
730
 
731
731
  ```bash
732
- agent-runner install --config <toml>
733
- systemctl --user start agent-runner@<project>
734
- systemctl --user enable agent-runner@<project> # restart on Pi reboot
732
+ agent-runner install --monitor --config <toml>
735
733
  ```
736
734
 
735
+ `install` already enables, starts, and (with `--monitor`) installs the
736
+ auto-stop sidekick in one step — no separate `systemctl --user start/enable`
737
+ needed.
738
+
737
739
  ## Supervision coverage on a 24/7 host
738
740
 
739
741
  Check this table before wrapping `[agent] command` in a shell script — most