orbi-cli 0.7.2__tar.gz → 0.7.4__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.2/src/orbi_cli.egg-info → orbi_cli-0.7.4}/PKG-INFO +1 -1
  2. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/pyproject.toml +1 -1
  3. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/__init__.py +1 -1
  4. orbi_cli-0.7.4/src/orbi/blocked_comment.py +35 -0
  5. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/claim.py +13 -13
  6. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/delivery_comment.py +44 -13
  7. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/failure.py +4 -6
  8. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/failure_report.py +51 -54
  9. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/language.py +32 -6
  10. orbi_cli-0.7.4/src/orbi/language_terms.py +77 -0
  11. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/progress.py +32 -33
  12. orbi_cli-0.7.4/src/orbi/progress_display.py +111 -0
  13. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/progress_summary.py +5 -3
  14. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/release.py +4 -6
  15. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/review_merge.py +27 -13
  16. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/runner.py +17 -16
  17. {orbi_cli-0.7.2 → orbi_cli-0.7.4/src/orbi_cli.egg-info}/PKG-INFO +1 -1
  18. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi_cli.egg-info/SOURCES.txt +9 -0
  19. orbi_cli-0.7.4/tests/test_blocked_comment.py +174 -0
  20. orbi_cli-0.7.4/tests/test_bootstrap_adapters.py +466 -0
  21. orbi_cli-0.7.4/tests/test_bootstrap_delivery.py +6125 -0
  22. orbi_cli-0.7.4/tests/test_bootstrap_pi.py +2856 -0
  23. orbi_cli-0.7.4/tests/test_bootstrap_preflight.py +2162 -0
  24. orbi_cli-0.7.4/tests/test_bootstrap_release.py +4579 -0
  25. orbi_cli-0.7.4/tests/test_bootstrap_runner.py +7075 -0
  26. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_deliver_pr_fakes.py +1 -1
  27. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_failure_report.py +44 -13
  28. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_fix_needed_loop.py +38 -2
  29. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_github_fakes.py +24 -2
  30. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_message_language.py +186 -7
  31. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_progress_wiring.py +104 -8
  32. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_resume_pr.py +113 -12
  33. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_review_merge.py +119 -9
  34. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_run_id_e2e.py +2 -1
  35. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_steering_review.py +1 -1
  36. orbi_cli-0.7.2/tests/test_bootstrap_runner.py +0 -24593
  37. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/LICENSE +0 -0
  38. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/README.md +0 -0
  39. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/docs/licenses/sustainable-use-license.md +0 -0
  40. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/setup.cfg +0 -0
  41. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/branch_reclaim.py +0 -0
  42. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/checks.py +0 -0
  43. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/clarify.py +0 -0
  44. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/cli.py +0 -0
  45. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/cli_source.py +0 -0
  46. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/comment_kind.py +0 -0
  47. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/config.py +0 -0
  48. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/delivery_labels.py +0 -0
  49. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/delivery_scene.py +0 -0
  50. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/engine_source.py +0 -0
  51. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/example_config.toml +0 -0
  52. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/git_identity.py +0 -0
  53. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/git_transport.py +0 -0
  54. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/github.py +0 -0
  55. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/gitops.py +0 -0
  56. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/human_review.py +0 -0
  57. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/journal.py +0 -0
  58. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/launchd_deploy.py +0 -0
  59. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/merge_gate.py +0 -0
  60. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/merge_handoff.py +0 -0
  61. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/milestone.py +0 -0
  62. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/milestone_command.py +0 -0
  63. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/notify.py +0 -0
  64. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/pi_activity.py +0 -0
  65. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/pi_command.py +0 -0
  66. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/pi_process.py +0 -0
  67. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/pi_recovery.py +0 -0
  68. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/pi_session.py +0 -0
  69. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/pilot_setup.py +0 -0
  70. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/pilot_slots.py +0 -0
  71. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/progress_outcome.py +0 -0
  72. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/release_comments.py +0 -0
  73. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/release_git.py +0 -0
  74. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/release_notes.py +0 -0
  75. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/repo_config.py +0 -0
  76. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/review_rounds.py +0 -0
  77. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/run_state.py +0 -0
  78. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/runner_health.py +0 -0
  79. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/scene.py +0 -0
  80. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/scheduler.py +0 -0
  81. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/scheduler_units.py +0 -0
  82. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/session_command.py +0 -0
  83. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/source_pool.py +0 -0
  84. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/status_observe.py +0 -0
  85. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/suggest.py +0 -0
  86. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/systemd_deploy.py +0 -0
  87. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi/ticket_command.py +0 -0
  88. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  89. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  90. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/src/orbi_cli.egg-info/top_level.txt +0 -0
  91. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_agents_md.py +0 -0
  92. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_apply_label_patch.py +0 -0
  93. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_branch_reclaim.py +0 -0
  94. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_ci_failure_issue_workflow.py +0 -0
  95. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_ci_failure_triage.py +0 -0
  96. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_ci_macos_workflow.py +0 -0
  97. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_ci_workflow.py +0 -0
  98. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_clarify.py +0 -0
  99. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_claude_plugin.py +0 -0
  100. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_cli_install.py +0 -0
  101. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_cli_milestone.py +0 -0
  102. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_cli_packaging.py +0 -0
  103. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_cli_source.py +0 -0
  104. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_comment_identity.py +0 -0
  105. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_comment_readers_contract.py +0 -0
  106. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_concurrency_e2e.py +0 -0
  107. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_constitution.py +0 -0
  108. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_coverage_gate.py +0 -0
  109. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_delivery_branch_delete.py +0 -0
  110. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_delivery_labels.py +0 -0
  111. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_delivery_scene.py +0 -0
  112. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docker_docs.py +0 -0
  113. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docker_entrypoint.py +0 -0
  114. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docker_image_workflow.py +0 -0
  115. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docs_i18n.py +0 -0
  116. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docs_issue_shapes.py +0 -0
  117. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docs_labels.py +0 -0
  118. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docs_mermaid.py +0 -0
  119. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docs_releases.py +0 -0
  120. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docs_single_source.py +0 -0
  121. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_docs_site.py +0 -0
  122. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_engine_source.py +0 -0
  123. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_failure.py +0 -0
  124. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_gh_read_retry.py +0 -0
  125. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_git_base_smoke.py +0 -0
  126. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_git_merge_smoke.py +0 -0
  127. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_git_network_retry.py +0 -0
  128. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_git_transport.py +0 -0
  129. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_github.py +0 -0
  130. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_gitops.py +0 -0
  131. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_gitops_fakes.py +0 -0
  132. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_human_review.py +0 -0
  133. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_import_graph.py +0 -0
  134. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_install_sh.py +0 -0
  135. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_issue_1367.py +0 -0
  136. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_issue_461.py +0 -0
  137. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_issue_template_config.py +0 -0
  138. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_journal.py +0 -0
  139. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_journal_events.py +0 -0
  140. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_launchd_deploy.py +0 -0
  141. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_license.py +0 -0
  142. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_milestone_command.py +0 -0
  143. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_notify.py +0 -0
  144. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_ops_e2e.py +0 -0
  145. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_orbi.py +0 -0
  146. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_orbi_dashboard.py +0 -0
  147. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_orbi_exporter.py +0 -0
  148. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_patch_ratchet.py +0 -0
  149. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pi_activity.py +0 -0
  150. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pi_command.py +0 -0
  151. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pi_extensions.py +0 -0
  152. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pi_provider_templates.py +0 -0
  153. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pi_recovery.py +0 -0
  154. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pi_session.py +0 -0
  155. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pilot_setup.py +0 -0
  156. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pilot_slots.py +0 -0
  157. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_pr_issue_link_workflow.py +0 -0
  158. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_prerequisite_check.py +0 -0
  159. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_progress.py +0 -0
  160. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_prompt_contract.py +0 -0
  161. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_public_contracts.py +0 -0
  162. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_publish_workflow.py +0 -0
  163. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_release_docs_workflow.py +0 -0
  164. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_release_ticket_templates.py +0 -0
  165. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_repo_config.py +0 -0
  166. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_resume_continue_e2e.py +0 -0
  167. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_resume_e2e.py +0 -0
  168. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_run_artifacts.py +0 -0
  169. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_runner_config.py +0 -0
  170. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_runner_health.py +0 -0
  171. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_runner_source_freshness.py +0 -0
  172. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_scene.py +0 -0
  173. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_scheduler.py +0 -0
  174. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_size_ratchet.py +0 -0
  175. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_src_layout.py +0 -0
  176. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_status_observe.py +0 -0
  177. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_suggest.py +0 -0
  178. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_suite_hygiene.py +0 -0
  179. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_systemd_deploy.py +0 -0
  180. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_systemd_timer.py +0 -0
  181. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_ticket_command.py +0 -0
  182. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/tests/test_user_acceptance_contract.py +0 -0
  183. {orbi_cli-0.7.2 → orbi_cli-0.7.4}/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.2
3
+ Version: 0.7.4
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.2"
47
+ version = "0.7.4"
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.2"
21
+ __version__ = "0.7.4"
@@ -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
- _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,
@@ -644,7 +645,7 @@ def pick_resumable_delivery(
644
645
  repo_dir: Path | None = None, *, hooks: ResumeHooks,
645
646
  language_code: str,
646
647
  ) -> tuple[dict, dict] | None:
647
- """Return the newest FREE opened-PR delivery and its resume scene.
648
+ """Return the oldest FREE opened-PR delivery and its resume scene.
648
649
 
649
650
  All opened-PR states are scanned: `ai-fix-needed`
650
651
  (awaiting the next review session after a finding or a base
@@ -691,12 +692,13 @@ def pick_resumable_delivery(
691
692
  excluded: `main` took it before the claim scan and holds it for the
692
693
  whole delivery.
693
694
 
694
- 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
695
697
  `max_concurrency` deliveries can be held by live co-runners, so a
696
698
  free candidate is inside the page when one exists; one whose OPEN PR
697
- still waits for CI (Issue #1473) is skipped too. The scan reviews the
698
- newest FREE candidate (held ones are skipped before any candidate
699
- 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).
700
702
  """
701
703
  held = slot_held_deliveries(slot_dir, max_concurrency)
702
704
  # `label:a,b` is GitHub's OR within one label qualifier
@@ -716,7 +718,7 @@ def pick_resumable_delivery(
716
718
  search=(
717
719
  f"label:{FIX_NEEDED_LABEL},{PR_OPENED_LABEL},"
718
720
  f"{AWAITING_MERGE_LABEL} "
719
- f"-label:{BLOCKED_LABEL} -label:{MERGED_LABEL}"
721
+ f"-label:{BLOCKED_LABEL} -label:{MERGED_LABEL} sort:created-asc"
720
722
  ),
721
723
  json_fields="number,title,state,url,labels,body",
722
724
  limit=max_concurrency + 1,
@@ -795,16 +797,14 @@ def pick_resumable_delivery(
795
797
  LOGGER.error("issue=%s resume scene is missing: %s", number, exc)
796
798
  marker = latest_run_marker(comments)
797
799
  try:
798
- apply_label_patch(
799
- number, repo=repo, event=EVENT_BLOCKED,
800
- current_labels={FIX_NEEDED_LABEL},
801
- )
802
- comment_issue(
800
+ post_blocked_comment(
803
801
  number, repo=repo,
804
802
  body=(f"{marker}\n" if marker else "")
805
803
  + language.text(
806
804
  language_code, "claim.scene_missing", error=exc,
807
805
  ),
806
+ current_labels={FIX_NEEDED_LABEL},
807
+ language_code=language_code,
808
808
  )
809
809
  except Exception:
810
810
  LOGGER.exception("issue=%s failure reporting failed", number)
@@ -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
- (the 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,7 +22,16 @@ 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
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
+ )
23
35
 
24
36
 
25
37
  def started_pi_comment_body(ctx: RunContext, run_info: str,
@@ -38,20 +50,38 @@ def started_pi_comment_body(ctx: RunContext, run_info: str,
38
50
  if extra_fields:
39
51
  fields.update(extra_fields)
40
52
  info = _run_info_fields(run_info)
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
+ }
41
59
  headline = language.text(
42
60
  language_code, "delivery.started_headline",
43
61
  ) + " " + " ".join(
44
- f"{key}={info[key]}" for key in ("run_id", "priority")
45
- if key in info
62
+ f"{key}={value}" for key, value in rendered_info.items()
46
63
  )
64
+ detail_keys = {"base_sha", "repo_config", "branch", "session"}
47
65
  return field_block(
48
- ctx.run_id, headline, fields,
49
- detail_keys={"base_sha", "repo_config", "branch", "session"},
66
+ ctx.run_id, headline, localized_fields(language_code, fields),
67
+ detail_keys={field_label(language_code, key) for key in detail_keys},
50
68
  kind=COMMENT_KIND_STARTED,
51
69
  language_code=language_code,
52
70
  )
53
71
 
54
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,
82
+ )
83
+
84
+
55
85
  def opened_pr_comment_body(run_id: str, run_info: str, pr_url: str,
56
86
  external: bool = False, *,
57
87
  language_code: str,
@@ -86,9 +116,10 @@ def opened_pr_comment_body(run_id: str, run_info: str, pr_url: str,
86
116
  pr_url=pr_url,
87
117
  external="true" if external else "",
88
118
  )
119
+ detail_keys = {"base_sha", "repo_config", "branch", "session"}
89
120
  body = field_block(
90
- run_id, headline, fields,
91
- detail_keys={"base_sha", "repo_config", "branch", "session"},
121
+ run_id, headline, localized_fields(language_code, fields),
122
+ detail_keys={field_label(language_code, key) for key in detail_keys},
92
123
  language_code=language_code,
93
124
  )
94
125
  lines = body.splitlines()
@@ -529,9 +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
- body = "\n\n".join(parts + ["\n".join(details)])
533
- if retrying:
534
- return body
535
- # The blocked diagnostic is a problem a person must resolve, so its
536
- # last paragraph is the community help line (Issue #1684).
537
- return f"{body}\n\n{message('help.community')}"
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.
535
+ return "\n\n".join(parts + ["\n".join(details)])
@@ -27,12 +27,13 @@ import re
27
27
  from pathlib import Path
28
28
 
29
29
  from orbi import failure, language, runner_health, scene
30
+ from orbi.blocked_comment import post_blocked_comment
30
31
  from orbi.comment_kind import (
31
32
  COMMENT_KIND_FAILURE,
32
33
  comment_kind_marker,
33
34
  )
34
35
  from orbi.delivery_labels import (
35
- EVENT_BLOCKED, EVENT_FIX_NEEDED, FIX_NEEDED_LABEL, IN_PROGRESS_LABEL,
36
+ EVENT_FIX_NEEDED, FIX_NEEDED_LABEL, IN_PROGRESS_LABEL,
36
37
  )
37
38
  from orbi.failure import (
38
39
  _failure_comment_body, _failure_detail, _failure_summary,
@@ -488,10 +489,6 @@ def block_scene_failure(issue: dict, error: ValueError, repo: str,
488
489
  )
489
490
  marker = latest_run_marker(comments)
490
491
  try:
491
- apply_label_patch(
492
- number, repo=repo, event=EVENT_BLOCKED,
493
- current_labels={FIX_NEEDED_LABEL},
494
- )
495
492
  # A scene that cannot be recovered is an external
496
493
  # precondition the AI cannot fix by itself (the runner cannot
497
494
  # derive run_id, branch, worktree or PR without the trusted
@@ -499,7 +496,7 @@ def block_scene_failure(issue: dict, error: ValueError, repo: str,
499
496
  # comment states the EXPLICIT reason why automatic recovery is
500
497
  # impossible plus the human next step (restore the scene comment,
501
498
  # then the ONE shared unblock step of the catalog, Issue #1724).
502
- comment_issue(
499
+ post_blocked_comment(
503
500
  number, repo=repo,
504
501
  body=(
505
502
  (f"{marker}\n"
@@ -508,6 +505,8 @@ def block_scene_failure(issue: dict, error: ValueError, repo: str,
508
505
  ) + language.text(
509
506
  language_code, "failure.scene_blocked", error=error,
510
507
  ),
508
+ current_labels={FIX_NEEDED_LABEL},
509
+ language_code=language_code,
511
510
  )
512
511
  except Exception:
513
512
  LOGGER.exception("issue=%s failure reporting failed", number)
@@ -529,11 +528,7 @@ def block_repo_config_failure(number: int, source_repo: str,
529
528
  cleanly (`main` releases the slot in its `finally`).
530
529
  """
531
530
  try:
532
- apply_label_patch(
533
- number, repo=source_repo, event=EVENT_BLOCKED,
534
- current_labels=set(current_labels),
535
- )
536
- comment_issue(
531
+ post_blocked_comment(
537
532
  number, repo=source_repo,
538
533
  body=(
539
534
  f"{run_marker(run_id)}\n"
@@ -542,6 +537,8 @@ def block_repo_config_failure(number: int, source_repo: str,
542
537
  language_code, "failure.repo_config_blocked", error=error,
543
538
  )
544
539
  ),
540
+ current_labels=set(current_labels),
541
+ language_code=language_code,
545
542
  )
546
543
  except Exception:
547
544
  LOGGER.exception("issue=%s repo_config_failure_report_failed", number)
@@ -600,14 +597,10 @@ def _report_resume_failure(*, number: int, source_repo: str, run_id: str,
600
597
  verified: continuing on a guessed identity risks a
601
598
  silent fresh redo on top of unknown work, and a fresh run would
602
599
  lose the existing work. The Issue goes `ai-blocked` ALONE (the
603
- claim label removed) with the exact reason, then the community
604
- help line — a human decides (restore or remove the worktree, relabel).
600
+ claim label removed) with the exact reason — a human decides (restore
601
+ or remove the worktree, relabel).
605
602
  """
606
- apply_label_patch(
607
- number, repo=source_repo, event=EVENT_BLOCKED,
608
- current_labels={IN_PROGRESS_LABEL},
609
- )
610
- comment_issue(
603
+ post_blocked_comment(
611
604
  number, repo=source_repo,
612
605
  body=(
613
606
  f"{run_marker(run_id)}\n"
@@ -616,8 +609,10 @@ def _report_resume_failure(*, number: int, source_repo: str, run_id: str,
616
609
  language_code, "failure.resume_blocked",
617
610
  detail=_redact_local_paths(_failure_detail(error)),
618
611
  run_id=run_id,
619
- ) + f"\n\n{language.text(language_code, 'help.community')}"
612
+ )
620
613
  ),
614
+ current_labels={IN_PROGRESS_LABEL},
615
+ language_code=language_code,
621
616
  )
622
617
 
623
618
 
@@ -804,52 +799,47 @@ def report_delivery_failure(
804
799
  action if workflow_refusal and action
805
800
  else _failure_summary(reason, language_code)
806
801
  )
802
+ scene = scene_line() or "- run: `-`"
807
803
  if blocked:
808
- apply_label_patch(
809
- number, repo=source_repo, event=EVENT_BLOCKED,
810
- current_labels=labels,
811
- )
812
- scene = scene_line() or "- run: `-`"
813
- body = _failure_comment_body(
814
- outcome="blocked", action=action, reason=reason,
815
- diagnosis=diagnosis, scene=scene, evidence=evidence_detail,
816
- pr_url=pr_url, issue=issue_context(source_repo, number),
817
- run_id=run_id or "-", failure_record=failure_record,
818
- language_code=language_code,
819
- )
820
- if classify:
821
- body = body.replace(
822
- "</details>",
823
- f"\n{language.text(language_code, _BLOCKED_PRECONDITION_PHRASE_KEY)}\n</details>",
824
- 1,
825
- )
826
- if blocked_suffix:
827
- body = body.replace(
828
- "</details>",
829
- f"\n{blocked_suffix.lstrip('; ')}\n</details>", 1,
830
- )
831
804
  outcome = "blocked"
832
805
  else:
806
+ # The recoverable scene is the only one that rewrites labels here:
807
+ # the blocked exit owns its own patch (Issue #1730).
833
808
  apply_label_patch(
834
809
  number, repo=source_repo, event=EVENT_FIX_NEEDED,
835
810
  current_labels=labels,
836
811
  )
837
- scene = scene_line() or "- run: `-`"
838
812
  if review_scene_block is not None:
839
813
  scene += f"\n{_redact_local_paths(review_scene_block)}"
840
- body = _failure_comment_body(
841
- outcome="fix needed", action=action, reason=reason,
842
- diagnosis=diagnosis, scene=scene, evidence=evidence_detail,
843
- pr_url=pr_url, issue=issue_context(source_repo, number),
844
- run_id=run_id or "-", failure_record=failure_record,
845
- language_code=language_code,
846
- )
847
814
  outcome = "fix needed"
815
+ body = _failure_comment_body(
816
+ outcome="blocked" if blocked else "fix needed",
817
+ action=action, reason=reason, diagnosis=diagnosis, scene=scene,
818
+ evidence=evidence_detail, pr_url=pr_url,
819
+ issue=issue_context(source_repo, number), run_id=run_id or "-",
820
+ failure_record=failure_record, language_code=language_code,
821
+ )
822
+ if blocked and classify:
823
+ body = body.replace(
824
+ "</details>",
825
+ f"\n{language.text(language_code, _BLOCKED_PRECONDITION_PHRASE_KEY)}\n</details>",
826
+ 1,
827
+ )
828
+ if blocked and blocked_suffix:
829
+ body = body.replace(
830
+ "</details>",
831
+ f"\n{blocked_suffix.lstrip('; ')}\n</details>", 1,
832
+ )
848
833
  # An oversized comment shrinks ONLY its evidence segments, never its
849
834
  # structure: a cut lands inside the last fence, so the closer and the
850
- # help line render as code in the collapsed block (Issue #1711).
835
+ # help line render as code in the collapsed block (Issue #1711). The
836
+ # help line the blocked exit appends is inside that budget too (#1730).
837
+ help_suffix = (
838
+ f"\n\n{language.text(language_code, 'help.community')}" if blocked else ""
839
+ )
851
840
  cap = EVIDENCE_SEGMENT_CAP
852
- while evidence_detail and cap and len(body) > FAILURE_COMMENT_MAX_CHARS:
841
+ while (evidence_detail and cap
842
+ and len(body) + len(help_suffix) > FAILURE_COMMENT_MAX_CHARS):
853
843
  cap = max(0, cap - _EVIDENCE_CAP_STEP)
854
844
  shrunk = _failure_evidence(
855
845
  worktree, exc, language_code=language_code, cap=cap)
@@ -928,9 +918,16 @@ def report_delivery_failure(
928
918
  "failure_comment_deduplicated", issue=number,
929
919
  run_id=run_id, fingerprint=fingerprint,
930
920
  )
931
- if post_new_comment:
921
+ if post_new_comment and blocked:
922
+ # The blocked exit owns the label patch and the help line (Issue
923
+ # #1730): the label lands first, the comment second.
924
+ post_blocked_comment(
925
+ number, repo=source_repo, body=body,
926
+ current_labels=labels, language_code=language_code,
927
+ )
928
+ elif post_new_comment:
932
929
  comment_issue(number, repo=source_repo, body=body)
933
- if pr_url and not blocked:
930
+ if pr_url:
934
931
  # The recoverable scene is written to the PR too:
935
932
  # the next review session and any human watcher see it where
936
933
  # the delivery lives. A blocked Issue is terminal — Issue only.
@@ -37,6 +37,8 @@ import os
37
37
  import re
38
38
  from typing import Mapping
39
39
 
40
+ from orbi.language_terms import DISPLAY_TERMS
41
+
40
42
  LANGUAGE_EN = "en"
41
43
  LANGUAGE_ZH = "zh"
42
44
  LANGUAGES = (LANGUAGE_EN, LANGUAGE_ZH)
@@ -220,7 +222,7 @@ MESSAGES: dict[str, tuple[str, str]] = {
220
222
  "review.findings_heading": ("### Findings", "### 评审发现"),
221
223
  "review.findings_milestone": ("review findings: round {round}, {blockers} blocker(s), {majors} major(s) for PR #{pr_number}", "评审发现:第 {round} 轮,PR #{pr_number} 有 {blockers} 个 blocker、{majors} 个 major"),
222
224
  "review.findings_outcome": ("**Orbi review findings**\n\nround {round}: {blockers} blocker(s), {majors} major(s); the next review session retries the same PR automatically", "**Orbi 评审发现**\n\n第 {round} 轮:{blockers} 个 blocker、{majors} 个 major;下一个评审 session 会自动重试同一个 PR"),
223
- "review.delivered_outcome": ("**Orbi delivered**\n\nPR {pr_url} merged (merge_commit={merge_commit} review_rounds={round})", "**Orbi 已交付**\n\nPR {pr_url} 已合并(merge_commit={merge_commit} review_rounds={round})"),
225
+ "review.delivered_outcome": ("**Orbi delivered**\n\nPR {pr_url} merged (merge_commit={merge_commit} review_rounds={round})", "**Orbi 已交付**\n\nPR {pr_url} 已合并(合并提交 {merge_commit},共 {round} 个评审轮次)"),
224
226
  "review.base_advance_heading": ("Orbi base advance retry {round} for PR #{pr_number}:", "Orbi base 同步重试第 {round} 次,PR #{pr_number}:"),
225
227
  "review.gate_ci_blocked": ("CI merge gate blocked: {message} (run_id={run_id})", "CI 合并门禁被阻塞:{message}(run_id={run_id})"),
226
228
  "review.gate_blocked": ("Merge gate blocked: {message} (run_id={run_id})", "合并门禁被阻塞:{message}(run_id={run_id})"),
@@ -289,6 +291,7 @@ MESSAGES: dict[str, tuple[str, str]] = {
289
291
  "progress.header": ("**Orbi progress**", "**Orbi 进度**"),
290
292
  "progress.run_details": ("Run details", "运行详情"),
291
293
  "progress.milestone": ("Orbi:", "Orbi:"),
294
+ "progress.milestone_heading": ("{prefix} {headline}", "{prefix}{headline}"),
292
295
  "progress.field.role": ("role", "角色"),
293
296
  "progress.field.step": ("step", "阶段"),
294
297
  "progress.field.reported_at": ("reported at", "报告时间"),
@@ -580,6 +583,10 @@ MESSAGES: dict[str, tuple[str, str]] = {
580
583
  "The delivery stopped before it could be completed: {detail}",
581
584
  "交付在完成之前停止了:{detail}",
582
585
  ),
586
+ "runner.pr_attribution": (
587
+ "Built by [Orbi]({link}) from Issue #{issue}",
588
+ "由 [Orbi]({link}) 构建,基于 Issue #{issue}",
589
+ ),
583
590
  "runner.pr_closed_milestone": ("blocked: PR {pr_url} was closed without a merge; the delivery is terminally failed", "已阻塞:PR {pr_url} 未合并就被关闭;本次交付最终失败"),
584
591
  # The standalone terminal failure comment of a closed PR (the
585
592
  # milestone above and the progress scene carry the same fact).
@@ -635,9 +642,12 @@ MESSAGES: dict[str, tuple[str, str]] = {
635
642
  ),
636
643
  # The implement-phase finish: the PR was OPENED, not merged, so this
637
644
  # is its own key (`review.delivered_outcome` is the merge outcome).
645
+ # It names the wait that follows (checks and the review session),
646
+ # never a wait duration: the tick interval is each deployment's own
647
+ # setting (Issue #1733).
638
648
  "delivery.outcome_pr_opened": (
639
- "**Orbi delivered**",
640
- "**Orbi 已交付**",
649
+ "**Orbi opened the PR: waiting for checks and review**",
650
+ "**Orbi 已打开 PR:等待检查与评审**",
641
651
  ),
642
652
  "delivery.recoverable_outcome": (
643
653
  "**Orbi {recoverable_name}**\n\n"
@@ -665,7 +675,7 @@ MESSAGES: dict[str, tuple[str, str]] = {
665
675
  ),
666
676
  "review.summary_human_action": ("maintainer-actionable merge retry", "需要维护者处理的合并重试"),
667
677
  "review.summary_pass_recorded": ("pass recorded on {head}; merged after CI", "在 {head} 上记录通过;CI 之后合并"),
668
- "review.summary_pass_clean": ("pass, no findings", "通过,没有发现"),
678
+ "review.summary_pass_clean": ("pass, no findings", "通过,没有发现问题"),
669
679
  "review.summary_pass_fixed": ("pass, {findings} findings fixed in-session", "通过,会话内修复了 {findings} 项发现"),
670
680
 
671
681
  # Delivery failures (blocked / fix needed).
@@ -841,12 +851,12 @@ MESSAGES: dict[str, tuple[str, str]] = {
841
851
  ),
842
852
  "clarify.repair_outcome": (
843
853
  "Outcome: what a user does, and what they should see differently.",
844
- "Outcome:用户要做什么,以及应该看到什么变化。",
854
+ "结果:用户要做什么,以及应该看到什么变化。",
845
855
  ),
846
856
  "clarify.repair_acceptance": (
847
857
  "Acceptance: how a finished delivery is told apart from an "
848
858
  "unfinished one.",
849
- "Acceptance:如何判断交付已完成、而不是半成品。",
859
+ "验收:如何判断交付已完成、而不是半成品。",
850
860
  ),
851
861
  "clarify.followup": (
852
862
  "Orbi re-checks the ticket when it claims it and starts the "
@@ -881,6 +891,10 @@ MESSAGES: dict[str, tuple[str, str]] = {
881
891
  ),
882
892
  }
883
893
 
894
+ # The display vocabulary of the run comments lives in its own module to
895
+ # keep this catalog owner under the size cap (Issue #1729).
896
+ MESSAGES.update(DISPLAY_TERMS)
897
+
884
898
 
885
899
  UNBLOCK_STEP_FIELD = "unblock_step"
886
900
 
@@ -905,6 +919,18 @@ def text(language: str, key: str, **fields: object) -> str:
905
919
  return template.format(**fields) if fields else template
906
920
 
907
921
 
922
+ def term(language: str, key: str, default: str) -> str:
923
+ """One catalogue term, or `default` when the key has no entry.
924
+
925
+ Used for display vocabulary whose raw value must survive when the
926
+ catalogue does not know it (an external tool name, a future phase):
927
+ the caller keeps the raw value instead of inventing a translation.
928
+ """
929
+ if key not in MESSAGES:
930
+ return default
931
+ return text(language, key)
932
+
933
+
908
934
  # --- the model-session boundary -------------------------------------------
909
935
  #
910
936
  # The instruction is appended to the rendered system prompt AT THE SESSION
@@ -0,0 +1,77 @@
1
+ """The display vocabulary of Orbi's run comments (Issue #1729).
2
+
3
+ Split out of `language.MESSAGES` so the catalog owner stays under the
4
+ size ratchet's 1,000-line cap. Every key here is a display label or a
5
+ stored display value; `language.MESSAGES` merges this map in place, so
6
+ `language.text()` / `language.term()` remain the ONE reading point. No
7
+ machine row (run marker, `run_id=`, `repo_config:`, `failure:`, receipt
8
+ rows) has an entry here.
9
+ """
10
+
11
+ DISPLAY_TERMS: dict[str, tuple[str, str]] = {
12
+ # Display labels of the started / opened-PR scene field blocks. The
13
+ # machine rows (`run_id=`, `repo_config:`) have no entry here and stay
14
+ # verbatim (Issue #1729).
15
+ "progress.field.base_branch": ("base_branch", "基础分支"),
16
+ "progress.field.base_sha": ("base_sha", "基础 SHA"),
17
+ "progress.field.task_type": ("task_type", "任务类型"),
18
+ "progress.field.external": ("external", "外部"),
19
+ "progress.field.repo_config_ignored": (
20
+ "repo_config_ignored", "忽略的仓库配置键",
21
+ ),
22
+ "progress.field.repo_config_changed": (
23
+ "repo_config_changed", "仓库配置变更",
24
+ ),
25
+ "progress.field.repo_config_diff": ("repo_config_diff", "仓库配置差异"),
26
+ # Display labels of the merged milestone's visible rows.
27
+ "progress.field.result": ("result", "结果"),
28
+ "progress.field.merge_commit": ("merge_commit", "合并提交"),
29
+ "progress.field.review_rounds": ("review_rounds", "评审轮次"),
30
+ "progress.field.external_commits": ("external_commits", "外部提交"),
31
+ "progress.field.commits": ("commits", "提交数"),
32
+ # The display vocabulary of the progress values (Issue #1729): the
33
+ # stored value stays the raw machine word, the rendered one follows
34
+ # the message language. An unknown value renders verbatim.
35
+ "progress.role.implement": ("implement", "实现"),
36
+ "progress.role.review": ("review", "评审"),
37
+ "progress.role.release": ("release", "发布"),
38
+ "progress.role.ticket": ("ticket", "工单"),
39
+ "progress.role.suggest": ("suggest", "建议"),
40
+ "progress.step.starting": ("starting", "启动中"),
41
+ "progress.step.implement": ("implement", "实现"),
42
+ "progress.step.review": ("review", "评审"),
43
+ "progress.step.checks": ("checks", "检查"),
44
+ "progress.step.merge": ("merge", "合并"),
45
+ "progress.step.release": ("release", "发布"),
46
+ "progress.priority.p0": ("p0", "P0"),
47
+ "progress.priority.normal": ("normal", "普通"),
48
+ "progress.tests.passed": ("passed", "通过"),
49
+ "progress.tests.pending": ("pending", "等待中"),
50
+ "progress.tests.failed": ("failed: {names}", "失败:{names}"),
51
+ "progress.activity.read": ("read", "读取"),
52
+ "progress.activity.edit": ("edit", "编辑"),
53
+ "progress.activity.command": ("command", "命令"),
54
+ "progress.activity.analysis": ("analysis", "分析"),
55
+ "progress.phase.starting": ("starting", "启动中"),
56
+ "progress.phase.session_pending": ("session_pending", "等待会话"),
57
+ "progress.phase.request_pending": ("request_pending", "等待响应"),
58
+ "progress.phase.read": ("read", "读取"),
59
+ "progress.phase.edit": ("edit", "编辑"),
60
+ "progress.phase.write": ("write", "写入"),
61
+ "progress.phase.bash": ("bash", "命令行"),
62
+ "progress.phase.pr": ("pr", "PR"),
63
+ "progress.phase.push": ("push", "推送"),
64
+ "progress.phase.commit": ("commit", "提交"),
65
+ "progress.phase.base": ("base", "基线"),
66
+ "progress.phase.worktree": ("worktree", "工作树"),
67
+ "progress.phase.ui": ("ui", "界面"),
68
+ "progress.task_type.ticket-only": ("ticket-only", "仅工单"),
69
+ "progress.task_type.ops": ("ops", "运维"),
70
+ "progress.boolean.true": ("true", "是"),
71
+ "progress.boolean.false": ("false", "否"),
72
+ # The merged milestone's display headline. It is the ONLY milestone
73
+ # whose event word arrives raw: every other milestone text is already
74
+ # a localized sentence from `language.text`, so an entry for its
75
+ # English word would never be reached (Issue #1729).
76
+ "progress.milestone_headline.merged": ("merged", "已合并"),
77
+ }