cli-agent-runner 0.2.4__tar.gz → 0.2.5__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 (273) hide show
  1. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.github/workflows/release.yml +7 -3
  2. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.vulture-whitelist.py +0 -4
  3. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/CHANGELOG.md +14 -1
  4. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/PKG-INFO +20 -9
  5. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/README.md +18 -7
  6. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/_version.py +2 -2
  7. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/api.py +51 -74
  8. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/events_cmd.py +126 -3
  9. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/monitor_cmd.py +61 -14
  10. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/config.py +3 -2
  11. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/monitor.py +40 -115
  12. cli_agent_runner-0.2.5/agent_runner/remote_relay.py +270 -0
  13. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/architecture.md +32 -16
  14. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/commands.md +53 -9
  15. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/configuration.md +5 -0
  16. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/events.md +3 -3
  17. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.28.md +2 -2
  18. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.30.md +3 -3
  19. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.35.md +1 -0
  20. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.2.0.md +2 -3
  21. cli_agent_runner-0.2.5/docs/migrations/0.2.5.md +99 -0
  22. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/plugins.md +28 -15
  23. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/runbook.md +93 -21
  24. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/thesis.md +4 -3
  25. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/pyproject.toml +3 -2
  26. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_monitor_seeded.py +2 -2
  27. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_module_boundaries.py +1 -0
  28. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_observation.py +8 -187
  29. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_service.py +2 -2
  30. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_auto_stop_gating.py +1 -5
  31. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_cli_service_peek_monitor.py +79 -56
  32. cli_agent_runner-0.2.5/tests/unit/test_events_cmd.py +343 -0
  33. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_events_cmd_tail.py +39 -2
  34. cli_agent_runner-0.2.5/tests/unit/test_monitor_remote.py +320 -0
  35. cli_agent_runner-0.2.4/tests/unit/test_events_cmd.py +0 -179
  36. cli_agent_runner-0.2.4/tests/unit/test_monitor_remote.py +0 -176
  37. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.codecov.yml +0 -0
  38. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  39. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  40. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  41. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  42. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.github/workflows/ci.yml +0 -0
  43. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/.gitignore +0 -0
  44. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/BACKLOG.md +0 -0
  45. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/CODE_OF_CONDUCT.md +0 -0
  46. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/CONTRIBUTING.md +0 -0
  47. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/LICENSE +0 -0
  48. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/README.zh.md +0 -0
  49. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/SECURITY.md +0 -0
  50. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/__init__.py +0 -0
  51. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/_docgen.py +0 -0
  52. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/_emit.py +0 -0
  53. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/_redact.py +0 -0
  54. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/_registry.py +0 -0
  55. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/_substrate.py +0 -0
  56. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/_throttle.py +0 -0
  57. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/agent_runtime.py +0 -0
  58. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/api_types.py +0 -0
  59. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/__init__.py +0 -0
  60. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/_constants.py +0 -0
  61. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  62. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/codewhale.py +0 -0
  63. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  64. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/gemini.py +0 -0
  65. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/kimi.py +0 -0
  66. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/builtin_plugins/pi.py +0 -0
  67. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/__init__.py +0 -0
  68. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/__main__.py +0 -0
  69. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/common.py +0 -0
  70. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/init_cmd.py +0 -0
  71. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/install_cmd.py +0 -0
  72. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/peek_cmd.py +0 -0
  73. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/round_cmd.py +0 -0
  74. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/serve_cmd.py +0 -0
  75. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/service_cmd.py +0 -0
  76. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/cli/upgrade_cmd.py +0 -0
  77. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/context_store.py +0 -0
  78. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/defenses.py +0 -0
  79. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/events.py +0 -0
  80. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/hooks.py +0 -0
  81. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/http_progress.py +0 -0
  82. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/lifecycle.py +0 -0
  83. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/metrics.py +0 -0
  84. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/presets/__init__.py +0 -0
  85. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/presets/aider.toml +0 -0
  86. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/presets/claude.toml +0 -0
  87. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/presets/codewhale.toml +0 -0
  88. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/presets/gemini.toml +0 -0
  89. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/presets/kimi.toml +0 -0
  90. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/presets/pi.toml +0 -0
  91. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/prompt_loader.py +0 -0
  92. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/round_log.py +0 -0
  93. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/round_view.py +0 -0
  94. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/runner.py +0 -0
  95. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/scaffold.py +0 -0
  96. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/service_unit.py +0 -0
  97. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/startup_check.py +0 -0
  98. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/agent_runner/vcs_state.py +0 -0
  99. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/build.sh +0 -0
  100. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/README.md +0 -0
  101. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/long-running-agents.md +0 -0
  102. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.16.md +0 -0
  103. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.17.md +0 -0
  104. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.19.md +0 -0
  105. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.20.md +0 -0
  106. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.21.md +0 -0
  107. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.22.md +0 -0
  108. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.23.md +0 -0
  109. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.24.md +0 -0
  110. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.25.md +0 -0
  111. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.26.md +0 -0
  112. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.27.md +0 -0
  113. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.29.md +0 -0
  114. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.31.md +0 -0
  115. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.32.md +0 -0
  116. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.33.md +0 -0
  117. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.34.md +0 -0
  118. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.36.md +0 -0
  119. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.37.md +0 -0
  120. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.38.md +0 -0
  121. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.39.md +0 -0
  122. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.40.md +0 -0
  123. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.1.42.md +0 -0
  124. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.2.1.md +0 -0
  125. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.2.2.md +0 -0
  126. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.2.3.md +0 -0
  127. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/migrations/0.2.4.md +0 -0
  128. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/quickstart.md +0 -0
  129. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/recipes/aider.md +0 -0
  130. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/recipes/codewhale.md +0 -0
  131. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/recipes/kimi.md +0 -0
  132. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/docs/recipes/pi.md +0 -0
  133. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/__init__.py +0 -0
  134. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/_test_helpers.py +0 -0
  135. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/conftest.py +0 -0
  136. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/contract/__init__.py +0 -0
  137. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/contract/test_public_api_surface.py +0 -0
  138. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/e2e/__init__.py +0 -0
  139. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/e2e/conftest.py +0 -0
  140. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  141. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/e2e/test_e2e_install_systemd.py +0 -0
  142. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  143. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  144. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  145. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  146. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  147. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/generate_vulture_whitelist.py +0 -0
  148. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/__init__.py +0 -0
  149. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_bounded_run.py +0 -0
  150. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_context_enricher_namespacing.py +0 -0
  151. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_dirty_handler_seam.py +0 -0
  152. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_fresh_eyes_signal.py +0 -0
  153. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_grace_kill_emission.py +0 -0
  154. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_install_dry_run.py +0 -0
  155. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_plugin_detector_loaded.py +0 -0
  156. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_plugin_owned_paths.py +0 -0
  157. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_plugin_real_flow.py +0 -0
  158. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  159. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  160. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_scaffold_presets.py +0 -0
  161. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_serve_loop.py +0 -0
  162. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_substrate_fingerprint.py +0 -0
  163. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/integration/test_transient_error_backoff.py +0 -0
  164. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/__init__.py +0 -0
  165. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/_event_scan.py +0 -0
  166. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_architecture.py +0 -0
  167. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_atomic_write_enforced.py +0 -0
  168. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  169. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_catalogs.py +0 -0
  170. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_classification_ssot.py +0 -0
  171. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_config_error_consistency.py +0 -0
  172. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_config_value_set_ssot.py +0 -0
  173. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  174. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  175. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_doc_cli_claims.py +0 -0
  176. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_doc_operator_surface.py +0 -0
  177. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  178. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_docs_generated.py +0 -0
  179. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_docs_index_complete.py +0 -0
  180. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_entry_points_resolve.py +0 -0
  181. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_event_kind_registry.py +0 -0
  182. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_event_kinds_ssot.py +0 -0
  183. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_events_doc_contract.py +0 -0
  184. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_hook_contract_docs.py +0 -0
  185. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_layer_2_loop_size.py +0 -0
  186. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_module_sizes.py +0 -0
  187. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_no_ai_signatures.py +0 -0
  188. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  189. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_peek_schema_version.py +0 -0
  190. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_plugins_config_stable.py +0 -0
  191. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  192. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  193. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_round_result_stable.py +0 -0
  194. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  195. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  196. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_upstream_schema_canary.py +0 -0
  197. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  198. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/literate/__init__.py +0 -0
  199. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/literate/parser.py +0 -0
  200. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/literate/test_parser.py +0 -0
  201. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/literate/test_quickstart.py +0 -0
  202. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/__init__.py +0 -0
  203. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_agent_runtime.py +0 -0
  204. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_agent_runtime_grace.py +0 -0
  205. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_agent_runtime_progress.py +0 -0
  206. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  207. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_assemble_prompt.py +0 -0
  208. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_events_stream.py +0 -0
  209. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_install.py +0 -0
  210. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_read_round_num.py +0 -0
  211. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_resolve_phase.py +0 -0
  212. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_api_types.py +0 -0
  213. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_catalogs.py +0 -0
  214. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_claude_error_detector.py +0 -0
  215. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_cli.py +0 -0
  216. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_cli_common.py +0 -0
  217. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_cli_init_install.py +0 -0
  218. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_cli_monitor_http.py +0 -0
  219. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_cli_upgrade.py +0 -0
  220. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_codewhale_plugin.py +0 -0
  221. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_config.py +0 -0
  222. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_config_fresh_eyes.py +0 -0
  223. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_config_max_rounds.py +0 -0
  224. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_config_prompt_delivery.py +0 -0
  225. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_config_stop_file.py +0 -0
  226. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  227. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_config_transient_error_action.py +0 -0
  228. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_context_store.py +0 -0
  229. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_default_dirty_handler.py +0 -0
  230. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_defenses.py +0 -0
  231. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_detector_protocol.py +0 -0
  232. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_dirty_handlers.py +0 -0
  233. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_docgen.py +0 -0
  234. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_events.py +0 -0
  235. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_exit_cause.py +0 -0
  236. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  237. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_gemini_plugin.py +0 -0
  238. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_hook_failure_isolation.py +0 -0
  239. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_hooks.py +0 -0
  240. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_http_progress.py +0 -0
  241. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_init_entry_points.py +0 -0
  242. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_kimi_plugin.py +0 -0
  243. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_lifecycle.py +0 -0
  244. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_metrics.py +0 -0
  245. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_monitor_assembly.py +0 -0
  246. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  247. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  248. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  249. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_monitor_detectors.py +0 -0
  250. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_network_blip_signal_guard.py +0 -0
  251. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_peek_argparse.py +0 -0
  252. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_peek_select.py +0 -0
  253. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_pi_plugin.py +0 -0
  254. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_plugin_constants.py +0 -0
  255. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_presets.py +0 -0
  256. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_prompt_loader.py +0 -0
  257. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_redact.py +0 -0
  258. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_round_log_helpers.py +0 -0
  259. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_round_view.py +0 -0
  260. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_runner.py +0 -0
  261. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_runner_throttle.py +0 -0
  262. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_scaffold.py +0 -0
  263. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_serve_cmd_bounded.py +0 -0
  264. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_serve_config_broken.py +0 -0
  265. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_serve_crash_loop.py +0 -0
  266. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_serve_round_log.py +0 -0
  267. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_serve_sentinel.py +0 -0
  268. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_serve_startup_hooks.py +0 -0
  269. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_service_unit.py +0 -0
  270. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_startup_check.py +0 -0
  271. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_substrate.py +0 -0
  272. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/tests/unit/test_throttle_plugin_classification.py +0 -0
  273. {cli_agent_runner-0.2.4 → cli_agent_runner-0.2.5}/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,10 +228,6 @@ path
228
228
  # agent_runner.monitor.LocalSource
229
229
  log_dir
230
230
 
231
- # agent_runner.monitor.RemoteSource
232
- host
233
- project
234
-
235
231
  # agent_runner.startup_check.CheckResult
236
232
  name
237
233
  ok
@@ -5,6 +5,17 @@ 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.5] - 2026-07-27
9
+
10
+ ### Added
11
+ - `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).
12
+ - `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`.
13
+
14
+ ### Removed
15
+ - 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.
16
+
17
+ See `docs/migrations/0.2.5.md`.
18
+
8
19
  ## [0.2.4] - 2026-07-27
9
20
 
10
21
  ### Added
@@ -648,6 +659,7 @@ No breaking changes. Plugin authors:
648
659
  between rounds, opt them out:
649
660
  ```python
650
661
  from agent_runner.vcs_state import register_plugin_owned_paths
662
+
651
663
  register_plugin_owned_paths(["your-output-dir/", "logs/your-plugin/**/*"])
652
664
  ```
653
665
  - If you followed the old `_register()` pattern from docs and noticed
@@ -938,7 +950,8 @@ Initial public release on PyPI as `cli-agent-runner`.
938
950
  - Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
939
951
  gated by a manual approval on the `pypi` GitHub environment.
940
952
 
941
- [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...HEAD
953
+ [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.5...HEAD
954
+ [0.2.5]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...v0.2.5
942
955
  [0.2.4]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.3...v0.2.4
943
956
  [0.2.3]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.2...v0.2.3
944
957
  [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.5
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'
@@ -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
 
@@ -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
 
@@ -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.5'
22
+ __version_tuple__ = version_tuple = (0, 2, 5)
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
  )
@@ -849,6 +791,41 @@ def stream_events_jsonl(log_dir: Path, *, poll_interval_s: float = 0.1) -> Itera
849
791
  yield from _tail_events_jsonl(log_dir, start_at_now=True, poll_interval_s=poll_interval_s)
850
792
 
851
793
 
794
+ def relay_remote_events(
795
+ host: str,
796
+ *,
797
+ log_dir: Path,
798
+ kinds: Sequence[str] | None = None,
799
+ remote_config: str | None = None,
800
+ failure_tolerance_s: float = 90.0,
801
+ out: TextIO | None = None,
802
+ ) -> int:
803
+ """Remote sibling of ``stream_events_jsonl``: relay ``host``'s event stream.
804
+
805
+ Spawns a managed ``ssh <host> -- agent-runner events --tail`` and passes its
806
+ JSONL through, reconnecting with ``--since`` so a dropped link replays its
807
+ gap. Returns a CLI exit code (0 on interrupt, 1 once the link stays down
808
+ past ``failure_tolerance_s``). Blocks until then.
809
+
810
+ ``log_dir`` is this CLIENT's log dir: the ``monitor_remote_blip`` /
811
+ ``monitor_remote_giveup`` events it writes describe the local machine's link
812
+ to ``host``, not the supervised project. Detection is deliberately NOT
813
+ relayed — the detectors run on ``host`` (see ``monitor_loop``).
814
+ """
815
+ # Late import: pulls agent_runtime (psutil, threading) only for the callers
816
+ # that actually stream from a remote host.
817
+ from agent_runner.remote_relay import relay_remote_events as _relay
818
+
819
+ return _relay(
820
+ host,
821
+ log_dir=log_dir,
822
+ kinds=kinds,
823
+ remote_config=remote_config,
824
+ failure_tolerance_s=failure_tolerance_s,
825
+ out=out,
826
+ )
827
+
828
+
852
829
  def _format_narrate_line(evt: dict[str, Any]) -> str:
853
830
  """Format an event dict as a one-line human-readable string.
854
831
 
@@ -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)
@@ -1,4 +1,4 @@
1
- """monitor subcommand — anomaly detection daemon (local or remote)."""
1
+ """monitor subcommand — anomaly detection, event stream (local or relayed), HTTP page."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -20,14 +20,29 @@ def add_parser(sub, parent) -> None:
20
20
  type=str,
21
21
  default=None,
22
22
  metavar="SSH-ALIAS",
23
- help="Remote ssh alias (unsupported in this version — errors at startup)",
23
+ help=(
24
+ "Remote ssh alias — supported with --mode events only: agent-runner "
25
+ "manages the ssh, resumes with --since after a drop, and kills the "
26
+ "ssh process group on exit. Detection modes run on the host itself."
27
+ ),
24
28
  )
25
29
  p.add_argument(
26
30
  "--interval",
27
31
  type=int,
28
32
  default=None,
29
33
  metavar="SECONDS",
30
- help="Poll interval (default 30s, 60s for remote)",
34
+ help="Poll interval (default 30s)",
35
+ )
36
+ p.add_argument(
37
+ "--kind",
38
+ type=str,
39
+ default=None,
40
+ metavar="K[,K2,...]",
41
+ help=(
42
+ "Event kinds to relay (--host --mode events only). Default: every "
43
+ "kind this client knows — built-ins plus locally installed plugin "
44
+ "kinds. A kind that exists only on the remote must be named here."
45
+ ),
31
46
  )
32
47
  p.add_argument(
33
48
  "--mode",
@@ -45,13 +60,32 @@ def add_parser(sub, parent) -> None:
45
60
  metavar="PORT",
46
61
  help="HTTP port for --mode http (default 8765, local-only)",
47
62
  )
63
+ p.add_argument(
64
+ "--remote-config",
65
+ type=str,
66
+ default=None,
67
+ metavar="PATH",
68
+ help=(
69
+ "Config path ON THE REMOTE HOST for the relayed events command "
70
+ "(--host --mode events only). Default: omit --config entirely, so "
71
+ "the remote resolves ./agent-runner.toml in the ssh landing directory."
72
+ ),
73
+ )
48
74
  p.set_defaults(func=cmd)
49
75
 
50
76
 
51
77
  def cmd(args) -> int:
52
78
  mode = getattr(args, "mode", "anomaly")
53
- if mode in ("narrate", "events", "http") and args.host is not None:
54
- return fail(f"--mode {mode} is local-only; remove --host or use --mode anomaly")
79
+ host = getattr(args, "host", None)
80
+ remote_only = [
81
+ f"--{name}"
82
+ for name in ("kind", "remote-config")
83
+ if getattr(args, name.replace("-", "_"), None) is not None
84
+ ]
85
+ if host is not None and mode != "events":
86
+ return fail(str(monitor.MonitorRemoteUnsupportedError(host, mode)))
87
+ if remote_only and (host is None or mode != "events"):
88
+ return fail(f"{', '.join(remote_only)} applies to --host --mode events only")
55
89
 
56
90
  if mode == "narrate":
57
91
  return _cmd_narrate(args)
@@ -63,11 +97,12 @@ def cmd(args) -> int:
63
97
 
64
98
 
65
99
  def _cmd_anomaly(args) -> int:
66
- interval = args.interval if args.interval is not None else (60 if args.host else 30)
100
+ interval = args.interval if args.interval is not None else 30
67
101
  json_mode = getattr(args, "json", False)
68
102
  try:
69
103
  work_dir = work_dir_from_args(args)
70
- for alert in api.monitor_loop(work_dir, host=args.host, interval_s=interval):
104
+ # No host: cmd() has already rejected --host for every detection mode.
105
+ for alert in api.monitor_loop(work_dir, interval_s=interval):
71
106
  if json_mode:
72
107
  print(json.dumps(_to_jsonable(alert)))
73
108
  sys.stdout.flush()
@@ -77,21 +112,33 @@ def _cmd_anomaly(args) -> int:
77
112
  sys.stdout.flush()
78
113
  except KeyboardInterrupt:
79
114
  return 0
80
- except monitor.MonitorRemoteUnsupportedError as e:
81
- return fail(str(e))
82
- except monitor.MonitorRemoteError as e:
83
- # Dormant alongside the rest of remote mode: ssh only runs under --host,
84
- # which the MonitorRemoteUnsupportedError handler above already rejects.
85
- return fail(f"cannot reach {e.host!r} via ssh: {e.stderr}")
86
115
  return 0
87
116
 
88
117
 
89
118
  def _cmd_events(args) -> int:
90
- """JSONL event stream — machine-readable variant of narrate."""
119
+ """JSONL event stream — a local tail, or a managed ssh relay under --host.
120
+
121
+ Both variants read the CLIENT's config: locally for the log dir to tail,
122
+ remotely for where the relay writes its own link telemetry (blip / give-up)
123
+ and for the reconnect deadline.
124
+ """
91
125
  from agent_runner.cli.common import cfg_from_args
92
126
 
93
127
  cfg = cfg_from_args(args)
94
128
  log_dir = cfg.runtime.log_dir
129
+
130
+ host = getattr(args, "host", None)
131
+ if host is not None:
132
+ raw_kinds = getattr(args, "kind", None)
133
+ kinds = [k.strip() for k in raw_kinds.split(",") if k.strip()] if raw_kinds else None
134
+ return api.relay_remote_events(
135
+ host,
136
+ log_dir=log_dir,
137
+ kinds=kinds,
138
+ remote_config=getattr(args, "remote_config", None),
139
+ failure_tolerance_s=cfg.monitor.remote_failure_tolerance_s,
140
+ )
141
+
95
142
  try:
96
143
  for evt in api.stream_events_jsonl(log_dir):
97
144
  print(json.dumps(evt), flush=True)