orbi-cli 0.7.28__tar.gz → 0.7.30__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 (235) hide show
  1. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/PKG-INFO +1 -1
  2. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/pyproject.toml +1 -1
  3. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/__init__.py +1 -1
  4. orbi_cli-0.7.28/src/orbi/criteria_command.py → orbi_cli-0.7.30/src/orbi/amend_command.py +9 -9
  5. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/base_ci_red.py +35 -24
  6. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/check_permission.py +1 -20
  7. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/checks.py +29 -0
  8. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/claim.py +8 -8
  9. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_commands.py +3 -3
  10. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_source.py +18 -15
  11. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_ticket_command.py +10 -12
  12. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/comment_kind.py +7 -1
  13. orbi_cli-0.7.30/src/orbi/comment_rest_id.py +29 -0
  14. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/config.py +16 -6
  15. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/failure_report.py +5 -13
  16. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/git_transport.py +109 -3
  17. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/github.py +33 -33
  18. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/human_review.py +2 -1
  19. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/language_terms.py +2 -2
  20. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/merge_exec.py +21 -0
  21. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/milestone.py +3 -2
  22. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/milestone_command.py +3 -7
  23. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_command.py +44 -10
  24. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_process.py +28 -29
  25. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_recovery.py +42 -1
  26. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_session.py +38 -8
  27. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_stream_log.py +34 -1
  28. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pilot_setup.py +6 -4
  29. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress.py +2 -2
  30. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_closeout.py +1 -1
  31. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner.py +15 -13
  32. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner_source.py +6 -1
  33. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/setup_checks.py +35 -10
  34. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/source_pool.py +6 -12
  35. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/split_command.py +9 -58
  36. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/ticket_command.py +80 -28
  37. orbi_cli-0.7.30/src/orbi/ticket_command_registry.py +86 -0
  38. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/PKG-INFO +1 -1
  39. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/SOURCES.txt +7 -2
  40. orbi_cli-0.7.28/tests/test_criteria_command.py → orbi_cli-0.7.30/tests/test_amend_command.py +137 -35
  41. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_base_ci_red.py +133 -10
  42. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_delivery.py +13 -10
  43. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_release.py +21 -12
  44. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_runner.py +68 -7
  45. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_install.py +1 -1
  46. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_packaging.py +2 -1
  47. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_source.py +118 -7
  48. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_ticket_commands.py +3 -3
  49. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_comment_identity.py +22 -0
  50. orbi_cli-0.7.30/tests/test_comment_rest_id.py +47 -0
  51. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_i18n.py +8 -0
  52. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_site.py +5 -0
  53. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_transport.py +66 -2
  54. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_github.py +101 -5
  55. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_github_fakes.py +42 -6
  56. orbi_cli-0.7.30/tests/test_pi_child_reap.py +181 -0
  57. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_command.py +67 -7
  58. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_session.py +82 -0
  59. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pilot_setup.py +85 -2
  60. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_prerequisite_check.py +56 -4
  61. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_progress.py +36 -0
  62. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_resume_continue_e2e.py +4 -0
  63. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_review_merge.py +80 -4
  64. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_runner_config.py +97 -1
  65. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_runner_source_freshness.py +28 -0
  66. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_split_command.py +0 -6
  67. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_steering_review.py +136 -18
  68. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_suggest.py +27 -15
  69. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ticket_command.py +11 -3
  70. orbi_cli-0.7.30/tests/test_ticket_command_registration.py +221 -0
  71. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/LICENSE +0 -0
  72. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/README.md +0 -0
  73. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/docs/licenses/sustainable-use-license.md +0 -0
  74. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/setup.cfg +0 -0
  75. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/blocked_comment.py +0 -0
  76. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/branch_reclaim.py +0 -0
  77. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/clarify.py +0 -0
  78. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli.py +0 -0
  79. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_doctor.py +0 -0
  80. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_parser.py +0 -0
  81. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_status.py +0 -0
  82. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/comment_trust.py +0 -0
  83. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/delivery_comment.py +0 -0
  84. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/delivery_labels.py +0 -0
  85. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/delivery_scene.py +0 -0
  86. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/engine_source.py +0 -0
  87. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/example_config.toml +0 -0
  88. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/extensions/clarify_verdict.ts +0 -0
  89. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/failure.py +0 -0
  90. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/gh_response.py +0 -0
  91. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/git_identity.py +0 -0
  92. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/github_feedback.py +0 -0
  93. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/gitops.py +0 -0
  94. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/journal.py +0 -0
  95. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/language.py +0 -0
  96. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/launchd_deploy.py +0 -0
  97. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/merge_gate.py +0 -0
  98. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/merge_handoff.py +0 -0
  99. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/notify.py +0 -0
  100. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_activity.py +0 -0
  101. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_idle.py +0 -0
  102. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pilot_slots.py +0 -0
  103. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pr_verification.py +0 -0
  104. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress_display.py +0 -0
  105. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress_outcome.py +0 -0
  106. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress_summary.py +0 -0
  107. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release.py +0 -0
  108. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_comments.py +0 -0
  109. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_declaration.py +0 -0
  110. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_git.py +0 -0
  111. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_notes.py +0 -0
  112. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_run.py +0 -0
  113. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/repo_config.py +0 -0
  114. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/resume_scan.py +0 -0
  115. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/resume_verification.py +0 -0
  116. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/review_merge.py +0 -0
  117. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/review_rounds.py +0 -0
  118. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/review_verdict.py +0 -0
  119. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/run_state.py +0 -0
  120. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner_health.py +0 -0
  121. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner_marker.py +0 -0
  122. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/scene.py +0 -0
  123. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/scheduler.py +0 -0
  124. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/scheduler_units.py +0 -0
  125. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/session_command.py +0 -0
  126. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/setup_config.py +0 -0
  127. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/status_observe.py +0 -0
  128. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/suggest.py +0 -0
  129. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/systemd_deploy.py +0 -0
  130. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/user_summary.py +0 -0
  131. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  132. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  133. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/top_level.txt +0 -0
  134. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_agents_md.py +0 -0
  135. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_apply_label_patch.py +0 -0
  136. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_base_branch_failure.py +0 -0
  137. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_blocked_comment.py +0 -0
  138. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_adapters.py +0 -0
  139. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_pi.py +0 -0
  140. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_preflight.py +0 -0
  141. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_branch_reclaim.py +0 -0
  142. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_check_permission.py +0 -0
  143. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_failure_issue_workflow.py +0 -0
  144. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_failure_triage.py +0 -0
  145. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_macos_workflow.py +0 -0
  146. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_workflow.py +0 -0
  147. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_clarify.py +0 -0
  148. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_claude_plugin.py +0 -0
  149. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_golden.py +0 -0
  150. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_milestone.py +0 -0
  151. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_command_receipt_contract.py +0 -0
  152. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_comment_readers_contract.py +0 -0
  153. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_comment_trust.py +0 -0
  154. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_concurrency_e2e.py +0 -0
  155. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_constitution.py +0 -0
  156. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_coverage_gate.py +0 -0
  157. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_deliver_pr_fakes.py +0 -0
  158. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_delivery_branch_delete.py +0 -0
  159. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_delivery_labels.py +0 -0
  160. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_delivery_scene.py +0 -0
  161. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docker_docs.py +0 -0
  162. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docker_entrypoint.py +0 -0
  163. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docker_image_workflow.py +0 -0
  164. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_issue_shapes.py +0 -0
  165. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_labels.py +0 -0
  166. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_mermaid.py +0 -0
  167. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_releases.py +0 -0
  168. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_single_source.py +0 -0
  169. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_engine_source.py +0 -0
  170. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_failure.py +0 -0
  171. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_failure_report.py +0 -0
  172. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_fix_needed_loop.py +0 -0
  173. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gh_read_retry.py +0 -0
  174. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gh_response.py +0 -0
  175. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_base_smoke.py +0 -0
  176. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_merge_smoke.py +0 -0
  177. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_network_retry.py +0 -0
  178. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gitops.py +0 -0
  179. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gitops_fakes.py +0 -0
  180. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_human_review.py +0 -0
  181. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_import_graph.py +0 -0
  182. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_install_sh.py +0 -0
  183. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_issue_1367.py +0 -0
  184. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_issue_461.py +0 -0
  185. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_issue_template_config.py +0 -0
  186. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_journal.py +0 -0
  187. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_journal_events.py +0 -0
  188. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_launchd_deploy.py +0 -0
  189. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_license.py +0 -0
  190. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_merge_exec_extraction.py +0 -0
  191. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_message_language.py +0 -0
  192. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_milestone_command.py +0 -0
  193. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_notify.py +0 -0
  194. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ops_e2e.py +0 -0
  195. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_orbi.py +0 -0
  196. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_orbi_dashboard.py +0 -0
  197. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_orbi_exporter.py +0 -0
  198. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_patch_ratchet.py +0 -0
  199. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_activity.py +0 -0
  200. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_extensions.py +0 -0
  201. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_provider_templates.py +0 -0
  202. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_recovery.py +0 -0
  203. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pilot_slots.py +0 -0
  204. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pr_issue_link_workflow.py +0 -0
  205. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pr_verification.py +0 -0
  206. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_progress_wiring.py +0 -0
  207. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_prompt_contract.py +0 -0
  208. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_public_contracts.py +0 -0
  209. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_public_helpers.py +0 -0
  210. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_publish_workflow.py +0 -0
  211. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_release_docs_workflow.py +0 -0
  212. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_release_extraction.py +0 -0
  213. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_release_ticket_templates.py +0 -0
  214. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_repo_config.py +0 -0
  215. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_resume_e2e.py +0 -0
  216. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_resume_pr.py +0 -0
  217. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_review_verdict.py +0 -0
  218. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_run_artifacts.py +0 -0
  219. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_run_id_e2e.py +0 -0
  220. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_run_id_fixture.py +0 -0
  221. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_runner_health.py +0 -0
  222. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_scene.py +0 -0
  223. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_scheduler.py +0 -0
  224. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_seam.py +0 -0
  225. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_setup_output_golden.py +0 -0
  226. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_size_ratchet.py +0 -0
  227. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_src_layout.py +0 -0
  228. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_status_observe.py +0 -0
  229. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_suite_hygiene.py +0 -0
  230. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_systemd_deploy.py +0 -0
  231. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_systemd_timer.py +0 -0
  232. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_user_acceptance_contract.py +0 -0
  233. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_user_summary.py +0 -0
  234. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_versioned_comment_block.py +0 -0
  235. {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_worktree_reclaim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orbi-cli
3
- Version: 0.7.28
3
+ Version: 0.7.30
4
4
  Summary: Orbi — local AI development worker: claims GitHub Issues, runs Pi in isolated worktrees, and delivers PRs (GitHub Issues + labels are the only state store).
5
5
  Author: xqliu
6
6
  License-Expression: AGPL-3.0-only OR LicenseRef-Sustainable-Use-1.0
@@ -44,7 +44,7 @@ build-backend = "setuptools.build_meta"
44
44
 
45
45
  [project]
46
46
  name = "orbi-cli"
47
- version = "0.7.28"
47
+ version = "0.7.30"
48
48
  description = "Orbi — local AI development worker: claims GitHub Issues, runs Pi in isolated worktrees, and delivers PRs (GitHub Issues + labels are the only state store)."
49
49
  readme = "README.md"
50
50
  requires-python = ">=3.14"
@@ -18,4 +18,4 @@ the package would shadow the installed package for every process with
18
18
  the checkout root on sys.path. The direct-execution compatibility entry
19
19
  is `python3 -m orbi.cli` (development path only).
20
20
  """
21
- __version__ = "0.7.28"
21
+ __version__ = "0.7.30"
@@ -1,7 +1,7 @@
1
1
  """`/orbi amend`: put the proposed acceptance criteria on the ticket.
2
2
 
3
3
  The clarification gate stops a thin ticket and may attach a `kind=criteria`
4
- proposal to its stop comment (Issue #1926). This module registers the one
4
+ proposal to its stop comment (Issue #1926). This module defines the one
5
5
  command that acts on it: a person with write permission replies
6
6
  `/orbi amend` and the proposed conditions — **as the comment reads at
7
7
  that moment**, so editing it first changes what lands — are appended to the
@@ -30,7 +30,9 @@ still scanned on the next tick and the same command continues from what is
30
30
  already on the ticket; a ticket that already carries this command's success
31
31
  receipt only gets step 4. The success receipt uses the command layer's own
32
32
  marker keyed on the command comment, so "has this command already
33
- succeeded?" is a marker lookup over the raw bodies, never prose.
33
+ succeeded?" is a marker lookup over the raw bodies, never prose — and only
34
+ a receipt THIS runner wrote counts (`runner_receipt`): the marker is public
35
+ text, so a copied one must never stand in for the work (Issue #1992).
34
36
 
35
37
  The two steps that change what the next tick reads — the append and the
36
38
  dispatch label — are wrapped in the shared `ticket_command.run_step`, so
@@ -53,8 +55,8 @@ from orbi.ticket_command import (
53
55
  TicketCommandError,
54
56
  command_comment_key,
55
57
  command_receipt_marker,
56
- register_command,
57
58
  run_step,
59
+ runner_receipt,
58
60
  )
59
61
 
60
62
  # The heading the conditions land under follows the message language
@@ -189,10 +191,10 @@ def _criteria_apply(
189
191
  language.text(language_code, "criteria.reason_no_suggestion"),
190
192
  )
191
193
  receipt = (
192
- f"<!-- {command_receipt_marker(ADD_CRITERIA_COMMAND)} "
194
+ f"<!-- {command_receipt_marker(AMEND_COMMAND)} "
193
195
  f"comment={command_comment_key(command_comment)} -->"
194
196
  )
195
- if not any(receipt in str(other.get("body")) for other in comments):
197
+ if not runner_receipt(comments, receipt):
196
198
  current = issue.get("body") or ""
197
199
  if appended_marker() not in current:
198
200
  section = render_acceptance(suggestion.items, language_code)
@@ -229,8 +231,8 @@ def _criteria_apply(
229
231
  edit_issue(issue_number, repo=repo, remove=NEEDS_DETAIL_LABEL)
230
232
 
231
233
 
232
- ADD_CRITERIA_COMMAND = CommandSpec(
233
- name="add_criteria",
234
+ AMEND_COMMAND = CommandSpec(
235
+ name="amend",
234
236
  subcommand="amend",
235
237
  arity=0,
236
238
  permission="write",
@@ -242,5 +244,3 @@ ADD_CRITERIA_COMMAND = CommandSpec(
242
244
  ),
243
245
  context=_criteria_context,
244
246
  )
245
-
246
- register_command(ADD_CRITERIA_COMMAND)
@@ -38,13 +38,14 @@ from __future__ import annotations
38
38
  import dataclasses
39
39
  import json
40
40
  import logging
41
- import re
42
41
  from datetime import datetime, timezone
43
42
 
44
43
  from orbi import comment_trust, failure, language, user_summary
45
44
  from orbi.comment_kind import (
46
45
  COMMENT_KIND_FAILURE, comment_kind, parse_versioned_block,
46
+ versioned_block_pattern,
47
47
  )
48
+ from orbi.comment_rest_id import rest_comment_id
48
49
  from orbi.delivery_labels import (
49
50
  BLOCKED_LABEL, FIX_NEEDED_LABEL, P0_LABEL, READY_LABEL,
50
51
  )
@@ -58,7 +59,7 @@ from orbi.pi_activity import parse_iso_utc
58
59
  from orbi.progress import fenced
59
60
  from orbi.ticket_command import (
60
61
  CommandSpec, TicketCommandError, command_comment_key,
61
- command_receipt_marker, process_commands, register_command,
62
+ command_receipt_marker, process_commands, runner_receipt,
62
63
  )
63
64
 
64
65
  SCHEMA_VERSION = 1
@@ -159,10 +160,27 @@ def _validate(record: BaseCIRed) -> None:
159
160
  )
160
161
 
161
162
 
163
+ def _serialize(payload: dict) -> str:
164
+ """One block payload: compact JSON with the HTML delimiters escaped.
165
+
166
+ The block is an HTML comment and the record carries GitHub's own
167
+ text: a check name may hold the comment terminator (a workflow named
168
+ after a shell pipeline) and the base branch is a user string.
169
+ Unescaped, a terminator closes the comment early, the next tick's
170
+ `parse` reads a truncated payload as corruption, and `paused_scene`
171
+ abandons the recovery (Issue #1998). The JSON \\u003c / \\u003e escapes
172
+ decode back to the exact same value, exactly the way the progress
173
+ block serializes its summary (`progress_summary.serialize_summary`).
174
+ """
175
+ return json.dumps(
176
+ payload, separators=(",", ":"), ensure_ascii=False,
177
+ ).replace("<", "\\u003c").replace(">", "\\u003e")
178
+
179
+
162
180
  def render(record: BaseCIRed) -> str:
163
181
  """Render the single hidden block for one record (deterministic)."""
164
182
  _validate(record)
165
- payload = json.dumps(
183
+ payload = _serialize(
166
184
  {
167
185
  "schema": record.schema,
168
186
  "state": record.state,
@@ -171,7 +189,6 @@ def render(record: BaseCIRed) -> str:
171
189
  "pr": record.pr,
172
190
  "failing_checks": list(record.failing_checks),
173
191
  },
174
- separators=(",", ":"), ensure_ascii=False,
175
192
  )
176
193
  return BLOCK_TEMPLATE.format(payload=payload)
177
194
 
@@ -474,6 +491,12 @@ def rewrite_state(body: str, state: str, *, language_code: str) -> str:
474
491
  The state line is APPENDED above the block, so the comment keeps a
475
492
  short timeline (paused -> green again -> merged) instead of
476
493
  rewriting history; the block itself is replaced, never duplicated.
494
+
495
+ The block is located by its marker ENVELOPE, never by the exact text
496
+ today's `render` produces: a comment written before #1998 escaped the
497
+ HTML delimiters carries a raw `<`/`>` (a check name such as
498
+ `tests (>=3.10)`) and still parses, so a text-exact replace would
499
+ silently no-op and leave it `waiting` for good.
477
500
  """
478
501
  record = parse(body)
479
502
  if record is None:
@@ -500,8 +523,8 @@ def rewrite_state(body: str, state: str, *, language_code: str) -> str:
500
523
  )
501
524
  if line.strip() not in (text.strip() for text in lines[:marker_index]):
502
525
  lines[marker_index:marker_index] = [line, ""]
503
- return "\n".join(lines).replace(
504
- render(record), render(updated), 1,
526
+ return versioned_block_pattern(_MARKER).sub(
527
+ lambda _match: render(updated), "\n".join(lines), count=1,
505
528
  ).rstrip() + "\n"
506
529
 
507
530
 
@@ -509,19 +532,6 @@ def rewrite_state(body: str, state: str, *, language_code: str) -> str:
509
532
  # The in-place edit of the one comment
510
533
  # --------------------------------------------------------------------------
511
534
 
512
- _COMMENT_URL_ID_RE = re.compile(r"#issuecomment-(\d+)$")
513
-
514
-
515
- def _rest_comment_id(comment: dict) -> int | None:
516
- """The REST id of one comment, from the id field or its permalink."""
517
- comment_id = comment.get("id")
518
- if isinstance(comment_id, int) and not isinstance(comment_id, bool):
519
- return comment_id
520
- url = comment.get("url")
521
- match = _COMMENT_URL_ID_RE.search(url) if isinstance(url, str) else None
522
- return int(match.group(1)) if match else None
523
-
524
-
525
535
  def _located(comments: list[dict]) -> tuple[BaseCIRed, dict] | None:
526
536
  """The newest comment carrying a valid block, and its record.
527
537
 
@@ -548,7 +558,7 @@ def _edit_state(repo: str, issue_number: int, comment: dict,
548
558
  body = comment.get("body")
549
559
  if not isinstance(body, str):
550
560
  return False
551
- comment_id = _rest_comment_id(comment)
561
+ comment_id = rest_comment_id(comment)
552
562
  if comment_id is None:
553
563
  LOGGER.warning(
554
564
  "base CI red comment has no REST id issue=%s repo=%s",
@@ -894,7 +904,10 @@ def _fix_base_apply(
894
904
  Idempotent per COMMENT: the reply carries the command layer's own
895
905
  receipt marker, so a second tick that still sees the same command
896
906
  comment does nothing (the block stays `waiting` until the base turns
897
- green, so the comment is visible for several ticks).
907
+ green, so the comment is visible for several ticks). Only a receipt
908
+ THIS runner wrote counts (`runner_receipt`): the marker is public
909
+ text, so a copied one must never read as "the fix Issue was opened"
910
+ (Issue #1992).
898
911
  """
899
912
  if not isinstance(record, BaseCIRed):
900
913
  # The layer's contract: a handler that cannot run raises its own
@@ -908,7 +921,7 @@ def _fix_base_apply(
908
921
  f"<!-- {command_receipt_marker(FIX_BASE_COMMAND)} "
909
922
  f"comment={command_comment_key(command_comment)} -->"
910
923
  )
911
- if any(receipt in str(other.get("body")) for other in comments):
924
+ if runner_receipt(comments, receipt):
912
925
  return
913
926
  checks = failing_checks(repo, list(record.failing_checks), record.base_sha)
914
927
  if not checks:
@@ -967,5 +980,3 @@ FIX_BASE_COMMAND = CommandSpec(
967
980
  ),
968
981
  context=_fix_base_context,
969
982
  )
970
-
971
- register_command(FIX_BASE_COMMAND)
@@ -49,6 +49,7 @@ from orbi.comment_kind import (
49
49
  COMMENT_KIND_FAILURE, comment_kind, comment_kind_marker,
50
50
  parse_versioned_block,
51
51
  )
52
+ from orbi.comment_rest_id import rest_comment_id
52
53
  from orbi.delivery_labels import (
53
54
  BLOCKED_LABEL, FIX_NEEDED_LABEL, READY_LABEL,
54
55
  )
@@ -81,9 +82,6 @@ _MARKER = "orbi:check-permission:v1"
81
82
  BLOCK_TEMPLATE = f"<!-- {_MARKER} {{payload}} -->"
82
83
  _FIELDS = ("schema", "pr", "state")
83
84
 
84
- # The REST id carried by a comment's permalink (see `rest_comment_id`).
85
- _COMMENT_URL_ID_RE = re.compile(r"#issuecomment-(\d+)$")
86
-
87
85
  # One bounded page per recovered scan: `ai-blocked` Issues are few, and an
88
86
  # unbounded scan would multiply the per-tick read cost.
89
87
  _SCAN_LIMIT = 200
@@ -369,23 +367,6 @@ def _waiting_block(comments: list[dict], *,
369
367
  return record, comment
370
368
 
371
369
 
372
- def rest_comment_id(comment: dict) -> int | None:
373
- """The REST id of one Issue comment, from its id field or its permalink.
374
-
375
- `gh issue view --json comments` reports the GraphQL node id — an
376
- `"IC_kw..."` string no REST route accepts — next to the permalink, and
377
- the permalink's `#issuecomment-<digits>` suffix IS the REST id the
378
- in-place edit's `PATCH /repos/{repo}/issues/comments/{id}` takes (the
379
- same rule `base_ci_red` and the failure reporter follow).
380
- """
381
- comment_id = comment.get("id")
382
- if isinstance(comment_id, int) and not isinstance(comment_id, bool):
383
- return comment_id
384
- url = comment.get("url")
385
- match = _COMMENT_URL_ID_RE.search(url) if isinstance(url, str) else None
386
- return int(match.group(1)) if match else None
387
-
388
-
389
370
  def _edit_state(repo: str, issue_number: int, comment: dict,
390
371
  record: CheckPermission) -> None:
391
372
  """Patch the ONE comment in place to the record's next state."""
@@ -12,6 +12,8 @@ stays the I/O leaf and no domain module owns a private copy.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
+ import json
16
+
15
17
  # A status a check reports once it has concluded (the rollup's CheckRun
16
18
  # `COMPLETED`, a legacy status context's `SUCCESS`/`FAILURE`/`ERROR`);
17
19
  # anything else is still running. GitHub recomputes mergeability and
@@ -21,6 +23,33 @@ _FINAL_STATUSES = ("COMPLETED", "SUCCESS", "FAILURE", "ERROR")
21
23
  _PASSING_CONCLUSIONS = ("SUCCESS", "NEUTRAL", "SKIPPED")
22
24
 
23
25
 
26
+ def parse_paginated_check_runs(raw: str) -> list:
27
+ """Flatten the check-run pages emitted by ``gh api --paginate --slurp``.
28
+
29
+ The list-check-runs REST response is one object per page —
30
+ ``{"total_count": N, "check_runs": [...]}`` — so the slurped output is
31
+ an array of those objects, one per page, and the caller receives every
32
+ run in page order (Issue #1993: GitHub serves 30 runs per page, so an
33
+ unpaginated read hid the 31st). Any other shape fails fast: a body that
34
+ cannot be read as pages must never pass for a complete — or passing —
35
+ check list.
36
+ """
37
+ pages = json.loads(raw)
38
+ if not isinstance(pages, list):
39
+ raise ValueError("paginated check-runs response must be an array of pages")
40
+ runs: list = []
41
+ for page in pages:
42
+ if not isinstance(page, dict) or not isinstance(page.get("check_runs"), list):
43
+ raise ValueError(
44
+ "paginated check-runs page must carry a check_runs array"
45
+ )
46
+ page_runs = page["check_runs"]
47
+ if any(not isinstance(run, dict) for run in page_runs):
48
+ raise ValueError("paginated check-runs page contains a non-object item")
49
+ runs.extend(page_runs)
50
+ return runs
51
+
52
+
24
53
  def render_check(entry: dict) -> str:
25
54
  """Render one classified check entry for humans, from the data.
26
55
 
@@ -15,12 +15,10 @@ import os
15
15
  from collections.abc import Sequence
16
16
  from pathlib import Path
17
17
 
18
- from orbi import base_ci_red, check_permission, gitops, ticket_command
19
- # Imported for its registration side effect: `ai-needs-detail` tickets run
20
- # the registered commands below, and `/orbi split` (Issue #1927) and
21
- # `/orbi amend` (Issue #1929, typed word renamed by #1975) must be in the
22
- # registry by then.
23
- from orbi import criteria_command, split_command # noqa: F401
18
+ from orbi import (
19
+ base_ci_red, check_permission, gitops, ticket_command,
20
+ ticket_command_registry,
21
+ )
24
22
  from orbi import milestone as milestone_bookkeeping
25
23
  from orbi.config import RunnerConfig
26
24
  from orbi.delivery_labels import (
@@ -216,7 +214,7 @@ def reconcile_open_epics(repo: str, run_id: str, language_code: str) -> list[str
216
214
  comments = issue_comments(int(number), repo=repo)
217
215
  if not epic_audit_posted(comments, child_evidence):
218
216
  comment_issue(int(number), repo=repo,
219
- body=f"<!-- orbi:run={run_id} -->\n{audit}\nrun_id={run_id}")
217
+ body=f"{run_marker(run_id)}\n{audit}\nrun_id={run_id}")
220
218
  close_issue(int(number), repo=repo)
221
219
  event("epic_closed", issue=number, repo=repo)
222
220
  evidence.append(f"Epic #{number} closed after verification ({'; '.join(child_evidence)})")
@@ -798,7 +796,9 @@ def pick_next_delivery(
798
796
  # recovery scans above.
799
797
  try:
800
798
  ticket_command.process_labeled_commands(
801
- repo, NEEDS_DETAIL_LABEL, dispatch_label=dispatch_label,
799
+ repo, NEEDS_DETAIL_LABEL,
800
+ commands=ticket_command_registry.ticket_commands(),
801
+ dispatch_label=dispatch_label,
802
802
  language_code=repo_language,
803
803
  )
804
804
  except Exception:
@@ -26,7 +26,7 @@ from orbi import (
26
26
  scheduler,
27
27
  status_observe,
28
28
  suggest,
29
- ticket_command,
29
+ ticket_command_registry,
30
30
  )
31
31
  from orbi.cli_doctor import doctor_report
32
32
  from orbi.cli_ticket_command import command_ticket_command
@@ -73,7 +73,7 @@ READ_ONLY_COMMANDS = frozenset({"status", "doctor", "session", "check"})
73
73
  # actions like the maintenance ones: no model call, so the same rule
74
74
  # applies (Issue #1930).
75
75
  _TICKET_COMMANDS = frozenset(
76
- spec.subcommand for spec in ticket_command.cli_specs()
76
+ spec.subcommand for spec in ticket_command_registry.cli_specs()
77
77
  )
78
78
 
79
79
  # A missing provider key must never block these deployment maintenance
@@ -388,6 +388,6 @@ COMMAND_HANDLERS = {
388
388
  # of `/orbi fix-base`, so the table is derived from the registry.
389
389
  **{
390
390
  spec.subcommand: command_ticket_command
391
- for spec in ticket_command.cli_specs()
391
+ for spec in ticket_command_registry.cli_specs()
392
392
  },
393
393
  }
@@ -46,7 +46,7 @@ import subprocess
46
46
  from pathlib import Path
47
47
 
48
48
  import orbi
49
- from orbi.journal import quote_value
49
+ from orbi.journal import quote_value, run_command
50
50
 
51
51
  # The Python floor (Issue #861): the same floor as the PEP 621
52
52
  # `requires-python` — pinned together by tests/test_cli_packaging.py
@@ -70,20 +70,21 @@ PROBE_TIMEOUT_SECONDS = 10
70
70
  def system_python3_compatible(exe: str) -> bool:
71
71
  """Whether one concrete interpreter satisfies REQUIRED_PYTHON.
72
72
 
73
- Runs the :data:`PYTHON_PROBE_CODE` probe on ``exe`` (with a
74
- timeout — a blocking command never runs bare). Any failure
75
- (missing executable, non-zero probe exit, timeout) is NOT
76
- compatible: the selection fails safe to the uv-provisioned
77
- interpreter, which uv resolves against system AND managed
78
- interpreters either way.
73
+ Runs the :data:`PYTHON_PROBE_CODE` probe on ``exe`` through the
74
+ single subprocess seam (:func:`orbi.journal.run_command`,
75
+ CONSTITUTION Article 3.4), with a timeout — a blocking command
76
+ never runs bare. Any failure (missing executable, non-zero probe
77
+ exit, timeout) is NOT compatible: the selection fails safe to the
78
+ uv-provisioned interpreter, which uv resolves against system AND
79
+ managed interpreters either way.
79
80
  """
80
81
  if not exe:
81
82
  return False
82
83
  try:
83
- probe = subprocess.run(
84
+ probe = run_command(
84
85
  [exe, "-S", "-c", PYTHON_PROBE_CODE],
85
- capture_output=True,
86
86
  timeout=PROBE_TIMEOUT_SECONDS,
87
+ check=False,
87
88
  )
88
89
  except (OSError, subprocess.SubprocessError):
89
90
  return False
@@ -99,6 +100,11 @@ def select_python_interpreter() -> str:
99
100
  ships 3.12.3; on a fresh uv-only host there is no system python3
100
101
  at all). ``orbi setup``, the Runner pre-start refresh and the
101
102
  systemd self-heal template all apply this one rule.
103
+
104
+ The rule runs a subprocess probe, so it is decided ONLY when a
105
+ reinstall (or the fix string that asks for one) is actually about
106
+ to happen — never at import (Issue #2001: importing the Runner
107
+ must not spawn a probe before logging is configured).
102
108
  """
103
109
  exe = shutil.which("python3")
104
110
  if exe is not None and system_python3_compatible(exe):
@@ -106,10 +112,6 @@ def select_python_interpreter() -> str:
106
112
  return "3.14"
107
113
 
108
114
 
109
- # The selection result the reinstall argv (and every fix string)
110
- # embeds — computed once per process.
111
- PYTHON_INTERPRETER = select_python_interpreter()
112
-
113
115
  # The runtime package directory inside a checkout (the src
114
116
  # layout): the editable install maps this WHOLE directory, so a newly
115
117
  # added package module is importable without regenerating any module
@@ -127,11 +129,12 @@ def reinstall_args(repo_dir: Path) -> list[str]:
127
129
  reinstallation") and ``--python`` carries the compatible
128
130
  interpreter selection (Issue #861: the system ``python3`` when it
129
131
  satisfies the floor, otherwise the bare ``3.14`` so uv provisions
130
- or selects one).
132
+ or selects one) — decided HERE, when the reinstall is about to
133
+ happen, so importing this module never runs the probe (Issue #2001).
131
134
  """
132
135
  return [
133
136
  "uv", "tool", "install", "--force", "--reinstall", "--editable",
134
- "--python", PYTHON_INTERPRETER, str(Path(repo_dir)),
137
+ "--python", select_python_interpreter(), str(Path(repo_dir)),
135
138
  ]
136
139
 
137
140
 
@@ -5,8 +5,8 @@ A ticket command has two spellings and ONE execution path: a
5
5
  <owner/name> --issue <N>` on the host. This module is the terminal one:
6
6
 
7
7
  - `register_cli` adds one subcommand per registered arity-0 command
8
- (`ticket_command.cli_specs`), so registering a command gives both
9
- spellings and there is nothing to edit here. A command that needs an
8
+ (`ticket_command_registry.cli_specs`), so registering a command gives
9
+ both spellings and there is nothing to edit here. A command that needs an
10
10
  argument (`/orbi milestone <version>`) keeps its own explicit CLI command
11
11
  (`orbi milestone set`) and is never generated;
12
12
  - `command_ticket_command` runs the SAME `ticket_command.execute_command`
@@ -24,14 +24,12 @@ from __future__ import annotations
24
24
 
25
25
  import sys
26
26
 
27
- # The registrants, imported for their registration side effect: a command
28
- # is registered at import time and both the tree below and the CLI
29
- # dispatch table are derived from the registry (Issue #1930). A new
30
- # command module is added here once, exactly like the comment scan's call
31
- # site imports it.
32
- from orbi import ( # noqa: F401
33
- base_ci_red, criteria_command, split_command, ticket_command,
34
- )
27
+ # Both the tree below and the CLI dispatch table derive from the ONE
28
+ # explicit list of registrants,
29
+ # `ticket_command_registry.ticket_commands()` (Issues #1930, #2000): this
30
+ # module names no command module, so a command can never be missing from
31
+ # the terminal spelling because an import somewhere else changed.
32
+ from orbi import ticket_command, ticket_command_registry
35
33
  from orbi.claim import _repo_scan_keys
36
34
  from orbi.github import authenticated_github_login
37
35
  from orbi.repo_config import merged_repo_language
@@ -39,7 +37,7 @@ from orbi.repo_config import merged_repo_language
39
37
 
40
38
  def register_cli(subparsers, common) -> None:
41
39
  """Add one subcommand per registered arity-0 ticket command."""
42
- for spec in ticket_command.cli_specs():
40
+ for spec in ticket_command_registry.cli_specs():
43
41
  command = subparsers.add_parser(
44
42
  spec.subcommand, parents=[common],
45
43
  help=f"run the registered `{spec.canonical}` ticket command on "
@@ -58,7 +56,7 @@ def register_cli(subparsers, common) -> None:
58
56
 
59
57
  def command_ticket_command(config, args, parser) -> int:
60
58
  """Run one registered ticket command on one Issue (Issue #1930)."""
61
- spec = ticket_command.TICKET_COMMANDS[args.ticket_command]
59
+ spec = ticket_command_registry.command_by_name(args.ticket_command)
62
60
  repo = args.repo
63
61
  milestone, dispatch_label = _repo_scan_keys(
64
62
  config, repo, config.active_milestone,
@@ -33,6 +33,11 @@ import re
33
33
  from collections.abc import Callable, Sequence
34
34
  from typing import Any
35
35
 
36
+ # The run-id format belongs to `journal.validate_run_id` (Issue #2003):
37
+ # the anchored prefix below interpolates that pattern instead of spelling
38
+ # the id out, so a reader can never accept an id the writer refuses.
39
+ from orbi.journal import RUN_ID_PATTERN
40
+
36
41
  COMMENT_KIND_STARTED = "started"
37
42
  COMMENT_KIND_FAILURE = "failure"
38
43
  COMMENT_KIND_REVIEW_ROUND = "review-round"
@@ -49,7 +54,8 @@ COMMENT_KIND_PATTERN = re.compile(r"<!-- orbi:comment=([a-z-]+) -->")
49
54
  # the line immediately after. Anchoring means a marker-shaped string
50
55
  # quoted in prose, a raw log or a code fence is never read as metadata.
51
56
  _KIND_PREFIX_RE = re.compile(
52
- r"\A<!-- orbi:run=[0-9a-f]{8} -->\r?\n<!-- orbi:comment=([a-z-]+) -->"
57
+ r"\A<!-- orbi:run=" + RUN_ID_PATTERN.pattern
58
+ + r" -->\r?\n<!-- orbi:comment=([a-z-]+) -->"
53
59
  )
54
60
 
55
61
 
@@ -0,0 +1,29 @@
1
+ """The REST id of one Issue comment record (Issue #2004).
2
+
3
+ `gh issue view --json comments` reports the GraphQL node id — an
4
+ `"IC_kw..."` string no REST route accepts — next to the comment's
5
+ permalink, and the permalink's fragment carries the REST id that the
6
+ in-place edit's `PATCH /repos/{repo}/issues/comments/{id}` route takes. A
7
+ numeric `id` field (an `gh api` read) is that id already.
8
+
9
+ One rule, one owner: the base-CI red recovery (`base_ci_red`), the
10
+ check-permission recovery (`check_permission`) and the failure reporter
11
+ (`failure_report`) all read the id from here, so the pattern lives in this
12
+ module only. `github.issue_comment_rest_id` stays the neighbouring rule
13
+ that resolves a GraphQL node id to a REST id with an API read.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+
19
+ _COMMENT_URL_ID_RE = re.compile(r"#issuecomment-(\d+)$")
20
+
21
+
22
+ def rest_comment_id(comment: dict) -> int | None:
23
+ """The REST id of one comment, from the id field or its permalink."""
24
+ comment_id = comment.get("id")
25
+ if isinstance(comment_id, int) and not isinstance(comment_id, bool):
26
+ return comment_id
27
+ url = comment.get("url")
28
+ match = _COMMENT_URL_ID_RE.search(url) if isinstance(url, str) else None
29
+ return int(match.group(1)) if match else None
@@ -190,6 +190,20 @@ class RunnerConfig:
190
190
  repositories: tuple[dict, ...] = ()
191
191
 
192
192
 
193
+ def parse_source_repos(value: object) -> tuple[str, ...]:
194
+ """Validate the task-pool declaration; the one ``source_repos`` parser.
195
+
196
+ ``load_config`` parses the whole config through it, and the source pool
197
+ uses it on a sibling deployment's plain TOML value (Issue #2002): the
198
+ rule is stated once, so the two callers can never drift apart.
199
+ """
200
+ if not isinstance(value, list) or not value:
201
+ raise ValueError("source_repos must be a non-empty list")
202
+ if not all(isinstance(repo, str) and repo for repo in value):
203
+ raise ValueError("source_repos must contain non-empty strings")
204
+ return tuple(value)
205
+
206
+
193
207
  class ConfigFileMissingError(FileNotFoundError):
194
208
  """The configured ``orbi.toml`` itself does not exist."""
195
209
 
@@ -209,11 +223,7 @@ def load_config(path: Path, *, check_provider_api_keys: bool = True,
209
223
  if not path.exists():
210
224
  raise ConfigFileMissingError(path)
211
225
  data = tomllib.loads(path.read_text(encoding="utf-8"))
212
- source_repos = data.get("source_repos")
213
- if not isinstance(source_repos, list) or not source_repos:
214
- raise ValueError("source_repos must be a non-empty list")
215
- if not all(isinstance(repo, str) and repo for repo in source_repos):
216
- raise ValueError("source_repos must contain non-empty strings")
226
+ source_repos = parse_source_repos(data.get("source_repos"))
217
227
  unit_name = data.get("unit_name")
218
228
  if unit_name is not None and (
219
229
  not isinstance(unit_name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", unit_name)
@@ -488,7 +498,7 @@ def load_config(path: Path, *, check_provider_api_keys: bool = True,
488
498
  }
489
499
  return RunnerConfig(
490
500
  config_path=path.resolve(),
491
- source_repos=tuple(source_repos),
501
+ source_repos=source_repos,
492
502
  repo_dir=repo_dir,
493
503
  deploy_home=deploy_home,
494
504
  unit_name=unit_name,
@@ -34,6 +34,8 @@ from orbi.comment_kind import (
34
34
  COMMENT_KIND_FAILURE,
35
35
  comment_kind_marker,
36
36
  )
37
+ from orbi.comment_rest_id import rest_comment_id
38
+ from orbi.comment_trust import latest_run_marker, trusted_comments
37
39
  from orbi.delivery_labels import (
38
40
  EVENT_FIX_NEEDED, FIX_NEEDED_LABEL, IN_PROGRESS_LABEL, READY_LABEL,
39
41
  )
@@ -41,7 +43,6 @@ from orbi.failure import (
41
43
  failure_comment_body, failure_detail, failure_summary,
42
44
  redact_local_paths, terminal_action,
43
45
  )
44
- from orbi.comment_trust import latest_run_marker, trusted_comments
45
46
  from orbi.github import (
46
47
  pr_number, apply_label_patch, comment_issue, issue_comments,
47
48
  issue_labels, issue_priority, pr_comments, run_gh_write_command,
@@ -875,17 +876,8 @@ def report_delivery_failure(
875
876
  # The #825 dedup: `gh issue view --json comments` returns a GraphQL
876
877
  # node id, but the PATCH route requires the REST id. The comment URL
877
878
  # already carries that id, so do not make a second API request.
878
- comment_id = reported_failure.get("id")
879
- if isinstance(comment_id, int) and not isinstance(comment_id, bool):
880
- rest_comment_id = comment_id
881
- else:
882
- url = reported_failure.get("url")
883
- match = (
884
- re.search(r"#issuecomment-(\d+)$", url)
885
- if isinstance(url, str) else None
886
- )
887
- rest_comment_id = int(match.group(1)) if match is not None else None
888
- if rest_comment_id is None:
879
+ comment_rest_id = rest_comment_id(reported_failure)
880
+ if comment_rest_id is None:
889
881
  # A malformed or missing URL is data loss in the dedup metadata,
890
882
  # not a delivery failure. Preserve the pre-#825 behavior so the
891
883
  # current failure is still recorded and the tick continues.
@@ -907,7 +899,7 @@ def report_delivery_failure(
907
899
  post_new_comment = False
908
900
  try:
909
901
  update_issue_comment(
910
- rest_comment_id, repo=source_repo,
902
+ comment_rest_id, repo=source_repo,
911
903
  # Apply the increment to the newly assembled body, not the
912
904
  # stale first report. This preserves scene/retry fields
913
905
  # added by this attempt while retaining the dedup counter.