cli-agent-runner 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.github/workflows/release.yml +7 -3
  2. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.vulture-whitelist.py +4 -3
  3. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/CHANGELOG.md +28 -1
  4. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/PKG-INFO +21 -10
  5. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/README.md +19 -8
  6. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/_emit.py +37 -0
  7. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/_version.py +2 -2
  8. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/api.py +52 -74
  9. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/events_cmd.py +126 -3
  10. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/monitor_cmd.py +61 -14
  11. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/serve_cmd.py +21 -1
  12. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/config.py +6 -5
  13. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/defenses.py +11 -0
  14. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/events.py +1 -0
  15. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/monitor.py +40 -115
  16. cli_agent_runner-0.2.6/agent_runner/remote_relay.py +270 -0
  17. cli_agent_runner-0.2.6/agent_runner/round_log.py +160 -0
  18. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/runner.py +11 -2
  19. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/architecture.md +36 -18
  20. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/commands.md +53 -9
  21. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/configuration.md +64 -1
  22. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/events.md +3 -3
  23. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.28.md +2 -2
  24. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.30.md +3 -3
  25. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.35.md +1 -0
  26. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.2.0.md +2 -3
  27. cli_agent_runner-0.2.6/docs/migrations/0.2.5.md +99 -0
  28. cli_agent_runner-0.2.6/docs/migrations/0.2.6.md +127 -0
  29. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/plugins.md +28 -15
  30. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/recipes/aider.md +6 -5
  31. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/runbook.md +133 -27
  32. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/thesis.md +4 -3
  33. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/pyproject.toml +3 -2
  34. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_monitor_seeded.py +2 -2
  35. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_architecture.py +1 -0
  36. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_module_boundaries.py +1 -0
  37. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_observation.py +8 -187
  38. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_service.py +2 -2
  39. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_auto_stop_gating.py +1 -5
  40. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_cli_service_peek_monitor.py +79 -56
  41. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_config.py +52 -3
  42. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_defenses.py +2 -2
  43. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_events.py +28 -0
  44. cli_agent_runner-0.2.6/tests/unit/test_events_cmd.py +343 -0
  45. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_events_cmd_tail.py +39 -2
  46. cli_agent_runner-0.2.6/tests/unit/test_monitor_remote.py +320 -0
  47. cli_agent_runner-0.2.6/tests/unit/test_round_log_helpers.py +269 -0
  48. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_runner.py +67 -0
  49. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_serve_round_log.py +99 -24
  50. cli_agent_runner-0.2.4/agent_runner/round_log.py +0 -95
  51. cli_agent_runner-0.2.4/tests/unit/test_events_cmd.py +0 -179
  52. cli_agent_runner-0.2.4/tests/unit/test_monitor_remote.py +0 -176
  53. cli_agent_runner-0.2.4/tests/unit/test_round_log_helpers.py +0 -143
  54. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.codecov.yml +0 -0
  55. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  56. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  57. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  58. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  59. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.github/workflows/ci.yml +0 -0
  60. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/.gitignore +0 -0
  61. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/BACKLOG.md +0 -0
  62. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/CODE_OF_CONDUCT.md +0 -0
  63. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/CONTRIBUTING.md +0 -0
  64. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/LICENSE +0 -0
  65. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/README.zh.md +0 -0
  66. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/SECURITY.md +0 -0
  67. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/__init__.py +0 -0
  68. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/_docgen.py +0 -0
  69. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/_redact.py +0 -0
  70. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/_registry.py +0 -0
  71. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/_substrate.py +0 -0
  72. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/_throttle.py +0 -0
  73. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/agent_runtime.py +0 -0
  74. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/api_types.py +0 -0
  75. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/__init__.py +0 -0
  76. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/_constants.py +0 -0
  77. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  78. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/codewhale.py +0 -0
  79. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  80. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/gemini.py +0 -0
  81. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/kimi.py +0 -0
  82. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/pi.py +0 -0
  83. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/__init__.py +0 -0
  84. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/__main__.py +0 -0
  85. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/common.py +0 -0
  86. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/init_cmd.py +0 -0
  87. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/install_cmd.py +0 -0
  88. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/peek_cmd.py +0 -0
  89. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/round_cmd.py +0 -0
  90. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/service_cmd.py +0 -0
  91. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/cli/upgrade_cmd.py +0 -0
  92. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/context_store.py +0 -0
  93. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/hooks.py +0 -0
  94. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/http_progress.py +0 -0
  95. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/lifecycle.py +0 -0
  96. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/metrics.py +0 -0
  97. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/presets/__init__.py +0 -0
  98. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/presets/aider.toml +0 -0
  99. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/presets/claude.toml +0 -0
  100. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/presets/codewhale.toml +0 -0
  101. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/presets/gemini.toml +0 -0
  102. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/presets/kimi.toml +0 -0
  103. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/presets/pi.toml +0 -0
  104. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/prompt_loader.py +0 -0
  105. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/round_view.py +0 -0
  106. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/scaffold.py +0 -0
  107. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/service_unit.py +0 -0
  108. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/startup_check.py +0 -0
  109. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/agent_runner/vcs_state.py +0 -0
  110. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/build.sh +0 -0
  111. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/README.md +0 -0
  112. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/long-running-agents.md +0 -0
  113. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.16.md +0 -0
  114. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.17.md +0 -0
  115. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.19.md +0 -0
  116. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.20.md +0 -0
  117. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.21.md +0 -0
  118. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.22.md +0 -0
  119. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.23.md +0 -0
  120. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.24.md +0 -0
  121. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.25.md +0 -0
  122. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.26.md +0 -0
  123. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.27.md +0 -0
  124. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.29.md +0 -0
  125. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.31.md +0 -0
  126. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.32.md +0 -0
  127. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.33.md +0 -0
  128. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.34.md +0 -0
  129. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.36.md +0 -0
  130. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.37.md +0 -0
  131. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.38.md +0 -0
  132. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.39.md +0 -0
  133. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.40.md +0 -0
  134. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.1.42.md +0 -0
  135. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.2.1.md +0 -0
  136. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.2.2.md +0 -0
  137. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.2.3.md +0 -0
  138. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/migrations/0.2.4.md +0 -0
  139. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/quickstart.md +0 -0
  140. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/recipes/codewhale.md +0 -0
  141. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/recipes/kimi.md +0 -0
  142. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/docs/recipes/pi.md +0 -0
  143. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/__init__.py +0 -0
  144. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/_test_helpers.py +0 -0
  145. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/conftest.py +0 -0
  146. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/contract/__init__.py +0 -0
  147. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/contract/test_public_api_surface.py +0 -0
  148. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/e2e/__init__.py +0 -0
  149. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/e2e/conftest.py +0 -0
  150. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  151. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_install_systemd.py +0 -0
  152. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  153. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  154. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  155. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  156. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  157. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/generate_vulture_whitelist.py +0 -0
  158. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/__init__.py +0 -0
  159. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_bounded_run.py +0 -0
  160. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_context_enricher_namespacing.py +0 -0
  161. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_dirty_handler_seam.py +0 -0
  162. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_fresh_eyes_signal.py +0 -0
  163. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_grace_kill_emission.py +0 -0
  164. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_install_dry_run.py +0 -0
  165. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_detector_loaded.py +0 -0
  166. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_owned_paths.py +0 -0
  167. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_real_flow.py +0 -0
  168. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  169. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  170. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_scaffold_presets.py +0 -0
  171. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_serve_loop.py +0 -0
  172. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_substrate_fingerprint.py +0 -0
  173. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/integration/test_transient_error_backoff.py +0 -0
  174. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/__init__.py +0 -0
  175. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/_event_scan.py +0 -0
  176. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_atomic_write_enforced.py +0 -0
  177. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  178. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_catalogs.py +0 -0
  179. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_classification_ssot.py +0 -0
  180. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_config_error_consistency.py +0 -0
  181. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_config_value_set_ssot.py +0 -0
  182. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  183. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  184. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_cli_claims.py +0 -0
  185. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_operator_surface.py +0 -0
  186. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  187. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_docs_generated.py +0 -0
  188. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_docs_index_complete.py +0 -0
  189. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_entry_points_resolve.py +0 -0
  190. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_event_kind_registry.py +0 -0
  191. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_event_kinds_ssot.py +0 -0
  192. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_events_doc_contract.py +0 -0
  193. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_hook_contract_docs.py +0 -0
  194. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_layer_2_loop_size.py +0 -0
  195. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_module_sizes.py +0 -0
  196. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_no_ai_signatures.py +0 -0
  197. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  198. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_peek_schema_version.py +0 -0
  199. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_plugins_config_stable.py +0 -0
  200. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  201. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  202. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_round_result_stable.py +0 -0
  203. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  204. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  205. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_upstream_schema_canary.py +0 -0
  206. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  207. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/literate/__init__.py +0 -0
  208. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/literate/parser.py +0 -0
  209. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/literate/test_parser.py +0 -0
  210. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/literate/test_quickstart.py +0 -0
  211. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/__init__.py +0 -0
  212. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime.py +0 -0
  213. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_grace.py +0 -0
  214. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_progress.py +0 -0
  215. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  216. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_assemble_prompt.py +0 -0
  217. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_events_stream.py +0 -0
  218. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_install.py +0 -0
  219. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_read_round_num.py +0 -0
  220. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_resolve_phase.py +0 -0
  221. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_api_types.py +0 -0
  222. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_catalogs.py +0 -0
  223. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_claude_error_detector.py +0 -0
  224. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_cli.py +0 -0
  225. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_cli_common.py +0 -0
  226. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_cli_init_install.py +0 -0
  227. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_cli_monitor_http.py +0 -0
  228. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_cli_upgrade.py +0 -0
  229. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_codewhale_plugin.py +0 -0
  230. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_config_fresh_eyes.py +0 -0
  231. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_config_max_rounds.py +0 -0
  232. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_config_prompt_delivery.py +0 -0
  233. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_config_stop_file.py +0 -0
  234. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  235. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_config_transient_error_action.py +0 -0
  236. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_context_store.py +0 -0
  237. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_default_dirty_handler.py +0 -0
  238. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_detector_protocol.py +0 -0
  239. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_dirty_handlers.py +0 -0
  240. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_docgen.py +0 -0
  241. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_exit_cause.py +0 -0
  242. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  243. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_gemini_plugin.py +0 -0
  244. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_hook_failure_isolation.py +0 -0
  245. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_hooks.py +0 -0
  246. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_http_progress.py +0 -0
  247. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_init_entry_points.py +0 -0
  248. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_kimi_plugin.py +0 -0
  249. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_lifecycle.py +0 -0
  250. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_metrics.py +0 -0
  251. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_assembly.py +0 -0
  252. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  253. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  254. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  255. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detectors.py +0 -0
  256. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_network_blip_signal_guard.py +0 -0
  257. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_peek_argparse.py +0 -0
  258. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_peek_select.py +0 -0
  259. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_pi_plugin.py +0 -0
  260. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_plugin_constants.py +0 -0
  261. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_presets.py +0 -0
  262. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_prompt_loader.py +0 -0
  263. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_redact.py +0 -0
  264. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_round_view.py +0 -0
  265. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_runner_throttle.py +0 -0
  266. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_scaffold.py +0 -0
  267. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_serve_cmd_bounded.py +0 -0
  268. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_serve_config_broken.py +0 -0
  269. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_serve_crash_loop.py +0 -0
  270. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_serve_sentinel.py +0 -0
  271. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_serve_startup_hooks.py +0 -0
  272. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_service_unit.py +0 -0
  273. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_startup_check.py +0 -0
  274. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_substrate.py +0 -0
  275. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_throttle_plugin_classification.py +0 -0
  276. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.6}/tests/unit/test_vcs_state.py +0 -0
@@ -9,9 +9,13 @@ permissions:
9
9
 
10
10
  # Trusted Publishing: the `environment` name below MUST match the
11
11
  # `Environment name` field in the PyPI pending publisher config.
12
- # A mismatch fails publish with an opaque OIDC error.
13
- # Note: TestPyPI stage intentionally omittedfirst release verification
14
- # happens via the manual approval gate on the `pypi` environment.
12
+ # A mismatch fails publish with an opaque OIDC error. Keep the environment
13
+ # declared even when it carries no protection rules it is the OIDC claim,
14
+ # not just an approval gate; deleting it breaks publishing outright. Whether
15
+ # a tag publishes straight through or waits for a reviewer is configured in
16
+ # the repo's environment settings, not here.
17
+ # Note: TestPyPI stage intentionally omitted — the tag is the release signal;
18
+ # pre-tag verification is `./build.sh check` plus the CI matrix on main.
15
19
 
16
20
  concurrency:
17
21
  group: release-${{ github.ref }}
@@ -228,9 +228,10 @@ path
228
228
  # agent_runner.monitor.LocalSource
229
229
  log_dir
230
230
 
231
- # agent_runner.monitor.RemoteSource
232
- host
233
- project
231
+ # agent_runner.round_log.PruneOutcome
232
+ deleted
233
+ deferred
234
+ existing
234
235
 
235
236
  # agent_runner.startup_check.CheckResult
236
237
  name
@@ -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.6] - 2026-07-28
9
+
10
+ ### Changed
11
+ - **Round-log pruning is now opt-in.** `runtime.round_log_retention` defaults to `0` (never prune) and `0` is now a legal value — previously both `0` and `-1` were rejected, so there was no off switch. Retaining too much is defended (`disk_warning` at 90%, `disk_critical` at 95% auto-stops the service); deleting too much was not defended at all. Both round-log families are affected, including the serve-level `round-<N>.log` family that had been capped at 100 for many releases. Set `round_log_retention = 100` for the previous behavior.
12
+
13
+ ### Fixed
14
+ - A prune that would delete more files than it keeps is now refused wholesale — nothing is deleted — so opting in on an existing backlog, or lowering the value later, can no longer wipe history in one pass. 0.2.4 and 0.2.5 did exactly that on their first post-upgrade round (one deployment: 12,193 of 12,293 transcripts, ~6.4 GB).
15
+
16
+ ### Added
17
+ - `round_logs_prune_deferred` event — emitted on every prune attempt the bulk guard defers, carrying `directory`, `existing`, `keep`, `would_delete` and a hint naming `runtime.round_log_retention`. Resolve by raising retention above the backlog or deleting the files yourself; the guard never blocks a round. Retention `0` emits nothing.
18
+
19
+ See `docs/migrations/0.2.6.md`.
20
+
21
+ ## [0.2.5] - 2026-07-27
22
+
23
+ ### Added
24
+ - `events --since <ISO ts>` — replays every matching event with `ts >= since`, across month files, one-shot or as the backlog phase of `--tail`. At-least-once: a client whose stream dropped reconnects with the last ts it saw and loses nothing (that one event may repeat).
25
+ - `monitor --host <alias> --mode events` — a managed ssh event relay, replacing a hand-rolled `ssh … --tail; sleep` loop. Passes the remote's JSONL through unmodified, reconnects with `--since <last ts>` so a dropped link replays its gap, emits `monitor_remote_blip` per drop and `monitor_remote_giveup` + exit 1 once the outage passes `[monitor] remote_failure_tolerance_s`, and kills the ssh process GROUP on exit so no orphan tree survives. New flags `--kind` and `--remote-config`.
26
+
27
+ ### Removed
28
+ - The polling remote monitor (`RemoteSource`, `run_remote_command`, `MonitorRemoteError`, auto-stop over ssh) — it read remote paths on the local filesystem and was already failing loud since 0.2.4. Detection is on-host by design; remote observation is the relay above. `monitor.on_alert` lost its `host` parameter and `monitor_auto_stop_triggered` its `host` field; `monitor_auto_stop_failed` now reports a failed *local* stop (previously a failed ssh stop) instead of crashing the monitor loop.
29
+
30
+ See `docs/migrations/0.2.5.md`.
31
+
8
32
  ## [0.2.4] - 2026-07-27
9
33
 
10
34
  ### Added
@@ -648,6 +672,7 @@ No breaking changes. Plugin authors:
648
672
  between rounds, opt them out:
649
673
  ```python
650
674
  from agent_runner.vcs_state import register_plugin_owned_paths
675
+
651
676
  register_plugin_owned_paths(["your-output-dir/", "logs/your-plugin/**/*"])
652
677
  ```
653
678
  - If you followed the old `_register()` pattern from docs and noticed
@@ -938,7 +963,9 @@ Initial public release on PyPI as `cli-agent-runner`.
938
963
  - Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
939
964
  gated by a manual approval on the `pypi` GitHub environment.
940
965
 
941
- [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...HEAD
966
+ [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.6...HEAD
967
+ [0.2.6]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.5...v0.2.6
968
+ [0.2.5]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...v0.2.5
942
969
  [0.2.4]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.3...v0.2.4
943
970
  [0.2.3]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.2...v0.2.3
944
971
  [0.2.2]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.1...v0.2.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Restart-on-exit supervisor for autonomous CLI agents
5
5
  Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
6
6
  Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
@@ -28,7 +28,7 @@ Requires-Dist: psutil>=5.9
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest-cov>=5; extra == 'dev'
30
30
  Requires-Dist: pytest>=8; extra == 'dev'
31
- Requires-Dist: ruff>=0.5; extra == 'dev'
31
+ Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
32
32
  Requires-Dist: vulture>=2.13; extra == 'dev'
33
33
  Provides-Extra: e2e
34
34
  Requires-Dist: pytest>=8; extra == 'e2e'
@@ -93,7 +93,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
93
93
 
94
94
  ## Defenses (built in)
95
95
 
96
- 12 named defenses, structured as data — see `agent-runner peek --select defenses`.
96
+ 13 named defenses, structured as data — see `agent-runner peek --select defenses`.
97
97
  Each carries the historical incident it codifies and the invariant test that
98
98
  guards it. Highlights:
99
99
 
@@ -123,13 +123,24 @@ agent-runner monitor # 30s poll
123
123
  agent-runner monitor --json | jq -c # pipe to downstream consumers
124
124
  ```
125
125
 
126
- > **Remote monitoring is unsupported in this version**: `monitor --host <alias>`
127
- > cannot read a remote host's round logs or events, so it exits with an error at
128
- > startup instead of watching an empty world and reporting healthy. Run the
129
- > monitor on the supervised host itself (`ssh <alias>`, then `agent-runner
130
- > monitor`). See [`docs/runbook.md`](docs/runbook.md) § "Remote monitor & SSH
131
- > trust" — the SSH trust boundary applies to any agent-runner command you drive
132
- > over ssh.
126
+ Watch a remote host's event stream from your laptop one command instead of a
127
+ hand-rolled `ssh ; sleep` loop:
128
+
129
+ ```bash
130
+ agent-runner monitor --host pi --mode events # managed ssh relay, JSONL stdout
131
+ ```
132
+
133
+ The relay reconnects with `--since <last ts>` so a dropped link replays its gap,
134
+ gives up (exit 1) once the outage passes `remote_failure_tolerance_s`, and kills
135
+ the ssh process group on exit so no orphan tree is left behind.
136
+
137
+ > **Detection stays on the host.** `--host` with `--mode anomaly | narrate |
138
+ > http` exits with an error: the detectors read the supervised host's logs and
139
+ > auto-stop its service, which must keep working with your laptop closed. Run
140
+ > the monitor there (`ssh <alias>`, then `agent-runner monitor`) and relay its
141
+ > events here. See [`docs/runbook.md`](docs/runbook.md) § "Remote event relay &
142
+ > SSH trust" — the SSH trust boundary applies to any agent-runner command you
143
+ > drive over ssh.
133
144
 
134
145
  ## Documentation
135
146
 
@@ -57,7 +57,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
57
57
 
58
58
  ## Defenses (built in)
59
59
 
60
- 12 named defenses, structured as data — see `agent-runner peek --select defenses`.
60
+ 13 named defenses, structured as data — see `agent-runner peek --select defenses`.
61
61
  Each carries the historical incident it codifies and the invariant test that
62
62
  guards it. Highlights:
63
63
 
@@ -87,13 +87,24 @@ agent-runner monitor # 30s poll
87
87
  agent-runner monitor --json | jq -c # pipe to downstream consumers
88
88
  ```
89
89
 
90
- > **Remote monitoring is unsupported in this version**: `monitor --host <alias>`
91
- > cannot read a remote host's round logs or events, so it exits with an error at
92
- > startup instead of watching an empty world and reporting healthy. Run the
93
- > monitor on the supervised host itself (`ssh <alias>`, then `agent-runner
94
- > monitor`). See [`docs/runbook.md`](docs/runbook.md) § "Remote monitor & SSH
95
- > trust" — the SSH trust boundary applies to any agent-runner command you drive
96
- > over ssh.
90
+ Watch a remote host's event stream from your laptop one command instead of a
91
+ hand-rolled `ssh ; sleep` loop:
92
+
93
+ ```bash
94
+ agent-runner monitor --host pi --mode events # managed ssh relay, JSONL stdout
95
+ ```
96
+
97
+ The relay reconnects with `--since <last ts>` so a dropped link replays its gap,
98
+ gives up (exit 1) once the outage passes `remote_failure_tolerance_s`, and kills
99
+ the ssh process group on exit so no orphan tree is left behind.
100
+
101
+ > **Detection stays on the host.** `--host` with `--mode anomaly | narrate |
102
+ > http` exits with an error: the detectors read the supervised host's logs and
103
+ > auto-stop its service, which must keep working with your laptop closed. Run
104
+ > the monitor there (`ssh <alias>`, then `agent-runner monitor`) and relay its
105
+ > events here. See [`docs/runbook.md`](docs/runbook.md) § "Remote event relay &
106
+ > SSH trust" — the SSH trust boundary applies to any agent-runner command you
107
+ > drive over ssh.
97
108
 
98
109
  ## Documentation
99
110
 
@@ -22,6 +22,7 @@ __all__ = [
22
22
  "emit_rate_limit_stop",
23
23
  "emit_round_grace_extended",
24
24
  "emit_round_grace_kill",
25
+ "emit_round_logs_prune_deferred",
25
26
  "emit_round_progress",
26
27
  "emit_round_substrate_after",
27
28
  "emit_round_substrate_before",
@@ -346,6 +347,42 @@ def emit_round_grace_extended(
346
347
  )
347
348
 
348
349
 
350
+ def emit_round_logs_prune_deferred(
351
+ log_dir: Path,
352
+ *,
353
+ directory: str,
354
+ existing: int,
355
+ keep: int,
356
+ would_delete: int,
357
+ ) -> None:
358
+ """Emit when a round-log prune was deferred because it would be *bulk*
359
+ (it would delete more files than it keeps), so nothing was deleted.
360
+
361
+ ``directory`` is the directory holding the family — ``{log_dir}/rounds``
362
+ for agent transcripts, ``{log_dir}`` for the serve-level ``round-<N>.log``
363
+ files. Re-emitted on every prune attempt while the condition holds: the
364
+ deferral is permanent until an operator acts, and a one-shot event would
365
+ be missed by anyone who started watching later.
366
+
367
+ The hint is composed here rather than at the call sites so both families
368
+ name the same knob with the same wording.
369
+ """
370
+ from agent_runner.events import ROUND_LOGS_PRUNE_DEFERRED, emit
371
+
372
+ emit(
373
+ log_dir,
374
+ ROUND_LOGS_PRUNE_DEFERRED,
375
+ directory=directory,
376
+ existing=existing,
377
+ keep=keep,
378
+ would_delete=would_delete,
379
+ hint=(
380
+ f"nothing deleted; raise runtime.round_log_retention to >= {existing} "
381
+ f"to keep this backlog, or delete files in {directory} yourself"
382
+ ),
383
+ )
384
+
385
+
349
386
  def emit_anomaly_repetitive_tool(
350
387
  log_dir: Path,
351
388
  *,
@@ -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.4'
22
- __version_tuple__ = version_tuple = (0, 2, 4)
21
+ __version__ = version = '0.2.6'
22
+ __version_tuple__ = version_tuple = (0, 2, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -16,9 +16,9 @@ import signal
16
16
  import subprocess # noqa: TID251 — api uses systemctl + ssh, both subprocess
17
17
  import sysconfig
18
18
  import time
19
- from collections.abc import Iterator
19
+ from collections.abc import Iterator, Sequence
20
20
  from pathlib import Path
21
- from typing import Any, Literal
21
+ from typing import Any, Literal, TextIO
22
22
 
23
23
  from agent_runner import events, lifecycle
24
24
  from agent_runner.api_types import (
@@ -383,8 +383,6 @@ from agent_runner import defenses, monitor # noqa: E402
383
383
  from agent_runner.events import ( # noqa: E402
384
384
  AGENT_NETWORK_BLIP,
385
385
  HOOK_FAILED,
386
- MONITOR_REMOTE_BLIP,
387
- MONITOR_REMOTE_GIVEUP,
388
386
  MONITOR_STARTED,
389
387
  )
390
388
 
@@ -479,16 +477,13 @@ def peek(
479
477
  return state if select is None else select_path(state, select)
480
478
 
481
479
 
482
- def _poll_once(project: str | Path, *, host: str | None) -> list[monitor.Alert]:
480
+ def _poll_once(project: str | Path) -> list[monitor.Alert]:
483
481
  work_dir = project if isinstance(project, Path) else Path.cwd()
484
482
  cfg = load_config(work_dir / "agent-runner.toml")
485
- src: monitor.StateSource
486
- if host is None:
487
- src = monitor.LocalSource(log_dir=cfg.runtime.log_dir)
488
- else:
489
- # Dormant: monitor_loop rejects --host before any poll, so this branch
490
- # is unreachable today. See monitor.MonitorRemoteUnsupportedError.
491
- src = monitor.RemoteSource(host=host, project=_project_name(work_dir))
483
+ # Always local: detection runs on the supervised host by design. Remote
484
+ # observation is an event RELAY (``monitor --host X --mode events``), not a
485
+ # remote poll — see agent_runner/remote_relay.py.
486
+ src: monitor.StateSource = monitor.LocalSource(log_dir=cfg.runtime.log_dir)
492
487
  events = monitor.parse_events_from_jsonl_files(src.events_files())
493
488
  metrics = monitor.parse_events_from_jsonl_files(src.metrics_files())
494
489
  log_tails = monitor.load_round_log_tails(src.rounds_dir())
@@ -512,11 +507,6 @@ def _poll_once(project: str | Path, *, host: str | None) -> list[monitor.Alert]:
512
507
  return builtin + plugin
513
508
 
514
509
 
515
- # Backoff schedule for remote-failure retries: each element is the sleep duration
516
- # in seconds for that attempt index (capped at 30s per step).
517
- _REMOTE_FAILURE_BACKOFF = (1, 2, 4, 8, 16, 30)
518
-
519
-
520
510
  def monitor_loop(
521
511
  project: str | Path | None = None, *, host: str | None = None, interval_s: int = 30
522
512
  ) -> Iterator[monitor.Alert]:
@@ -527,11 +517,11 @@ def monitor_loop(
527
517
  to that kind as the canonical "supervision is up" signal (monitor is otherwise
528
518
  silent during healthy operation by design).
529
519
 
530
- ``host`` (remote mode) raises ``MonitorRemoteUnsupportedError`` immediately:
531
- remote reads are unimplemented, so a remote loop would poll an empty world
532
- and report healthy forever. The check is eager — this wrapper validates
533
- before handing back the generator, so the failure lands at startup rather
534
- than at the first ``next()``.
520
+ ``host`` raises ``MonitorRemoteUnsupportedError`` immediately: detection runs
521
+ on the supervised host by design, so there is no remote polling mode. The
522
+ check is eager — this wrapper validates before handing back the generator,
523
+ so the failure lands at startup rather than at the first ``next()``. For a
524
+ remote event stream, see ``relay_remote_events``.
535
525
  """
536
526
  if host is not None:
537
527
  raise monitor.MonitorRemoteUnsupportedError(host)
@@ -543,13 +533,9 @@ def _monitor_loop_iter(
543
533
  ) -> Iterator[monitor.Alert]:
544
534
  """Polling generator behind ``monitor_loop``.
545
535
 
546
- Tolerates transient ``MonitorRemoteError`` failures (from ``--host`` ssh)
547
- for up to ``cfg.monitor.remote_failure_tolerance_s`` seconds with exponential
548
- backoff (1s 2s 4s → ... → 30s cap). Each retry emits ``monitor_remote_blip``;
549
- crossing the cap emits one ``monitor_remote_giveup`` and propagates the error
550
- (CLI exits 1; systemd restarts the process). Setting tolerance to 0 preserves
551
- the 0.1.10 immediate-propagate behavior with no blip events emitted. That
552
- path is dormant while remote mode is rejected at startup.
536
+ ``host`` is None by construction (``monitor_loop`` rejects anything else);
537
+ it is carried into the ``monitor_started`` payload as an explicit record
538
+ that this monitor watches its own host.
553
539
  """
554
540
  import json as _json
555
541
 
@@ -566,51 +552,8 @@ def _monitor_loop_iter(
566
552
  mode="anomaly-only",
567
553
  )
568
554
 
569
- tolerance_s = cfg.monitor.remote_failure_tolerance_s
570
- blip_start: float | None = None
571
- attempt = 0
572
-
573
555
  while True:
574
- try:
575
- alerts = _poll_once(work_dir, host=host)
576
- # Success: reset retry state.
577
- blip_start = None
578
- attempt = 0
579
- except monitor.MonitorRemoteError as e:
580
- if tolerance_s == 0:
581
- raise # 0.1.10 behavior preserved
582
- now = time.monotonic()
583
- if blip_start is None:
584
- blip_start = now
585
- attempt += 1
586
- elapsed = now - blip_start
587
- if elapsed >= tolerance_s:
588
- events.emit(
589
- cfg.runtime.log_dir,
590
- MONITOR_REMOTE_GIVEUP,
591
- host=host,
592
- total_attempts=attempt,
593
- total_elapsed_s=elapsed,
594
- cap_s=tolerance_s,
595
- final_error=e.stderr,
596
- )
597
- raise
598
- backoff_idx = min(attempt - 1, len(_REMOTE_FAILURE_BACKOFF) - 1)
599
- next_sleep_s = min(_REMOTE_FAILURE_BACKOFF[backoff_idx], tolerance_s - elapsed)
600
- events.emit(
601
- cfg.runtime.log_dir,
602
- MONITOR_REMOTE_BLIP,
603
- host=host,
604
- error=e.stderr,
605
- attempt=attempt,
606
- elapsed_s=elapsed,
607
- cap_s=tolerance_s,
608
- interval_s=interval_s,
609
- next_sleep_s=next_sleep_s,
610
- )
611
- time.sleep(next_sleep_s)
612
- continue
613
-
556
+ alerts = _poll_once(work_dir)
614
557
  for alert in alerts:
615
558
  key = f"{alert.detector}:{_json.dumps(alert.context, sort_keys=True)}"
616
559
  if key in seen:
@@ -620,7 +563,6 @@ def _monitor_loop_iter(
620
563
  monitor.on_alert(
621
564
  alert,
622
565
  project=_project_name(work_dir),
623
- host=host,
624
566
  log_dir=cfg.runtime.log_dir,
625
567
  allowed_stop_names=cfg.monitor.auto_stop_on,
626
568
  )
@@ -800,6 +742,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
800
742
  emit_rate_limit_stop,
801
743
  emit_round_grace_extended,
802
744
  emit_round_grace_kill,
745
+ emit_round_logs_prune_deferred,
803
746
  emit_round_progress,
804
747
  emit_round_substrate_after,
805
748
  emit_round_substrate_before,
@@ -849,6 +792,41 @@ def stream_events_jsonl(log_dir: Path, *, poll_interval_s: float = 0.1) -> Itera
849
792
  yield from _tail_events_jsonl(log_dir, start_at_now=True, poll_interval_s=poll_interval_s)
850
793
 
851
794
 
795
+ def relay_remote_events(
796
+ host: str,
797
+ *,
798
+ log_dir: Path,
799
+ kinds: Sequence[str] | None = None,
800
+ remote_config: str | None = None,
801
+ failure_tolerance_s: float = 90.0,
802
+ out: TextIO | None = None,
803
+ ) -> int:
804
+ """Remote sibling of ``stream_events_jsonl``: relay ``host``'s event stream.
805
+
806
+ Spawns a managed ``ssh <host> -- agent-runner events --tail`` and passes its
807
+ JSONL through, reconnecting with ``--since`` so a dropped link replays its
808
+ gap. Returns a CLI exit code (0 on interrupt, 1 once the link stays down
809
+ past ``failure_tolerance_s``). Blocks until then.
810
+
811
+ ``log_dir`` is this CLIENT's log dir: the ``monitor_remote_blip`` /
812
+ ``monitor_remote_giveup`` events it writes describe the local machine's link
813
+ to ``host``, not the supervised project. Detection is deliberately NOT
814
+ relayed — the detectors run on ``host`` (see ``monitor_loop``).
815
+ """
816
+ # Late import: pulls agent_runtime (psutil, threading) only for the callers
817
+ # that actually stream from a remote host.
818
+ from agent_runner.remote_relay import relay_remote_events as _relay
819
+
820
+ return _relay(
821
+ host,
822
+ log_dir=log_dir,
823
+ kinds=kinds,
824
+ remote_config=remote_config,
825
+ failure_tolerance_s=failure_tolerance_s,
826
+ out=out,
827
+ )
828
+
829
+
852
830
  def _format_narrate_line(evt: dict[str, Any]) -> str:
853
831
  """Format an event dict as a one-line human-readable string.
854
832
 
@@ -3,7 +3,13 @@
3
3
  One-shot (--window N) or streaming (--tail) query against events.jsonl.
4
4
  JSON Lines output (one JSON object per line, no pretty-print).
5
5
 
6
- Current-month scope only. Tail mode follows month rollover via per-poll glob.
6
+ Current-month scope only, except under ``--since <ISO ts>``, which replays every
7
+ matching event with ``ts >= since`` across month files (and then follows, under
8
+ ``--tail``). That replay is at-least-once by contract: a client resumes by
9
+ passing the last ts it saw, so the boundary event may arrive twice — no event is
10
+ ever silently lost across a dropped connection.
11
+
12
+ Tail mode follows month rollover via per-poll glob.
7
13
  """
8
14
 
9
15
  from __future__ import annotations
@@ -16,6 +22,8 @@ import time
16
22
  from datetime import UTC, datetime
17
23
  from pathlib import Path
18
24
 
25
+ from agent_runner.events import parse_iso_ms
26
+
19
27
  # Sentinel for "user did not explicitly set --window" so we can detect
20
28
  # --window + --tail combinations. argparse mutually-exclusive group would
21
29
  # be cleaner but argparse doesn't support "exclusive only when X has value Y".
@@ -64,6 +72,18 @@ def add_parser(sub, parent) -> None:
64
72
  action="store_true",
65
73
  help=("Streaming mode: emit each new matching event as it fires (blocks until SIGINT)."),
66
74
  )
75
+ p.add_argument(
76
+ "--since",
77
+ type=str,
78
+ default=None,
79
+ metavar="ISO-TS",
80
+ help=(
81
+ "Replay every matching event with ts >= this ISO-8601 timestamp "
82
+ "(e.g. 2026-07-27T10:00:00.000Z), across month files, then keep "
83
+ "streaming if --tail. At-least-once: resume by passing the last ts "
84
+ "you saw — that event may repeat, none is lost. Excludes --window."
85
+ ),
86
+ )
67
87
  p.set_defaults(func=cmd_events)
68
88
 
69
89
 
@@ -97,6 +117,27 @@ def cmd_events(args) -> int:
97
117
  )
98
118
  return 2
99
119
 
120
+ since_raw = getattr(args, "since", None)
121
+ since: datetime | None = None
122
+ if since_raw is not None:
123
+ if window_explicit:
124
+ print(
125
+ "Error: --window and --since are mutually exclusive",
126
+ file=sys.stderr,
127
+ )
128
+ return 2
129
+ try:
130
+ since = parse_iso_ms(since_raw)
131
+ except ValueError:
132
+ print(
133
+ f"Error: --since expects an ISO-8601 timestamp "
134
+ f"(e.g. 2026-07-27T10:00:00.000Z), got {since_raw!r}",
135
+ file=sys.stderr,
136
+ )
137
+ return 2
138
+ if since.tzinfo is None:
139
+ since = since.replace(tzinfo=UTC)
140
+
100
141
  try:
101
142
  log_dir = _resolve_log_dir(args)
102
143
  except FileNotFoundError as e:
@@ -104,7 +145,15 @@ def cmd_events(args) -> int:
104
145
  return 1
105
146
 
106
147
  if args.tail:
107
- return _tail_events(log_dir, kind_set)
148
+ return _tail_events(log_dir, kind_set, since=since)
149
+
150
+ if since is not None:
151
+ try:
152
+ _replay_since(log_dir, kind_set, since) # offset only matters to --tail
153
+ except OSError as e:
154
+ print(f"Error: events file unreadable: {e}", file=sys.stderr)
155
+ return 1
156
+ return 0
108
157
 
109
158
  window = args.window if args.window != _WINDOW_DEFAULT_SENTINEL else 10
110
159
  return _query_events(log_dir, kind_set, window)
@@ -115,6 +164,68 @@ def _current_month_events_file(log_dir: Path) -> Path:
115
164
  return log_dir / f"events-{month}.jsonl"
116
165
 
117
166
 
167
+ def _month_of(events_file: Path) -> str:
168
+ """The YYYY-MM embedded in an ``events-YYYY-MM.jsonl`` filename."""
169
+ return events_file.name[len("events-") : -len(".jsonl")]
170
+
171
+
172
+ def _matches_since(line: str, kind_set: set[str], since: datetime) -> bool:
173
+ """True if ``line`` is a matching kind whose ts is at or after ``since``.
174
+
175
+ Malformed lines and lines carrying no parseable ``ts`` are skipped silently,
176
+ matching the tolerance of the other parse loops in this module.
177
+ """
178
+ try:
179
+ evt = json.loads(line)
180
+ except json.JSONDecodeError:
181
+ return False
182
+ if evt.get("event") not in kind_set:
183
+ return False
184
+ ts = evt.get("ts")
185
+ if not isinstance(ts, str):
186
+ return False
187
+ try:
188
+ parsed = parse_iso_ms(ts)
189
+ except ValueError:
190
+ return False
191
+ if parsed.tzinfo is None:
192
+ parsed = parsed.replace(tzinfo=UTC)
193
+ return parsed >= since
194
+
195
+
196
+ def _replay_since(log_dir: Path, kind_set: set[str], since: datetime) -> tuple[Path, int]:
197
+ """Emit every matching event with ``ts >= since``, oldest month file first.
198
+
199
+ Not windowed: replay is the resume path, so it emits the whole backlog.
200
+
201
+ Returns ``(current-month file, bytes consumed of it)``. That offset is the
202
+ handoff point for ``--tail``: it is the true end-of-read position, so a line
203
+ appended while the replay was running is picked up by the poll loop instead
204
+ of being skipped, and no line is emitted in both phases.
205
+ """
206
+ since_month = since.astimezone(UTC).strftime("%Y-%m")
207
+ current = _current_month_events_file(log_dir)
208
+ offset = 0
209
+ for path in sorted(log_dir.glob("events-*.jsonl")):
210
+ if _month_of(path) < since_month:
211
+ # Nothing to replay from a month that ended before `since`. The live
212
+ # file lands here only when `since` names a future month — seed the
213
+ # tail at its end rather than rewinding it to 0.
214
+ if path == current:
215
+ offset = path.stat().st_size
216
+ continue
217
+ with path.open("r", encoding="utf-8") as f:
218
+ for line in f:
219
+ line = line.strip()
220
+ if not line:
221
+ continue
222
+ if _matches_since(line, kind_set, since):
223
+ print(line, flush=True)
224
+ if path == current:
225
+ offset = f.tell()
226
+ return current, offset
227
+
228
+
118
229
  def _query_events(log_dir: Path, kind_set: set[str], window: int) -> int:
119
230
  """One-shot: read current-month events.jsonl, filter, print last N."""
120
231
  events_file = _current_month_events_file(log_dir)
@@ -143,10 +254,15 @@ def _query_events(log_dir: Path, kind_set: set[str], window: int) -> int:
143
254
  return 0
144
255
 
145
256
 
146
- def _tail_events(log_dir: Path, kind_set: set[str]) -> int:
257
+ def _tail_events(log_dir: Path, kind_set: set[str], since: datetime | None = None) -> int:
147
258
  """Streaming: poll current-month events.jsonl at 1s interval; emit each
148
259
  new matching line as it fires. Blocks until SIGINT (KeyboardInterrupt).
149
260
  Follows month rollover via per-poll glob.
261
+
262
+ With ``since``, the backlog (``ts >= since``, across month files) is replayed
263
+ first and the poll resumes at the exact byte the replay stopped on — no gap
264
+ and no duplicate across the handoff. At-least-once overall: the caller
265
+ resumes from the last ts it saw, so that one event may repeat.
150
266
  """
151
267
  last_size = 0
152
268
  current_file: Path | None = None
@@ -156,6 +272,13 @@ def _tail_events(log_dir: Path, kind_set: set[str]) -> int:
156
272
 
157
273
  signal.signal(signal.SIGINT, _handle_sigint)
158
274
 
275
+ if since is not None:
276
+ try:
277
+ current_file, last_size = _replay_since(log_dir, kind_set, since)
278
+ except OSError as e:
279
+ print(f"Error: events file unreadable: {e}", file=sys.stderr)
280
+ return 1
281
+
159
282
  try:
160
283
  while True:
161
284
  events_file = _current_month_events_file(log_dir)