cli-agent-runner 0.2.5__tar.gz → 0.2.7__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 (282) hide show
  1. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.vulture-whitelist.py +23 -0
  2. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/CHANGELOG.md +27 -1
  3. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/PKG-INFO +3 -3
  4. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/README.md +1 -1
  5. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_docgen.py +2 -0
  6. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_emit.py +61 -0
  7. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_version.py +2 -2
  8. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/api.py +4 -0
  9. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/api_types.py +1 -0
  10. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/serve_cmd.py +93 -1
  11. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/config.py +46 -3
  12. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/defenses.py +11 -0
  13. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/events.py +3 -0
  14. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/monitor.py +52 -1
  15. cli_agent_runner-0.2.7/agent_runner/round_log.py +160 -0
  16. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/runner.py +11 -2
  17. cli_agent_runner-0.2.7/agent_runner/schedule.py +178 -0
  18. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/architecture.md +6 -2
  19. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/configuration.md +139 -1
  20. cli_agent_runner-0.2.7/docs/migrations/0.2.6.md +127 -0
  21. cli_agent_runner-0.2.7/docs/migrations/0.2.7.md +121 -0
  22. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/aider.md +6 -5
  23. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/runbook.md +87 -7
  24. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/_test_helpers.py +2 -0
  25. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_architecture.py +4 -0
  26. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config.py +52 -3
  27. cli_agent_runner-0.2.7/tests/unit/test_config_schedule.py +65 -0
  28. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_defenses.py +2 -2
  29. cli_agent_runner-0.2.7/tests/unit/test_emit_schedule.py +32 -0
  30. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_events.py +28 -0
  31. cli_agent_runner-0.2.7/tests/unit/test_monitor_schedule_pause.py +103 -0
  32. cli_agent_runner-0.2.7/tests/unit/test_peek_schedule.py +35 -0
  33. cli_agent_runner-0.2.7/tests/unit/test_round_log_helpers.py +269 -0
  34. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_runner.py +67 -0
  35. cli_agent_runner-0.2.7/tests/unit/test_schedule.py +183 -0
  36. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_round_log.py +99 -24
  37. cli_agent_runner-0.2.7/tests/unit/test_serve_schedule_gate.py +143 -0
  38. cli_agent_runner-0.2.5/agent_runner/round_log.py +0 -95
  39. cli_agent_runner-0.2.5/tests/unit/test_round_log_helpers.py +0 -143
  40. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.codecov.yml +0 -0
  41. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  42. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  43. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  45. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/workflows/ci.yml +0 -0
  46. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/workflows/release.yml +0 -0
  47. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.gitignore +0 -0
  48. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/BACKLOG.md +0 -0
  49. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/CODE_OF_CONDUCT.md +0 -0
  50. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/CONTRIBUTING.md +0 -0
  51. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/LICENSE +0 -0
  52. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/README.zh.md +0 -0
  53. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/SECURITY.md +0 -0
  54. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/__init__.py +0 -0
  55. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_redact.py +0 -0
  56. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_registry.py +0 -0
  57. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_substrate.py +0 -0
  58. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_throttle.py +0 -0
  59. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/agent_runtime.py +0 -0
  60. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/__init__.py +0 -0
  61. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/_constants.py +0 -0
  62. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  63. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/codewhale.py +0 -0
  64. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  65. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/gemini.py +0 -0
  66. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/kimi.py +0 -0
  67. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/pi.py +0 -0
  68. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/__init__.py +0 -0
  69. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/__main__.py +0 -0
  70. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/common.py +0 -0
  71. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/events_cmd.py +0 -0
  72. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/init_cmd.py +0 -0
  73. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/install_cmd.py +0 -0
  74. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/monitor_cmd.py +0 -0
  75. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/peek_cmd.py +0 -0
  76. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/round_cmd.py +0 -0
  77. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/service_cmd.py +0 -0
  78. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/upgrade_cmd.py +0 -0
  79. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/context_store.py +0 -0
  80. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/hooks.py +0 -0
  81. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/http_progress.py +0 -0
  82. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/lifecycle.py +0 -0
  83. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/metrics.py +0 -0
  84. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/__init__.py +0 -0
  85. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/aider.toml +0 -0
  86. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/claude.toml +0 -0
  87. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/codewhale.toml +0 -0
  88. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/gemini.toml +0 -0
  89. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/kimi.toml +0 -0
  90. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/pi.toml +0 -0
  91. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/prompt_loader.py +0 -0
  92. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/remote_relay.py +0 -0
  93. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/round_view.py +0 -0
  94. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/scaffold.py +0 -0
  95. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/service_unit.py +0 -0
  96. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/startup_check.py +0 -0
  97. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/vcs_state.py +0 -0
  98. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/build.sh +0 -0
  99. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/README.md +0 -0
  100. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/commands.md +0 -0
  101. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/events.md +0 -0
  102. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/long-running-agents.md +0 -0
  103. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.16.md +0 -0
  104. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.17.md +0 -0
  105. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.19.md +0 -0
  106. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.20.md +0 -0
  107. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.21.md +0 -0
  108. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.22.md +0 -0
  109. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.23.md +0 -0
  110. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.24.md +0 -0
  111. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.25.md +0 -0
  112. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.26.md +0 -0
  113. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.27.md +0 -0
  114. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.28.md +0 -0
  115. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.29.md +0 -0
  116. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.30.md +0 -0
  117. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.31.md +0 -0
  118. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.32.md +0 -0
  119. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.33.md +0 -0
  120. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.34.md +0 -0
  121. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.35.md +0 -0
  122. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.36.md +0 -0
  123. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.37.md +0 -0
  124. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.38.md +0 -0
  125. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.39.md +0 -0
  126. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.40.md +0 -0
  127. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.42.md +0 -0
  128. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.0.md +0 -0
  129. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.1.md +0 -0
  130. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.2.md +0 -0
  131. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.3.md +0 -0
  132. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.4.md +0 -0
  133. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.5.md +0 -0
  134. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/plugins.md +0 -0
  135. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/quickstart.md +0 -0
  136. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/codewhale.md +0 -0
  137. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/kimi.md +0 -0
  138. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/pi.md +0 -0
  139. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/thesis.md +0 -0
  140. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/pyproject.toml +0 -0
  141. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/__init__.py +0 -0
  142. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/conftest.py +0 -0
  143. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/contract/__init__.py +0 -0
  144. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/contract/test_public_api_surface.py +0 -0
  145. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/__init__.py +0 -0
  146. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/conftest.py +0 -0
  147. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  148. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_install_systemd.py +0 -0
  149. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  150. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  151. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  152. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  153. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  154. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/generate_vulture_whitelist.py +0 -0
  155. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/__init__.py +0 -0
  156. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_bounded_run.py +0 -0
  157. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_context_enricher_namespacing.py +0 -0
  158. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_dirty_handler_seam.py +0 -0
  159. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_fresh_eyes_signal.py +0 -0
  160. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_grace_kill_emission.py +0 -0
  161. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_install_dry_run.py +0 -0
  162. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_monitor_seeded.py +0 -0
  163. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_detector_loaded.py +0 -0
  164. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_owned_paths.py +0 -0
  165. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_real_flow.py +0 -0
  166. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  167. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  168. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_scaffold_presets.py +0 -0
  169. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_serve_loop.py +0 -0
  170. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_substrate_fingerprint.py +0 -0
  171. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_transient_error_backoff.py +0 -0
  172. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/__init__.py +0 -0
  173. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/_event_scan.py +0 -0
  174. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_atomic_write_enforced.py +0 -0
  175. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  176. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_catalogs.py +0 -0
  177. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_classification_ssot.py +0 -0
  178. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_config_error_consistency.py +0 -0
  179. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_config_value_set_ssot.py +0 -0
  180. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  181. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  182. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_cli_claims.py +0 -0
  183. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_operator_surface.py +0 -0
  184. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  185. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_docs_generated.py +0 -0
  186. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_docs_index_complete.py +0 -0
  187. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_entry_points_resolve.py +0 -0
  188. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_event_kind_registry.py +0 -0
  189. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_event_kinds_ssot.py +0 -0
  190. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_events_doc_contract.py +0 -0
  191. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_hook_contract_docs.py +0 -0
  192. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_layer_2_loop_size.py +0 -0
  193. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_module_boundaries.py +0 -0
  194. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_module_sizes.py +0 -0
  195. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_no_ai_signatures.py +0 -0
  196. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  197. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_peek_schema_version.py +0 -0
  198. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_plugins_config_stable.py +0 -0
  199. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  200. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  201. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_round_result_stable.py +0 -0
  202. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  203. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  204. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_upstream_schema_canary.py +0 -0
  205. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  206. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/__init__.py +0 -0
  207. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/parser.py +0 -0
  208. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/test_parser.py +0 -0
  209. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/test_quickstart.py +0 -0
  210. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/__init__.py +0 -0
  211. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime.py +0 -0
  212. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_grace.py +0 -0
  213. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_progress.py +0 -0
  214. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  215. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_assemble_prompt.py +0 -0
  216. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_events_stream.py +0 -0
  217. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_install.py +0 -0
  218. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_observation.py +0 -0
  219. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_read_round_num.py +0 -0
  220. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_resolve_phase.py +0 -0
  221. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_service.py +0 -0
  222. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_types.py +0 -0
  223. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_auto_stop_gating.py +0 -0
  224. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_catalogs.py +0 -0
  225. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_claude_error_detector.py +0 -0
  226. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli.py +0 -0
  227. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_common.py +0 -0
  228. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_init_install.py +0 -0
  229. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_monitor_http.py +0 -0
  230. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  231. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_upgrade.py +0 -0
  232. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_codewhale_plugin.py +0 -0
  233. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_fresh_eyes.py +0 -0
  234. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_max_rounds.py +0 -0
  235. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_prompt_delivery.py +0 -0
  236. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_stop_file.py +0 -0
  237. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  238. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_transient_error_action.py +0 -0
  239. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_context_store.py +0 -0
  240. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_default_dirty_handler.py +0 -0
  241. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_detector_protocol.py +0 -0
  242. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_dirty_handlers.py +0 -0
  243. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_docgen.py +0 -0
  244. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_events_cmd.py +0 -0
  245. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_events_cmd_tail.py +0 -0
  246. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_exit_cause.py +0 -0
  247. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  248. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_gemini_plugin.py +0 -0
  249. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_hook_failure_isolation.py +0 -0
  250. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_hooks.py +0 -0
  251. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_http_progress.py +0 -0
  252. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_init_entry_points.py +0 -0
  253. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_kimi_plugin.py +0 -0
  254. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_lifecycle.py +0 -0
  255. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_metrics.py +0 -0
  256. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_assembly.py +0 -0
  257. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  258. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  259. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  260. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detectors.py +0 -0
  261. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_remote.py +0 -0
  262. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_network_blip_signal_guard.py +0 -0
  263. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_peek_argparse.py +0 -0
  264. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_peek_select.py +0 -0
  265. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_pi_plugin.py +0 -0
  266. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_plugin_constants.py +0 -0
  267. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_presets.py +0 -0
  268. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_prompt_loader.py +0 -0
  269. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_redact.py +0 -0
  270. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_round_view.py +0 -0
  271. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_runner_throttle.py +0 -0
  272. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_scaffold.py +0 -0
  273. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_cmd_bounded.py +0 -0
  274. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_config_broken.py +0 -0
  275. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_crash_loop.py +0 -0
  276. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_sentinel.py +0 -0
  277. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_startup_hooks.py +0 -0
  278. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_service_unit.py +0 -0
  279. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_startup_check.py +0 -0
  280. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_substrate.py +0 -0
  281. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_throttle_plugin_classification.py +0 -0
  282. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_vcs_state.py +0 -0
@@ -50,6 +50,7 @@ service
50
50
  recent_events
51
51
  recent_hook_failures
52
52
  recent_blips
53
+ schedule
53
54
 
54
55
  # agent_runner.api_types.RateLimitState
55
56
  throttled_until_epoch
@@ -121,6 +122,7 @@ vcs
121
122
  monitor
122
123
  phases
123
124
  plugins
125
+ schedule
124
126
 
125
127
  # agent_runner.config.MonitorConfig
126
128
  auth_fail_patterns
@@ -176,6 +178,11 @@ dry_run
176
178
  max_grace_after_result_s
177
179
  grace_kill_ignore_patterns
178
180
 
181
+ # agent_runner.config.ScheduleConfig
182
+ timezone
183
+ run_windows
184
+ pause_windows
185
+
179
186
  # agent_runner.config.VcsConfig
180
187
  stash_idempotency_s
181
188
  dirty_action
@@ -228,6 +235,22 @@ path
228
235
  # agent_runner.monitor.LocalSource
229
236
  log_dir
230
237
 
238
+ # agent_runner.round_log.PruneOutcome
239
+ deleted
240
+ deferred
241
+ existing
242
+
243
+ # agent_runner.schedule.PauseDecision
244
+ paused
245
+ resume_at
246
+ active_window
247
+
248
+ # agent_runner.schedule.Window
249
+ start_min
250
+ end_min
251
+ label
252
+ days
253
+
231
254
  # agent_runner.startup_check.CheckResult
232
255
  name
233
256
  ok
@@ -5,6 +5,31 @@ 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.7] - 2026-08-25
9
+
10
+ ### Added
11
+ - `[schedule]` config gates the serve loop between rounds: `pause_windows` / `run_windows` decide whether the next round launches now or the supervisor idle-sleeps until a window opens, then auto-resumes. Windows are `[WEEKDAYS ]HH:MM-HH:MM` (24h, end-exclusive, midnight-wrap; optional `Mon-Fri` / `Sat,Sun` weekday prefix, bare = every day). Timezone-aware (`timezone`, IANA; omit → host local). Evaluation is `run AND NOT pause`. Absent section → unchanged 7×24 behavior. An in-flight round is never interrupted; `agent-runner round` is never gated.
12
+ - `schedule_paused` / `schedule_resumed` events; `peek` surfaces the current pause state (`schedule` field).
13
+ - `serve --ignore-schedule` runs rounds regardless of the windows (catch-up / testing).
14
+
15
+ ### Fixed
16
+ - Monitor no longer raises `supervisor_stale` during an intentional schedule pause (suppressed until `resume_at` plus one staleness window).
17
+
18
+ See `docs/migrations/0.2.7.md`.
19
+
20
+ ## [0.2.6] - 2026-07-28
21
+
22
+ ### Changed
23
+ - **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.
24
+
25
+ ### Fixed
26
+ - 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).
27
+
28
+ ### Added
29
+ - `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.
30
+
31
+ See `docs/migrations/0.2.6.md`.
32
+
8
33
  ## [0.2.5] - 2026-07-27
9
34
 
10
35
  ### Added
@@ -950,7 +975,8 @@ Initial public release on PyPI as `cli-agent-runner`.
950
975
  - Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
951
976
  gated by a manual approval on the `pypi` GitHub environment.
952
977
 
953
- [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.5...HEAD
978
+ [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.6...HEAD
979
+ [0.2.6]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.5...v0.2.6
954
980
  [0.2.5]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...v0.2.5
955
981
  [0.2.4]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.3...v0.2.4
956
982
  [0.2.3]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.2...v0.2.3
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.5
3
+ Version: 0.2.7
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
@@ -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
 
@@ -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
 
@@ -22,6 +22,7 @@ from agent_runner.config import (
22
22
  PluginsConfig,
23
23
  PromptConfig,
24
24
  RuntimeConfig,
25
+ ScheduleConfig,
25
26
  VcsConfig,
26
27
  )
27
28
  from agent_runner.defenses import catalog
@@ -45,6 +46,7 @@ _SECTIONS: list[tuple[str, type, list[tuple[str, type]]]] = [
45
46
  ("monitor", MonitorConfig, [("monitor.host_health", MonitorHostHealthConfig)]),
46
47
  ("phases", PhasesConfig, []),
47
48
  ("plugins", PluginsConfig, []),
49
+ ("schedule", ScheduleConfig, []),
48
50
  ]
49
51
 
50
52
 
@@ -22,9 +22,12 @@ __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",
29
+ "emit_schedule_paused",
30
+ "emit_schedule_resumed",
28
31
  "emit_stop_file_detected",
29
32
  "emit_transient_error_backoff_capped",
30
33
  "emit_transient_error_detected",
@@ -84,6 +87,28 @@ def emit_stop_file_detected(
84
87
  )
85
88
 
86
89
 
90
+ def emit_schedule_paused(
91
+ log_dir: Path, *, active_window: str, resume_at: str, timezone: str
92
+ ) -> None:
93
+ """Emit schedule_paused when the serve loop enters a configured pause window."""
94
+ from agent_runner.events import SCHEDULE_PAUSED, emit
95
+
96
+ emit(
97
+ log_dir,
98
+ SCHEDULE_PAUSED,
99
+ active_window=active_window,
100
+ resume_at=resume_at,
101
+ timezone=timezone,
102
+ )
103
+
104
+
105
+ def emit_schedule_resumed(log_dir: Path, *, paused_for_s: int) -> None:
106
+ """Emit schedule_resumed when the serve loop exits a pause window."""
107
+ from agent_runner.events import SCHEDULE_RESUMED, emit
108
+
109
+ emit(log_dir, SCHEDULE_RESUMED, paused_for_s=paused_for_s)
110
+
111
+
87
112
  def emit_round_substrate_before(
88
113
  log_dir: Path, *, round_num: int, git_head: str | None, paths_hash: str | None
89
114
  ) -> None:
@@ -346,6 +371,42 @@ def emit_round_grace_extended(
346
371
  )
347
372
 
348
373
 
374
+ def emit_round_logs_prune_deferred(
375
+ log_dir: Path,
376
+ *,
377
+ directory: str,
378
+ existing: int,
379
+ keep: int,
380
+ would_delete: int,
381
+ ) -> None:
382
+ """Emit when a round-log prune was deferred because it would be *bulk*
383
+ (it would delete more files than it keeps), so nothing was deleted.
384
+
385
+ ``directory`` is the directory holding the family — ``{log_dir}/rounds``
386
+ for agent transcripts, ``{log_dir}`` for the serve-level ``round-<N>.log``
387
+ files. Re-emitted on every prune attempt while the condition holds: the
388
+ deferral is permanent until an operator acts, and a one-shot event would
389
+ be missed by anyone who started watching later.
390
+
391
+ The hint is composed here rather than at the call sites so both families
392
+ name the same knob with the same wording.
393
+ """
394
+ from agent_runner.events import ROUND_LOGS_PRUNE_DEFERRED, emit
395
+
396
+ emit(
397
+ log_dir,
398
+ ROUND_LOGS_PRUNE_DEFERRED,
399
+ directory=directory,
400
+ existing=existing,
401
+ keep=keep,
402
+ would_delete=would_delete,
403
+ hint=(
404
+ f"nothing deleted; raise runtime.round_log_retention to >= {existing} "
405
+ f"to keep this backlog, or delete files in {directory} yourself"
406
+ ),
407
+ )
408
+
409
+
349
410
  def emit_anomaly_repetitive_tool(
350
411
  log_dir: Path,
351
412
  *,
@@ -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.5'
22
- __version_tuple__ = version_tuple = (0, 2, 5)
21
+ __version__ = version = '0.2.7'
22
+ __version_tuple__ = version_tuple = (0, 2, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -473,6 +473,7 @@ def peek(
473
473
  recent_events=recent,
474
474
  recent_hook_failures=recent_hook_failures,
475
475
  recent_blips=recent_blips,
476
+ schedule=monitor.latest_schedule_state(parsed_events),
476
477
  )
477
478
  return state if select is None else select_path(state, select)
478
479
 
@@ -742,9 +743,12 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
742
743
  emit_rate_limit_stop,
743
744
  emit_round_grace_extended,
744
745
  emit_round_grace_kill,
746
+ emit_round_logs_prune_deferred,
745
747
  emit_round_progress,
746
748
  emit_round_substrate_after,
747
749
  emit_round_substrate_before,
750
+ emit_schedule_paused,
751
+ emit_schedule_resumed,
748
752
  emit_stop_file_detected,
749
753
  emit_transient_error_backoff_capped,
750
754
  emit_transient_error_detected,
@@ -98,6 +98,7 @@ class ProjectState:
98
98
  recent_events: list[dict[str, Any]] = field(default_factory=list)
99
99
  recent_hook_failures: list[dict[str, Any]] = field(default_factory=list)
100
100
  recent_blips: list[dict[str, Any]] = field(default_factory=list)
101
+ schedule: dict | None = None
101
102
 
102
103
 
103
104
  @dataclass(frozen=True)
@@ -15,7 +15,9 @@ import signal
15
15
  import subprocess # noqa: TID251
16
16
  import sys
17
17
  import time
18
+ from pathlib import Path
18
19
 
20
+ from agent_runner import schedule
19
21
  from agent_runner._substrate import compute_git_head, compute_paths_hash
20
22
  from agent_runner._throttle import _check_throttle_state
21
23
  from agent_runner._throttle import reset_counters as _reset_counters
@@ -26,8 +28,11 @@ from agent_runner.api import (
26
28
  emit_fresh_eyes_round_triggered,
27
29
  emit_max_rounds_reached,
28
30
  emit_rate_limit_stop,
31
+ emit_round_logs_prune_deferred,
29
32
  emit_round_substrate_after,
30
33
  emit_round_substrate_before,
34
+ emit_schedule_paused,
35
+ emit_schedule_resumed,
31
36
  emit_stop_file_detected,
32
37
  post_round_decision,
33
38
  )
@@ -56,6 +61,86 @@ def _resolve_max_rounds(*, cli_value: int | None, config_value: int | None) -> i
56
61
  return effective
57
62
 
58
63
 
64
+ def _maybe_pause_for_schedule(
65
+ cfg,
66
+ log_dir,
67
+ stop,
68
+ *,
69
+ now_fn=schedule.now_in_zone,
70
+ sleep_fn=time.sleep,
71
+ chunk_s: int = 30,
72
+ ) -> bool:
73
+ """If the current time is outside the run schedule, pause until it opens.
74
+
75
+ Returns True if a pause was entered (caller should ``continue`` so the
76
+ top-of-loop guards re-run), False if runnable now. During a pause, sleeps
77
+ in <= chunk_s slices so SIGTERM / SIGINT (which set stop["requested"]) lands
78
+ within one slice.
79
+
80
+ We do NOT check the self-terminate sentinel here: no round runs during a
81
+ pause, so no new sentinel can appear, and any pre-existing one already broke
82
+ the loop at its top before this gate. schedule_resumed is emitted ONLY when
83
+ the window actually opens — an interrupted pause (stop) is a termination, not
84
+ a resume, and correctly leaves schedule_paused as the newest event."""
85
+ sched = cfg.schedule
86
+ if not sched.enabled:
87
+ return False
88
+ decision = schedule.evaluate(
89
+ run_windows=sched.run_windows,
90
+ pause_windows=sched.pause_windows,
91
+ now_local=now_fn(sched.timezone),
92
+ )
93
+ if not decision.paused:
94
+ return False
95
+
96
+ started = time.monotonic()
97
+ emit_schedule_paused(
98
+ log_dir,
99
+ active_window=decision.active_window or "",
100
+ resume_at=decision.resume_at.isoformat() if decision.resume_at else "",
101
+ timezone=sched.timezone or "local",
102
+ )
103
+ stop_file = cfg.runtime.stop_file
104
+ window_opened = False
105
+ while not stop["requested"]:
106
+ if stop_file is not None and stop_file.exists():
107
+ # Operator stop_file dropped mid-pause: break without emitting
108
+ # schedule_resumed (the window did not open). The serve loop's
109
+ # top-of-loop stop_file check then emits stop_file_detected and exits.
110
+ break
111
+ # #3: should_run (not evaluate) avoids the 8-day next_resume_at scan on
112
+ # every poll; should_run is the negation of the pre-loop paused decision.
113
+ if schedule.should_run(
114
+ now_fn(sched.timezone),
115
+ run_windows=sched.run_windows,
116
+ pause_windows=sched.pause_windows,
117
+ ):
118
+ window_opened = True
119
+ break
120
+ sleep_fn(chunk_s)
121
+ if window_opened:
122
+ emit_schedule_resumed(log_dir, paused_for_s=int(time.monotonic() - started))
123
+ return True
124
+
125
+
126
+ def _prune_serve_round_logs(log_dir: Path, retention: int) -> None:
127
+ """Prune the serve-level ``round-<N>.log`` family; report a deferred prune.
128
+
129
+ A prune that would delete more files than it keeps is deferred wholesale
130
+ (nothing is deleted) and surfaced as ``round_logs_prune_deferred`` — the
131
+ same contract ``rounds/`` gets at round start, since one knob governs both.
132
+ """
133
+ outcome = prune_old_round_logs(log_dir, retention)
134
+ if outcome.deferred:
135
+ emit_round_logs_prune_deferred(
136
+ log_dir,
137
+ directory=str(log_dir),
138
+ existing=outcome.existing,
139
+ keep=retention,
140
+ would_delete=outcome.deferred,
141
+ )
142
+
143
+
59
144
  def _is_fresh_eyes_round(*, round_num: int, every_n: int | None) -> bool:
60
145
  """True if this round should signal fresh-eyes mode to the agent subprocess.
61
146
 
@@ -81,6 +166,11 @@ def add_parser(sub, parent) -> None:
81
166
  p = sub.add_parser("serve", parents=[parent], help="Long-running supervisor loop")
82
167
  p.add_argument("--once", action="store_true", help="Run a single round then exit (debug)")
83
168
  _add_max_rounds_arg(p)
169
+ p.add_argument(
170
+ "--ignore-schedule",
171
+ action="store_true",
172
+ help="Run rounds regardless of [schedule] pause/run windows (testing / catch-up)",
173
+ )
84
174
  p.set_defaults(func=cmd)
85
175
 
86
176
 
@@ -106,7 +196,7 @@ def cmd(args) -> int:
106
196
 
107
197
  # Pre-loop cleanup: remove stale sentinel, prune old round logs.
108
198
  (log_dir / ".agent-done").unlink(missing_ok=True)
109
- prune_old_round_logs(log_dir, cfg.runtime.round_log_retention)
199
+ _prune_serve_round_logs(log_dir, cfg.runtime.round_log_retention)
110
200
 
111
201
  round_env = {**os.environ, "AGENT_RUNNER_LOG_DIR": str(log_dir)}
112
202
 
@@ -157,6 +247,8 @@ def cmd(args) -> int:
157
247
  max_rounds=effective_max_rounds,
158
248
  )
159
249
  break
250
+ if not args.ignore_schedule and _maybe_pause_for_schedule(cfg, log_dir, stop):
251
+ continue
160
252
  round_num = next_round_num(log_dir)
161
253
  git_head_before = compute_git_head(work_dir)
162
254
  paths_hash_before = compute_paths_hash(
@@ -8,6 +8,8 @@ from dataclasses import dataclass, field
8
8
  from pathlib import Path
9
9
  from typing import Any, Literal
10
10
 
11
+ from agent_runner import schedule
12
+
11
13
  _VALID_INJECTION_MODES: frozenset[str] = frozenset({"prepend", "file", "none"})
12
14
  _VALID_DIRTY_ACTIONS: frozenset[str] = frozenset({"stash", "ignore", "auto_commit"})
13
15
  _VALID_TRANSIENT_ERROR_ACTIONS: frozenset[str] = frozenset({"back_off", "skip", "stop"})
@@ -39,7 +41,7 @@ class RuntimeConfig:
39
41
  round_timeout_s: int = 1800
40
42
  restart_delay_s: int = 3
41
43
  disable_pre_round_hooks: bool = False
42
- round_log_retention: int = 100
44
+ round_log_retention: int = 0 # 0 = never prune (pruning is opt-in)
43
45
  narrative_file: Path | None = None
44
46
  transient_error_action: Literal["back_off", "skip", "stop"] = "back_off"
45
47
  max_rounds: int | None = None # None = unbounded
@@ -168,6 +170,17 @@ class MonitorConfig:
168
170
  """
169
171
 
170
172
 
173
+ @dataclass(frozen=True)
174
+ class ScheduleConfig:
175
+ timezone: str | None = None # IANA name; None = host local time
176
+ run_windows: tuple[schedule.Window, ...] = ()
177
+ pause_windows: tuple[schedule.Window, ...] = ()
178
+
179
+ @property
180
+ def enabled(self) -> bool:
181
+ return bool(self.run_windows or self.pause_windows)
182
+
183
+
171
184
  @dataclass(frozen=True)
172
185
  class Config:
173
186
  agent: AgentConfig
@@ -177,6 +190,7 @@ class Config:
177
190
  monitor: MonitorConfig = field(default_factory=MonitorConfig)
178
191
  phases: PhasesConfig = field(default_factory=PhasesConfig)
179
192
  plugins: PluginsConfig = field(default_factory=PluginsConfig)
193
+ schedule: ScheduleConfig = field(default_factory=ScheduleConfig)
180
194
 
181
195
 
182
196
  def _require(d: dict, *path: str) -> object:
@@ -357,6 +371,32 @@ def _parse_fresh_eyes_every_n(runtime_d: dict) -> int | None:
357
371
  return _require_positive_int(raw, field="runtime.fresh_eyes_every_n")
358
372
 
359
373
 
374
+ def _parse_schedule(schedule_d: dict) -> ScheduleConfig:
375
+ tz = schedule_d.get("timezone")
376
+ if tz is not None:
377
+ tz = str(tz)
378
+ if not schedule.valid_timezone(tz):
379
+ raise ConfigError(f"schedule.timezone: unknown IANA zone {tz!r}")
380
+
381
+ def _parse_list(key: str) -> tuple[schedule.Window, ...]:
382
+ raw = schedule_d.get(key, [])
383
+ if not isinstance(raw, list):
384
+ raise ConfigError(f"schedule.{key} must be a list of window strings")
385
+ out = []
386
+ for w in raw:
387
+ try:
388
+ out.append(schedule.parse_window(str(w)))
389
+ except ValueError as e:
390
+ raise ConfigError(f"schedule.{key}: {e}") from e
391
+ return tuple(out)
392
+
393
+ return ScheduleConfig(
394
+ timezone=tz,
395
+ run_windows=_parse_list("run_windows"),
396
+ pause_windows=_parse_list("pause_windows"),
397
+ )
398
+
399
+
360
400
  def load_config(toml_path: Path) -> Config:
361
401
  if not toml_path.exists():
362
402
  raise FileNotFoundError(f"config not found: {toml_path}")
@@ -439,8 +479,8 @@ def load_config(toml_path: Path) -> Config:
439
479
  runtime_d.get("disable_pre_round_hooks", False),
440
480
  field="runtime.disable_pre_round_hooks",
441
481
  ),
442
- round_log_retention=_require_positive_int(
443
- runtime_d.get("round_log_retention", 100), field="runtime.round_log_retention"
482
+ round_log_retention=_require_non_negative_int(
483
+ runtime_d.get("round_log_retention", 0), field="runtime.round_log_retention"
444
484
  ),
445
485
  narrative_file=_expand_and_resolve(str(runtime_d["narrative_file"]), project_name, work_dir)
446
486
  if "narrative_file" in runtime_d
@@ -571,6 +611,8 @@ def load_config(toml_path: Path) -> Config:
571
611
  disable = list(plugins_raw.pop("disable", []))
572
612
  plugins = PluginsConfig(disable=disable, raw=plugins_raw)
573
613
 
614
+ schedule_cfg = _parse_schedule(raw.get("schedule", {}))
615
+
574
616
  cfg = Config(
575
617
  agent=agent,
576
618
  runtime=runtime,
@@ -579,6 +621,7 @@ def load_config(toml_path: Path) -> Config:
579
621
  monitor=monitor,
580
622
  phases=phases_cfg,
581
623
  plugins=plugins,
624
+ schedule=schedule_cfg,
582
625
  )
583
626
 
584
627
  # Honor [plugins] disable — must happen after registries are populated by
@@ -102,6 +102,17 @@ def catalog(cfg: Config) -> list[Defense]:
102
102
  guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
103
103
  current_state="active",
104
104
  ),
105
+ Defense(
106
+ name="bulk_round_log_prune_guard",
107
+ value="a prune deleting more files than it keeps deletes nothing",
108
+ codifies=(
109
+ "0.2.4 — rounds/ pruning shipped against backlogs it never built; "
110
+ "one deployment's first post-upgrade round would have deleted "
111
+ "12,193 of 12,293 transcripts silently"
112
+ ),
113
+ guarded_by=Path("tests/unit/test_round_log_helpers.py"),
114
+ current_state="active",
115
+ ),
105
116
  Defense(
106
117
  name="flock_concurrency",
107
118
  value="agent-runner.lock",
@@ -55,11 +55,14 @@ PROMPT_OVERWRITTEN = "prompt_overwritten"
55
55
  ROUND_END = "round_end"
56
56
  ROUND_GRACE_EXTENDED = "round_grace_extended"
57
57
  ROUND_GRACE_KILL = "round_grace_kill"
58
+ ROUND_LOGS_PRUNE_DEFERRED = "round_logs_prune_deferred"
58
59
  ROUND_PROGRESS = "round_progress"
59
60
  ROUND_START = "round_start"
60
61
  ROUND_SUBSTRATE_AFTER = "round_substrate_after"
61
62
  ROUND_SUBSTRATE_BEFORE = "round_substrate_before"
62
63
  ROUND_TIMEOUT_KILL = "round_timeout_kill"
64
+ SCHEDULE_PAUSED = "schedule_paused"
65
+ SCHEDULE_RESUMED = "schedule_resumed"
63
66
  SELF_TERMINATED = "agent_self_terminated"
64
67
  SERVE_STARTUP_HOOK_FAILED = "serve_startup_hook_failed"
65
68
  SERVICE_UPGRADE_ROLLBACK_FAILED = "service_upgrade_rollback_failed"
@@ -22,7 +22,7 @@ import time
22
22
  from collections import deque
23
23
  from collections.abc import Iterable
24
24
  from dataclasses import dataclass
25
- from datetime import UTC, datetime
25
+ from datetime import UTC, datetime, timedelta
26
26
  from pathlib import Path
27
27
  from typing import Any, Protocol
28
28
 
@@ -48,6 +48,8 @@ from agent_runner.events import (
48
48
  ORPHAN_STASHED,
49
49
  ROUND_END,
50
50
  ROUND_START,
51
+ SCHEDULE_PAUSED,
52
+ SCHEDULE_RESUMED,
51
53
  TRANSIENT_ERROR_DETECTED,
52
54
  TRANSIENT_ERROR_RECOVERED,
53
55
  now_iso_ms,
@@ -447,6 +449,16 @@ def detect_anomaly_repetitive_active(
447
449
  )
448
450
 
449
451
 
452
+ def _latest_schedule_event(events: list[dict[str, Any]]) -> dict[str, Any] | None:
453
+ """Newest schedule_paused/schedule_resumed event by stream order, or None.
454
+
455
+ Reverse-walk with early break — events grows unboundedly over a project's life."""
456
+ for e in reversed(events):
457
+ if e.get("event") in (SCHEDULE_PAUSED, SCHEDULE_RESUMED):
458
+ return e
459
+ return None
460
+
461
+
450
462
  def detect_supervisor_stale(
451
463
  events: list[dict[str, Any]],
452
464
  *,
@@ -471,6 +483,30 @@ def detect_supervisor_stale(
471
483
  age_s = (now - parse_iso_ms(last_ts_str)).total_seconds()
472
484
  if age_s <= stale_threshold_s:
473
485
  return None
486
+
487
+ newest = _latest_schedule_event(events)
488
+ if newest is not None and newest.get("event") == SCHEDULE_PAUSED:
489
+ # Suppress only while the pause is plausibly still live: until its
490
+ # announced resume_at plus one staleness window. A supervisor that died
491
+ # mid-pause therefore still alarms once that bound passes.
492
+ paused = newest
493
+ bound = None
494
+ try:
495
+ resume_dt = parse_iso_ms(paused.get("resume_at") or "")
496
+ if resume_dt.tzinfo is not None:
497
+ bound = resume_dt
498
+ except (ValueError, TypeError):
499
+ bound = None
500
+ if bound is None:
501
+ # No usable resume_at (empty/unparseable/naive) → bound by the pause's
502
+ # own timestamp + the 8-day resume horizon, so an always-paused config
503
+ # is not a permanent silent-death blind spot.
504
+ try:
505
+ bound = parse_iso_ms(paused["ts"]) + timedelta(days=8)
506
+ except (KeyError, ValueError, TypeError):
507
+ return None # truly no anchor → suppress conservatively
508
+ if now <= bound + timedelta(seconds=stale_threshold_s):
509
+ return None
474
510
  return _alert(
475
511
  "supervisor_stale",
476
512
  "warning",
@@ -480,6 +516,21 @@ def detect_supervisor_stale(
480
516
  )
481
517
 
482
518
 
519
+ def latest_schedule_state(events: list[dict[str, Any]]) -> dict[str, Any] | None:
520
+ """Current schedule-pause state derived from the event stream.
521
+
522
+ Returns None when there is no pause in effect (no schedule events, or the
523
+ newest one is a resume). When paused, returns the paused indicator dict."""
524
+ newest = _latest_schedule_event(events)
525
+ if newest is None or newest.get("event") != SCHEDULE_PAUSED:
526
+ return None
527
+ return {
528
+ "paused": True,
529
+ "resume_at": newest.get("resume_at", ""),
530
+ "active_window": newest.get("active_window", ""),
531
+ }
532
+
533
+
483
534
  # ---------------------------------------------------------------------------
484
535
  # State-tree assembly (Task 3.2)
485
536
  # ---------------------------------------------------------------------------