cli-agent-runner 0.1.41__tar.gz → 0.1.42__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 (235) hide show
  1. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/CHANGELOG.md +15 -0
  2. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/PKG-INFO +5 -5
  3. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/README.md +4 -4
  4. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/README.zh.md +5 -5
  5. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/_emit.py +23 -0
  6. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/_version.py +2 -2
  7. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/api.py +56 -1
  8. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/serve_cmd.py +26 -5
  9. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/defenses.py +12 -2
  10. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/events.py +2 -0
  11. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/monitor.py +0 -25
  12. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/runner.py +5 -2
  13. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/vcs_state.py +51 -3
  14. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/architecture.md +7 -5
  15. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/commands.md +1 -1
  16. cli_agent_runner-0.1.42/docs/migrations/0.1.42.md +58 -0
  17. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/thesis.md +12 -7
  18. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/_test_helpers.py +8 -3
  19. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_bounded_run.py +10 -2
  20. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_fresh_eyes_signal.py +2 -0
  21. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_substrate_fingerprint.py +5 -1
  22. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_architecture.py +4 -1
  23. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_defenses.py +2 -2
  24. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_monitor_detectors.py +1 -18
  25. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_runner.py +2 -2
  26. cli_agent_runner-0.1.42/tests/unit/test_serve_config_broken.py +33 -0
  27. cli_agent_runner-0.1.42/tests/unit/test_serve_crash_loop.py +128 -0
  28. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_vcs_state.py +69 -0
  29. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.codecov.yml +0 -0
  30. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  32. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  33. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  34. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.github/workflows/ci.yml +0 -0
  35. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.github/workflows/release.yml +0 -0
  36. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.gitignore +0 -0
  37. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/.vulture-whitelist.py +0 -0
  38. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/CODE_OF_CONDUCT.md +0 -0
  39. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/CONTRIBUTING.md +0 -0
  40. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/LICENSE +0 -0
  41. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/SECURITY.md +0 -0
  42. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/__init__.py +0 -0
  43. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/_docgen.py +0 -0
  44. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/_redact.py +0 -0
  45. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/_registry.py +0 -0
  46. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/_substrate.py +0 -0
  47. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/_throttle.py +0 -0
  48. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/agent_runtime.py +0 -0
  49. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/api_types.py +0 -0
  50. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/builtin_plugins/__init__.py +0 -0
  51. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/builtin_plugins/_constants.py +0 -0
  52. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  53. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/builtin_plugins/codewhale.py +0 -0
  54. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/builtin_plugins/gemini.py +0 -0
  55. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/__init__.py +0 -0
  56. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/__main__.py +0 -0
  57. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/common.py +0 -0
  58. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/events_cmd.py +0 -0
  59. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/init_cmd.py +0 -0
  60. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/install_cmd.py +0 -0
  61. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/monitor_cmd.py +0 -0
  62. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/peek_cmd.py +0 -0
  63. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/round_cmd.py +0 -0
  64. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/service_cmd.py +0 -0
  65. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/cli/upgrade_cmd.py +0 -0
  66. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/config.py +0 -0
  67. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/context_store.py +0 -0
  68. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/detector_helpers.py +0 -0
  69. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/hooks.py +0 -0
  70. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/http_progress.py +0 -0
  71. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/lifecycle.py +0 -0
  72. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/metrics.py +0 -0
  73. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/presets/__init__.py +0 -0
  74. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/presets/aider.toml +0 -0
  75. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/presets/claude.toml +0 -0
  76. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/presets/codewhale.toml +0 -0
  77. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/presets/gemini.toml +0 -0
  78. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/prompt_loader.py +0 -0
  79. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/round_log.py +0 -0
  80. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/round_view.py +0 -0
  81. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/scaffold.py +0 -0
  82. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/service_unit.py +0 -0
  83. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/agent_runner/startup_check.py +0 -0
  84. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/build.sh +0 -0
  85. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/deploy/example-agent-runner.toml +0 -0
  86. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/deploy/launchd.plist.tmpl +0 -0
  87. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/deploy/run-loop.sh +0 -0
  88. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/deploy/systemd.service.tmpl +0 -0
  89. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/README.md +0 -0
  90. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/configuration.md +0 -0
  91. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/events.md +0 -0
  92. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/long-running-agents.md +0 -0
  93. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/marketing/README.md +0 -0
  94. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/marketing/promo-cn.html +0 -0
  95. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.16.md +0 -0
  96. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.17.md +0 -0
  97. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.19.md +0 -0
  98. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.20.md +0 -0
  99. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.21.md +0 -0
  100. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.22.md +0 -0
  101. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.23.md +0 -0
  102. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.24.md +0 -0
  103. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.25.md +0 -0
  104. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.26.md +0 -0
  105. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.27.md +0 -0
  106. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.28.md +0 -0
  107. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.29.md +0 -0
  108. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.30.md +0 -0
  109. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.31.md +0 -0
  110. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.32.md +0 -0
  111. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.33.md +0 -0
  112. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.34.md +0 -0
  113. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.35.md +0 -0
  114. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.36.md +0 -0
  115. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.37.md +0 -0
  116. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.38.md +0 -0
  117. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.39.md +0 -0
  118. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/migrations/0.1.40.md +0 -0
  119. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/plugins.md +0 -0
  120. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/quickstart.md +0 -0
  121. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/recipes/aider.md +0 -0
  122. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/recipes/codewhale.md +0 -0
  123. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/docs/runbook.md +0 -0
  124. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/pyproject.toml +0 -0
  125. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/__init__.py +0 -0
  126. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/conftest.py +0 -0
  127. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/contract/__init__.py +0 -0
  128. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/contract/test_public_api_surface.py +0 -0
  129. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/e2e/__init__.py +0 -0
  130. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/e2e/conftest.py +0 -0
  131. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  132. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/e2e/test_e2e_install_systemd.py +0 -0
  133. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  134. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  135. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  136. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  137. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  138. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/__init__.py +0 -0
  139. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_context_enricher_namespacing.py +0 -0
  140. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_grace_kill_emission.py +0 -0
  141. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_install_dry_run.py +0 -0
  142. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_monitor_seeded.py +0 -0
  143. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_plugin_detector_loaded.py +0 -0
  144. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_plugin_owned_paths.py +0 -0
  145. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_plugin_real_flow.py +0 -0
  146. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  147. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_scaffold_presets.py +0 -0
  148. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_serve_loop.py +0 -0
  149. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/integration/test_transient_error_backoff.py +0 -0
  150. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/__init__.py +0 -0
  151. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_atomic_write_enforced.py +0 -0
  152. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_catalogs.py +0 -0
  153. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_classification_ssot.py +0 -0
  154. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  155. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_docs_generated.py +0 -0
  156. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_entry_points_resolve.py +0 -0
  157. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_event_kind_registry.py +0 -0
  158. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_event_kinds_ssot.py +0 -0
  159. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_events_doc_contract.py +0 -0
  160. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_layer_2_loop_size.py +0 -0
  161. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_module_boundaries.py +0 -0
  162. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_module_sizes.py +0 -0
  163. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_no_ai_signatures.py +0 -0
  164. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  165. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_peek_schema_version.py +0 -0
  166. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  167. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_round_result_stable.py +0 -0
  168. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  169. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/invariants/test_upstream_schema_canary.py +0 -0
  170. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/literate/__init__.py +0 -0
  171. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/literate/parser.py +0 -0
  172. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/literate/test_parser.py +0 -0
  173. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/literate/test_quickstart.py +0 -0
  174. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/__init__.py +0 -0
  175. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_agent_runtime.py +0 -0
  176. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_agent_runtime_grace.py +0 -0
  177. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_agent_runtime_progress.py +0 -0
  178. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_assemble_prompt.py +0 -0
  179. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_events_stream.py +0 -0
  180. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_install.py +0 -0
  181. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_observation.py +0 -0
  182. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_read_round_num.py +0 -0
  183. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_resolve_phase.py +0 -0
  184. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_service.py +0 -0
  185. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_api_types.py +0 -0
  186. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_auto_stop_gating.py +0 -0
  187. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_claude_error_detector.py +0 -0
  188. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_cli.py +0 -0
  189. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_cli_common.py +0 -0
  190. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_cli_init_install.py +0 -0
  191. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_cli_monitor_http.py +0 -0
  192. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  193. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_cli_upgrade.py +0 -0
  194. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_codewhale_plugin.py +0 -0
  195. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_config.py +0 -0
  196. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_config_fresh_eyes.py +0 -0
  197. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_config_max_rounds.py +0 -0
  198. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_config_stop_file.py +0 -0
  199. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  200. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_config_transient_error_action.py +0 -0
  201. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_context_store.py +0 -0
  202. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_detector_helpers.py +0 -0
  203. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_detector_protocol.py +0 -0
  204. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_docgen.py +0 -0
  205. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_events.py +0 -0
  206. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_events_cmd.py +0 -0
  207. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  208. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_gemini_plugin.py +0 -0
  209. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_hook_failure_isolation.py +0 -0
  210. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_hooks.py +0 -0
  211. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_http_progress.py +0 -0
  212. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_init_entry_points.py +0 -0
  213. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_lifecycle.py +0 -0
  214. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_metrics.py +0 -0
  215. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_monitor_assembly.py +0 -0
  216. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  217. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  218. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  219. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_monitor_remote.py +0 -0
  220. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_peek_argparse.py +0 -0
  221. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_peek_select.py +0 -0
  222. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_presets.py +0 -0
  223. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_prompt_loader.py +0 -0
  224. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_redact.py +0 -0
  225. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_round_log_helpers.py +0 -0
  226. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_round_view.py +0 -0
  227. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_runner_throttle.py +0 -0
  228. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_scaffold.py +0 -0
  229. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_serve_cmd_bounded.py +0 -0
  230. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_serve_round_log.py +0 -0
  231. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_serve_sentinel.py +0 -0
  232. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_serve_startup_hooks.py +0 -0
  233. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_service_unit.py +0 -0
  234. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_startup_check.py +0 -0
  235. {cli_agent_runner-0.1.41 → cli_agent_runner-0.1.42}/tests/unit/test_substrate.py +0 -0
@@ -5,6 +5,21 @@ 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.1.42] - 2026-06-25
9
+
10
+ ### Added
11
+ - `crash_loop` defense — serve stops after 5 consecutive *unknown* short crashes (non-zero exit, <60s, no classified transient), escalating the restart delay and recording the failure reason. Ends the respawn-forever crash loop; recoverable-slow failures (rate-limit / quota / 5xx / timeout) still ride the transient-error backoff unchanged.
12
+ - `config_broken` defense — a permanent startup-battery failure now halts serve (distinct no-retry exit code `78`) instead of respawning a broken config every round.
13
+
14
+ ### Fixed
15
+ - `vcs.dirty_action` no longer sweeps the runner's own `log_dir` bookkeeping when `log_dir` is inside `work_dir`: `auto_commit` excludes it from the commit (no more phantom `git_head` advance on a zero-work round) and `stash` excludes it from `git stash push -u` (logs no longer vanish). `.evolving/` and agent work are unaffected.
16
+
17
+ ### Removed
18
+ - The inert `smoke_fail_rate` monitor alert (could never fire — superseded by the always-on `config_broken` stop). Monitor now ships 11 detectors.
19
+
20
+ ### Docs
21
+ - `thesis.md`: the stuck-loop defense is described honestly as a notify-level, opt-in-to-auto-stop monitor detector (`anomaly_repetitive_active`), not a default hard-stop; fixed the `stuck_loop_detected` naming drift.
22
+
8
23
  ## [0.1.41] - 2026-06-07
9
24
 
10
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.41
3
+ Version: 0.1.42
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
@@ -49,7 +49,7 @@ full disks, runaway memory.
49
49
 
50
50
  ```
51
51
  ┌──────────────────────────────────────────┐
52
- │ Layer 3: The Witness (monitor) │ 12 detectors + auto-stop
52
+ │ Layer 3: The Witness (monitor) │ 11 detectors + auto-stop
53
53
  ├──────────────────────────────────────────┤
54
54
  │ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
55
55
  ├──────────────────────────────────────────┤
@@ -86,14 +86,14 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
86
86
  |---|---|
87
87
  | `init` / `install` / `uninstall` | `peek` — state snapshot |
88
88
  | `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
89
- | `restart` / `status` | `monitor` — 12 detectors, alerts, auto-stop |
89
+ | `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
90
90
  | `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
91
91
 
92
92
  Verb reference: [`docs/commands.md`](docs/commands.md).
93
93
 
94
94
  ## Defenses (built in)
95
95
 
96
- 11 named defenses, structured as data — see `agent-runner peek --select defenses`.
96
+ 12 named defenses, structured as data — see `agent-runner peek --select defenses`.
97
97
  Each carries the historical incident it codifies and the invariant test that
98
98
  guards it. Highlights:
99
99
 
@@ -106,7 +106,7 @@ guards it. Highlights:
106
106
 
107
107
  Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
108
108
 
109
- ## Monitor: 12 detectors
109
+ ## Monitor: 11 detectors
110
110
 
111
111
  Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
112
112
  `mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
@@ -12,7 +12,7 @@ full disks, runaway memory.
12
12
 
13
13
  ```
14
14
  ┌──────────────────────────────────────────┐
15
- │ Layer 3: The Witness (monitor) │ 12 detectors + auto-stop
15
+ │ Layer 3: The Witness (monitor) │ 11 detectors + auto-stop
16
16
  ├──────────────────────────────────────────┤
17
17
  │ Layer 2: The Loop (serve, ~120 LOC) │ signal-trapping restart loop
18
18
  ├──────────────────────────────────────────┤
@@ -49,14 +49,14 @@ Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
49
49
  |---|---|
50
50
  | `init` / `install` / `uninstall` | `peek` — state snapshot |
51
51
  | `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
52
- | `restart` / `status` | `monitor` — 12 detectors, alerts, auto-stop |
52
+ | `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
53
53
  | `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
54
54
 
55
55
  Verb reference: [`docs/commands.md`](docs/commands.md).
56
56
 
57
57
  ## Defenses (built in)
58
58
 
59
- 11 named defenses, structured as data — see `agent-runner peek --select defenses`.
59
+ 12 named defenses, structured as data — see `agent-runner peek --select defenses`.
60
60
  Each carries the historical incident it codifies and the invariant test that
61
61
  guards it. Highlights:
62
62
 
@@ -69,7 +69,7 @@ guards it. Highlights:
69
69
 
70
70
  Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
71
71
 
72
- ## Monitor: 12 detectors
72
+ ## Monitor: 11 detectors
73
73
 
74
74
  Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
75
75
  `mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
@@ -6,7 +6,7 @@
6
6
 
7
7
  把任意 CLI agent(Claude Code、自研 agent、任何长跑命令)包装成可被
8
8
  systemd / launchd 拉起、能被远程观测的服务。**每轮跑完进程退出**,外层
9
- supervisor 重启 —— 这是核心模式。中间穿插 11 条防御,避开 production 上
9
+ supervisor 重启 —— 这是核心模式。中间穿插 12 条防御,避开 production 上
10
10
  最容易翻车的几条路:
11
11
 
12
12
  - 轮卡死、Tool 调用空转 → 硬墙 timeout
@@ -20,7 +20,7 @@ supervisor 重启 —— 这是核心模式。中间穿插 11 条防御,避开
20
20
 
21
21
  ```
22
22
  ┌──────────────────────────────────────────┐
23
- │ Layer 3:Witness(monitor) │ 12 个检测器 + 自动停服
23
+ │ Layer 3:Witness(monitor) │ 11 个检测器 + 自动停服
24
24
  ├──────────────────────────────────────────┤
25
25
  │ Layer 2:Loop(serve,~120 LOC 薄壳) │ 捕获信号,循环拉起 round
26
26
  ├──────────────────────────────────────────┤
@@ -63,7 +63,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
63
63
  |---|---|
64
64
  | `init` / `install` / `uninstall` | `peek` —— 项目状态快照 |
65
65
  | `start` / `stop` / `kill` / `cancel` | `watch` —— peek 在刷新循环里 |
66
- | `restart` / `status` | `monitor` —— 12 个检测器 + 告警 + 自动停服 |
66
+ | `restart` / `status` | `monitor` —— 11 个检测器 + 告警 + 自动停服 |
67
67
  | `round` / `serve` / `upgrade` | `events` —— 查询 / 流式订阅 events.jsonl |
68
68
 
69
69
  **停服三动词**有清晰的语义分层:
@@ -73,7 +73,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
73
73
 
74
74
  动词参考:[`docs/commands.md`](docs/commands.md)。
75
75
 
76
- ## 内置防御(11 条)
76
+ ## 内置防御(12 条)
77
77
 
78
78
  防御以数据形式定义在 `agent_runner/defenses.py`,可通过
79
79
  `agent-runner peek --select defenses` 直接拿到。每条防御自带:
@@ -95,7 +95,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
95
95
 
96
96
  完整列表 + 历史出处:[`docs/architecture.md`](docs/architecture.md)。
97
97
 
98
- ## Monitor:12 个检测器
98
+ ## Monitor:11 个检测器
99
99
 
100
100
  **只告警**(warning 级,服务继续跑):
101
101
  `timeout_rate` / `hung` / `orphan_chain` / `disk_warning` /
@@ -45,6 +45,29 @@ def emit_max_rounds_reached(log_dir: Path, *, rounds_completed: int, max_rounds:
45
45
  emit(log_dir, MAX_ROUNDS_REACHED, rounds_completed=rounds_completed, max_rounds=max_rounds)
46
46
 
47
47
 
48
+ def emit_config_broken(log_dir: Path, *, reason: str) -> None:
49
+ """Emit config_broken (serve stopped on a permanent startup-battery failure)."""
50
+ from agent_runner.events import CONFIG_BROKEN, emit
51
+
52
+ emit(log_dir, CONFIG_BROKEN, reason=reason)
53
+
54
+
55
+ def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
56
+ """Emit crash_loop (serve stopped after consecutive unknown short crashes).
57
+
58
+ Captures the failure reason — a redacted tail of the round log — so a
59
+ recurring unknown crash can later be classified into a transient bucket.
60
+ """
61
+ from agent_runner._redact import redact_secrets
62
+ from agent_runner.events import CRASH_LOOP, emit
63
+
64
+ try:
65
+ reason = redact_secrets(log_path.read_text(errors="replace")[-2000:])
66
+ except OSError:
67
+ reason = ""
68
+ emit(log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, reason=reason)
69
+
70
+
48
71
  def emit_stop_file_detected(
49
72
  log_dir: Path, *, stop_file: Path, content: str, rounds_completed: int
50
73
  ) -> None:
@@ -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.41'
22
- __version_tuple__ = version_tuple = (0, 1, 41)
21
+ __version__ = version = '0.1.42'
22
+ __version_tuple__ = version_tuple = (0, 1, 42)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -18,7 +18,7 @@ import sysconfig
18
18
  import time
19
19
  from collections.abc import Iterator
20
20
  from pathlib import Path
21
- from typing import Any
21
+ from typing import Any, Literal
22
22
 
23
23
  from agent_runner import events, lifecycle
24
24
  from agent_runner.api_types import (
@@ -45,6 +45,59 @@ from agent_runner.service_unit import (
45
45
  serve_unit_filename,
46
46
  )
47
47
 
48
+ # Exit code for a permanent (no-retry) startup-battery failure. A broken config
49
+ # does not self-heal between rounds, so serve STOPS rather than respawning it
50
+ # forever. 78 = EX_CONFIG (sysexits) — avoids argparse's 2 and the generic 1.
51
+ # Lives here (not runner.py) so serve_cmd can import it from the sanctioned api
52
+ # facade without coupling to runner (runner imports api, not the reverse).
53
+ PERMANENT_CONFIG_EXIT = 78
54
+
55
+ # Crash-loop circuit breaker (b12). The serve loop escalates the restart delay
56
+ # on consecutive UNKNOWN short crashes (non-zero exit, short duration, no
57
+ # classified transient) and STOPS after CRASH_LOOP_THRESHOLD of them — the Run 6
58
+ # ~100-empty-rounds scar. Recoverable-slow failures (rate limit / 5h quota / 5xx
59
+ # / timeout) are already handled by the transient-error throttle and never reach
60
+ # this path. A clean (exit 0), long, or classified-transient round resets the run.
61
+ CRASH_LOOP_THRESHOLD = 5
62
+ CRASH_LOOP_SHORT_EXIT_S = 60 # mirrors monitor.SHORT_EXIT_THRESHOLD_S
63
+ CRASH_LOOP_MAX_DELAY_S = 1800 # cap the escalating restart delay (30 min)
64
+
65
+
66
+ def post_round_decision(
67
+ *,
68
+ returncode: int,
69
+ duration_s: float,
70
+ throttle_active: bool,
71
+ consecutive: int,
72
+ restart_delay_s: int,
73
+ ) -> tuple[Literal["config_broken", "crash_loop", "continue"], int, int]:
74
+ """Restart policy after one round — keeps the serve loop a thin dispatcher.
75
+
76
+ Returns ``(action, delay_s, consecutive)`` where action is:
77
+ - ``"config_broken"`` — permanent startup failure (b18): stop.
78
+ - ``"crash_loop"`` — CRASH_LOOP_THRESHOLD consecutive unknown short crashes
79
+ (b12): stop. An unknown short crash is a non-zero, fast exit with no
80
+ classified transient (rate-limit/5xx/timeout are handled by the throttle).
81
+ - ``"continue"`` — sleep ``delay_s`` then run the next round.
82
+
83
+ A clean (exit 0), long, or transient round resets ``consecutive`` to 0; an
84
+ unknown short crash escalates the delay (restart × 2ⁿ, capped) until the stop.
85
+ """
86
+ if returncode == PERMANENT_CONFIG_EXIT:
87
+ return ("config_broken", 0, consecutive)
88
+ unknown_short_crash = (
89
+ returncode != 0 and duration_s < CRASH_LOOP_SHORT_EXIT_S and not throttle_active
90
+ )
91
+ if unknown_short_crash:
92
+ consecutive += 1
93
+ if consecutive >= CRASH_LOOP_THRESHOLD:
94
+ return ("crash_loop", 0, consecutive)
95
+ delay = min(restart_delay_s * 2**consecutive, CRASH_LOOP_MAX_DELAY_S)
96
+ return ("continue", delay, consecutive)
97
+ delay = restart_delay_s if returncode == 0 else restart_delay_s * 2
98
+ return ("continue", delay, 0)
99
+
100
+
48
101
  _PROJECT_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
49
102
 
50
103
  _LINGER_HINT = (
@@ -730,6 +783,8 @@ def check_self_terminated_sentinel(log_dir: Path) -> bool:
730
783
  from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-export
731
784
  emit_agent_usage_recorded,
732
785
  emit_anomaly_repetitive_tool,
786
+ emit_config_broken,
787
+ emit_crash_loop,
733
788
  emit_fresh_eyes_round_triggered,
734
789
  emit_max_rounds_reached,
735
790
  emit_rate_limit_stop,
@@ -23,12 +23,15 @@ from agent_runner._throttle import _check_throttle_state
23
23
  from agent_runner._throttle import reset_counters as _reset_counters
24
24
  from agent_runner.api import (
25
25
  check_self_terminated_sentinel,
26
+ emit_config_broken,
27
+ emit_crash_loop,
26
28
  emit_fresh_eyes_round_triggered,
27
29
  emit_max_rounds_reached,
28
30
  emit_rate_limit_stop,
29
31
  emit_round_substrate_after,
30
32
  emit_round_substrate_before,
31
33
  emit_stop_file_detected,
34
+ post_round_decision,
32
35
  )
33
36
  from agent_runner.cli.common import cfg_from_args
34
37
  from agent_runner.hooks import run_serve_startup_hooks
@@ -135,6 +138,7 @@ def cmd(args) -> int:
135
138
  stop_file = cfg.runtime.stop_file # cache: same pattern as effective_max_rounds
136
139
  work_dir = cfg.runtime.work_dir
137
140
  rounds_completed = 0
141
+ consecutive_crashes = 0 # b12: consecutive UNKNOWN short crashes (crash-loop breaker)
138
142
 
139
143
  try:
140
144
  pid_file.write(os.getpid())
@@ -197,6 +201,7 @@ def cmd(args) -> int:
197
201
  every_n=cfg.runtime.fresh_eyes_every_n,
198
202
  )
199
203
  round_log_path = log_dir / f"round-{round_num}.log"
204
+ round_started = time.monotonic()
200
205
  with round_log_path.open("w") as f:
201
206
  r = subprocess.run(
202
207
  [
@@ -211,6 +216,7 @@ def cmd(args) -> int:
211
216
  stdout=f,
212
217
  stderr=subprocess.STDOUT,
213
218
  )
219
+ round_duration_s = time.monotonic() - round_started
214
220
  atomic_relink(log_dir / ROUND_CURRENT_LINK, round_log_path)
215
221
  git_head_after = compute_git_head(work_dir)
216
222
  paths_hash_after = compute_paths_hash(work_dir, cfg.runtime.substrate_fingerprint_paths)
@@ -221,13 +227,28 @@ def cmd(args) -> int:
221
227
  paths_hash=paths_hash_after,
222
228
  )
223
229
  rounds_completed += 1
230
+ # Restart policy (config_broken / crash_loop / continue) lives in the
231
+ # tested api.post_round_decision helper so this loop stays thin.
232
+ action, delay, consecutive_crashes = post_round_decision(
233
+ returncode=r.returncode,
234
+ duration_s=round_duration_s,
235
+ throttle_active=_check_throttle_state(log_dir) is not None,
236
+ consecutive=consecutive_crashes,
237
+ restart_delay_s=cfg.runtime.restart_delay_s,
238
+ )
239
+ if action == "config_broken":
240
+ emit_config_broken(log_dir, reason="startup battery permanent failure")
241
+ break
242
+ if action == "crash_loop":
243
+ emit_crash_loop(
244
+ log_dir,
245
+ consecutive=consecutive_crashes,
246
+ exit_code=r.returncode,
247
+ log_path=round_log_path,
248
+ )
249
+ break
224
250
  if args.once or stop["requested"]:
225
251
  break
226
- delay = (
227
- cfg.runtime.restart_delay_s
228
- if r.returncode == 0
229
- else cfg.runtime.restart_delay_s * 2
230
- )
231
252
  time.sleep(delay)
232
253
  finally:
233
254
  pid_file.unlink()
@@ -83,8 +83,18 @@ def catalog(cfg: Config) -> list[Defense]:
83
83
  Defense(
84
84
  name="startup_smoke_check",
85
85
  value="6 checks (config / log_dir / agent_cli / git / prompt_file / prompt_smoke)",
86
- codifies="R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn",
87
- guarded_by=None,
86
+ codifies=(
87
+ "R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; "
88
+ "now halts serve (config_broken) instead of respawning a broken config"
89
+ ),
90
+ guarded_by=Path("tests/unit/test_serve_config_broken.py"),
91
+ current_state="active",
92
+ ),
93
+ Defense(
94
+ name="crash_loop_breaker",
95
+ value="stop after 5 consecutive short crashes; exp-escalating delay",
96
+ codifies="Run 6 — crashing agent respawned ~100 empty rounds at a fixed 2x delay",
97
+ guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
88
98
  current_state="active",
89
99
  ),
90
100
  Defense(
@@ -32,6 +32,8 @@ ANOMALY_REPETITIVE_TOOL = "anomaly_repetitive_tool"
32
32
  AGENT_NETWORK_BLIP = "agent_network_blip"
33
33
  AGENT_SPAWN = "agent_spawn"
34
34
  AGENT_USAGE_RECORDED = "agent_usage_recorded"
35
+ CONFIG_BROKEN = "config_broken"
36
+ CRASH_LOOP = "crash_loop"
35
37
  DIRTY_COMMIT_FAILED = "dirty_commit_failed"
36
38
  DIRTY_DETECTED = "dirty_detected"
37
39
  FRESH_EYES_ROUND_TRIGGERED = "fresh_eyes_round_triggered"
@@ -49,7 +49,6 @@ KNOWN_ALERT_KINDS: frozenset[str] = frozenset(
49
49
  "disk_warning",
50
50
  "disk_critical",
51
51
  "mem_pressure",
52
- "smoke_fail_rate",
53
52
  "oauth_fail",
54
53
  "network_fail",
55
54
  "rate_limit_active",
@@ -265,29 +264,6 @@ def detect_mem_pressure(metrics: list[dict[str, Any]], *, threshold_mb: int = 20
265
264
  )
266
265
 
267
266
 
268
- def detect_smoke_fail_rate(
269
- events: list[dict[str, Any]], *, window: int = 10, threshold: float = 0.1
270
- ) -> Alert | None:
271
- ends = [e for e in events if e.get("event") == "round_end"]
272
- if len(ends) < window:
273
- return None
274
- recent_round_nums = [e.get("round_num") for e in ends[-window:]]
275
- fails = sum(
276
- 1
277
- for e in events
278
- if e.get("event") == "smoke_check_failed" and e.get("round_num") in recent_round_nums
279
- )
280
- rate = fails / window
281
- if rate < threshold:
282
- return None
283
- return _alert(
284
- "smoke_fail_rate",
285
- "warning",
286
- f"{fails}/{window} recent rounds had smoke_check_failed",
287
- {"rate": rate, "threshold": threshold, "hint": "Inspect events.jsonl for failure reasons"},
288
- )
289
-
290
-
291
267
  def detect_oauth_fail(
292
268
  events: list[dict[str, Any]],
293
269
  log_tails: dict[int, str],
@@ -603,7 +579,6 @@ def run_all_detectors(
603
579
  ),
604
580
  detect_disk_critical(metrics, threshold_pct=disk_critical_pct),
605
581
  detect_mem_pressure(metrics, threshold_mb=mem_avail_min_mb),
606
- detect_smoke_fail_rate(events),
607
582
  detect_oauth_fail(events, log_tails, patterns=compiled_auth_pats, hint=auth_fail_hint),
608
583
  detect_network_fail(events, log_tails),
609
584
  detect_rate_limit_active(events, now=now.timestamp()),
@@ -369,7 +369,7 @@ def run_one_round(cfg: Config, *, phase_override: str | None = None) -> RoundRes
369
369
  file=sys.stderr,
370
370
  )
371
371
  events.emit(log_dir, "smoke_check_failed", reason=f"{r.name}: {r.reason}")
372
- sys.exit(1)
372
+ sys.exit(api.PERMANENT_CONFIG_EXIT)
373
373
 
374
374
  # Concurrency lock (per-project)
375
375
  lock_path = log_dir / "agent-runner.lock"
@@ -521,6 +521,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
521
521
  round_num=round_num,
522
522
  phase=phase,
523
523
  idempotency_s=cfg.vcs.stash_idempotency_s,
524
+ log_dir=cfg.runtime.log_dir,
524
525
  )
525
526
  if ref is not None:
526
527
  context_store.write_orphan_state(
@@ -546,7 +547,9 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
546
547
  # Leave tree dirty for next round; dirty_detected already emitted
547
548
  pass
548
549
  elif action == "auto_commit":
549
- err = vcs_state.try_auto_commit(cfg.runtime.work_dir, round_num, phase)
550
+ err = vcs_state.try_auto_commit(
551
+ cfg.runtime.work_dir, round_num, phase, log_dir=cfg.runtime.log_dir
552
+ )
550
553
  if err is not None:
551
554
  events.emit(
552
555
  log_dir,
@@ -223,12 +223,18 @@ def stash_orphan(
223
223
  round_num: int,
224
224
  phase: str | None,
225
225
  idempotency_s: int = 5,
226
+ log_dir: Path | None = None,
226
227
  ) -> StashRef | None:
227
228
  """Stash dirty tree as ORPHAN entry, SHA-locked.
228
229
 
229
230
  Returns existing ref if a matching ORPHAN was created within ``idempotency_s``
230
231
  (R820 lesson — same-second multiple calls would otherwise pile up duplicate
231
232
  stashes). Returns None if tree is clean.
233
+
234
+ ``log_dir`` (when under ``repo``) is excluded from the stash so ``git stash
235
+ push -u`` does not sweep the runner's own bookkeeping (lock / pid / event
236
+ logs) out of the work tree. If only ``log_dir`` churned, nothing is stashed
237
+ and this returns None.
232
238
  """
233
239
  if not detect_dirty_files(repo):
234
240
  return None
@@ -238,7 +244,8 @@ def stash_orphan(
238
244
  ts = time.strftime("%Y-%m-%dT%H:%M:%S")
239
245
  phase_part = f" phase={phase}" if phase else ""
240
246
  msg = f"ORPHAN R{round_num}{phase_part} ts={ts}"
241
- push = _git(repo, "stash", "push", "-u", "-m", msg, timeout=30)
247
+ exclude = _log_dir_exclude_pathspec(repo, log_dir)
248
+ push = _git(repo, "stash", "push", "-u", "-m", msg, *exclude, timeout=30)
242
249
  if push.returncode != 0:
243
250
  return None
244
251
  listing = _git(repo, "stash", "list", "-1", "--format=%H %s")
@@ -287,22 +294,63 @@ def pop_stash(repo: Path, sha: str) -> bool:
287
294
  return _git(repo, "stash", "pop", sel).returncode == 0
288
295
 
289
296
 
290
- def try_auto_commit(work_dir: Path, round_num: int, phase: str | None) -> str | None:
297
+ def _log_dir_exclude_pathspec(root: Path, log_dir: Path | None) -> list[str]:
298
+ """Git pathspec args excluding the runner's own ``log_dir`` from an add/stash,
299
+ applied only when it lives inside the work tree AND is not already gitignored.
300
+ Empty otherwise: an outside or gitignored log_dir is skipped by git's own
301
+ handling, and folding an ignored path into a stash pathspec breaks untracked
302
+ capture (git refuses the ignored path).
303
+
304
+ Keeps supervisor bookkeeping (lock / pid / event logs) out of the agent's
305
+ dirty-tree handling: without it a zero-work round's log churn lands in a
306
+ commit (``git_head`` lies) or a ``git stash push -u`` (the logs vanish).
307
+ """
308
+ if log_dir is None:
309
+ return []
310
+ try:
311
+ rel = log_dir.resolve().relative_to(root.resolve()).as_posix()
312
+ except ValueError:
313
+ return [] # log_dir outside work_dir → nothing to exclude
314
+ if _git(root, "check-ignore", "-q", rel).returncode == 0:
315
+ return [] # already gitignored → git skips it; pathspec would misfire
316
+ return ["--", f":(exclude){rel}"]
317
+
318
+
319
+ def try_auto_commit(
320
+ work_dir: Path,
321
+ round_num: int,
322
+ phase: str | None,
323
+ *,
324
+ log_dir: Path | None = None,
325
+ ) -> str | None:
291
326
  """Auto-commit dirty tree with hardcoded subject. Return None on success, error on failure.
292
327
 
293
328
  Subject: ``agent-runner auto-commit: R<N> <phase>`` (phase part omitted if None).
294
329
  Uses ``git -c commit.gpgsign=false`` to skip GPG; honors pre-commit hooks
295
330
  (no ``--no-verify``). DOES NOT push — local commit only.
296
331
 
332
+ ``log_dir`` (when under ``work_dir``) is excluded from the add so a zero-work
333
+ round that only churned the runner's own bookkeeping (lock/pid/event logs)
334
+ does not advance ``git_head``. The agent's work and ``.evolving/`` live
335
+ outside ``log_dir`` and are still committed. If nothing remains staged after
336
+ the exclusion, this is a no-op (returns None, leaves HEAD untouched).
337
+
297
338
  Callers (runner.py) emit ``dirty_commit_failed`` event when return value is not None.
298
339
  """
299
340
  phase_part = f" {phase}" if phase else ""
300
341
  subject = f"agent-runner auto-commit: R{round_num}{phase_part}"
301
342
 
302
- add_result = _git(work_dir, "add", "-A")
343
+ exclude = _log_dir_exclude_pathspec(work_dir, log_dir)
344
+ add_result = _git(work_dir, "add", "-A", *exclude)
303
345
  if add_result.returncode != 0:
304
346
  return (add_result.stderr or "git add failed")[:200]
305
347
 
348
+ # Only the exclusion can leave nothing staged (a zero-work round that churned
349
+ # only log_dir); without it the tree was dirty so there is always something to
350
+ # commit. Skip the extra git call on the common (no-exclusion) path.
351
+ if exclude and _git(work_dir, "diff", "--cached", "--quiet").returncode == 0:
352
+ return None
353
+
306
354
  commit_result = _git(
307
355
  work_dir,
308
356
  "commit",
@@ -34,7 +34,7 @@ All three accept the same drill-down flags: `--round N`, `--log`, `--events N`,
34
34
 
35
35
  ## Defenses-as-data
36
36
 
37
- `agent_runner.defenses.catalog(cfg)` returns 11 structured `Defense` entries.
37
+ `agent_runner.defenses.catalog(cfg)` returns 12 structured `Defense` entries.
38
38
  Each entry carries:
39
39
 
40
40
  - `name` — stable identifier
@@ -59,13 +59,14 @@ surfacing everywhere.
59
59
  | `sha_locked_stash` | §9 IMMUTABLE — batch drop by index breaks under concurrent stash | `tests/invariants/test_stash_uses_sha_not_index.py` |
60
60
  | `set_diff_classification` | R2110 — rotation-only diff via +-line scan misclassifies | `—` |
61
61
  | `critical_envs_injection` | Env injection via [agent.env] block — preset-supplied per CLI (e.g. DISABLE_AUTOUPDATER for claude prevents mid-loop self-updates) | `—` |
62
- | `startup_smoke_check` | R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn | `—` |
62
+ | `startup_smoke_check` | R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; now halts serve (config_broken) instead of respawning a broken config | `tests/unit/test_serve_config_broken.py` |
63
+ | `crash_loop_breaker` | Run 6 — crashing agent respawned ~100 empty rounds at a fixed 2x delay | `tests/unit/test_serve_crash_loop.py` |
63
64
  | `flock_concurrency` | Architectural — prevent concurrent supervisors corrupting state | `—` |
64
65
  | `atomic_state_writes` | Data integrity — crashes never leave half-written state files | `tests/invariants/test_atomic_write_enforced.py` |
65
66
  | `event_kind_registry` | Prevent events.emit() typos / unregistered kinds slipping past CI | `tests/invariants/test_event_kind_registry.py` |
66
67
  <!-- /gen:defenses-table -->
67
68
 
68
- ## Monitor: 12 detectors
69
+ ## Monitor: 11 detectors
69
70
 
70
71
  Three categories by `auto_action`:
71
72
 
@@ -88,7 +89,6 @@ API quota / writing to a near-full disk).
88
89
  - `oauth_fail` — **auto-stop**
89
90
  - `orphan_chain`
90
91
  - `rate_limit_active`
91
- - `smoke_fail_rate`
92
92
  - `supervisor_stale`
93
93
  - `timeout_rate`
94
94
  <!-- /gen:detector-list -->
@@ -151,6 +151,8 @@ hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"
151
151
  - `agent_spawn`
152
152
  - `agent_usage_recorded`
153
153
  - `anomaly_repetitive_tool`
154
+ - `config_broken`
155
+ - `crash_loop`
154
156
  - `dirty_commit_failed`
155
157
  - `dirty_detected`
156
158
  - `fresh_eyes_round_triggered`
@@ -192,4 +194,4 @@ hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"
192
194
 
193
195
  三层架构:Round(一轮 agent)/ Loop(serve 薄壳)/ Witness(monitor)。
194
196
  三视角对称:peek(快照)/ watch(快照循环)/ monitor(异常检测),共用下钻参数。
195
- 防御以结构化目录形式存在(11 条),每条防御自描述「防的是哪条历史教训、被哪个 invariant test 守、当前状态」。
197
+ 防御以结构化目录形式存在(12 条),每条防御自描述「防的是哪条历史教训、被哪个 invariant test 守、当前状态」。
@@ -145,7 +145,7 @@ agent-runner events --kind transient_error_backoff_capped --tail
145
145
 
146
146
  ### `agent-runner monitor [--host SSH-ALIAS] [--interval N] [--mode MODE] [--port PORT] [--json]`
147
147
 
148
- Anomaly-detection daemon. Runs the 12 detectors against the live state on every
148
+ Anomaly-detection daemon. Runs the 11 detectors against the live state on every
149
149
  poll. Without `--host`, watches local logs at default 30s interval. With
150
150
  `--host`, watches a remote agent-runner over plain ssh at default 60s interval.
151
151
 
@@ -0,0 +1,58 @@
1
+ # Migrating to 0.1.42
2
+
3
+ ## TL;DR
4
+
5
+ ```bash
6
+ pip install --upgrade cli-agent-runner==0.1.42
7
+ ```
8
+
9
+ Two new always-on serve defenses (`crash_loop`, `config_broken`), one removed
10
+ (inert) monitor alert, and an `auto_commit` scope fix. No config-schema change;
11
+ no action required for a healthy deployment.
12
+
13
+ ## Behavior change: serve now STOPS on two harmful states (instead of respawning)
14
+
15
+ Both fire in the always-on path (no `monitor` process required):
16
+
17
+ - **`config_broken`** — if the startup battery fails (broken config: missing
18
+ prompt, non-git `work_dir`, agent CLI not on PATH, sub-500-byte prompt, …),
19
+ the round exits with the no-retry code `78` and serve emits `config_broken`
20
+ and stops. Previously the round exited `1` and serve respawned the broken
21
+ config forever. The specific cause is in the round's `smoke_check_failed`
22
+ event. Fix the config and restart.
23
+ - **`crash_loop`** — after **5 consecutive** *unknown short crashes* (a round
24
+ that exits non-zero in under 60s with no classified transient error), serve
25
+ emits `crash_loop` (carrying `consecutive`, `exit_code`, and a redacted reason
26
+ tail) and stops, escalating the restart delay along the way. Previously such a
27
+ round respawned forever at a fixed 2× delay (the Run 6 ~100-empty-rounds
28
+ incident).
29
+
30
+ Recoverable-slow failures are unaffected: rate-limit / 5h-quota / 5xx / timeout
31
+ are classified as transient errors and still ride the existing
32
+ `transient_error_*` backoff (`rate_limit_account` waits the server's exact
33
+ `resetsAt`). They never count toward the crash-loop breaker.
34
+
35
+ To watch for these: `grep -E '"event": "(crash_loop|config_broken)"' events-*.jsonl`.
36
+
37
+ ## Removed: the `smoke_fail_rate` monitor alert
38
+
39
+ It could never fire (it matched on `round_num`, which `smoke_check_failed` never
40
+ carried) and is now superseded by the always-on `config_broken` stop. If you
41
+ subscribed to `smoke_fail_rate` (it never emitted), switch to `config_broken`.
42
+ `monitor` now reports **11** detectors.
43
+
44
+ ## Fixed: dirty-tree handling no longer sweeps the runner's `log_dir`
45
+
46
+ When `log_dir` is inside `work_dir`, **both** VCS dirty-actions now exclude the
47
+ supervisor's own bookkeeping (lock, pid, `events-*.jsonl`, round logs):
48
+
49
+ - `dirty_action = "auto_commit"` excludes it from the commit — previously the
50
+ per-round churn produced a non-empty commit even on a zero-work round,
51
+ advancing `git_head` and making the progress signal lie.
52
+ - `dirty_action = "stash"` (the default) excludes it from `git stash push -u` —
53
+ previously the logs (and the events file being written) were swept into the
54
+ stash and vanished from the work tree each round.
55
+
56
+ Your agent's work and the `.evolving/` ledger live outside `log_dir` and are
57
+ unaffected. Default deployments (`log_dir` at `~/.agent-runner/{project}/logs`,
58
+ outside `work_dir`) were never affected by either.