cli-agent-runner 0.1.38__tar.gz → 0.1.39__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 (225) hide show
  1. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/CHANGELOG.md +14 -0
  2. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/PKG-INFO +1 -1
  3. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_docgen.py +0 -6
  4. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_emit.py +7 -0
  5. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_version.py +2 -2
  6. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/agent_runtime.py +35 -15
  7. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/config.py +30 -0
  8. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/claude.toml +1 -0
  9. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/runner.py +7 -2
  10. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/commands.md +21 -4
  11. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/configuration.md +5 -0
  12. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/long-running-agents.md +1 -1
  13. cli_agent_runner-0.1.39/docs/migrations/0.1.39.md +74 -0
  14. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/plugins.md +4 -2
  15. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/runbook.md +11 -3
  16. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/thesis.md +4 -3
  17. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_grace_kill_emission.py +63 -0
  18. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_architecture.py +3 -16
  19. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_catalogs.py +0 -1
  20. cli_agent_runner-0.1.39/tests/invariants/test_doc_claims_match_ssot.py +102 -0
  21. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime_grace.py +74 -7
  22. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_observation.py +3 -2
  23. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config.py +45 -0
  24. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_docgen.py +19 -13
  25. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_presets.py +20 -0
  26. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.codecov.yml +0 -0
  27. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  28. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  29. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  30. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  31. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/workflows/ci.yml +0 -0
  32. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.github/workflows/release.yml +0 -0
  33. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.gitignore +0 -0
  34. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/.vulture-whitelist.py +0 -0
  35. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/CODE_OF_CONDUCT.md +0 -0
  36. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/CONTRIBUTING.md +0 -0
  37. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/LICENSE +0 -0
  38. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/README.md +0 -0
  39. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/README.zh.md +0 -0
  40. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/SECURITY.md +0 -0
  41. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/__init__.py +0 -0
  42. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_registry.py +0 -0
  43. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_substrate.py +0 -0
  44. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/_throttle.py +0 -0
  45. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/api.py +0 -0
  46. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/api_types.py +0 -0
  47. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/__init__.py +0 -0
  48. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/_constants.py +0 -0
  49. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  50. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/gemini.py +0 -0
  51. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/__init__.py +0 -0
  52. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/__main__.py +0 -0
  53. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/common.py +0 -0
  54. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/events_cmd.py +0 -0
  55. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/init_cmd.py +0 -0
  56. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/install_cmd.py +0 -0
  57. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/monitor_cmd.py +0 -0
  58. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/peek_cmd.py +0 -0
  59. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/round_cmd.py +0 -0
  60. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/serve_cmd.py +0 -0
  61. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/service_cmd.py +0 -0
  62. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/cli/upgrade_cmd.py +0 -0
  63. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/context_store.py +0 -0
  64. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/defenses.py +0 -0
  65. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/detector_helpers.py +0 -0
  66. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/events.py +0 -0
  67. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/hooks.py +0 -0
  68. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/http_progress.py +0 -0
  69. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/lifecycle.py +0 -0
  70. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/metrics.py +0 -0
  71. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/monitor.py +0 -0
  72. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/__init__.py +0 -0
  73. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/aider.toml +0 -0
  74. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/presets/gemini.toml +0 -0
  75. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/prompt_loader.py +0 -0
  76. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/round_log.py +0 -0
  77. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/round_view.py +0 -0
  78. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/scaffold.py +0 -0
  79. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/service_unit.py +0 -0
  80. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/startup_check.py +0 -0
  81. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/agent_runner/vcs_state.py +0 -0
  82. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/build.sh +0 -0
  83. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/example-agent-runner.toml +0 -0
  84. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/launchd.plist.tmpl +0 -0
  85. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/run-loop.sh +0 -0
  86. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/deploy/systemd.service.tmpl +0 -0
  87. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/README.md +0 -0
  88. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/architecture.md +0 -0
  89. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/events.md +0 -0
  90. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/marketing/README.md +0 -0
  91. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/marketing/promo-cn.html +0 -0
  92. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.16.md +0 -0
  93. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.17.md +0 -0
  94. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.19.md +0 -0
  95. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.20.md +0 -0
  96. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.21.md +0 -0
  97. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.22.md +0 -0
  98. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.23.md +0 -0
  99. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.24.md +0 -0
  100. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.25.md +0 -0
  101. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.26.md +0 -0
  102. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.27.md +0 -0
  103. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.28.md +0 -0
  104. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.29.md +0 -0
  105. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.30.md +0 -0
  106. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.31.md +0 -0
  107. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.32.md +0 -0
  108. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.33.md +0 -0
  109. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.34.md +0 -0
  110. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.35.md +0 -0
  111. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.36.md +0 -0
  112. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.37.md +0 -0
  113. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/migrations/0.1.38.md +0 -0
  114. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/quickstart.md +0 -0
  115. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/docs/recipes/aider.md +0 -0
  116. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/pyproject.toml +0 -0
  117. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/__init__.py +0 -0
  118. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/_test_helpers.py +0 -0
  119. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/conftest.py +0 -0
  120. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/contract/__init__.py +0 -0
  121. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/contract/test_public_api_surface.py +0 -0
  122. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/__init__.py +0 -0
  123. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/conftest.py +0 -0
  124. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  125. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_install_systemd.py +0 -0
  126. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  127. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  128. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  129. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  130. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  131. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/__init__.py +0 -0
  132. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_bounded_run.py +0 -0
  133. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_context_enricher_namespacing.py +0 -0
  134. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_fresh_eyes_signal.py +0 -0
  135. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_install_dry_run.py +0 -0
  136. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_monitor_seeded.py +0 -0
  137. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_detector_loaded.py +0 -0
  138. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_owned_paths.py +0 -0
  139. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_real_flow.py +0 -0
  140. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  141. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_scaffold_presets.py +0 -0
  142. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_serve_loop.py +0 -0
  143. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_substrate_fingerprint.py +0 -0
  144. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/integration/test_transient_error_backoff.py +0 -0
  145. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/__init__.py +0 -0
  146. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_atomic_write_enforced.py +0 -0
  147. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_classification_ssot.py +0 -0
  148. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_docs_generated.py +0 -0
  149. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_entry_points_resolve.py +0 -0
  150. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kind_registry.py +0 -0
  151. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kinds_ssot.py +0 -0
  152. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_events_doc_contract.py +0 -0
  153. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_layer_2_loop_size.py +0 -0
  154. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_module_boundaries.py +0 -0
  155. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_module_sizes.py +0 -0
  156. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_no_ai_signatures.py +0 -0
  157. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  158. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_peek_schema_version.py +0 -0
  159. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  160. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_round_result_stable.py +0 -0
  161. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  162. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/invariants/test_upstream_schema_canary.py +0 -0
  163. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/__init__.py +0 -0
  164. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/parser.py +0 -0
  165. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/test_parser.py +0 -0
  166. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/literate/test_quickstart.py +0 -0
  167. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/__init__.py +0 -0
  168. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime.py +0 -0
  169. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime_progress.py +0 -0
  170. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_assemble_prompt.py +0 -0
  171. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_events_stream.py +0 -0
  172. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_install.py +0 -0
  173. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_read_round_num.py +0 -0
  174. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_resolve_phase.py +0 -0
  175. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_service.py +0 -0
  176. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_api_types.py +0 -0
  177. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_auto_stop_gating.py +0 -0
  178. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_claude_error_detector.py +0 -0
  179. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli.py +0 -0
  180. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_common.py +0 -0
  181. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_init_install.py +0 -0
  182. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_monitor_http.py +0 -0
  183. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  184. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_cli_upgrade.py +0 -0
  185. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_fresh_eyes.py +0 -0
  186. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_max_rounds.py +0 -0
  187. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_stop_file.py +0 -0
  188. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  189. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_config_transient_error_action.py +0 -0
  190. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_context_store.py +0 -0
  191. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_defenses.py +0 -0
  192. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_detector_helpers.py +0 -0
  193. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_detector_protocol.py +0 -0
  194. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_events.py +0 -0
  195. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_events_cmd.py +0 -0
  196. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  197. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_gemini_plugin.py +0 -0
  198. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_hook_failure_isolation.py +0 -0
  199. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_hooks.py +0 -0
  200. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_http_progress.py +0 -0
  201. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_init_entry_points.py +0 -0
  202. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_lifecycle.py +0 -0
  203. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_metrics.py +0 -0
  204. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_assembly.py +0 -0
  205. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  206. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  207. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  208. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detectors.py +0 -0
  209. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_remote.py +0 -0
  210. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_peek_argparse.py +0 -0
  211. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_peek_select.py +0 -0
  212. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_prompt_loader.py +0 -0
  213. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_round_log_helpers.py +0 -0
  214. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_round_view.py +0 -0
  215. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_runner.py +0 -0
  216. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_runner_throttle.py +0 -0
  217. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_scaffold.py +0 -0
  218. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_cmd_bounded.py +0 -0
  219. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_round_log.py +0 -0
  220. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_sentinel.py +0 -0
  221. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_serve_startup_hooks.py +0 -0
  222. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_service_unit.py +0 -0
  223. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_startup_check.py +0 -0
  224. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_substrate.py +0 -0
  225. {cli_agent_runner-0.1.38 → cli_agent_runner-0.1.39}/tests/unit/test_vcs_state.py +0 -0
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+ - Grace-kill (`max_grace_after_result_s`) is no longer defeated by long-lived helper subprocesses (e.g. claude's persistent Bash-tool shell-snapshot). `[runtime] grace_kill_ignore_patterns` lists regexes for cmdlines to exclude from the liveness count; the claude preset ships a matching default.
12
+
13
+ ### Added
14
+ - `[runtime] grace_kill_ignore_patterns: list[str]` — regex patterns; matching child cmdlines are excluded from the grace-kill liveness check.
15
+ - `round_grace_extended` event payload gains `ignored_children` — cmdlines filtered by `grace_kill_ignore_patterns`.
16
+
17
+ ### Changed
18
+ - Docs: `commands.md` documents `monitor --mode/--port` and `init --preset`; the Chinese verb list and `[monitor]` default values now point to the generated tables instead of restating them; runbook upgrade examples use a version placeholder.
19
+
20
+ ### Internal
21
+ - New invariant `test_doc_claims_match_ssot` gates documented counts (detectors / defenses / verbs) and config value-sets (`dirty_action` / `context_injection_mode` / transient classification) against their code SSOT — count/enum doc drift now fails CI at the introducing commit.
22
+ - Removed the unused `alert-kinds` docgen renderer; de-duplicated redundant defense-count and alert-kind guards to one canonical tripwire each.
23
+
10
24
  ## [0.1.38] - 2026-05-24
11
25
 
12
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.38
3
+ Version: 0.1.39
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
@@ -110,11 +110,6 @@ def render_defenses_table() -> str:
110
110
  return "\n".join(lines)
111
111
 
112
112
 
113
- def render_alert_kinds_list() -> str:
114
- """Flat bullet list of all known alert kinds, alphabetised."""
115
- return "\n".join(f"- `{k}`" for k in sorted(KNOWN_ALERT_KINDS))
116
-
117
-
118
113
  def render_detector_list() -> str:
119
114
  """Bullet list of detectors; auto-stop kinds flagged inline."""
120
115
  lines: list[str] = []
@@ -155,7 +150,6 @@ def render_verb_table() -> str:
155
150
 
156
151
  RENDERERS: dict[str, Callable[[], str]] = {
157
152
  "defenses-table": render_defenses_table,
158
- "alert-kinds": render_alert_kinds_list,
159
153
  "detector-list": render_detector_list,
160
154
  "event-kinds": render_event_kinds_list,
161
155
  "config-schema": render_config_schema_table,
@@ -258,10 +258,16 @@ def emit_round_grace_extended(
258
258
  round_num: int,
259
259
  grace_s: int,
260
260
  live_children: list[str],
261
+ ignored_children: list[str] | None = None,
261
262
  ) -> None:
262
263
  """Emit when the grace-after-result timer expired but the agent still had
263
264
  live worker processes (e.g. a backgrounded build), so the round was NOT
264
265
  killed; it continues until it finishes or hits round_timeout_s.
266
+
267
+ ignored_children: cmdlines that matched a grace_kill_ignore_patterns entry
268
+ and were excluded from the liveness count — useful for verifying
269
+ patterns are firing and for noticing when an upstream CLI changes
270
+ its helper path.
265
271
  """
266
272
  from agent_runner.events import ROUND_GRACE_EXTENDED, emit
267
273
 
@@ -271,6 +277,7 @@ def emit_round_grace_extended(
271
277
  round_num=round_num,
272
278
  grace_s=grace_s,
273
279
  live_children=live_children,
280
+ ignored_children=ignored_children or [],
274
281
  )
275
282
 
276
283
 
@@ -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.38'
22
- __version_tuple__ = version_tuple = (0, 1, 38)
21
+ __version__ = version = '0.1.39'
22
+ __version_tuple__ = version_tuple = (0, 1, 39)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -11,6 +11,7 @@ Defenses encoded here:
11
11
  from __future__ import annotations
12
12
 
13
13
  import os
14
+ import re
14
15
  import signal
15
16
  import subprocess # noqa: TID251 — sanctioned subprocess caller
16
17
  import time
@@ -57,18 +58,26 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
57
58
  pass
58
59
 
59
60
 
60
- def _live_children(proc: subprocess.Popen, *, max_n: int = 5, max_len: int = 120) -> list[str]:
61
- """Cmdlines of live (non-zombie) descendant processes of ``proc``.
62
-
63
- Empty when ``proc`` has no live workers (a stuck agent that emitted
64
- type=result then hung). Non-empty when the round backgrounded work (e.g. a
65
- build) still running. Bounded so the resulting event stays small.
61
+ def _live_children(
62
+ proc: subprocess.Popen,
63
+ *,
64
+ ignore_patterns: list[re.Pattern[str]] | None = None,
65
+ max_n: int = 5,
66
+ max_len: int = 120,
67
+ ) -> tuple[list[str], list[str]]:
68
+ """Cmdlines of live (non-zombie) descendants of ``proc``, split into
69
+ ``(live, ignored)``: ``live`` is what counts toward the grace-kill
70
+ liveness check; ``ignored`` matched an ``ignore_patterns`` entry and is
71
+ excluded (e.g. claude's persistent shell-snapshot helper). Both lists
72
+ are bounded by ``max_n``/``max_len`` to keep events small. ``ignore_patterns
73
+ is None`` → no filtering, ``ignored`` is empty, ``live`` matches 0.1.38.
66
74
  """
67
75
  try:
68
76
  parent = psutil.Process(proc.pid)
69
77
  except (psutil.NoSuchProcess, psutil.AccessDenied):
70
- return []
71
- out: list[str] = []
78
+ return [], []
79
+ live: list[str] = []
80
+ ignored: list[str] = []
72
81
  for child in parent.children(recursive=True):
73
82
  try:
74
83
  if child.status() == psutil.STATUS_ZOMBIE:
@@ -76,10 +85,16 @@ def _live_children(proc: subprocess.Popen, *, max_n: int = 5, max_len: int = 120
76
85
  line = " ".join(child.cmdline()) or child.name()
77
86
  except (psutil.NoSuchProcess, psutil.AccessDenied):
78
87
  continue
79
- out.append(line[:max_len])
80
- if len(out) >= max_n:
88
+ short = line[:max_len]
89
+ if ignore_patterns and any(p.search(line) for p in ignore_patterns):
90
+ if len(ignored) < max_n:
91
+ ignored.append(short)
92
+ else:
93
+ if len(live) < max_n:
94
+ live.append(short)
95
+ if len(live) >= max_n and len(ignored) >= max_n:
81
96
  break
82
- return out
97
+ return live, ignored
83
98
 
84
99
 
85
100
  # Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
@@ -99,7 +114,8 @@ def run(
99
114
  max_grace_after_result_s: int = 0,
100
115
  progress_callback: Callable[[dict], None] | None = None,
101
116
  progress_interval_s: int = 0,
102
- on_grace_extended: Callable[[list[str]], None] | None = None,
117
+ on_grace_extended: Callable[[list[str], list[str]], None] | None = None,
118
+ grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
103
119
  ) -> RunResult:
104
120
  """Spawn the agent subprocess and wait for exit or timeout.
105
121
 
@@ -116,6 +132,10 @@ def run(
116
132
  progress_interval_s seconds with a dict of log stats (log_size_kb,
117
133
  last_write_age_s, wall_age_s). Keeps agent_runtime event-free; callers
118
134
  build the callback to emit events.
135
+
136
+ grace_kill_ignore_patterns: pre-compiled regex patterns; child cmdlines
137
+ matching any pattern (re.search) are excluded from the liveness count
138
+ (persistent helpers that aren't real workers). None = no filtering.
119
139
  """
120
140
  argv = _build_argv(command, prompt_arg_template, prompt)
121
141
  env = {**os.environ, **env_extra}
@@ -157,13 +177,13 @@ def run(
157
177
  except OSError:
158
178
  pass # log not flushed yet; check next tick
159
179
  if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
160
- children = _live_children(proc)
161
- if children:
180
+ live, ignored = _live_children(proc, ignore_patterns=grace_kill_ignore_patterns)
181
+ if live:
162
182
  # Busy: a backgrounded worker is still running. Don't
163
183
  # reap — defer to the wall-clock ceiling. Signal once.
164
184
  if not grace_extended_emitted:
165
185
  if on_grace_extended is not None:
166
- on_grace_extended(children)
186
+ on_grace_extended(live, ignored)
167
187
  grace_extended_emitted = True
168
188
  else:
169
189
  _kill_pgroup(proc)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import tomllib
6
7
  from dataclasses import dataclass, field
7
8
  from pathlib import Path
@@ -40,6 +41,12 @@ class RuntimeConfig:
40
41
  fresh_eyes_every_n: int | None = None # None = disabled
41
42
  dry_run: bool = False
42
43
  max_grace_after_result_s: int = 0 # 0 = disabled
44
+ grace_kill_ignore_patterns: list[str] = field(default_factory=list)
45
+ """Regex patterns (re.search) tested against each child process's joined
46
+ cmdline. Matching children are excluded from the grace-kill liveness
47
+ check — for persistent helper subprocesses (e.g. claude's shell-snapshot
48
+ bash) that would otherwise defeat max_grace_after_result_s. Empty list
49
+ = no filtering (0.1.38 behavior preserved)."""
43
50
 
44
51
 
45
52
  @dataclass(frozen=True)
@@ -221,6 +228,25 @@ def _validate_remote_failure_tolerance(value: Any) -> int:
221
228
  return v
222
229
 
223
230
 
231
+ def _validate_regex_list(value: Any, *, field: str) -> list[str]:
232
+ """Validate a list of regex pattern strings (each must compile). Returns the
233
+ raw strings unchanged; callers compile when they need ``re.Pattern`` objects."""
234
+ if not isinstance(value, list):
235
+ raise ValueError(f"{field}: expected a list of regex strings, got {type(value).__name__}")
236
+ out: list[str] = []
237
+ for p in value:
238
+ if not isinstance(p, str):
239
+ raise ValueError(
240
+ f"{field}: each pattern must be a string, got {type(p).__name__}: {p!r}"
241
+ )
242
+ try:
243
+ re.compile(p)
244
+ except re.error as e:
245
+ raise ValueError(f"{field}: invalid regex {p!r}: {e}") from e
246
+ out.append(p)
247
+ return out
248
+
249
+
224
250
  _PHASE_OVERRIDE_ALLOWED_FIELDS = frozenset(
225
251
  {
226
252
  "round_timeout_s",
@@ -392,6 +418,10 @@ def load_config(toml_path: Path) -> Config:
392
418
  runtime_d.get("max_grace_after_result_s", 0),
393
419
  field="runtime.max_grace_after_result_s",
394
420
  ),
421
+ grace_kill_ignore_patterns=_validate_regex_list(
422
+ runtime_d.get("grace_kill_ignore_patterns", []),
423
+ field="runtime.grace_kill_ignore_patterns",
424
+ ),
395
425
  )
396
426
  prompt_d = raw.get("prompt", {})
397
427
  mode = prompt_d.get("context_injection_mode", "prepend")
@@ -16,6 +16,7 @@ work_dir = "."
16
16
  log_dir = "~/.agent-runner/{project}/logs"
17
17
  round_timeout_s = 1800
18
18
  restart_delay_s = 3
19
+ grace_kill_ignore_patterns = ['\.claude/shell-snapshots/snapshot-bash-']
19
20
 
20
21
  [prompt]
21
22
  file = "./prompts/main.md"
@@ -10,6 +10,7 @@ import hashlib
10
10
  import json
11
11
  import os
12
12
  import random
13
+ import re
13
14
  import sys
14
15
  import time
15
16
  import traceback as tb_mod
@@ -466,12 +467,15 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
466
467
  **stats,
467
468
  )
468
469
 
469
- def _grace_extended_emit(children: list[str]) -> None:
470
+ grace_kill_ignore_patterns = [re.compile(p) for p in cfg.runtime.grace_kill_ignore_patterns]
471
+
472
+ def _grace_extended_emit(live: list[str], ignored: list[str]) -> None:
470
473
  api.emit_round_grace_extended(
471
474
  log_dir,
472
475
  round_num=round_num,
473
476
  grace_s=cfg.runtime.max_grace_after_result_s,
474
- live_children=children,
477
+ live_children=live,
478
+ ignored_children=ignored,
475
479
  )
476
480
 
477
481
  result = agent_runtime.run(
@@ -485,6 +489,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
485
489
  progress_callback=_progress_emit,
486
490
  progress_interval_s=cfg.monitor.round_progress_interval_s,
487
491
  on_grace_extended=_grace_extended_emit,
492
+ grace_kill_ignore_patterns=grace_kill_ignore_patterns,
488
493
  )
489
494
  events.emit(
490
495
  log_dir,
@@ -34,8 +34,15 @@ are shared between `peek`, `watch`, and `monitor`.
34
34
  Scaffold a new project: writes `agent-runner.toml`, `prompts/main.md`, and
35
35
  appends `logs/` to `.gitignore`. By default also creates a git commit.
36
36
 
37
+ Flags:
38
+
39
+ - `--preset {claude,aider,gemini}` — agent CLI preset to scaffold (default: `claude`)
40
+ - `--force` — overwrite an existing `agent-runner.toml`
41
+ - `--no-commit` — skip the initial git commit
42
+
37
43
  ```bash
38
- agent-runner init # default: commit
44
+ agent-runner init # default: claude preset, commit
45
+ agent-runner init --preset aider # aider preset
39
46
  agent-runner init --no-commit # skip the commit
40
47
  agent-runner init --force # overwrite an existing toml
41
48
  ```
@@ -133,7 +140,7 @@ agent-runner events --kind transient_error_backoff_capped --tail
133
140
 
134
141
  `peek` in a clear-and-refresh loop. Default 2s interval. Stop with Ctrl-C.
135
142
 
136
- ### `agent-runner monitor [--host SSH-ALIAS] [--interval N] [--json]`
143
+ ### `agent-runner monitor [--host SSH-ALIAS] [--interval N] [--mode MODE] [--port PORT] [--json]`
137
144
 
138
145
  Anomaly-detection daemon. Runs the 12 detectors against the live state on every
139
146
  poll. Without `--host`, watches local logs at default 30s interval. With
@@ -143,15 +150,25 @@ When OAuth-fail or disk-critical detectors fire, monitor automatically issues a
143
150
  graceful stop (locally via `api.stop`; remotely via `ssh <host> 'agent-runner stop'`).
144
151
  Override with `[monitor]` config block (see configuration.md).
145
152
 
153
+ Flags:
154
+
155
+ - `--mode {anomaly,narrate,events,http}` — output mode (default: `anomaly`). `narrate`
156
+ streams a human-readable narrative; `events` streams raw event JSON; `http` serves
157
+ a local progress page.
158
+ - `--port PORT` — HTTP port for `--mode http` (default: `8765`, local-only).
159
+ - `--host SSH-ALIAS` — watch a remote agent-runner via ssh (anomaly mode only).
160
+
146
161
  ```bash
147
- agent-runner monitor # local
162
+ agent-runner monitor # local anomaly mode
148
163
  agent-runner monitor --host pi # remote
164
+ agent-runner monitor --mode narrate # streaming narrative
165
+ agent-runner monitor --mode http --port 9000 # HTTP progress page on port 9000
149
166
  agent-runner monitor --json | jq -c # pipe alerts to a downstream consumer
150
167
  ```
151
168
 
152
169
  ## 中文摘要
153
170
 
154
- 16 个动词:`init / install / uninstall / start / stop / kill / cancel / restart / status / round / serve / upgrade / peek / watch / events / monitor`。
171
+ 16 个动词,完整列表见上方动词表(自动生成)。
155
172
 
156
173
  观察类(peek/watch/monitor)三视角对称,全部共用 `--round / --log / --events / --select / --json` 下钻参数。
157
174
 
@@ -47,6 +47,7 @@ running with newly-set `dirty_action = "auto_commit"` is undefined).
47
47
  | `fresh_eyes_every_n` | `int | None` | None |
48
48
  | `dry_run` | `bool` | False |
49
49
  | `max_grace_after_result_s` | `int` | 0 |
50
+ | `grace_kill_ignore_patterns` | `list[str]` | [] |
50
51
 
51
52
  ### `[prompt]`
52
53
 
@@ -200,6 +201,10 @@ Unconfigured phases (and configs without `[phases]`) keep using the global
200
201
 
201
202
  ## `[monitor]` (optional, defaults shown)
202
203
 
204
+ > Authoritative field-level defaults are in the generated schema table above
205
+ > (`[monitor]` section). The snippet below shows only the fields most commonly
206
+ > customised, with operational notes.
207
+
203
208
  ```toml
204
209
  [monitor]
205
210
  auto_stop_on = ["oauth_fail", "disk_critical"]
@@ -140,7 +140,7 @@ token breakdown + cost (where the underlying CLI exposes it).
140
140
  ```
141
141
 
142
142
  Use as input to a cost-tracking detector or external billing reconciler.
143
- See `docs/migrations/0.1.28.md` for the current 12-field payload schema
143
+ See `docs/migrations/0.1.28.md` for the current payload schema
144
144
  (includes `cache_creation_tokens`, `tool_call_count`, `phase`, `success`)
145
145
  plus a consumer dispatcher sketch. Aggregation (rollups, budget warnings)
146
146
  is the consumer's responsibility — agent-runner emits raw per-round
@@ -0,0 +1,74 @@
1
+ # Migrating to 0.1.39
2
+
3
+ ## TL;DR
4
+
5
+ ```bash
6
+ pip install --upgrade cli-agent-runner==0.1.39
7
+ ```
8
+
9
+ **Claude users running 0.1.38**: add one line to `[runtime]` to unblock
10
+ grace-kill against claude's persistent shell-snapshot helper (see below).
11
+ New `agent-runner init --preset=claude` scaffolds get this automatically.
12
+
13
+ **Everyone else**: no action.
14
+
15
+ ## Persistent-helper exclusion (the live fix)
16
+
17
+ 0.1.38's grace-kill liveness check was correctly conservative — it refused to
18
+ reap a round with live worker children — but claude's `-p` mode keeps a
19
+ persistent Bash-tool shell-snapshot subprocess alive for the whole session.
20
+ That subprocess is not doing work; it's idle infrastructure. 0.1.38 saw it as
21
+ a live worker and deferred every post-result hang to `round_timeout_s` instead
22
+ of reaping at `max_grace_after_result_s`. This is the "persistent-helper
23
+ caveat" 0.1.38's migration doc flagged.
24
+
25
+ 0.1.39 adds `[runtime] grace_kill_ignore_patterns` — a list of regex patterns;
26
+ child cmdlines matching any pattern (via `re.search`) are excluded from the
27
+ liveness count. `presets/claude.toml` ships a default pattern matching
28
+ claude's shell-snapshot.
29
+
30
+ ### Existing claude operators — one line
31
+
32
+ Add to your `[runtime]` block:
33
+
34
+ ```toml
35
+ [runtime]
36
+ grace_kill_ignore_patterns = ['\.claude/shell-snapshots/snapshot-bash-']
37
+ ```
38
+
39
+ Or run `agent-runner init --preset=claude` in a scratch directory and diff
40
+ the generated `agent-runner.toml` against yours.
41
+
42
+ After the change, post-result hangs are reaped at `max_grace_after_result_s`.
43
+ Without it, they continue to defer to `round_timeout_s` (the 0.1.38 behavior).
44
+
45
+ ### Verifying the pattern is firing
46
+
47
+ The `round_grace_extended` event payload gains `ignored_children` listing
48
+ cmdlines that matched a pattern. Use it to:
49
+
50
+ - confirm the shell-snapshot is being filtered (`ignored_children` non-empty)
51
+ - catch the day claude renames its helper (`live_children` shows a new
52
+ unfiltered persistent process)
53
+
54
+ ### Other presets
55
+
56
+ `aider.toml` and `gemini.toml` ship no default patterns. Add operator-specific
57
+ patterns to your own `agent-runner.toml` if needed.
58
+
59
+ ## SSOT consistency hardening (also in 0.1.39)
60
+
61
+ A new invariant `test_doc_claims_match_ssot` gates documented counts
62
+ (detectors / defenses / verbs) and config value-sets against code SSOT.
63
+ `commands.md` documents `monitor --mode/--port` and `init --preset`.
64
+ Redundant count guards collapsed to one canonical tripwire each. The unused
65
+ `alert-kinds` docgen renderer was removed. No action required.
66
+
67
+ ## What did NOT change
68
+
69
+ - The 0.1.38 grace-kill liveness semantics (still process-group-based;
70
+ patterns are an exclusion filter on top).
71
+ - `round_grace_kill` (still fires only when the post-filter live set is empty).
72
+ - `round_timeout_s` (still the hard ceiling).
73
+ - `max_grace_after_result_s` (knob unchanged).
74
+ - For non-claude deployments: zero behavior change.
@@ -170,6 +170,8 @@ Any exception raised by a hook is caught by the runner and emitted as a built-in
170
170
  }
171
171
  ```
172
172
 
173
+ (Fields emitted by the `HOOK_FAILED` path in `runner.py` + `_summarize_error` in `hooks.py`.)
174
+
173
175
  The round itself continues — a broken plugin must not crash the supervisor.
174
176
 
175
177
  ### What `plugin_context_enrichers()` surfaces
@@ -293,7 +295,7 @@ event with `classification` ∈ {`rate_limit_account`, `rate_limit_model`,
293
295
 
294
296
  Per round (regardless of error state), also emits `agent_usage_recorded`
295
297
  with token/cost/duration data extracted from the claude result event —
296
- see `docs/migrations/0.1.28.md` for the full 12-field schema. The
298
+ see `docs/migrations/0.1.28.md` for the full payload schema. The
297
299
  supervisor reads `transient_error_detected` on the next dispatch cycle
298
300
  and applies the configured `transient_error_action` (default `back_off`;
299
301
  `rate_limit_action` retained as a deprecated alias).
@@ -309,7 +311,7 @@ to ship equivalent detectors for other agent CLIs — the bundled
309
311
  ## Custom monitor detectors (§3.3)
310
312
 
311
313
  0.1.5 adds a fourth extension point — plugin authors can ship custom monitor
312
- detectors that run alongside the 11 builtins on every monitor poll.
314
+ detectors that run alongside the 12 builtins on every monitor poll.
313
315
 
314
316
  ### Group + Protocol
315
317
 
@@ -153,7 +153,7 @@ RestartSec=5
153
153
 
154
154
  ### Path 1 — systemd --user service (installed via `agent-runner install`)
155
155
 
156
- agent-runner upgrade --target 0.1.37
156
+ agent-runner upgrade --target <version>
157
157
 
158
158
  Does stop → pip → smoke → start, with auto-rollback on smoke failure.
159
159
 
@@ -167,7 +167,7 @@ package-only upgrade (pip + smoke + rollback), then prints the restart command.
167
167
  It never runs `sudo` and never starts a service it didn't install. Restart your
168
168
  supervisor yourself:
169
169
 
170
- python3 -m pip install --user --break-system-packages --upgrade cli-agent-runner==0.1.37
170
+ python3 -m pip install --user --break-system-packages --upgrade cli-agent-runner==<version>
171
171
  agent-runner --version
172
172
  sudo systemctl restart <your-unit>
173
173
 
@@ -177,7 +177,7 @@ supervisor yourself:
177
177
  Use `--no-restart` to force package-only mode even on a systemd --user host
178
178
  (upgrade the package now, restart later):
179
179
 
180
- agent-runner upgrade --target 0.1.37 --no-restart
180
+ agent-runner upgrade --target <version> --no-restart
181
181
 
182
182
  ### Manual rollback
183
183
 
@@ -584,6 +584,14 @@ work past `type=result`. Check the `live_children` field in the event to identif
584
584
  the process; consider restructuring the agent to emit `type=result` only when
585
585
  truly done.
586
586
 
587
+ **Persistent-helper exclusion (0.1.39+):** when an agent CLI keeps long-lived
588
+ helper subprocesses alive past `type=result` (claude does this with a Bash-tool
589
+ shell-snapshot), they would otherwise count as "live workers" and defer every
590
+ post-result hang to `round_timeout_s`. Set `[runtime] grace_kill_ignore_patterns
591
+ = [<regex>, ...]` to exclude them; the `claude` preset ships a default. The
592
+ `round_grace_extended` event's `ignored_children` field shows which cmdlines
593
+ matched a pattern.
594
+
587
595
  ### Disk pressure
588
596
 
589
597
  **Symptom:** `[WARN] disk_warning` at >90%; `[CRIT] disk_critical` at >95% (auto-stops).
@@ -54,9 +54,10 @@ would produce constant false positives across diverse workloads.
54
54
  The `anomaly_repetitive_active` detector (added 0.1.32) is the live example:
55
55
  it fires when the claude plugin emits `anomaly_repetitive_tool` events
56
56
  above a fixed threshold within a window — a specific signature, not N-σ.
57
- `max_grace_after_result_s` (0.1.31) is another: kills the subprocess after
58
- a fixed grace following the `result` event specific signature, not "is
59
- this subprocess behaving unusually".
57
+ `max_grace_after_result_s` (0.1.31, refined 0.1.38) is another: a fixed
58
+ grace after the `result` event, the subprocess is killed only if its
59
+ process group has no live worker left — specific signature, not "is this
60
+ subprocess behaving unusually".
60
61
 
61
62
  > **Example**: A 2026-05-18 proposal requested a "cost spike detector" that
62
63
  > fires when this round's cost is N× the rolling 7-day average. Rejected.
@@ -79,6 +79,28 @@ def test_grace_kill_emits_round_grace_kill_event(tmp_path: Path) -> None:
79
79
  assert len(timeout_events) == 0
80
80
 
81
81
 
82
+ def _make_grace_config_with_patterns(
83
+ work_dir: Path, script_path: Path, grace_s: int, patterns: list[str]
84
+ ) -> Config:
85
+ log_dir = work_dir / "logs"
86
+ log_dir.mkdir(exist_ok=True)
87
+ prompt = work_dir / "p.md"
88
+ prompt.write_text("Test prompt. " * 50)
89
+ return Config(
90
+ agent=AgentConfig(command=[str(script_path)], prompt_arg_template=[]),
91
+ runtime=RuntimeConfig(
92
+ work_dir=work_dir,
93
+ log_dir=log_dir,
94
+ round_timeout_s=10,
95
+ max_grace_after_result_s=grace_s,
96
+ grace_kill_ignore_patterns=patterns,
97
+ ),
98
+ prompt=PromptConfig(file=prompt, inject_context=False),
99
+ vcs=VcsConfig(),
100
+ phases=PhasesConfig(),
101
+ )
102
+
103
+
82
104
  def test_round_grace_extended_emitted_when_worker_alive(tmp_path: Path) -> None:
83
105
  """Full runner flow: subprocess emits result then backgrounds a long child;
84
106
  round_grace_extended event fires (not round_grace_kill); wall timeout reaps."""
@@ -109,3 +131,44 @@ def test_round_grace_extended_emitted_when_worker_alive(tmp_path: Path) -> None:
109
131
  # round_grace_kill must NOT appear (round was busy, not idle)
110
132
  grace_kill_events = [e for e in events if e.get("event") == "round_grace_kill"]
111
133
  assert len(grace_kill_events) == 0
134
+
135
+
136
+ def test_round_grace_extended_carries_ignored_children(tmp_path: Path) -> None:
137
+ """With grace_kill_ignore_patterns set, persistent helpers appear under
138
+ ignored_children, not live_children — even when a real worker is also alive."""
139
+ _init_git(tmp_path)
140
+
141
+ script = tmp_path / "agent.sh"
142
+ # Emit result, then background both a snapshot-like helper and a 'real' sleep.
143
+ # exec -a renames the subprocess's argv[0] so the pattern can match it.
144
+ script.write_text(
145
+ "#!/bin/bash\n"
146
+ 'echo \'{"type":"result","is_error":false}\'\n'
147
+ "exec -a snapshot-bash-test sleep 30 &\n"
148
+ "sleep 30 &\n"
149
+ "wait\n",
150
+ encoding="utf-8",
151
+ )
152
+ script.chmod(0o755)
153
+
154
+ cfg = _make_grace_config_with_patterns(
155
+ tmp_path, script, grace_s=1, patterns=["snapshot-bash-test"]
156
+ )
157
+ result = run_one_round(cfg)
158
+
159
+ assert result.killed_for_grace is False # real worker kept it alive
160
+ assert result.timed_out is True # wall-clock reaped it
161
+
162
+ events_list = read_events_for_current_month(cfg.runtime.log_dir)
163
+ extended_events = [e for e in events_list if e.get("event") == "round_grace_extended"]
164
+ assert len(extended_events) == 1
165
+ ev = extended_events[0]
166
+
167
+ # The plain sleep goes to live_children (real worker)
168
+ assert any("sleep" in c for c in ev["live_children"])
169
+ # The exec -a snapshot-bash-test process goes to ignored_children
170
+ assert any("snapshot-bash-test" in c for c in ev["ignored_children"])
171
+
172
+ # round_grace_kill must NOT appear (real worker still alive)
173
+ grace_kill_events = [e for e in events_list if e.get("event") == "round_grace_kill"]
174
+ assert len(grace_kill_events) == 0
@@ -118,21 +118,8 @@ def test_given_api_types_when_inspected_then_all_frozen_dataclasses() -> None:
118
118
  assert cls.__dataclass_params__.frozen, f"{name} not frozen"
119
119
 
120
120
 
121
- def test_given_known_alert_kinds_when_inspected_then_matches_twelve_detectors() -> None:
121
+ def test_given_known_alert_kinds_when_inspected_then_well_formed() -> None:
122
122
  from agent_runner.monitor import KNOWN_ALERT_KINDS
123
123
 
124
- expected = {
125
- "timeout_rate",
126
- "hung",
127
- "orphan_chain",
128
- "disk_warning",
129
- "disk_critical",
130
- "mem_pressure",
131
- "smoke_fail_rate",
132
- "oauth_fail",
133
- "network_fail",
134
- "rate_limit_active",
135
- "anomaly_repetitive_active",
136
- "supervisor_stale",
137
- }
138
- assert KNOWN_ALERT_KINDS == expected
124
+ assert len(KNOWN_ALERT_KINDS) == 12
125
+ assert all(re.fullmatch(r"[a-z][a-z0-9_]*", k) for k in KNOWN_ALERT_KINDS)
@@ -26,7 +26,6 @@ def test_given_defenses_catalog_when_loaded_then_each_entry_has_required_fields(
26
26
  phases=None,
27
27
  )
28
28
  cat = catalog(cfg)
29
- assert len(cat) == 11
30
29
  for d in cat:
31
30
  assert d.name and isinstance(d.name, str)
32
31
  assert d.current_state in {"active", "degraded", "off"}