cli-agent-runner 0.1.32__tar.gz → 0.1.34__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 (223) hide show
  1. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.gitignore +3 -0
  2. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/CHANGELOG.md +22 -0
  3. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/CONTRIBUTING.md +3 -8
  4. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/PKG-INFO +3 -3
  5. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/README.md +2 -2
  6. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_emit.py +34 -9
  7. cli_agent_runner-0.1.34/agent_runner/_throttle.py +133 -0
  8. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_version.py +2 -2
  9. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/api_types.py +1 -0
  10. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/_constants.py +18 -2
  11. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/__init__.py +2 -0
  12. cli_agent_runner-0.1.34/agent_runner/cli/events_cmd.py +188 -0
  13. cli_agent_runner-0.1.34/agent_runner/cli/peek_cmd.py +85 -0
  14. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/serve_cmd.py +5 -0
  15. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/metrics.py +28 -2
  16. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/monitor.py +1 -0
  17. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/runner.py +35 -7
  18. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/architecture.md +1 -1
  19. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/commands.md +20 -0
  20. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/long-running-agents.md +5 -4
  21. cli_agent_runner-0.1.34/docs/migrations/0.1.33.md +88 -0
  22. cli_agent_runner-0.1.34/docs/migrations/0.1.34.md +110 -0
  23. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/plugins.md +125 -3
  24. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/thesis.md +38 -0
  25. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/pyproject.toml +2 -1
  26. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_architecture.py +2 -2
  27. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_module_boundaries.py +1 -0
  28. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_types.py +38 -0
  29. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_claude_error_detector.py +16 -0
  30. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_events.py +61 -0
  31. cli_agent_runner-0.1.34/tests/unit/test_events_cmd.py +179 -0
  32. cli_agent_runner-0.1.34/tests/unit/test_peek_select.py +41 -0
  33. cli_agent_runner-0.1.34/tests/unit/test_runner_throttle.py +339 -0
  34. cli_agent_runner-0.1.32/.githooks/commit-msg +0 -33
  35. cli_agent_runner-0.1.32/agent_runner/_throttle.py +0 -63
  36. cli_agent_runner-0.1.32/agent_runner/cli/peek_cmd.py +0 -156
  37. cli_agent_runner-0.1.32/tests/unit/test_peek_select.py +0 -76
  38. cli_agent_runner-0.1.32/tests/unit/test_runner_throttle.py +0 -125
  39. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.codecov.yml +0 -0
  40. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  41. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  42. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  43. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  44. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/workflows/ci.yml +0 -0
  45. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/workflows/release.yml +0 -0
  46. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.vulture-whitelist.py +0 -0
  47. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/CODE_OF_CONDUCT.md +0 -0
  48. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/LICENSE +0 -0
  49. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/README.zh.md +0 -0
  50. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/SECURITY.md +0 -0
  51. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/__init__.py +0 -0
  52. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_docgen.py +0 -0
  53. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_registry.py +0 -0
  54. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_substrate.py +0 -0
  55. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/agent_runtime.py +0 -0
  56. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/api.py +0 -0
  57. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/__init__.py +0 -0
  58. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  59. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/gemini.py +0 -0
  60. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/__main__.py +0 -0
  61. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/common.py +0 -0
  62. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/init_cmd.py +0 -0
  63. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/install_cmd.py +0 -0
  64. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/monitor_cmd.py +0 -0
  65. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/round_cmd.py +0 -0
  66. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/service_cmd.py +0 -0
  67. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/upgrade_cmd.py +0 -0
  68. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/config.py +0 -0
  69. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/context_store.py +0 -0
  70. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/defenses.py +0 -0
  71. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/detector_helpers.py +0 -0
  72. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/events.py +0 -0
  73. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/hooks.py +0 -0
  74. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/http_progress.py +0 -0
  75. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/lifecycle.py +0 -0
  76. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/__init__.py +0 -0
  77. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/aider.toml +0 -0
  78. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/claude.toml +0 -0
  79. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/gemini.toml +0 -0
  80. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/prompt_loader.py +0 -0
  81. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/round_log.py +0 -0
  82. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/round_view.py +0 -0
  83. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/scaffold.py +0 -0
  84. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/service_unit.py +0 -0
  85. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/startup_check.py +0 -0
  86. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/vcs_state.py +0 -0
  87. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/build.sh +0 -0
  88. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/example-agent-runner.toml +0 -0
  89. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/launchd.plist.tmpl +0 -0
  90. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/run-loop.sh +0 -0
  91. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/systemd.service.tmpl +0 -0
  92. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/README.md +0 -0
  93. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/configuration.md +0 -0
  94. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/events.md +0 -0
  95. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/marketing/README.md +0 -0
  96. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/marketing/promo-cn.html +0 -0
  97. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.16.md +0 -0
  98. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.17.md +0 -0
  99. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.19.md +0 -0
  100. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.20.md +0 -0
  101. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.21.md +0 -0
  102. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.22.md +0 -0
  103. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.23.md +0 -0
  104. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.24.md +0 -0
  105. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.25.md +0 -0
  106. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.26.md +0 -0
  107. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.27.md +0 -0
  108. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.28.md +0 -0
  109. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.29.md +0 -0
  110. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.30.md +0 -0
  111. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.31.md +0 -0
  112. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.32.md +0 -0
  113. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/quickstart.md +0 -0
  114. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/recipes/aider.md +0 -0
  115. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/runbook.md +0 -0
  116. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/__init__.py +0 -0
  117. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/_test_helpers.py +0 -0
  118. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/conftest.py +0 -0
  119. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/contract/__init__.py +0 -0
  120. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/contract/test_public_api_surface.py +0 -0
  121. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/__init__.py +0 -0
  122. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/conftest.py +0 -0
  123. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  124. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_install_systemd.py +0 -0
  125. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  126. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  127. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  128. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  129. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  130. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/__init__.py +0 -0
  131. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_bounded_run.py +0 -0
  132. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_context_enricher_namespacing.py +0 -0
  133. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_fresh_eyes_signal.py +0 -0
  134. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_grace_kill_emission.py +0 -0
  135. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_install_dry_run.py +0 -0
  136. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_monitor_seeded.py +0 -0
  137. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_plugin_detector_loaded.py +0 -0
  138. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_plugin_owned_paths.py +0 -0
  139. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_plugin_real_flow.py +0 -0
  140. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  141. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_scaffold_presets.py +0 -0
  142. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_serve_loop.py +0 -0
  143. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_substrate_fingerprint.py +0 -0
  144. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_transient_error_backoff.py +0 -0
  145. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/__init__.py +0 -0
  146. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_atomic_write_enforced.py +0 -0
  147. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_catalogs.py +0 -0
  148. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_classification_ssot.py +0 -0
  149. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_docs_generated.py +0 -0
  150. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_entry_points_resolve.py +0 -0
  151. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_event_kind_registry.py +0 -0
  152. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_event_kinds_ssot.py +0 -0
  153. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_events_doc_contract.py +0 -0
  154. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_layer_2_loop_size.py +0 -0
  155. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_module_sizes.py +0 -0
  156. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_no_ai_signatures.py +0 -0
  157. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  158. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_peek_schema_version.py +0 -0
  159. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  160. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_round_result_stable.py +0 -0
  161. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  162. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_upstream_schema_canary.py +0 -0
  163. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/__init__.py +0 -0
  164. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/parser.py +0 -0
  165. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/test_parser.py +0 -0
  166. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/test_quickstart.py +0 -0
  167. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/__init__.py +0 -0
  168. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_agent_runtime.py +0 -0
  169. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_agent_runtime_grace.py +0 -0
  170. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_agent_runtime_progress.py +0 -0
  171. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_assemble_prompt.py +0 -0
  172. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_events_stream.py +0 -0
  173. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_install.py +0 -0
  174. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_observation.py +0 -0
  175. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_read_round_num.py +0 -0
  176. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_resolve_phase.py +0 -0
  177. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_service.py +0 -0
  178. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_auto_stop_gating.py +0 -0
  179. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli.py +0 -0
  180. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_common.py +0 -0
  181. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_init_install.py +0 -0
  182. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_monitor_http.py +0 -0
  183. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  184. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_upgrade.py +0 -0
  185. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config.py +0 -0
  186. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_fresh_eyes.py +0 -0
  187. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_max_rounds.py +0 -0
  188. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_stop_file.py +0 -0
  189. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  190. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_transient_error_action.py +0 -0
  191. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_context_store.py +0 -0
  192. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_defenses.py +0 -0
  193. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_detector_helpers.py +0 -0
  194. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_detector_protocol.py +0 -0
  195. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_docgen.py +0 -0
  196. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  197. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_gemini_plugin.py +0 -0
  198. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_hook_failure_isolation.py +0 -0
  199. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_hooks.py +0 -0
  200. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_http_progress.py +0 -0
  201. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_init_entry_points.py +0 -0
  202. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_lifecycle.py +0 -0
  203. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_metrics.py +0 -0
  204. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_assembly.py +0 -0
  205. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  206. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  207. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_detectors.py +0 -0
  208. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_remote.py +0 -0
  209. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_peek_argparse.py +0 -0
  210. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_presets.py +0 -0
  211. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_prompt_loader.py +0 -0
  212. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_round_log_helpers.py +0 -0
  213. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_round_view.py +0 -0
  214. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_runner.py +0 -0
  215. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_scaffold.py +0 -0
  216. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_cmd_bounded.py +0 -0
  217. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_round_log.py +0 -0
  218. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_sentinel.py +0 -0
  219. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_startup_hooks.py +0 -0
  220. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_service_unit.py +0 -0
  221. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_startup_check.py +0 -0
  222. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_substrate.py +0 -0
  223. {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_vcs_state.py +0 -0
@@ -1,6 +1,9 @@
1
1
  # Internal working notes (specs, plans, drafts) — not for public repo.
2
2
  docs/internal/
3
3
 
4
+ # Local git hooks — opt-in per clone; CI lint-commits is the authoritative gate.
5
+ .githooks/
6
+
4
7
  # Python
5
8
  __pycache__/
6
9
  *.py[cod]
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.34] - 2026-05-20
11
+
12
+ ### Added
13
+ - New verb `agent-runner events --kind K[,K2,...] [--window N] [--tail]` for event-stream observation. One-shot or streaming mode (JSON Lines output). Verbs: 13 → 14.
14
+ - `SystemMetrics.agent_process_count: int` — `pgrep -xc` of agent binary basename, host-wide. Surfaces orphan agent processes in peek output.
15
+ - `docs/plugins.md` worked example for custom monitor detector with plugin-emitted exempt flag — pattern for project-specific detectors that exclude exempt-by-design rounds.
16
+
17
+ ### Removed
18
+ - `peek --select events.<kind>` selector (mis-placed in state-snapshot verb). Use `events --kind <kind>` instead. 1-line `s///` migration.
19
+
20
+ See `docs/migrations/0.1.34.md`.
21
+
22
+ ## [0.1.33] - 2026-05-19
23
+
24
+ ### Added
25
+ - `_5XX_STATUSES` includes 529 (Anthropic's "overloaded") — now classified as `api_transient_5xx`.
26
+ - Exp backoff for estimated-class transient errors (`rate_limit_model` / `api_transient_5xx` / `api_timeout`): consecutive failures multiply the wait `2^N` capped at 32× and 30 minutes absolute. Server-authoritative `rate_limit_account` unchanged.
27
+ - `transient_error_backoff_capped` event gains `original_reset_at_epoch`, `applied_reset_at_epoch`, `consecutive_count`, `capped_by_absolute_max` fields for backoff-curve observability.
28
+ - `docs/thesis.md` names the server-authoritative vs estimated reset principle.
29
+
30
+ See `docs/migrations/0.1.33.md`.
31
+
10
32
  ## [0.1.32] - 2026-05-18
11
33
 
12
34
  ### Added
@@ -9,7 +9,6 @@ git clone https://github.com/wan9yu/cli-agent-runner.git
9
9
  cd cli-agent-runner
10
10
  python3 -m venv .venv && source .venv/bin/activate
11
11
  pip install -e ".[dev]"
12
- git config core.hooksPath .githooks # enables the commit-msg lint hook
13
12
  ./build.sh check
14
13
  ```
15
14
 
@@ -17,13 +16,6 @@ git config core.hooksPath .githooks # enables the commit-msg lint hook
17
16
  + integration tests, the literate quickstart, and the docs CI gate. It's
18
17
  what GitHub Actions runs on every push and PR.
19
18
 
20
- `git config core.hooksPath .githooks` activates the in-repo
21
- [`.githooks/commit-msg`](.githooks/commit-msg) hook which rejects commit
22
- messages containing `Co-Authored-By:` trailers, robot emojis, or other
23
- AI-tool attribution patterns. The same check runs in CI (`lint-commits`
24
- job) and as a pytest invariant (`tests/invariants/test_no_ai_signatures.py`)
25
- — defense in depth.
26
-
27
19
  ## Workflow
28
20
 
29
21
  1. Open an issue first for non-trivial changes — saves wasted work on both sides.
@@ -33,6 +25,9 @@ job) and as a pytest invariant (`tests/invariants/test_no_ai_signatures.py`)
33
25
  5. Run `./build.sh check` locally before pushing.
34
26
  6. Conventional Commits: `feat:` / `fix:` / `docs:` / `refactor:` / `test:` /
35
27
  `chore:` / `ci:` / `build:` / `perf:`. Subjects in English, imperative mood.
28
+ CI (`lint-commits` job) and `tests/invariants/test_no_ai_signatures.py`
29
+ reject auto-generated trailers and robot signatures — keep messages
30
+ human-authored.
36
31
 
37
32
  ## Architecture / docs
38
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.32
3
+ Version: 0.1.34
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
@@ -80,14 +80,14 @@ agent-runner monitor # live anomaly detection
80
80
 
81
81
  Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
82
82
 
83
- ## 13 verbs
83
+ ## 14 verbs
84
84
 
85
85
  | Lifecycle | Observation |
86
86
  |---|---|
87
87
  | `init` / `install` / `uninstall` | `peek` — state snapshot |
88
88
  | `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
89
89
  | `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
90
- | `round` / `serve` | |
90
+ | `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
91
91
 
92
92
  Verb reference: [`docs/commands.md`](docs/commands.md).
93
93
 
@@ -43,14 +43,14 @@ agent-runner monitor # live anomaly detection
43
43
 
44
44
  Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
45
45
 
46
- ## 13 verbs
46
+ ## 14 verbs
47
47
 
48
48
  | Lifecycle | Observation |
49
49
  |---|---|
50
50
  | `init` / `install` / `uninstall` | `peek` — state snapshot |
51
51
  | `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
52
52
  | `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
53
- | `round` / `serve` | |
53
+ | `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
54
54
 
55
55
  Verb reference: [`docs/commands.md`](docs/commands.md).
56
56
 
@@ -281,15 +281,40 @@ def emit_transient_error_backoff_capped(
281
281
  agent: str,
282
282
  requested_sleep_s: int,
283
283
  applied_sleep_s: int,
284
+ original_reset_at_epoch: int | None = None,
285
+ applied_reset_at_epoch: int | None = None,
286
+ consecutive_count: int | None = None,
287
+ capped_by_absolute_max: bool | None = None,
284
288
  ) -> None:
285
- """Emit defensive event when computed back-off exceeded 8h cap."""
289
+ """Emit when supervisor adjusts the plugin-emitted transient back-off.
290
+
291
+ Fires in two cases:
292
+ 1. **Exp backoff applied** (0.1.33+): estimated-class transient errors
293
+ (`rate_limit_model` / `api_transient_5xx` / `api_timeout`) doubled
294
+ on consecutive failures. ``consecutive_count`` > 1, multiplier > 1×.
295
+ 2. **Defensive cap hit** (0.1.20+): malformed `reset_at_epoch` or the
296
+ 30-min absolute cap clipped the wait. ``capped_by_absolute_max`` True.
297
+
298
+ Fields ``original_reset_at_epoch`` / ``applied_reset_at_epoch`` /
299
+ ``consecutive_count`` / ``capped_by_absolute_max`` are 0.1.33+. Older
300
+ callers that pass only the first 4 kwargs continue to work; the new
301
+ fields are omitted from the payload when None.
302
+ """
286
303
  from agent_runner.events import TRANSIENT_ERROR_BACKOFF_CAPPED, emit
287
304
 
288
- emit(
289
- log_dir,
290
- TRANSIENT_ERROR_BACKOFF_CAPPED,
291
- classification=classification,
292
- agent=agent,
293
- requested_sleep_s=requested_sleep_s,
294
- applied_sleep_s=applied_sleep_s,
295
- )
305
+ kwargs: dict = {
306
+ "classification": classification,
307
+ "agent": agent,
308
+ "requested_sleep_s": requested_sleep_s,
309
+ "applied_sleep_s": applied_sleep_s,
310
+ }
311
+ if original_reset_at_epoch is not None:
312
+ kwargs["original_reset_at_epoch"] = original_reset_at_epoch
313
+ if applied_reset_at_epoch is not None:
314
+ kwargs["applied_reset_at_epoch"] = applied_reset_at_epoch
315
+ if consecutive_count is not None:
316
+ kwargs["consecutive_count"] = consecutive_count
317
+ if capped_by_absolute_max is not None:
318
+ kwargs["capped_by_absolute_max"] = capped_by_absolute_max
319
+
320
+ emit(log_dir, TRANSIENT_ERROR_BACKOFF_CAPPED, **kwargs)
@@ -0,0 +1,133 @@
1
+ """Throttle state helpers — read events.jsonl tail for transient error state.
2
+
3
+ Internal module. Callers: runner.py (serve loop back-off), api.py (peek).
4
+ Separated from runner.py to satisfy the ouroboros defense: runner.py writes
5
+ events.jsonl but must never read it back (§3 module boundary invariant).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ from collections import deque
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from agent_runner.api_types import TransientErrorState
17
+
18
+
19
+ def _check_throttle_state(log_dir: Path) -> TransientErrorState | None:
20
+ """Scan events.jsonl tail for latest unmatched transient error.
21
+
22
+ Reads `transient_error_detected` / `transient_error_recovered` event names.
23
+ Returns TransientErrorState if currently throttled (reset still in future,
24
+ no matching recovered after). Restart-safe.
25
+ """
26
+ candidates = sorted(log_dir.glob("events-*.jsonl"))
27
+ if not candidates:
28
+ return None
29
+ with candidates[-1].open() as f:
30
+ tail = deque(f, maxlen=100)
31
+ events: list[dict[str, Any]] = []
32
+ for line in tail:
33
+ line = line.strip()
34
+ if not line:
35
+ continue
36
+ try:
37
+ events.append(json.loads(line))
38
+ except json.JSONDecodeError:
39
+ continue
40
+
41
+ latest_detected: dict[str, Any] | None = None
42
+ for ev in reversed(events):
43
+ kind = ev.get("event")
44
+ if kind == "transient_error_recovered":
45
+ return None
46
+ if kind == "transient_error_detected":
47
+ latest_detected = ev
48
+ break
49
+
50
+ if latest_detected is None:
51
+ return None
52
+ reset_at = int(latest_detected.get("reset_at_epoch", 0))
53
+ if reset_at <= time.time():
54
+ return None # Reset already passed without recovery emit; treat as recovered
55
+
56
+ classification = str(latest_detected.get("classification", "rate_limit_account"))
57
+
58
+ return TransientErrorState(
59
+ reset_at_epoch=reset_at,
60
+ classification=classification,
61
+ agent=str(latest_detected.get("agent", "unknown")),
62
+ since_round=int(latest_detected.get("round_num", 0)),
63
+ )
64
+
65
+
66
+ # Module-level supervisor state — bucket → consecutive-failure count.
67
+ # Cleared by reset_counters() or by serve restart.
68
+ _consecutive_failures: dict[str, int] = {}
69
+
70
+
71
+ def compute_adjusted_reset_at(
72
+ *,
73
+ classification: str,
74
+ original_reset_at_epoch: int,
75
+ agent: str,
76
+ log_dir: Path,
77
+ ) -> tuple[int, int, bool]:
78
+ """Apply exp backoff for estimated-class transient errors.
79
+
80
+ Returns (applied_reset_at_epoch, consecutive_count, capped_by_absolute_max).
81
+
82
+ For server-authoritative classification (``rate_limit_account``): returns
83
+ the original reset epoch verbatim, never increments the counter, and
84
+ never emits an adjustment event. Anthropic's resetsAt is authoritative.
85
+
86
+ For estimated classifications (``rate_limit_model``, ``api_transient_5xx``,
87
+ ``api_timeout``): increments the counter for this bucket, computes
88
+ duration = base × 2^min(n, _EXP_CAP), caps at _ABSOLUTE_CAP_S, emits
89
+ ``transient_error_backoff_capped`` if multiplier > 1 or capped.
90
+ """
91
+ from agent_runner._emit import emit_transient_error_backoff_capped
92
+ from agent_runner.builtin_plugins._constants import (
93
+ _ABSOLUTE_CAP_S,
94
+ _BACK_OFF_DEFAULTS,
95
+ _EXP_CAP,
96
+ )
97
+
98
+ if classification == "rate_limit_account":
99
+ # Server-authoritative: respect resetsAt verbatim, no counter touch.
100
+ return (original_reset_at_epoch, 0, False)
101
+
102
+ # Estimated class: apply exp backoff.
103
+ base = _BACK_OFF_DEFAULTS[classification]
104
+ n = _consecutive_failures.get(classification, 0)
105
+ multiplier = 2 ** min(n, _EXP_CAP)
106
+ extended_duration = base * multiplier
107
+ capped_by_absolute_max = extended_duration > _ABSOLUTE_CAP_S
108
+ applied_duration = min(extended_duration, _ABSOLUTE_CAP_S)
109
+ applied_reset_at = int(time.time()) + applied_duration
110
+
111
+ new_count = n + 1
112
+ _consecutive_failures[classification] = new_count
113
+
114
+ # Emit observability event when supervisor adjusted the wait.
115
+ if multiplier > 1 or capped_by_absolute_max:
116
+ emit_transient_error_backoff_capped(
117
+ log_dir,
118
+ classification=classification,
119
+ agent=agent,
120
+ requested_sleep_s=int(base),
121
+ applied_sleep_s=applied_duration,
122
+ original_reset_at_epoch=original_reset_at_epoch,
123
+ applied_reset_at_epoch=applied_reset_at,
124
+ consecutive_count=new_count,
125
+ capped_by_absolute_max=capped_by_absolute_max,
126
+ )
127
+
128
+ return (applied_reset_at, new_count, capped_by_absolute_max)
129
+
130
+
131
+ def reset_counters() -> None:
132
+ """Clear all bucket counters. Called by serve loop when no active throttle."""
133
+ _consecutive_failures.clear()
@@ -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.1.32'
22
- __version_tuple__ = version_tuple = (0, 1, 32)
21
+ __version__ = version = '0.1.34'
22
+ __version_tuple__ = version_tuple = (0, 1, 34)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -56,6 +56,7 @@ class SystemMetrics:
56
56
  disk_free_gb: float = 0.0
57
57
  load_1m: float | None = None
58
58
  cpu_pct: float | None = None
59
+ agent_process_count: int = 0 # 0.1.34 — pgrep -xc <agent_binary>, host-wide
59
60
 
60
61
 
61
62
  @dataclass(frozen=True)
@@ -21,9 +21,11 @@ _BACK_OFF_DEFAULTS: dict[str, int] = {
21
21
  }
22
22
 
23
23
  # 5xx codes treated as transient (retry-worthy server errors per RFC 9110):
24
- # 500=unexpected, 502=bad gateway, 503=unavailable, 504=gateway timeout.
24
+ # 500=unexpected, 502=bad gateway, 503=unavailable, 504=gateway timeout,
25
+ # 529=overloaded (Anthropic's non-RFC code emitted during sustained capacity
26
+ # issues; treated as transient per Anthropic SDK behavior).
25
27
  # Excluded: 501 (not implemented = permanent), 505 (HTTP version mismatch).
26
- _5XX_STATUSES: frozenset[int] = frozenset({500, 502, 503, 504})
28
+ _5XX_STATUSES: frozenset[int] = frozenset({500, 502, 503, 504, 529})
27
29
 
28
30
  _CLASSIFICATIONS: frozenset[str] = frozenset(
29
31
  {
@@ -38,3 +40,17 @@ _CLASSIFICATIONS: frozenset[str] = frozenset(
38
40
  rate_limit_account uses server-provided resetsAt (excluded from
39
41
  _BACK_OFF_DEFAULTS table); others use defaults from that table.
40
42
  """
43
+
44
+ _EXP_CAP: int = 5
45
+ """Maximum exponent for transient-error consecutive backoff: 2^5 = 32×.
46
+
47
+ Beyond this, the multiplier plateaus. Combined with _ABSOLUTE_CAP_S, this
48
+ prevents runaway wait times during sustained outages (max wait = 30min).
49
+ """
50
+
51
+ _ABSOLUTE_CAP_S: int = 1800
52
+ """Absolute upper bound on supervisor-applied transient back-off (30 min).
53
+
54
+ Applies after exp multiplier — even if base × 2^5 exceeds this, the wait
55
+ is clipped here. Defends against an indefinitely-stuck supervisor.
56
+ """
@@ -17,6 +17,7 @@ from pathlib import Path
17
17
 
18
18
  from agent_runner import __version__
19
19
  from agent_runner.cli import (
20
+ events_cmd,
20
21
  init_cmd,
21
22
  install_cmd,
22
23
  monitor_cmd,
@@ -63,6 +64,7 @@ def _build_parser() -> argparse.ArgumentParser:
63
64
  install_cmd.add_parser(sub, parent)
64
65
  service_cmd.add_parser(sub, parent)
65
66
  peek_cmd.add_parser(sub, parent)
67
+ events_cmd.add_parser(sub, parent)
66
68
  monitor_cmd.add_parser(sub, parent)
67
69
  serve_cmd.add_parser(sub, parent)
68
70
  round_cmd.add_parser(sub, parent)
@@ -0,0 +1,188 @@
1
+ """agent-runner events — event-stream observation verb (0.1.34+).
2
+
3
+ One-shot (--window N) or streaming (--tail) query against events.jsonl.
4
+ JSON Lines output (one JSON object per line, no pretty-print).
5
+
6
+ Current-month scope only. Tail mode follows month rollover via per-poll glob.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import signal
14
+ import sys
15
+ import time
16
+ from datetime import UTC, datetime
17
+ from pathlib import Path
18
+
19
+ # Sentinel for "user did not explicitly set --window" so we can detect
20
+ # --window + --tail combinations. argparse mutually-exclusive group would
21
+ # be cleaner but argparse doesn't support "exclusive only when X has value Y".
22
+ _WINDOW_DEFAULT_SENTINEL = -1
23
+
24
+
25
+ def _positive_int(s: str) -> int:
26
+ """Parse positive integer (duplicate of peek_cmd._positive_int; KISS)."""
27
+ try:
28
+ n = int(s)
29
+ except ValueError as e:
30
+ raise argparse.ArgumentTypeError(f"expects positive int, got {s!r}") from e
31
+ if n <= 0:
32
+ raise argparse.ArgumentTypeError(f"expects positive int (> 0), got {n}")
33
+ return n
34
+
35
+
36
+ def _parse_kinds(raw: str) -> set[str]:
37
+ """Parse comma-separated kinds; strip whitespace; reject empty."""
38
+ parts = [k.strip() for k in (raw or "").split(",") if k.strip()]
39
+ return set(parts)
40
+
41
+
42
+ def add_parser(sub, parent) -> None:
43
+ p = sub.add_parser(
44
+ "events",
45
+ parents=[parent],
46
+ help="Query / stream events from events.jsonl by kind",
47
+ )
48
+ p.add_argument(
49
+ "--kind",
50
+ type=str,
51
+ required=True,
52
+ metavar="K[,K2,...]",
53
+ help="Comma-separated event kinds (OR-filtered). At least one required.",
54
+ )
55
+ p.add_argument(
56
+ "--window",
57
+ type=_positive_int,
58
+ default=_WINDOW_DEFAULT_SENTINEL,
59
+ metavar="N",
60
+ help="One-shot mode: emit last N matching events (default 10).",
61
+ )
62
+ p.add_argument(
63
+ "--tail",
64
+ action="store_true",
65
+ help=("Streaming mode: emit each new matching event as it fires (blocks until SIGINT)."),
66
+ )
67
+ p.set_defaults(func=cmd_events)
68
+
69
+
70
+ def _resolve_log_dir(args) -> Path:
71
+ """Resolve log_dir from --config (used by both cmd_events and tests)."""
72
+ if getattr(args, "_log_dir_override", None) is not None:
73
+ return args._log_dir_override
74
+ from agent_runner.cli.common import work_dir_from_args
75
+ from agent_runner.config import load_config
76
+
77
+ cfg = load_config(work_dir_from_args(args) / "agent-runner.toml")
78
+ return cfg.runtime.log_dir
79
+
80
+
81
+ def cmd_events(args) -> int:
82
+ kind_set = _parse_kinds(args.kind)
83
+ if not kind_set:
84
+ print(
85
+ "Error: --kind requires at least one non-empty event kind",
86
+ file=sys.stderr,
87
+ )
88
+ return 2
89
+
90
+ window_explicit = getattr(args, "_window_explicit", False) or (
91
+ args.window != _WINDOW_DEFAULT_SENTINEL
92
+ )
93
+ if args.tail and window_explicit:
94
+ print(
95
+ "Error: --window and --tail are mutually exclusive",
96
+ file=sys.stderr,
97
+ )
98
+ return 2
99
+
100
+ try:
101
+ log_dir = _resolve_log_dir(args)
102
+ except FileNotFoundError as e:
103
+ print(f"Error: config not found: {e}", file=sys.stderr)
104
+ return 1
105
+
106
+ if args.tail:
107
+ return _tail_events(log_dir, kind_set)
108
+
109
+ window = args.window if args.window != _WINDOW_DEFAULT_SENTINEL else 10
110
+ return _query_events(log_dir, kind_set, window)
111
+
112
+
113
+ def _current_month_events_file(log_dir: Path) -> Path:
114
+ month = datetime.now(UTC).strftime("%Y-%m")
115
+ return log_dir / f"events-{month}.jsonl"
116
+
117
+
118
+ def _query_events(log_dir: Path, kind_set: set[str], window: int) -> int:
119
+ """One-shot: read current-month events.jsonl, filter, print last N."""
120
+ events_file = _current_month_events_file(log_dir)
121
+ if not events_file.exists():
122
+ return 0
123
+
124
+ matches: list[str] = []
125
+ try:
126
+ with events_file.open("r", encoding="utf-8") as f:
127
+ for line in f:
128
+ line = line.strip()
129
+ if not line:
130
+ continue
131
+ try:
132
+ evt = json.loads(line)
133
+ except json.JSONDecodeError:
134
+ continue
135
+ if evt.get("event") in kind_set:
136
+ matches.append(line)
137
+ except OSError as e:
138
+ print(f"Error: events file unreadable: {e}", file=sys.stderr)
139
+ return 1
140
+
141
+ for line in matches[-window:]:
142
+ print(line)
143
+ return 0
144
+
145
+
146
+ def _tail_events(log_dir: Path, kind_set: set[str]) -> int:
147
+ """Streaming: poll current-month events.jsonl at 1s interval; emit each
148
+ new matching line as it fires. Blocks until SIGINT (KeyboardInterrupt).
149
+ Follows month rollover via per-poll glob.
150
+ """
151
+ last_size = 0
152
+ current_file: Path | None = None
153
+
154
+ def _handle_sigint(_signum, _frame):
155
+ raise KeyboardInterrupt()
156
+
157
+ signal.signal(signal.SIGINT, _handle_sigint)
158
+
159
+ try:
160
+ while True:
161
+ events_file = _current_month_events_file(log_dir)
162
+ if events_file != current_file:
163
+ # Month rollover OR first iteration: reset offset
164
+ current_file = events_file
165
+ last_size = events_file.stat().st_size if events_file.exists() else 0
166
+
167
+ if events_file.exists():
168
+ size = events_file.stat().st_size
169
+ if size > last_size:
170
+ with events_file.open("r", encoding="utf-8") as f:
171
+ f.seek(last_size)
172
+ for line in f:
173
+ line = line.strip()
174
+ if not line:
175
+ continue
176
+ try:
177
+ evt = json.loads(line)
178
+ except json.JSONDecodeError:
179
+ continue
180
+ if evt.get("event") in kind_set:
181
+ print(line, flush=True)
182
+ last_size = size
183
+ elif size < last_size:
184
+ # File truncated / rotated underneath us; reset
185
+ last_size = 0
186
+ time.sleep(1.0)
187
+ except KeyboardInterrupt:
188
+ return 0
@@ -0,0 +1,85 @@
1
+ """peek and watch subcommands — snapshot + auto-refresh."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ import time
8
+
9
+ from agent_runner import api
10
+ from agent_runner.cli.common import emit, fail, work_dir_from_args
11
+
12
+
13
+ def _round_arg(s: str) -> int | str:
14
+ if s == "latest":
15
+ return s
16
+ try:
17
+ return int(s)
18
+ except ValueError as e:
19
+ raise argparse.ArgumentTypeError(f"--round expects int or 'latest', got {s!r}") from e
20
+
21
+
22
+ def add_parser(sub, parent) -> None:
23
+ for verb, fn in (("peek", cmd_peek), ("watch", cmd_watch)):
24
+ p = sub.add_parser(
25
+ verb, parents=[parent], help=f"{verb} project state with optional drill-down"
26
+ )
27
+ p.add_argument(
28
+ "--round",
29
+ type=_round_arg,
30
+ default=None,
31
+ metavar="N",
32
+ help="Drill into round N (int or 'latest')",
33
+ )
34
+ p.add_argument("--log", action="store_true", help="Include current round's log tail")
35
+ p.add_argument(
36
+ "--events", type=int, default=None, metavar="N", help="Include last N events"
37
+ )
38
+ p.add_argument(
39
+ "--select",
40
+ type=str,
41
+ default=None,
42
+ help=(
43
+ "Selector: dot-path (e.g. system.disk_used_pct) extracts a subtree from "
44
+ "peek state. For event-stream queries, use the dedicated `events` verb."
45
+ ),
46
+ )
47
+ if verb == "watch":
48
+ p.add_argument(
49
+ "--interval",
50
+ type=int,
51
+ default=2,
52
+ metavar="SECONDS",
53
+ help="Refresh interval (default 2)",
54
+ )
55
+ p.set_defaults(func=fn)
56
+
57
+
58
+ def cmd_peek(args) -> int:
59
+ select = args.select
60
+ try:
61
+ result = api.peek(
62
+ work_dir_from_args(args),
63
+ round=args.round,
64
+ log=args.log,
65
+ events=args.events,
66
+ select=select,
67
+ )
68
+ except KeyError as e:
69
+ return fail(str(e))
70
+ except FileNotFoundError as e:
71
+ return fail(f"config not found: {e}")
72
+ emit(result, json_mode=getattr(args, "json", False))
73
+ return 0
74
+
75
+
76
+ def cmd_watch(args) -> int:
77
+ while True:
78
+ sys.stdout.write("\x1b[2J\x1b[H")
79
+ rc = cmd_peek(args)
80
+ if rc != 0:
81
+ return rc
82
+ try:
83
+ time.sleep(args.interval)
84
+ except KeyboardInterrupt:
85
+ return 0
@@ -20,6 +20,7 @@ from pathlib import Path
20
20
 
21
21
  from agent_runner._substrate import compute_git_head, compute_paths_hash
22
22
  from agent_runner._throttle import _check_throttle_state
23
+ from agent_runner._throttle import reset_counters as _reset_counters
23
24
  from agent_runner.api import (
24
25
  check_self_terminated_sentinel,
25
26
  emit_fresh_eyes_round_triggered,
@@ -151,6 +152,10 @@ def cmd(args) -> int:
151
152
  elif action == "stop":
152
153
  emit_rate_limit_stop(log_dir)
153
154
  break
155
+ else:
156
+ # No active throttle this round — supervisor counters can reset.
157
+ # Next failure (if any) restarts the exp backoff curve from 1×.
158
+ _reset_counters()
154
159
  if stop_file is not None and stop_file.exists():
155
160
  try:
156
161
  content = stop_file.read_text(encoding="utf-8", errors="replace")[:200]