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.
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/PKG-INFO +4 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/README.md +3 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/pyproject.toml +1 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/__init__.py +1 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/branch_reclaim.py +48 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/claim.py +6 -10
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/clarify.py +39 -29
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/cli.py +22 -24
- orbi_cli-0.7.1/src/orbi/comment_kind.py +68 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/config.py +30 -1
- orbi_cli-0.7.1/src/orbi/delivery_comment.py +105 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/delivery_scene.py +4 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/example_config.toml +19 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/failure.py +71 -51
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/failure_report.py +125 -120
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/github.py +27 -29
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/human_review.py +58 -56
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/journal.py +4 -0
- orbi_cli-0.7.1/src/orbi/language.py +658 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/merge_handoff.py +51 -21
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/milestone.py +5 -1
- orbi_cli-0.7.1/src/orbi/notify.py +458 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_process.py +3 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_session.py +15 -2
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pilot_setup.py +3 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/progress.py +157 -113
- orbi_cli-0.7.1/src/orbi/progress_outcome.py +84 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/release.py +45 -75
- orbi_cli-0.7.1/src/orbi/release_comments.py +78 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/release_git.py +33 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/repo_config.py +25 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/review_merge.py +129 -142
- orbi_cli-0.7.1/src/orbi/review_rounds.py +229 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/runner.py +107 -126
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/status_observe.py +30 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/suggest.py +56 -7
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/PKG-INFO +4 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/SOURCES.txt +10 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_bootstrap_runner.py +145 -12
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_branch_reclaim.py +53 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_clarify.py +3 -3
- orbi_cli-0.7.1/tests/test_comment_identity.py +313 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_comment_readers_contract.py +14 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_failure_report.py +38 -12
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_fix_needed_loop.py +2 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_github.py +9 -7
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_human_review.py +15 -15
- orbi_cli-0.7.1/tests/test_message_language.py +555 -0
- orbi_cli-0.7.1/tests/test_notify.py +934 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_orbi.py +56 -7
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pilot_setup.py +40 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_progress.py +86 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_progress_wiring.py +7 -2
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_resume_e2e.py +3 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_resume_pr.py +12 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_review_merge.py +97 -10
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_runner_config.py +22 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_status_observe.py +243 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_suggest.py +141 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/LICENSE +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/docs/licenses/sustainable-use-license.md +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/setup.cfg +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/checks.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/cli_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/delivery_labels.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/engine_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/git_identity.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/git_transport.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/gitops.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/launchd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/merge_gate.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/milestone_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_activity.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pi_recovery.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/pilot_slots.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/progress_summary.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/release_notes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/run_state.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/runner_health.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/scene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/scheduler.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/scheduler_units.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/session_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/source_pool.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/systemd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi/ticket_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/entry_points.txt +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/src/orbi_cli.egg-info/top_level.txt +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_agents_md.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_apply_label_patch.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_failure_issue_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_failure_triage.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_macos_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ci_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_claude_plugin.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_install.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_milestone.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_packaging.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_cli_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_concurrency_e2e.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_constitution.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_coverage_gate.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_deliver_pr_fakes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_delivery_branch_delete.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_delivery_labels.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_delivery_scene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docker_docs.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docker_entrypoint.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docker_image_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_i18n.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_issue_shapes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_labels.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_mermaid.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_releases.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_single_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_docs_site.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_engine_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_failure.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_gh_read_retry.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_base_smoke.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_merge_smoke.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_network_retry.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_git_transport.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_github_fakes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_gitops.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_gitops_fakes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_import_graph.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_install_sh.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_issue_1367.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_issue_461.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_issue_template_config.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_journal.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_journal_events.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_launchd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_license.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_milestone_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ops_e2e.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_orbi_dashboard.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_orbi_exporter.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_patch_ratchet.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_activity.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_extensions.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_provider_templates.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_recovery.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pi_session.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pilot_slots.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_pr_issue_link_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_prerequisite_check.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_prompt_contract.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_public_contracts.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_publish_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_release_docs_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_release_ticket_templates.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_repo_config.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_resume_continue_e2e.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_run_artifacts.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_run_id_e2e.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_runner_health.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_runner_source_freshness.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_scene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_scheduler.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_size_ratchet.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_src_layout.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_steering_review.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_suite_hygiene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_systemd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_systemd_timer.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_ticket_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.1}/tests/test_user_acceptance_contract.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
800
|
-
"
|
|
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
|
|
47
|
-
#
|
|
48
|
-
#
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
174
|
-
"
|
|
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
|
-
"
|
|
178
|
-
f"`{ready_label}` label again:\n\n"
|
|
182
|
+
f"{message('clarify.repair', ready_label=ready_label)}\n\n"
|
|
179
183
|
"```\n"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"unfinished one.\n"
|
|
184
|
+
f"{message('clarify.repair_outcome')}\n"
|
|
185
|
+
f"{message('clarify.repair_acceptance')}\n"
|
|
183
186
|
"```\n\n"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
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
|
|
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(
|
|
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
|
-
|
|
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),
|