cli-agent-runner 0.1.37__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 (227) hide show
  1. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/CHANGELOG.md +24 -0
  2. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/PKG-INFO +1 -1
  3. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_docgen.py +0 -6
  4. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_emit.py +42 -6
  5. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_version.py +2 -2
  6. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/agent_runtime.py +77 -15
  7. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/api.py +1 -0
  8. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/config.py +30 -0
  9. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/events.py +1 -0
  10. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/claude.toml +1 -0
  11. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/runner.py +15 -0
  12. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/architecture.md +1 -0
  13. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/commands.md +21 -4
  14. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/configuration.md +5 -0
  15. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/long-running-agents.md +1 -1
  16. cli_agent_runner-0.1.39/docs/migrations/0.1.38.md +53 -0
  17. cli_agent_runner-0.1.39/docs/migrations/0.1.39.md +74 -0
  18. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/plugins.md +4 -2
  19. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/runbook.md +30 -3
  20. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/thesis.md +4 -3
  21. cli_agent_runner-0.1.39/tests/integration/test_grace_kill_emission.py +174 -0
  22. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_architecture.py +3 -16
  23. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_catalogs.py +7 -1
  24. cli_agent_runner-0.1.39/tests/invariants/test_doc_claims_match_ssot.py +102 -0
  25. cli_agent_runner-0.1.39/tests/unit/test_agent_runtime_grace.py +226 -0
  26. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_observation.py +3 -2
  27. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config.py +45 -0
  28. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_docgen.py +19 -13
  29. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_presets.py +20 -0
  30. cli_agent_runner-0.1.37/tests/integration/test_grace_kill_emission.py +0 -79
  31. cli_agent_runner-0.1.37/tests/unit/test_agent_runtime_grace.py +0 -72
  32. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.codecov.yml +0 -0
  33. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  34. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  35. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  36. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  37. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/workflows/ci.yml +0 -0
  38. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.github/workflows/release.yml +0 -0
  39. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.gitignore +0 -0
  40. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/.vulture-whitelist.py +0 -0
  41. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/CODE_OF_CONDUCT.md +0 -0
  42. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/CONTRIBUTING.md +0 -0
  43. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/LICENSE +0 -0
  44. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/README.md +0 -0
  45. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/README.zh.md +0 -0
  46. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/SECURITY.md +0 -0
  47. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/__init__.py +0 -0
  48. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_registry.py +0 -0
  49. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_substrate.py +0 -0
  50. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/_throttle.py +0 -0
  51. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/api_types.py +0 -0
  52. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/__init__.py +0 -0
  53. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/_constants.py +0 -0
  54. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  55. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/builtin_plugins/gemini.py +0 -0
  56. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/__init__.py +0 -0
  57. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/__main__.py +0 -0
  58. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/common.py +0 -0
  59. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/events_cmd.py +0 -0
  60. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/init_cmd.py +0 -0
  61. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/install_cmd.py +0 -0
  62. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/monitor_cmd.py +0 -0
  63. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/peek_cmd.py +0 -0
  64. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/round_cmd.py +0 -0
  65. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/serve_cmd.py +0 -0
  66. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/service_cmd.py +0 -0
  67. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/cli/upgrade_cmd.py +0 -0
  68. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/context_store.py +0 -0
  69. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/defenses.py +0 -0
  70. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/detector_helpers.py +0 -0
  71. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/hooks.py +0 -0
  72. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/http_progress.py +0 -0
  73. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/lifecycle.py +0 -0
  74. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/metrics.py +0 -0
  75. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/monitor.py +0 -0
  76. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/__init__.py +0 -0
  77. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/aider.toml +0 -0
  78. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/presets/gemini.toml +0 -0
  79. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/prompt_loader.py +0 -0
  80. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/round_log.py +0 -0
  81. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/round_view.py +0 -0
  82. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/scaffold.py +0 -0
  83. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/service_unit.py +0 -0
  84. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/startup_check.py +0 -0
  85. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/agent_runner/vcs_state.py +0 -0
  86. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/build.sh +0 -0
  87. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/example-agent-runner.toml +0 -0
  88. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/launchd.plist.tmpl +0 -0
  89. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/run-loop.sh +0 -0
  90. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/deploy/systemd.service.tmpl +0 -0
  91. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/README.md +0 -0
  92. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/events.md +0 -0
  93. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/marketing/README.md +0 -0
  94. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/marketing/promo-cn.html +0 -0
  95. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.16.md +0 -0
  96. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.17.md +0 -0
  97. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.19.md +0 -0
  98. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.20.md +0 -0
  99. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.21.md +0 -0
  100. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.22.md +0 -0
  101. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.23.md +0 -0
  102. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.24.md +0 -0
  103. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.25.md +0 -0
  104. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.26.md +0 -0
  105. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.27.md +0 -0
  106. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.28.md +0 -0
  107. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.29.md +0 -0
  108. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.30.md +0 -0
  109. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.31.md +0 -0
  110. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.32.md +0 -0
  111. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.33.md +0 -0
  112. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.34.md +0 -0
  113. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.35.md +0 -0
  114. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.36.md +0 -0
  115. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/migrations/0.1.37.md +0 -0
  116. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/quickstart.md +0 -0
  117. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/docs/recipes/aider.md +0 -0
  118. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/pyproject.toml +0 -0
  119. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/__init__.py +0 -0
  120. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/_test_helpers.py +0 -0
  121. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/conftest.py +0 -0
  122. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/contract/__init__.py +0 -0
  123. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/contract/test_public_api_surface.py +0 -0
  124. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/__init__.py +0 -0
  125. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/conftest.py +0 -0
  126. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  127. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_install_systemd.py +0 -0
  128. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  129. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  130. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  131. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  132. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  133. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/__init__.py +0 -0
  134. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_bounded_run.py +0 -0
  135. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_context_enricher_namespacing.py +0 -0
  136. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_fresh_eyes_signal.py +0 -0
  137. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_install_dry_run.py +0 -0
  138. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_monitor_seeded.py +0 -0
  139. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_detector_loaded.py +0 -0
  140. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_owned_paths.py +0 -0
  141. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_plugin_real_flow.py +0 -0
  142. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  143. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_scaffold_presets.py +0 -0
  144. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_serve_loop.py +0 -0
  145. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_substrate_fingerprint.py +0 -0
  146. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/integration/test_transient_error_backoff.py +0 -0
  147. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/__init__.py +0 -0
  148. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_atomic_write_enforced.py +0 -0
  149. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_classification_ssot.py +0 -0
  150. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_docs_generated.py +0 -0
  151. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_entry_points_resolve.py +0 -0
  152. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kind_registry.py +0 -0
  153. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_event_kinds_ssot.py +0 -0
  154. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_events_doc_contract.py +0 -0
  155. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_layer_2_loop_size.py +0 -0
  156. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_module_boundaries.py +0 -0
  157. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_module_sizes.py +0 -0
  158. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_no_ai_signatures.py +0 -0
  159. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  160. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_peek_schema_version.py +0 -0
  161. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  162. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_round_result_stable.py +0 -0
  163. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  164. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/invariants/test_upstream_schema_canary.py +0 -0
  165. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/__init__.py +0 -0
  166. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/parser.py +0 -0
  167. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/test_parser.py +0 -0
  168. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/literate/test_quickstart.py +0 -0
  169. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/__init__.py +0 -0
  170. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime.py +0 -0
  171. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_agent_runtime_progress.py +0 -0
  172. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_assemble_prompt.py +0 -0
  173. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_events_stream.py +0 -0
  174. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_install.py +0 -0
  175. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_read_round_num.py +0 -0
  176. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_resolve_phase.py +0 -0
  177. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_service.py +0 -0
  178. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_api_types.py +0 -0
  179. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_auto_stop_gating.py +0 -0
  180. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_claude_error_detector.py +0 -0
  181. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli.py +0 -0
  182. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_common.py +0 -0
  183. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_init_install.py +0 -0
  184. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_monitor_http.py +0 -0
  185. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  186. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_cli_upgrade.py +0 -0
  187. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_fresh_eyes.py +0 -0
  188. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_max_rounds.py +0 -0
  189. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_stop_file.py +0 -0
  190. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  191. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_config_transient_error_action.py +0 -0
  192. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_context_store.py +0 -0
  193. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_defenses.py +0 -0
  194. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_detector_helpers.py +0 -0
  195. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_detector_protocol.py +0 -0
  196. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_events.py +0 -0
  197. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_events_cmd.py +0 -0
  198. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  199. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_gemini_plugin.py +0 -0
  200. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_hook_failure_isolation.py +0 -0
  201. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_hooks.py +0 -0
  202. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_http_progress.py +0 -0
  203. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_init_entry_points.py +0 -0
  204. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_lifecycle.py +0 -0
  205. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_metrics.py +0 -0
  206. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_assembly.py +0 -0
  207. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  208. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  209. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  210. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_detectors.py +0 -0
  211. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_monitor_remote.py +0 -0
  212. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_peek_argparse.py +0 -0
  213. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_peek_select.py +0 -0
  214. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_prompt_loader.py +0 -0
  215. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_round_log_helpers.py +0 -0
  216. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_round_view.py +0 -0
  217. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_runner.py +0 -0
  218. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_runner_throttle.py +0 -0
  219. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_scaffold.py +0 -0
  220. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_cmd_bounded.py +0 -0
  221. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_round_log.py +0 -0
  222. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_sentinel.py +0 -0
  223. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_serve_startup_hooks.py +0 -0
  224. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_service_unit.py +0 -0
  225. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_startup_check.py +0 -0
  226. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_substrate.py +0 -0
  227. {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.39}/tests/unit/test_vcs_state.py +0 -0
@@ -7,6 +7,30 @@ 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
+
24
+ ## [0.1.38] - 2026-05-24
25
+
26
+ ### Fixed
27
+ - Grace-kill (`max_grace_after_result_s`) no longer reaps a round that emitted `type=result` while a backgrounded child process (e.g. a long build) is still running. It now reaps only when the agent's process group has no live worker processes left (a genuine hang); otherwise it waits for the round to finish or for the `round_timeout_s` ceiling.
28
+ - Corrected `round_grace_kill`'s description: the kill is gated on the process group being idle (no live workers), not on log silence.
29
+
30
+ ### Added
31
+ - New event `round_grace_extended` — emitted once when grace elapsed after `type=result` but a live worker process kept the round busy; carries the worker cmdlines.
32
+ - `round_grace_kill` now carries `live_children` (cmdlines observed at kill time; empty for a genuine idle hang).
33
+
10
34
  ## [0.1.37] - 2026-05-22
11
35
 
12
36
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.37
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,
@@ -19,6 +19,7 @@ __all__ = [
19
19
  "emit_fresh_eyes_round_triggered",
20
20
  "emit_max_rounds_reached",
21
21
  "emit_rate_limit_stop",
22
+ "emit_round_grace_extended",
22
23
  "emit_round_grace_kill",
23
24
  "emit_round_progress",
24
25
  "emit_round_substrate_after",
@@ -233,16 +234,51 @@ def emit_round_grace_kill(
233
234
  *,
234
235
  round_num: int,
235
236
  grace_s: int,
237
+ live_children: list[str] | None = None,
236
238
  ) -> None:
237
- """Emit when subprocess killed because grace-after-result timer expired.
238
-
239
- Subprocess emitted type=result in JSONL log then sat silent for longer
240
- than max_grace_after_result_s seconds. Distinguishes from round_timeout_kill
241
- (wall-clock exceeded without result event).
239
+ """Emit when the subprocess was killed because the grace-after-result timer
240
+ expired AND the agent's process group had no live worker processes left
241
+ (a genuine hang). Distinct from round_grace_extended (grace elapsed but a
242
+ worker was still running) and round_timeout_kill (wall-clock exceeded).
242
243
  """
243
244
  from agent_runner.events import ROUND_GRACE_KILL, emit
244
245
 
245
- emit(log_dir, ROUND_GRACE_KILL, round_num=round_num, grace_s=grace_s)
246
+ emit(
247
+ log_dir,
248
+ ROUND_GRACE_KILL,
249
+ round_num=round_num,
250
+ grace_s=grace_s,
251
+ live_children=live_children or [],
252
+ )
253
+
254
+
255
+ def emit_round_grace_extended(
256
+ log_dir: Path,
257
+ *,
258
+ round_num: int,
259
+ grace_s: int,
260
+ live_children: list[str],
261
+ ignored_children: list[str] | None = None,
262
+ ) -> None:
263
+ """Emit when the grace-after-result timer expired but the agent still had
264
+ live worker processes (e.g. a backgrounded build), so the round was NOT
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.
271
+ """
272
+ from agent_runner.events import ROUND_GRACE_EXTENDED, emit
273
+
274
+ emit(
275
+ log_dir,
276
+ ROUND_GRACE_EXTENDED,
277
+ round_num=round_num,
278
+ grace_s=grace_s,
279
+ live_children=live_children,
280
+ ignored_children=ignored_children or [],
281
+ )
246
282
 
247
283
 
248
284
  def emit_anomaly_repetitive_tool(
@@ -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.37'
22
- __version_tuple__ = version_tuple = (0, 1, 37)
21
+ __version__ = version = '0.1.39'
22
+ __version_tuple__ = version_tuple = (0, 1, 39)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -11,13 +11,16 @@ 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
17
18
  from collections.abc import Callable
18
- from dataclasses import dataclass
19
+ from dataclasses import dataclass, field
19
20
  from pathlib import Path
20
21
 
22
+ import psutil
23
+
21
24
  REAP_GRACE_S = 5
22
25
 
23
26
 
@@ -28,6 +31,7 @@ class RunResult:
28
31
  timed_out: bool
29
32
  pid: int
30
33
  killed_for_grace: bool = False
34
+ grace_kill_children: list[str] = field(default_factory=list)
31
35
 
32
36
 
33
37
  def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
@@ -54,6 +58,45 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
54
58
  pass
55
59
 
56
60
 
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.
74
+ """
75
+ try:
76
+ parent = psutil.Process(proc.pid)
77
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
78
+ return [], []
79
+ live: list[str] = []
80
+ ignored: list[str] = []
81
+ for child in parent.children(recursive=True):
82
+ try:
83
+ if child.status() == psutil.STATUS_ZOMBIE:
84
+ continue
85
+ line = " ".join(child.cmdline()) or child.name()
86
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
87
+ continue
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:
96
+ break
97
+ return live, ignored
98
+
99
+
57
100
  # Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
58
101
  # A future CLI variant emitting `{"type": "result", ...}` (with space) would
59
102
  # bypass this scan; revisit if that happens.
@@ -71,19 +114,28 @@ def run(
71
114
  max_grace_after_result_s: int = 0,
72
115
  progress_callback: Callable[[dict], None] | None = None,
73
116
  progress_interval_s: int = 0,
117
+ on_grace_extended: Callable[[list[str], list[str]], None] | None = None,
118
+ grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
74
119
  ) -> RunResult:
75
120
  """Spawn the agent subprocess and wait for exit or timeout.
76
121
 
77
122
  Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
78
123
 
79
124
  max_grace_after_result_s: when > 0, start a countdown after the first
80
- type=result event is detected in the log; kill if subprocess is still
81
- running after this many seconds (HUNG defense). 0 = disabled.
125
+ type=result event is detected in the log. After it elapses, reap the
126
+ process group only if the agent has no live worker processes left (a
127
+ genuine hang). If a worker is still running (e.g. a backgrounded build),
128
+ do not reap — invoke ``on_grace_extended`` once and keep waiting until the
129
+ round finishes or hits the wall-clock ``timeout_s`` ceiling. 0 = disabled.
82
130
 
83
131
  progress_callback: when not None and progress_interval_s > 0, called every
84
132
  progress_interval_s seconds with a dict of log stats (log_size_kb,
85
133
  last_write_age_s, wall_age_s). Keeps agent_runtime event-free; callers
86
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.
87
139
  """
88
140
  argv = _build_argv(command, prompt_arg_template, prompt)
89
141
  env = {**os.environ, **env_extra}
@@ -100,6 +152,7 @@ def run(
100
152
  start_new_session=True,
101
153
  )
102
154
  result_seen_at: float | None = None
155
+ grace_extended_emitted = False
103
156
  try:
104
157
  while True:
105
158
  ret = proc.poll()
@@ -114,10 +167,9 @@ def run(
114
167
  return RunResult(
115
168
  exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
116
169
  )
117
- # Grace kill: result emitted but subprocess still running
170
+ # Grace kill: result emitted but subprocess still running.
118
171
  if max_grace_after_result_s > 0:
119
172
  if result_seen_at is None:
120
- # Cheap check: byte-scan log for marker substring
121
173
  try:
122
174
  with log_path.open("rb") as f:
123
175
  if _RESULT_MARKER in f.read():
@@ -125,16 +177,26 @@ def run(
125
177
  except OSError:
126
178
  pass # log not flushed yet; check next tick
127
179
  if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
128
- _kill_pgroup(proc)
129
- duration = time.time() - start
130
- exit_code = proc.returncode if proc.returncode is not None else -1
131
- return RunResult(
132
- exit_code=exit_code,
133
- duration_s=duration,
134
- timed_out=True,
135
- pid=proc.pid,
136
- killed_for_grace=True,
137
- )
180
+ live, ignored = _live_children(proc, ignore_patterns=grace_kill_ignore_patterns)
181
+ if live:
182
+ # Busy: a backgrounded worker is still running. Don't
183
+ # reap — defer to the wall-clock ceiling. Signal once.
184
+ if not grace_extended_emitted:
185
+ if on_grace_extended is not None:
186
+ on_grace_extended(live, ignored)
187
+ grace_extended_emitted = True
188
+ else:
189
+ _kill_pgroup(proc)
190
+ duration = time.time() - start
191
+ exit_code = proc.returncode if proc.returncode is not None else -1
192
+ return RunResult(
193
+ exit_code=exit_code,
194
+ duration_s=duration,
195
+ timed_out=True,
196
+ pid=proc.pid,
197
+ killed_for_grace=True,
198
+ grace_kill_children=[],
199
+ )
138
200
  # Progress heartbeat: call back if interval elapsed
139
201
  if progress_callback is not None and progress_interval_s > 0:
140
202
  if now - last_progress_at >= progress_interval_s:
@@ -733,6 +733,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
733
733
  emit_fresh_eyes_round_triggered,
734
734
  emit_max_rounds_reached,
735
735
  emit_rate_limit_stop,
736
+ emit_round_grace_extended,
736
737
  emit_round_grace_kill,
737
738
  emit_round_progress,
738
739
  emit_round_substrate_after,
@@ -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")
@@ -49,6 +49,7 @@ ORPHAN_STASHED = "orphan_stashed"
49
49
  PACKAGE_UPGRADED = "package_upgraded"
50
50
  PROMPT_OVERWRITTEN = "prompt_overwritten"
51
51
  ROUND_END = "round_end"
52
+ ROUND_GRACE_EXTENDED = "round_grace_extended"
52
53
  ROUND_GRACE_KILL = "round_grace_kill"
53
54
  ROUND_PROGRESS = "round_progress"
54
55
  ROUND_START = "round_start"
@@ -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,6 +467,17 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
466
467
  **stats,
467
468
  )
468
469
 
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:
473
+ api.emit_round_grace_extended(
474
+ log_dir,
475
+ round_num=round_num,
476
+ grace_s=cfg.runtime.max_grace_after_result_s,
477
+ live_children=live,
478
+ ignored_children=ignored,
479
+ )
480
+
469
481
  result = agent_runtime.run(
470
482
  command=cfg.agent.command,
471
483
  prompt_arg_template=cfg.agent.prompt_arg_template,
@@ -476,6 +488,8 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
476
488
  max_grace_after_result_s=cfg.runtime.max_grace_after_result_s,
477
489
  progress_callback=_progress_emit,
478
490
  progress_interval_s=cfg.monitor.round_progress_interval_s,
491
+ on_grace_extended=_grace_extended_emit,
492
+ grace_kill_ignore_patterns=grace_kill_ignore_patterns,
479
493
  )
480
494
  events.emit(
481
495
  log_dir,
@@ -549,6 +563,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
549
563
  log_dir,
550
564
  round_num=round_num,
551
565
  grace_s=cfg.runtime.max_grace_after_result_s,
566
+ live_children=result.grace_kill_children,
552
567
  )
553
568
  elif result.timed_out:
554
569
  events.emit(
@@ -168,6 +168,7 @@ hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"
168
168
  - `package_upgraded`
169
169
  - `prompt_overwritten`
170
170
  - `round_end`
171
+ - `round_grace_extended`
171
172
  - `round_grace_kill`
172
173
  - `round_progress`
173
174
  - `round_start`
@@ -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,53 @@
1
+ # Migrating to 0.1.38
2
+
3
+ ## TL;DR
4
+
5
+ ```bash
6
+ pip install --upgrade cli-agent-runner==0.1.38
7
+ ```
8
+
9
+ No action or config change. If you use `max_grace_after_result_s`, grace-kill
10
+ now distinguishes a hung agent from a still-busy one.
11
+
12
+ ## What changed
13
+
14
+ Previously, grace-kill reaped the whole process group `max_grace_after_result_s`
15
+ seconds after the agent emitted `type=result`, with no awareness of child
16
+ processes. A round that backgrounded a long build and emitted `type=result`
17
+ ("waiting for build…") could have the still-running build reaped.
18
+
19
+ Now, at grace expiry, agent-runner checks for live worker processes in the
20
+ agent's process group:
21
+
22
+ - **No live workers** → genuine hang → reaped (`round_grace_kill`, as before).
23
+ - **A live worker** (e.g. a build) → not reaped; agent-runner emits
24
+ `round_grace_extended` once and waits until the round finishes or hits the
25
+ `round_timeout_s` wall-clock ceiling.
26
+
27
+ The check re-runs each poll tick, so once a backgrounded worker exits, a still-
28
+ stuck agent is reaped promptly rather than waiting out `round_timeout_s`.
29
+
30
+ ## Three distinct outcomes
31
+
32
+ - `round_grace_extended` — grace elapsed but a worker is still running (busy).
33
+ - `round_grace_kill` — grace elapsed and the process group is idle (hang).
34
+ - `round_timeout_kill` — wall-clock `round_timeout_s` exceeded (hard ceiling).
35
+
36
+ ## Recommended agent contract
37
+
38
+ Treat the safety net as a net, not a crutch: emit `type=result` only when the
39
+ turn is truly done. Run long builds/tests foreground and commit before ending
40
+ the turn, rather than backgrounding work past `type=result`.
41
+
42
+ ## Known limitation
43
+
44
+ The "live worker" check treats *any* live non-zombie descendant as busy. If
45
+ your agent keeps persistent helper subprocesses alive past `type=result` (e.g.
46
+ MCP servers), grace-kill will defer hangs to the `round_timeout_s` ceiling. This
47
+ never false-kills; the `round_grace_extended` events make it visible if it
48
+ happens.
49
+
50
+ ## What did NOT change
51
+
52
+ - `max_grace_after_result_s` config, default, and `0 = disabled`.
53
+ - `round_timeout_s` wall-clock kill (still the hard ceiling and backstop).
@@ -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.