orbi-cli 0.7.0__tar.gz → 0.7.1__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.0 → orbi_cli-0.7.1}/PKG-INFO +4 -4
  2. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/README.md +3 -3
  3. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/pyproject.toml +1 -1
  4. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/__init__.py +1 -1
  5. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/branch_reclaim.py +48 -1
  6. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/claim.py +6 -10
  7. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/clarify.py +39 -29
  8. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/cli.py +22 -24
  9. orbi_cli-0.7.1/src/orbi/comment_kind.py +68 -0
  10. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/config.py +30 -1
  11. orbi_cli-0.7.1/src/orbi/delivery_comment.py +105 -0
  12. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/delivery_scene.py +4 -0
  13. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/example_config.toml +19 -0
  14. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/failure.py +71 -51
  15. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/failure_report.py +125 -120
  16. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/github.py +27 -29
  17. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/human_review.py +58 -56
  18. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/journal.py +4 -0
  19. orbi_cli-0.7.1/src/orbi/language.py +658 -0
  20. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/merge_handoff.py +51 -21
  21. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/milestone.py +5 -1
  22. orbi_cli-0.7.1/src/orbi/notify.py +458 -0
  23. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_process.py +3 -3
  24. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_session.py +15 -2
  25. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pilot_setup.py +3 -3
  26. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/progress.py +157 -113
  27. orbi_cli-0.7.1/src/orbi/progress_outcome.py +84 -0
  28. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/release.py +45 -75
  29. orbi_cli-0.7.1/src/orbi/release_comments.py +78 -0
  30. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/release_git.py +33 -1
  31. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/repo_config.py +25 -1
  32. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/review_merge.py +129 -142
  33. orbi_cli-0.7.1/src/orbi/review_rounds.py +229 -0
  34. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/runner.py +107 -126
  35. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/status_observe.py +30 -4
  36. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/suggest.py +56 -7
  37. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/PKG-INFO +4 -4
  38. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/SOURCES.txt +10 -0
  39. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_bootstrap_runner.py +145 -12
  40. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_branch_reclaim.py +53 -0
  41. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_clarify.py +3 -3
  42. orbi_cli-0.7.1/tests/test_comment_identity.py +313 -0
  43. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_comment_readers_contract.py +14 -4
  44. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_failure_report.py +38 -12
  45. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_fix_needed_loop.py +2 -1
  46. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_github.py +9 -7
  47. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_human_review.py +15 -15
  48. orbi_cli-0.7.1/tests/test_message_language.py +555 -0
  49. orbi_cli-0.7.1/tests/test_notify.py +934 -0
  50. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_orbi.py +56 -7
  51. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pilot_setup.py +40 -3
  52. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_progress.py +86 -0
  53. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_progress_wiring.py +7 -2
  54. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_resume_e2e.py +3 -1
  55. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_resume_pr.py +12 -0
  56. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_review_merge.py +97 -10
  57. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_runner_config.py +22 -0
  58. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_status_observe.py +243 -3
  59. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_suggest.py +141 -0
  60. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/LICENSE +0 -0
  61. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/docs/licenses/sustainable-use-license.md +0 -0
  62. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/setup.cfg +0 -0
  63. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/checks.py +0 -0
  64. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/cli_source.py +0 -0
  65. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/delivery_labels.py +0 -0
  66. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/engine_source.py +0 -0
  67. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/git_identity.py +0 -0
  68. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/git_transport.py +0 -0
  69. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/gitops.py +0 -0
  70. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/launchd_deploy.py +0 -0
  71. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/merge_gate.py +0 -0
  72. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/milestone_command.py +0 -0
  73. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_activity.py +0 -0
  74. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_command.py +0 -0
  75. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_recovery.py +0 -0
  76. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pilot_slots.py +0 -0
  77. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/progress_summary.py +0 -0
  78. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/release_notes.py +0 -0
  79. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/run_state.py +0 -0
  80. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/runner_health.py +0 -0
  81. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/scene.py +0 -0
  82. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/scheduler.py +0 -0
  83. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/scheduler_units.py +0 -0
  84. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/session_command.py +0 -0
  85. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/source_pool.py +0 -0
  86. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/systemd_deploy.py +0 -0
  87. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/ticket_command.py +0 -0
  88. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  89. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  90. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/top_level.txt +0 -0
  91. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_agents_md.py +0 -0
  92. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_apply_label_patch.py +0 -0
  93. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_failure_issue_workflow.py +0 -0
  94. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_failure_triage.py +0 -0
  95. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_macos_workflow.py +0 -0
  96. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_workflow.py +0 -0
  97. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_claude_plugin.py +0 -0
  98. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_install.py +0 -0
  99. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_milestone.py +0 -0
  100. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_packaging.py +0 -0
  101. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_source.py +0 -0
  102. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_concurrency_e2e.py +0 -0
  103. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_constitution.py +0 -0
  104. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_coverage_gate.py +0 -0
  105. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_deliver_pr_fakes.py +0 -0
  106. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_delivery_branch_delete.py +0 -0
  107. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_delivery_labels.py +0 -0
  108. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_delivery_scene.py +0 -0
  109. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docker_docs.py +0 -0
  110. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docker_entrypoint.py +0 -0
  111. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docker_image_workflow.py +0 -0
  112. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_i18n.py +0 -0
  113. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_issue_shapes.py +0 -0
  114. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_labels.py +0 -0
  115. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_mermaid.py +0 -0
  116. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_releases.py +0 -0
  117. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_single_source.py +0 -0
  118. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_site.py +0 -0
  119. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_engine_source.py +0 -0
  120. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_failure.py +0 -0
  121. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_gh_read_retry.py +0 -0
  122. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_base_smoke.py +0 -0
  123. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_merge_smoke.py +0 -0
  124. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_network_retry.py +0 -0
  125. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_transport.py +0 -0
  126. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_github_fakes.py +0 -0
  127. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_gitops.py +0 -0
  128. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_gitops_fakes.py +0 -0
  129. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_import_graph.py +0 -0
  130. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_install_sh.py +0 -0
  131. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_issue_1367.py +0 -0
  132. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_issue_461.py +0 -0
  133. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_issue_template_config.py +0 -0
  134. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_journal.py +0 -0
  135. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_journal_events.py +0 -0
  136. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_launchd_deploy.py +0 -0
  137. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_license.py +0 -0
  138. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_milestone_command.py +0 -0
  139. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ops_e2e.py +0 -0
  140. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_orbi_dashboard.py +0 -0
  141. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_orbi_exporter.py +0 -0
  142. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_patch_ratchet.py +0 -0
  143. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_activity.py +0 -0
  144. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_command.py +0 -0
  145. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_extensions.py +0 -0
  146. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_provider_templates.py +0 -0
  147. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_recovery.py +0 -0
  148. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_session.py +0 -0
  149. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pilot_slots.py +0 -0
  150. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pr_issue_link_workflow.py +0 -0
  151. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_prerequisite_check.py +0 -0
  152. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_prompt_contract.py +0 -0
  153. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_public_contracts.py +0 -0
  154. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_publish_workflow.py +0 -0
  155. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_release_docs_workflow.py +0 -0
  156. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_release_ticket_templates.py +0 -0
  157. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_repo_config.py +0 -0
  158. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_resume_continue_e2e.py +0 -0
  159. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_run_artifacts.py +0 -0
  160. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_run_id_e2e.py +0 -0
  161. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_runner_health.py +0 -0
  162. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_runner_source_freshness.py +0 -0
  163. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_scene.py +0 -0
  164. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_scheduler.py +0 -0
  165. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_size_ratchet.py +0 -0
  166. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_src_layout.py +0 -0
  167. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_steering_review.py +0 -0
  168. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_suite_hygiene.py +0 -0
  169. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_systemd_deploy.py +0 -0
  170. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_systemd_timer.py +0 -0
  171. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ticket_command.py +0 -0
  172. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_user_acceptance_contract.py +0 -0
  173. {orbi_cli-0.7.0 → orbi_cli-0.7.1}/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.0
3
+ Version: 0.7.1
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
@@ -18,7 +18,7 @@ English | [简体中文](README.zh-CN.md)
18
18
 
19
19
  # Orbi
20
20
 
21
- **Label a GitHub Issue. Get back a reviewed, merged PR — and a release.**
21
+ **Label a GitHub Issue. Get back a reviewed, merged PR. Release when you decide.**
22
22
 
23
23
  [Try Orbi Cloud — 3 merged deliveries free](https://orbi.build/?ref=gh-readme) · no server, no credit card required
24
24
 
@@ -26,7 +26,7 @@ English | [简体中文](README.zh-CN.md)
26
26
 
27
27
  **See one real loop:** [Issue #1367](https://github.com/orbi-build/orbi/issues/1367) → [PR #1370](https://github.com/orbi-build/orbi/pull/1370) → [Release v0.5.47](https://github.com/orbi-build/orbi/releases/tag/v0.5.47)
28
28
 
29
- Orbi is an open-source autonomous coding agent: label a GitHub Issue `ai-ready`, and it writes the code in an isolated worktree, opens a PR, has an independent review session check it against the Issue's acceptance criteria, merges only the reviewed head, and cuts a tagged release. GitHub Issues are the only state store—no database, queue, or daemon.
29
+ Orbi is an open-source **software dark factory**: a software delivery system for GitHub. You define the goal and acceptance criteria in an Issue and label it `ai-ready`; Orbi implements it in an isolated worktree, runs tests, opens a PR, and takes it through independent review, fixes, and merge. When you request a release, Orbi runs the release gates and publishes the tag and release notes. GitHub Issues are the only state store—no database, queue, or daemon.
30
30
 
31
31
  **Ledger:** [merged PRs](https://github.com/orbi-build/orbi/pulls?q=is:merged) · [closed Issues](https://github.com/orbi-build/orbi/issues?q=is:closed) · [tagged releases](https://github.com/orbi-build/orbi/releases) — the repository is the record.
32
32
 
@@ -60,7 +60,7 @@ orbi doctor --config orbi.toml # 6. verify deployment health
60
60
 
61
61
  ## The loop, in 35 seconds
62
62
 
63
- That same loop, recorded from the live GitHub pages — including the review that rejected the first attempt:
63
+ Another real delivery, recorded from the live GitHub pages — including the review that rejected the first attempt:
64
64
 
65
65
  <video src="https://github.com/user-attachments/assets/1f0d3a62-5fc9-44ae-bf68-b312a6a55f85" controls muted></video>
66
66
 
@@ -2,7 +2,7 @@ English | [简体中文](README.zh-CN.md)
2
2
 
3
3
  # Orbi
4
4
 
5
- **Label a GitHub Issue. Get back a reviewed, merged PR — and a release.**
5
+ **Label a GitHub Issue. Get back a reviewed, merged PR. Release when you decide.**
6
6
 
7
7
  [Try Orbi Cloud — 3 merged deliveries free](https://orbi.build/?ref=gh-readme) · no server, no credit card required
8
8
 
@@ -10,7 +10,7 @@ English | [简体中文](README.zh-CN.md)
10
10
 
11
11
  **See one real loop:** [Issue #1367](https://github.com/orbi-build/orbi/issues/1367) → [PR #1370](https://github.com/orbi-build/orbi/pull/1370) → [Release v0.5.47](https://github.com/orbi-build/orbi/releases/tag/v0.5.47)
12
12
 
13
- Orbi is an open-source autonomous coding agent: label a GitHub Issue `ai-ready`, and it writes the code in an isolated worktree, opens a PR, has an independent review session check it against the Issue's acceptance criteria, merges only the reviewed head, and cuts a tagged release. GitHub Issues are the only state store—no database, queue, or daemon.
13
+ Orbi is an open-source **software dark factory**: a software delivery system for GitHub. You define the goal and acceptance criteria in an Issue and label it `ai-ready`; Orbi implements it in an isolated worktree, runs tests, opens a PR, and takes it through independent review, fixes, and merge. When you request a release, Orbi runs the release gates and publishes the tag and release notes. GitHub Issues are the only state store—no database, queue, or daemon.
14
14
 
15
15
  **Ledger:** [merged PRs](https://github.com/orbi-build/orbi/pulls?q=is:merged) · [closed Issues](https://github.com/orbi-build/orbi/issues?q=is:closed) · [tagged releases](https://github.com/orbi-build/orbi/releases) — the repository is the record.
16
16
 
@@ -44,7 +44,7 @@ orbi doctor --config orbi.toml # 6. verify deployment health
44
44
 
45
45
  ## The loop, in 35 seconds
46
46
 
47
- That same loop, recorded from the live GitHub pages — including the review that rejected the first attempt:
47
+ Another real delivery, recorded from the live GitHub pages — including the review that rejected the first attempt:
48
48
 
49
49
  <video src="https://github.com/user-attachments/assets/1f0d3a62-5fc9-44ae-bf68-b312a6a55f85" controls muted></video>
50
50
 
@@ -44,7 +44,7 @@ build-backend = "setuptools.build_meta"
44
44
 
45
45
  [project]
46
46
  name = "orbi-cli"
47
- version = "0.7.0"
47
+ version = "0.7.1"
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.0"
21
+ __version__ = "0.7.1"
@@ -20,17 +20,39 @@ import logging
20
20
  import re
21
21
  import subprocess
22
22
  from pathlib import Path
23
+ from typing import TYPE_CHECKING
23
24
  from urllib.parse import quote
24
25
 
25
- from orbi import config as config_domain
26
26
  from orbi.journal import GIT_NETWORK_TIMEOUT_SECONDS, event, run_command
27
27
 
28
+ if TYPE_CHECKING:
29
+ # Annotation-only: `config` reaches `notify` through `release` and
30
+ # `github`, so a runtime import here would close a cycle once `notify`
31
+ # imports this module (the `read_merged_pull_requests` scan, #1674).
32
+ from orbi import config as config_domain
33
+
28
34
  # Remote delivery-branch reclamation: the tick-start pass deletes at most
29
35
  # this many merged-PR `orbi/*` branches per tick, so a large backlog (255
30
36
  # branches on 2026-09-28) drains over ticks and one tick never spends
31
37
  # unbounded time on `gh api` deletes. The same bound as the worktree pass.
32
38
  BRANCH_RECLAIM_MAX_PER_TICK = 25
33
39
 
40
+ # The one merged-PR read the notification scan uses (Issue #1674). The
41
+ # repository connection, not the search API: a just-merged PR is visible
42
+ # at once, while the search index can lag and a lagged PR older than the
43
+ # watermark would then be skipped forever. A merge updates the PR, so
44
+ # recent merges come first; 100 is GitHub's page maximum.
45
+ MERGED_PULL_REQUESTS_QUERY = """query($owner: String!, $name: String!) {
46
+ repository(owner: $owner, name: $name) {
47
+ pullRequests(
48
+ states: MERGED, first: 100,
49
+ orderBy: {field: UPDATED_AT, direction: DESC}
50
+ ) {
51
+ nodes { number title url headRefName mergedAt }
52
+ }
53
+ }
54
+ }"""
55
+
34
56
 
35
57
  def delete_remote_branch(source_repo: str, branch: str) -> None:
36
58
  """Delete one remote branch through the GitHub REST API.
@@ -126,6 +148,31 @@ def _merged_pr_heads(prs: list[dict]) -> dict[str, set[str]]:
126
148
  return heads
127
149
 
128
150
 
151
+ def read_merged_pull_requests(source_repo: str) -> list[dict]:
152
+ """Read the most recently updated MERGED PRs (one GraphQL page).
153
+
154
+ The read the notification scan uses (Issue #1674). A malformed
155
+ payload (a failed read, an unexpected shape) raises so the caller
156
+ records one ``notify_failed reason=merged_check_failed`` and leaves
157
+ its watermark unchanged; a malformed node is skipped, never guessed
158
+ into a merge.
159
+ """
160
+ owner, _, name = source_repo.partition("/")
161
+ raw = run_command(
162
+ [
163
+ "gh", "api", "graphql",
164
+ "-f", f"query={MERGED_PULL_REQUESTS_QUERY}",
165
+ "-f", f"owner={owner}", "-f", f"name={name}",
166
+ ],
167
+ timeout=GIT_NETWORK_TIMEOUT_SECONDS,
168
+ log_command=["gh", "api", "graphql", "merged_pull_requests"],
169
+ )
170
+ nodes = json.loads(raw)["data"]["repository"]["pullRequests"]["nodes"]
171
+ if not isinstance(nodes, list):
172
+ raise ValueError("malformed merged-pull-request read")
173
+ return [node for node in nodes if isinstance(node, dict)]
174
+
175
+
129
176
  def reclaim_merged_delivery_branches(config: config_domain.RunnerConfig) -> None:
130
177
  """Delete remote `orbi/*` branches whose delivery PR has MERGED.
131
178
 
@@ -38,6 +38,7 @@ from orbi.github import (
38
38
  from orbi.gitops import task_branch
39
39
  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
+ from orbi import language
41
42
  from orbi.progress import run_marker
42
43
  from orbi.release import release_target_milestone
43
44
  from orbi.repo_config import RepoConfigError, RepoPolicy, load_repo_policy
@@ -637,6 +638,7 @@ def pick_next_issue(
637
638
  def pick_resumable_delivery(
638
639
  repo: str, slot_dir: Path, max_concurrency: int,
639
640
  repo_dir: Path | None = None, *, hooks: ResumeHooks,
641
+ language_code: str = language.DEFAULT_LANGUAGE,
640
642
  ) -> tuple[dict, dict] | None:
641
643
  """Return the newest FREE opened-PR delivery and its resume scene.
642
644
 
@@ -795,15 +797,9 @@ def pick_resumable_delivery(
795
797
  )
796
798
  comment_issue(
797
799
  number, repo=repo,
798
- body=(f"{marker}\n" if marker else "") + (
799
- f"Orbi failed: {exc}; no trusted 'Orbi opened PR' "
800
- "scene comment exists on this Issue, so the "
801
- "opened-PR delivery cannot be resumed — this is an "
802
- "external precondition the AI cannot safely judge "
803
- "or fix, so it cannot be recovered automatically "
804
- "(the Issue stays ai-blocked until a human "
805
- "decides) — restore the trusted 'Orbi opened PR' "
806
- "scene comment or relabel the Issue ai-fix-needed"
800
+ body=(f"{marker}\n" if marker else "")
801
+ + language.text(
802
+ language_code, "claim.scene_missing", error=exc,
807
803
  ),
808
804
  )
809
805
  except Exception:
@@ -915,7 +911,7 @@ def pick_next_delivery(
915
911
  else:
916
912
  selected = pick_resumable_delivery(
917
913
  repo, slot_dir, max_concurrency, config.repo_dir,
918
- hooks=hooks,
914
+ hooks=hooks, language_code=language.of(config),
919
915
  )
920
916
  if selected is not None:
921
917
  issue, scene = selected
@@ -33,6 +33,7 @@ import re
33
33
  from dataclasses import dataclass
34
34
  from typing import TYPE_CHECKING
35
35
 
36
+ from orbi import language, notify
36
37
  from orbi.delivery_labels import NEEDS_DETAIL_LABEL, READY_LABEL
37
38
  from orbi.github import comment_issue, edit_issue
38
39
  from orbi.journal import event
@@ -43,24 +44,23 @@ if TYPE_CHECKING:
43
44
  from orbi.config import RunnerConfig
44
45
 
45
46
 
46
- # The three semantic checks, in the identifier -> human-terms shape the
47
- # comment renders: the model answers with the identifiers, the person
48
- # reads the sentence.
47
+ # The three semantic check identifiers the model answers with, mapped to
48
+ # the catalog key of the human sentence the comment renders. The JSON
49
+ # protocol is unchanged (#1660): the identifiers stay machine values, and
50
+ # only the RENDERER turns each one into a sentence in the run's message
51
+ # language (English and Chinese both live in the shared catalog).
49
52
  MISSING: dict[str, str] = {
50
- "observable_result": (
51
- "No observable result: the ticket never says what a user sees or "
52
- "does differently once the change ships."
53
- ),
54
- "acceptance_condition": (
55
- "No acceptance condition: there is no concrete way to tell a "
56
- "finished delivery from an unfinished one."
57
- ),
58
- "single_outcome": (
59
- "More than one outcome: the ticket asks for several unrelated "
60
- "things instead of one fix or one change."
61
- ),
53
+ "observable_result": "clarify.missing.observable_result",
54
+ "acceptance_condition": "clarify.missing.acceptance_condition",
55
+ "single_outcome": "clarify.missing.single_outcome",
62
56
  }
63
57
 
58
+
59
+ def missing_text(item: str, language_code: str = language.DEFAULT_LANGUAGE,
60
+ ) -> str:
61
+ """The human sentence for one failed check, in one language."""
62
+ return language.text(language_code, MISSING[item])
63
+
64
64
  CLARIFY_SYSTEM_PROMPT = (
65
65
  "You are Orbi's ticket gate. Before an unattended delivery starts, "
66
66
  "you decide whether this Issue states enough to deliver it. Judge "
@@ -153,7 +153,8 @@ def parse_verdict(output: str) -> ClarifyVerdict | None:
153
153
 
154
154
 
155
155
  def render_comment(issue: dict, verdict: ClarifyVerdict, run_id: str, *,
156
- ready_label: str = READY_LABEL) -> str:
156
+ ready_label: str = READY_LABEL,
157
+ language_code: str = language.DEFAULT_LANGUAGE) -> str:
157
158
  """Render the ONE comment that names the missing pieces.
158
159
 
159
160
  The first line mentions the Issue author (`@<login>`, #1336) so the
@@ -164,27 +165,27 @@ def render_comment(issue: dict, verdict: ClarifyVerdict, run_id: str, *,
164
165
  scan really reads, never a hardcoded default a custom-label
165
166
  repository does not use.
166
167
  """
167
- pieces = "\n".join(f"- {MISSING[item]}" for item in verdict.missing)
168
+ def message(key: str, **fields: object) -> str:
169
+ return language.text(language_code, key, **fields)
170
+
171
+ pieces = "\n".join(
172
+ f"- {missing_text(item, language_code)}" for item in verdict.missing
173
+ )
168
174
  mention = _author_mention(issue)
169
175
  prefix = f"{mention}\n" if mention else ""
170
176
  return (
171
177
  f"{prefix}"
172
178
  f"{run_marker(run_id)}\n"
173
- "**Orbi: this ticket is not ready to deliver.**\n\n"
174
- "The pre-flight check says the Issue does not state pieces a "
175
- "delivery needs:\n\n"
179
+ f"{message('clarify.heading')}\n\n"
180
+ f"{message('clarify.intro')}\n\n"
176
181
  f"{pieces}\n\n"
177
- "Edit the Issue body into this shape, then add the "
178
- f"`{ready_label}` label again:\n\n"
182
+ f"{message('clarify.repair', ready_label=ready_label)}\n\n"
179
183
  "```\n"
180
- "Outcome: what a user does, and what they should see differently.\n"
181
- "Acceptance: how a finished delivery is told apart from an "
182
- "unfinished one.\n"
184
+ f"{message('clarify.repair_outcome')}\n"
185
+ f"{message('clarify.repair_acceptance')}\n"
183
186
  "```\n\n"
184
- "Orbi re-checks the ticket when it claims it and starts the "
185
- "delivery once the pieces are there. Until then the ticket "
186
- f"carries `{NEEDS_DETAIL_LABEL}` and waits for you.\n\n"
187
- f"run_id={run_id}\n"
187
+ f"{message('clarify.followup', needs_detail_label=NEEDS_DETAIL_LABEL)}"
188
+ f"\n\nrun_id={run_id}\n"
188
189
  )
189
190
 
190
191
 
@@ -241,6 +242,7 @@ def enforce(issue: dict, config: RunnerConfig, source_repo: str,
241
242
  issue["number"], repo=source_repo,
242
243
  body=render_comment(
243
244
  issue, verdict, run_id, ready_label=ready_label,
245
+ language_code=language.of(config),
244
246
  ),
245
247
  )
246
248
  edit_issue(
@@ -253,6 +255,14 @@ def enforce(issue: dict, config: RunnerConfig, source_repo: str,
253
255
  issue=issue["number"], reason=type(exc).__name__,
254
256
  )
255
257
  return True
258
+ # Tell the person outside GitHub (Issue #1673): OUTSIDE the try (a
259
+ # notify problem must never turn the failed-write branch into "deliver
260
+ # anyway") and every stop sends — the old `ai-needs-detail` can still
261
+ # be on a ticket someone just re-readied.
262
+ notify.label_added(
263
+ source_repo, issue["number"], NEEDS_DETAIL_LABEL,
264
+ issue.get("title"),
265
+ )
256
266
  event(
257
267
  "clarify_needs_detail", issue=issue["number"],
258
268
  missing=",".join(verdict.missing),
@@ -36,6 +36,7 @@ from orbi import (
36
36
  config as config_domain,
37
37
  engine_source,
38
38
  git_transport,
39
+ notify,
39
40
  runner,
40
41
  scheduler,
41
42
  suggest,
@@ -156,11 +157,12 @@ def dispatch_issue(repo: str, title: str, body: str, *,
156
157
 
157
158
 
158
159
  def list_labeled_issues(repo: str, label: str, state: str = "open",
159
- search: str | None = None) -> list[dict]:
160
+ search: str | None = None, *,
161
+ timeout: int | None = None) -> list[dict]:
160
162
  """Return the newest Issues matching a label search (gh lists newest first)."""
161
163
  return list_issues(
162
164
  repo, state=state, search=search or f"label:{label}",
163
- json_fields="number,title,url,state", limit=1,
165
+ json_fields="number,title,url,state", limit=1, timeout=timeout,
164
166
  )
165
167
 
166
168
 
@@ -170,8 +172,14 @@ def current_issue(repo: str) -> dict | None:
170
172
 
171
173
 
172
174
  def ready_issue(repo: str) -> dict | None:
175
+ """The ready queue, read under the status cycle's bound.
176
+
177
+ The status view owns this read, so a stalled `gh issue list` fails
178
+ the observation within the bound instead of hanging it (Issue #1668).
179
+ """
173
180
  issues = list_labeled_issues(
174
181
  repo, READY_LABEL, search=f"label:{READY_LABEL} -label:{IN_PROGRESS_LABEL}",
182
+ timeout=status_observe.STATUS_READ_TIMEOUT_SECONDS,
175
183
  )
176
184
  return issues[0] if issues else None
177
185
 
@@ -186,7 +194,10 @@ def recent_result(repo: str) -> dict | None:
186
194
  """
187
195
  newest = None
188
196
  for label in RESULT_LABELS:
189
- for issue in list_labeled_issues(repo, label, state="all"):
197
+ for issue in list_labeled_issues(
198
+ repo, label, state="all",
199
+ timeout=status_observe.STATUS_READ_TIMEOUT_SECONDS,
200
+ ):
190
201
  if newest is None or int(issue["number"]) > int(newest["number"]):
191
202
  newest = issue
192
203
  return newest
@@ -409,6 +420,7 @@ def doctor_report(config: config_domain.RunnerConfig, installed_dir: Path | None
409
420
  f"{finding['provider']} key={finding['variable']} "
410
421
  f"{finding['state']} (file: {finding['path']})"
411
422
  )
423
+ lines.extend(notify.doctor_lines(config))
412
424
  provider = pilot_setup.model_provider_status(config)
413
425
  if provider["state"] == "ok":
414
426
  lines.append(
@@ -574,25 +586,7 @@ def main(argv: list[str] | None = None) -> int:
574
586
  "--repo", default=None,
575
587
  help="source repo (default: first configured source)",
576
588
  )
577
- suggest_parser = subparsers.add_parser(
578
- "suggest", parents=[common],
579
- help="read the repository and propose up to three Issues to "
580
- "deliver (interactive; --json prints the Suggestions "
581
- "document)",
582
- )
583
- suggest_parser.add_argument(
584
- "--repo", default=None,
585
- help="source repo (default: first configured source)",
586
- )
587
- suggest_parser.add_argument(
588
- "--json", action="store_true",
589
- help="print the Suggestions document to stdout and write nothing",
590
- )
591
- suggest_parser.add_argument(
592
- "--locale", choices=["zh", "en"], default=None,
593
- help="write the suggestions and the repository summary in this "
594
- "language (default: the repository's own language)",
595
- )
589
+ suggest.register_cli(subparsers, common)
596
590
  status_parser = subparsers.add_parser(
597
591
  "status", parents=[common],
598
592
  help="show every active delivery with attributed local activity, "
@@ -695,6 +689,7 @@ def main(argv: list[str] | None = None) -> int:
695
689
  "title",
696
690
  help="the exact Milestone title, e.g. v0.6.0 (must be unique)",
697
691
  )
692
+ notify.register_cli(subparsers, common)
698
693
  args = parser.parse_args(argv)
699
694
  configure_logging()
700
695
 
@@ -773,10 +768,11 @@ def main(argv: list[str] | None = None) -> int:
773
768
  # enforces the key), so they load the config with the
774
769
  # doctor's lenient provider flags.
775
770
  check_provider_api_keys=args.command not in (
776
- "doctor", "sync-engine-source", "milestone",
771
+ "doctor", "sync-engine-source", "milestone", "notify",
777
772
  ),
778
773
  allow_missing_pi_providers=args.command in (
779
774
  "setup", "doctor", "sync-engine-source", "milestone",
775
+ "notify",
780
776
  ),
781
777
  )
782
778
  validate_config(config)
@@ -836,7 +832,7 @@ def main(argv: list[str] | None = None) -> int:
836
832
  )
837
833
  return suggest.run_suggest(
838
834
  config, repo, json_output=args.json, locale=args.locale,
839
- dispatch_issue=dispatch_issue,
835
+ count=args.count, dispatch_issue=dispatch_issue,
840
836
  )
841
837
  elif args.command == "session":
842
838
  path = find_session_file(config.repo_dir)
@@ -890,6 +886,8 @@ def main(argv: list[str] | None = None) -> int:
890
886
  return 1
891
887
  elif args.command == "doctor":
892
888
  print(doctor_report(config, args.installed_dir))
889
+ elif args.command == "notify":
890
+ return notify.test_command(config)
893
891
  elif args.command == "setup":
894
892
  try:
895
893
  result = pilot_setup.run_setup(
@@ -0,0 +1,68 @@
1
+ """Canonical, display-independent comment kinds (Issue #1659).
2
+
3
+ Every run-scoped comment starts with the hidden run marker
4
+ (`<!-- orbi:run=<run_id> -->`). The four KINDED comments carry one more
5
+ hidden marker on the line right after it:
6
+
7
+ <!-- orbi:comment=started -->
8
+ <!-- orbi:comment=failure -->
9
+ <!-- orbi:comment=review-round -->
10
+ <!-- orbi:comment=merged -->
11
+
12
+ so a consumer identifies the comment without parsing a translatable
13
+ heading. The kind is read only from that anchored prefix; a
14
+ marker-shaped string quoted in prose, a raw log or a code fence is not
15
+ official metadata. Progress comments stay deliberately kindless: their
16
+ documented formal prefix (the run marker followed immediately by the
17
+ `orbi:progress:v1` block) identifies them.
18
+
19
+ The module is pure (the `scene` model): every function is a
20
+ deterministic function of its input string, with no I/O, so the writer
21
+ and every reader share one definition.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+
27
+ COMMENT_KIND_STARTED = "started"
28
+ COMMENT_KIND_FAILURE = "failure"
29
+ COMMENT_KIND_REVIEW_ROUND = "review-round"
30
+ COMMENT_KIND_MERGED = "merged"
31
+ COMMENT_KINDS = (
32
+ COMMENT_KIND_STARTED, COMMENT_KIND_FAILURE,
33
+ COMMENT_KIND_REVIEW_ROUND, COMMENT_KIND_MERGED,
34
+ )
35
+ COMMENT_KIND_TEMPLATE = "<!-- orbi:comment={kind} -->"
36
+ COMMENT_KIND_PATTERN = re.compile(r"<!-- orbi:comment=([a-z-]+) -->")
37
+
38
+ # The official metadata prefix, anchored at the body's start: the run
39
+ # marker on the first line and, on a kinded comment, the kind marker on
40
+ # the line immediately after. Anchoring means a marker-shaped string
41
+ # quoted in prose, a raw log or a code fence is never read as metadata.
42
+ _KIND_PREFIX_RE = re.compile(
43
+ r"\A<!-- orbi:run=[0-9a-f]{8} -->\r?\n<!-- orbi:comment=([a-z-]+) -->"
44
+ )
45
+
46
+
47
+ def comment_kind_marker(kind: str) -> str:
48
+ """Render the hidden canonical kind marker for one comment kind."""
49
+ if kind not in COMMENT_KINDS:
50
+ raise ValueError(f"unknown comment kind: {kind!r}")
51
+ return COMMENT_KIND_TEMPLATE.format(kind=kind)
52
+
53
+
54
+ def comment_kind(body: object) -> str | None:
55
+ """The canonical kind from the body's official metadata prefix.
56
+
57
+ None means the body carries no kind marker in that prefix: a
58
+ historical comment (read through the existing English fallback) or a
59
+ kindless comment (progress, opened PR, short milestone). The kind is
60
+ read only from the anchored prefix, so a marker-shaped string inside
61
+ a raw log, a details fold or a code fence is never official
62
+ metadata. An unsupported kind is returned verbatim, never
63
+ reinterpreted through the prose next to it.
64
+ """
65
+ if not isinstance(body, str):
66
+ return None
67
+ match = _KIND_PREFIX_RE.match(body)
68
+ return match.group(1) if match is not None else None
@@ -12,7 +12,7 @@ import tomllib
12
12
  from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
 
15
- from orbi import engine_source
15
+ from orbi import engine_source, language
16
16
  from orbi.pi_process import PI_MODEL_WAIT_DEAD_SECONDS, PI_MODEL_WAIT_PROBE_SECONDS
17
17
  from orbi.pilot_slots import slot_dir_for
18
18
  from orbi.release import RELEASE_CI_WAIT_SECONDS, RELEASE_DELIVERIES_WAIT_SECONDS
@@ -136,6 +136,16 @@ class RunnerConfig:
136
136
  max_concurrency: int = 1
137
137
  allow_stale_runner: bool = False
138
138
  human_review_gate: bool = False
139
+ # Telegram notifications (Issue #1673): ON when `telegram_chat_id` is
140
+ # set. `ORBI_TELEGRAM_BOT_TOKEN` lives in `<deploy_home>/.orbi/env`,
141
+ # never here; the message language is the host-level
142
+ # `message_language` (#1660), read by `notify` when it is present.
143
+ telegram_chat_id: str | None = None
144
+ # The message language of Orbi's own comments (Issue #1660), validated
145
+ # to `zh`/`en` by `load_config`. `resolve_policy` replaces it with the
146
+ # repository's explicit `.github/orbi.toml` value when the file
147
+ # declares one; absent everywhere -> "en".
148
+ message_language: str = language.DEFAULT_LANGUAGE
139
149
  attribution_footer: bool = True
140
150
  attribution_link: str = "https://github.com/orbi-build/orbi"
141
151
  slot_dir: Path | None = None
@@ -254,6 +264,23 @@ def load_config(path: Path, *, check_provider_api_keys: bool = True,
254
264
  human_review_gate = data.get("human_review_gate", False)
255
265
  if not isinstance(human_review_gate, bool):
256
266
  raise ValueError("human_review_gate must be a boolean")
267
+ # Notifications (Issue #1673): ON when `telegram_chat_id` is a
268
+ # non-empty string; absent = off. A present-but-invalid value is a
269
+ # misconfiguration and fails the load fast (Article 4.5); the bot
270
+ # token itself is a separate, optional bypass and never fails a start.
271
+ telegram_chat_id = data.get("telegram_chat_id")
272
+ if telegram_chat_id is not None and (
273
+ not isinstance(telegram_chat_id, str) or not telegram_chat_id
274
+ ):
275
+ raise ValueError("telegram_chat_id must be a non-empty string")
276
+ # The message language of Orbi's comments (Issue #1660). Host-only
277
+ # validation: an unsupported value fails the load fast naming the file,
278
+ # the key, the value and the allowed values; the repository's explicit
279
+ # override is validated by the repository policy reader.
280
+ message_language = language.normalize_language(
281
+ data.get(language.MESSAGE_LANGUAGE_KEY, language.DEFAULT_LANGUAGE),
282
+ source=str(path.resolve()),
283
+ )
257
284
  attribution_footer = data.get("attribution_footer", True)
258
285
  if not isinstance(attribution_footer, bool):
259
286
  raise ValueError("attribution_footer must be a boolean")
@@ -458,6 +485,8 @@ def load_config(path: Path, *, check_provider_api_keys: bool = True,
458
485
  max_concurrency=max_concurrency,
459
486
  allow_stale_runner=allow_stale_runner,
460
487
  human_review_gate=human_review_gate,
488
+ telegram_chat_id=telegram_chat_id,
489
+ message_language=message_language,
461
490
  attribution_footer=attribution_footer,
462
491
  attribution_link=attribution_link,
463
492
  slot_dir=slot_dir_for(repo_dir),