cli-agent-runner 0.2.3__tar.gz → 0.2.4__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 (270) hide show
  1. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/CHANGELOG.md +21 -3
  2. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/PKG-INFO +10 -10
  3. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/README.md +9 -9
  4. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/_emit.py +37 -1
  5. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/_version.py +2 -2
  6. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/agent_runtime.py +34 -7
  7. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/api.py +21 -1
  8. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/api_types.py +9 -0
  9. cli_agent_runner-0.2.4/agent_runner/builtin_plugins/_constants.py +125 -0
  10. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/builtin_plugins/claude_rate_limit.py +11 -27
  11. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/builtin_plugins/codewhale.py +16 -36
  12. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/builtin_plugins/gemini.py +6 -28
  13. cli_agent_runner-0.2.4/agent_runner/builtin_plugins/kimi.py +93 -0
  14. cli_agent_runner-0.2.4/agent_runner/builtin_plugins/pi.py +279 -0
  15. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/monitor_cmd.py +5 -1
  16. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/config.py +7 -1
  17. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/events.py +1 -0
  18. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/hooks.py +5 -5
  19. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/monitor.py +82 -11
  20. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/presets/pi.toml +2 -3
  21. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/round_log.py +30 -0
  22. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/runner.py +6 -2
  23. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/service_unit.py +7 -1
  24. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/startup_check.py +19 -20
  25. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/architecture.md +7 -3
  26. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/commands.md +7 -6
  27. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/configuration.md +2 -0
  28. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.2.3.md +5 -8
  29. cli_agent_runner-0.2.4/docs/migrations/0.2.4.md +66 -0
  30. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/plugins.md +78 -5
  31. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/recipes/aider.md +5 -3
  32. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/recipes/pi.md +14 -7
  33. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/runbook.md +63 -22
  34. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/thesis.md +3 -2
  35. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/pyproject.toml +2 -0
  36. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/_test_helpers.py +26 -5
  37. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_run_one_round_with_fake_agent.py +2 -0
  38. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_doc_claims_match_ssot.py +2 -1
  39. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_agent_runtime.py +21 -6
  40. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_observation.py +47 -6
  41. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_claude_error_detector.py +31 -30
  42. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_cli_service_peek_monitor.py +27 -0
  43. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_codewhale_plugin.py +14 -12
  44. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_config.py +59 -0
  45. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_events.py +39 -0
  46. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_gemini_plugin.py +12 -12
  47. cli_agent_runner-0.2.4/tests/unit/test_kimi_plugin.py +147 -0
  48. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_monitor_detectors.py +57 -0
  49. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_monitor_remote.py +9 -0
  50. cli_agent_runner-0.2.4/tests/unit/test_pi_plugin.py +449 -0
  51. cli_agent_runner-0.2.4/tests/unit/test_plugin_constants.py +61 -0
  52. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_presets.py +1 -6
  53. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_round_log_helpers.py +60 -0
  54. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_runner.py +0 -39
  55. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_service_unit.py +9 -0
  56. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_startup_check.py +1 -1
  57. cli_agent_runner-0.2.3/agent_runner/builtin_plugins/_constants.py +0 -61
  58. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.codecov.yml +0 -0
  59. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  60. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  61. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  62. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  63. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.github/workflows/ci.yml +0 -0
  64. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.github/workflows/release.yml +0 -0
  65. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.gitignore +0 -0
  66. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/.vulture-whitelist.py +0 -0
  67. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/BACKLOG.md +0 -0
  68. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/CODE_OF_CONDUCT.md +0 -0
  69. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/CONTRIBUTING.md +0 -0
  70. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/LICENSE +0 -0
  71. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/README.zh.md +0 -0
  72. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/SECURITY.md +0 -0
  73. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/__init__.py +0 -0
  74. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/_docgen.py +0 -0
  75. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/_redact.py +0 -0
  76. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/_registry.py +0 -0
  77. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/_substrate.py +0 -0
  78. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/_throttle.py +0 -0
  79. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/builtin_plugins/__init__.py +0 -0
  80. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  81. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/__init__.py +0 -0
  82. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/__main__.py +0 -0
  83. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/common.py +0 -0
  84. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/events_cmd.py +0 -0
  85. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/init_cmd.py +0 -0
  86. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/install_cmd.py +0 -0
  87. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/peek_cmd.py +0 -0
  88. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/round_cmd.py +0 -0
  89. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/serve_cmd.py +0 -0
  90. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/service_cmd.py +0 -0
  91. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/cli/upgrade_cmd.py +0 -0
  92. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/context_store.py +0 -0
  93. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/defenses.py +0 -0
  94. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/http_progress.py +0 -0
  95. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/lifecycle.py +0 -0
  96. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/metrics.py +0 -0
  97. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/presets/__init__.py +0 -0
  98. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/presets/aider.toml +0 -0
  99. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/presets/claude.toml +0 -0
  100. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/presets/codewhale.toml +0 -0
  101. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/presets/gemini.toml +0 -0
  102. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/presets/kimi.toml +0 -0
  103. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/prompt_loader.py +0 -0
  104. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/round_view.py +0 -0
  105. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/scaffold.py +0 -0
  106. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/agent_runner/vcs_state.py +0 -0
  107. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/build.sh +0 -0
  108. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/README.md +0 -0
  109. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/events.md +0 -0
  110. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/long-running-agents.md +0 -0
  111. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.16.md +0 -0
  112. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.17.md +0 -0
  113. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.19.md +0 -0
  114. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.20.md +0 -0
  115. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.21.md +0 -0
  116. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.22.md +0 -0
  117. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.23.md +0 -0
  118. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.24.md +0 -0
  119. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.25.md +0 -0
  120. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.26.md +0 -0
  121. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.27.md +0 -0
  122. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.28.md +0 -0
  123. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.29.md +0 -0
  124. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.30.md +0 -0
  125. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.31.md +0 -0
  126. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.32.md +0 -0
  127. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.33.md +0 -0
  128. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.34.md +0 -0
  129. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.35.md +0 -0
  130. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.36.md +0 -0
  131. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.37.md +0 -0
  132. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.38.md +0 -0
  133. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.39.md +0 -0
  134. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.40.md +0 -0
  135. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.1.42.md +0 -0
  136. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.2.0.md +0 -0
  137. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.2.1.md +0 -0
  138. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/migrations/0.2.2.md +0 -0
  139. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/quickstart.md +0 -0
  140. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/recipes/codewhale.md +0 -0
  141. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/docs/recipes/kimi.md +0 -0
  142. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/__init__.py +0 -0
  143. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/conftest.py +0 -0
  144. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/contract/__init__.py +0 -0
  145. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/contract/test_public_api_surface.py +0 -0
  146. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/e2e/__init__.py +0 -0
  147. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/e2e/conftest.py +0 -0
  148. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  149. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/e2e/test_e2e_install_systemd.py +0 -0
  150. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  151. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  152. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  153. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  154. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  155. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/generate_vulture_whitelist.py +0 -0
  156. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/__init__.py +0 -0
  157. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_bounded_run.py +0 -0
  158. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_context_enricher_namespacing.py +0 -0
  159. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_dirty_handler_seam.py +0 -0
  160. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_fresh_eyes_signal.py +0 -0
  161. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_grace_kill_emission.py +0 -0
  162. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_install_dry_run.py +0 -0
  163. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_monitor_seeded.py +0 -0
  164. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_plugin_detector_loaded.py +0 -0
  165. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_plugin_owned_paths.py +0 -0
  166. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_plugin_real_flow.py +0 -0
  167. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  168. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_scaffold_presets.py +0 -0
  169. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_serve_loop.py +0 -0
  170. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_substrate_fingerprint.py +0 -0
  171. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/integration/test_transient_error_backoff.py +0 -0
  172. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/__init__.py +0 -0
  173. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/_event_scan.py +0 -0
  174. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_architecture.py +0 -0
  175. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_atomic_write_enforced.py +0 -0
  176. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  177. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_catalogs.py +0 -0
  178. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_classification_ssot.py +0 -0
  179. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_config_error_consistency.py +0 -0
  180. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_config_value_set_ssot.py +0 -0
  181. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  182. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_doc_cli_claims.py +0 -0
  183. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_doc_operator_surface.py +0 -0
  184. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  185. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_docs_generated.py +0 -0
  186. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_docs_index_complete.py +0 -0
  187. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_entry_points_resolve.py +0 -0
  188. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_event_kind_registry.py +0 -0
  189. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_event_kinds_ssot.py +0 -0
  190. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_events_doc_contract.py +0 -0
  191. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_hook_contract_docs.py +0 -0
  192. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_layer_2_loop_size.py +0 -0
  193. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_module_boundaries.py +0 -0
  194. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_module_sizes.py +0 -0
  195. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_no_ai_signatures.py +0 -0
  196. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  197. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_peek_schema_version.py +0 -0
  198. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_plugins_config_stable.py +0 -0
  199. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  200. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  201. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_round_result_stable.py +0 -0
  202. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  203. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  204. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_upstream_schema_canary.py +0 -0
  205. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  206. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/literate/__init__.py +0 -0
  207. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/literate/parser.py +0 -0
  208. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/literate/test_parser.py +0 -0
  209. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/literate/test_quickstart.py +0 -0
  210. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/__init__.py +0 -0
  211. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_agent_runtime_grace.py +0 -0
  212. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_agent_runtime_progress.py +0 -0
  213. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  214. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_assemble_prompt.py +0 -0
  215. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_events_stream.py +0 -0
  216. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_install.py +0 -0
  217. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_read_round_num.py +0 -0
  218. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_resolve_phase.py +0 -0
  219. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_service.py +0 -0
  220. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_api_types.py +0 -0
  221. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_auto_stop_gating.py +0 -0
  222. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_catalogs.py +0 -0
  223. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_cli.py +0 -0
  224. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_cli_common.py +0 -0
  225. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_cli_init_install.py +0 -0
  226. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_cli_monitor_http.py +0 -0
  227. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_cli_upgrade.py +0 -0
  228. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_config_fresh_eyes.py +0 -0
  229. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_config_max_rounds.py +0 -0
  230. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_config_prompt_delivery.py +0 -0
  231. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_config_stop_file.py +0 -0
  232. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  233. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_config_transient_error_action.py +0 -0
  234. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_context_store.py +0 -0
  235. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_default_dirty_handler.py +0 -0
  236. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_defenses.py +0 -0
  237. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_detector_protocol.py +0 -0
  238. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_dirty_handlers.py +0 -0
  239. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_docgen.py +0 -0
  240. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_events_cmd.py +0 -0
  241. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_events_cmd_tail.py +0 -0
  242. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_exit_cause.py +0 -0
  243. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  244. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_hook_failure_isolation.py +0 -0
  245. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_hooks.py +0 -0
  246. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_http_progress.py +0 -0
  247. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_init_entry_points.py +0 -0
  248. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_lifecycle.py +0 -0
  249. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_metrics.py +0 -0
  250. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_monitor_assembly.py +0 -0
  251. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  252. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  253. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  254. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_network_blip_signal_guard.py +0 -0
  255. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_peek_argparse.py +0 -0
  256. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_peek_select.py +0 -0
  257. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_prompt_loader.py +0 -0
  258. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_redact.py +0 -0
  259. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_round_view.py +0 -0
  260. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_runner_throttle.py +0 -0
  261. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_scaffold.py +0 -0
  262. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_serve_cmd_bounded.py +0 -0
  263. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_serve_config_broken.py +0 -0
  264. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_serve_crash_loop.py +0 -0
  265. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_serve_round_log.py +0 -0
  266. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_serve_sentinel.py +0 -0
  267. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_serve_startup_hooks.py +0 -0
  268. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_substrate.py +0 -0
  269. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_throttle_plugin_classification.py +0 -0
  270. {cli_agent_runner-0.2.3 → cli_agent_runner-0.2.4}/tests/unit/test_vcs_state.py +0 -0
@@ -5,9 +5,24 @@ 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.3] - 2026-07-26
8
+ ## [0.2.4] - 2026-07-27
9
+
10
+ ### Added
11
+ - `kimi_error_detector` plugin — classifies transient errors from Kimi Code CLI rounds (`turn.step.retrying` records) so a rate-limited round backs off instead of hot-restarting. No usage events: the CLI's stream-json output carries no token counters.
12
+ - `agent_auth_error_detected` event — a plugin reports an auth failure named by the agent CLI's own structured output, and the `oauth_fail` detector counts those rounds directly. Makes pi's 401 loop visible (pi exits 0); the text-heuristic path keeps its nonzero-exit gate unchanged.
13
+ - `pi_error_detector` plugin — emits per-round token usage (summed across the round's assistant messages, since pi reports usage per message) and classifies transient errors from Pi Coding Agent rounds. pi exits 0 on provider failure, so the classifier reads the final message's `stopReason`/`errorMessage` rather than the exit code.
14
+
15
+ ### Fixed
16
+ - Round-log tail scanning hardened: plugin JSONL parsers filter non-JSON chatter *before* windowing (a stderr burst of any size can no longer evict the terminal event), monitor detectors share the same 200-line window (was 50 — the oauth/network text scans had the identical eviction risk), and each monitor poll reads only the newest 20 round logs instead of every log ever written.
17
+ - A relative `runtime.work_dir` (and every path derived from it) now resolves against the config file's directory instead of the caller's cwd, so `--config /abs/proj/agent-runner.toml` drives `/abs/proj` no matter where the supervisor was launched from.
18
+ - Remote monitor (`monitor --host <alias>`) was silently observing an empty world and reporting healthy — it listed remote filenames over ssh but read every path locally. It now fails loudly at startup (exit 1, with guidance to run the monitor on the host) until remote reads are implemented.
19
+ - Agent round logs (`{log_dir}/rounds/R*-*.log`) are now pruned under `runtime.round_log_retention` at the start of every round — previously that family grew unboundedly, since only the serve-level `round-<N>.log` family was pruned, and only at serve startup.
20
+ - Startup validation resolves the agent command exactly as the spawn does (child's PATH — `[agent.env]` may override it — and `work_dir` base); `PWD` now stays pinned to `work_dir` even if `[agent.env]` sets it. The stdout+stderr merge is now pinned by a behavioral test, not just prose.
21
+ - Generated serve systemd units now set `KillMode=mixed`: with systemd's default `control-group`, `systemctl stop` SIGTERMed the whole cgroup — agent child included — making the graceful round drain structurally ineffective. Existing installs: re-run `agent-runner install` (or add a drop-in) to pick this up.
22
+
23
+ See `docs/migrations/0.2.4.md`.
9
24
 
10
- Details: [docs/migrations/0.2.3.md](docs/migrations/0.2.3.md)
25
+ ## [0.2.3] - 2026-07-26
11
26
 
12
27
  ### Added
13
28
  - `--preset kimi` — a preset for [Kimi Code CLI](https://github.com/MoonshotAI/kimi-code) (`kimi -p --output-format stream-json`). `docs/recipes/kimi.md` also documents running Kimi K3 through the existing `claude` preset via Moonshot's Anthropic-compatible endpoint.
@@ -17,6 +32,8 @@ Details: [docs/migrations/0.2.3.md](docs/migrations/0.2.3.md)
17
32
  - The agent subprocess now runs in `runtime.work_dir` (`cwd=` on spawn). Previously it inherited the supervisor's cwd and only launch conventions (systemd `WorkingDirectory=`, relative `--config`) kept the two aligned — fatal for agent CLIs with no working-directory flag of their own (e.g. `pi`). The startup check now also validates a relative `agent.command[0]` against `work_dir`, matching where it executes.
18
33
  - Plugin round-log tail window widened 50 → 200 lines: a stderr burst after the agent's terminal JSONL event could evict it from the scan window, silently dropping usage/transient classification. The round log's merged stdout+stderr contract is now documented (`HookContext.agent_log_path`, `docs/long-running-agents.md`) — the merge is deliberate; auth/network detection reads stderr text from it.
19
34
 
35
+ See `docs/migrations/0.2.3.md`.
36
+
20
37
  ## [0.2.2] - 2026-07-18
21
38
 
22
39
  ### Removed
@@ -921,7 +938,8 @@ Initial public release on PyPI as `cli-agent-runner`.
921
938
  - Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
922
939
  gated by a manual approval on the `pypi` GitHub environment.
923
940
 
924
- [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.3...HEAD
941
+ [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...HEAD
942
+ [0.2.4]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.3...v0.2.4
925
943
  [0.2.3]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.2...v0.2.3
926
944
  [0.2.2]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.1...v0.2.2
927
945
  [0.2.1]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.0...v0.2.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.2.3
3
+ Version: 0.2.4
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
@@ -116,20 +116,20 @@ Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
116
116
  - `oauth_fail` — burning API quota on auth-rejected rounds
117
117
  - `disk_critical` — writing to a near-full disk risks corruption
118
118
 
119
- Runs locally or against a remote host via ssh:
119
+ Runs against the supervised project's local logs:
120
120
 
121
121
  ```bash
122
- agent-runner monitor # local, 30s poll
123
- agent-runner monitor --host pi # remote, 60s poll
122
+ agent-runner monitor # 30s poll
124
123
  agent-runner monitor --json | jq -c # pipe to downstream consumers
125
124
  ```
126
125
 
127
- > **SSH trust boundary**: `monitor --host <alias>` shells out via plain SSH
128
- > using your local `~/.ssh/config` (aliases, `StrictHostKeyChecking` behavior).
129
- > A monitor with `auto_stop` enabled can issue `agent-runner stop` on the
130
- > remote verify your SSH config before pointing monitor at a remote service.
131
- > See [`docs/runbook.md`](docs/runbook.md) § "Remote monitor & SSH trust" for
132
- > recommended hygiene.
126
+ > **Remote monitoring is unsupported in this version**: `monitor --host <alias>`
127
+ > cannot read a remote host's round logs or events, so it exits with an error at
128
+ > startup instead of watching an empty world and reporting healthy. Run the
129
+ > monitor on the supervised host itself (`ssh <alias>`, then `agent-runner
130
+ > monitor`). See [`docs/runbook.md`](docs/runbook.md) § "Remote monitor & SSH
131
+ > trust" — the SSH trust boundary applies to any agent-runner command you drive
132
+ > over ssh.
133
133
 
134
134
  ## Documentation
135
135
 
@@ -80,20 +80,20 @@ Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
80
80
  - `oauth_fail` — burning API quota on auth-rejected rounds
81
81
  - `disk_critical` — writing to a near-full disk risks corruption
82
82
 
83
- Runs locally or against a remote host via ssh:
83
+ Runs against the supervised project's local logs:
84
84
 
85
85
  ```bash
86
- agent-runner monitor # local, 30s poll
87
- agent-runner monitor --host pi # remote, 60s poll
86
+ agent-runner monitor # 30s poll
88
87
  agent-runner monitor --json | jq -c # pipe to downstream consumers
89
88
  ```
90
89
 
91
- > **SSH trust boundary**: `monitor --host <alias>` shells out via plain SSH
92
- > using your local `~/.ssh/config` (aliases, `StrictHostKeyChecking` behavior).
93
- > A monitor with `auto_stop` enabled can issue `agent-runner stop` on the
94
- > remote verify your SSH config before pointing monitor at a remote service.
95
- > See [`docs/runbook.md`](docs/runbook.md) § "Remote monitor & SSH trust" for
96
- > recommended hygiene.
90
+ > **Remote monitoring is unsupported in this version**: `monitor --host <alias>`
91
+ > cannot read a remote host's round logs or events, so it exits with an error at
92
+ > startup instead of watching an empty world and reporting healthy. Run the
93
+ > monitor on the supervised host itself (`ssh <alias>`, then `agent-runner
94
+ > monitor`). See [`docs/runbook.md`](docs/runbook.md) § "Remote monitor & SSH
95
+ > trust" — the SSH trust boundary applies to any agent-runner command you drive
96
+ > over ssh.
97
97
 
98
98
  ## Documentation
99
99
 
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
  from pathlib import Path
15
15
 
16
16
  __all__ = [
17
+ "emit_agent_auth_error_detected",
17
18
  "emit_agent_usage_recorded",
18
19
  "emit_anomaly_repetitive_tool",
19
20
  "emit_fresh_eyes_round_triggered",
@@ -150,6 +151,38 @@ def emit_transient_error_detected(
150
151
  )
151
152
 
152
153
 
154
+ def emit_agent_auth_error_detected(
155
+ log_dir: Path,
156
+ *,
157
+ round_num: int,
158
+ agent: str,
159
+ raw: str,
160
+ ) -> None:
161
+ """Emit an authentication/authorization failure the agent itself reported.
162
+
163
+ Contract: emitted by a per-CLI plugin only when the agent's OWN structured
164
+ output names the failure (e.g. an HTTP 401 in its JSON event stream). That
165
+ is certain evidence, unlike the monitor's ``oauth_fail`` text heuristic,
166
+ which scans free-text log tails and therefore needs a nonzero-exit shield
167
+ against prose that merely mentions "401". The monitor counts a round
168
+ carrying this event without that shield — which is what makes an auth loop
169
+ visible for a CLI that exits 0 on provider failure.
170
+
171
+ No back-off partner event: an auth failure is permanent until an operator
172
+ fixes the credential, so it is deliberately not a transient classification.
173
+ """
174
+ from agent_runner._redact import redact_secrets
175
+ from agent_runner.events import AGENT_AUTH_ERROR_DETECTED, emit
176
+
177
+ emit(
178
+ log_dir,
179
+ AGENT_AUTH_ERROR_DETECTED,
180
+ round_num=round_num,
181
+ agent=agent,
182
+ raw=redact_secrets(raw),
183
+ )
184
+
185
+
153
186
  def emit_transient_error_recovered(
154
187
  log_dir: Path,
155
188
  *,
@@ -205,7 +238,10 @@ def emit_agent_usage_recorded(
205
238
  Claude: count of ``tool_use`` content blocks across all assistant events.
206
239
  Gemini: ``stats.tool_calls``.
207
240
  - ``phase``: phase label from HookContext (e.g. "planning"); empty string when None.
208
- - ``success``: True when exit_code == 0 and not timed_out.
241
+ - ``success``: the supervisor's clean-exit predicate (``RoundResult.ok``).
242
+ A plugin MAY additionally fold in the agent's own terminal verdict when
243
+ its CLI's exit code is unreliable — pi does, because pi exits 0 on
244
+ provider failure.
209
245
  """
210
246
  from agent_runner.events import AGENT_USAGE_RECORDED, emit
211
247
 
@@ -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.3'
22
- __version_tuple__ = version_tuple = (0, 2, 3)
21
+ __version__ = version = '0.2.4'
22
+ __version_tuple__ = version_tuple = (0, 2, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import os
13
13
  import re
14
+ import shutil
14
15
  import signal
15
16
  import subprocess # noqa: TID251 — sanctioned subprocess caller
16
17
  import threading
@@ -52,6 +53,15 @@ class RunResult:
52
53
  killed_for_grace: bool = False
53
54
  grace_kill_children: list[dict] = field(default_factory=list)
54
55
 
56
+ @property
57
+ def ok(self) -> bool:
58
+ """The supervisor's round-success predicate: clean exit, no timeout.
59
+
60
+ Single definition of "did this round fail" at the supervisor level. A
61
+ plugin MAY narrow it further when its CLI's exit code is unreliable.
62
+ """
63
+ return self.exit_code == 0 and not self.timed_out
64
+
55
65
 
56
66
  def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
57
67
  """Build full argv: command + prompt args (with {prompt} substituted)."""
@@ -130,6 +140,24 @@ def _live_children(
130
140
  _RESULT_MARKER = b'"type":"result"'
131
141
 
132
142
 
143
+ def resolve_exec_target(command0: str, work_dir: Path, env_path: str | None = None) -> str | None:
144
+ """Model of Popen's POSIX exec resolution for argv[0].
145
+
146
+ Kept beside the ``Popen`` in :func:`run` so validation and exec cannot
147
+ drift: a slash-containing argv[0] resolves against the child's cwd
148
+ (``work_dir``); a bare name is looked up on the CHILD's PATH
149
+ (``env_path`` — pass ``[agent.env]``'s PATH override if set; ``None``
150
+ falls back to the supervisor's, matching env inheritance). Returns the
151
+ resolved executable path, or ``None`` if it would not exec.
152
+ """
153
+ if "/" in command0:
154
+ candidate = Path(command0)
155
+ if not candidate.is_absolute():
156
+ candidate = work_dir / candidate
157
+ return shutil.which(str(candidate))
158
+ return shutil.which(command0, path=env_path)
159
+
160
+
133
161
  def run(
134
162
  *,
135
163
  command: list[str],
@@ -150,11 +178,9 @@ def run(
150
178
 
151
179
  Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
152
180
 
153
- work_dir: the agent child's working directory (required). Before 0.2.3 the
154
- child inherited the supervisor's cwd and only launch conventions (systemd
155
- WorkingDirectory=, relative --config) kept the two aligned — fatal for CLIs
156
- with no --cwd flag of their own (e.g. pi). Callers pass the already-absolute
157
- cfg.runtime.work_dir.
181
+ work_dir: the agent child's working directory; callers pass the
182
+ already-absolute cfg.runtime.work_dir. CLIs with no --cwd flag of their
183
+ own (e.g. pi) depend on this.
158
184
 
159
185
  max_grace_after_result_s: when > 0, start a countdown after the first
160
186
  type=result event is detected in the log. After it elapses, reap the
@@ -182,8 +208,9 @@ def run(
182
208
  if stdin_mode
183
209
  else _build_argv(command, prompt_arg_template, prompt)
184
210
  )
185
- # PWD pinned alongside cwd= so shell-reported paths agree; env_extra wins.
186
- env = {**os.environ, "PWD": str(work_dir), **env_extra}
211
+ # PWD pinned last — it mirrors cwd= (a correctness pin, not a knob), so
212
+ # an [agent.env] PWD cannot silently diverge from where the child runs.
213
+ env = {**os.environ, **env_extra, "PWD": str(work_dir)}
187
214
  log_path.parent.mkdir(parents=True, exist_ok=True)
188
215
  log_file = log_path.open("w", encoding="utf-8")
189
216
  start = time.time()
@@ -486,6 +486,8 @@ def _poll_once(project: str | Path, *, host: str | None) -> list[monitor.Alert]:
486
486
  if host is None:
487
487
  src = monitor.LocalSource(log_dir=cfg.runtime.log_dir)
488
488
  else:
489
+ # Dormant: monitor_loop rejects --host before any poll, so this branch
490
+ # is unreachable today. See monitor.MonitorRemoteUnsupportedError.
489
491
  src = monitor.RemoteSource(host=host, project=_project_name(work_dir))
490
492
  events = monitor.parse_events_from_jsonl_files(src.events_files())
491
493
  metrics = monitor.parse_events_from_jsonl_files(src.metrics_files())
@@ -525,12 +527,29 @@ def monitor_loop(
525
527
  to that kind as the canonical "supervision is up" signal (monitor is otherwise
526
528
  silent during healthy operation by design).
527
529
 
530
+ ``host`` (remote mode) raises ``MonitorRemoteUnsupportedError`` immediately:
531
+ remote reads are unimplemented, so a remote loop would poll an empty world
532
+ and report healthy forever. The check is eager — this wrapper validates
533
+ before handing back the generator, so the failure lands at startup rather
534
+ than at the first ``next()``.
535
+ """
536
+ if host is not None:
537
+ raise monitor.MonitorRemoteUnsupportedError(host)
538
+ return _monitor_loop_iter(project, host=host, interval_s=interval_s)
539
+
540
+
541
+ def _monitor_loop_iter(
542
+ project: str | Path | None = None, *, host: str | None = None, interval_s: int = 30
543
+ ) -> Iterator[monitor.Alert]:
544
+ """Polling generator behind ``monitor_loop``.
545
+
528
546
  Tolerates transient ``MonitorRemoteError`` failures (from ``--host`` ssh)
529
547
  for up to ``cfg.monitor.remote_failure_tolerance_s`` seconds with exponential
530
548
  backoff (1s → 2s → 4s → ... → 30s cap). Each retry emits ``monitor_remote_blip``;
531
549
  crossing the cap emits one ``monitor_remote_giveup`` and propagates the error
532
550
  (CLI exits 1; systemd restarts the process). Setting tolerance to 0 preserves
533
- the 0.1.10 immediate-propagate behavior with no blip events emitted.
551
+ the 0.1.10 immediate-propagate behavior with no blip events emitted. That
552
+ path is dormant while remote mode is rejected at startup.
534
553
  """
535
554
  import json as _json
536
555
 
@@ -771,6 +790,7 @@ def check_self_terminated_sentinel(log_dir: Path) -> bool:
771
790
  # Re-export emit_* wrappers from _emit module (extracted for size hygiene).
772
791
  # Preserves the public import surface: `from agent_runner.api import emit_*` continues to work.
773
792
  from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-export
793
+ emit_agent_auth_error_detected,
774
794
  emit_agent_usage_recorded,
775
795
  emit_anomaly_repetitive_tool,
776
796
  emit_config_broken,
@@ -173,6 +173,15 @@ class RoundResult:
173
173
  dirty_outcome: DirtyOutcome | None = None
174
174
  killed_for_grace: bool = False
175
175
 
176
+ @property
177
+ def ok(self) -> bool:
178
+ """The supervisor's round-success predicate: clean exit, no timeout.
179
+
180
+ Single definition of "did this round fail" at the supervisor level. A
181
+ plugin MAY narrow it further when its CLI's exit code is unreliable.
182
+ """
183
+ return self.exit_code == 0 and not self.timed_out
184
+
176
185
 
177
186
  @dataclass(frozen=True)
178
187
  class InitResult:
@@ -0,0 +1,125 @@
1
+ """Shared constants and tail-scan helpers for built-in CLI plugins.
2
+
3
+ Extracted to single source of truth so every round-log tail-scanner
4
+ (plugin JSONL parsers and monitor text detectors) uses the same window
5
+ size, raw-text caps, transient-error back-off defaults, and the same
6
+ HTTP-status → classification ladder.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections import deque
13
+ from collections.abc import Iterator
14
+ from pathlib import Path
15
+ from typing import Any, TextIO
16
+
17
+ _TAIL_LINES: int = 200
18
+ """Number of log lines to scan from the end of a round log.
19
+
20
+ The round log is merged stdout+stderr. JSONL consumers window via
21
+ ``json_tail`` (chatter filtered before windowing); plain-text consumers
22
+ (monitor oauth/network detectors) window the raw tail — keep this generous
23
+ so a stderr burst cannot evict what they scan for."""
24
+
25
+
26
+ def json_tail(f: TextIO, maxlen: int = _TAIL_LINES) -> deque[str]:
27
+ """Last ``maxlen`` JSON-looking lines of a merged round log.
28
+
29
+ Non-JSON chatter (stderr text) is filtered BEFORE windowing, so a burst
30
+ of any size cannot evict the terminal JSONL event from the window.
31
+ """
32
+ return deque((ln for ln in f if ln.lstrip()[:1] in "{["), maxlen=maxlen)
33
+
34
+
35
+ def json_events(log_path: Path) -> Iterator[dict]:
36
+ """JSON objects in a round log's tail window, in file order.
37
+
38
+ One reader for every plugin's parse loop: window via ``json_tail``, then
39
+ per line strip / skip blank / ``json.loads`` / drop anything that is not an
40
+ object. Non-JSON lines are expected — the round log merges stdout+stderr,
41
+ and every CLI writes some plain text there.
42
+ """
43
+ with log_path.open("r", encoding="utf-8", errors="replace") as f:
44
+ tail = json_tail(f)
45
+ for line in tail:
46
+ line = line.strip()
47
+ if not line:
48
+ continue
49
+ try:
50
+ event = json.loads(line)
51
+ except json.JSONDecodeError:
52
+ continue
53
+ if isinstance(event, dict):
54
+ yield event
55
+
56
+
57
+ _RAW_CAP: int = 200
58
+ """Maximum length for ``raw`` field in transient_error_detected payload."""
59
+
60
+ # Default back-off durations (seconds) for non-precise transient classifications.
61
+ # rate_limit_account uses exact resetsAt epoch from claude; not in this table.
62
+ _BACK_OFF_DEFAULTS: dict[str, int] = {
63
+ "rate_limit_model": 60,
64
+ "api_transient_5xx": 60,
65
+ "api_timeout": 30,
66
+ }
67
+
68
+ # 5xx codes treated as transient (retry-worthy server errors per RFC 9110):
69
+ # 500=unexpected, 502=bad gateway, 503=unavailable, 504=gateway timeout,
70
+ # 529=overloaded (Anthropic's non-RFC code emitted during sustained capacity
71
+ # issues; treated as transient per Anthropic SDK behavior).
72
+ # Excluded: 501 (not implemented = permanent), 505 (HTTP version mismatch).
73
+ _5XX_STATUSES: frozenset[int] = frozenset({500, 502, 503, 504, 529})
74
+
75
+
76
+ def classify_transient_status(status: Any) -> str | None:
77
+ """Map an HTTP status from a CLI's error record to a transient bucket.
78
+
79
+ The one ladder every plugin classifier delegates to, so a status is
80
+ bucketed identically no matter which CLI reported it.
81
+
82
+ ``None`` means 'not transient — do not back off'. Notably 401 (auth) and
83
+ 404 (unknown model) land here: both are permanent until an operator fixes
84
+ configuration, which is oauth/config territory rather than something a
85
+ back-off would clear. Non-integer or missing statuses are ``None`` too;
86
+ a CLI that reports no status gets no classification.
87
+ """
88
+ if not isinstance(status, int) or isinstance(status, bool):
89
+ return None
90
+ if status == 429:
91
+ return "rate_limit_model"
92
+ if status in _5XX_STATUSES:
93
+ return "api_transient_5xx"
94
+ if status == 408:
95
+ return "api_timeout"
96
+ return None
97
+
98
+
99
+ _CLASSIFICATIONS: frozenset[str] = frozenset(
100
+ {
101
+ "rate_limit_account",
102
+ "rate_limit_model",
103
+ "api_transient_5xx",
104
+ "api_timeout",
105
+ }
106
+ )
107
+ """Canonical set of transient-error classifications.
108
+
109
+ rate_limit_account uses server-provided resetsAt (excluded from
110
+ _BACK_OFF_DEFAULTS table); others use defaults from that table.
111
+ """
112
+
113
+ _EXP_CAP: int = 5
114
+ """Maximum exponent for transient-error consecutive backoff: 2^5 = 32×.
115
+
116
+ Beyond this, the multiplier plateaus. Combined with _ABSOLUTE_CAP_S, this
117
+ prevents runaway wait times during sustained outages (max wait = 30min).
118
+ """
119
+
120
+ _ABSOLUTE_CAP_S: int = 1800
121
+ """Absolute upper bound on supervisor-applied transient back-off (30 min).
122
+
123
+ Applies after exp multiplier — even if base × 2^5 exceeds this, the wait
124
+ is clipped here. Defends against an indefinitely-stuck supervisor.
125
+ """
@@ -14,9 +14,7 @@ rate-limit detector was generalized to multi-classification in 0.1.23
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import json
18
17
  import time
19
- from collections import deque
20
18
  from pathlib import Path
21
19
  from typing import Any
22
20
 
@@ -26,10 +24,10 @@ from agent_runner.api import (
26
24
  emit_transient_error_detected,
27
25
  )
28
26
  from agent_runner.builtin_plugins._constants import (
29
- _5XX_STATUSES,
30
27
  _BACK_OFF_DEFAULTS,
31
28
  _RAW_CAP,
32
- _TAIL_LINES,
29
+ classify_transient_status,
30
+ json_events,
33
31
  )
34
32
  from agent_runner.hooks import HookContext, register_post_round_hook
35
33
 
@@ -60,7 +58,7 @@ class ClaudeErrorDetector:
60
58
  ctx.log_dir,
61
59
  round_num=ctx.round_num,
62
60
  phase=ctx.phase or "",
63
- success=(result.exit_code == 0 and not result.timed_out),
61
+ success=result.ok,
64
62
  **parsed["usage"],
65
63
  )
66
64
 
@@ -114,26 +112,17 @@ def _parse_claude_log(
114
112
  anomaly_window: int = 0,
115
113
  anomaly_threshold: int = 0,
116
114
  ) -> dict[str, Any]:
117
- """Scan last _TAIL_LINES for rate_limit/result/assistant events.
115
+ """Scan the JSON tail window for rate_limit/result/assistant events.
118
116
 
119
117
  Returns dict with optional 'transient_error', 'usage', and 'anomaly' keys.
120
118
  anomaly_window/anomaly_threshold: when both > 0, slide a window over
121
119
  (tool_name, target) tuples; populate 'anomaly' if threshold reached.
122
120
  """
123
- with log_path.open("r", encoding="utf-8", errors="replace") as f:
124
- tail = deque(f, maxlen=_TAIL_LINES)
125
121
  rate_limit_info: dict | None = None
126
122
  result_event: dict | None = None
127
123
  assistant_model: str | None = None
128
124
  tool_calls: list[tuple[str, str | None]] = []
129
- for line in tail:
130
- line = line.strip()
131
- if not line:
132
- continue
133
- try:
134
- event = json.loads(line)
135
- except json.JSONDecodeError:
136
- continue
125
+ for event in json_events(log_path):
137
126
  event_type = event.get("type")
138
127
  if event_type == "rate_limit_event":
139
128
  rli = event.get("rate_limit_info", {})
@@ -179,8 +168,8 @@ def _parse_claude_log(
179
168
  def _classify_transient_error(
180
169
  rate_limit_info: dict | None, result_event: dict | None
181
170
  ) -> dict | None:
182
- """Refactored from prior _scan_log_for_transient_error 0.1.23 logic; same shape, same
183
- priority (rate_limit_event.rejected > 429 > 5xx > 408).
171
+ """Claude-specific precedence: an account-level rate_limit_event outranks the
172
+ result event's ``api_error_status``, which is handed to the shared ladder.
184
173
  """
185
174
  if rate_limit_info is not None and rate_limit_info.get("rateLimitType") == "five_hour":
186
175
  return {
@@ -193,15 +182,10 @@ def _classify_transient_error(
193
182
  # classification below.
194
183
  if result_event is None or result_event.get("is_error") is not True:
195
184
  return None
196
- status = result_event.get("api_error_status")
197
- raw = str(result_event.get("result", ""))[:_RAW_CAP]
198
- if status == 429:
199
- return _classify("rate_limit_model", raw)
200
- if status in _5XX_STATUSES:
201
- return _classify("api_transient_5xx", raw)
202
- if status == 408:
203
- return _classify("api_timeout", raw)
204
- return None
185
+ classification = classify_transient_status(result_event.get("api_error_status"))
186
+ if classification is None:
187
+ return None
188
+ return _classify(classification, str(result_event.get("result", ""))[:_RAW_CAP])
205
189
 
206
190
 
207
191
  def _extract_usage(result_event: dict, *, model: str | None, tool_call_count: int) -> dict | None:
@@ -12,9 +12,7 @@ is added when a real rate-limit sample is captured.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- import json
16
15
  import time
17
- from collections import deque
18
16
  from pathlib import Path
19
17
  from typing import Any
20
18
 
@@ -23,10 +21,10 @@ from agent_runner.api import (
23
21
  emit_transient_error_detected,
24
22
  )
25
23
  from agent_runner.builtin_plugins._constants import (
26
- _5XX_STATUSES,
27
24
  _BACK_OFF_DEFAULTS,
28
25
  _RAW_CAP,
29
- _TAIL_LINES,
26
+ classify_transient_status,
27
+ json_events,
30
28
  )
31
29
  from agent_runner.hooks import HookContext, register_post_round_hook
32
30
 
@@ -52,32 +50,22 @@ class CodewhaleErrorDetector:
52
50
  ctx.log_dir,
53
51
  round_num=ctx.round_num,
54
52
  phase=ctx.phase or "",
55
- success=(result.exit_code == 0 and not result.timed_out),
53
+ success=result.ok,
56
54
  **parsed["usage"],
57
55
  )
58
56
 
59
57
 
60
58
  def _parse_codewhale_log(log_path: Path) -> dict[str, Any]:
61
- """Scan last _TAIL_LINES of codewhale NDJSON; extract usage from the metadata
59
+ """Scan the JSON tail window of codewhale NDJSON; extract usage from the metadata
62
60
  record; classify any {"type":"error"} that maps to a transient bucket.
63
61
 
64
- Tolerates non-JSON lines (codewhale prefixes some stdout with terminal
65
- escapes) via per-line try/except.
62
+ codewhale prefixes some stdout lines with terminal escapes, so the
63
+ non-JSON lines ``json_events`` drops are routine here, not a corruption
64
+ signal.
66
65
  """
67
- with log_path.open("r", encoding="utf-8", errors="replace") as f:
68
- tail = deque(f, maxlen=_TAIL_LINES)
69
66
  metadata: dict | None = None
70
67
  error_event: dict | None = None
71
- for line in tail:
72
- line = line.strip()
73
- if not line:
74
- continue
75
- try:
76
- event = json.loads(line)
77
- except json.JSONDecodeError:
78
- continue
79
- if not isinstance(event, dict):
80
- continue
68
+ for event in json_events(log_path):
81
69
  etype = event.get("type")
82
70
  if etype == "metadata":
83
71
  metadata = event.get("meta") or {}
@@ -111,23 +99,15 @@ def _parse_codewhale_log(log_path: Path) -> dict[str, Any]:
111
99
 
112
100
 
113
101
  def _classify_codewhale_error(error_event: dict[str, Any]) -> str | None:
114
- """Map a codewhale {"type":"error"} record to a transient bucket, or None.
115
-
116
- None means 'not a transient error' (e.g. auth failure -> handled by the
117
- monitor's oauth_fail log-scan, not the transient classifier). codewhale's
118
- error record currently carries only a free-text 'error' string with no
119
- status code; until a real rate-limit/5xx sample is captured we cannot map
120
- to rate_limit_model / api_transient_5xx / api_timeout, so we return None.
121
- A future revision keys on a numeric status field once observed.
102
+ """Pull a status code out of a codewhale {"type":"error"} record for the
103
+ shared ladder.
104
+
105
+ The only error record captured so far carries a free-text 'error' string
106
+ and no status code at all (an auth failure), so nothing maps today; the
107
+ numeric-field lookup is the forward path for when a real rate-limit or 5xx
108
+ sample is captured.
122
109
  """
123
- code = error_event.get("code") or error_event.get("status_code")
124
- if code == 429:
125
- return "rate_limit_model"
126
- if code in _5XX_STATUSES:
127
- return "api_transient_5xx"
128
- if code == 408:
129
- return "api_timeout"
130
- return None
110
+ return classify_transient_status(error_event.get("code") or error_event.get("status_code"))
131
111
 
132
112
 
133
113
  register_post_round_hook(CodewhaleErrorDetector())