cli-agent-runner 0.1.42__tar.gz → 0.2.0__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 (241) hide show
  1. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/CHANGELOG.md +11 -1
  2. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/PKG-INFO +2 -2
  3. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/README.md +1 -1
  4. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/README.zh.md +1 -1
  5. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/__init__.py +4 -0
  6. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_version.py +2 -2
  7. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/api.py +6 -0
  8. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/api_types.py +13 -0
  9. cli_agent_runner-0.2.0/agent_runner/builtin_plugins/default_dirty_handler.py +83 -0
  10. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/events.py +1 -0
  11. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/hooks.py +67 -0
  12. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/runner.py +8 -45
  13. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/vcs_state.py +15 -17
  14. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/architecture.md +26 -2
  15. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/configuration.md +6 -2
  16. cli_agent_runner-0.2.0/docs/migrations/0.2.0.md +166 -0
  17. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/plugins.md +122 -0
  18. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/runbook.md +21 -0
  19. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/pyproject.toml +3 -0
  20. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/_test_helpers.py +18 -5
  21. cli_agent_runner-0.2.0/tests/integration/test_dirty_handler_seam.py +74 -0
  22. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_types.py +27 -0
  23. cli_agent_runner-0.2.0/tests/unit/test_catalogs.py +10 -0
  24. cli_agent_runner-0.2.0/tests/unit/test_default_dirty_handler.py +77 -0
  25. cli_agent_runner-0.2.0/tests/unit/test_dirty_handlers.py +49 -0
  26. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_init_entry_points.py +3 -2
  27. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_vcs_state.py +27 -4
  28. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.codecov.yml +0 -0
  29. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  31. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  33. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/workflows/ci.yml +0 -0
  34. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.github/workflows/release.yml +0 -0
  35. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.gitignore +0 -0
  36. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/.vulture-whitelist.py +0 -0
  37. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/CODE_OF_CONDUCT.md +0 -0
  38. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/CONTRIBUTING.md +0 -0
  39. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/LICENSE +0 -0
  40. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/SECURITY.md +0 -0
  41. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_docgen.py +0 -0
  42. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_emit.py +0 -0
  43. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_redact.py +0 -0
  44. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_registry.py +0 -0
  45. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_substrate.py +0 -0
  46. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/_throttle.py +0 -0
  47. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/agent_runtime.py +0 -0
  48. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/__init__.py +0 -0
  49. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/_constants.py +0 -0
  50. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  51. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/codewhale.py +0 -0
  52. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/builtin_plugins/gemini.py +0 -0
  53. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/__init__.py +0 -0
  54. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/__main__.py +0 -0
  55. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/common.py +0 -0
  56. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/events_cmd.py +0 -0
  57. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/init_cmd.py +0 -0
  58. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/install_cmd.py +0 -0
  59. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/monitor_cmd.py +0 -0
  60. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/peek_cmd.py +0 -0
  61. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/round_cmd.py +0 -0
  62. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/serve_cmd.py +0 -0
  63. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/service_cmd.py +0 -0
  64. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/cli/upgrade_cmd.py +0 -0
  65. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/config.py +0 -0
  66. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/context_store.py +0 -0
  67. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/defenses.py +0 -0
  68. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/detector_helpers.py +0 -0
  69. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/http_progress.py +0 -0
  70. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/lifecycle.py +0 -0
  71. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/metrics.py +0 -0
  72. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/monitor.py +0 -0
  73. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/__init__.py +0 -0
  74. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/aider.toml +0 -0
  75. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/claude.toml +0 -0
  76. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/codewhale.toml +0 -0
  77. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/presets/gemini.toml +0 -0
  78. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/prompt_loader.py +0 -0
  79. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/round_log.py +0 -0
  80. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/round_view.py +0 -0
  81. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/scaffold.py +0 -0
  82. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/service_unit.py +0 -0
  83. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/agent_runner/startup_check.py +0 -0
  84. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/build.sh +0 -0
  85. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/example-agent-runner.toml +0 -0
  86. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/launchd.plist.tmpl +0 -0
  87. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/run-loop.sh +0 -0
  88. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/deploy/systemd.service.tmpl +0 -0
  89. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/README.md +0 -0
  90. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/commands.md +0 -0
  91. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/events.md +0 -0
  92. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/long-running-agents.md +0 -0
  93. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/marketing/README.md +0 -0
  94. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/marketing/promo-cn.html +0 -0
  95. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.16.md +0 -0
  96. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.17.md +0 -0
  97. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.19.md +0 -0
  98. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.20.md +0 -0
  99. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.21.md +0 -0
  100. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.22.md +0 -0
  101. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.23.md +0 -0
  102. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.24.md +0 -0
  103. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.25.md +0 -0
  104. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.26.md +0 -0
  105. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.27.md +0 -0
  106. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.28.md +0 -0
  107. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.29.md +0 -0
  108. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.30.md +0 -0
  109. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.31.md +0 -0
  110. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.32.md +0 -0
  111. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.33.md +0 -0
  112. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.34.md +0 -0
  113. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.35.md +0 -0
  114. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.36.md +0 -0
  115. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.37.md +0 -0
  116. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.38.md +0 -0
  117. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.39.md +0 -0
  118. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.40.md +0 -0
  119. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/migrations/0.1.42.md +0 -0
  120. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/quickstart.md +0 -0
  121. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/recipes/aider.md +0 -0
  122. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/recipes/codewhale.md +0 -0
  123. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/docs/thesis.md +0 -0
  124. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/__init__.py +0 -0
  125. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/conftest.py +0 -0
  126. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/contract/__init__.py +0 -0
  127. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/contract/test_public_api_surface.py +0 -0
  128. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/__init__.py +0 -0
  129. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/conftest.py +0 -0
  130. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  131. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_install_systemd.py +0 -0
  132. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  133. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  134. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  135. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  136. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  137. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/__init__.py +0 -0
  138. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_bounded_run.py +0 -0
  139. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_context_enricher_namespacing.py +0 -0
  140. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_fresh_eyes_signal.py +0 -0
  141. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_grace_kill_emission.py +0 -0
  142. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_install_dry_run.py +0 -0
  143. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_monitor_seeded.py +0 -0
  144. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_detector_loaded.py +0 -0
  145. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_owned_paths.py +0 -0
  146. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_plugin_real_flow.py +0 -0
  147. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  148. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_scaffold_presets.py +0 -0
  149. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_serve_loop.py +0 -0
  150. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_substrate_fingerprint.py +0 -0
  151. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/integration/test_transient_error_backoff.py +0 -0
  152. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/__init__.py +0 -0
  153. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_architecture.py +0 -0
  154. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_atomic_write_enforced.py +0 -0
  155. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_catalogs.py +0 -0
  156. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_classification_ssot.py +0 -0
  157. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  158. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_docs_generated.py +0 -0
  159. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_entry_points_resolve.py +0 -0
  160. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_event_kind_registry.py +0 -0
  161. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_event_kinds_ssot.py +0 -0
  162. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_events_doc_contract.py +0 -0
  163. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_layer_2_loop_size.py +0 -0
  164. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_module_boundaries.py +0 -0
  165. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_module_sizes.py +0 -0
  166. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_no_ai_signatures.py +0 -0
  167. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  168. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_peek_schema_version.py +0 -0
  169. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  170. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_round_result_stable.py +0 -0
  171. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  172. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/invariants/test_upstream_schema_canary.py +0 -0
  173. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/__init__.py +0 -0
  174. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/parser.py +0 -0
  175. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/test_parser.py +0 -0
  176. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/literate/test_quickstart.py +0 -0
  177. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/__init__.py +0 -0
  178. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime.py +0 -0
  179. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime_grace.py +0 -0
  180. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_agent_runtime_progress.py +0 -0
  181. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_assemble_prompt.py +0 -0
  182. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_events_stream.py +0 -0
  183. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_install.py +0 -0
  184. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_observation.py +0 -0
  185. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_read_round_num.py +0 -0
  186. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_resolve_phase.py +0 -0
  187. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_api_service.py +0 -0
  188. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_auto_stop_gating.py +0 -0
  189. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_claude_error_detector.py +0 -0
  190. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli.py +0 -0
  191. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_common.py +0 -0
  192. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_init_install.py +0 -0
  193. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_monitor_http.py +0 -0
  194. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  195. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_cli_upgrade.py +0 -0
  196. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_codewhale_plugin.py +0 -0
  197. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config.py +0 -0
  198. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_fresh_eyes.py +0 -0
  199. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_max_rounds.py +0 -0
  200. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_stop_file.py +0 -0
  201. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  202. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_config_transient_error_action.py +0 -0
  203. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_context_store.py +0 -0
  204. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_defenses.py +0 -0
  205. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_detector_helpers.py +0 -0
  206. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_detector_protocol.py +0 -0
  207. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_docgen.py +0 -0
  208. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_events.py +0 -0
  209. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_events_cmd.py +0 -0
  210. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  211. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_gemini_plugin.py +0 -0
  212. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_hook_failure_isolation.py +0 -0
  213. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_hooks.py +0 -0
  214. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_http_progress.py +0 -0
  215. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_lifecycle.py +0 -0
  216. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_metrics.py +0 -0
  217. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_assembly.py +0 -0
  218. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  219. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  220. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  221. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_detectors.py +0 -0
  222. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_monitor_remote.py +0 -0
  223. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_peek_argparse.py +0 -0
  224. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_peek_select.py +0 -0
  225. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_presets.py +0 -0
  226. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_prompt_loader.py +0 -0
  227. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_redact.py +0 -0
  228. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_round_log_helpers.py +0 -0
  229. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_round_view.py +0 -0
  230. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_runner.py +0 -0
  231. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_runner_throttle.py +0 -0
  232. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_scaffold.py +0 -0
  233. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_cmd_bounded.py +0 -0
  234. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_config_broken.py +0 -0
  235. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_crash_loop.py +0 -0
  236. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_round_log.py +0 -0
  237. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_sentinel.py +0 -0
  238. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_serve_startup_hooks.py +0 -0
  239. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_service_unit.py +0 -0
  240. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_startup_check.py +0 -0
  241. {cli_agent_runner-0.1.42 → cli_agent_runner-0.2.0}/tests/unit/test_substrate.py +0 -0
@@ -5,6 +5,15 @@ 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.0] - 2026-07-02
9
+
10
+ ### Changed
11
+ - Dirty-tree handling is now pluggable. The clean-exit-dirty policy moved out of core into a `dirty_handler` hook; `stash` / `ignore` / `auto_commit` now ship as the bundled, default-on `default_dirty_handler` plugin. **No behavior or config change for existing users** — `[vcs] dirty_action` works exactly as before.
12
+
13
+ ### Added
14
+ - `dirty_handler` plugin seam (4th lifecycle hook) + `DirtyOutcome` on `RoundResult`; `stash_orphan` / `try_auto_commit` are now public `api` primitives. Consumers can fully own dirty-tree policy — see `docs/migrations/0.2.0.md`.
15
+ - `dirty_auto_committed` event (emitted when a handler auto-commits).
16
+
8
17
  ## [0.1.42] - 2026-06-25
9
18
 
10
19
  ### Added
@@ -866,7 +875,8 @@ Initial public release on PyPI as `cli-agent-runner`.
866
875
  - Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
867
876
  gated by a manual approval on the `pypi` GitHub environment.
868
877
 
869
- [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.26...HEAD
878
+ [Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.0...HEAD
879
+ [0.2.0]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.42...v0.2.0
870
880
  [0.1.26]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.25...v0.1.26
871
881
  [0.1.25]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.24...v0.1.25
872
882
  [0.1.24]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.23...v0.1.24
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.42
3
+ Version: 0.2.0
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
@@ -109,7 +109,7 @@ Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
109
109
  ## Monitor: 11 detectors
110
110
 
111
111
  Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
112
- `mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
112
+ `mem_pressure`, `network_fail`, `rate_limit_active`,
113
113
  `anomaly_repetitive_active`, `supervisor_stale`.
114
114
 
115
115
  **Auto-stop the service** (continuing is harmful):
@@ -72,7 +72,7 @@ Full list and rationale: [`docs/architecture.md`](docs/architecture.md).
72
72
  ## Monitor: 11 detectors
73
73
 
74
74
  Notify only: `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`,
75
- `mem_pressure`, `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
75
+ `mem_pressure`, `network_fail`, `rate_limit_active`,
76
76
  `anomaly_repetitive_active`, `supervisor_stale`.
77
77
 
78
78
  **Auto-stop the service** (continuing is harmful):
@@ -99,7 +99,7 @@ agent-runner monitor # 实时异常检测,OAuth/磁盘 critical
99
99
 
100
100
  **只告警**(warning 级,服务继续跑):
101
101
  `timeout_rate` / `hung` / `orphan_chain` / `disk_warning` /
102
- `mem_pressure` / `smoke_fail_rate` / `network_fail` / `rate_limit_active` /
102
+ `mem_pressure` / `network_fail` / `rate_limit_active` /
103
103
  `anomaly_repetitive_active` / `supervisor_stale`
104
104
 
105
105
  **自动停服**(critical 级,继续是 net negative):
@@ -14,6 +14,7 @@ _HOOK_GROUPS = (
14
14
  "agent_runner.context_enrichers",
15
15
  "agent_runner.post_round_hooks",
16
16
  "agent_runner.serve_startup_hooks",
17
+ "agent_runner.dirty_handler_hooks",
17
18
  )
18
19
 
19
20
  # Tracks the names passed to the most recent ``apply_plugin_disable`` call.
@@ -111,6 +112,9 @@ def apply_plugin_disable(names: list[str]) -> None:
111
112
  # Serve-startup hooks
112
113
  _prune_by_name(hooks._SERVE_STARTUP_HOOKS, desired, found)
113
114
 
115
+ # Dirty handlers
116
+ _prune_by_name(hooks._DIRTY_HANDLERS, desired, found)
117
+
114
118
  # Plugin event kinds
115
119
  for name in list(events._PLUGIN_KINDS):
116
120
  if name in desired:
@@ -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.42'
22
- __version_tuple__ = version_tuple = (0, 1, 42)
21
+ __version__ = version = '0.2.0'
22
+ __version_tuple__ = version_tuple = (0, 2, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -799,6 +799,12 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
799
799
  emit_transient_error_recovered,
800
800
  )
801
801
 
802
+ # Re-export git primitives so external callers can reach them via the api facade.
803
+ from agent_runner.vcs_state import ( # noqa: E402,F401 — public primitives
804
+ stash_orphan,
805
+ try_auto_commit,
806
+ )
807
+
802
808
 
803
809
  def narrate_events(log_dir: Path, *, poll_interval_s: float = 0.5) -> Iterator[str]:
804
810
  """Tail events-*.jsonl files in log_dir, yielding one formatted line per event.
@@ -28,6 +28,18 @@ class ServiceMode(StrEnum):
28
28
  NONE = "none"
29
29
 
30
30
 
31
+ @dataclass(frozen=True)
32
+ class DirtyOutcome:
33
+ """What a dirty tree resolved to, reported on RoundResult and via events.
34
+
35
+ ``ref`` is the stash SHA (kind="stashed") or commit SHA (kind="committed");
36
+ None for "ignored".
37
+ """
38
+
39
+ kind: Literal["stashed", "committed", "ignored"]
40
+ ref: str | None = None
41
+
42
+
31
43
  @dataclass(frozen=True)
32
44
  class RateLimitState:
33
45
  """Public: surfaced via peek --json when supervisor is currently throttled."""
@@ -158,6 +170,7 @@ class RoundResult:
158
170
  log_path: Path
159
171
  dirty_files: list[str]
160
172
  stashed: bool
173
+ dirty_outcome: DirtyOutcome | None = None
161
174
  killed_for_grace: bool = False
162
175
 
163
176
 
@@ -0,0 +1,83 @@
1
+ """Bundled default dirty-tree handler — reproduces the pre-0.2.0 built-in
2
+ behavior (stash / ignore / auto_commit per [vcs] dirty_action) as a plugin.
3
+ Ships enabled; disable via [plugins] disable = ["default_dirty_handler"]."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from agent_runner import api, context_store, events, vcs_state
8
+ from agent_runner.api_types import DirtyOutcome
9
+ from agent_runner.events import now_iso_ms # match the helper runner.py uses
10
+ from agent_runner.hooks import HookContext, register_dirty_handler
11
+ from agent_runner.vcs_state import AutoCommitError
12
+
13
+
14
+ class DefaultDirtyHandler:
15
+ name = "default_dirty_handler"
16
+ priority = 1000 # last-resort fallback
17
+
18
+ def handle_dirty(self, ctx: HookContext, dirty_files) -> DirtyOutcome | None:
19
+ action = ctx.vcs.dirty_action if ctx.vcs else "stash"
20
+ if action == "ignore":
21
+ return DirtyOutcome(kind="ignored")
22
+ if action == "auto_commit":
23
+ try:
24
+ sha = api.try_auto_commit(
25
+ ctx.work_dir, ctx.round_num, ctx.phase, log_dir=ctx.log_dir
26
+ )
27
+ except AutoCommitError as exc:
28
+ # Parity with runner.py: emit failure event, leave tree dirty
29
+ # (no stash fallback). Dirty tree carries into the next round.
30
+ events.emit(
31
+ ctx.log_dir,
32
+ events.DIRTY_COMMIT_FAILED,
33
+ round_num=ctx.round_num,
34
+ phase=ctx.phase,
35
+ reason=str(exc),
36
+ )
37
+ return DirtyOutcome(kind="ignored")
38
+ if not sha:
39
+ return DirtyOutcome(kind="ignored")
40
+ events.emit(
41
+ ctx.log_dir,
42
+ events.DIRTY_AUTO_COMMITTED,
43
+ round_num=ctx.round_num,
44
+ files=dirty_files[:20],
45
+ ref=sha,
46
+ )
47
+ return DirtyOutcome(kind="committed", ref=sha)
48
+ return self._stash(ctx, dirty_files)
49
+
50
+ def _stash(self, ctx: HookContext, dirty_files) -> DirtyOutcome:
51
+ # via vcs_state (not api): test_runner.py monkeypatches vcs_state.stash_orphan;
52
+ # the api re-export is a separate binding a patch wouldn't intercept.
53
+ ref = vcs_state.stash_orphan(
54
+ ctx.work_dir,
55
+ round_num=ctx.round_num,
56
+ phase=ctx.phase,
57
+ idempotency_s=ctx.vcs.stash_idempotency_s if ctx.vcs else 5,
58
+ log_dir=ctx.log_dir,
59
+ )
60
+ if ref is None:
61
+ return DirtyOutcome(kind="ignored")
62
+ context_store.write_orphan_state(
63
+ ctx.log_dir,
64
+ context_store.OrphanState(
65
+ round_num=ctx.round_num,
66
+ files=dirty_files,
67
+ stashed_ref=ref.sha,
68
+ stash_message=ref.message,
69
+ timestamp=now_iso_ms(),
70
+ phase=ctx.phase,
71
+ ),
72
+ )
73
+ events.emit(
74
+ ctx.log_dir,
75
+ events.ORPHAN_STASHED,
76
+ round_num=ctx.round_num,
77
+ ref=ref.sha,
78
+ reason="clean_exit_with_dirty_tree",
79
+ )
80
+ return DirtyOutcome(kind="stashed", ref=ref.sha)
81
+
82
+
83
+ register_dirty_handler(DefaultDirtyHandler())
@@ -34,6 +34,7 @@ AGENT_SPAWN = "agent_spawn"
34
34
  AGENT_USAGE_RECORDED = "agent_usage_recorded"
35
35
  CONFIG_BROKEN = "config_broken"
36
36
  CRASH_LOOP = "crash_loop"
37
+ DIRTY_AUTO_COMMITTED = "dirty_auto_committed"
37
38
  DIRTY_COMMIT_FAILED = "dirty_commit_failed"
38
39
  DIRTY_DETECTED = "dirty_detected"
39
40
  FRESH_EYES_ROUND_TRIGGERED = "fresh_eyes_round_triggered"
@@ -30,6 +30,7 @@ Public API:
30
30
  from __future__ import annotations
31
31
 
32
32
  import sys
33
+ import traceback as tb_mod
33
34
  from dataclasses import dataclass
34
35
  from pathlib import Path
35
36
  from typing import Any, Protocol, runtime_checkable
@@ -43,6 +44,14 @@ _TAIL_BYTES = 1024
43
44
  _TRUNC_MARKER = "\n... [truncated] ...\n"
44
45
 
45
46
 
47
+ @dataclass(frozen=True)
48
+ class VcsHookView:
49
+ """Minimal vcs config a dirty handler needs (keeps HookContext narrow)."""
50
+
51
+ dirty_action: str
52
+ stash_idempotency_s: int
53
+
54
+
46
55
  @dataclass(frozen=True)
47
56
  class HookContext:
48
57
  """Narrow per-round context passed to all hook callbacks.
@@ -86,6 +95,26 @@ class HookContext:
86
95
  """Count threshold for repetitive-tool anomaly (0 = disabled).
87
96
  Populated from ``[monitor] anomaly_repetitive_threshold`` (0.1.31+).
88
97
  """
98
+ vcs: VcsHookView | None = None
99
+ """Narrowed vcs config slice populated by the runner for all per-round hooks
100
+ (pre-round, enrichers, post-round, dirty-handler) (0.2.0+).
101
+ May be None only when a HookContext is constructed without it (e.g. tests).
102
+ """
103
+
104
+
105
+ @runtime_checkable
106
+ class DirtyHandler(Protocol):
107
+ """Resolves a clean-exit dirty tree. Runs ascending by ``priority``; the
108
+ first to return a DirtyOutcome wins. Return None to pass to the next."""
109
+
110
+ name: str
111
+ priority: int
112
+
113
+ def handle_dirty(
114
+ self,
115
+ ctx: HookContext,
116
+ dirty_files: list[str],
117
+ ) -> Any | None: ...
89
118
 
90
119
 
91
120
  @runtime_checkable
@@ -148,6 +177,7 @@ _PRE_ROUND_HOOKS: list[PreRoundHook] = []
148
177
  _CONTEXT_ENRICHERS: list[ContextEnricher] = []
149
178
  _POST_ROUND_HOOKS: list[PostRoundHook] = []
150
179
  _SERVE_STARTUP_HOOKS: list[ServeStartupHook] = []
180
+ _DIRTY_HANDLERS: list[DirtyHandler] = []
151
181
 
152
182
 
153
183
  def register_pre_round_hook(hook: PreRoundHook) -> None:
@@ -191,6 +221,43 @@ def plugin_context_enrichers() -> list[str]:
191
221
  return sorted(e.name for e in _CONTEXT_ENRICHERS)
192
222
 
193
223
 
224
+ def register_dirty_handler(handler: DirtyHandler) -> None:
225
+ ensure_unique(handler.name, _DIRTY_HANDLERS, "dirty_handler")
226
+ _DIRTY_HANDLERS.append(handler)
227
+
228
+
229
+ def dirty_handlers() -> list[DirtyHandler]:
230
+ return list(_DIRTY_HANDLERS)
231
+
232
+
233
+ def dispatch_dirty(
234
+ ctx: HookContext,
235
+ dirty_files: list[str],
236
+ log_dir: Path,
237
+ ) -> Any | None:
238
+ """Run registered DirtyHandlers ascending by priority; first non-None wins.
239
+
240
+ A handler that raises is isolated — emits ``hook_failed`` with
241
+ ``hook_kind="dirty_handler"`` and treated as pass (continue to next).
242
+ """
243
+ ordered = sorted(_DIRTY_HANDLERS, key=lambda h: getattr(h, "priority", 0))
244
+ for h in ordered:
245
+ try:
246
+ outcome = h.handle_dirty(ctx, dirty_files)
247
+ except Exception as exc: # noqa: BLE001 — isolate; fall through to next
248
+ events.emit(
249
+ log_dir,
250
+ events.HOOK_FAILED,
251
+ hook_name=h.name,
252
+ hook_kind="dirty_handler",
253
+ **_summarize_error(exc, tb=tb_mod.format_exc()),
254
+ )
255
+ continue
256
+ if outcome is not None:
257
+ return outcome
258
+ return None
259
+
260
+
194
261
  def run_serve_startup_hooks(cfg: Any, log_dir: Path) -> bool:
195
262
  """Run all serve_startup_hooks. Returns True on success, False on abort.
196
263
 
@@ -423,6 +423,10 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
423
423
  dry_run=cfg.runtime.dry_run,
424
424
  anomaly_repetitive_window=cfg.monitor.anomaly_repetitive_window,
425
425
  anomaly_repetitive_threshold=cfg.monitor.anomaly_repetitive_threshold,
426
+ vcs=hooks.VcsHookView(
427
+ dirty_action=cfg.vcs.dirty_action,
428
+ stash_idempotency_s=cfg.vcs.stash_idempotency_s,
429
+ ),
426
430
  )
427
431
  _run_pre_round_hooks(
428
432
  hook_ctx,
@@ -512,54 +516,12 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
512
516
  if dirty:
513
517
  events.emit(log_dir, "dirty_detected", round_num=round_num, files=dirty[:20])
514
518
 
515
- stashed = False
516
- action = cfg.vcs.dirty_action
519
+ dirty_outcome = None
517
520
  if dirty and not result.timed_out and result.exit_code == 0:
518
- if action == "stash":
519
- ref = vcs_state.stash_orphan(
520
- cfg.runtime.work_dir,
521
- round_num=round_num,
522
- phase=phase,
523
- idempotency_s=cfg.vcs.stash_idempotency_s,
524
- log_dir=cfg.runtime.log_dir,
525
- )
526
- if ref is not None:
527
- context_store.write_orphan_state(
528
- log_dir,
529
- context_store.OrphanState(
530
- round_num=round_num,
531
- files=dirty,
532
- stashed_ref=ref.sha,
533
- stash_message=ref.message,
534
- timestamp=now_iso_ms(),
535
- phase=phase,
536
- ),
537
- )
538
- events.emit(
539
- log_dir,
540
- "orphan_stashed",
541
- round_num=round_num,
542
- ref=ref.sha,
543
- reason="clean_exit_with_dirty_tree",
544
- )
545
- stashed = True
546
- elif action == "ignore":
547
- # Leave tree dirty for next round; dirty_detected already emitted
548
- pass
549
- elif action == "auto_commit":
550
- err = vcs_state.try_auto_commit(
551
- cfg.runtime.work_dir, round_num, phase, log_dir=cfg.runtime.log_dir
552
- )
553
- if err is not None:
554
- events.emit(
555
- log_dir,
556
- events.DIRTY_COMMIT_FAILED,
557
- round_num=round_num,
558
- phase=phase,
559
- reason=err,
560
- )
521
+ dirty_outcome = hooks.dispatch_dirty(hook_ctx, dirty, log_dir=log_dir)
561
522
  elif not dirty:
562
523
  context_store.clear_orphan_state(log_dir)
524
+ stashed = bool(dirty_outcome and dirty_outcome.kind == "stashed")
563
525
 
564
526
  if result.killed_for_grace:
565
527
  api.emit_round_grace_kill(
@@ -609,6 +571,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
609
571
  log_path=log_path,
610
572
  dirty_files=dirty,
611
573
  stashed=stashed,
574
+ dirty_outcome=dirty_outcome,
612
575
  killed_for_grace=result.killed_for_grace,
613
576
  )
614
577
  _run_post_round_hooks(hook_ctx, round_result, log_dir)
@@ -72,6 +72,10 @@ def _matches_owned_path(path: str) -> bool:
72
72
  return False
73
73
 
74
74
 
75
+ class AutoCommitError(RuntimeError):
76
+ """git add/commit failed during try_auto_commit (reason capped at 200 chars)."""
77
+
78
+
75
79
  @dataclass(frozen=True)
76
80
  class StashRef:
77
81
  sha: str # full commit SHA — IMMUTABLE under concurrent stash
@@ -322,20 +326,13 @@ def try_auto_commit(
322
326
  phase: str | None,
323
327
  *,
324
328
  log_dir: Path | None = None,
325
- ) -> str | None:
326
- """Auto-commit dirty tree with hardcoded subject. Return None on success, error on failure.
327
-
328
- Subject: ``agent-runner auto-commit: R<N> <phase>`` (phase part omitted if None).
329
- Uses ``git -c commit.gpgsign=false`` to skip GPG; honors pre-commit hooks
330
- (no ``--no-verify``). DOES NOT push — local commit only.
329
+ ) -> str:
330
+ """Auto-commit the dirty tree with a hardcoded subject; return the commit SHA.
331
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
-
338
- Callers (runner.py) emit ``dirty_commit_failed`` event when return value is not None.
332
+ Returns "" when nothing remained staged after excluding log_dir (no-op;
333
+ HEAD untouched). Raises AutoCommitError on git failure. DOES NOT push.
334
+ Subject: ``agent-runner auto-commit: R<N> <phase>``. Uses
335
+ ``git -c commit.gpgsign=false``; honors pre-commit hooks (no --no-verify).
339
336
  """
340
337
  phase_part = f" {phase}" if phase else ""
341
338
  subject = f"agent-runner auto-commit: R{round_num}{phase_part}"
@@ -343,13 +340,13 @@ def try_auto_commit(
343
340
  exclude = _log_dir_exclude_pathspec(work_dir, log_dir)
344
341
  add_result = _git(work_dir, "add", "-A", *exclude)
345
342
  if add_result.returncode != 0:
346
- return (add_result.stderr or "git add failed")[:200]
343
+ raise AutoCommitError((add_result.stderr or "git add failed")[:200])
347
344
 
348
345
  # Only the exclusion can leave nothing staged (a zero-work round that churned
349
346
  # only log_dir); without it the tree was dirty so there is always something to
350
347
  # commit. Skip the extra git call on the common (no-exclusion) path.
351
348
  if exclude and _git(work_dir, "diff", "--cached", "--quiet").returncode == 0:
352
- return None
349
+ return ""
353
350
 
354
351
  commit_result = _git(
355
352
  work_dir,
@@ -359,6 +356,7 @@ def try_auto_commit(
359
356
  pre_flags=("-c", "commit.gpgsign=false"),
360
357
  )
361
358
  if commit_result.returncode != 0:
362
- return (commit_result.stderr or "git commit failed")[:200]
359
+ raise AutoCommitError((commit_result.stderr or "git commit failed")[:200])
363
360
 
364
- return None
361
+ head = _git(work_dir, "rev-parse", "HEAD")
362
+ return head.stdout.strip()
@@ -72,8 +72,8 @@ Three categories by `auto_action`:
72
72
 
73
73
  **Notify only** (severity `warning`):
74
74
  `timeout_rate`, `hung`, `orphan_chain`, `disk_warning`, `mem_pressure`,
75
- `smoke_fail_rate`, `network_fail`, `rate_limit_active`,
76
- `anomaly_repetitive_active`, `supervisor_stale`.
75
+ `network_fail`, `rate_limit_active`, `anomaly_repetitive_active`,
76
+ `supervisor_stale`.
77
77
 
78
78
  **Auto-stop service** (severity `critical`, `auto_action="stop_service"`):
79
79
  `oauth_fail`, `disk_critical`. Continuing in either state is harmful (burning
@@ -142,6 +142,29 @@ Setting `inject_context = false` does NOT disable PreRoundHooks. Setting
142
142
  If you want neither injection: set both. If you want to disable a specific plugin
143
143
  hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"]`.
144
144
 
145
+ ## Dirty-handler seam (0.2.0+)
146
+
147
+ After a round exits cleanly with a dirty working tree, the runner dispatches a
148
+ priority-ordered chain of registered `DirtyHandler` plugins. The first to return
149
+ a non-`None` `DirtyOutcome` wins; remaining handlers are skipped.
150
+
151
+ This makes dirty-tree policy fully pluggable. The `default_dirty_handler` plugin
152
+ (bundled, default-on, priority 1000) implements the `stash` / `ignore` /
153
+ `auto_commit` policy from `[vcs] dirty_action` — so the external behavior is
154
+ identical to pre-0.2.0 unless a consumer plugin intervenes first.
155
+
156
+ **Lifecycle-hook family** (4 groups, run in this order per round):
157
+
158
+ 1. `serve_startup_hooks` — once at `agent-runner serve` boot, before the loop
159
+ 2. `pre_round_hooks` — after lock acquire, before context is written
160
+ 3. `post_round_hooks` (+ `context_enrichers`) — after agent exits, before `round_end`
161
+ 4. `dirty_handler_hooks` — after post_round, if the tree is dirty on a clean exit
162
+
163
+ The stash *mechanism* (SHA lock, idempotency guard, `stash_orphan` /
164
+ `try_auto_commit` primitives) stays in core. The stash *policy* is plugin-provided.
165
+
166
+ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
167
+
145
168
  ## Known event kinds
146
169
 
147
170
  <!-- gen:event-kinds -->
@@ -153,6 +176,7 @@ hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"
153
176
  - `anomaly_repetitive_tool`
154
177
  - `config_broken`
155
178
  - `crash_loop`
179
+ - `dirty_auto_committed`
156
180
  - `dirty_commit_failed`
157
181
  - `dirty_detected`
158
182
  - `fresh_eyes_round_triggered`
@@ -90,13 +90,17 @@ Type: string, one of `"stash"`, `"ignore"`, `"auto_commit"`
90
90
  Default: `"stash"`
91
91
 
92
92
  Controls supervisor behavior when round subprocess exits with a dirty
93
- working tree:
93
+ working tree. This config is read by the bundled `default_dirty_handler` plugin
94
+ (0.2.0+, default-on). Existing behavior and TOML are unchanged.
94
95
 
95
96
  | Value | Behavior |
96
97
  |---|---|
97
98
  | `"stash"` | Auto-stash dirty tree with ORPHAN-prefix message. `dirty_detected` + `orphan_stashed` events emitted. |
98
99
  | `"ignore"` | Emit `dirty_detected` event only. Working tree left dirty for next round. |
99
- | `"auto_commit"` | Supervisor commits with subject `agent-runner auto-commit: R<N> <phase>`. No push. On failure, emits `dirty_commit_failed`, leaves tree dirty. |
100
+ | `"auto_commit"` | Supervisor commits with subject `agent-runner auto-commit: R<N> <phase>`. No push. On success, emits `dirty_auto_committed`. On failure, emits `dirty_commit_failed`, leaves tree dirty. |
101
+
102
+ To replace this policy entirely, disable the bundled plugin and register your own
103
+ `DirtyHandler` — see `docs/plugins.md` and `docs/migrations/0.2.0.md`.
100
104
 
101
105
  ## `[agent.env]` (optional)
102
106