orbi-cli 0.7.0__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.0 → orbi_cli-0.7.2}/PKG-INFO +4 -4
  2. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/README.md +3 -3
  3. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/pyproject.toml +1 -1
  4. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/__init__.py +1 -1
  5. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/branch_reclaim.py +48 -1
  6. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/claim.py +38 -39
  7. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/clarify.py +42 -30
  8. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/cli.py +22 -24
  9. orbi_cli-0.7.2/src/orbi/comment_kind.py +68 -0
  10. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/config.py +30 -1
  11. orbi_cli-0.7.2/src/orbi/delivery_comment.py +96 -0
  12. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/example_config.toml +19 -0
  13. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/failure.py +77 -52
  14. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/failure_report.py +148 -142
  15. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/github.py +25 -36
  16. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/human_review.py +64 -56
  17. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/journal.py +4 -0
  18. orbi_cli-0.7.2/src/orbi/language.py +942 -0
  19. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/merge_handoff.py +51 -21
  20. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/milestone.py +61 -31
  21. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/milestone_command.py +8 -5
  22. orbi_cli-0.7.2/src/orbi/notify.py +595 -0
  23. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_process.py +3 -3
  24. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_session.py +19 -4
  25. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pilot_setup.py +3 -3
  26. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/progress.py +167 -118
  27. orbi_cli-0.7.2/src/orbi/progress_outcome.py +95 -0
  28. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/release.py +54 -84
  29. orbi_cli-0.7.2/src/orbi/release_comments.py +123 -0
  30. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/release_git.py +33 -1
  31. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/repo_config.py +43 -1
  32. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/review_merge.py +190 -201
  33. orbi_cli-0.7.2/src/orbi/review_rounds.py +229 -0
  34. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/runner.py +165 -175
  35. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/runner_health.py +17 -10
  36. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/status_observe.py +30 -4
  37. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/suggest.py +109 -16
  38. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/ticket_command.py +61 -19
  39. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/PKG-INFO +4 -4
  40. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/SOURCES.txt +10 -0
  41. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_bootstrap_runner.py +778 -129
  42. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_branch_reclaim.py +53 -0
  43. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_clarify.py +3 -3
  44. orbi_cli-0.7.2/tests/test_comment_identity.py +313 -0
  45. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_comment_readers_contract.py +20 -9
  46. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_deliver_pr_fakes.py +3 -3
  47. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_delivery_branch_delete.py +5 -5
  48. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_delivery_scene.py +64 -14
  49. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_failure.py +21 -21
  50. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_failure_report.py +400 -38
  51. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_fix_needed_loop.py +30 -10
  52. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_merge_smoke.py +6 -6
  53. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_github.py +9 -7
  54. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_human_review.py +24 -23
  55. orbi_cli-0.7.2/tests/test_message_language.py +815 -0
  56. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_milestone_command.py +203 -0
  57. orbi_cli-0.7.2/tests/test_notify.py +1168 -0
  58. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_orbi.py +56 -7
  59. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pilot_setup.py +67 -3
  60. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_progress.py +147 -29
  61. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_progress_wiring.py +82 -8
  62. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_repo_config.py +32 -11
  63. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_resume_continue_e2e.py +6 -0
  64. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_resume_e2e.py +145 -5
  65. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_resume_pr.py +158 -51
  66. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_review_merge.py +127 -40
  67. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_runner_config.py +96 -0
  68. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_runner_health.py +127 -2
  69. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_status_observe.py +243 -3
  70. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_steering_review.py +26 -0
  71. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_suggest.py +419 -15
  72. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ticket_command.py +3 -1
  73. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/LICENSE +0 -0
  74. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/docs/licenses/sustainable-use-license.md +0 -0
  75. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/setup.cfg +0 -0
  76. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/checks.py +0 -0
  77. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/cli_source.py +0 -0
  78. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/delivery_labels.py +0 -0
  79. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/delivery_scene.py +0 -0
  80. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/engine_source.py +0 -0
  81. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/git_identity.py +0 -0
  82. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/git_transport.py +0 -0
  83. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/gitops.py +0 -0
  84. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/launchd_deploy.py +0 -0
  85. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/merge_gate.py +0 -0
  86. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_activity.py +0 -0
  87. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_command.py +0 -0
  88. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_recovery.py +0 -0
  89. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pilot_slots.py +0 -0
  90. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/progress_summary.py +0 -0
  91. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/release_notes.py +0 -0
  92. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/run_state.py +0 -0
  93. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/scene.py +0 -0
  94. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/scheduler.py +0 -0
  95. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/scheduler_units.py +0 -0
  96. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/session_command.py +0 -0
  97. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/source_pool.py +0 -0
  98. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/systemd_deploy.py +0 -0
  99. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
  100. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/entry_points.txt +0 -0
  101. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/top_level.txt +0 -0
  102. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_agents_md.py +0 -0
  103. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_apply_label_patch.py +0 -0
  104. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_failure_issue_workflow.py +0 -0
  105. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_failure_triage.py +0 -0
  106. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_macos_workflow.py +0 -0
  107. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_workflow.py +0 -0
  108. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_claude_plugin.py +0 -0
  109. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_install.py +0 -0
  110. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_milestone.py +0 -0
  111. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_packaging.py +0 -0
  112. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_source.py +0 -0
  113. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_concurrency_e2e.py +0 -0
  114. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_constitution.py +0 -0
  115. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_coverage_gate.py +0 -0
  116. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_delivery_labels.py +0 -0
  117. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docker_docs.py +0 -0
  118. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docker_entrypoint.py +0 -0
  119. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docker_image_workflow.py +0 -0
  120. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_i18n.py +0 -0
  121. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_issue_shapes.py +0 -0
  122. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_labels.py +0 -0
  123. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_mermaid.py +0 -0
  124. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_releases.py +0 -0
  125. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_single_source.py +0 -0
  126. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_site.py +0 -0
  127. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_engine_source.py +0 -0
  128. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_gh_read_retry.py +0 -0
  129. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_base_smoke.py +0 -0
  130. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_network_retry.py +0 -0
  131. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_transport.py +0 -0
  132. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_github_fakes.py +0 -0
  133. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_gitops.py +0 -0
  134. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_gitops_fakes.py +0 -0
  135. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_import_graph.py +0 -0
  136. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_install_sh.py +0 -0
  137. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_issue_1367.py +0 -0
  138. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_issue_461.py +0 -0
  139. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_issue_template_config.py +0 -0
  140. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_journal.py +0 -0
  141. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_journal_events.py +0 -0
  142. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_launchd_deploy.py +0 -0
  143. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_license.py +0 -0
  144. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ops_e2e.py +0 -0
  145. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_orbi_dashboard.py +0 -0
  146. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_orbi_exporter.py +0 -0
  147. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_patch_ratchet.py +0 -0
  148. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_activity.py +0 -0
  149. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_command.py +0 -0
  150. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_extensions.py +0 -0
  151. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_provider_templates.py +0 -0
  152. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_recovery.py +0 -0
  153. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_session.py +0 -0
  154. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pilot_slots.py +0 -0
  155. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pr_issue_link_workflow.py +0 -0
  156. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_prerequisite_check.py +0 -0
  157. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_prompt_contract.py +0 -0
  158. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_public_contracts.py +0 -0
  159. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_publish_workflow.py +0 -0
  160. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_release_docs_workflow.py +0 -0
  161. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_release_ticket_templates.py +0 -0
  162. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_run_artifacts.py +0 -0
  163. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_run_id_e2e.py +0 -0
  164. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_runner_source_freshness.py +0 -0
  165. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_scene.py +0 -0
  166. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_scheduler.py +0 -0
  167. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_size_ratchet.py +0 -0
  168. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_src_layout.py +0 -0
  169. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_suite_hygiene.py +0 -0
  170. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_systemd_deploy.py +0 -0
  171. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_systemd_timer.py +0 -0
  172. {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_user_acceptance_contract.py +0 -0
  173. {orbi_cli-0.7.0 → 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.0
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
@@ -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.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.0"
21
+ __version__ = "0.7.2"
@@ -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
 
@@ -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,
@@ -38,9 +38,12 @@ 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
- from orbi.release import release_target_milestone
43
- 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
+ )
44
47
 
45
48
 
46
49
  class ResumeHooks(NamedTuple):
@@ -48,11 +51,14 @@ class ResumeHooks(NamedTuple):
48
51
 
49
52
  The physical scene handling of `pick_resumable_delivery` plus the PR
50
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.
51
57
  """
52
58
  resume_scene: Callable[[list[dict]], dict]
53
- route_external_pr_ticket: Callable[[dict, str], bool]
54
- block_scene_failure: Callable[[dict, ValueError, str, list[dict]], None]
55
- 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]
56
62
  has_recoverable_pr_scene: Callable[[dict, str, Path], bool]
57
63
  comment_pr: Callable[..., None]
58
64
 
@@ -194,7 +200,7 @@ def is_release(issue: dict) -> bool:
194
200
  return False
195
201
 
196
202
 
197
- 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]:
198
204
  """Sweep open Epics once per tick; ordinary pickup must not depend on it."""
199
205
  epics = list_issues(
200
206
  repo, state="open", search=f"label:{EPIC_LABEL}",
@@ -210,9 +216,9 @@ def reconcile_open_epics(repo: str, run_id: str) -> list[str]:
210
216
  event("epic_kept_open", issue=number, repo=repo, reason=reason)
211
217
  evidence.append(f"Epic #{number} kept open: {reason}")
212
218
  continue
213
- audit = _epic_audit(child_evidence)
219
+ audit = _epic_audit(child_evidence, None, language_code)
214
220
  comments = issue_comments(int(number), repo=repo)
215
- if not any(audit in str(comment.get("body", "")) for comment in comments):
221
+ if not epic_audit_posted(comments, child_evidence):
216
222
  comment_issue(int(number), repo=repo,
217
223
  body=f"<!-- orbi:run={run_id} -->\n{audit}\nrun_id={run_id}")
218
224
  close_issue(int(number), repo=repo)
@@ -242,7 +248,7 @@ def orphan_pr_branch_issue(head_ref: object) -> int | None:
242
248
 
243
249
 
244
250
  def reconcile_orphan_prs(
245
- repo: str, run_id: str, *, hooks: ResumeHooks,
251
+ repo: str, run_id: str, *, hooks: ResumeHooks, language_code: str,
246
252
  ) -> list[str]:
247
253
  """Report open delivery PRs whose source Issue is closed.
248
254
 
@@ -286,11 +292,10 @@ def reconcile_orphan_prs(
286
292
  pr_number, repo=repo,
287
293
  body=(
288
294
  f"{ORPHAN_PR_MARK}\n{run_marker(run_id)}\n"
289
- f"Orbi orphan PR: the source Issue #{issue_number} is "
290
- f"closed while this PR is still open, so it will never "
291
- f"be merged automatically. Human decision: merge this "
292
- f"PR to keep the work, or close it.\n"
293
- f"run_id={run_id}"
295
+ + language.text(
296
+ language_code, "claim.orphan_pr",
297
+ issue_number=issue_number, run_id=run_id,
298
+ )
294
299
  ),
295
300
  )
296
301
  event(
@@ -637,6 +642,7 @@ def pick_next_issue(
637
642
  def pick_resumable_delivery(
638
643
  repo: str, slot_dir: Path, max_concurrency: int,
639
644
  repo_dir: Path | None = None, *, hooks: ResumeHooks,
645
+ language_code: str,
640
646
  ) -> tuple[dict, dict] | None:
641
647
  """Return the newest FREE opened-PR delivery and its resume scene.
642
648
 
@@ -732,15 +738,15 @@ def pick_resumable_delivery(
732
738
  # probe the #726 external route first; otherwise this is the
733
739
  # ONLY trigger of `block_scene_failure` — a present-but-broken
734
740
  # scene is a writer bug or tampering and needs a human.
735
- if hooks.route_external_pr_ticket(issue, repo):
741
+ if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
736
742
  continue
737
- hooks.block_scene_failure(issue, exc, repo, comments)
743
+ hooks.block_scene_failure(issue, exc, repo, comments, language_code=language_code)
738
744
  continue
739
745
  except scene.SceneMissingError as exc:
740
746
  # No trusted comment carries a scene at all — a distinct branch
741
747
  # from corruption. The original #726 incident was
742
748
  # exactly this shape, so the external route is probed first.
743
- if hooks.route_external_pr_ticket(issue, repo):
749
+ if hooks.route_external_pr_ticket(issue, repo, language_code=language_code):
744
750
  continue
745
751
  # Issue #1216: `awaiting_merge` can deliberately remove the
746
752
  # queue label before the delivery returns to `ai-fix-needed`.
@@ -772,7 +778,7 @@ def pick_resumable_delivery(
772
778
  # scene comment write. Retry it from the durable local run
773
779
  # state; keep ai-pr-opened for another tick if the retry fails.
774
780
  recovered = (
775
- hooks.recover_missing_pr_scene(issue, repo, repo_dir)
781
+ hooks.recover_missing_pr_scene(issue, repo, repo_dir, language_code=language_code)
776
782
  if repo_dir is not None else None
777
783
  )
778
784
  if recovered is not None:
@@ -795,15 +801,9 @@ def pick_resumable_delivery(
795
801
  )
796
802
  comment_issue(
797
803
  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"
804
+ body=(f"{marker}\n" if marker else "")
805
+ + language.text(
806
+ language_code, "claim.scene_missing", error=exc,
807
807
  ),
808
808
  )
809
809
  except Exception:
@@ -873,10 +873,12 @@ def pick_next_delivery(
873
873
  tick_run_id = new_run_id()
874
874
  set_run_id(tick_run_id)
875
875
  for repo in repos:
876
+ repo_language = (language.DEFAULT_LANGUAGE if config is None
877
+ else merged_repo_language(config, repo))
876
878
  # Epic reconciliation is a per-tick bypass: a broken GitHub query or
877
879
  # mutation must never prevent the ordinary delivery scans.
878
880
  try:
879
- reconcile_open_epics(repo, tick_run_id)
881
+ reconcile_open_epics(repo, tick_run_id, repo_language)
880
882
  except Exception:
881
883
  LOGGER.exception("epic_reconcile_failed repo=%s", repo)
882
884
  # Milestone reconciliation intentionally follows the Epic sweep so a
@@ -905,18 +907,15 @@ def pick_next_delivery(
905
907
  # pattern: a broken GitHub query must never prevent the ordinary
906
908
  # delivery scans.
907
909
  try:
908
- reconcile_orphan_prs(repo, tick_run_id, hooks=hooks)
910
+ reconcile_orphan_prs(repo, tick_run_id, hooks=hooks,
911
+ language_code=repo_language)
909
912
  except Exception:
910
913
  LOGGER.exception("orphan_pr_reconcile_failed repo=%s", repo)
911
- if config is None:
912
- selected = pick_resumable_delivery(
913
- repo, slot_dir, max_concurrency, hooks=hooks,
914
- )
915
- else:
916
- selected = pick_resumable_delivery(
917
- repo, slot_dir, max_concurrency, config.repo_dir,
918
- hooks=hooks,
919
- )
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
+ )
920
919
  if selected is not None:
921
920
  issue, scene = selected
922
921
  return repo, issue, scene
@@ -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,
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) -> 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
@@ -162,29 +163,31 @@ def render_comment(issue: dict, verdict: ClarifyVerdict, run_id: str, *,
162
163
  before. `ready_label` is the repository's claim label (default
163
164
  `ai-ready`, #527): the repair instruction names the label the claim
164
165
  scan really reads, never a hardcoded default a custom-label
165
- 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.
166
168
  """
167
- pieces = "\n".join(f"- {MISSING[item]}" for item in verdict.missing)
169
+ def message(key: str, **fields: object) -> str:
170
+ return language.text(language_code, key, **fields)
171
+
172
+ pieces = "\n".join(
173
+ f"- {missing_text(item, language_code)}" for item in verdict.missing
174
+ )
168
175
  mention = _author_mention(issue)
169
176
  prefix = f"{mention}\n" if mention else ""
170
177
  return (
171
178
  f"{prefix}"
172
179
  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"
180
+ f"{message('clarify.heading')}\n\n"
181
+ f"{message('clarify.intro')}\n\n"
176
182
  f"{pieces}\n\n"
177
- "Edit the Issue body into this shape, then add the "
178
- f"`{ready_label}` label again:\n\n"
183
+ f"{message('clarify.repair', ready_label=ready_label)}\n\n"
179
184
  "```\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"
185
+ f"{message('clarify.repair_outcome')}\n"
186
+ f"{message('clarify.repair_acceptance')}\n"
183
187
  "```\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"
188
+ f"{message('clarify.followup', needs_detail_label=NEEDS_DETAIL_LABEL)}"
189
+ f"\n\nrun_id={run_id}"
190
+ f"\n\n{message('help.community')}\n"
188
191
  )
189
192
 
190
193
 
@@ -241,6 +244,7 @@ def enforce(issue: dict, config: RunnerConfig, source_repo: str,
241
244
  issue["number"], repo=source_repo,
242
245
  body=render_comment(
243
246
  issue, verdict, run_id, ready_label=ready_label,
247
+ language_code=language.of(config),
244
248
  ),
245
249
  )
246
250
  edit_issue(
@@ -253,6 +257,14 @@ def enforce(issue: dict, config: RunnerConfig, source_repo: str,
253
257
  issue=issue["number"], reason=type(exc).__name__,
254
258
  )
255
259
  return True
260
+ # Tell the person outside GitHub (Issue #1673): OUTSIDE the try (a
261
+ # notify problem must never turn the failed-write branch into "deliver
262
+ # anyway") and every stop sends — the old `ai-needs-detail` can still
263
+ # be on a ticket someone just re-readied.
264
+ notify.label_added(
265
+ source_repo, issue["number"], NEEDS_DETAIL_LABEL,
266
+ issue.get("title"),
267
+ )
256
268
  event(
257
269
  "clarify_needs_detail", issue=issue["number"],
258
270
  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