cli-agent-runner 0.1.36__tar.gz → 0.1.38__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 (224) hide show
  1. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/CHANGELOG.md +23 -0
  2. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/PKG-INFO +1 -1
  3. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_emit.py +35 -6
  4. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_version.py +2 -2
  5. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/agent_runtime.py +57 -15
  6. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/api.py +1 -0
  7. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/upgrade_cmd.py +147 -20
  8. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/events.py +2 -0
  9. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/runner.py +10 -0
  10. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/architecture.md +2 -0
  11. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/commands.md +19 -1
  12. cli_agent_runner-0.1.38/docs/migrations/0.1.37.md +65 -0
  13. cli_agent_runner-0.1.38/docs/migrations/0.1.38.md +53 -0
  14. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/runbook.md +50 -23
  15. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_grace_kill_emission.py +33 -1
  16. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_catalogs.py +14 -0
  17. cli_agent_runner-0.1.38/tests/unit/test_agent_runtime_grace.py +159 -0
  18. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_upgrade.py +235 -0
  19. cli_agent_runner-0.1.36/tests/unit/test_agent_runtime_grace.py +0 -72
  20. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.codecov.yml +0 -0
  21. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  23. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/workflows/ci.yml +0 -0
  26. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.github/workflows/release.yml +0 -0
  27. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.gitignore +0 -0
  28. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/.vulture-whitelist.py +0 -0
  29. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/CODE_OF_CONDUCT.md +0 -0
  30. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/CONTRIBUTING.md +0 -0
  31. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/LICENSE +0 -0
  32. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/README.md +0 -0
  33. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/README.zh.md +0 -0
  34. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/SECURITY.md +0 -0
  35. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/__init__.py +0 -0
  36. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_docgen.py +0 -0
  37. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_registry.py +0 -0
  38. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_substrate.py +0 -0
  39. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/_throttle.py +0 -0
  40. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/api_types.py +0 -0
  41. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/__init__.py +0 -0
  42. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/_constants.py +0 -0
  43. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  44. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/gemini.py +0 -0
  45. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/__init__.py +0 -0
  46. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/__main__.py +0 -0
  47. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/common.py +0 -0
  48. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/events_cmd.py +0 -0
  49. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/init_cmd.py +0 -0
  50. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/install_cmd.py +0 -0
  51. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/monitor_cmd.py +0 -0
  52. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/peek_cmd.py +0 -0
  53. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/round_cmd.py +0 -0
  54. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/serve_cmd.py +0 -0
  55. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/cli/service_cmd.py +0 -0
  56. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/config.py +0 -0
  57. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/context_store.py +0 -0
  58. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/defenses.py +0 -0
  59. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/detector_helpers.py +0 -0
  60. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/hooks.py +0 -0
  61. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/http_progress.py +0 -0
  62. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/lifecycle.py +0 -0
  63. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/metrics.py +0 -0
  64. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/monitor.py +0 -0
  65. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/__init__.py +0 -0
  66. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/aider.toml +0 -0
  67. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/claude.toml +0 -0
  68. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/presets/gemini.toml +0 -0
  69. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/prompt_loader.py +0 -0
  70. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/round_log.py +0 -0
  71. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/round_view.py +0 -0
  72. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/scaffold.py +0 -0
  73. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/service_unit.py +0 -0
  74. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/startup_check.py +0 -0
  75. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/agent_runner/vcs_state.py +0 -0
  76. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/build.sh +0 -0
  77. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/example-agent-runner.toml +0 -0
  78. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/launchd.plist.tmpl +0 -0
  79. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/run-loop.sh +0 -0
  80. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/deploy/systemd.service.tmpl +0 -0
  81. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/README.md +0 -0
  82. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/configuration.md +0 -0
  83. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/events.md +0 -0
  84. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/long-running-agents.md +0 -0
  85. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/marketing/README.md +0 -0
  86. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/marketing/promo-cn.html +0 -0
  87. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.16.md +0 -0
  88. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.17.md +0 -0
  89. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.19.md +0 -0
  90. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.20.md +0 -0
  91. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.21.md +0 -0
  92. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.22.md +0 -0
  93. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.23.md +0 -0
  94. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.24.md +0 -0
  95. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.25.md +0 -0
  96. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.26.md +0 -0
  97. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.27.md +0 -0
  98. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.28.md +0 -0
  99. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.29.md +0 -0
  100. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.30.md +0 -0
  101. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.31.md +0 -0
  102. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.32.md +0 -0
  103. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.33.md +0 -0
  104. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.34.md +0 -0
  105. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.35.md +0 -0
  106. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/migrations/0.1.36.md +0 -0
  107. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/plugins.md +0 -0
  108. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/quickstart.md +0 -0
  109. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/recipes/aider.md +0 -0
  110. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/docs/thesis.md +0 -0
  111. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/pyproject.toml +0 -0
  112. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/__init__.py +0 -0
  113. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/_test_helpers.py +0 -0
  114. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/conftest.py +0 -0
  115. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/contract/__init__.py +0 -0
  116. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/contract/test_public_api_surface.py +0 -0
  117. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/__init__.py +0 -0
  118. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/conftest.py +0 -0
  119. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  120. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_install_systemd.py +0 -0
  121. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  122. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  123. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  124. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  125. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  126. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/__init__.py +0 -0
  127. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_bounded_run.py +0 -0
  128. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_context_enricher_namespacing.py +0 -0
  129. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_fresh_eyes_signal.py +0 -0
  130. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_install_dry_run.py +0 -0
  131. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_monitor_seeded.py +0 -0
  132. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_detector_loaded.py +0 -0
  133. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_owned_paths.py +0 -0
  134. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_real_flow.py +0 -0
  135. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  136. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_scaffold_presets.py +0 -0
  137. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_serve_loop.py +0 -0
  138. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_substrate_fingerprint.py +0 -0
  139. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/integration/test_transient_error_backoff.py +0 -0
  140. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/__init__.py +0 -0
  141. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_architecture.py +0 -0
  142. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_atomic_write_enforced.py +0 -0
  143. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_classification_ssot.py +0 -0
  144. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_docs_generated.py +0 -0
  145. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_entry_points_resolve.py +0 -0
  146. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_event_kind_registry.py +0 -0
  147. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_event_kinds_ssot.py +0 -0
  148. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_events_doc_contract.py +0 -0
  149. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_layer_2_loop_size.py +0 -0
  150. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_module_boundaries.py +0 -0
  151. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_module_sizes.py +0 -0
  152. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_no_ai_signatures.py +0 -0
  153. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  154. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_peek_schema_version.py +0 -0
  155. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  156. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_round_result_stable.py +0 -0
  157. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  158. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/invariants/test_upstream_schema_canary.py +0 -0
  159. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/__init__.py +0 -0
  160. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/parser.py +0 -0
  161. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/test_parser.py +0 -0
  162. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/literate/test_quickstart.py +0 -0
  163. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/__init__.py +0 -0
  164. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_agent_runtime.py +0 -0
  165. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_agent_runtime_progress.py +0 -0
  166. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_assemble_prompt.py +0 -0
  167. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_events_stream.py +0 -0
  168. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_install.py +0 -0
  169. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_observation.py +0 -0
  170. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_read_round_num.py +0 -0
  171. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_resolve_phase.py +0 -0
  172. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_service.py +0 -0
  173. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_api_types.py +0 -0
  174. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_auto_stop_gating.py +0 -0
  175. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_claude_error_detector.py +0 -0
  176. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli.py +0 -0
  177. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_common.py +0 -0
  178. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_init_install.py +0 -0
  179. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_monitor_http.py +0 -0
  180. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  181. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config.py +0 -0
  182. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_fresh_eyes.py +0 -0
  183. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_max_rounds.py +0 -0
  184. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_stop_file.py +0 -0
  185. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  186. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_config_transient_error_action.py +0 -0
  187. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_context_store.py +0 -0
  188. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_defenses.py +0 -0
  189. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_detector_helpers.py +0 -0
  190. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_detector_protocol.py +0 -0
  191. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_docgen.py +0 -0
  192. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_events.py +0 -0
  193. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_events_cmd.py +0 -0
  194. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  195. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_gemini_plugin.py +0 -0
  196. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_hook_failure_isolation.py +0 -0
  197. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_hooks.py +0 -0
  198. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_http_progress.py +0 -0
  199. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_init_entry_points.py +0 -0
  200. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_lifecycle.py +0 -0
  201. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_metrics.py +0 -0
  202. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_assembly.py +0 -0
  203. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  204. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  205. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  206. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detectors.py +0 -0
  207. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_remote.py +0 -0
  208. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_peek_argparse.py +0 -0
  209. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_peek_select.py +0 -0
  210. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_presets.py +0 -0
  211. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_prompt_loader.py +0 -0
  212. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_round_log_helpers.py +0 -0
  213. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_round_view.py +0 -0
  214. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_runner.py +0 -0
  215. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_runner_throttle.py +0 -0
  216. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_scaffold.py +0 -0
  217. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_cmd_bounded.py +0 -0
  218. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_round_log.py +0 -0
  219. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_sentinel.py +0 -0
  220. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_serve_startup_hooks.py +0 -0
  221. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_service_unit.py +0 -0
  222. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_startup_check.py +0 -0
  223. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_substrate.py +0 -0
  224. {cli_agent_runner-0.1.36 → cli_agent_runner-0.1.38}/tests/unit/test_vcs_state.py +0 -0
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.38] - 2026-05-24
11
+
12
+ ### Fixed
13
+ - 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.
14
+ - Corrected `round_grace_kill`'s description: the kill is gated on the process group being idle (no live workers), not on log silence.
15
+
16
+ ### Added
17
+ - 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.
18
+ - `round_grace_kill` now carries `live_children` (cmdlines observed at kill time; empty for a genuine idle hang).
19
+
20
+ ## [0.1.37] - 2026-05-22
21
+
22
+ ### Fixed
23
+ - `upgrade` no longer crashes when run from a directory without `agent-runner.toml` — it upgrades the package and falls back to package-only mode.
24
+ - `upgrade` handles PEP 668 externally-managed environments (Debian 12 etc.): retries pip with `--break-system-packages` (and `--user` for user-site installs) when not in a venv.
25
+
26
+ ### Changed
27
+ - `upgrade` only stop/start-orchestrates the `systemd --user` service it installed. For a self-managed service (e.g. a systemd system unit) it does package-only upgrade + smoke and prints the restart command to run yourself — no more silent no-op, and no more `agent-runner start` suggestion (which could spawn a conflicting second supervisor).
28
+ - New `--no-restart` flag forces package-only upgrade.
29
+
30
+ ### Added
31
+ - New event `package_upgraded` (on-disk package changed; restart deferred to the operator), distinct from `service_upgraded` (the live service is now on the new version).
32
+
10
33
  ## [0.1.36] - 2026-05-21
11
34
 
12
35
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.36
3
+ Version: 0.1.38
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
@@ -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,44 @@ 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
+ ) -> None:
262
+ """Emit when the grace-after-result timer expired but the agent still had
263
+ live worker processes (e.g. a backgrounded build), so the round was NOT
264
+ killed; it continues until it finishes or hits round_timeout_s.
265
+ """
266
+ from agent_runner.events import ROUND_GRACE_EXTENDED, emit
267
+
268
+ emit(
269
+ log_dir,
270
+ ROUND_GRACE_EXTENDED,
271
+ round_num=round_num,
272
+ grace_s=grace_s,
273
+ live_children=live_children,
274
+ )
246
275
 
247
276
 
248
277
  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.36'
22
- __version_tuple__ = version_tuple = (0, 1, 36)
21
+ __version__ = version = '0.1.38'
22
+ __version_tuple__ = version_tuple = (0, 1, 38)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -15,9 +15,11 @@ import signal
15
15
  import subprocess # noqa: TID251 — sanctioned subprocess caller
16
16
  import time
17
17
  from collections.abc import Callable
18
- from dataclasses import dataclass
18
+ from dataclasses import dataclass, field
19
19
  from pathlib import Path
20
20
 
21
+ import psutil
22
+
21
23
  REAP_GRACE_S = 5
22
24
 
23
25
 
@@ -28,6 +30,7 @@ class RunResult:
28
30
  timed_out: bool
29
31
  pid: int
30
32
  killed_for_grace: bool = False
33
+ grace_kill_children: list[str] = field(default_factory=list)
31
34
 
32
35
 
33
36
  def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
@@ -54,6 +57,31 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
54
57
  pass
55
58
 
56
59
 
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.
66
+ """
67
+ try:
68
+ parent = psutil.Process(proc.pid)
69
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
70
+ return []
71
+ out: list[str] = []
72
+ for child in parent.children(recursive=True):
73
+ try:
74
+ if child.status() == psutil.STATUS_ZOMBIE:
75
+ continue
76
+ line = " ".join(child.cmdline()) or child.name()
77
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
78
+ continue
79
+ out.append(line[:max_len])
80
+ if len(out) >= max_n:
81
+ break
82
+ return out
83
+
84
+
57
85
  # Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
58
86
  # A future CLI variant emitting `{"type": "result", ...}` (with space) would
59
87
  # bypass this scan; revisit if that happens.
@@ -71,14 +99,18 @@ def run(
71
99
  max_grace_after_result_s: int = 0,
72
100
  progress_callback: Callable[[dict], None] | None = None,
73
101
  progress_interval_s: int = 0,
102
+ on_grace_extended: Callable[[list[str]], None] | None = None,
74
103
  ) -> RunResult:
75
104
  """Spawn the agent subprocess and wait for exit or timeout.
76
105
 
77
106
  Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
78
107
 
79
108
  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.
109
+ type=result event is detected in the log. After it elapses, reap the
110
+ process group only if the agent has no live worker processes left (a
111
+ genuine hang). If a worker is still running (e.g. a backgrounded build),
112
+ do not reap — invoke ``on_grace_extended`` once and keep waiting until the
113
+ round finishes or hits the wall-clock ``timeout_s`` ceiling. 0 = disabled.
82
114
 
83
115
  progress_callback: when not None and progress_interval_s > 0, called every
84
116
  progress_interval_s seconds with a dict of log stats (log_size_kb,
@@ -100,6 +132,7 @@ def run(
100
132
  start_new_session=True,
101
133
  )
102
134
  result_seen_at: float | None = None
135
+ grace_extended_emitted = False
103
136
  try:
104
137
  while True:
105
138
  ret = proc.poll()
@@ -114,10 +147,9 @@ def run(
114
147
  return RunResult(
115
148
  exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
116
149
  )
117
- # Grace kill: result emitted but subprocess still running
150
+ # Grace kill: result emitted but subprocess still running.
118
151
  if max_grace_after_result_s > 0:
119
152
  if result_seen_at is None:
120
- # Cheap check: byte-scan log for marker substring
121
153
  try:
122
154
  with log_path.open("rb") as f:
123
155
  if _RESULT_MARKER in f.read():
@@ -125,16 +157,26 @@ def run(
125
157
  except OSError:
126
158
  pass # log not flushed yet; check next tick
127
159
  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
- )
160
+ children = _live_children(proc)
161
+ if children:
162
+ # Busy: a backgrounded worker is still running. Don't
163
+ # reap — defer to the wall-clock ceiling. Signal once.
164
+ if not grace_extended_emitted:
165
+ if on_grace_extended is not None:
166
+ on_grace_extended(children)
167
+ grace_extended_emitted = True
168
+ else:
169
+ _kill_pgroup(proc)
170
+ duration = time.time() - start
171
+ exit_code = proc.returncode if proc.returncode is not None else -1
172
+ return RunResult(
173
+ exit_code=exit_code,
174
+ duration_s=duration,
175
+ timed_out=True,
176
+ pid=proc.pid,
177
+ killed_for_grace=True,
178
+ grace_kill_children=[],
179
+ )
138
180
  # Progress heartbeat: call back if interval elapsed
139
181
  if progress_callback is not None and progress_interval_s > 0:
140
182
  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,
@@ -18,7 +18,9 @@ import sys
18
18
  import time
19
19
  from pathlib import Path
20
20
 
21
+ import agent_runner
21
22
  from agent_runner import __version__, api, events
23
+ from agent_runner.api_types import ServiceMode
22
24
  from agent_runner.cli.common import cfg_from_args, fail, info
23
25
  from agent_runner.config import Config
24
26
 
@@ -28,8 +30,8 @@ def add_parser(sub, parent) -> None:
28
30
  "upgrade",
29
31
  parents=[parent],
30
32
  help=(
31
- "Round-boundary upgrade: stop pip install → smoke → start"
32
- " (auto-rollback on smoke fail)"
33
+ "Package upgrade with service-mode gate: orchestrated stop/start"
34
+ " for systemd --user; package-only otherwise"
33
35
  ),
34
36
  )
35
37
  p.add_argument(
@@ -40,24 +42,69 @@ def add_parser(sub, parent) -> None:
40
42
  help="Pin a specific version (e.g. 0.1.13). Default: latest from PyPI. "
41
43
  "Use to roll back: `--target <previous-version>`.",
42
44
  )
45
+ p.add_argument(
46
+ "--no-restart",
47
+ action="store_true",
48
+ help="Upgrade the package + smoke only; do not stop/start the service "
49
+ "(you restart it yourself).",
50
+ )
43
51
  p.set_defaults(func=cmd)
44
52
 
45
53
 
46
54
  def cmd(args) -> int:
47
- cfg = cfg_from_args(args)
48
- return _run_upgrade(cfg, target=args.target, cfg_path=args.config)
55
+ cfg = _try_load_cfg(args)
56
+ return _run_upgrade(
57
+ cfg,
58
+ target=args.target,
59
+ cfg_path=args.config,
60
+ no_restart=getattr(args, "no_restart", False),
61
+ )
49
62
 
50
63
 
51
- def _pip_install(spec: str, *, force_reinstall: bool = False) -> subprocess.CompletedProcess:
52
- """Invoke pip install with the given spec. Returns CompletedProcess (rc check by caller).
64
+ def _try_load_cfg(args) -> Config | None:
65
+ """Load the project config if present; None when absent (package-only)."""
66
+ try:
67
+ return cfg_from_args(args)
68
+ except FileNotFoundError:
69
+ return None
70
+
71
+
72
+ def _pip_env_flags() -> list[str]:
73
+ """Extra pip flags for the current install under PEP 668.
53
74
 
54
- Uses ``sys.executable -m pip`` to match the smoke functions and guarantee
55
- we install into the same interpreter we will smoke-test against.
75
+ Inside a venv: none (pip is unrestricted). Otherwise (system/user
76
+ interpreter on an externally-managed distro) the caller retries with these.
77
+ ``--user`` is added only when agent_runner lives in user-site, matching
78
+ where the existing install actually is.
79
+ """
80
+ import sys
81
+
82
+ if sys.prefix != sys.base_prefix: # inside a venv → no PEP 668
83
+ return []
84
+ import site
85
+
86
+ flags = ["--break-system-packages"]
87
+ user_site = site.getusersitepackages()
88
+ if str(Path(agent_runner.__file__)).startswith(str(Path(user_site))):
89
+ flags.insert(0, "--user")
90
+ return flags
91
+
92
+
93
+ def _pip_install(spec: str, *, force_reinstall: bool = False) -> subprocess.CompletedProcess:
94
+ """pip install --upgrade <spec>, retrying once with PEP668 flags on an
95
+ externally-managed environment. Returns CompletedProcess (rc check by caller).
56
96
  """
57
- cmd = [sys.executable, "-m", "pip", "install", "--upgrade", spec]
97
+ base = [sys.executable, "-m", "pip", "install", "--upgrade", spec]
58
98
  if force_reinstall:
59
- cmd.insert(4, "--force-reinstall")
60
- return subprocess.run(cmd, capture_output=True, text=True, check=False)
99
+ base.insert(4, "--force-reinstall")
100
+ r = subprocess.run(base, capture_output=True, text=True, check=False)
101
+ if r.returncode == 0 or "externally-managed-environment" not in (r.stderr or ""):
102
+ return r
103
+ extra = _pip_env_flags()
104
+ if not extra:
105
+ return r
106
+ info(f"externally-managed env detected; retrying pip with {' '.join(extra)}")
107
+ return subprocess.run(base + extra, capture_output=True, text=True, check=False)
61
108
 
62
109
 
63
110
  def _smoke_version() -> tuple[int, str]:
@@ -93,18 +140,40 @@ def _smoke_peek(cfg_path: Path) -> tuple[int, str]:
93
140
  return 0, ""
94
141
 
95
142
 
96
- def _run_upgrade(cfg: Config, *, target: str | None, cfg_path: Path) -> int:
97
- """Orchestrate the full upgrade flow.
98
-
99
- Returns exit code (0 success, 1 user-recoverable, 2 critical).
100
- """
143
+ def _run_upgrade(
144
+ cfg: Config | None,
145
+ *,
146
+ target: str | None,
147
+ cfg_path: Path,
148
+ no_restart: bool = False,
149
+ ) -> int:
150
+ """Dispatch: full orchestration for the systemd --user service we installed;
151
+ package-only everywhere else."""
101
152
  if target is not None and not target.strip():
102
153
  return fail("--target must be a non-empty version string (e.g. 0.1.13)")
154
+ from_version = __version__
155
+ if _orchestrate_capable(cfg, no_restart):
156
+ return _orchestrated_upgrade(
157
+ cfg, target=target, cfg_path=cfg_path, from_version=from_version
158
+ )
159
+ return _package_only_upgrade(cfg, target=target, from_version=from_version)
160
+
161
+
162
+ def _orchestrate_capable(cfg: Config | None, no_restart: bool) -> bool:
163
+ if cfg is None or no_restart:
164
+ return False
165
+ pname = api._resolve_project(cfg.runtime.work_dir)
166
+ return api.detect_service_mode(pname, log_dir=cfg.runtime.log_dir) == ServiceMode.SYSTEMD_USER
103
167
 
168
+
169
+ def _orchestrated_upgrade(
170
+ cfg: Config, *, target: str | None, cfg_path: Path, from_version: str
171
+ ) -> int:
172
+ """Full stop → pip → smoke(--version + peek) → start → emit service_upgraded,
173
+ with auto-rollback on smoke failure. Only reached for the systemd --user
174
+ service agent-runner installed (api.start works there)."""
104
175
  log_dir = cfg.runtime.log_dir
105
176
  log_dir.mkdir(parents=True, exist_ok=True)
106
-
107
- from_version = __version__
108
177
  t0 = time.monotonic()
109
178
 
110
179
  info("stopping service...")
@@ -155,14 +224,13 @@ def _run_upgrade(cfg: Config, *, target: str | None, cfg_path: Path) -> int:
155
224
  started_at=t0,
156
225
  cfg_path=cfg_path,
157
226
  )
158
-
159
227
  info(f"smoke OK (now at {to_version})")
160
228
 
161
229
  info("starting service...")
162
230
  t_start = time.monotonic()
163
231
  try:
164
232
  api.start(cfg.runtime.work_dir)
165
- except Exception as e: # noqa: BLE001 — new version installed but service stopped; no safe auto-rollback
233
+ except Exception as e: # noqa: BLE001 — new version installed but service stopped
166
234
  return _rollback_failed(
167
235
  log_dir,
168
236
  to_version,
@@ -183,6 +251,65 @@ def _run_upgrade(cfg: Config, *, target: str | None, cfg_path: Path) -> int:
183
251
  return 0
184
252
 
185
253
 
254
+ def _package_only_upgrade(cfg: Config | None, *, target: str | None, from_version: str) -> int:
255
+ """Upgrade the on-disk package + smoke (--version), with pip-level rollback.
256
+ Never touches the service — the operator restarts it. Used for any deployment
257
+ not managed as a systemd --user service (system unit, foreground, none, no
258
+ config, or --no-restart)."""
259
+ spec = "cli-agent-runner" if target is None else f"cli-agent-runner=={target}"
260
+ info(f"package-only upgrade (service not managed by agent-runner); installing {spec}...")
261
+ pip_result = _pip_install(spec)
262
+ if pip_result.returncode != 0:
263
+ return fail(
264
+ f"pip install failed (rc={pip_result.returncode}): "
265
+ f"{pip_result.stderr.strip()[:200]}; "
266
+ f"package unchanged, your service keeps running the current version"
267
+ )
268
+
269
+ rc_v, version_or_err = _smoke_version()
270
+ if rc_v != 0:
271
+ attempted = target or "latest"
272
+ info(f"smoke failed at {attempted} ({version_or_err}); reinstalling {from_version}...")
273
+ rb = _pip_install(f"cli-agent-runner=={from_version}", force_reinstall=True)
274
+ if rb.returncode != 0:
275
+ return fail(
276
+ f"package smoke failed AND rollback reinstall failed (rc={rb.returncode}): "
277
+ f"{rb.stderr.strip()[:200]}; run: "
278
+ f"pip install --force-reinstall cli-agent-runner=={from_version}"
279
+ )
280
+ return fail(
281
+ f"package smoke failed at {attempted}; reinstalled {from_version}; service untouched"
282
+ )
283
+ to_version = version_or_err
284
+
285
+ if cfg is not None:
286
+ log_dir = cfg.runtime.log_dir
287
+ log_dir.mkdir(parents=True, exist_ok=True)
288
+ events.emit(
289
+ log_dir,
290
+ events.PACKAGE_UPGRADED,
291
+ from_version=from_version,
292
+ to_version=to_version,
293
+ restart_deferred=True,
294
+ )
295
+ info(f"package upgraded {from_version} → {to_version}. Restart your supervisor to load it:")
296
+ info(_restart_hint(cfg))
297
+ return 0
298
+
299
+
300
+ def _restart_hint(cfg: Config | None) -> str:
301
+ """Mode-correct restart command. Never suggests `agent-runner start`
302
+ (which would spawn a conflicting supervisor on a system-unit host)."""
303
+ if cfg is not None:
304
+ pname = api._resolve_project(cfg.runtime.work_dir)
305
+ if api.detect_service_mode(pname, log_dir=cfg.runtime.log_dir) == ServiceMode.SYSTEMD_USER:
306
+ return f" systemctl --user restart {api.serve_unit_filename(pname)}"
307
+ return (
308
+ " sudo systemctl restart <your-unit> # if run by a systemd system unit\n"
309
+ " (agent-runner can't know a service it didn't install; substitute your unit name)"
310
+ )
311
+
312
+
186
313
  def _rollback(
187
314
  cfg: Config,
188
315
  log_dir: Path,
@@ -46,8 +46,10 @@ MONITOR_STARTED = "monitor_started"
46
46
  ORPHAN_IDEMPOTENT_SKIP = "orphan_idempotent_skip"
47
47
  ORPHAN_STASH_FAILED = "orphan_stash_failed"
48
48
  ORPHAN_STASHED = "orphan_stashed"
49
+ PACKAGE_UPGRADED = "package_upgraded"
49
50
  PROMPT_OVERWRITTEN = "prompt_overwritten"
50
51
  ROUND_END = "round_end"
52
+ ROUND_GRACE_EXTENDED = "round_grace_extended"
51
53
  ROUND_GRACE_KILL = "round_grace_kill"
52
54
  ROUND_PROGRESS = "round_progress"
53
55
  ROUND_START = "round_start"
@@ -466,6 +466,14 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
466
466
  **stats,
467
467
  )
468
468
 
469
+ def _grace_extended_emit(children: list[str]) -> None:
470
+ api.emit_round_grace_extended(
471
+ log_dir,
472
+ round_num=round_num,
473
+ grace_s=cfg.runtime.max_grace_after_result_s,
474
+ live_children=children,
475
+ )
476
+
469
477
  result = agent_runtime.run(
470
478
  command=cfg.agent.command,
471
479
  prompt_arg_template=cfg.agent.prompt_arg_template,
@@ -476,6 +484,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
476
484
  max_grace_after_result_s=cfg.runtime.max_grace_after_result_s,
477
485
  progress_callback=_progress_emit,
478
486
  progress_interval_s=cfg.monitor.round_progress_interval_s,
487
+ on_grace_extended=_grace_extended_emit,
479
488
  )
480
489
  events.emit(
481
490
  log_dir,
@@ -549,6 +558,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
549
558
  log_dir,
550
559
  round_num=round_num,
551
560
  grace_s=cfg.runtime.max_grace_after_result_s,
561
+ live_children=result.grace_kill_children,
552
562
  )
553
563
  elif result.timed_out:
554
564
  events.emit(
@@ -165,8 +165,10 @@ hook (vs ALL pre-round hooks), use `[plugins] disable = ["that_entry_point_name"
165
165
  - `orphan_idempotent_skip`
166
166
  - `orphan_stash_failed`
167
167
  - `orphan_stashed`
168
+ - `package_upgraded`
168
169
  - `prompt_overwritten`
169
170
  - `round_end`
171
+ - `round_grace_extended`
170
172
  - `round_grace_kill`
171
173
  - `round_progress`
172
174
  - `round_start`
@@ -24,7 +24,7 @@ are shared between `peek`, `watch`, and `monitor`.
24
24
  | `monitor` | Anomaly detection, narrate/events stream, or HTTP progress page |
25
25
  | `serve` | Long-running supervisor loop |
26
26
  | `round` | Run one round and exit |
27
- | `upgrade` | Round-boundary upgrade: stop pip install smoke start (auto-rollback on smoke fail) |
27
+ | `upgrade` | Package upgrade with service-mode gate: orchestrated stop/start for systemd --user; package-only otherwise |
28
28
  <!-- /gen:verb-table -->
29
29
 
30
30
  ## Lifecycle
@@ -76,6 +76,24 @@ Long-running supervisor loop. Traps SIGTERM (graceful stop), SIGINT (graceful),
76
76
  SIGUSR1 (cancel — forwards SIGINT to current round). Writes `serve.pid` and
77
77
  `round.pid`. `--once` runs a single round then exits (debug).
78
78
 
79
+ ### `agent-runner upgrade [--target VERSION] [--no-restart] [--config PATH]`
80
+
81
+ Upgrade the agent-runner package. Behavior depends on the detected service mode:
82
+
83
+ - **systemd --user service** (installed via `agent-runner install`): full
84
+ orchestrated flow — stop → pip install → smoke (`--version` + `peek`) →
85
+ start → emit `service_upgraded`. Auto-rollback on smoke failure.
86
+ - **Anything else** (system unit, foreground, no config): package-only —
87
+ PEP 668-aware pip + `--version` smoke + pip-level rollback, emits
88
+ `package_upgraded`, prints the restart command. Never touches your running
89
+ service, never runs `sudo`.
90
+
91
+ `--config` is optional: when omitted (or the file is absent), `upgrade` falls
92
+ back to package-only mode automatically.
93
+
94
+ `--no-restart` forces package-only even on a systemd --user host (upgrade the
95
+ package now, restart your service yourself).
96
+
79
97
  ## Observation
80
98
 
81
99
  ### `agent-runner peek [flags]`
@@ -0,0 +1,65 @@
1
+ # Migrating to 0.1.37
2
+
3
+ ## TL;DR
4
+
5
+ ```bash
6
+ pip install --upgrade cli-agent-runner==0.1.37
7
+ ```
8
+
9
+ If you run agent-runner as a `systemd --user` service (the kind `agent-runner
10
+ install` creates), `agent-runner upgrade` works end-to-end as before. If you run
11
+ it any other way (a systemd **system** unit, a foreground process, etc.), see
12
+ "Upgrading a self-managed service" below.
13
+
14
+ ## What was fixed
15
+
16
+ - `upgrade` no longer crashes when run from a directory without
17
+ `agent-runner.toml`.
18
+ - `upgrade` handles Debian 12 / PEP 668 `externally-managed-environment`:
19
+ outside a venv it retries pip with `--break-system-packages` (plus `--user`
20
+ when the install lives in user-site).
21
+ - `upgrade` no longer silently no-ops on a non-user-managed service, and never
22
+ suggests `agent-runner start` (which would start a second, conflicting
23
+ supervisor next to your real one).
24
+
25
+ ## How `upgrade` behaves now
26
+
27
+ - **systemd --user service** → full flow: stop → pip → smoke → start →
28
+ `service_upgraded`, with auto-rollback on smoke failure. Unchanged.
29
+ - **Anything else** (system unit / foreground / no config / `--no-restart`) →
30
+ **package-only**: PEP668-aware pip + `--version` smoke + pip-level rollback,
31
+ then it emits `package_upgraded` and prints the restart command for you to run.
32
+ It does not touch your running service.
33
+
34
+ ## Upgrading a self-managed service (e.g. systemd system unit)
35
+
36
+ agent-runner never runs `sudo` and does not manage a unit it did not create.
37
+ The canonical recipe:
38
+
39
+ ```bash
40
+ # 1. Upgrade the package (PEP 668: --break-system-packages only touches ~/.local)
41
+ python3 -m pip install --user --break-system-packages --upgrade cli-agent-runner==0.1.37
42
+ # (or: agent-runner upgrade --target 0.1.37 --no-restart — does the pip + smoke for you)
43
+
44
+ # 2. Verify
45
+ agent-runner --version # → 0.1.37
46
+ python3 -c "import your_plugin" # if you use a plugin
47
+
48
+ # 3. Restart your supervisor so the long-running process loads the new code
49
+ sudo systemctl restart <your-unit>
50
+ ```
51
+
52
+ Do **not** run `agent-runner start` on a system-unit host — it would spawn a
53
+ second supervisor alongside the one systemd manages.
54
+
55
+ ## `package_upgraded` vs `service_upgraded`
56
+
57
+ - `service_upgraded` — the live service is now running the new version (emitted
58
+ only in the orchestrated systemd --user flow).
59
+ - `package_upgraded` — the on-disk package changed but the running supervisor
60
+ still runs the old code until you restart it (`restart_deferred: true`).
61
+
62
+ ## What did NOT change
63
+
64
+ - The systemd --user happy path (the 0.1.13 flow) and its auto-rollback events.
65
+ - Smoke design (fresh-subprocess `--version`, plus `peek` in the orchestrated flow).