orbi-cli 0.7.3__tar.gz → 0.7.5__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 (184) hide show
  1. {orbi_cli-0.7.3/src/orbi_cli.egg-info → orbi_cli-0.7.5}/PKG-INFO +1 -1
  2. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/pyproject.toml +1 -1
  3. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/__init__.py +1 -1
  4. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/claim.py +42 -232
  5. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/delivery_comment.py +5 -1
  6. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/failure.py +38 -2
  7. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/language.py +18 -0
  8. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/language_terms.py +5 -0
  9. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/merge_handoff.py +7 -4
  10. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/progress.py +2 -1
  11. orbi_cli-0.7.5/src/orbi/resume_scan.py +314 -0
  12. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/runner.py +105 -97
  13. {orbi_cli-0.7.3 → orbi_cli-0.7.5/src/orbi_cli.egg-info}/PKG-INFO +1 -1
  14. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi_cli.egg-info/SOURCES.txt +2 -0
  15. orbi_cli-0.7.5/tests/test_base_branch_failure.py +216 -0
  16. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_bootstrap_delivery.py +7 -0
  17. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_bootstrap_runner.py +6 -0
  18. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_delivery_scene.py +4 -4
  19. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docs_labels.py +10 -2
  20. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_failure.py +75 -0
  21. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_message_language.py +64 -1
  22. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pilot_setup.py +6 -1
  23. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_resume_pr.py +194 -20
  24. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/LICENSE +0 -0
  25. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/README.md +0 -0
  26. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/docs/licenses/sustainable-use-license.md +0 -0
  27. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/setup.cfg +0 -0
  28. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/blocked_comment.py +0 -0
  29. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/branch_reclaim.py +0 -0
  30. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/checks.py +0 -0
  31. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/clarify.py +0 -0
  32. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/cli.py +0 -0
  33. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/cli_source.py +0 -0
  34. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/comment_kind.py +0 -0
  35. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/config.py +0 -0
  36. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/delivery_labels.py +0 -0
  37. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/delivery_scene.py +0 -0
  38. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/engine_source.py +0 -0
  39. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/example_config.toml +0 -0
  40. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/failure_report.py +0 -0
  41. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/git_identity.py +0 -0
  42. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/git_transport.py +0 -0
  43. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/github.py +0 -0
  44. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/gitops.py +0 -0
  45. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/human_review.py +0 -0
  46. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/journal.py +0 -0
  47. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/launchd_deploy.py +0 -0
  48. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/merge_gate.py +0 -0
  49. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/milestone.py +0 -0
  50. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/milestone_command.py +0 -0
  51. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/notify.py +0 -0
  52. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/pi_activity.py +0 -0
  53. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/pi_command.py +0 -0
  54. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/pi_process.py +0 -0
  55. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/pi_recovery.py +0 -0
  56. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/pi_session.py +0 -0
  57. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/pilot_setup.py +0 -0
  58. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/pilot_slots.py +0 -0
  59. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/progress_display.py +0 -0
  60. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/progress_outcome.py +0 -0
  61. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/progress_summary.py +0 -0
  62. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/release.py +0 -0
  63. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/release_comments.py +0 -0
  64. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/release_git.py +0 -0
  65. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/release_notes.py +0 -0
  66. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/repo_config.py +0 -0
  67. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/review_merge.py +0 -0
  68. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/review_rounds.py +0 -0
  69. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/run_state.py +0 -0
  70. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/runner_health.py +0 -0
  71. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/scene.py +0 -0
  72. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/scheduler.py +0 -0
  73. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/scheduler_units.py +0 -0
  74. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/session_command.py +0 -0
  75. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/source_pool.py +0 -0
  76. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/status_observe.py +0 -0
  77. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/suggest.py +0 -0
  78. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/systemd_deploy.py +0 -0
  79. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi/ticket_command.py +0 -0
  80. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  81. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  82. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/src/orbi_cli.egg-info/top_level.txt +0 -0
  83. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_agents_md.py +0 -0
  84. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_apply_label_patch.py +0 -0
  85. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_blocked_comment.py +0 -0
  86. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_bootstrap_adapters.py +0 -0
  87. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_bootstrap_pi.py +0 -0
  88. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_bootstrap_preflight.py +0 -0
  89. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_bootstrap_release.py +0 -0
  90. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_branch_reclaim.py +0 -0
  91. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_ci_failure_issue_workflow.py +0 -0
  92. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_ci_failure_triage.py +0 -0
  93. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_ci_macos_workflow.py +0 -0
  94. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_ci_workflow.py +0 -0
  95. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_clarify.py +0 -0
  96. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_claude_plugin.py +0 -0
  97. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_cli_install.py +0 -0
  98. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_cli_milestone.py +0 -0
  99. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_cli_packaging.py +0 -0
  100. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_cli_source.py +0 -0
  101. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_comment_identity.py +0 -0
  102. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_comment_readers_contract.py +0 -0
  103. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_concurrency_e2e.py +0 -0
  104. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_constitution.py +0 -0
  105. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_coverage_gate.py +0 -0
  106. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_deliver_pr_fakes.py +0 -0
  107. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_delivery_branch_delete.py +0 -0
  108. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_delivery_labels.py +0 -0
  109. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docker_docs.py +0 -0
  110. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docker_entrypoint.py +0 -0
  111. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docker_image_workflow.py +0 -0
  112. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docs_i18n.py +0 -0
  113. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docs_issue_shapes.py +0 -0
  114. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docs_mermaid.py +0 -0
  115. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docs_releases.py +0 -0
  116. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docs_single_source.py +0 -0
  117. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_docs_site.py +0 -0
  118. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_engine_source.py +0 -0
  119. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_failure_report.py +0 -0
  120. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_fix_needed_loop.py +0 -0
  121. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_gh_read_retry.py +0 -0
  122. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_git_base_smoke.py +0 -0
  123. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_git_merge_smoke.py +0 -0
  124. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_git_network_retry.py +0 -0
  125. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_git_transport.py +0 -0
  126. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_github.py +0 -0
  127. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_github_fakes.py +0 -0
  128. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_gitops.py +0 -0
  129. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_gitops_fakes.py +0 -0
  130. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_human_review.py +0 -0
  131. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_import_graph.py +0 -0
  132. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_install_sh.py +0 -0
  133. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_issue_1367.py +0 -0
  134. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_issue_461.py +0 -0
  135. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_issue_template_config.py +0 -0
  136. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_journal.py +0 -0
  137. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_journal_events.py +0 -0
  138. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_launchd_deploy.py +0 -0
  139. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_license.py +0 -0
  140. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_milestone_command.py +0 -0
  141. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_notify.py +0 -0
  142. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_ops_e2e.py +0 -0
  143. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_orbi.py +0 -0
  144. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_orbi_dashboard.py +0 -0
  145. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_orbi_exporter.py +0 -0
  146. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_patch_ratchet.py +0 -0
  147. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pi_activity.py +0 -0
  148. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pi_command.py +0 -0
  149. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pi_extensions.py +0 -0
  150. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pi_provider_templates.py +0 -0
  151. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pi_recovery.py +0 -0
  152. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pi_session.py +0 -0
  153. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pilot_slots.py +0 -0
  154. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_pr_issue_link_workflow.py +0 -0
  155. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_prerequisite_check.py +0 -0
  156. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_progress.py +0 -0
  157. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_progress_wiring.py +0 -0
  158. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_prompt_contract.py +0 -0
  159. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_public_contracts.py +0 -0
  160. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_publish_workflow.py +0 -0
  161. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_release_docs_workflow.py +0 -0
  162. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_release_ticket_templates.py +0 -0
  163. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_repo_config.py +0 -0
  164. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_resume_continue_e2e.py +0 -0
  165. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_resume_e2e.py +0 -0
  166. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_review_merge.py +0 -0
  167. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_run_artifacts.py +0 -0
  168. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_run_id_e2e.py +0 -0
  169. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_runner_config.py +0 -0
  170. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_runner_health.py +0 -0
  171. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_runner_source_freshness.py +0 -0
  172. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_scene.py +0 -0
  173. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_scheduler.py +0 -0
  174. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_size_ratchet.py +0 -0
  175. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_src_layout.py +0 -0
  176. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_status_observe.py +0 -0
  177. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_steering_review.py +0 -0
  178. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_suggest.py +0 -0
  179. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_suite_hygiene.py +0 -0
  180. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_systemd_deploy.py +0 -0
  181. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_systemd_timer.py +0 -0
  182. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_ticket_command.py +0 -0
  183. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/tests/test_user_acceptance_contract.py +0 -0
  184. {orbi_cli-0.7.3 → orbi_cli-0.7.5}/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.3
3
+ Version: 0.7.5
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.3"
47
+ version = "0.7.5"
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.3"
21
+ __version__ = "0.7.5"
@@ -4,7 +4,8 @@ Extracted from `orbi.runner` (Issue #1259, Article 3.2): the five claim scans
4
4
  (P0, bug, plain, release fallback, external takeover), the classifiers and
5
5
  reconcilers they share, and the resumable-PR / in-flight / fresh-claim
6
6
  selection order. The module never imports `orbi.runner` (Article 3.3): the
7
- delivery-side helpers arrive as `ResumeHooks`.
7
+ delivery-side helpers arrive as `ResumeHooks`. The opened-PR candidate scan
8
+ itself lives in `orbi.resume_scan` (Article 3.5 split, Issue #1229).
8
9
  """
9
10
  from __future__ import annotations
10
11
 
@@ -12,17 +13,13 @@ import json
12
13
  import logging
13
14
  import os
14
15
  import re
15
- from collections.abc import Callable, Sequence
16
+ from collections.abc import Sequence
16
17
  from pathlib import Path
17
- from typing import NamedTuple
18
18
 
19
19
  from orbi import milestone as milestone_bookkeeping
20
- from orbi import scene
21
- from orbi.blocked_comment import post_blocked_comment
22
- from orbi.checks import _classify_rollup, _render_check
23
20
  from orbi.config import RunnerConfig
24
21
  from orbi.delivery_labels import (
25
- AWAITING_MERGE_LABEL, BLOCKED_LABEL, EPIC_LABEL,
22
+ BLOCKED_LABEL, EPIC_LABEL,
26
23
  FIX_NEEDED_LABEL, IN_PROGRESS_LABEL, MERGED_LABEL, P0_LABEL,
27
24
  PR_OPENED_LABEL, READY_LABEL, RELEASE_LABEL,
28
25
  )
@@ -32,13 +29,15 @@ from orbi.delivery_scene import (
32
29
  from orbi.github import (
33
30
  _verify_epic_complete, close_issue,
34
31
  comment_issue, issue_comments, issue_priority, issue_view,
35
- latest_run_marker, list_issues, milestone_open_issue_count,
36
- open_blocker_numbers, open_pr_for_branch, pr_comments,
37
- pr_delivery_rollup, run_gh_read_command,
32
+ list_issues, milestone_open_issue_count,
33
+ open_blocker_numbers, pr_comments,
34
+ run_gh_read_command,
38
35
  )
39
- from orbi.gitops import task_branch
40
36
  from orbi.journal import LOGGER, current_run_id, event, new_run_id, set_run_id
41
37
  from orbi.pilot_slots import slot_held_deliveries, slot_occupancy
38
+ from orbi.resume_scan import (
39
+ ResumeHooks, pick_awaiting_merge_delivery, pick_resumable_delivery,
40
+ )
42
41
  from orbi import language
43
42
  from orbi.progress import run_marker
44
43
  from orbi.release import _epic_audit, epic_audit_posted, release_target_milestone
@@ -47,23 +46,6 @@ from orbi.repo_config import (
47
46
  )
48
47
 
49
48
 
50
- class ResumeHooks(NamedTuple):
51
- """The delivery-side helpers `runner.main` passes in (Article 3.3).
52
-
53
- The physical scene handling of `pick_resumable_delivery` plus the PR
54
- comment writer `reconcile_orphan_prs` reports its orphan through.
55
- Every hook that writes a comment takes the claim's already-resolved
56
- per-repository `language_code` (Issue #1687): the claim knows the
57
- merged repository config before the first comment is written.
58
- """
59
- resume_scene: Callable[[list[dict]], dict]
60
- route_external_pr_ticket: Callable[..., bool]
61
- block_scene_failure: Callable[..., None]
62
- recover_missing_pr_scene: Callable[..., dict | None]
63
- has_recoverable_pr_scene: Callable[[dict, str, Path], bool]
64
- comment_pr: Callable[..., None]
65
-
66
-
67
49
  # Ready scans: P0 urgent Issues are claimed before
68
50
  # bugs, bugs before new features — if the delivery loop is broken,
69
51
  # claiming enhancements only piles up unreviewed PRs, and a production
@@ -632,7 +614,13 @@ def pick_in_progress_issue(
632
614
  def pick_next_issue(
633
615
  repos: list[str], active_milestone: str | None = None,
634
616
  ) -> tuple[str, dict] | None:
635
- """Scan sources in order; return the first ready issue and its source."""
617
+ """Scan sources in order; return the first ready issue and its source.
618
+
619
+ Only the fresh ready scan lives here; the opened-PR resume, the
620
+ in-flight restart and the deferred `ai-awaiting-merge` re-check are
621
+ `pick_next_delivery`'s scans (Issue #1745: a delivery waiting for a
622
+ maintainer never competes with them).
623
+ """
636
624
  for repo in repos:
637
625
  issue = pick_issue(repo, active_milestone)
638
626
  if issue is not None:
@@ -640,209 +628,6 @@ def pick_next_issue(
640
628
  return None
641
629
 
642
630
 
643
- def pick_resumable_delivery(
644
- repo: str, slot_dir: Path, max_concurrency: int,
645
- repo_dir: Path | None = None, *, hooks: ResumeHooks,
646
- language_code: str,
647
- ) -> tuple[dict, dict] | None:
648
- """Return the oldest FREE opened-PR delivery and its resume scene.
649
-
650
- All opened-PR states are scanned: `ai-fix-needed`
651
- (awaiting the next review session after a finding or a base
652
- conflict — the review session fixes findings in the same
653
- session), `ai-pr-opened` (awaiting review), and
654
- `ai-awaiting-merge` (a known policy blocker whose maintainer action
655
- is retried without another review). The `ai-pr-opened`
656
- scan exists because the delivery that opened the PR can be gone: the
657
- runner can die inside the delivery wait loop, leaving a valid
658
- MERGEABLE PR with no owner. Without the scan such a delivery is
659
- picked up by no other scan (`pick_issue` excludes `ai-pr-opened`)
660
- and is stranded forever. `ai-blocked` Issues are excluded (they need
661
- a human decision first), as are merged Issues and closed Issues.
662
- `ai-in-progress` is NOT excluded: a runner killed
663
- during review leaves the backfilled in-flight label behind on the
664
- opened-PR delivery, and the same scan must pick it back up — the
665
- positive `label:ai-fix-needed,ai-pr-opened` qualifier already
666
- restricts the scan to opened-PR Issues (an implement-phase Issue
667
- has `ai-ready`+`ai-in-progress` but neither opened-PR label, so it
668
- never matches). A missing scene on `ai-fix-needed` with exactly one
669
- open PR on the stable internal branch is a fresh-claim takeover: the PR
670
- supplies the continuation anchor and the claim writes a new trusted
671
- scene. Other scenes that cannot be recovered are SINGLE-Issue failures:
672
- the Issue is marked `ai-blocked` with the concrete reason
673
- (`block_scene_failure`) and the scan moves on to the next candidate, so
674
- the tick continues with the in-flight and ready scans and exits 0 — one
675
- corrupted Issue must never make every tick crash while the whole queue
676
- waits.
677
-
678
- Only the deliveries a live co-runner CURRENTLY holds are skipped:
679
- every holder names its (repo, issue) in its slot file
680
- (`pilot_slots.mark_slot_delivery`), so this scan skips exactly the
681
- in-flight deliveries. That is the round-1 protection at
682
- the right granularity — a held delivery is never resumed here, so a
683
- second review Pi never starts in the same worktree/branch/run and a
684
- second `gh pr merge --match-head-commit` never hits the merged PR.
685
- The pre-#809 guard abandoned the WHOLE scan whenever any slot in the
686
- (often shared, multi-repo) slot dir was held: with any concurrency
687
- the review never ran while fresh claims kept opening PRs — the
688
- reported starvation (nine MERGEABLE PRs, the oldest 90 minutes,
689
- zero review ticks). A free delivery is now resumed even while other
690
- deliveries are in flight, and the review backlog drains at the
691
- concurrency rate instead of only growing. This runner's own slot is
692
- excluded: `main` took it before the claim scan and holds it for the
693
- whole delivery.
694
-
695
- The query page is `max_concurrency + 1` candidates, ordered OLDEST
696
- Issue first (`sort:created-asc`, Issue #1734). At most
697
- `max_concurrency` deliveries can be held by live co-runners, so a
698
- free candidate is inside the page when one exists; one whose OPEN PR
699
- still waits for CI (Issue #1473) is skipped too. The scan reviews
700
- the oldest FREE candidate (held ones are skipped before any
701
- candidate read — an in-flight delivery is never touched).
702
- """
703
- held = slot_held_deliveries(slot_dir, max_concurrency)
704
- # `label:a,b` is GitHub's OR within one label qualifier
705
- # (verified live: repeating the qualifier matches only the
706
- # first label). `ai-in-progress` is intentionally NOT excluded:
707
- # a killed review runner leaves the backfilled
708
- # in-flight label behind, and the positive qualifier above
709
- # already keeps implement-phase Issues out.
710
- # `labels`: a resumed P0 delivery keeps its
711
- # priority in the progress comment through review/merge.
712
- # `body`: the scene classification reads the
713
- # delivery markers — without it the #726 external routing of a
714
- # marker ticket with no trusted scene comment is unreachable in
715
- # production (the probe read a body the query never fetched).
716
- issues = list_issues(
717
- repo, state="open",
718
- search=(
719
- f"label:{FIX_NEEDED_LABEL},{PR_OPENED_LABEL},"
720
- f"{AWAITING_MERGE_LABEL} "
721
- f"-label:{BLOCKED_LABEL} -label:{MERGED_LABEL} sort:created-asc"
722
- ),
723
- json_fields="number,title,state,url,labels,body",
724
- limit=max_concurrency + 1,
725
- )
726
- for issue in issues:
727
- if issue.get("state") != "OPEN":
728
- # Left the opened-PR state between the query and this read
729
- # (a close/merge race): this candidate is gone, not the scan.
730
- continue
731
- if (repo, int(issue["number"])) in held:
732
- # In flight in another live runner: never a
733
- # second review Pi for it — and no candidate read either.
734
- continue
735
- comments = issue_comments(int(issue["number"]), repo=repo)
736
- try:
737
- found = hooks.resume_scene(comments)
738
- except scene.SceneError as exc:
739
- # A trusted scene comment exists but is corrupted:
740
- # probe the #726 external route first; otherwise this is the
741
- # ONLY trigger of `block_scene_failure` — a present-but-broken
742
- # scene is a writer bug or tampering and needs a human.
743
- if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
744
- continue
745
- hooks.block_scene_failure(issue, exc, repo, comments, language_code=language_code)
746
- continue
747
- except scene.SceneMissingError as exc:
748
- # No trusted comment carries a scene at all — a distinct branch
749
- # from corruption. The original #726 incident was
750
- # exactly this shape, so the external route is probed first.
751
- if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
752
- continue
753
- # Issue #1216: `awaiting_merge` can deliberately remove the
754
- # queue label before the delivery returns to `ai-fix-needed`.
755
- # An open PR on the stable internal branch is enough durable
756
- # identity to route this through the ordinary fresh-claim
757
- # takeover, which writes a new trusted scene before review.
758
- # Probe the exact branch (and require the helper's sole-PR
759
- # contract); no open PR falls through to the existing missing-
760
- # scene failure below.
761
- current_labels = _issue_label_set(issue)
762
- if (repo_dir is not None
763
- and FIX_NEEDED_LABEL in current_labels
764
- and open_pr_for_branch(
765
- repo_dir,
766
- task_branch(repo, int(issue["number"])),
767
- ) is not None):
768
- found_scene = classify(
769
- labels=current_labels, scene=None, pr_state="OPEN",
770
- body_markers=body_markers(issue.get("body")),
771
- )
772
- if found_scene is DeliveryScene.FRESH_CLAIM:
773
- return issue, None
774
- event(
775
- "claim_yield", issue=int(issue["number"]),
776
- reason=f"scene_{found_scene.value}",
777
- )
778
- continue
779
- # A runner may have created the PR and label, then lost the
780
- # scene comment write. Retry it from the durable local run
781
- # state; keep ai-pr-opened for another tick if the retry fails.
782
- recovered = (
783
- hooks.recover_missing_pr_scene(issue, repo, repo_dir, language_code=language_code)
784
- if repo_dir is not None else None
785
- )
786
- if recovered is not None:
787
- found = recovered
788
- elif repo_dir is not None and hooks.has_recoverable_pr_scene(
789
- issue, repo, repo_dir):
790
- LOGGER.warning(
791
- "issue=%s resume scene recovery pending: %s",
792
- issue["number"], exc,
793
- )
794
- continue
795
- else:
796
- number = int(issue["number"])
797
- LOGGER.error("issue=%s resume scene is missing: %s", number, exc)
798
- marker = latest_run_marker(comments)
799
- try:
800
- post_blocked_comment(
801
- number, repo=repo,
802
- body=(f"{marker}\n" if marker else "")
803
- + language.text(
804
- language_code, "claim.scene_missing", error=exc,
805
- ),
806
- current_labels={FIX_NEEDED_LABEL},
807
- language_code=language_code,
808
- )
809
- except Exception:
810
- LOGGER.exception("issue=%s failure reporting failed", number)
811
- continue
812
- # The scan and the dispatch classify with the same pure
813
- # function. This scan owns the resumable route only: a candidate
814
- # that classifies elsewhere left the opened-PR state between the
815
- # query and this read (a relabel race) — this candidate is skipped
816
- # and the scan moves on. A candidate with no readable labels fails
817
- # open (the is_epic / is_release convention): the trusted scene is
818
- # the authority.
819
- current_labels = _issue_label_set(issue)
820
- if current_labels:
821
- found_scene = classify(
822
- labels=current_labels, scene=found, pr_state=None,
823
- body_markers=body_markers(issue.get("body")),
824
- )
825
- if found_scene is not DeliveryScene.RESUME_REVIEW:
826
- event(
827
- "claim_yield", issue=int(issue["number"]),
828
- reason=f"scene_{found_scene.value}",
829
- )
830
- continue
831
- # Issue #1473: skip a CI-pending OPEN PR; MERGED/CLOSED and a
832
- # failed rollup read still return the candidate.
833
- try:
834
- pr_state, rollup = pr_delivery_rollup(found["pr_url"], repo)
835
- except Exception:
836
- return issue, found
837
- pending = _classify_rollup(rollup)[0] if pr_state == "OPEN" else []
838
- if pending:
839
- event("delivery_ci_pending", issue=int(issue["number"]),
840
- pr=found["pr_url"], pending="; ".join(map(_render_check, pending)))
841
- continue
842
- return issue, found
843
- return None
844
-
845
-
846
631
  def pick_next_delivery(
847
632
  repos: Sequence[str], slot_dir: Path, max_concurrency: int,
848
633
  active_milestone: str | None = None, config: RunnerConfig | None = None,
@@ -860,6 +645,15 @@ def pick_next_delivery(
860
645
  run id, so the same progress comment is kept instead of a second
861
646
  run being started on an Issue that is already in flight.
862
647
 
648
+ Issue #1745: an `ai-awaiting-merge` delivery waits for a maintainer,
649
+ so it is NOT part of the opened-PR scan. It is re-checked
650
+ (`pick_awaiting_merge_delivery`, the existing merge-only path) only
651
+ after all three scans above found nothing for this tick. A
652
+ maintainer-blocked PR therefore no longer starves the review, the
653
+ restart and the fresh-claim scans; the cost is that after the
654
+ maintainer acts the merge happens on the first otherwise-idle tick,
655
+ not on the very next one.
656
+
863
657
  `active_milestone` scopes only the FRESH claim
864
658
  (`pick_issue`) — the resumable-PR and in-flight restart scans are
865
659
  resume states, and running an in-flight or opened-PR delivery to
@@ -932,6 +726,22 @@ def pick_next_delivery(
932
726
  issue = _pick_issue_with_repo_policy(repo, active_milestone, config, held)
933
727
  if issue is not None:
934
728
  return repo, issue, None
729
+ # Issue #1745: a delivery waiting for a maintainer is re-checked LAST —
730
+ # only on a tick where nothing above could move. Everything it needs
731
+ # (the merge gate, the de-duplicated #1422 handoff) already exists; it
732
+ # just never takes the tick away from reviewable, resumable or fresh
733
+ # work.
734
+ for repo in repos:
735
+ repo_language = (language.DEFAULT_LANGUAGE if config is None
736
+ else merged_repo_language(config, repo))
737
+ selected = pick_awaiting_merge_delivery(
738
+ repo, slot_dir, max_concurrency,
739
+ repo_dir=config.repo_dir if config is not None else None,
740
+ hooks=hooks, language_code=repo_language,
741
+ )
742
+ if selected is not None:
743
+ issue, scene = selected
744
+ return repo, issue, scene
935
745
  return None
936
746
 
937
747
 
@@ -56,10 +56,14 @@ def started_pi_comment_body(ctx: RunContext, run_info: str,
56
56
  for key in ("run_id", "priority")
57
57
  if key in info
58
58
  }
59
+ # The headline's key is the same display label the rows use
60
+ # (Issue #1746): `run_id` has no catalogue entry and stays the
61
+ # machine row, `priority` renders `优先级` in a Chinese comment.
59
62
  headline = language.text(
60
63
  language_code, "delivery.started_headline",
61
64
  ) + " " + " ".join(
62
- f"{key}={value}" for key, value in rendered_info.items()
65
+ f"{field_label(language_code, key)}={value}"
66
+ for key, value in rendered_info.items()
63
67
  )
64
68
  detail_keys = {"base_sha", "repo_config", "branch", "session"}
65
69
  return field_block(
@@ -55,6 +55,9 @@ REASON_CODES = frozenset({
55
55
  "credential_missing",
56
56
  # The model provider quota is exhausted (429/RESOURCE_EXHAUSTED).
57
57
  "provider_quota",
58
+ # The configured base branch does not exist in the repository
59
+ # (Issue #1749): a repository/config precondition a human repairs.
60
+ "base_branch_missing",
58
61
  # Any other runner exception the classifier does not recognize.
59
62
  "unclassified",
60
63
  })
@@ -244,6 +247,12 @@ _WORKFLOW_PERMISSION_CAUSE_KEY = "failure.workflow_cause"
244
247
  _TESTS_FAILED_RE = re.compile(
245
248
  r"\btests? failed\b|\d+ failed", re.IGNORECASE,
246
249
  )
250
+ # Git's answer to `git fetch origin <branch>` when the remote has no such
251
+ # branch (Issue #1749): the two wordings real Git emits, and nothing else —
252
+ # a generic fetch failure keeps its existing classification.
253
+ _BASE_BRANCH_MISSING_RE = re.compile(
254
+ r"could ?n['o]t find remote ref", re.IGNORECASE,
255
+ )
247
256
  _GITHUB_TRANSIENT_RE = re.compile(
248
257
  r"http 5\d\d|http 429|rate limit|"
249
258
  r"something went wrong while executing your query|"
@@ -264,6 +273,7 @@ DISPOSITIONS: dict[str, tuple[str, bool]] = {
264
273
  "human_decision_required": ("decide", False),
265
274
  "credential_missing": ("check_credentials", False),
266
275
  "provider_quota": ("wait_quota", True),
276
+ "base_branch_missing": ("fix_ticket", False),
267
277
  "unclassified": ("fix_ticket", False),
268
278
  }
269
279
 
@@ -292,6 +302,8 @@ def classify(detail: str, *, outcome: str = "blocked",
292
302
  # cannot grant: the existing `credential_missing` /
293
303
  # `check_credentials` category, a repair required before retry.
294
304
  reason_code = "credential_missing"
305
+ elif _BASE_BRANCH_MISSING_RE.search(detail):
306
+ reason_code = "base_branch_missing"
295
307
  elif _CREDENTIAL_MISSING_RE.search(detail):
296
308
  reason_code = "credential_missing"
297
309
  elif _TESTS_FAILED_RE.search(detail):
@@ -362,14 +374,38 @@ def terminal_action(record: Failure,
362
374
  """The reader action for the implement-phase terminal handler.
363
375
 
364
376
  An unrecognized failure says the cause could not be determined
365
- instead of implying the Issue text is the problem (Issue #1651);
366
- every recognized terminal failure keeps the existing generic action.
377
+ instead of implying the Issue text is the problem (Issue #1651); a
378
+ missing base branch names its one repair (Issue #1749); every other
379
+ recognized terminal failure keeps the existing generic action.
367
380
  """
368
381
  if record.reason_code == "unclassified":
369
382
  return language.text(language_code, "failure.terminal_unclassified")
383
+ if record.reason_code == "base_branch_missing":
384
+ return language.text(
385
+ language_code, "failure.terminal_base_branch_missing",
386
+ )
370
387
  return language.text(language_code, "failure.terminal_generic")
371
388
 
372
389
 
390
+ def terminal_reason(detail: str, record: Failure, *,
391
+ base_branch: str | None, language_code: str) -> str:
392
+ """The reader-facing reason of the implement-phase terminal handler.
393
+
394
+ The missing-base-branch failure names the CONFIGURED branch: Git's own
395
+ `couldn't find remote ref <value>` line is the evidence, never the
396
+ setting a human has to repair (Issue #1749). Every other terminal
397
+ failure keeps the generic stopped-delivery sentence.
398
+ """
399
+ if record.reason_code == "base_branch_missing":
400
+ return language.text(
401
+ language_code, "failure.base_branch_missing_reason",
402
+ base_branch=base_branch or "-", detail=detail,
403
+ )
404
+ return language.text(
405
+ language_code, "runner.delivery_stopped_reason", detail=detail,
406
+ )
407
+
408
+
373
409
  # --------------------------------------------------------------------------
374
410
  # The comment: bounded detail + the reader-facing hierarchy
375
411
  # --------------------------------------------------------------------------
@@ -726,6 +726,24 @@ MESSAGES: dict[str, tuple[str, str]] = {
726
726
  "Fix the failure described below and re-run this Issue.",
727
727
  "请修复下面描述的失败,然后重新运行这个 Issue。",
728
728
  ),
729
+ # Issue #1749: the configured base branch does not exist in the
730
+ # repository. The reason names the CONFIGURED branch (Git's own line
731
+ # only carries the value it was asked to fetch) and the action names
732
+ # the one repair a human decides.
733
+ "failure.base_branch_missing_reason": (
734
+ "base branch \u0060{base_branch}\u0060 does not exist in the repository: "
735
+ "{detail}",
736
+ "仓库中不存在 base branch \u0060{base_branch}\u0060:{detail}",
737
+ ),
738
+ "failure.terminal_base_branch_missing": (
739
+ "Create the missing base branch in the repository, or correct "
740
+ "\u0060base_branch\u0060 in the Runner configuration, then remove "
741
+ "\u0060ai-blocked\u0060 and re-add \u0060ai-ready\u0060 to run this "
742
+ "Issue again.",
743
+ "请在仓库中创建缺失的 base branch,或修正 Runner 配置里的 "
744
+ "\u0060base_branch\u0060,然后移除 \u0060ai-blocked\u0060 并重新添加 "
745
+ "\u0060ai-ready\u0060 以再次运行这个 Issue。",
746
+ ),
729
747
  "failure.workflow_cause": (
730
748
  "GitHub rejected the push because the current GitHub App "
731
749
  "credential cannot modify Actions workflows.",
@@ -48,6 +48,11 @@ DISPLAY_TERMS: dict[str, tuple[str, str]] = {
48
48
  "progress.tests.passed": ("passed", "通过"),
49
49
  "progress.tests.pending": ("pending", "等待中"),
50
50
  "progress.tests.failed": ("failed: {names}", "失败:{names}"),
51
+ # The progress comment's recovery row while a session is stuck
52
+ # (Issue #1746): the stored state stays `wait`/`term`/`kill`.
53
+ "progress.recovery.wait": ("wait", "等待"),
54
+ "progress.recovery.term": ("term", "终止"),
55
+ "progress.recovery.kill": ("kill", "强制终止"),
51
56
  "progress.activity.read": ("read", "读取"),
52
57
  "progress.activity.edit": ("edit", "编辑"),
53
58
  "progress.activity.command": ("command", "命令"),
@@ -47,10 +47,13 @@ def is_maintainer_actionable(stderr: str, preflight: list[str]) -> bool:
47
47
 
48
48
 
49
49
  # The handoff comment identifies the exact waiting state it announced.
50
- # ``ai-awaiting-merge`` is rescanned every tick by design, so the same
51
- # policy blocker reaches the merge gate again and again; the head OID and
52
- # the blocker text decide whether the wait is a new state or the same one
53
- # already posted (Issue #1422).
50
+ # ``ai-awaiting-merge`` is re-checked by design — but only on a tick
51
+ # whose queue is otherwise empty (Issue #1745: the deferred scan never
52
+ # takes the tick away from reviewable, resumable or fresh work), so the
53
+ # same policy blocker still reaches the merge gate again and again. The
54
+ # head OID and the blocker text decide whether the wait is a new state or
55
+ # the same one already posted (Issue #1422), so an unchanged blocker
56
+ # posts nothing.
54
57
  AWAITING_MERGE_HANDOFF_PHRASE = (
55
58
  "is delivered and waiting for a maintainer action"
56
59
  )
@@ -505,7 +505,8 @@ def progress_body(state: dict) -> str:
505
505
  # Exceptional state belongs with the at-a-glance status. A normal
506
506
  # first or second review round carries no useful signal on its own.
507
507
  if state.get("recovery"):
508
- lines.append(row("recovery", state["recovery"]))
508
+ lines.append(row("recovery", vocabulary(
509
+ language_code, "recovery", state["recovery"])))
509
510
  if state.get("review_round", 0) >= 3:
510
511
  lines.append(row("review_round", value("review_round")))
511
512