orbi-cli 0.7.1__tar.gz → 0.7.2__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 (173) hide show
  1. {orbi_cli-0.7.1/src/orbi_cli.egg-info → orbi_cli-0.7.2}/PKG-INFO +1 -1
  2. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/pyproject.toml +1 -1
  3. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/__init__.py +1 -1
  4. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/claim.py +34 -31
  5. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/clarify.py +6 -4
  6. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/delivery_comment.py +7 -16
  7. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/delivery_scene.py +0 -4
  8. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/failure.py +11 -6
  9. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/failure_report.py +46 -45
  10. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/github.py +12 -21
  11. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/human_review.py +9 -3
  12. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/language.py +311 -27
  13. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/merge_handoff.py +2 -2
  14. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/milestone.py +57 -31
  15. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/milestone_command.py +8 -5
  16. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/notify.py +177 -40
  17. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/pi_session.py +4 -2
  18. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/progress.py +20 -15
  19. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/progress_outcome.py +12 -1
  20. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/release.py +12 -12
  21. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/release_comments.py +47 -2
  22. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/repo_config.py +18 -0
  23. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/review_merge.py +69 -67
  24. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/review_rounds.py +2 -2
  25. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/runner.py +75 -66
  26. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/runner_health.py +17 -10
  27. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/suggest.py +59 -15
  28. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/ticket_command.py +61 -19
  29. {orbi_cli-0.7.1 → orbi_cli-0.7.2/src/orbi_cli.egg-info}/PKG-INFO +1 -1
  30. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_bootstrap_runner.py +633 -117
  31. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_clarify.py +3 -3
  32. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_comment_identity.py +5 -5
  33. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_comment_readers_contract.py +6 -5
  34. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_deliver_pr_fakes.py +3 -3
  35. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_delivery_branch_delete.py +5 -5
  36. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_delivery_scene.py +64 -14
  37. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_failure.py +21 -21
  38. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_failure_report.py +362 -26
  39. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_fix_needed_loop.py +28 -9
  40. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_git_merge_smoke.py +6 -6
  41. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_human_review.py +9 -8
  42. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_message_language.py +271 -11
  43. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_milestone_command.py +203 -0
  44. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_notify.py +266 -32
  45. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pilot_setup.py +27 -0
  46. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_progress.py +62 -30
  47. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_progress_wiring.py +75 -6
  48. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_repo_config.py +32 -11
  49. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_resume_continue_e2e.py +6 -0
  50. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_resume_e2e.py +142 -4
  51. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_resume_pr.py +146 -51
  52. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_review_merge.py +33 -33
  53. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_runner_config.py +74 -0
  54. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_runner_health.py +127 -2
  55. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_steering_review.py +26 -0
  56. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_suggest.py +283 -20
  57. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_ticket_command.py +3 -1
  58. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/LICENSE +0 -0
  59. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/README.md +0 -0
  60. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/docs/licenses/sustainable-use-license.md +0 -0
  61. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/setup.cfg +0 -0
  62. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/branch_reclaim.py +0 -0
  63. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/checks.py +0 -0
  64. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/cli.py +0 -0
  65. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/cli_source.py +0 -0
  66. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/comment_kind.py +0 -0
  67. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/config.py +0 -0
  68. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/delivery_labels.py +0 -0
  69. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/engine_source.py +0 -0
  70. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/example_config.toml +0 -0
  71. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/git_identity.py +0 -0
  72. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/git_transport.py +0 -0
  73. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/gitops.py +0 -0
  74. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/journal.py +0 -0
  75. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/launchd_deploy.py +0 -0
  76. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/merge_gate.py +0 -0
  77. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/pi_activity.py +0 -0
  78. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/pi_command.py +0 -0
  79. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/pi_process.py +0 -0
  80. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/pi_recovery.py +0 -0
  81. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/pilot_setup.py +0 -0
  82. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/pilot_slots.py +0 -0
  83. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/progress_summary.py +0 -0
  84. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/release_git.py +0 -0
  85. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/release_notes.py +0 -0
  86. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/run_state.py +0 -0
  87. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/scene.py +0 -0
  88. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/scheduler.py +0 -0
  89. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/scheduler_units.py +0 -0
  90. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/session_command.py +0 -0
  91. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/source_pool.py +0 -0
  92. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/status_observe.py +0 -0
  93. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi/systemd_deploy.py +0 -0
  94. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/SOURCES.txt +0 -0
  95. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  96. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  97. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/top_level.txt +0 -0
  98. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_agents_md.py +0 -0
  99. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_apply_label_patch.py +0 -0
  100. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_branch_reclaim.py +0 -0
  101. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_ci_failure_issue_workflow.py +0 -0
  102. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_ci_failure_triage.py +0 -0
  103. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_ci_macos_workflow.py +0 -0
  104. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_ci_workflow.py +0 -0
  105. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_claude_plugin.py +0 -0
  106. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_cli_install.py +0 -0
  107. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_cli_milestone.py +0 -0
  108. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_cli_packaging.py +0 -0
  109. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_cli_source.py +0 -0
  110. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_concurrency_e2e.py +0 -0
  111. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_constitution.py +0 -0
  112. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_coverage_gate.py +0 -0
  113. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_delivery_labels.py +0 -0
  114. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docker_docs.py +0 -0
  115. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docker_entrypoint.py +0 -0
  116. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docker_image_workflow.py +0 -0
  117. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docs_i18n.py +0 -0
  118. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docs_issue_shapes.py +0 -0
  119. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docs_labels.py +0 -0
  120. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docs_mermaid.py +0 -0
  121. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docs_releases.py +0 -0
  122. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docs_single_source.py +0 -0
  123. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_docs_site.py +0 -0
  124. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_engine_source.py +0 -0
  125. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_gh_read_retry.py +0 -0
  126. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_git_base_smoke.py +0 -0
  127. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_git_network_retry.py +0 -0
  128. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_git_transport.py +0 -0
  129. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_github.py +0 -0
  130. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_github_fakes.py +0 -0
  131. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_gitops.py +0 -0
  132. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_gitops_fakes.py +0 -0
  133. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_import_graph.py +0 -0
  134. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_install_sh.py +0 -0
  135. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_issue_1367.py +0 -0
  136. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_issue_461.py +0 -0
  137. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_issue_template_config.py +0 -0
  138. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_journal.py +0 -0
  139. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_journal_events.py +0 -0
  140. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_launchd_deploy.py +0 -0
  141. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_license.py +0 -0
  142. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_ops_e2e.py +0 -0
  143. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_orbi.py +0 -0
  144. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_orbi_dashboard.py +0 -0
  145. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_orbi_exporter.py +0 -0
  146. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_patch_ratchet.py +0 -0
  147. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pi_activity.py +0 -0
  148. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pi_command.py +0 -0
  149. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pi_extensions.py +0 -0
  150. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pi_provider_templates.py +0 -0
  151. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pi_recovery.py +0 -0
  152. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pi_session.py +0 -0
  153. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pilot_slots.py +0 -0
  154. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_pr_issue_link_workflow.py +0 -0
  155. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_prerequisite_check.py +0 -0
  156. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_prompt_contract.py +0 -0
  157. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_public_contracts.py +0 -0
  158. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_publish_workflow.py +0 -0
  159. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_release_docs_workflow.py +0 -0
  160. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_release_ticket_templates.py +0 -0
  161. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_run_artifacts.py +0 -0
  162. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_run_id_e2e.py +0 -0
  163. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_runner_source_freshness.py +0 -0
  164. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_scene.py +0 -0
  165. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_scheduler.py +0 -0
  166. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_size_ratchet.py +0 -0
  167. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_src_layout.py +0 -0
  168. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_status_observe.py +0 -0
  169. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_suite_hygiene.py +0 -0
  170. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_systemd_deploy.py +0 -0
  171. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_systemd_timer.py +0 -0
  172. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/tests/test_user_acceptance_contract.py +0 -0
  173. {orbi_cli-0.7.1 → orbi_cli-0.7.2}/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.2
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.2"
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.2"
@@ -29,7 +29,7 @@ from orbi.delivery_scene import (
29
29
  EXTERNAL_PR_MARKER, DeliveryScene, _issue_label_set, body_markers, classify,
30
30
  )
31
31
  from orbi.github import (
32
- _epic_audit, _verify_epic_complete, apply_label_patch, close_issue,
32
+ _verify_epic_complete, apply_label_patch, close_issue,
33
33
  comment_issue, issue_comments, issue_priority, issue_view,
34
34
  latest_run_marker, list_issues, milestone_open_issue_count,
35
35
  open_blocker_numbers, open_pr_for_branch, pr_comments,
@@ -40,8 +40,10 @@ from orbi.journal import LOGGER, current_run_id, event, new_run_id, set_run_id
40
40
  from orbi.pilot_slots import slot_held_deliveries, slot_occupancy
41
41
  from orbi import language
42
42
  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
43
+ from orbi.release import _epic_audit, epic_audit_posted, release_target_milestone
44
+ from orbi.repo_config import (
45
+ RepoConfigError, RepoPolicy, load_repo_policy, merged_repo_language,
46
+ )
45
47
 
46
48
 
47
49
  class ResumeHooks(NamedTuple):
@@ -49,11 +51,14 @@ class ResumeHooks(NamedTuple):
49
51
 
50
52
  The physical scene handling of `pick_resumable_delivery` plus the PR
51
53
  comment writer `reconcile_orphan_prs` reports its orphan through.
54
+ Every hook that writes a comment takes the claim's already-resolved
55
+ per-repository `language_code` (Issue #1687): the claim knows the
56
+ merged repository config before the first comment is written.
52
57
  """
53
58
  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]
59
+ route_external_pr_ticket: Callable[..., bool]
60
+ block_scene_failure: Callable[..., None]
61
+ recover_missing_pr_scene: Callable[..., dict | None]
57
62
  has_recoverable_pr_scene: Callable[[dict, str, Path], bool]
58
63
  comment_pr: Callable[..., None]
59
64
 
@@ -195,7 +200,7 @@ def is_release(issue: dict) -> bool:
195
200
  return False
196
201
 
197
202
 
198
- def reconcile_open_epics(repo: str, run_id: str) -> list[str]:
203
+ def reconcile_open_epics(repo: str, run_id: str, language_code: str) -> list[str]:
199
204
  """Sweep open Epics once per tick; ordinary pickup must not depend on it."""
200
205
  epics = list_issues(
201
206
  repo, state="open", search=f"label:{EPIC_LABEL}",
@@ -211,9 +216,9 @@ def reconcile_open_epics(repo: str, run_id: str) -> list[str]:
211
216
  event("epic_kept_open", issue=number, repo=repo, reason=reason)
212
217
  evidence.append(f"Epic #{number} kept open: {reason}")
213
218
  continue
214
- audit = _epic_audit(child_evidence)
219
+ audit = _epic_audit(child_evidence, None, language_code)
215
220
  comments = issue_comments(int(number), repo=repo)
216
- if not any(audit in str(comment.get("body", "")) for comment in comments):
221
+ if not epic_audit_posted(comments, child_evidence):
217
222
  comment_issue(int(number), repo=repo,
218
223
  body=f"<!-- orbi:run={run_id} -->\n{audit}\nrun_id={run_id}")
219
224
  close_issue(int(number), repo=repo)
@@ -243,7 +248,7 @@ def orphan_pr_branch_issue(head_ref: object) -> int | None:
243
248
 
244
249
 
245
250
  def reconcile_orphan_prs(
246
- repo: str, run_id: str, *, hooks: ResumeHooks,
251
+ repo: str, run_id: str, *, hooks: ResumeHooks, language_code: str,
247
252
  ) -> list[str]:
248
253
  """Report open delivery PRs whose source Issue is closed.
249
254
 
@@ -287,11 +292,10 @@ def reconcile_orphan_prs(
287
292
  pr_number, repo=repo,
288
293
  body=(
289
294
  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}"
295
+ + language.text(
296
+ language_code, "claim.orphan_pr",
297
+ issue_number=issue_number, run_id=run_id,
298
+ )
295
299
  ),
296
300
  )
297
301
  event(
@@ -638,7 +642,7 @@ def pick_next_issue(
638
642
  def pick_resumable_delivery(
639
643
  repo: str, slot_dir: Path, max_concurrency: int,
640
644
  repo_dir: Path | None = None, *, hooks: ResumeHooks,
641
- language_code: str = language.DEFAULT_LANGUAGE,
645
+ language_code: str,
642
646
  ) -> tuple[dict, dict] | None:
643
647
  """Return the newest FREE opened-PR delivery and its resume scene.
644
648
 
@@ -734,15 +738,15 @@ def pick_resumable_delivery(
734
738
  # probe the #726 external route first; otherwise this is the
735
739
  # ONLY trigger of `block_scene_failure` — a present-but-broken
736
740
  # scene is a writer bug or tampering and needs a human.
737
- if hooks.route_external_pr_ticket(issue, repo):
741
+ if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
738
742
  continue
739
- hooks.block_scene_failure(issue, exc, repo, comments)
743
+ hooks.block_scene_failure(issue, exc, repo, comments, language_code=language_code)
740
744
  continue
741
745
  except scene.SceneMissingError as exc:
742
746
  # No trusted comment carries a scene at all — a distinct branch
743
747
  # from corruption. The original #726 incident was
744
748
  # exactly this shape, so the external route is probed first.
745
- if hooks.route_external_pr_ticket(issue, repo):
749
+ if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
746
750
  continue
747
751
  # Issue #1216: `awaiting_merge` can deliberately remove the
748
752
  # queue label before the delivery returns to `ai-fix-needed`.
@@ -774,7 +778,7 @@ def pick_resumable_delivery(
774
778
  # scene comment write. Retry it from the durable local run
775
779
  # state; keep ai-pr-opened for another tick if the retry fails.
776
780
  recovered = (
777
- hooks.recover_missing_pr_scene(issue, repo, repo_dir)
781
+ hooks.recover_missing_pr_scene(issue, repo, repo_dir, language_code=language_code)
778
782
  if repo_dir is not None else None
779
783
  )
780
784
  if recovered is not None:
@@ -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
 
@@ -5,7 +5,7 @@ Both comments double as the run's machine-readable anchors: the started
5
5
  comment carries the `started` kind marker and the opened-PR comment
6
6
  carries the `orbi:scene:v1` block the next tick resumes from. Only their
7
7
  HEADLINE is human prose and follows the run's bound message language
8
- (`RunContext.language`, or an explicit `language_code`); the hidden
8
+ (the explicit `language_code`); the hidden
9
9
  markers, the field lines and the scene block stay exactly as they were.
10
10
 
11
11
  Extracted from `runner.py` unchanged apart from the language, so the
@@ -22,16 +22,9 @@ from orbi.journal import validate_run_id
22
22
  from orbi.progress import STARTED_HEADER, _run_info_fields, field_block
23
23
 
24
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)
30
-
31
-
32
25
  def started_pi_comment_body(ctx: RunContext, run_info: str,
33
26
  extra_fields: dict | None = None, *,
34
- language_code: str | None = None) -> str:
27
+ language_code: str) -> str:
35
28
  """The start comment doubles as the recoverable run scene.
36
29
 
37
30
  `extra_fields` carries the repo-config audit fields;
@@ -42,28 +35,26 @@ def started_pi_comment_body(ctx: RunContext, run_info: str,
42
35
  """
43
36
  fields = _run_info_fields(run_info)
44
37
  fields["branch"] = str(ctx.branch)
45
- fields["worktree"] = str(ctx.worktree)
46
38
  if extra_fields:
47
39
  fields.update(extra_fields)
48
40
  info = _run_info_fields(run_info)
49
- bound = _language_of(ctx, language_code)
50
41
  headline = language.text(
51
- bound, "delivery.started_headline",
42
+ language_code, "delivery.started_headline",
52
43
  ) + " " + " ".join(
53
44
  f"{key}={info[key]}" for key in ("run_id", "priority")
54
45
  if key in info
55
46
  )
56
47
  return field_block(
57
48
  ctx.run_id, headline, fields,
58
- detail_keys={"base_sha", "repo_config", "branch", "worktree", "session"},
49
+ detail_keys={"base_sha", "repo_config", "branch", "session"},
59
50
  kind=COMMENT_KIND_STARTED,
60
- language_code=bound,
51
+ language_code=language_code,
61
52
  )
62
53
 
63
54
 
64
55
  def opened_pr_comment_body(run_id: str, run_info: str, pr_url: str,
65
56
  external: bool = False, *,
66
- language_code: str = language.DEFAULT_LANGUAGE,
57
+ language_code: str,
67
58
  ) -> str:
68
59
  """The PR-opened comment records the recoverable run scene.
69
60
 
@@ -97,7 +88,7 @@ def opened_pr_comment_body(run_id: str, run_info: str, pr_url: str,
97
88
  )
98
89
  body = field_block(
99
90
  run_id, headline, fields,
100
- detail_keys={"base_sha", "repo_config", "branch", "worktree", "session"},
91
+ detail_keys={"base_sha", "repo_config", "branch", "session"},
101
92
  language_code=language_code,
102
93
  )
103
94
  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,9 @@ 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
- return "\n\n".join(parts + ["\n".join(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')}"
@@ -270,7 +270,7 @@ def _session_summary(session_file: Path,
270
270
 
271
271
  def _failure_scene(snapshot: dict, *, run_id: str, issue: str, role: str,
272
272
  branch: str, worktree: str, pr_url: str | None = None,
273
- language_code: str = language.DEFAULT_LANGUAGE) -> str:
273
+ language_code: str) -> str:
274
274
  """Render run facts as wrapping Markdown, never as a host-path dump.
275
275
 
276
276
  The field labels are human prose and follow the run's message
@@ -312,8 +312,15 @@ def _failure_scene(snapshot: dict, *, run_id: str, issue: str, role: str,
312
312
  )
313
313
 
314
314
 
315
+ # One evidence segment's cap (Issue #1711, `_fenced`'s default) and one
316
+ # fitting step: an oversized comment shrinks these, never its structure.
317
+ EVIDENCE_SEGMENT_CAP = 4000
318
+ _EVIDENCE_CAP_STEP = 500
319
+
320
+
315
321
  def _failure_evidence(worktree: Path | None, exc: BaseException, *,
316
- language_code: str = language.DEFAULT_LANGUAGE) -> str:
322
+ language_code: str,
323
+ cap: int = EVIDENCE_SEGMENT_CAP) -> str:
317
324
  """Render the bounded evidence that survives terminal worktree cleanup.
318
325
 
319
326
  Pi subprocess streams come from ``CalledProcessError``. The session and
@@ -322,6 +329,8 @@ def _failure_evidence(worktree: Path | None, exc: BaseException, *,
322
329
  the session tail is a structural summary whose duplicates are removed,
323
330
  and the full session log is named for the deep-dive. The
324
331
  failure reason itself stays the readable head of the comment.
332
+
333
+ `cap` bounds each segment, so every fence closes (Issue #1711).
325
334
  """
326
335
  stderr = getattr(exc, "stderr", None)
327
336
  stdout = getattr(exc, "output", None)
@@ -348,11 +357,11 @@ def _failure_evidence(worktree: Path | None, exc: BaseException, *,
348
357
  return (
349
358
  f"\n\n{language.text(language_code, 'failure.evidence.heading')}\n"
350
359
  f"exit_code={return_code if return_code is not None else '<unknown>'}\n"
351
- f"\nstderr_tail:\n{_fenced(_redact_local_paths(stderr))}\n"
352
- f"\nstdout_tail:\n{_fenced(_redact_local_paths(stdout))}\n"
360
+ f"\nstderr_tail:\n{_fenced(_redact_local_paths(stderr), cap=cap)}\n"
361
+ f"\nstdout_tail:\n{_fenced(_redact_local_paths(stdout), cap=cap)}\n"
353
362
  f"\n{language.text(language_code, 'failure.evidence.session_last_events', limit=SESSION_SUMMARY_LIMIT, label=session_label)}\n"
354
- f"{_fenced(_redact_local_paths(session))}\n"
355
- f"\ntest_log_tail:\n{_fenced(_redact_local_paths(test_log))}"
363
+ f"{_fenced(_redact_local_paths(session), cap=cap)}\n"
364
+ f"\ntest_log_tail:\n{_fenced(_redact_local_paths(test_log), cap=cap)}"
356
365
  )
357
366
 
358
367
 
@@ -389,8 +398,8 @@ def _snapshot_or_placeholder(session_dir: Path, *, number: int) -> dict:
389
398
  _BLOCKED_PRECONDITION_PHRASE_KEY = "failure.blocked_precondition"
390
399
 
391
400
 
392
- # The whole failure-comment body is capped well below the
393
- # GitHub comment limit; the cut is noted with the full session log path.
401
+ # The failure comment's size limit: the body stays under it by shrinking
402
+ # only the evidence segments, never by cutting the comment (Issue #1711).
394
403
  FAILURE_COMMENT_MAX_CHARS = 20000
395
404
 
396
405
 
@@ -461,7 +470,7 @@ def _failure_streak(comments: list, run_id: str,
461
470
 
462
471
  def block_scene_failure(issue: dict, error: ValueError, repo: str,
463
472
  comments: list[dict], *,
464
- language_code: str = language.DEFAULT_LANGUAGE) -> None:
473
+ language_code: str) -> None:
465
474
  """Mark an opened-PR Issue `ai-blocked` when its scene is malformed.
466
475
 
467
476
  The blocked transition is scoped to this one Issue: the
@@ -488,8 +497,8 @@ def block_scene_failure(issue: dict, error: ValueError, repo: str,
488
497
  # derive run_id, branch, worktree or PR without the trusted
489
498
  # scene comment, so it cannot start a review session): the
490
499
  # comment states the EXPLICIT reason why automatic recovery is
491
- # impossible plus the human next step (restore the scene or
492
- # relabel).
500
+ # impossible plus the human next step (restore the scene comment,
501
+ # then the ONE shared unblock step of the catalog, Issue #1724).
493
502
  comment_issue(
494
503
  number, repo=repo,
495
504
  body=(
@@ -506,8 +515,8 @@ def block_scene_failure(issue: dict, error: ValueError, repo: str,
506
515
 
507
516
  def block_repo_config_failure(number: int, source_repo: str,
508
517
  error: ValueError, run_id: str,
509
- current_labels=(),
510
- language_code: str = language.DEFAULT_LANGUAGE,
518
+ current_labels=(), *,
519
+ language_code: str,
511
520
  ) -> None:
512
521
  """Mark an Issue `ai-blocked` when its repo config is invalid (#527).
513
522
 
@@ -583,7 +592,7 @@ def _raise_if_preexisting_ci_failure(
583
592
 
584
593
  def _report_resume_failure(*, number: int, source_repo: str, run_id: str,
585
594
  error: Exception,
586
- language_code: str = language.DEFAULT_LANGUAGE,
595
+ language_code: str,
587
596
  ) -> None:
588
597
  """Report a failed resume decision through the terminal path.
589
598
 
@@ -591,8 +600,8 @@ def _report_resume_failure(*, number: int, source_repo: str, run_id: str,
591
600
  verified: continuing on a guessed identity risks a
592
601
  silent fresh redo on top of unknown work, and a fresh run would
593
602
  lose the existing work. The Issue goes `ai-blocked` ALONE (the
594
- claim label removed) with the exact reason in the failure comment
595
- — a human decides (restore or remove the worktree, then re-label).
603
+ claim label removed) with the exact reason, then the community
604
+ help line — a human decides (restore or remove the worktree, relabel).
596
605
  """
597
606
  apply_label_patch(
598
607
  number, repo=source_repo, event=EVENT_BLOCKED,
@@ -605,8 +614,9 @@ def _report_resume_failure(*, number: int, source_repo: str, run_id: str,
605
614
  f"{comment_kind_marker(COMMENT_KIND_FAILURE)}\n"
606
615
  + language.text(
607
616
  language_code, "failure.resume_blocked",
608
- detail=_failure_detail(error), run_id=run_id,
609
- )
617
+ detail=_redact_local_paths(_failure_detail(error)),
618
+ run_id=run_id,
619
+ ) + f"\n\n{language.text(language_code, 'help.community')}"
610
620
  ),
611
621
  )
612
622
 
@@ -621,7 +631,7 @@ def report_delivery_failure(
621
631
  blocked_suffix: str = "", review_round: int | None = None,
622
632
  review_scene_block: str | None = None,
623
633
  finish: bool = True, publisher: ProgressPublisher | None = None,
624
- language_code: str = language.DEFAULT_LANGUAGE,
634
+ language_code: str,
625
635
  ) -> str:
626
636
  """Report one delivery failure through the classified-failure flow.
627
637
 
@@ -727,11 +737,11 @@ def report_delivery_failure(
727
737
  streak = _failure_streak(history, run_id, fingerprint)
728
738
  if streak + 1 >= FAILURE_STREAK_LIMIT:
729
739
  blocked = True
730
- reason = (
731
- f"{reason}; the same failure has now occurred "
732
- f"{streak + 1} consecutive times for run_id={run_id} "
733
- f"(fingerprint {fingerprint}) with unchanged "
734
- "preconditions — a dead loop, not a transient error"
740
+ reason += language.text(
741
+ language_code,
742
+ "delivery.failure_streak_escalation",
743
+ count=streak + 1, run_id=run_id,
744
+ fingerprint=fingerprint,
735
745
  )
736
746
  event(
737
747
  "failure_streak_escalated", level=logging.ERROR,
@@ -783,10 +793,8 @@ def report_delivery_failure(
783
793
  current_labels if current_labels is not None
784
794
  else issue_labels(number, source_repo)
785
795
  )
786
- evidence_detail = (
787
- _failure_evidence(worktree, exc, language_code=language_code)
788
- if evidence else ""
789
- )
796
+ evidence_detail = _failure_evidence(
797
+ worktree, exc, language_code=language_code) if evidence else ""
790
798
  # The milestone is a short mobile notification: for the workflow
791
799
  # refusal it must carry the same cause AND repair path as the
792
800
  # detailed comment, so it shows the reader-facing action instead of
@@ -837,23 +845,16 @@ def report_delivery_failure(
837
845
  language_code=language_code,
838
846
  )
839
847
  outcome = "fix needed"
840
- if len(body) > FAILURE_COMMENT_MAX_CHARS:
841
- # The comment body is capped; the cut preserves the
842
- # cause-first head and names the full session log for the part
843
- # that was dropped.
844
- session_file = _latest_session_file(worktree)
845
- body = (
846
- body[:FAILURE_COMMENT_MAX_CHARS]
847
- + language.text(
848
- language_code, "failure.comment_truncated",
849
- chars=FAILURE_COMMENT_MAX_CHARS,
850
- log=(
851
- language.text(
852
- language_code, "failure.scene.local_session_log")
853
- if session_file else "<unavailable>"
854
- ),
855
- )
856
- )
848
+ # An oversized comment shrinks ONLY its evidence segments, never its
849
+ # 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).
851
+ cap = EVIDENCE_SEGMENT_CAP
852
+ while evidence_detail and cap and len(body) > FAILURE_COMMENT_MAX_CHARS:
853
+ cap = max(0, cap - _EVIDENCE_CAP_STEP)
854
+ shrunk = _failure_evidence(
855
+ worktree, exc, language_code=language_code, cap=cap)
856
+ body = body.replace(evidence_detail.lstrip(), shrunk.lstrip(), 1)
857
+ evidence_detail = shrunk
857
858
  if run_id:
858
859
  # The hidden fingerprint marker rides under the run marker on
859
860
  # the recoverable path only: a blocked Issue is terminal, so
@@ -648,13 +648,6 @@ def _epic_child_evidence(repo: str, kind: str, number: int) -> str:
648
648
  return f"Issue #{number} closed"
649
649
 
650
650
 
651
- def _epic_audit(child_evidence: list[str], version: str | None = None) -> str:
652
- prefix = f" for {version}" if version else ""
653
- return (f"Epic reconciliation{prefix}: complete; "
654
- f"children: {', '.join(child_evidence)}; "
655
- "no open native blockers/dependencies.")
656
-
657
-
658
651
  # Comments are trusted through one of two channels — a repo maintainer
659
652
  # association, or the currently authenticated runner account itself (the
660
653
  # login fallback in `_comment_is_trusted`, which requires the local gh
@@ -688,9 +681,9 @@ def apply_label_patch(number: int, *, repo: str, event: str,
688
681
  patch. The patch is applied through `edit_issue`: one call for the
689
682
  add plus the first remove, then one call per extra remove (the exact
690
683
  same `edit_issue` kwargs the pre-#175 code emitted). A no-op patch
691
- (nothing to add or remove) applies nothing. A NEWLY added attention
692
- label then notifies the person through `notify` (Issue #1673),
693
- after the last write.
684
+ (nothing to add or remove) applies nothing. After the last write,
685
+ `notify.patch_applied` (Issue #1673) owns the one notification the
686
+ patch warrants — a newly added attention label, never the wait.
694
687
  """
695
688
  to_add, to_remove = label_patch(event, current_labels)
696
689
  if not to_add and not to_remove:
@@ -704,17 +697,15 @@ def apply_label_patch(number: int, *, repo: str, event: str,
704
697
  edit_issue(number, **kwargs)
705
698
  for label in to_remove[1:]:
706
699
  edit_issue(number, repo=repo, remove=label)
707
- # Notify only a NEWLY added attention label, and only when
708
- # notifications are on (no extra gh call when they are off).
709
- if (
710
- to_add
711
- and to_add[0] in notify.ATTENTION_LABELS
712
- and to_add[0] not in set(current_labels)
713
- and notify.enabled()
714
- ):
715
- notify.label_added(
716
- repo, number, to_add[0], _issue_title(number, repo=repo),
717
- )
700
+ # Notifying the person is a pure bypass owned by `notify` (Issues
701
+ # #1673/#1691): it decides from the event, the added labels and the
702
+ # current labels, and reads the title lazily through the callable --
703
+ # so a tick with notifications off makes no extra `gh` call.
704
+ notify.patch_applied(
705
+ event, to_add, current_labels,
706
+ repo=repo, number=number,
707
+ read_title=lambda: _issue_title(number, repo=repo),
708
+ )
718
709
 
719
710
 
720
711
  def _issue_title(number: int, *, repo: str) -> str | None:
@@ -111,7 +111,7 @@ def _is_deploy_path(path: str) -> bool:
111
111
 
112
112
  def build_checklist(*, test_status: str | None,
113
113
  changed_files: list[str] | None,
114
- language_code: str = language.DEFAULT_LANGUAGE) -> dict:
114
+ language_code: str) -> dict:
115
115
  """Split the delivery's evidence into the two checklist columns.
116
116
 
117
117
  `test_status` is the PR head's CI check status (Issue #1613:
@@ -156,7 +156,7 @@ def build_checklist(*, test_status: str | None,
156
156
 
157
157
  def render_checklist_comment(*, run_id: str, pr_url: str,
158
158
  checklist: dict,
159
- language_code: str = language.DEFAULT_LANGUAGE,
159
+ language_code: str,
160
160
  ) -> str:
161
161
  """Render the Issue comment: run marker, both columns, machine block.
162
162
 
@@ -164,7 +164,8 @@ def render_checklist_comment(*, run_id: str, pr_url: str,
164
164
  `Orbi review round ` (`review_rounds_so_far` counts it and the
165
165
  checklist would burn the bounded review budget); an empty column 2
166
166
  says so explicitly and does not require the label; the wording never
167
- claims a guarantee of direction.
167
+ claims a guarantee of direction. A non-empty column 2 (a person
168
+ must confirm) ends with the community help line (Issue #1684).
168
169
  """
169
170
  def message(key: str, **fields: object) -> str:
170
171
  return language.text(language_code, key, **fields)
@@ -214,4 +215,9 @@ def render_checklist_comment(*, run_id: str, pr_url: str,
214
215
  ),
215
216
  "-->",
216
217
  ]
218
+ if column2:
219
+ # A person must confirm: this problem message ends with the
220
+ # community help line (Issue #1684). An empty column 2 needs no
221
+ # human, so it stays exactly as before.
222
+ lines += ["", message("help.community")]
217
223
  return "\n".join(lines)