orbi-cli 0.7.8__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.8 → orbi_cli-0.7.9}/PKG-INFO +1 -1
  2. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/pyproject.toml +1 -1
  3. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/__init__.py +1 -1
  4. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/journal.py +3 -1
  5. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/review_merge.py +49 -27
  6. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/run_state.py +19 -0
  7. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/PKG-INFO +1 -1
  8. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_merge_smoke.py +138 -0
  9. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_review_merge.py +103 -15
  10. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/LICENSE +0 -0
  11. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/README.md +0 -0
  12. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/docs/licenses/sustainable-use-license.md +0 -0
  13. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/setup.cfg +0 -0
  14. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/blocked_comment.py +0 -0
  15. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/branch_reclaim.py +0 -0
  16. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/checks.py +0 -0
  17. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/claim.py +0 -0
  18. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/clarify.py +0 -0
  19. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/cli.py +0 -0
  20. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/cli_source.py +0 -0
  21. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/comment_kind.py +0 -0
  22. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/config.py +0 -0
  23. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/delivery_comment.py +0 -0
  24. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/delivery_labels.py +0 -0
  25. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/delivery_scene.py +0 -0
  26. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/engine_source.py +0 -0
  27. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/example_config.toml +0 -0
  28. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/failure.py +0 -0
  29. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/failure_report.py +0 -0
  30. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/gh_response.py +0 -0
  31. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/git_identity.py +0 -0
  32. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/git_transport.py +0 -0
  33. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/github.py +0 -0
  34. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/gitops.py +0 -0
  35. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/human_review.py +0 -0
  36. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/language.py +0 -0
  37. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/language_terms.py +0 -0
  38. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/launchd_deploy.py +0 -0
  39. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/merge_gate.py +0 -0
  40. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/merge_handoff.py +0 -0
  41. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/milestone.py +0 -0
  42. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/milestone_command.py +0 -0
  43. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/notify.py +0 -0
  44. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_activity.py +0 -0
  45. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_command.py +0 -0
  46. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_process.py +0 -0
  47. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_recovery.py +0 -0
  48. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_session.py +0 -0
  49. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pilot_setup.py +0 -0
  50. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pilot_slots.py +0 -0
  51. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress.py +0 -0
  52. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress_display.py +0 -0
  53. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress_outcome.py +0 -0
  54. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress_summary.py +0 -0
  55. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release.py +0 -0
  56. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release_comments.py +0 -0
  57. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release_git.py +0 -0
  58. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release_notes.py +0 -0
  59. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/repo_config.py +0 -0
  60. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/resume_scan.py +0 -0
  61. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/review_rounds.py +0 -0
  62. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/runner.py +0 -0
  63. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/runner_health.py +0 -0
  64. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/scene.py +0 -0
  65. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/scheduler.py +0 -0
  66. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/scheduler_units.py +0 -0
  67. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/session_command.py +0 -0
  68. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/source_pool.py +0 -0
  69. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/status_observe.py +0 -0
  70. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/suggest.py +0 -0
  71. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/systemd_deploy.py +0 -0
  72. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/ticket_command.py +0 -0
  73. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/SOURCES.txt +0 -0
  74. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  75. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  76. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/top_level.txt +0 -0
  77. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_agents_md.py +0 -0
  78. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_apply_label_patch.py +0 -0
  79. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_base_branch_failure.py +0 -0
  80. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_blocked_comment.py +0 -0
  81. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_adapters.py +0 -0
  82. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_delivery.py +0 -0
  83. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_pi.py +0 -0
  84. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_preflight.py +0 -0
  85. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_release.py +0 -0
  86. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_runner.py +0 -0
  87. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_branch_reclaim.py +0 -0
  88. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_failure_issue_workflow.py +0 -0
  89. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_failure_triage.py +0 -0
  90. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_macos_workflow.py +0 -0
  91. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_workflow.py +0 -0
  92. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_clarify.py +0 -0
  93. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_claude_plugin.py +0 -0
  94. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_install.py +0 -0
  95. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_milestone.py +0 -0
  96. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_packaging.py +0 -0
  97. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_source.py +0 -0
  98. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_comment_identity.py +0 -0
  99. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_comment_readers_contract.py +0 -0
  100. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_concurrency_e2e.py +0 -0
  101. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_constitution.py +0 -0
  102. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_coverage_gate.py +0 -0
  103. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_deliver_pr_fakes.py +0 -0
  104. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_delivery_branch_delete.py +0 -0
  105. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_delivery_labels.py +0 -0
  106. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_delivery_scene.py +0 -0
  107. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docker_docs.py +0 -0
  108. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docker_entrypoint.py +0 -0
  109. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docker_image_workflow.py +0 -0
  110. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_i18n.py +0 -0
  111. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_issue_shapes.py +0 -0
  112. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_labels.py +0 -0
  113. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_mermaid.py +0 -0
  114. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_releases.py +0 -0
  115. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_single_source.py +0 -0
  116. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_site.py +0 -0
  117. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_engine_source.py +0 -0
  118. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_failure.py +0 -0
  119. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_failure_report.py +0 -0
  120. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_fix_needed_loop.py +0 -0
  121. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gh_read_retry.py +0 -0
  122. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gh_response.py +0 -0
  123. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_base_smoke.py +0 -0
  124. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_network_retry.py +0 -0
  125. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_transport.py +0 -0
  126. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_github.py +0 -0
  127. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_github_fakes.py +0 -0
  128. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gitops.py +0 -0
  129. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gitops_fakes.py +0 -0
  130. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_human_review.py +0 -0
  131. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_import_graph.py +0 -0
  132. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_install_sh.py +0 -0
  133. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_issue_1367.py +0 -0
  134. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_issue_461.py +0 -0
  135. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_issue_template_config.py +0 -0
  136. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_journal.py +0 -0
  137. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_journal_events.py +0 -0
  138. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_launchd_deploy.py +0 -0
  139. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_license.py +0 -0
  140. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_message_language.py +0 -0
  141. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_milestone_command.py +0 -0
  142. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_notify.py +0 -0
  143. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ops_e2e.py +0 -0
  144. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_orbi.py +0 -0
  145. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_orbi_dashboard.py +0 -0
  146. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_orbi_exporter.py +0 -0
  147. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_patch_ratchet.py +0 -0
  148. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_activity.py +0 -0
  149. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_command.py +0 -0
  150. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_extensions.py +0 -0
  151. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_provider_templates.py +0 -0
  152. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_recovery.py +0 -0
  153. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_session.py +0 -0
  154. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pilot_setup.py +0 -0
  155. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pilot_slots.py +0 -0
  156. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pr_issue_link_workflow.py +0 -0
  157. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_prerequisite_check.py +0 -0
  158. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_progress.py +0 -0
  159. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_progress_wiring.py +0 -0
  160. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_prompt_contract.py +0 -0
  161. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_public_contracts.py +0 -0
  162. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_publish_workflow.py +0 -0
  163. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_release_docs_workflow.py +0 -0
  164. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_release_ticket_templates.py +0 -0
  165. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_repo_config.py +0 -0
  166. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_resume_continue_e2e.py +0 -0
  167. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_resume_e2e.py +0 -0
  168. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_resume_pr.py +0 -0
  169. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_run_artifacts.py +0 -0
  170. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_run_id_e2e.py +0 -0
  171. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_runner_config.py +0 -0
  172. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_runner_health.py +0 -0
  173. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_runner_source_freshness.py +0 -0
  174. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_scene.py +0 -0
  175. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_scheduler.py +0 -0
  176. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_size_ratchet.py +0 -0
  177. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_src_layout.py +0 -0
  178. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_status_observe.py +0 -0
  179. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_steering_review.py +0 -0
  180. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_suggest.py +0 -0
  181. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_suite_hygiene.py +0 -0
  182. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_systemd_deploy.py +0 -0
  183. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_systemd_timer.py +0 -0
  184. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ticket_command.py +0 -0
  185. {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_user_acceptance_contract.py +0 -0
  186. {orbi_cli-0.7.8 → 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.8
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.8"
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.8"
21
+ __version__ = "0.7.9"
@@ -364,9 +364,11 @@ JOURNAL_EVENTS: dict[str, str] = {
364
364
  "merge_gate_issue_blocked": "the source Issue is ai-blocked; the merge was skipped (Issue #1504)",
365
365
  "merge_gate_rulesets_unavailable": "the rulesets API is unavailable on the repository plan (Issue #1361)",
366
366
  "merged": "the PR merge landed on the reviewed head",
367
+ "merge_landed_after_error": "the merge call failed but the reviewed head landed on the base branch (Issue #1798)",
367
368
  "merge_method_unknown": "the repository merge settings could not be read; --merge is used",
368
369
  "confirm_merged_missing_on_base": "the merged commit is missing from the base branch",
369
- "confirm_merged_not_merged": "the PR reports merged but GitHub contradicts the state",
370
+ "confirm_merged_not_merged": "the PR is not merged and its reviewed head is not on the base branch",
371
+ "confirm_merged_landed_on_base": "the reviewed head landed on the base branch while the PR still reads unmerged (Issue #1798)",
370
372
  # Human review gate.
371
373
  "human_review_waiting": "the delivery waits for the human review label",
372
374
  "human_review_checklist_failed": "publishing the human review checklist failed (bypass)",
@@ -673,20 +673,32 @@ def merge_gate(worktree: Path, pr: dict, base_branch: str,
673
673
  "--match-head-commit", pr["head_oid"], merge_method,
674
674
  ], cwd=worktree)
675
675
  except subprocess.CalledProcessError as exc:
676
- preflight = github.merge_gate_preflight(
677
- merge_repo, base_branch,
678
- )
679
- stderr = str(exc.stderr or "")
680
- if is_maintainer_actionable(stderr, preflight):
681
- failed_preflight = [
682
- line for line in preflight
683
- if line.startswith("merge_gate: FAILED")
684
- ]
685
- raise MergeHandoffRequired(
686
- f"PR #{pr['number']} is ready for the named maintainer action",
687
- preflight=failed_preflight,
688
- ) from None
689
- raise
676
+ # Issue #1798: GitHub can create the merge commit and this call still
677
+ # fail (`502 Bad Gateway`), leaving the PR OPEN with `mergedAt: null`.
678
+ # `confirm_merged` decides: landed continues like a success (never a
679
+ # second merge call), "not landed" keeps today's path.
680
+ try:
681
+ confirm_merged(worktree, pr, base_branch, repo_dir=repo_dir)
682
+ landed = True
683
+ except RuntimeError:
684
+ landed = False
685
+ if not landed:
686
+ preflight = github.merge_gate_preflight(merge_repo, base_branch)
687
+ stderr = str(exc.stderr or "")
688
+ if is_maintainer_actionable(stderr, preflight):
689
+ failed_preflight = [
690
+ line for line in preflight
691
+ if line.startswith("merge_gate: FAILED")
692
+ ]
693
+ raise MergeHandoffRequired(
694
+ f"PR #{pr['number']} is ready for the named maintainer "
695
+ "action",
696
+ preflight=failed_preflight,
697
+ ) from None
698
+ raise
699
+ event("merge_landed_after_error", level=logging.WARNING,
700
+ pr=pr["number"], head=pr["head_oid"], method=merge_method,
701
+ returncode=exc.returncode, stderr=str(exc.stderr or "").strip())
690
702
  event("merged", pr=pr["number"], head=pr["head_oid"],
691
703
  method=merge_method)
692
704
  delete_merged_delivery_branch(merge_repo, pr["number"], pr["head_ref"])
@@ -695,27 +707,37 @@ def merge_gate(worktree: Path, pr: dict, base_branch: str,
695
707
 
696
708
  def confirm_merged(worktree: Path, pr: dict, base_branch: str,
697
709
  *, repo_dir: Path) -> dict:
698
- """Confirm the PR is MERGED and origin/<base> contains the merge commit.
710
+ """Confirm the reviewed PR's merge landed on origin/<base>.
711
+
712
+ A MERGED PR answers with its merge commit, which must be on the base
713
+ branch. Issue #1798: the merge can also land while the merge call
714
+ itself errors, leaving the PR OPEN/CLOSED with `mergedAt: null` — the
715
+ reviewed head being on origin/<base> after the fresh fetch is that
716
+ merge. Anything else is a rejection; a failed read propagates.
699
717
 
700
- The base fetch updates the shared remote-tracking ref, so it runs
701
- under the base-sync lock with the deployment checkout
702
- as the lock location.
718
+ The base fetch updates the shared remote-tracking ref, so it runs under
719
+ the base-sync lock with the deployment checkout as the lock location.
703
720
  """
704
721
  state = pr_view(pr["number"], "state,mergedAt,mergeCommit", cwd=worktree)
705
- if state.get("state") != "MERGED" or not state.get("mergedAt"):
706
- event(
707
- "confirm_merged_not_merged", level=logging.ERROR,
708
- pr=pr["number"], state=state.get("state"),
709
- )
710
- raise RuntimeError(
711
- f"PR #{pr['number']} is not merged (state={state.get('state')})"
712
- )
722
+ merged = state.get("state") == "MERGED" and bool(state.get("mergedAt"))
713
723
  merge_commit = (state.get("mergeCommit") or {}).get("oid")
714
- if not merge_commit:
724
+ if merged and not merge_commit:
715
725
  raise RuntimeError(
716
726
  f"PR #{pr['number']} is merged but has no merge commit oid"
717
727
  )
718
728
  fetch_base_ref(repo_dir, base_branch, cwd=worktree)
729
+ if not merged:
730
+ # Issue #1798: it can have landed while the merge call errored.
731
+ merge_commit = run_state.landed_merge_commit(
732
+ pr["head_oid"], f"origin/{base_branch}", cwd=worktree)
733
+ if merge_commit is None:
734
+ event("confirm_merged_not_merged", level=logging.ERROR,
735
+ pr=pr["number"], state=state.get("state"))
736
+ raise RuntimeError(f"PR #{pr['number']} is not merged "
737
+ f"(state={state.get('state')})")
738
+ event("confirm_merged_landed_on_base", pr=pr["number"],
739
+ head=pr["head_oid"], merge_commit=merge_commit)
740
+ return {"state": "MERGED", "merge_commit": merge_commit}
719
741
  if not _is_ancestor(merge_commit, f"origin/{base_branch}", cwd=worktree):
720
742
  event(
721
743
  "confirm_merged_missing_on_base", level=logging.ERROR,
@@ -58,6 +58,25 @@ def assess_base_freshness(worktree: Path, base_branch: str, *,
58
58
  return BaseFreshness.ABSORBABLE
59
59
 
60
60
 
61
+ def landed_merge_commit(head: str, base_ref: str, *,
62
+ cwd: Path) -> str | None:
63
+ """The commit that landed *head* on *base_ref*, or None (Issue #1798).
64
+
65
+ GitHub can create the merge commit while the merge call itself fails
66
+ (`502 Bad Gateway`), so the PR state alone cannot show that merge: the
67
+ head being reachable from the base branch is it. `--topo-order
68
+ --reverse` lists parents before children on that ancestry path, so the
69
+ first commit is the one that landed the head.
70
+ """
71
+ if not _is_ancestor(head, base_ref, cwd=cwd):
72
+ return None
73
+ commits = run_command([
74
+ "git", "rev-list", "--ancestry-path", "--topo-order", "--reverse",
75
+ f"{head}..{base_ref}",
76
+ ], cwd=cwd).split()
77
+ return commits[0] if commits else None
78
+
79
+
61
80
  def delivery_commit_guard(worktree: Path, *, local_head: str,
62
81
  base_sha: str, branch: str) -> None:
63
82
  """Fail the delivery when the task branch carries no real change.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orbi-cli
3
- Version: 0.7.8
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
@@ -307,3 +307,141 @@ def test_sync_base_checkout_fails_fast_on_drifted_checkout(
307
307
 
308
308
  with pytest.raises(RuntimeError, match="cannot fast-forward"):
309
309
  review_merge.sync_base_checkout(checkout, "main")
310
+
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # Issue #1798: a merge that landed despite an error from the merge call
314
+ # ---------------------------------------------------------------------------
315
+
316
+ def install_failing_merge_gh(monkeypatch, clone: Path, head_oid: str, *,
317
+ landed: bool = True, pr_after=None) -> list:
318
+ """Run real git; `gh pr merge` lands a real merge and THEN fails.
319
+
320
+ This is the Issue #1798 scene (orbi-build/orbi-cloud PR #2652): GitHub
321
+ creates the merge commit on the base branch and the merge call itself
322
+ returns `502 Bad Gateway`. `landed=False` models the same error with
323
+ nothing on the base branch. `pr_after` answers `gh pr view` once the
324
+ merge call has failed; before it the reviewed head reads OPEN and
325
+ mergeable.
326
+ """
327
+ real_run = runner.run_command
328
+ calls: list = []
329
+ failed = False
330
+
331
+ def fake_run(command, **kwargs):
332
+ nonlocal failed
333
+ if command[0] == "gh" and command[1:3] == ["pr", "view"]:
334
+ calls.append(command)
335
+ if failed and pr_after is not None:
336
+ return pr_after()
337
+ return make_pr(head_oid)
338
+ if command[0] == "gh" and command[1] == "pr" and "merge" in command:
339
+ calls.append(command)
340
+ if landed:
341
+ git(clone, "checkout", "main")
342
+ git(clone, "merge", "--no-ff", command[command.index(
343
+ "--match-head-commit") + 1])
344
+ git(clone, "push", "origin", "main")
345
+ git(clone, "checkout", "-")
346
+ failed = True
347
+ raise subprocess.CalledProcessError(
348
+ 1, command, stderr="HTTP 502: Bad Gateway",
349
+ )
350
+ if command[0] == "gh":
351
+ calls.append(command)
352
+ return ""
353
+ return real_run(command, **kwargs)
354
+
355
+ monkeypatch.setattr(seam, "run_command", fake_run)
356
+ return calls
357
+
358
+
359
+ def _delivery_pr(clone: Path, head_oid: str) -> dict:
360
+ return {"number": 4, "url": "u", "base_ref": "main",
361
+ "base_oid": git(clone, "rev-parse", "origin/main"),
362
+ "head_ref": "orbi/owner-repo-issue-4", "head_oid": head_oid}
363
+
364
+
365
+ def test_merge_gate_accepts_a_merge_that_landed_despite_a_502(
366
+ clone, monkeypatch, caplog):
367
+ """Issue #1798 acceptance 1: `gh pr merge` fails 5xx and the PR then
368
+ reads MERGED with its merge commit on origin/<base> — the delivery
369
+ continues like a successful merge instead of a fix-needed round."""
370
+ git(clone, "checkout", "-b", "orbi/owner-repo-issue-4")
371
+ head_oid = commit_file(clone, "delivery.txt", "delivery")
372
+ calls = install_failing_merge_gh(
373
+ monkeypatch, clone, head_oid,
374
+ pr_after=lambda: make_pr(
375
+ head_oid, state="MERGED", merged_at="2026-10-19T13:38:31Z",
376
+ merge_commit=git(clone, "rev-parse", "origin/main"),
377
+ ),
378
+ )
379
+ with caplog.at_level("WARNING"):
380
+ merged = review_merge.merge_gate(
381
+ clone, _delivery_pr(clone, head_oid), "main",
382
+ repo_dir=clone, language_code="en",
383
+ )
384
+ assert merged["merged"] is True
385
+ assert "merge_landed_after_error" in caplog.text
386
+ # Still exactly one merge call: the error is confirmed, never retried.
387
+ assert len([c for c in calls
388
+ if c[0] == "gh" and c[1] == "pr" and "merge" in c]) == 1
389
+ merge_commit = git(clone, "rev-parse", "origin/main")
390
+ confirmed = review_merge.confirm_merged(clone, merged, "main",
391
+ repo_dir=clone)
392
+ assert confirmed == {"state": "MERGED", "merge_commit": merge_commit}
393
+
394
+
395
+ def test_merge_gate_accepts_a_landed_head_while_the_pr_reads_open(
396
+ clone, monkeypatch, caplog):
397
+ """Issue #1798 acceptance 2 (PR #2652's shape): GitHub created the merge
398
+ commit on the base branch and never marked the PR merged, so re-reading
399
+ the PR state cannot recognise the merge. The reviewed head being
400
+ reachable from origin/<base> does, and the landed merge commit is the
401
+ one the delivery records."""
402
+ git(clone, "checkout", "-b", "orbi/owner-repo-issue-4")
403
+ head_oid = commit_file(clone, "delivery.txt", "delivery")
404
+ calls = install_failing_merge_gh(monkeypatch, clone, head_oid)
405
+ with caplog.at_level("INFO"):
406
+ merged = review_merge.merge_gate(
407
+ clone, _delivery_pr(clone, head_oid), "main",
408
+ repo_dir=clone, language_code="en",
409
+ )
410
+ assert merged["merged"] is True
411
+ assert len([c for c in calls
412
+ if c[0] == "gh" and c[1] == "pr" and "merge" in c]) == 1
413
+ merge_commit = git(clone, "rev-parse", "origin/main")
414
+ # The commit that landed the reviewed head is a real merge commit whose
415
+ # second parent is that head.
416
+ assert git(clone, "rev-parse", f"{merge_commit}^2") == head_oid
417
+ with caplog.at_level("INFO"):
418
+ confirmed = review_merge.confirm_merged(clone, merged, "main",
419
+ repo_dir=clone)
420
+ assert confirmed == {"state": "MERGED", "merge_commit": merge_commit}
421
+ assert "confirm_merged_landed_on_base" in caplog.text
422
+
423
+
424
+ def test_merge_gate_still_fails_when_the_merge_did_not_land(
425
+ clone, monkeypatch):
426
+ """Issue #1798 acceptance 3: the same 5xx with the reviewed head NOT on
427
+ origin/<base> keeps today's outcome — the error surfaces."""
428
+ git(clone, "checkout", "-b", "orbi/owner-repo-issue-4")
429
+ head_oid = commit_file(clone, "delivery.txt", "delivery")
430
+ pr = _delivery_pr(clone, head_oid)
431
+ calls = install_failing_merge_gh(monkeypatch, clone, head_oid,
432
+ landed=False)
433
+ monkeypatch.setattr(
434
+ seam, "merge_gate_preflight",
435
+ lambda *args: ["merge_gate: PASS protection readable"],
436
+ )
437
+ with pytest.raises(subprocess.CalledProcessError) as raised:
438
+ review_merge.merge_gate(
439
+ clone, pr, "main", repo_dir=clone, language_code="en",
440
+ )
441
+ assert raised.value.returncode == 1
442
+ assert "502" in (raised.value.stderr or "")
443
+ assert len([c for c in calls
444
+ if c[0] == "gh" and c[1] == "pr" and "merge" in c]) == 1
445
+ # Nothing landed: the base branch is untouched.
446
+ assert git(clone, "rev-parse", "origin/main") == pr["base_oid"]
447
+
@@ -1676,6 +1676,13 @@ def test_merge_gate_hands_off_each_actionable_policy_rejection(
1676
1676
  raise subprocess.CalledProcessError(
1677
1677
  1, command, stderr="the base branch policy prohibits the merge",
1678
1678
  )
1679
+ if command[0] == "git" and command[1] == "merge-base":
1680
+ # The head contains the base, but the base does NOT contain
1681
+ # the reviewed head: this merge did not land (Issue #1798 asks
1682
+ # the ancestry question before treating the error as final).
1683
+ if command[3] == "origin/main":
1684
+ return ""
1685
+ raise subprocess.CalledProcessError(1, command, stderr="no")
1679
1686
  return ""
1680
1687
  monkeypatch.setattr(seam, "run_command", fake_run)
1681
1688
  monkeypatch.setattr(runner.github, "merge_gate_preflight", lambda *a: [
@@ -1697,15 +1704,25 @@ def test_merge_gate_hands_off_each_actionable_policy_rejection(
1697
1704
  ])
1698
1705
  def test_merge_gate_does_not_hand_off_without_failed_preflight(
1699
1706
  monkeypatch, tmp_path, preflight):
1700
- monkeypatch.setattr(seam, "run_command", lambda command, **kwargs: (
1701
- (_ for _ in ()).throw(subprocess.CalledProcessError(
1702
- 1, command, stderr="the base branch policy prohibits the merge",
1703
- )) if command[0] == "gh" and command[1] == "pr" and "merge" in command else (
1704
- json.dumps({"state": "OPEN", "mergeable": "MERGEABLE",
1705
- "headRefOid": "h1", "statusCheckRollup": []})
1706
- if command[0] == "gh" and command[1] == "pr" and "view" in command else ""
1707
- )
1708
- ))
1707
+ def fake_run(command, **kwargs):
1708
+ if command[0] == "gh" and command[1] == "pr" and "merge" in command:
1709
+ raise subprocess.CalledProcessError(
1710
+ 1, command, stderr="the base branch policy prohibits the merge",
1711
+ )
1712
+ if command[0] == "gh" and command[1] == "pr" and "view" in command:
1713
+ return json.dumps({
1714
+ "state": "OPEN", "mergeable": "MERGEABLE", "headRefOid": "h1",
1715
+ "statusCheckRollup": [],
1716
+ })
1717
+ if command[0] == "git" and command[1] == "merge-base":
1718
+ # The head contains the base, but the base does NOT contain the
1719
+ # reviewed head: the failed merge did not land (Issue #1798).
1720
+ if command[3] == "origin/main":
1721
+ return ""
1722
+ raise subprocess.CalledProcessError(1, command, stderr="no")
1723
+ return ""
1724
+
1725
+ monkeypatch.setattr(seam, "run_command", fake_run)
1709
1726
  monkeypatch.setattr(
1710
1727
  runner.github, "merge_gate_preflight", lambda *a: preflight,
1711
1728
  )
@@ -2009,12 +2026,21 @@ def test_confirm_merged_fetches_under_the_base_sync_lock(
2009
2026
 
2010
2027
 
2011
2028
  def test_confirm_merged_rejects_unmerged_pr(monkeypatch, tmp_path):
2012
- monkeypatch.setattr(seam, "run_command",
2013
- lambda command, **kwargs: json.dumps({
2014
- "number": 4, "url": "u", "state": "OPEN",
2015
- "mergedAt": None, "mergeCommit": None,
2016
- }),
2017
- )
2029
+ """Issue #1798: an unmerged PR whose reviewed head is NOT on
2030
+ origin/<base> is still a failed confirm — the head's ancestry is the
2031
+ fact that decides it, so this fixture answers it (the reviewed head is
2032
+ not reachable from origin/main)."""
2033
+ def fake_run(command, **kwargs):
2034
+ if command[0] == "gh" and command[1] == "pr" and "view" in command:
2035
+ return json.dumps({
2036
+ "number": 4, "url": "u", "state": "OPEN",
2037
+ "mergedAt": None, "mergeCommit": None,
2038
+ })
2039
+ if command[0] == "git" and command[1] == "merge-base":
2040
+ raise subprocess.CalledProcessError(1, command, stderr="not ancestor")
2041
+ return ""
2042
+
2043
+ monkeypatch.setattr(seam, "run_command", fake_run)
2018
2044
  with pytest.raises(RuntimeError, match="not merged"):
2019
2045
  review_merge.confirm_merged(
2020
2046
  tmp_path, {"number": 4, "url": "u", "base_ref": "main",
@@ -2023,6 +2049,68 @@ def test_confirm_merged_rejects_unmerged_pr(monkeypatch, tmp_path):
2023
2049
  )
2024
2050
 
2025
2051
 
2052
+ def test_confirm_merged_accepts_a_reviewed_head_that_landed_on_base(
2053
+ monkeypatch, tmp_path, caplog):
2054
+ """Issue #1798: GitHub creates the merge commit and the merge call
2055
+ still fails, so the PR reads OPEN with `mergedAt: null` and no merge
2056
+ commit. The reviewed head being reachable from origin/<base> is that
2057
+ landed merge; the commit that landed it is the answer.
2058
+
2059
+ `--topo-order --reverse` lists parents before children on the
2060
+ ancestry path, so the first commit is the one that landed the head —
2061
+ verified against a real repository in test_git_merge_smoke.py."""
2062
+ commands = []
2063
+
2064
+ def fake_run(command, **kwargs):
2065
+ commands.append(command)
2066
+ if command[0] == "gh" and command[1] == "pr" and "view" in command:
2067
+ return json.dumps({
2068
+ "number": 4, "url": "u", "state": "OPEN",
2069
+ "mergedAt": None, "mergeCommit": None,
2070
+ })
2071
+ if command[0] == "git" and command[1] == "rev-list":
2072
+ return "m1\nlater\n"
2073
+ return ""
2074
+
2075
+ monkeypatch.setattr(seam, "run_command", fake_run)
2076
+ with caplog.at_level("INFO"):
2077
+ result = review_merge.confirm_merged(
2078
+ tmp_path, {"number": 4, "head_oid": "h1"}, "main",
2079
+ repo_dir=tmp_path,
2080
+ )
2081
+ assert result == {"state": "MERGED", "merge_commit": "m1"}
2082
+ rev_list = [c for c in commands if c[1:2] == ["rev-list"]][0]
2083
+ assert rev_list[2:] == [
2084
+ "--ancestry-path", "--topo-order", "--reverse", "h1..origin/main",
2085
+ ]
2086
+ assert "confirm_merged_landed_on_base" in caplog.text
2087
+
2088
+
2089
+ def test_confirm_merged_needs_a_landing_commit_for_an_open_pr(
2090
+ monkeypatch, tmp_path, caplog):
2091
+ """Issue #1798: a head that is an ancestor of origin/<base> with no
2092
+ commit between them names no merge commit, so the confirm stays a
2093
+ rejection instead of inventing one."""
2094
+ def fake_run(command, **kwargs):
2095
+ if command[0] == "gh" and command[1] == "pr" and "view" in command:
2096
+ return json.dumps({
2097
+ "number": 4, "url": "u", "state": "OPEN",
2098
+ "mergedAt": None, "mergeCommit": None,
2099
+ })
2100
+ return ""
2101
+
2102
+ monkeypatch.setattr(seam, "run_command", fake_run)
2103
+ with caplog.at_level("ERROR"), pytest.raises(
2104
+ RuntimeError, match="not merged",
2105
+ ):
2106
+ review_merge.confirm_merged(
2107
+ tmp_path, {"number": 4, "head_oid": "h1"}, "main",
2108
+ repo_dir=tmp_path,
2109
+ )
2110
+ assert "confirm_merged_not_merged" in caplog.text
2111
+ assert "confirm_merged_landed_on_base" not in caplog.text
2112
+
2113
+
2026
2114
  def test_confirm_merged_rejects_merge_commit_missing_from_origin_main(
2027
2115
  monkeypatch, tmp_path, caplog):
2028
2116
  def fake_run(command, **kwargs):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes