cli-agent-runner 0.2.17__tar.gz → 0.2.18__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 (356) hide show
  1. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.github/workflows/ci.yml +8 -1
  2. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.vulture-whitelist.py +5 -1
  3. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/CHANGELOG.md +22 -0
  4. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/CONTRIBUTING.md +4 -1
  5. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/PKG-INFO +3 -1
  6. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/README.zh.md +1 -1
  7. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/__init__.py +20 -5
  8. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_emit.py +110 -2
  9. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_monitor_detectors.py +10 -4
  10. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_monitor_state.py +35 -15
  11. cli_agent_runner-0.2.18/agent_runner/_plugin_scan.py +109 -0
  12. cli_agent_runner-0.2.18/agent_runner/_round_outcome.py +255 -0
  13. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_serve_policy.py +12 -10
  14. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_substrate.py +2 -1
  15. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_throttle.py +54 -188
  16. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_version.py +2 -2
  17. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/api.py +52 -72
  18. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/_serve_round.py +276 -14
  19. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/events_cmd.py +28 -6
  20. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/serve_cmd.py +36 -13
  21. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/clock.py +2 -1
  22. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/defenses.py +2 -1
  23. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/events.py +49 -1
  24. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/http_progress.py +7 -5
  25. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/metrics.py +174 -16
  26. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/monitor.py +27 -9
  27. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/runner.py +7 -2
  28. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/schedule.py +2 -1
  29. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/service_unit.py +10 -7
  30. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/build.sh +21 -4
  31. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/architecture.md +7 -1
  32. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/configuration.md +3 -2
  33. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/migrations/0.2.md +28 -0
  34. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/runbook.md +51 -1
  35. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/pyproject.toml +12 -2
  36. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/_test_helpers.py +64 -0
  37. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/contract/test_public_api_surface.py +2 -0
  38. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_auto_stop_e2e.py +58 -16
  39. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_bounded_run.py +18 -3
  40. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_grace_kill_emission.py +20 -6
  41. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_run_one_round_with_fake_agent.py +31 -7
  42. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_serve_loop.py +10 -2
  43. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_spawn_round_mem_floor.py +115 -26
  44. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/_event_scan.py +5 -1
  45. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_architecture.py +4 -5
  46. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_docs_generated.py +2 -0
  47. cli_agent_runner-0.2.18/tests/invariants/test_import_footprint.py +209 -0
  48. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_no_internal_refs.py +13 -2
  49. cli_agent_runner-0.2.18/tests/invariants/test_no_price_logic.py +51 -0
  50. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_no_vacuous_invariants.py +3 -0
  51. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_peek_schema_version.py +5 -2
  52. cli_agent_runner-0.2.18/tests/invariants/test_round_alloc_growth.py +127 -0
  53. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_timeout_budget_invariant.py +15 -4
  54. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_agent_runtime.py +29 -10
  55. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_agent_runtime_grace.py +143 -51
  56. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_agent_runtime_marker_scan.py +5 -3
  57. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_agent_runtime_progress.py +2 -2
  58. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_agent_runtime_reap.py +9 -1
  59. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_observation.py +2 -2
  60. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_service.py +51 -0
  61. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_auto_stop_gating.py +13 -2
  62. cli_agent_runner-0.2.18/tests/unit/test_cgroup_probe.py +451 -0
  63. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_cli_service_peek_monitor.py +7 -8
  64. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_events.py +38 -1
  65. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_events_cmd.py +9 -1
  66. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_events_jsonl_decode.py +1 -1
  67. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_git_timeout.py +6 -2
  68. cli_agent_runner-0.2.18/tests/unit/test_http_progress_epoch.py +20 -0
  69. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_init_entry_points.py +33 -33
  70. cli_agent_runner-0.2.18/tests/unit/test_metrics_cgroup_usage.py +54 -0
  71. cli_agent_runner-0.2.18/tests/unit/test_metrics_swap.py +21 -0
  72. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_detect_rate_limit.py +42 -0
  73. cli_agent_runner-0.2.18/tests/unit/test_monitor_loop.py +110 -0
  74. cli_agent_runner-0.2.18/tests/unit/test_monitor_on_alert.py +104 -0
  75. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_remote.py +35 -1
  76. cli_agent_runner-0.2.18/tests/unit/test_plugin_scan_parity.py +156 -0
  77. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_round_outcome.py +11 -5
  78. cli_agent_runner-0.2.18/tests/unit/test_round_outcome_agent_axis.py +116 -0
  79. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_runner.py +1 -1
  80. cli_agent_runner-0.2.18/tests/unit/test_serve_round_cgroup_spine.py +187 -0
  81. cli_agent_runner-0.2.18/tests/unit/test_serve_round_oom_killed.py +198 -0
  82. cli_agent_runner-0.2.18/tests/unit/test_serve_round_terminate.py +32 -0
  83. cli_agent_runner-0.2.18/tests/unit/test_throttle_epoch_bound.py +77 -0
  84. cli_agent_runner-0.2.17/tests/unit/test_cgroup_probe.py +0 -233
  85. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.codecov.yml +0 -0
  86. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  87. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  88. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  89. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  90. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.github/workflows/release.yml +0 -0
  91. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/.gitignore +0 -0
  92. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/BACKLOG.md +0 -0
  93. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/CODE_OF_CONDUCT.md +0 -0
  94. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/LICENSE +0 -0
  95. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/README.md +0 -0
  96. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/SECURITY.md +0 -0
  97. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_docgen.py +0 -0
  98. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_monitor_registry.py +0 -0
  99. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_redact.py +0 -0
  100. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_registry.py +0 -0
  101. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_resolve.py +0 -0
  102. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/_round_support.py +0 -0
  103. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/agent_runtime.py +0 -0
  104. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/api_types.py +0 -0
  105. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/__init__.py +0 -0
  106. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/_constants.py +0 -0
  107. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  108. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/codewhale.py +0 -0
  109. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  110. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/gemini.py +0 -0
  111. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/kimi.py +0 -0
  112. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/builtin_plugins/pi.py +0 -0
  113. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/__init__.py +0 -0
  114. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/__main__.py +0 -0
  115. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/common.py +0 -0
  116. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/init_cmd.py +0 -0
  117. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/install_cmd.py +0 -0
  118. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/migrate_cmd.py +0 -0
  119. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/monitor_cmd.py +0 -0
  120. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/peek_cmd.py +0 -0
  121. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/round_cmd.py +0 -0
  122. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/service_cmd.py +0 -0
  123. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/cli/upgrade_cmd.py +0 -0
  124. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/config/__init__.py +0 -0
  125. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/config/errors.py +0 -0
  126. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/config/loader.py +0 -0
  127. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/config/models.py +0 -0
  128. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/config/parsers.py +0 -0
  129. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/config/validators.py +0 -0
  130. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/context_store.py +0 -0
  131. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/hooks.py +0 -0
  132. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/host_health.py +0 -0
  133. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/lifecycle.py +0 -0
  134. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/migrations.py +0 -0
  135. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/phase_select.py +0 -0
  136. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/presets/__init__.py +0 -0
  137. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/presets/aider.toml +0 -0
  138. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/presets/claude.toml +0 -0
  139. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/presets/codewhale.toml +0 -0
  140. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/presets/gemini.toml +0 -0
  141. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/presets/kimi.toml +0 -0
  142. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/presets/pi.toml +0 -0
  143. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/prompt_loader.py +0 -0
  144. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/remote_relay.py +0 -0
  145. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/round_log.py +0 -0
  146. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/round_view.py +0 -0
  147. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/scaffold.py +0 -0
  148. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/startup_check.py +0 -0
  149. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/agent_runner/vcs_state.py +0 -0
  150. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/README.md +0 -0
  151. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/commands.md +0 -0
  152. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/events.md +0 -0
  153. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/long-running-agents.md +0 -0
  154. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/plugins.md +0 -0
  155. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/quickstart.md +0 -0
  156. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/recipes/aider.md +0 -0
  157. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/recipes/codewhale.md +0 -0
  158. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/recipes/kimi.md +0 -0
  159. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/recipes/pi.md +0 -0
  160. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/docs/thesis.md +0 -0
  161. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/__init__.py +0 -0
  162. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/_clock.py +0 -0
  163. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/conftest.py +0 -0
  164. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/contract/__init__.py +0 -0
  165. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/e2e/__init__.py +0 -0
  166. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/e2e/conftest.py +0 -0
  167. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  168. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/e2e/test_e2e_install_systemd.py +0 -0
  169. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  170. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  171. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  172. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  173. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  174. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/generate_vulture_whitelist.py +0 -0
  175. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/__init__.py +0 -0
  176. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_context_enricher_namespacing.py +0 -0
  177. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_dirty_handler_seam.py +0 -0
  178. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_fresh_eyes_signal.py +0 -0
  179. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_install_dry_run.py +0 -0
  180. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_monitor_seeded.py +0 -0
  181. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_plugin_detector_loaded.py +0 -0
  182. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_plugin_owned_paths.py +0 -0
  183. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_plugin_real_flow.py +0 -0
  184. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  185. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_scaffold_presets.py +0 -0
  186. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_spawn_round_wedged.py +0 -0
  187. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_substrate_fingerprint.py +0 -0
  188. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/integration/test_transient_error_backoff.py +0 -0
  189. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/__init__.py +0 -0
  190. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/_docs.py +0 -0
  191. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_atomic_write_enforced.py +0 -0
  192. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_authored_facts_declared.py +0 -0
  193. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  194. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_catalogs.py +0 -0
  195. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_classification_ssot.py +0 -0
  196. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_config_error_consistency.py +0 -0
  197. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_config_value_set_ssot.py +0 -0
  198. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  199. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  200. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  201. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_doc_cli_claims.py +0 -0
  202. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_doc_operator_surface.py +0 -0
  203. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  204. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  205. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  206. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_docs_index_complete.py +0 -0
  207. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_entry_points_resolve.py +0 -0
  208. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_event_int_coercion.py +0 -0
  209. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_event_kind_registry.py +0 -0
  210. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_event_kinds_ssot.py +0 -0
  211. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_events_doc_contract.py +0 -0
  212. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  213. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_hook_contract_docs.py +0 -0
  214. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_layer_2_loop_size.py +0 -0
  215. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  216. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_module_boundaries.py +0 -0
  217. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_module_sizes.py +0 -0
  218. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  219. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_no_ai_signatures.py +0 -0
  220. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  221. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_no_raw_time.py +0 -0
  222. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_phase_select_stateless.py +0 -0
  223. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_plugins_config_stable.py +0 -0
  224. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  225. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  226. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  227. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_round_num_single_source.py +0 -0
  228. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_round_result_stable.py +0 -0
  229. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  230. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  231. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_upstream_schema_canary.py +0 -0
  232. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  233. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/literate/__init__.py +0 -0
  234. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/literate/parser.py +0 -0
  235. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/literate/test_parser.py +0 -0
  236. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/literate/test_quickstart.py +0 -0
  237. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/__init__.py +0 -0
  238. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  239. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_assemble_prompt.py +0 -0
  240. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_events_stream.py +0 -0
  241. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_install.py +0 -0
  242. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_read_round_num.py +0 -0
  243. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_resolve_phase.py +0 -0
  244. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_api_types.py +0 -0
  245. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_catalogs.py +0 -0
  246. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_claude_error_detector.py +0 -0
  247. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_claude_foreign_schema.py +0 -0
  248. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_cli.py +0 -0
  249. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_cli_common.py +0 -0
  250. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_cli_init_install.py +0 -0
  251. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_cli_main_boundary.py +0 -0
  252. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_cli_monitor_http.py +0 -0
  253. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_cli_upgrade.py +0 -0
  254. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_codewhale_code_precedence.py +0 -0
  255. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_codewhale_plugin.py +0 -0
  256. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_coerce_int_poison.py +0 -0
  257. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config.py +0 -0
  258. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_fresh_eyes.py +0 -0
  259. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_max_rounds.py +0 -0
  260. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_nested_strictness.py +0 -0
  261. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_prompt_delivery.py +0 -0
  262. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_schedule.py +0 -0
  263. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_stop_file.py +0 -0
  264. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_strictness.py +0 -0
  265. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  266. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_config_transient_error_action.py +0 -0
  267. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_context_store.py +0 -0
  268. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_control_char_config.py +0 -0
  269. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_default_dirty_handler.py +0 -0
  270. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_defenses.py +0 -0
  271. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_detect_hung_latch.py +0 -0
  272. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_detector_isolation.py +0 -0
  273. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_detector_protocol.py +0 -0
  274. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_dirty_handlers.py +0 -0
  275. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_docgen.py +0 -0
  276. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_effective_throttle_view.py +0 -0
  277. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_emit_config_migrated.py +0 -0
  278. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_emit_schedule.py +0 -0
  279. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  280. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_events_cmd_rollover.py +0 -0
  281. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_events_cmd_tail.py +0 -0
  282. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_example_dirty_handler.py +0 -0
  283. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_example_plugin.py +0 -0
  284. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_exit_cause.py +0 -0
  285. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_exit_classifier.py +0 -0
  286. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  287. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_gemini_plugin.py +0 -0
  288. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_hook_failure_isolation.py +0 -0
  289. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_hook_redact_order.py +0 -0
  290. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_hooks.py +0 -0
  291. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_host_health.py +0 -0
  292. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_http_progress.py +0 -0
  293. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  294. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_kimi_plugin.py +0 -0
  295. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_lifecycle.py +0 -0
  296. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_metrics.py +0 -0
  297. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_migrate_cmd.py +0 -0
  298. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_migrations.py +0 -0
  299. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_assembly.py +0 -0
  300. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_cmd_severity.py +0 -0
  301. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_dedup_identity.py +0 -0
  302. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  303. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  304. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  305. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_detectors.py +0 -0
  306. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_event_tail.py +0 -0
  307. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  308. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_monitor_schedule_pause.py +0 -0
  309. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_network_blip_signal_guard.py +0 -0
  310. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_open_round_log.py +0 -0
  311. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  312. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_outer_round_ceiling.py +0 -0
  313. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_peek_argparse.py +0 -0
  314. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_peek_project_isolation.py +0 -0
  315. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_peek_schedule.py +0 -0
  316. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_peek_select.py +0 -0
  317. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_phase_agent_launch.py +0 -0
  318. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_phase_profiles.py +0 -0
  319. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_phase_select.py +0 -0
  320. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_pi_plugin.py +0 -0
  321. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_plugin_constants.py +0 -0
  322. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_presets.py +0 -0
  323. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_primary_prompt_file_home.py +0 -0
  324. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_prompt_loader.py +0 -0
  325. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_read_json_hardening.py +0 -0
  326. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_redact.py +0 -0
  327. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_resolve.py +0 -0
  328. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_round_cmd.py +0 -0
  329. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_round_cmd_sigterm.py +0 -0
  330. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_round_log_helpers.py +0 -0
  331. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_round_view.py +0 -0
  332. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  333. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_runner_throttle.py +0 -0
  334. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_scaffold.py +0 -0
  335. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_schedule.py +0 -0
  336. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_cmd_bounded.py +0 -0
  337. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_config_broken.py +0 -0
  338. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_crash_loop.py +0 -0
  339. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_mem_gate.py +0 -0
  340. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_phase_schedule.py +0 -0
  341. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_policy.py +0 -0
  342. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_round.py +0 -0
  343. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_round_log.py +0 -0
  344. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_schedule_gate.py +0 -0
  345. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_sentinel.py +0 -0
  346. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_serve_startup_hooks.py +0 -0
  347. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_service_mode.py +0 -0
  348. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_service_unit.py +0 -0
  349. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_startup_battery_exit.py +0 -0
  350. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_startup_check.py +0 -0
  351. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_substrate.py +0 -0
  352. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  353. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  354. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_throttle_plugin_classification.py +0 -0
  355. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_upgrade_migrate.py +0 -0
  356. {cli_agent_runner-0.2.17 → cli_agent_runner-0.2.18}/tests/unit/test_vcs_state.py +0 -0
@@ -92,7 +92,14 @@ jobs:
92
92
  # `| tail` would swallow the exit code and let dead code through.
93
93
  run: vulture
94
94
  - name: tests with coverage
95
- run: pytest -q --ignore=tests/e2e --ignore=tests/literate --cov --cov-report=xml --cov-report=term
95
+ # Two passes so `serial` tests (install process-wide signal handlers /
96
+ # self-signals -- unsafe to share an xdist worker process) still run
97
+ # every time: parallel set + serial set == the full suite, mirroring
98
+ # ./build.sh test. pytest-cov 7.x auto-combines worker coverage data.
99
+ run: |
100
+ pytest -q --ignore=tests/e2e --ignore=tests/literate \
101
+ -n auto --dist worksteal -m "not serial" --cov --cov-report=xml --cov-report=term
102
+ pytest -q --ignore=tests/e2e --ignore=tests/literate -m serial --cov --cov-append --cov-report=xml --cov-report=term
96
103
  - name: docs CI gate
97
104
  run: |
98
105
  python -m agent_runner._docgen
@@ -13,12 +13,16 @@ log_dir
13
13
  offsets
14
14
  buffer
15
15
 
16
- # agent_runner._throttle.RoundOutcome
16
+ # agent_runner._round_outcome.RoundOutcome
17
17
  mem_terminated
18
18
  usage_capable
19
19
  newest_usage_ts
20
20
  newest_substrate_before_ts
21
21
  latest_transient_per_agent
22
+ ran_agent
23
+ usage_capable_by_agent
24
+ newest_usage_ts_by_agent
25
+ backoff_exponent_by_agent
22
26
 
23
27
  # agent_runner.agent_runtime.RunResult
24
28
  exit_code
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.18] - 2026-09-08
9
+
10
+ ### Added
11
+ - Cgroup pressure spine: `round_cgroup_memory` emits once per round (when this process's cgroup has a finite `memory.max`) with peak `memory.current`/`memory.swap.current` and `memory.events` counter deltas over the round — pressure becomes visible before it trips a give-up verdict.
12
+ - `round_oom_killed`: a pointer-only event (never round-log content) when the kernel cgroup-OOM-kills a round; the round still exits 137 and counts toward the crash streak exactly as before — this only names the cause.
13
+ - `host_cgroup_memory_limit` gains four advisory fields (`swap_total_bytes`, `swap_cap_pct`, `memory_high`, `advisory`) computed once at serve startup, plus a swap-plausibility guard on the cgroup-defer decision.
14
+ - Docs: a real-RAM ledger, a zram worked example, and a commented `# MemoryHigh` hint (with the PSI-floor interaction warning).
15
+
16
+ ### Fixed
17
+ - Per-agent no-progress verdict: a usageless round on a never-emits-usage CLI is no longer misread as stalled in a mixed `[phases]` config.
18
+ - Fail-open hardening: the monitor no longer crashes on the stop/classify path (an emit error, a poisoned epoch field, or an unreapable round now degrade instead of raising).
19
+ - `kill` now escalates SIGTERM to SIGKILL under systemd-user after the grace window, matching the docs.
20
+
21
+ ### Changed
22
+ - Plugin discovery scans `entry_points.txt` directly with a metadata parity fallback, ahead of 0.3's group growth.
23
+ - Internal cleanup: `RoundOutcome` carved into `_round_outcome`, constants single-sourced, lazy `hashlib`/`zoneinfo` behind an import-footprint gate, a tenet-3 price-blind invariant, plus test-speed work (parallel gate + faster inner loop).
24
+
25
+ ### Notes
26
+ - Not breaking: no `agent-runner migrate` step, no systemd unit change; `peek --json` gains the two new event kinds above, existing kinds unchanged.
27
+
28
+ See `docs/migrations/0.2.md`.
29
+
8
30
  ## [0.2.17] - 2026-09-05
9
31
 
10
32
  ### Changed
@@ -105,7 +105,10 @@ drifts.
105
105
 
106
106
  ## Tests
107
107
 
108
- - Unit + integration: `pytest -q --ignore=tests/e2e --ignore=tests/literate`
108
+ - Unit + integration (parallel gate): `./build.sh test`pytest-xdist
109
+ (`-n $AR_TEST_JOBS`, default `auto`) plus a serial pass for tests that
110
+ install process-wide signal handlers. Never set `AR_TEST_JOBS` above 2 on a
111
+ constrained host (e.g. a 462MB Pi).
109
112
  - Literate quickstart: `./build.sh literate`
110
113
  - Pi e2e (opt-in): `AGENT_RUNNER_E2E_PI=1 pytest tests/e2e/` (needs a `pi`
111
114
  ssh alias; not run in CI).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.17
3
+ Version: 0.2.18
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
@@ -27,6 +27,8 @@ Requires-Python: >=3.11
27
27
  Requires-Dist: psutil>=5.9
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest-cov>=5; extra == 'dev'
30
+ Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
31
+ Requires-Dist: pytest-xdist>=3.8; extra == 'dev'
30
32
  Requires-Dist: pytest>=8; extra == 'dev'
31
33
  Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
32
34
  Requires-Dist: vulture>=2.13; extra == 'dev'
@@ -10,7 +10,7 @@ supervisor 重启 —— 这是核心模式。中间穿插一组防御,避开
10
10
 
11
11
  三层,每层都能独立运行,上层都是可选的:`round` 跑一次 agent 跑完即退;
12
12
  `serve` 捕获信号循环拉起 round;`monitor` 检测器 + 告警,critical 时自动停服
13
- (本机或 ssh 远程)。
13
+ (本机)。
14
14
 
15
15
  ## 安装
16
16
 
@@ -27,16 +27,31 @@ def _load_plugins_from_group(group: str) -> None:
27
27
 
28
28
  Called at package import. A broken plugin must not crash the supervisor;
29
29
  each failure surfaces as a ``UserWarning``.
30
+
31
+ Discovery goes through the ``entry_points.txt`` scanner (cheaper than a
32
+ fresh ``importlib.metadata.entry_points()`` scan per group per process;
33
+ see ``_plugin_scan``), with a hard fallback to ``importlib.metadata`` on
34
+ any parse failure. Loading a plugin means only importing its module and
35
+ resolving its attribute — same as ``EntryPoint.load()`` — since every
36
+ built-in plugin registers itself as a module-top side effect (documented
37
+ in docs/plugins.md); this loader never instantiates or calls the target.
30
38
  """
39
+ import importlib
40
+ import sys
31
41
  import warnings
32
- from importlib.metadata import entry_points
33
42
 
34
- for ep in entry_points(group=group):
43
+ from agent_runner._plugin_scan import scan_entry_points
44
+
45
+ for name, value in scan_entry_points(sys.path, group):
35
46
  try:
36
- ep.load()
47
+ module_path, _, attr_path = value.partition(":")
48
+ mod = importlib.import_module(module_path)
49
+ target = mod
50
+ for attr in filter(None, attr_path.split(".")):
51
+ target = getattr(target, attr)
37
52
  except Exception as e:
38
53
  warnings.warn(
39
- f"failed to load {group} plugin {ep.name!r}: {e}",
54
+ f"failed to load {group} plugin {name!r}: {e}",
40
55
  stacklevel=3,
41
56
  )
42
57
 
@@ -80,7 +95,7 @@ def apply_plugin_disable(names: list[str]) -> None:
80
95
  registered plugin (typo catcher; tolerates cross-env config drift).
81
96
 
82
97
  Plugin packages still load at import time — this removes from the registries
83
- that the runner and peek consult. Side effects from ep.load() (module-level
98
+ that the runner and peek consult. Side effects from loading (module-level
84
99
  imports, etc.) have already happened by the time this runs.
85
100
 
86
101
  Known limitation: vcs_state._PLUGIN_OWNED_PATHS lacks per-plugin name
@@ -25,12 +25,14 @@ __all__ = [
25
25
  "emit_mem_loop_persistent",
26
26
  "emit_mem_pressure_deferred_to_cgroup",
27
27
  "emit_rate_limit_stop",
28
+ "emit_round_cgroup_memory",
28
29
  "emit_round_deferred",
29
30
  "emit_round_grace_extended",
30
31
  "emit_round_grace_kill",
31
32
  "emit_round_logs_prune_deferred",
32
33
  "emit_round_mem_critical_sample",
33
34
  "emit_round_mem_terminated",
35
+ "emit_round_oom_killed",
34
36
  "emit_round_progress",
35
37
  "emit_round_resumed",
36
38
  "emit_round_substrate_after",
@@ -330,13 +332,34 @@ def emit_round_mem_critical_sample(
330
332
 
331
333
 
332
334
  def emit_host_cgroup_memory_limit(
333
- log_dir: Path, *, memory_max: int | None, memory_swap_max: int | None, cgroup_path: str | None
335
+ log_dir: Path,
336
+ *,
337
+ memory_max: int | None,
338
+ memory_swap_max: int | None,
339
+ cgroup_path: str | None,
340
+ swap_total_bytes: int | None = None,
341
+ swap_cap_pct: float | None = None,
342
+ memory_high: int | None = None,
343
+ advisory: str | None = None,
334
344
  ) -> None:
335
345
  """Emit once at serve startup: this process's cgroup v2 memory budget
336
346
  (``metrics.cgroup_memory_limits``). ``None`` fields mean unlimited (or
337
347
  cgroup v2 unavailable). Serve uses this once to decide whether the
338
348
  mid-round hard floor can defer to kernel cgroup-OOM -- see
339
- ``emit_mem_pressure_deferred_to_cgroup`` below."""
349
+ ``emit_mem_pressure_deferred_to_cgroup`` below.
350
+
351
+ 0.2.18 adds an optional startup swap-cap advisory
352
+ (``swap_total_bytes`` / ``swap_cap_pct`` / ``memory_high`` / ``advisory``)
353
+ as FIELDS on this SAME event -- never a separate event kind, and never an
354
+ auto-change to the operator's cgroup or unit. ``swap_total_bytes`` is the
355
+ host's total swap (``metrics.swap_total_bytes``); ``swap_cap_pct`` is
356
+ ``memory_swap_max`` as a percentage of it (``None`` when either side is
357
+ unknown); ``memory_high`` is the bounding ancestor's ``memory.high`` in
358
+ bytes (``metrics.cgroup_memory_high``) -- ``None`` means unset (the
359
+ cgroup read the literal ``"max"``, or no finite value at all), never the
360
+ raw ``"max"`` token, so a caller can't mistake "unset" for a real
361
+ ceiling; ``advisory`` is the human-readable warning text when the swap
362
+ cap looks implausibly tight, else ``None``."""
340
363
  from agent_runner.events import HOST_CGROUP_MEMORY_LIMIT, emit
341
364
 
342
365
  emit(
@@ -345,6 +368,91 @@ def emit_host_cgroup_memory_limit(
345
368
  memory_max=memory_max,
346
369
  memory_swap_max=memory_swap_max,
347
370
  cgroup_path=cgroup_path,
371
+ swap_total_bytes=swap_total_bytes,
372
+ swap_cap_pct=swap_cap_pct,
373
+ memory_high=memory_high,
374
+ advisory=advisory,
375
+ )
376
+
377
+
378
+ def emit_round_cgroup_memory(
379
+ log_dir: Path,
380
+ *,
381
+ round_num: int,
382
+ memory_current_peak: int,
383
+ memory_swap_current_peak: int,
384
+ events_high_delta: int,
385
+ events_max_delta: int,
386
+ events_oom_delta: int,
387
+ events_oom_kill_delta: int,
388
+ bounding_cgroup_path: str | None,
389
+ ) -> None:
390
+ """Emit once per round: the round's peak cgroup ``memory.current`` (+swap) and
391
+ the ``memory.events`` DELTAS (high/max/oom/oom_kill) over the round, read at the
392
+ bounding ancestor. Deltas, never absolutes -- an operator sees pressure BUILDING
393
+ without SSH. Peak is the max over the existing ~10s mid-round tick (NOT
394
+ ``memory.peak``, which is cumulative since cgroup creation). No-op when this
395
+ host has no finite cgroup bound (``_serve_round._emit_round_cgroup_memory``
396
+ never calls this in that case).
397
+
398
+ ``bounding_cgroup_path`` is deliberately NOT named ``cgroup_path`` --
399
+ ``host_cgroup_memory_limit`` already uses that name for the LEAF (this
400
+ process's own) cgroup; here it names the BOUNDING ANCESTOR (the one whose
401
+ ``memory.max`` actually binds). Same key, two meanings across the two
402
+ events an operator correlates would be a trap -- events.md pins field
403
+ semantics permanently once shipped, so the distinct name is chosen now."""
404
+ from agent_runner.events import ROUND_CGROUP_MEMORY, emit
405
+
406
+ emit(
407
+ log_dir,
408
+ ROUND_CGROUP_MEMORY,
409
+ round_num=round_num,
410
+ memory_current_peak=memory_current_peak,
411
+ memory_swap_current_peak=memory_swap_current_peak,
412
+ events_high_delta=events_high_delta,
413
+ events_max_delta=events_max_delta,
414
+ events_oom_delta=events_oom_delta,
415
+ events_oom_kill_delta=events_oom_kill_delta,
416
+ bounding_cgroup_path=bounding_cgroup_path,
417
+ )
418
+
419
+
420
+ def emit_round_oom_killed(
421
+ log_dir: Path,
422
+ *,
423
+ round_num: int,
424
+ log_path: Path,
425
+ log_bytes: int,
426
+ oom_kill_delta: int,
427
+ partial_log: bool,
428
+ ) -> None:
429
+ """Emit when the kernel cgroup-OOM-killed a round (``memory.events.oom_kill``
430
+ rose over the round, per ``_serve_round._maybe_emit_oom_killed`` -- folded
431
+ from the SAME ``cgroup_memory_usage()`` read ``round_cgroup_memory`` already
432
+ took, no second sysfs read). Symmetric with ``round_mem_terminated``
433
+ (supervisor-killed) -- this is the kernel-killed sibling.
434
+
435
+ POINTER-ONLY: ``log_path`` + ``log_bytes`` + ``oom_kill_delta`` -- NEVER the
436
+ round-log/transcript content itself (durability, secret-redaction, size all
437
+ argue against embedding it in events.jsonl). ``partial_log`` reports whether
438
+ the supervisor's own truncation trailer (see ``_mark_partial_log``) was
439
+ written onto the round log, so a later reader can tell the log's tail is
440
+ supervisor residue, not the agent's own output.
441
+
442
+ Classification is UNCHANGED by this event: the round still exits 137 and
443
+ counts toward the crash streak exactly as before -- this is pure
444
+ observability layered on top, computed and emitted after that decision is
445
+ already made."""
446
+ from agent_runner.events import ROUND_OOM_KILLED, emit
447
+
448
+ emit(
449
+ log_dir,
450
+ ROUND_OOM_KILLED,
451
+ round_num=round_num,
452
+ log_path=str(log_path),
453
+ log_bytes=log_bytes,
454
+ oom_kill_delta=oom_kill_delta,
455
+ partial_log=partial_log,
348
456
  )
349
457
 
350
458
 
@@ -14,7 +14,7 @@ the plugin registry (``_monitor_registry``), and the cycle-edge wiring
14
14
  from __future__ import annotations
15
15
 
16
16
  import re
17
- from datetime import UTC, datetime, timedelta
17
+ from datetime import datetime, timedelta
18
18
  from pathlib import Path
19
19
  from typing import Any
20
20
 
@@ -407,15 +407,21 @@ def detect_rate_limit_active(
407
407
  if kind == TRANSIENT_ERROR_RECOVERED:
408
408
  return None
409
409
  if kind == TRANSIENT_ERROR_DETECTED:
410
- from agent_runner._throttle import _coerce_int, _events_derived_reset
410
+ from agent_runner._throttle import (
411
+ _coerce_epoch_int,
412
+ _events_derived_reset,
413
+ _safe_epoch_iso,
414
+ )
411
415
 
412
416
  agent = str(ev.get("agent", "unknown"))
413
417
  classification = ev.get("classification", "unknown")
414
- reset = _coerce_int(ev.get("reset_at_epoch"), 0)
418
+ reset = _coerce_epoch_int(ev.get("reset_at_epoch"), 0, now_epoch=now)
415
419
  if log_dir is not None:
416
420
  reset = _events_derived_reset(log_dir, agent, str(classification), reset)
417
421
  if reset > now:
418
- iso = datetime.fromtimestamp(reset, UTC).isoformat()
422
+ iso = _safe_epoch_iso(reset)
423
+ if iso is None:
424
+ return None # poisoned epoch — don't blind-crash the detector
419
425
  return _alert(
420
426
  "rate_limit_active",
421
427
  "warning",
@@ -9,15 +9,16 @@ cycle-edge wiring (``run_all_detectors``/``on_alert``) in ``monitor.py``.
9
9
  from __future__ import annotations
10
10
 
11
11
  from collections import deque
12
- from collections.abc import Iterable
12
+ from collections.abc import Iterable, Iterator
13
13
  from dataclasses import dataclass, field
14
14
  from pathlib import Path
15
15
  from typing import Any, Protocol
16
16
 
17
17
  from agent_runner.api_types import ProjectState, ServiceMode, ServiceStatus, SystemMetrics
18
18
  from agent_runner.builtin_plugins._constants import _TAIL_LINES
19
+ from agent_runner.clock import SYSTEM_CLOCK
19
20
  from agent_runner.context_store import read_json
20
- from agent_runner.events import _iter_parsed_lines, iter_event_dicts, open_events_jsonl
21
+ from agent_runner.events import iter_event_dicts, read_new
21
22
 
22
23
 
23
24
  class StateSource(Protocol):
@@ -82,22 +83,41 @@ class _EventTail:
82
83
  )
83
84
 
84
85
  def read(self, files: list[Path]) -> list[dict[str, Any]]:
85
- for path in files:
86
- pos = self.offsets.get(path, 0)
86
+ new_events, self.offsets = read_new(files, self.offsets)
87
+ self.buffer.extend(new_events)
88
+ return list(self.buffer)
89
+
90
+
91
+ def _tail_events_jsonl(
92
+ log_dir: Path,
93
+ *,
94
+ start_at_now: bool,
95
+ poll_interval_s: float,
96
+ ) -> Iterator[dict[str, Any]]:
97
+ """Polling tailer: yields parsed event dicts from events-*.jsonl files.
98
+
99
+ ``start_at_now``: if True, snapshot current file sizes at init so existing
100
+ events are skipped (machine-consumption use case). If False, yield from
101
+ byte 0 of every file present at start (human-narrate use case).
102
+
103
+ Follows file rotation transparently — when a new events-YYYY-MM.jsonl
104
+ appears, it is picked up from byte 0.
105
+ """
106
+ offsets: dict[Path, int] = {}
107
+ if start_at_now:
108
+ for path in sorted(log_dir.glob("events-*.jsonl")):
87
109
  try:
88
- size = path.stat().st_size
110
+ offsets[path] = path.stat().st_size
89
111
  except FileNotFoundError:
90
112
  continue
91
- if size < pos:
92
- pos = 0 # rotated/truncated underneath us
93
- if size == pos:
94
- continue
95
- with open_events_jsonl(path) as f:
96
- f.seek(pos)
97
- for _, parsed in _iter_parsed_lines(f):
98
- self.buffer.append(parsed)
99
- self.offsets[path] = f.tell()
100
- return list(self.buffer)
113
+
114
+ while True:
115
+ files = sorted(log_dir.glob("events-*.jsonl"))
116
+ new_events, offsets = read_new(files, offsets)
117
+ if new_events:
118
+ yield from new_events
119
+ else:
120
+ SYSTEM_CLOCK.sleep(poll_interval_s)
101
121
 
102
122
 
103
123
  _MAX_TAIL_FILES = 20
@@ -0,0 +1,109 @@
1
+ """Plugin discovery scanner — replaces per-group importlib.metadata.entry_points
2
+ scans at startup with one cheap entry_points.txt parse. Hard-falls-back to
3
+ importlib.metadata on any parse failure; AGENT_RUNNER_PLUGIN_DISCOVERY=metadata
4
+ forces the fallback. Parity with importlib.metadata is pinned by
5
+ tests/unit/test_plugin_scan_parity.py per group."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import configparser
10
+ import os
11
+ import warnings
12
+ from pathlib import Path
13
+
14
+
15
+ def _metadata_entry_points(group: str) -> list[tuple[str, str]]:
16
+ """The old path: importlib.metadata's own distribution scan.
17
+
18
+ Imported lazily — importlib.metadata pulls in email.* (METADATA parsing)
19
+ at import time, which is most of the footprint this scanner exists to
20
+ avoid. Importing it at module top would tax every process even when the
21
+ fast path never falls back.
22
+ """
23
+ from importlib.metadata import entry_points
24
+
25
+ return [(ep.name, ep.value) for ep in entry_points(group=group)]
26
+
27
+
28
+ # Parsed entry_points.txt contents, memoized per dist-info file for the life
29
+ # of the process: {ep_file_path: {group: [(name, value), ...]}}. Package
30
+ # import calls _parse_entry_points_files once per plugin group (7x today,
31
+ # _HOOK_GROUPS + event_kinds + detectors) across the SAME sys.path, so every
32
+ # call after the first re-walks the identical dist-info directories --
33
+ # without this cache each one would re-open and re-parse (configparser) every
34
+ # entry_points.txt found, 7x over, just to pull out a different [group]
35
+ # section each time. Keyed by the file's path only (not mtime) -- a dist-info
36
+ # is not expected to change under a running process.
37
+ _PARSED_DIST_INFO_CACHE: dict[Path, dict[str, list[tuple[str, str]]]] = {}
38
+
39
+
40
+ def _parsed_entry_points_by_group(ep_file: Path) -> dict[str, list[tuple[str, str]]]:
41
+ """One ``entry_points.txt``, parsed once and bucketed by ``[group]``
42
+ section: ``{group: [(name, value), ...]}``. Memoized in
43
+ :data:`_PARSED_DIST_INFO_CACHE` so a second call for the same file (a
44
+ different plugin group asking about the same dist-info) reuses the
45
+ parse instead of re-opening + re-running configparser over it."""
46
+ cached = _PARSED_DIST_INFO_CACHE.get(ep_file)
47
+ if cached is not None:
48
+ return cached
49
+ parser = configparser.ConfigParser(interpolation=None)
50
+ parser.optionxform = str # preserve case -- entry-point names are case-sensitive
51
+ parser.read(ep_file, encoding="utf-8")
52
+ parsed = {section: list(parser[section].items()) for section in parser.sections()}
53
+ _PARSED_DIST_INFO_CACHE[ep_file] = parsed
54
+ return parsed
55
+
56
+
57
+ def _parse_entry_points_files(sys_path: list[str], group: str) -> list[tuple[str, str]]:
58
+ """(name, value) pairs for ``group`` by parsing each ``*.dist-info/entry_points.txt``
59
+ found on ``sys_path``. A name seen in an earlier ``sys.path`` entry wins over a
60
+ later one — the same first-found precedence ``sys.path`` gives real imports.
61
+
62
+ A dropped duplicate emits a ``UserWarning`` naming the group, the duplicated
63
+ name, and which value won — the same diagnostic value the old
64
+ ``importlib.metadata`` + ``ensure_unique`` path gave operators (that path
65
+ let both entries load and had ``ensure_unique`` reject the second with a
66
+ warning); silently dropping it here would regress that visibility.
67
+ """
68
+ out: list[tuple[str, str]] = []
69
+ seen: dict[str, str] = {}
70
+ for entry in sys_path:
71
+ base = Path(entry) if entry else Path.cwd()
72
+ if not base.is_dir():
73
+ continue
74
+ for dist_info in base.glob("*.dist-info"):
75
+ ep_file = dist_info / "entry_points.txt"
76
+ if not ep_file.is_file():
77
+ continue
78
+ groups = _parsed_entry_points_by_group(ep_file)
79
+ if group not in groups:
80
+ continue
81
+ for name, value in groups[group]:
82
+ winner = seen.get(name)
83
+ if winner is None: # dedup: first sys.path entry wins
84
+ seen[name] = value
85
+ out.append((name, value))
86
+ elif winner != value:
87
+ warnings.warn(
88
+ f"duplicate entry_point {name!r} in group {group!r}: "
89
+ f"{value!r} dropped in favor of the earlier {winner!r} "
90
+ f"(first sys.path entry wins)",
91
+ stacklevel=2,
92
+ )
93
+ return out
94
+
95
+
96
+ def scan_entry_points(sys_path: list[str], group: str) -> list[tuple[str, str]]:
97
+ """(name, 'module:attr') pairs for ``group`` across ``sys_path``.
98
+
99
+ Hard-falls-back to ``importlib.metadata.entry_points`` on any parse
100
+ failure, or unconditionally when ``AGENT_RUNNER_PLUGIN_DISCOVERY=metadata``
101
+ is set (escape hatch for an environment where the scan and metadata
102
+ disagree).
103
+ """
104
+ if os.environ.get("AGENT_RUNNER_PLUGIN_DISCOVERY") == "metadata":
105
+ return _metadata_entry_points(group)
106
+ try:
107
+ return _parse_entry_points_files(sys_path, group)
108
+ except Exception: # noqa: BLE001 — never let discovery crash import; fall back
109
+ return _metadata_entry_points(group)