orbi-cli 0.7.7__tar.gz → 0.7.9__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 (186) hide show
  1. {orbi_cli-0.7.7/src/orbi_cli.egg-info → orbi_cli-0.7.9}/PKG-INFO +1 -1
  2. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/pyproject.toml +1 -1
  3. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/__init__.py +1 -1
  4. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/blocked_comment.py +4 -3
  5. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/branch_reclaim.py +15 -10
  6. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/claim.py +132 -36
  7. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/cli.py +33 -15
  8. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/delivery_labels.py +50 -20
  9. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/failure_report.py +22 -22
  10. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/github.py +8 -4
  11. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/gitops.py +28 -1
  12. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/journal.py +4 -1
  13. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/merge_handoff.py +5 -3
  14. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/notify.py +18 -10
  15. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/release.py +10 -5
  16. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/resume_scan.py +49 -20
  17. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/review_merge.py +108 -112
  18. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/review_rounds.py +138 -3
  19. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/run_state.py +19 -0
  20. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/runner.py +26 -26
  21. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/status_observe.py +5 -2
  22. {orbi_cli-0.7.7 → orbi_cli-0.7.9/src/orbi_cli.egg-info}/PKG-INFO +1 -1
  23. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_apply_label_patch.py +42 -5
  24. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_blocked_comment.py +3 -1
  25. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_bootstrap_delivery.py +401 -10
  26. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_bootstrap_release.py +77 -12
  27. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_bootstrap_runner.py +178 -16
  28. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_branch_reclaim.py +70 -35
  29. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_ci_failure_issue_workflow.py +15 -0
  30. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_ci_failure_triage.py +156 -8
  31. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_comment_identity.py +130 -0
  32. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_concurrency_e2e.py +4 -3
  33. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_delivery_branch_delete.py +6 -3
  34. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_delivery_labels.py +76 -0
  35. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_failure_report.py +66 -1
  36. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_fix_needed_loop.py +99 -7
  37. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_git_merge_smoke.py +138 -0
  38. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_github.py +4 -1
  39. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_github_fakes.py +323 -1
  40. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_gitops.py +25 -0
  41. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_human_review.py +21 -2
  42. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_notify.py +24 -0
  43. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_orbi.py +47 -35
  44. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_progress_wiring.py +126 -1
  45. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_repo_config.py +6 -6
  46. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_resume_continue_e2e.py +4 -3
  47. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_resume_pr.py +155 -57
  48. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_review_merge.py +125 -16
  49. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_status_observe.py +66 -5
  50. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/LICENSE +0 -0
  51. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/README.md +0 -0
  52. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/docs/licenses/sustainable-use-license.md +0 -0
  53. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/setup.cfg +0 -0
  54. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/checks.py +0 -0
  55. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/clarify.py +0 -0
  56. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/cli_source.py +0 -0
  57. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/comment_kind.py +0 -0
  58. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/config.py +0 -0
  59. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/delivery_comment.py +0 -0
  60. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/delivery_scene.py +0 -0
  61. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/engine_source.py +0 -0
  62. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/example_config.toml +0 -0
  63. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/failure.py +0 -0
  64. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/gh_response.py +0 -0
  65. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/git_identity.py +0 -0
  66. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/git_transport.py +0 -0
  67. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/human_review.py +0 -0
  68. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/language.py +0 -0
  69. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/language_terms.py +0 -0
  70. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/launchd_deploy.py +0 -0
  71. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/merge_gate.py +0 -0
  72. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/milestone.py +0 -0
  73. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/milestone_command.py +0 -0
  74. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/pi_activity.py +0 -0
  75. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/pi_command.py +0 -0
  76. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/pi_process.py +0 -0
  77. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/pi_recovery.py +0 -0
  78. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/pi_session.py +0 -0
  79. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/pilot_setup.py +0 -0
  80. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/pilot_slots.py +0 -0
  81. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/progress.py +0 -0
  82. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/progress_display.py +0 -0
  83. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/progress_outcome.py +0 -0
  84. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/progress_summary.py +0 -0
  85. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/release_comments.py +0 -0
  86. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/release_git.py +0 -0
  87. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/release_notes.py +0 -0
  88. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/repo_config.py +0 -0
  89. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/runner_health.py +0 -0
  90. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/scene.py +0 -0
  91. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/scheduler.py +0 -0
  92. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/scheduler_units.py +0 -0
  93. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/session_command.py +0 -0
  94. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/source_pool.py +0 -0
  95. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/suggest.py +0 -0
  96. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/systemd_deploy.py +0 -0
  97. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi/ticket_command.py +0 -0
  98. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/SOURCES.txt +0 -0
  99. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  100. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  101. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/top_level.txt +0 -0
  102. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_agents_md.py +0 -0
  103. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_base_branch_failure.py +0 -0
  104. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_bootstrap_adapters.py +0 -0
  105. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_bootstrap_pi.py +0 -0
  106. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_bootstrap_preflight.py +0 -0
  107. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_ci_macos_workflow.py +0 -0
  108. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_ci_workflow.py +0 -0
  109. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_clarify.py +0 -0
  110. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_claude_plugin.py +0 -0
  111. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_cli_install.py +0 -0
  112. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_cli_milestone.py +0 -0
  113. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_cli_packaging.py +0 -0
  114. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_cli_source.py +0 -0
  115. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_comment_readers_contract.py +0 -0
  116. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_constitution.py +0 -0
  117. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_coverage_gate.py +0 -0
  118. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_deliver_pr_fakes.py +0 -0
  119. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_delivery_scene.py +0 -0
  120. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docker_docs.py +0 -0
  121. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docker_entrypoint.py +0 -0
  122. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docker_image_workflow.py +0 -0
  123. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docs_i18n.py +0 -0
  124. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docs_issue_shapes.py +0 -0
  125. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docs_labels.py +0 -0
  126. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docs_mermaid.py +0 -0
  127. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docs_releases.py +0 -0
  128. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docs_single_source.py +0 -0
  129. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_docs_site.py +0 -0
  130. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_engine_source.py +0 -0
  131. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_failure.py +0 -0
  132. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_gh_read_retry.py +0 -0
  133. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_gh_response.py +0 -0
  134. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_git_base_smoke.py +0 -0
  135. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_git_network_retry.py +0 -0
  136. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_git_transport.py +0 -0
  137. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_gitops_fakes.py +0 -0
  138. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_import_graph.py +0 -0
  139. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_install_sh.py +0 -0
  140. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_issue_1367.py +0 -0
  141. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_issue_461.py +0 -0
  142. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_issue_template_config.py +0 -0
  143. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_journal.py +0 -0
  144. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_journal_events.py +0 -0
  145. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_launchd_deploy.py +0 -0
  146. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_license.py +0 -0
  147. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_message_language.py +0 -0
  148. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_milestone_command.py +0 -0
  149. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_ops_e2e.py +0 -0
  150. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_orbi_dashboard.py +0 -0
  151. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_orbi_exporter.py +0 -0
  152. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_patch_ratchet.py +0 -0
  153. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pi_activity.py +0 -0
  154. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pi_command.py +0 -0
  155. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pi_extensions.py +0 -0
  156. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pi_provider_templates.py +0 -0
  157. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pi_recovery.py +0 -0
  158. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pi_session.py +0 -0
  159. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pilot_setup.py +0 -0
  160. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pilot_slots.py +0 -0
  161. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_pr_issue_link_workflow.py +0 -0
  162. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_prerequisite_check.py +0 -0
  163. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_progress.py +0 -0
  164. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_prompt_contract.py +0 -0
  165. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_public_contracts.py +0 -0
  166. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_publish_workflow.py +0 -0
  167. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_release_docs_workflow.py +0 -0
  168. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_release_ticket_templates.py +0 -0
  169. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_resume_e2e.py +0 -0
  170. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_run_artifacts.py +0 -0
  171. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_run_id_e2e.py +0 -0
  172. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_runner_config.py +0 -0
  173. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_runner_health.py +0 -0
  174. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_runner_source_freshness.py +0 -0
  175. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_scene.py +0 -0
  176. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_scheduler.py +0 -0
  177. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_size_ratchet.py +0 -0
  178. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_src_layout.py +0 -0
  179. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_steering_review.py +0 -0
  180. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_suggest.py +0 -0
  181. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_suite_hygiene.py +0 -0
  182. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_systemd_deploy.py +0 -0
  183. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_systemd_timer.py +0 -0
  184. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_ticket_command.py +0 -0
  185. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/tests/test_user_acceptance_contract.py +0 -0
  186. {orbi_cli-0.7.7 → orbi_cli-0.7.9}/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.7
3
+ Version: 0.7.9
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.7"
47
+ version = "0.7.9"
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.7"
21
+ __version__ = "0.7.9"
@@ -12,12 +12,13 @@ receipt) never goes through this exit and never gets the line.
12
12
  from __future__ import annotations
13
13
 
14
14
  from orbi import language
15
- from orbi.delivery_labels import EVENT_BLOCKED
15
+ from orbi.delivery_labels import EVENT_BLOCKED, READY_LABEL
16
16
  from orbi.github import apply_label_patch, comment_issue
17
17
 
18
18
 
19
19
  def post_blocked_comment(number: int, *, repo: str, body: str,
20
- current_labels, language_code: str) -> None:
20
+ current_labels, language_code: str,
21
+ ready_label: str = READY_LABEL) -> None:
21
22
  """Block `number`: apply the `ai-blocked` label patch, then comment.
22
23
 
23
24
  The order is part of the contract: the label transition lands first, the
@@ -27,7 +28,7 @@ def post_blocked_comment(number: int, *, repo: str, body: str,
27
28
  """
28
29
  apply_label_patch(
29
30
  number, repo=repo, event=EVENT_BLOCKED,
30
- current_labels=current_labels,
31
+ current_labels=current_labels, ready_label=ready_label,
31
32
  )
32
33
  comment_issue(
33
34
  number, repo=repo,
@@ -23,6 +23,7 @@ from pathlib import Path
23
23
  from typing import TYPE_CHECKING
24
24
  from urllib.parse import quote
25
25
 
26
+ from orbi import gitops
26
27
  from orbi.journal import GIT_NETWORK_TIMEOUT_SECONDS, event, run_command
27
28
 
28
29
  if TYPE_CHECKING:
@@ -93,14 +94,16 @@ def delete_merged_delivery_branch(
93
94
  ) -> None:
94
95
  """Delete a MERGED delivery PR's remote head branch (Issue #1478).
95
96
 
96
- Called right after `gh pr merge` lands. Only an `orbi/`-prefixed
97
- head (a branch Orbi itself created) is deleted; an external takeover
98
- PR's head is left to its owner. A 404/422 (the reference is already
99
- gone) counts as success. Any other failure emits a warning event and
100
- returns — the merge has already landed, so this cleanup never fails
101
- the delivery.
97
+ Called right after `gh pr merge` lands. Only this source repo's own
98
+ delivery branch (`gitops.is_delivery_branch`, the one ownership
99
+ check, Issue #1788) is deleted; a head a user named
100
+ (`orbi/manual`), another repository's delivery name or an external
101
+ takeover PR's head is left to its owner. A 404/422 (the reference is
102
+ already gone) counts as success. Any other failure emits a warning
103
+ event and returns — the merge has already landed, so this cleanup
104
+ never fails the delivery.
102
105
  """
103
- if not branch.startswith("orbi/"):
106
+ if not gitops.is_delivery_branch(source_repo, branch):
104
107
  return
105
108
  try:
106
109
  delete_remote_branch(source_repo, branch)
@@ -174,7 +177,7 @@ def read_merged_pull_requests(source_repo: str) -> list[dict]:
174
177
 
175
178
 
176
179
  def reclaim_merged_delivery_branches(config: config_domain.RunnerConfig) -> None:
177
- """Delete remote `orbi/*` branches whose delivery PR has MERGED.
180
+ """Delete this repo's remote delivery branches whose PR has MERGED.
178
181
 
179
182
  A merged delivery leaves its `orbi/<slug>-issue-<n>` branch on the
180
183
  remote forever; #1478 stops NEW ones, this pass drains the BACKLOG.
@@ -182,7 +185,9 @@ def reclaim_merged_delivery_branches(config: config_domain.RunnerConfig) -> None
182
185
  idempotent, bounded (at most `BRANCH_RECLAIM_MAX_PER_TICK` deletes)
183
186
  and never fatal.
184
187
 
185
- Only a branch whose CURRENT remote oid equals the `headRefOid` a
188
+ Only a branch this source repo OWNS (`gitops.is_delivery_branch`,
189
+ Issue #1788: `orbi/<owner>-<repo>-issue-<n>`, never an `orbi/`
190
+ prefix alone) whose CURRENT remote oid equals the `headRefOid` a
186
191
  MERGED PR reports is deleted. The delivery branch name is fixed per
187
192
  Issue, so a reopened Issue's new attempt pushes to the same name
188
193
  while the old merged PR still reports it: without the oid match the
@@ -201,7 +206,7 @@ def reclaim_merged_delivery_branches(config: config_domain.RunnerConfig) -> None
201
206
  heads = {
202
207
  branch: oid
203
208
  for branch, oid in _parse_remote_heads(raw).items()
204
- if branch.startswith("orbi/")
209
+ if gitops.is_delivery_branch(source_repo, branch)
205
210
  }
206
211
  if not heads:
207
212
  return
@@ -12,10 +12,10 @@ from __future__ import annotations
12
12
  import json
13
13
  import logging
14
14
  import os
15
- import re
16
15
  from collections.abc import Sequence
17
16
  from pathlib import Path
18
17
 
18
+ from orbi import gitops
19
19
  from orbi import milestone as milestone_bookkeeping
20
20
  from orbi.config import RunnerConfig
21
21
  from orbi.delivery_labels import (
@@ -214,20 +214,21 @@ def reconcile_open_epics(repo: str, run_id: str, language_code: str) -> list[str
214
214
  # idempotency key (one report per PR, never one per tick).
215
215
  ORPHAN_PR_MARK = "<!-- orbi:orphan-pr -->"
216
216
 
217
- _ORPHAN_PR_BRANCH_RE = re.compile(r"orbi/.+-issue-(\d+)\Z")
218
217
 
218
+ def orphan_pr_branch_issue(repo: str, head_ref: object) -> int | None:
219
+ """The source Issue number of THIS repo's delivery branch, or None.
219
220
 
220
- def orphan_pr_branch_issue(head_ref: object) -> int | None:
221
- """The source Issue number of a stable delivery branch, or None.
222
-
223
- `task_branch` names every Runner delivery branch
224
- `orbi/<source_repo slug>-issue-<N>`; any other head (a human's or an
225
- external contributor's branch) is never the Runner's business.
221
+ The ownership decision is `gitops.is_delivery_branch` and the number
222
+ comes from the same naming source (`gitops.delivery_branch_prefix`,
223
+ Issue #1788): only `orbi/<this source repo>-issue-<N>` names an Issue
224
+ here, so another repository's `orbi/x-y-issue-3` — or a human's own
225
+ `orbi/manual` — is never the Runner's business.
226
226
  """
227
- if not isinstance(head_ref, str):
227
+ if not isinstance(head_ref, str) or not gitops.is_delivery_branch(
228
+ repo, head_ref,
229
+ ):
228
230
  return None
229
- match = _ORPHAN_PR_BRANCH_RE.fullmatch(head_ref)
230
- return int(match.group(1)) if match else None
231
+ return int(head_ref.removeprefix(gitops.delivery_branch_prefix(repo)))
231
232
 
232
233
 
233
234
  def reconcile_orphan_prs(
@@ -256,7 +257,9 @@ def reconcile_orphan_prs(
256
257
  if not isinstance(pr, dict):
257
258
  continue
258
259
  pr_number = pr.get("number")
259
- issue_number = orphan_pr_branch_issue(pr.get("headRefName"))
260
+ issue_number = orphan_pr_branch_issue(
261
+ repo, pr.get("headRefName"),
262
+ )
260
263
  if not isinstance(pr_number, int) or issue_number is None:
261
264
  continue
262
265
  details = issue_view(issue_number, "state", repo=repo, timeout=30)
@@ -315,6 +318,7 @@ def _pick_from_scan(
315
318
  active_milestone: str | None = None,
316
319
  ready_label: str = READY_LABEL,
317
320
  external_only: bool = False, held: frozenset = frozenset(),
321
+ raise_on_read_failure: bool = False,
318
322
  ) -> dict | None:
319
323
  """Return the first claimable Issue of one scan result, else None.
320
324
 
@@ -361,6 +365,13 @@ def _pick_from_scan(
361
365
  `held` names the deliveries a live co-runner works on: such an Issue
362
366
  is skipped (`claim_yield reason=held_by_live_runner`) and never
363
367
  claimed twice — the skip is the FIRST guard of this loop.
368
+
369
+ `raise_on_read_failure` decides what a failed Milestone read of the
370
+ release gate MEANS to the caller — the same meaning every other read
371
+ of the decision carries (`_read_failed`): the claim fails safe
372
+ and skips the release this tick, an OBSERVER of the decision
373
+ (`orbi status`) re-raises so the failed read is named instead of
374
+ rendering as an empty queue (Issue #1782).
364
375
  """
365
376
  for issue in issues:
366
377
  if (repo, int(issue["number"])) in held:
@@ -386,10 +397,12 @@ def _pick_from_scan(
386
397
  repo, target_milestone,
387
398
  )
388
399
  except Exception as exc:
389
- event(
390
- "release_milestone_check_failed", level=logging.ERROR,
400
+ _read_failed(
401
+ "release_milestone_check_failed", exc,
402
+ raise_on_read_failure,
391
403
  issue=issue.get("number"), repo=repo,
392
- milestone=target_milestone, error=exc)
404
+ milestone=target_milestone,
405
+ )
393
406
  return None
394
407
  if open_issues > 1:
395
408
  event("release_milestone_incomplete", issue=issue.get("number"),
@@ -420,9 +433,31 @@ def _pick_from_scan(
420
433
  return None
421
434
 
422
435
 
436
+ def _read_failed(
437
+ read: str, exc: Exception, raise_on_read_failure: bool, **fields: object,
438
+ ) -> None:
439
+ """Journal one failed read, then fail open OR re-raise.
440
+
441
+ THE one place `raise_on_read_failure` is decided (Issue #1794):
442
+ every failing read in this module calls it with its OWN journal event
443
+ name and fields (`release_milestone_check_failed`,
444
+ `blocked_by_check_failed`), so each site keeps its line while the
445
+ meaning of a failed read cannot drift between sites.
446
+ The claim fails open: a broken query must never deadlock the queue,
447
+ so this tick claims nothing and the next one retries. An OBSERVER of
448
+ the same decision (`orbi status`) must not read a failed read as an
449
+ empty queue — it asks for the failure instead (#1668, #1782).
450
+ """
451
+ event(read, level=logging.ERROR, **fields, error=exc)
452
+ if raise_on_read_failure:
453
+ raise
454
+
455
+
423
456
  def pick_issue(repo: str, active_milestone: str | None = None,
424
457
  dispatch_label: str = READY_LABEL,
425
- held: frozenset = frozenset()) -> dict | None:
458
+ held: frozenset = frozenset(),
459
+ timeout: int | None = None,
460
+ raise_on_read_failure: bool = False) -> dict | None:
426
461
  # A merged delivery keeps `ai-ready` + `ai-merged` on the (still
427
462
  # open) Issue; `ai-merged` is the success terminal state, so it is
428
463
  # excluded from the ready scan like every other delivery state.
@@ -456,24 +491,34 @@ def pick_issue(repo: str, active_milestone: str | None = None,
456
491
  # code-layer marker guard to the triage tickets an external
457
492
  # contribution routes to — an external review is supplementary
458
493
  # capacity, never a competitor of this version's deliveries.
494
+ # `timeout` is the CALLER's read bound (the status view's #1668
495
+ # bound); the claim itself reads unbounded. The ready decision is
496
+ # one function for both readers (Issue #1773), so the bound travels
497
+ # with the call instead of a second query being built.
498
+ # `raise_on_read_failure` says what a failed scan read MEANS to the
499
+ # caller: the claim fails open (default), the status observation
500
+ # re-raises so the failure is named instead of reading as an empty
501
+ # queue (`_read_failed`).
459
502
  for search in ready_searches(active_milestone, dispatch_label):
460
503
  try:
461
504
  issues = list_issues(
462
505
  repo, state="open", search=search,
463
506
  json_fields="number,title,body,labels,blockedBy,author", limit=200,
507
+ timeout=timeout,
464
508
  )
465
509
  except Exception as exc:
466
510
  # Fail open: a failed blockedBy query must
467
511
  # never deadlock the queue. This tick claims nothing from
468
512
  # this repo and the next tick retries the query; the error
469
513
  # is logged, never raised, and no label is touched.
470
- event(
471
- "blocked_by_check_failed", level=logging.ERROR,
472
- repo=repo, error=exc,
514
+ _read_failed(
515
+ "blocked_by_check_failed", exc, raise_on_read_failure,
516
+ repo=repo,
473
517
  )
474
518
  return None
475
519
  picked = _pick_from_scan(
476
520
  issues, repo, ready_label=dispatch_label, held=held,
521
+ raise_on_read_failure=raise_on_read_failure,
477
522
  )
478
523
  if picked is not None:
479
524
  return picked
@@ -489,18 +534,18 @@ def pick_issue(repo: str, active_milestone: str | None = None,
489
534
  repo, state="open",
490
535
  search=release_fallback_search(active_milestone, dispatch_label),
491
536
  json_fields="number,title,body,labels,blockedBy,milestone",
492
- limit=200,
537
+ limit=200, timeout=timeout,
493
538
  )
494
539
  except Exception as exc:
495
- event(
496
- "blocked_by_check_failed", level=logging.ERROR,
497
- repo=repo, error=exc,
540
+ _read_failed(
541
+ "blocked_by_check_failed", exc, raise_on_read_failure, repo=repo,
498
542
  )
499
543
  return None
500
544
  picked = _pick_from_scan(
501
545
  issues, repo, allow_release=True, held=held,
502
546
  active_milestone=active_milestone,
503
547
  ready_label=dispatch_label,
548
+ raise_on_read_failure=raise_on_read_failure,
504
549
  )
505
550
  if picked is not None:
506
551
  return picked
@@ -518,17 +563,17 @@ def pick_issue(repo: str, active_milestone: str | None = None,
518
563
  repo, state="open",
519
564
  search=external_takeover_search(dispatch_label),
520
565
  json_fields="number,title,body,labels,blockedBy",
521
- limit=200,
566
+ limit=200, timeout=timeout,
522
567
  )
523
568
  except Exception as exc:
524
- event(
525
- "blocked_by_check_failed", level=logging.ERROR,
526
- repo=repo, error=exc,
569
+ _read_failed(
570
+ "blocked_by_check_failed", exc, raise_on_read_failure, repo=repo,
527
571
  )
528
572
  return None
529
573
  return _pick_from_scan(
530
574
  issues, repo, external_only=True, held=held,
531
575
  ready_label=dispatch_label,
576
+ raise_on_read_failure=raise_on_read_failure,
532
577
  )
533
578
 
534
579
 
@@ -669,6 +714,7 @@ def pick_next_delivery(
669
714
  for repo in repos:
670
715
  repo_language = (language.DEFAULT_LANGUAGE if config is None
671
716
  else merged_repo_language(config, repo))
717
+ _, dispatch_label = _repo_scan_keys(config, repo, active_milestone)
672
718
  # Epic reconciliation is a per-tick bypass: a broken GitHub query or
673
719
  # mutation must never prevent the ordinary delivery scans.
674
720
  try:
@@ -709,6 +755,7 @@ def pick_next_delivery(
709
755
  repo, slot_dir, max_concurrency,
710
756
  repo_dir=config.repo_dir if config is not None else None,
711
757
  hooks=hooks, language_code=repo_language,
758
+ ready_label=dispatch_label,
712
759
  )
713
760
  if selected is not None:
714
761
  issue, scene = selected
@@ -723,7 +770,7 @@ def pick_next_delivery(
723
770
  return repo, issue, None
724
771
  for repo in repos:
725
772
  held = slot_held_deliveries(slot_dir, max_concurrency)
726
- issue = _pick_issue_with_repo_policy(repo, active_milestone, config, held)
773
+ issue = pick_ready_issue(repo, active_milestone, config, held)
727
774
  if issue is not None:
728
775
  return repo, issue, None
729
776
  # Issue #1745: a delivery waiting for a maintainer is re-checked LAST —
@@ -734,10 +781,12 @@ def pick_next_delivery(
734
781
  for repo in repos:
735
782
  repo_language = (language.DEFAULT_LANGUAGE if config is None
736
783
  else merged_repo_language(config, repo))
784
+ _, dispatch_label = _repo_scan_keys(config, repo, active_milestone)
737
785
  selected = pick_awaiting_merge_delivery(
738
786
  repo, slot_dir, max_concurrency,
739
787
  repo_dir=config.repo_dir if config is not None else None,
740
788
  hooks=hooks, language_code=repo_language,
789
+ ready_label=dispatch_label,
741
790
  )
742
791
  if selected is not None:
743
792
  issue, scene = selected
@@ -791,19 +840,66 @@ def _repo_scan_context(
791
840
  )
792
841
 
793
842
 
794
- def _pick_issue_with_repo_policy(
795
- repo: str, active_milestone: str | None, config: RunnerConfig | None,
796
- held: frozenset = frozenset(),
843
+ def _live_held_deliveries(
844
+ config: RunnerConfig | None,
845
+ ) -> frozenset[tuple[str, int]]:
846
+ """The deliveries live co-runners hold RIGHT NOW, from the host config.
847
+
848
+ The claim path reads its slots through `slot_held_deliveries`
849
+ (`pick_next_delivery`); this is the same read for a reader that has
850
+ only the configuration — a host without a readable `slot_dir`
851
+ contributes nothing (fail open: skip only what is provably held).
852
+ """
853
+ if config is None or config.slot_dir is None:
854
+ return frozenset()
855
+ return frozenset(
856
+ slot_held_deliveries(config.slot_dir, config.max_concurrency)
857
+ )
858
+
859
+
860
+ def pick_ready_issue(
861
+ repo: str, active_milestone: str | None = None,
862
+ config: RunnerConfig | None = None,
863
+ held: frozenset | None = None,
864
+ *, timeout: int | None = None, raise_on_read_failure: bool = False,
797
865
  ) -> dict | None:
798
- """Fresh ready scan with the repo's scan keys.
866
+ """The Issue the next fresh claim would pick: THE ready decision.
867
+
868
+ Every reader of "which Issue is ready" asks THIS function, so the
869
+ answer cannot drift into a second query (Issue #1773): the claim
870
+ loop (`pick_next_delivery`) and `orbi status` (`cli.ready_issue`, with
871
+ its own read bound) both arrive here. It carries the claim's own
872
+ searches and delivery-state exclusions (see `ready_searches`), the
873
+ repository policy's scan keys, the Epic/blockedBy/scene guards and
874
+ the release + external-takeover fallbacks.
799
875
 
800
876
  `dispatch_label` and `active_milestone` are resolved from the
801
- repository policy before the scan (see `_repo_scan_keys`), and
802
- `held` (the deliveries live co-runners work on) is passed through.
877
+ repository policy before the scan (see `_repo_scan_keys`), `held`
878
+ (the deliveries live co-runners work on) is passed through, and
879
+ `timeout` bounds every scan's read and `raise_on_read_failure`
880
+ decides what a failed read means to the caller: the claim fails open,
881
+ while `orbi status` re-raises so a broken read is named as a failure
882
+ and never rendered as an empty queue (`_read_failed`).
883
+
884
+ An unset `held` reads the SAME live slots the claim reads, from the
885
+ configuration's own `slot_dir`/`max_concurrency`
886
+ (`_live_held_deliveries`): the status reader must not name a delivery
887
+ a live co-runner already holds during the seconds between that
888
+ runner's slot-identity write and its `ai-in-progress` label (Issue
889
+ #1782). Both readers decide on the same held set, and no held logic
890
+ lives in `cli.py`.
803
891
  """
892
+ if held is None:
893
+ held = _live_held_deliveries(config)
804
894
  milestone, dispatch_label = _repo_scan_keys(
805
895
  config, repo, active_milestone,
806
896
  )
807
897
  if dispatch_label == READY_LABEL:
808
- return pick_issue(repo, milestone, held=held)
809
- return pick_issue(repo, milestone, dispatch_label=dispatch_label, held=held)
898
+ return pick_issue(
899
+ repo, milestone, held=held, timeout=timeout,
900
+ raise_on_read_failure=raise_on_read_failure,
901
+ )
902
+ return pick_issue(
903
+ repo, milestone, dispatch_label=dispatch_label, held=held,
904
+ timeout=timeout, raise_on_read_failure=raise_on_read_failure,
905
+ )
@@ -32,10 +32,12 @@ from pathlib import Path
32
32
 
33
33
  from orbi import (
34
34
  __version__,
35
+ claim,
35
36
  cli_source,
36
37
  config as config_domain,
37
38
  engine_source,
38
39
  git_transport,
40
+ journal as journal_domain,
39
41
  notify,
40
42
  runner,
41
43
  scheduler,
@@ -156,12 +158,15 @@ def dispatch_issue(repo: str, title: str, body: str, *,
156
158
  return url
157
159
 
158
160
 
159
- def list_labeled_issues(repo: str, label: str, state: str = "open",
160
- search: str | None = None, *,
161
+ def list_labeled_issues(repo: str, label: str, state: str = "open", *,
161
162
  timeout: int | None = None) -> list[dict]:
162
- """Return the newest Issues matching a label search (gh lists newest first)."""
163
+ """Return the newest Issue carrying one label (gh lists newest first).
164
+
165
+ The label IS the query: the status view never assembles a search of
166
+ its own. "Which Issue is ready" is the claim's decision (#1773).
167
+ """
163
168
  return list_issues(
164
- repo, state=state, search=search or f"label:{label}",
169
+ repo, state=state, search=f"label:{label}",
165
170
  json_fields="number,title,url,state", limit=1, timeout=timeout,
166
171
  )
167
172
 
@@ -171,17 +176,27 @@ def current_issue(repo: str) -> dict | None:
171
176
  return issues[0] if issues else None
172
177
 
173
178
 
174
- def ready_issue(repo: str) -> dict | None:
175
- """The ready queue, read under the status cycle's bound.
179
+ def ready_issue(repo: str,
180
+ config: config_domain.RunnerConfig) -> dict | None:
181
+ """The Issue the claim would pick, read under the status bound (#1668).
176
182
 
177
- The status view owns this read, so a stalled `gh issue list` fails
178
- the observation within the bound instead of hanging it (Issue #1668).
183
+ The ready decision lives on the claim side (Issue #1773): an Issue in
184
+ a delivery state (ai-pr-opened/ai-fix-needed/ai-merged/ai-blocked) is
185
+ never ready, and the claim's pick/skip lines stay out of the view; an
186
+ unreadable scan fails the observation (#1668), not an empty queue.
187
+ The held set and the Milestone read arrive from the claim (#1782).
179
188
  """
180
- issues = list_labeled_issues(
181
- repo, READY_LABEL, search=f"label:{READY_LABEL} -label:{IN_PROGRESS_LABEL}",
182
- timeout=status_observe.STATUS_READ_TIMEOUT_SECONDS,
183
- )
184
- return issues[0] if issues else None
189
+ journal = journal_domain.LOGGER
190
+ level = journal.level
191
+ journal.setLevel(max(level, logging.WARNING))
192
+ try:
193
+ return claim.pick_ready_issue(
194
+ repo, config.active_milestone, config,
195
+ timeout=status_observe.STATUS_READ_TIMEOUT_SECONDS,
196
+ raise_on_read_failure=True,
197
+ )
198
+ finally:
199
+ journal.setLevel(level)
185
200
 
186
201
 
187
202
  def recent_result(repo: str) -> dict | None:
@@ -204,7 +219,9 @@ def recent_result(repo: str) -> dict | None:
204
219
 
205
220
 
206
221
  def format_issue(issue: dict) -> str:
207
- return f"#{issue['number']} {issue['title']} {issue['url']}"
222
+ """Render `#N title url`; a claim-scan record carries no url (#1773)."""
223
+ url = issue.get("url")
224
+ return f"#{issue['number']} {issue['title']}" + (f" {url}" if url else "")
208
225
 
209
226
 
210
227
  def slot_lines(state_dir: Path, capacity: int) -> list[str]:
@@ -943,7 +960,8 @@ def main(argv: list[str] | None = None) -> int:
943
960
  return 1
944
961
  observer = status_observe.StatusObserver(
945
962
  config, scope,
946
- read_ready=ready_issue, read_result=recent_result,
963
+ read_ready=lambda repo: ready_issue(repo, config),
964
+ read_result=recent_result,
947
965
  read_base=freeze_base,
948
966
  )
949
967