orbi-cli 0.7.1__tar.gz → 0.7.3__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 (183) hide show
  1. {orbi_cli-0.7.1/src/orbi_cli.egg-info → orbi_cli-0.7.3}/PKG-INFO +1 -1
  2. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/pyproject.toml +1 -1
  3. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/__init__.py +1 -1
  4. orbi_cli-0.7.3/src/orbi/blocked_comment.py +35 -0
  5. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/claim.py +46 -43
  6. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/clarify.py +6 -4
  7. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/delivery_comment.py +48 -26
  8. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/delivery_scene.py +0 -4
  9. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/failure.py +8 -5
  10. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/failure_report.py +90 -92
  11. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/github.py +12 -21
  12. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/human_review.py +9 -3
  13. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/language.py +341 -31
  14. orbi_cli-0.7.3/src/orbi/language_terms.py +77 -0
  15. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/merge_handoff.py +2 -2
  16. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/milestone.py +57 -31
  17. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/milestone_command.py +8 -5
  18. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/notify.py +177 -40
  19. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/pi_session.py +4 -2
  20. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/progress.py +52 -48
  21. orbi_cli-0.7.3/src/orbi/progress_display.py +111 -0
  22. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/progress_outcome.py +12 -1
  23. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/progress_summary.py +5 -3
  24. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/release.py +16 -18
  25. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/release_comments.py +47 -2
  26. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/repo_config.py +18 -0
  27. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/review_merge.py +96 -80
  28. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/review_rounds.py +2 -2
  29. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/runner.py +91 -81
  30. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/runner_health.py +17 -10
  31. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/suggest.py +59 -15
  32. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/ticket_command.py +61 -19
  33. {orbi_cli-0.7.1 → orbi_cli-0.7.3/src/orbi_cli.egg-info}/PKG-INFO +1 -1
  34. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi_cli.egg-info/SOURCES.txt +9 -0
  35. orbi_cli-0.7.3/tests/test_blocked_comment.py +174 -0
  36. orbi_cli-0.7.3/tests/test_bootstrap_adapters.py +466 -0
  37. orbi_cli-0.7.3/tests/test_bootstrap_delivery.py +6125 -0
  38. orbi_cli-0.7.3/tests/test_bootstrap_pi.py +2856 -0
  39. orbi_cli-0.7.3/tests/test_bootstrap_preflight.py +2162 -0
  40. orbi_cli-0.7.3/tests/test_bootstrap_release.py +4579 -0
  41. orbi_cli-0.7.3/tests/test_bootstrap_runner.py +7075 -0
  42. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_clarify.py +3 -3
  43. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_comment_identity.py +5 -5
  44. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_comment_readers_contract.py +6 -5
  45. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_deliver_pr_fakes.py +4 -4
  46. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_delivery_branch_delete.py +5 -5
  47. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_delivery_scene.py +64 -14
  48. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_failure.py +21 -21
  49. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_failure_report.py +406 -39
  50. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_fix_needed_loop.py +66 -11
  51. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_git_merge_smoke.py +6 -6
  52. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_github_fakes.py +24 -2
  53. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_human_review.py +9 -8
  54. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_message_language.py +452 -13
  55. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_milestone_command.py +203 -0
  56. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_notify.py +266 -32
  57. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pilot_setup.py +27 -0
  58. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_progress.py +62 -30
  59. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_progress_wiring.py +177 -12
  60. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_repo_config.py +32 -11
  61. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_resume_continue_e2e.py +6 -0
  62. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_resume_e2e.py +142 -4
  63. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_resume_pr.py +258 -62
  64. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_review_merge.py +152 -42
  65. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_run_id_e2e.py +2 -1
  66. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_runner_config.py +74 -0
  67. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_runner_health.py +127 -2
  68. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_steering_review.py +27 -1
  69. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_suggest.py +283 -20
  70. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_ticket_command.py +3 -1
  71. orbi_cli-0.7.1/tests/test_bootstrap_runner.py +0 -24077
  72. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/LICENSE +0 -0
  73. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/README.md +0 -0
  74. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/docs/licenses/sustainable-use-license.md +0 -0
  75. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/setup.cfg +0 -0
  76. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/branch_reclaim.py +0 -0
  77. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/checks.py +0 -0
  78. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/cli.py +0 -0
  79. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/cli_source.py +0 -0
  80. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/comment_kind.py +0 -0
  81. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/config.py +0 -0
  82. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/delivery_labels.py +0 -0
  83. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/engine_source.py +0 -0
  84. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/example_config.toml +0 -0
  85. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/git_identity.py +0 -0
  86. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/git_transport.py +0 -0
  87. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/gitops.py +0 -0
  88. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/journal.py +0 -0
  89. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/launchd_deploy.py +0 -0
  90. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/merge_gate.py +0 -0
  91. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/pi_activity.py +0 -0
  92. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/pi_command.py +0 -0
  93. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/pi_process.py +0 -0
  94. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/pi_recovery.py +0 -0
  95. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/pilot_setup.py +0 -0
  96. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/pilot_slots.py +0 -0
  97. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/release_git.py +0 -0
  98. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/release_notes.py +0 -0
  99. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/run_state.py +0 -0
  100. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/scene.py +0 -0
  101. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/scheduler.py +0 -0
  102. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/scheduler_units.py +0 -0
  103. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/session_command.py +0 -0
  104. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/source_pool.py +0 -0
  105. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/status_observe.py +0 -0
  106. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi/systemd_deploy.py +0 -0
  107. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  108. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  109. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/src/orbi_cli.egg-info/top_level.txt +0 -0
  110. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_agents_md.py +0 -0
  111. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_apply_label_patch.py +0 -0
  112. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_branch_reclaim.py +0 -0
  113. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_ci_failure_issue_workflow.py +0 -0
  114. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_ci_failure_triage.py +0 -0
  115. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_ci_macos_workflow.py +0 -0
  116. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_ci_workflow.py +0 -0
  117. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_claude_plugin.py +0 -0
  118. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_cli_install.py +0 -0
  119. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_cli_milestone.py +0 -0
  120. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_cli_packaging.py +0 -0
  121. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_cli_source.py +0 -0
  122. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_concurrency_e2e.py +0 -0
  123. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_constitution.py +0 -0
  124. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_coverage_gate.py +0 -0
  125. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_delivery_labels.py +0 -0
  126. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docker_docs.py +0 -0
  127. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docker_entrypoint.py +0 -0
  128. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docker_image_workflow.py +0 -0
  129. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docs_i18n.py +0 -0
  130. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docs_issue_shapes.py +0 -0
  131. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docs_labels.py +0 -0
  132. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docs_mermaid.py +0 -0
  133. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docs_releases.py +0 -0
  134. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docs_single_source.py +0 -0
  135. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_docs_site.py +0 -0
  136. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_engine_source.py +0 -0
  137. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_gh_read_retry.py +0 -0
  138. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_git_base_smoke.py +0 -0
  139. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_git_network_retry.py +0 -0
  140. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_git_transport.py +0 -0
  141. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_github.py +0 -0
  142. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_gitops.py +0 -0
  143. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_gitops_fakes.py +0 -0
  144. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_import_graph.py +0 -0
  145. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_install_sh.py +0 -0
  146. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_issue_1367.py +0 -0
  147. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_issue_461.py +0 -0
  148. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_issue_template_config.py +0 -0
  149. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_journal.py +0 -0
  150. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_journal_events.py +0 -0
  151. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_launchd_deploy.py +0 -0
  152. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_license.py +0 -0
  153. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_ops_e2e.py +0 -0
  154. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_orbi.py +0 -0
  155. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_orbi_dashboard.py +0 -0
  156. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_orbi_exporter.py +0 -0
  157. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_patch_ratchet.py +0 -0
  158. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pi_activity.py +0 -0
  159. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pi_command.py +0 -0
  160. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pi_extensions.py +0 -0
  161. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pi_provider_templates.py +0 -0
  162. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pi_recovery.py +0 -0
  163. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pi_session.py +0 -0
  164. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pilot_slots.py +0 -0
  165. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_pr_issue_link_workflow.py +0 -0
  166. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_prerequisite_check.py +0 -0
  167. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_prompt_contract.py +0 -0
  168. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_public_contracts.py +0 -0
  169. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_publish_workflow.py +0 -0
  170. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_release_docs_workflow.py +0 -0
  171. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_release_ticket_templates.py +0 -0
  172. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_run_artifacts.py +0 -0
  173. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_runner_source_freshness.py +0 -0
  174. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_scene.py +0 -0
  175. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_scheduler.py +0 -0
  176. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_size_ratchet.py +0 -0
  177. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_src_layout.py +0 -0
  178. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_status_observe.py +0 -0
  179. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_suite_hygiene.py +0 -0
  180. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_systemd_deploy.py +0 -0
  181. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_systemd_timer.py +0 -0
  182. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/tests/test_user_acceptance_contract.py +0 -0
  183. {orbi_cli-0.7.1 → orbi_cli-0.7.3}/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.1
3
+ Version: 0.7.3
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.1"
47
+ version = "0.7.3"
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.1"
21
+ __version__ = "0.7.3"
@@ -0,0 +1,35 @@
1
+ """The ONE exit for a comment that blocks an Issue (Issue #1730).
2
+
3
+ Ten scenes put an Issue into `ai-blocked` and each own their reason text,
4
+ unblock step, hidden markers and extra fields. What they must NOT each own is
5
+ the pointer to where a person can ask for help: it is appended here, once, so
6
+ a blocked scene added later through this exit carries the community help line
7
+ without anyone remembering to add it.
8
+
9
+ A comment that is not a blocked scene (a retrying diagnostic, progress, a
10
+ receipt) never goes through this exit and never gets the line.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from orbi import language
15
+ from orbi.delivery_labels import EVENT_BLOCKED
16
+ from orbi.github import apply_label_patch, comment_issue
17
+
18
+
19
+ def post_blocked_comment(number: int, *, repo: str, body: str,
20
+ current_labels, language_code: str) -> None:
21
+ """Block `number`: apply the `ai-blocked` label patch, then comment.
22
+
23
+ The order is part of the contract: the label transition lands first, the
24
+ comment second. The body is posted exactly as the scene rendered it,
25
+ followed by a blank line and the `help.community` line in the run's
26
+ language — added here and only here, so it appears exactly once.
27
+ """
28
+ apply_label_patch(
29
+ number, repo=repo, event=EVENT_BLOCKED,
30
+ current_labels=current_labels,
31
+ )
32
+ comment_issue(
33
+ number, repo=repo,
34
+ body=f"{body}\n\n{language.text(language_code, 'help.community')}",
35
+ )
@@ -18,10 +18,11 @@ from typing import NamedTuple
18
18
 
19
19
  from orbi import milestone as milestone_bookkeeping
20
20
  from orbi import scene
21
+ from orbi.blocked_comment import post_blocked_comment
21
22
  from orbi.checks import _classify_rollup, _render_check
22
23
  from orbi.config import RunnerConfig
23
24
  from orbi.delivery_labels import (
24
- AWAITING_MERGE_LABEL, BLOCKED_LABEL, EPIC_LABEL, EVENT_BLOCKED,
25
+ AWAITING_MERGE_LABEL, BLOCKED_LABEL, EPIC_LABEL,
25
26
  FIX_NEEDED_LABEL, IN_PROGRESS_LABEL, MERGED_LABEL, P0_LABEL,
26
27
  PR_OPENED_LABEL, READY_LABEL, RELEASE_LABEL,
27
28
  )
@@ -29,7 +30,7 @@ from orbi.delivery_scene import (
29
30
  EXTERNAL_PR_MARKER, DeliveryScene, _issue_label_set, body_markers, classify,
30
31
  )
31
32
  from orbi.github import (
32
- _epic_audit, _verify_epic_complete, apply_label_patch, close_issue,
33
+ _verify_epic_complete, close_issue,
33
34
  comment_issue, issue_comments, issue_priority, issue_view,
34
35
  latest_run_marker, list_issues, milestone_open_issue_count,
35
36
  open_blocker_numbers, open_pr_for_branch, pr_comments,
@@ -40,8 +41,10 @@ from orbi.journal import LOGGER, current_run_id, event, new_run_id, set_run_id
40
41
  from orbi.pilot_slots import slot_held_deliveries, slot_occupancy
41
42
  from orbi import language
42
43
  from orbi.progress import run_marker
43
- from orbi.release import release_target_milestone
44
- from orbi.repo_config import RepoConfigError, RepoPolicy, load_repo_policy
44
+ from orbi.release import _epic_audit, epic_audit_posted, release_target_milestone
45
+ from orbi.repo_config import (
46
+ RepoConfigError, RepoPolicy, load_repo_policy, merged_repo_language,
47
+ )
45
48
 
46
49
 
47
50
  class ResumeHooks(NamedTuple):
@@ -49,11 +52,14 @@ class ResumeHooks(NamedTuple):
49
52
 
50
53
  The physical scene handling of `pick_resumable_delivery` plus the PR
51
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.
52
58
  """
53
59
  resume_scene: Callable[[list[dict]], dict]
54
- route_external_pr_ticket: Callable[[dict, str], bool]
55
- block_scene_failure: Callable[[dict, ValueError, str, list[dict]], None]
56
- recover_missing_pr_scene: Callable[[dict, str, Path], dict | None]
60
+ route_external_pr_ticket: Callable[..., bool]
61
+ block_scene_failure: Callable[..., None]
62
+ recover_missing_pr_scene: Callable[..., dict | None]
57
63
  has_recoverable_pr_scene: Callable[[dict, str, Path], bool]
58
64
  comment_pr: Callable[..., None]
59
65
 
@@ -195,7 +201,7 @@ def is_release(issue: dict) -> bool:
195
201
  return False
196
202
 
197
203
 
198
- def reconcile_open_epics(repo: str, run_id: str) -> list[str]:
204
+ def reconcile_open_epics(repo: str, run_id: str, language_code: str) -> list[str]:
199
205
  """Sweep open Epics once per tick; ordinary pickup must not depend on it."""
200
206
  epics = list_issues(
201
207
  repo, state="open", search=f"label:{EPIC_LABEL}",
@@ -211,9 +217,9 @@ def reconcile_open_epics(repo: str, run_id: str) -> list[str]:
211
217
  event("epic_kept_open", issue=number, repo=repo, reason=reason)
212
218
  evidence.append(f"Epic #{number} kept open: {reason}")
213
219
  continue
214
- audit = _epic_audit(child_evidence)
220
+ audit = _epic_audit(child_evidence, None, language_code)
215
221
  comments = issue_comments(int(number), repo=repo)
216
- if not any(audit in str(comment.get("body", "")) for comment in comments):
222
+ if not epic_audit_posted(comments, child_evidence):
217
223
  comment_issue(int(number), repo=repo,
218
224
  body=f"<!-- orbi:run={run_id} -->\n{audit}\nrun_id={run_id}")
219
225
  close_issue(int(number), repo=repo)
@@ -243,7 +249,7 @@ def orphan_pr_branch_issue(head_ref: object) -> int | None:
243
249
 
244
250
 
245
251
  def reconcile_orphan_prs(
246
- repo: str, run_id: str, *, hooks: ResumeHooks,
252
+ repo: str, run_id: str, *, hooks: ResumeHooks, language_code: str,
247
253
  ) -> list[str]:
248
254
  """Report open delivery PRs whose source Issue is closed.
249
255
 
@@ -287,11 +293,10 @@ def reconcile_orphan_prs(
287
293
  pr_number, repo=repo,
288
294
  body=(
289
295
  f"{ORPHAN_PR_MARK}\n{run_marker(run_id)}\n"
290
- f"Orbi orphan PR: the source Issue #{issue_number} is "
291
- f"closed while this PR is still open, so it will never "
292
- f"be merged automatically. Human decision: merge this "
293
- f"PR to keep the work, or close it.\n"
294
- f"run_id={run_id}"
296
+ + language.text(
297
+ language_code, "claim.orphan_pr",
298
+ issue_number=issue_number, run_id=run_id,
299
+ )
295
300
  ),
296
301
  )
297
302
  event(
@@ -638,9 +643,9 @@ def pick_next_issue(
638
643
  def pick_resumable_delivery(
639
644
  repo: str, slot_dir: Path, max_concurrency: int,
640
645
  repo_dir: Path | None = None, *, hooks: ResumeHooks,
641
- language_code: str = language.DEFAULT_LANGUAGE,
646
+ language_code: str,
642
647
  ) -> tuple[dict, dict] | None:
643
- """Return the newest FREE opened-PR delivery and its resume scene.
648
+ """Return the oldest FREE opened-PR delivery and its resume scene.
644
649
 
645
650
  All opened-PR states are scanned: `ai-fix-needed`
646
651
  (awaiting the next review session after a finding or a base
@@ -687,12 +692,13 @@ def pick_resumable_delivery(
687
692
  excluded: `main` took it before the claim scan and holds it for the
688
693
  whole delivery.
689
694
 
690
- The query page is `max_concurrency + 1` candidates. At most
695
+ The query page is `max_concurrency + 1` candidates, ordered OLDEST
696
+ Issue first (`sort:created-asc`, Issue #1734). At most
691
697
  `max_concurrency` deliveries can be held by live co-runners, so a
692
698
  free candidate is inside the page when one exists; one whose OPEN PR
693
- still waits for CI (Issue #1473) is skipped too. The scan reviews the
694
- newest FREE candidate (held ones are skipped before any candidate
695
- read — an in-flight delivery is never touched).
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).
696
702
  """
697
703
  held = slot_held_deliveries(slot_dir, max_concurrency)
698
704
  # `label:a,b` is GitHub's OR within one label qualifier
@@ -712,7 +718,7 @@ def pick_resumable_delivery(
712
718
  search=(
713
719
  f"label:{FIX_NEEDED_LABEL},{PR_OPENED_LABEL},"
714
720
  f"{AWAITING_MERGE_LABEL} "
715
- f"-label:{BLOCKED_LABEL} -label:{MERGED_LABEL}"
721
+ f"-label:{BLOCKED_LABEL} -label:{MERGED_LABEL} sort:created-asc"
716
722
  ),
717
723
  json_fields="number,title,state,url,labels,body",
718
724
  limit=max_concurrency + 1,
@@ -734,15 +740,15 @@ def pick_resumable_delivery(
734
740
  # probe the #726 external route first; otherwise this is the
735
741
  # ONLY trigger of `block_scene_failure` — a present-but-broken
736
742
  # scene is a writer bug or tampering and needs a human.
737
- if hooks.route_external_pr_ticket(issue, repo):
743
+ if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
738
744
  continue
739
- hooks.block_scene_failure(issue, exc, repo, comments)
745
+ hooks.block_scene_failure(issue, exc, repo, comments, language_code=language_code)
740
746
  continue
741
747
  except scene.SceneMissingError as exc:
742
748
  # No trusted comment carries a scene at all — a distinct branch
743
749
  # from corruption. The original #726 incident was
744
750
  # exactly this shape, so the external route is probed first.
745
- if hooks.route_external_pr_ticket(issue, repo):
751
+ if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
746
752
  continue
747
753
  # Issue #1216: `awaiting_merge` can deliberately remove the
748
754
  # queue label before the delivery returns to `ai-fix-needed`.
@@ -774,7 +780,7 @@ def pick_resumable_delivery(
774
780
  # scene comment write. Retry it from the durable local run
775
781
  # state; keep ai-pr-opened for another tick if the retry fails.
776
782
  recovered = (
777
- hooks.recover_missing_pr_scene(issue, repo, repo_dir)
783
+ hooks.recover_missing_pr_scene(issue, repo, repo_dir, language_code=language_code)
778
784
  if repo_dir is not None else None
779
785
  )
780
786
  if recovered is not None:
@@ -791,16 +797,14 @@ def pick_resumable_delivery(
791
797
  LOGGER.error("issue=%s resume scene is missing: %s", number, exc)
792
798
  marker = latest_run_marker(comments)
793
799
  try:
794
- apply_label_patch(
795
- number, repo=repo, event=EVENT_BLOCKED,
796
- current_labels={FIX_NEEDED_LABEL},
797
- )
798
- comment_issue(
800
+ post_blocked_comment(
799
801
  number, repo=repo,
800
802
  body=(f"{marker}\n" if marker else "")
801
803
  + language.text(
802
804
  language_code, "claim.scene_missing", error=exc,
803
805
  ),
806
+ current_labels={FIX_NEEDED_LABEL},
807
+ language_code=language_code,
804
808
  )
805
809
  except Exception:
806
810
  LOGGER.exception("issue=%s failure reporting failed", number)
@@ -869,10 +873,12 @@ def pick_next_delivery(
869
873
  tick_run_id = new_run_id()
870
874
  set_run_id(tick_run_id)
871
875
  for repo in repos:
876
+ repo_language = (language.DEFAULT_LANGUAGE if config is None
877
+ else merged_repo_language(config, repo))
872
878
  # Epic reconciliation is a per-tick bypass: a broken GitHub query or
873
879
  # mutation must never prevent the ordinary delivery scans.
874
880
  try:
875
- reconcile_open_epics(repo, tick_run_id)
881
+ reconcile_open_epics(repo, tick_run_id, repo_language)
876
882
  except Exception:
877
883
  LOGGER.exception("epic_reconcile_failed repo=%s", repo)
878
884
  # Milestone reconciliation intentionally follows the Epic sweep so a
@@ -901,18 +907,15 @@ def pick_next_delivery(
901
907
  # pattern: a broken GitHub query must never prevent the ordinary
902
908
  # delivery scans.
903
909
  try:
904
- reconcile_orphan_prs(repo, tick_run_id, hooks=hooks)
910
+ reconcile_orphan_prs(repo, tick_run_id, hooks=hooks,
911
+ language_code=repo_language)
905
912
  except Exception:
906
913
  LOGGER.exception("orphan_pr_reconcile_failed repo=%s", repo)
907
- if config is None:
908
- selected = pick_resumable_delivery(
909
- repo, slot_dir, max_concurrency, hooks=hooks,
910
- )
911
- else:
912
- selected = pick_resumable_delivery(
913
- repo, slot_dir, max_concurrency, config.repo_dir,
914
- hooks=hooks, language_code=language.of(config),
915
- )
914
+ selected = pick_resumable_delivery(
915
+ repo, slot_dir, max_concurrency,
916
+ repo_dir=config.repo_dir if config is not None else None,
917
+ hooks=hooks, language_code=repo_language,
918
+ )
916
919
  if selected is not None:
917
920
  issue, scene = selected
918
921
  return repo, issue, scene
@@ -56,7 +56,7 @@ MISSING: dict[str, str] = {
56
56
  }
57
57
 
58
58
 
59
- def missing_text(item: str, language_code: str = language.DEFAULT_LANGUAGE,
59
+ def missing_text(item: str, language_code: str,
60
60
  ) -> str:
61
61
  """The human sentence for one failed check, in one language."""
62
62
  return language.text(language_code, MISSING[item])
@@ -154,7 +154,7 @@ def parse_verdict(output: str) -> ClarifyVerdict | None:
154
154
 
155
155
  def render_comment(issue: dict, verdict: ClarifyVerdict, run_id: str, *,
156
156
  ready_label: str = READY_LABEL,
157
- language_code: str = language.DEFAULT_LANGUAGE) -> str:
157
+ language_code: str) -> str:
158
158
  """Render the ONE comment that names the missing pieces.
159
159
 
160
160
  The first line mentions the Issue author (`@<login>`, #1336) so the
@@ -163,7 +163,8 @@ def render_comment(issue: dict, verdict: ClarifyVerdict, run_id: str, *,
163
163
  before. `ready_label` is the repository's claim label (default
164
164
  `ai-ready`, #527): the repair instruction names the label the claim
165
165
  scan really reads, never a hardcoded default a custom-label
166
- repository does not use.
166
+ repository does not use. The comment ends with the community help
167
+ line (Issue #1684): this stop asks the author to step in.
167
168
  """
168
169
  def message(key: str, **fields: object) -> str:
169
170
  return language.text(language_code, key, **fields)
@@ -185,7 +186,8 @@ def render_comment(issue: dict, verdict: ClarifyVerdict, run_id: str, *,
185
186
  f"{message('clarify.repair_acceptance')}\n"
186
187
  "```\n\n"
187
188
  f"{message('clarify.followup', needs_detail_label=NEEDS_DETAIL_LABEL)}"
188
- f"\n\nrun_id={run_id}\n"
189
+ f"\n\nrun_id={run_id}"
190
+ f"\n\n{message('help.community')}\n"
189
191
  )
190
192
 
191
193
 
@@ -1,12 +1,15 @@
1
- """The runner-owned run comments: the started-Pi and opened-PR scenes
2
- (Issue #1660).
1
+ """The runner-owned run comments and the PR body's footer: the
2
+ started-Pi and opened-PR scenes (Issue #1660) and the attribution footer
3
+ (Issue #1729).
3
4
 
4
5
  Both comments double as the run's machine-readable anchors: the started
5
6
  comment carries the `started` kind marker and the opened-PR comment
6
- carries the `orbi:scene:v1` block the next tick resumes from. Only their
7
- HEADLINE is human prose and follows the run's bound message language
8
- (`RunContext.language`, or an explicit `language_code`); the hidden
9
- markers, the field lines and the scene block stay exactly as they were.
7
+ carries the `orbi:scene:v1` block the next tick resumes from. Their
8
+ human text — the headline, the field labels and the stored display
9
+ values — follows the run's bound message language (the explicit
10
+ `language_code`); the hidden markers, the public machine rows
11
+ (`run_id=`, `repo_config:`) and the scene block stay exactly as they
12
+ were.
10
13
 
11
14
  Extracted from `runner.py` unchanged apart from the language, so the
12
15
  frozen runner module does not grow (Issue #1229) and a caller keeps
@@ -19,19 +22,21 @@ from orbi import language, scene
19
22
  from orbi.comment_kind import COMMENT_KIND_STARTED
20
23
  from orbi.delivery_scene import RunContext
21
24
  from orbi.journal import validate_run_id
22
- from orbi.progress import STARTED_HEADER, _run_info_fields, field_block
23
-
24
-
25
- def _language_of(ctx: RunContext, language_code: str | None) -> str:
26
- """The explicit language, else the run context's bound language."""
27
- if language_code is not None:
28
- return language_code
29
- return getattr(ctx, "language", language.DEFAULT_LANGUAGE)
25
+ from orbi.progress import (
26
+ STARTED_HEADER,
27
+ _run_info_fields,
28
+ field_block,
29
+ )
30
+ from orbi.progress_display import (
31
+ field_label,
32
+ localized_fields,
33
+ vocabulary,
34
+ )
30
35
 
31
36
 
32
37
  def started_pi_comment_body(ctx: RunContext, run_info: str,
33
38
  extra_fields: dict | None = None, *,
34
- language_code: str | None = None) -> str:
39
+ language_code: str) -> str:
35
40
  """The start comment doubles as the recoverable run scene.
36
41
 
37
42
  `extra_fields` carries the repo-config audit fields;
@@ -42,28 +47,44 @@ def started_pi_comment_body(ctx: RunContext, run_info: str,
42
47
  """
43
48
  fields = _run_info_fields(run_info)
44
49
  fields["branch"] = str(ctx.branch)
45
- fields["worktree"] = str(ctx.worktree)
46
50
  if extra_fields:
47
51
  fields.update(extra_fields)
48
52
  info = _run_info_fields(run_info)
49
- bound = _language_of(ctx, language_code)
53
+ rendered_info = {
54
+ key: (vocabulary(language_code, "priority", info[key])
55
+ if key == "priority" else info[key])
56
+ for key in ("run_id", "priority")
57
+ if key in info
58
+ }
50
59
  headline = language.text(
51
- bound, "delivery.started_headline",
60
+ language_code, "delivery.started_headline",
52
61
  ) + " " + " ".join(
53
- f"{key}={info[key]}" for key in ("run_id", "priority")
54
- if key in info
62
+ f"{key}={value}" for key, value in rendered_info.items()
55
63
  )
64
+ detail_keys = {"base_sha", "repo_config", "branch", "session"}
56
65
  return field_block(
57
- ctx.run_id, headline, fields,
58
- detail_keys={"base_sha", "repo_config", "branch", "worktree", "session"},
66
+ ctx.run_id, headline, localized_fields(language_code, fields),
67
+ detail_keys={field_label(language_code, key) for key in detail_keys},
59
68
  kind=COMMENT_KIND_STARTED,
60
- language_code=bound,
69
+ language_code=language_code,
70
+ )
71
+
72
+
73
+ def pr_attribution_footer(issue: int, attribution_link: str, *,
74
+ language_code: str) -> str:
75
+ """The PR body's attribution footer in the bound message language.
76
+
77
+ The link URL and `#<issue>` are the machine facts and stay verbatim.
78
+ """
79
+ return language.text(
80
+ language_code, "runner.pr_attribution",
81
+ link=attribution_link, issue=issue,
61
82
  )
62
83
 
63
84
 
64
85
  def opened_pr_comment_body(run_id: str, run_info: str, pr_url: str,
65
86
  external: bool = False, *,
66
- language_code: str = language.DEFAULT_LANGUAGE,
87
+ language_code: str,
67
88
  ) -> str:
68
89
  """The PR-opened comment records the recoverable run scene.
69
90
 
@@ -95,9 +116,10 @@ def opened_pr_comment_body(run_id: str, run_info: str, pr_url: str,
95
116
  pr_url=pr_url,
96
117
  external="true" if external else "",
97
118
  )
119
+ detail_keys = {"base_sha", "repo_config", "branch", "session"}
98
120
  body = field_block(
99
- run_id, headline, fields,
100
- detail_keys={"base_sha", "repo_config", "branch", "worktree", "session"},
121
+ run_id, headline, localized_fields(language_code, fields),
122
+ detail_keys={field_label(language_code, key) for key in detail_keys},
101
123
  language_code=language_code,
102
124
  )
103
125
  lines = body.splitlines()
@@ -204,10 +204,6 @@ class RunContext:
204
204
  worktree: Path
205
205
  source_repo: str
206
206
  pr: str | None = None
207
- # The run's bound message language (Issue #1660): `zh` or `en`, bound
208
- # from the effective config (host + repository policy) before the
209
- # first comment is written. Runtime state only -- never persisted.
210
- language: str = "en"
211
207
 
212
208
 
213
209
  @dataclasses.dataclass(frozen=True)
@@ -327,7 +327,7 @@ def workflow_permission_refused(detail: str) -> bool:
327
327
 
328
328
 
329
329
  def workflow_permission_cause(
330
- language_code: str = language.DEFAULT_LANGUAGE,
330
+ language_code: str,
331
331
  ) -> str:
332
332
  """The one-sentence cause of an Actions-workflow permission refusal."""
333
333
  return language.text(language_code, _WORKFLOW_PERMISSION_CAUSE_KEY)
@@ -335,7 +335,7 @@ def workflow_permission_cause(
335
335
 
336
336
  def workflow_permission_action(
337
337
  pr_url: str | None = None, *,
338
- language_code: str = language.DEFAULT_LANGUAGE,
338
+ language_code: str,
339
339
  ) -> str:
340
340
  """The concrete human repair path for the workflow refusal.
341
341
 
@@ -358,7 +358,7 @@ def workflow_permission_action(
358
358
 
359
359
 
360
360
  def terminal_action(record: Failure,
361
- language_code: str = language.DEFAULT_LANGUAGE) -> str:
361
+ language_code: str) -> str:
362
362
  """The reader action for the implement-phase terminal handler.
363
363
 
364
364
  An unrecognized failure says the cause could not be determined
@@ -410,7 +410,7 @@ def _workflow_permission_line(text: str) -> str | None:
410
410
 
411
411
 
412
412
  def _failure_summary(reason: str,
413
- language_code: str = language.DEFAULT_LANGUAGE) -> str:
413
+ language_code: str) -> str:
414
414
  """Turn a runner exception description into a short reader summary."""
415
415
  command_failed = language.text(language_code, "failure.command_failed")
416
416
  stderr_match = re.search(r"\s+stderr=(.*)$", reason,
@@ -452,7 +452,7 @@ def _failure_comment_body(*, outcome: str, action: str, reason: str,
452
452
  diagnosis: str, scene: str, evidence: str,
453
453
  pr_url: str | None, issue: str, run_id: str,
454
454
  failure_record: Failure | None = None,
455
- language_code: str = language.DEFAULT_LANGUAGE,
455
+ language_code: str,
456
456
  ) -> str:
457
457
  """Build the reader-facing hierarchy shared by classified failures.
458
458
 
@@ -529,4 +529,7 @@ def _failure_comment_body(*, outcome: str, action: str, reason: str,
529
529
  if evidence:
530
530
  details.append(evidence.lstrip())
531
531
  details.append("</details>")
532
+ # The blocked diagnostic's community help line is NOT added here: the
533
+ # ONE blocked exit appends it to every blocked comment (Issue #1730),
534
+ # so this renderer and that exit can never both add it.
532
535
  return "\n\n".join(parts + ["\n".join(details)])