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.
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/PKG-INFO +4 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/README.md +3 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/pyproject.toml +1 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/__init__.py +1 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/branch_reclaim.py +48 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/claim.py +38 -39
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/clarify.py +42 -30
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/cli.py +22 -24
- orbi_cli-0.7.2/src/orbi/comment_kind.py +68 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/config.py +30 -1
- orbi_cli-0.7.2/src/orbi/delivery_comment.py +96 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/example_config.toml +19 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/failure.py +77 -52
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/failure_report.py +148 -142
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/github.py +25 -36
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/human_review.py +64 -56
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/journal.py +4 -0
- orbi_cli-0.7.2/src/orbi/language.py +942 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/merge_handoff.py +51 -21
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/milestone.py +61 -31
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/milestone_command.py +8 -5
- orbi_cli-0.7.2/src/orbi/notify.py +595 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_process.py +3 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_session.py +19 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pilot_setup.py +3 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/progress.py +167 -118
- orbi_cli-0.7.2/src/orbi/progress_outcome.py +95 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/release.py +54 -84
- orbi_cli-0.7.2/src/orbi/release_comments.py +123 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/release_git.py +33 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/repo_config.py +43 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/review_merge.py +190 -201
- orbi_cli-0.7.2/src/orbi/review_rounds.py +229 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/runner.py +165 -175
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/runner_health.py +17 -10
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/status_observe.py +30 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/suggest.py +109 -16
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/ticket_command.py +61 -19
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/PKG-INFO +4 -4
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/SOURCES.txt +10 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_bootstrap_runner.py +778 -129
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_branch_reclaim.py +53 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_clarify.py +3 -3
- orbi_cli-0.7.2/tests/test_comment_identity.py +313 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_comment_readers_contract.py +20 -9
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_deliver_pr_fakes.py +3 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_delivery_branch_delete.py +5 -5
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_delivery_scene.py +64 -14
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_failure.py +21 -21
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_failure_report.py +400 -38
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_fix_needed_loop.py +30 -10
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_merge_smoke.py +6 -6
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_github.py +9 -7
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_human_review.py +24 -23
- orbi_cli-0.7.2/tests/test_message_language.py +815 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_milestone_command.py +203 -0
- orbi_cli-0.7.2/tests/test_notify.py +1168 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_orbi.py +56 -7
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pilot_setup.py +67 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_progress.py +147 -29
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_progress_wiring.py +82 -8
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_repo_config.py +32 -11
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_resume_continue_e2e.py +6 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_resume_e2e.py +145 -5
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_resume_pr.py +158 -51
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_review_merge.py +127 -40
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_runner_config.py +96 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_runner_health.py +127 -2
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_status_observe.py +243 -3
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_steering_review.py +26 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_suggest.py +419 -15
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ticket_command.py +3 -1
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/LICENSE +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/docs/licenses/sustainable-use-license.md +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/setup.cfg +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/checks.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/cli_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/delivery_labels.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/delivery_scene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/engine_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/git_identity.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/git_transport.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/gitops.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/launchd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/merge_gate.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_activity.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pi_recovery.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/pilot_slots.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/progress_summary.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/release_notes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/run_state.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/scene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/scheduler.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/scheduler_units.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/session_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/source_pool.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi/systemd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/entry_points.txt +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/src/orbi_cli.egg-info/top_level.txt +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_agents_md.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_apply_label_patch.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_failure_issue_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_failure_triage.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_macos_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ci_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_claude_plugin.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_install.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_milestone.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_packaging.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_cli_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_concurrency_e2e.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_constitution.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_coverage_gate.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_delivery_labels.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docker_docs.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docker_entrypoint.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docker_image_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_i18n.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_issue_shapes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_labels.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_mermaid.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_releases.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_single_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_docs_site.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_engine_source.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_gh_read_retry.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_base_smoke.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_network_retry.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_git_transport.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_github_fakes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_gitops.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_gitops_fakes.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_import_graph.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_install_sh.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_issue_1367.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_issue_461.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_issue_template_config.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_journal.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_journal_events.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_launchd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_license.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_ops_e2e.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_orbi_dashboard.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_orbi_exporter.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_patch_ratchet.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_activity.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_command.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_extensions.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_provider_templates.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_recovery.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pi_session.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pilot_slots.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_pr_issue_link_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_prerequisite_check.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_prompt_contract.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_public_contracts.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_publish_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_release_docs_workflow.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_release_ticket_templates.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_run_artifacts.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_run_id_e2e.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_runner_source_freshness.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_scene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_scheduler.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_size_ratchet.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_src_layout.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_suite_hygiene.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_systemd_deploy.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_systemd_timer.py +0 -0
- {orbi_cli-0.7.0 → orbi_cli-0.7.2}/tests/test_user_acceptance_contract.py +0 -0
- {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.
|
|
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
|
|
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.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"
|
|
@@ -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
|
-
|
|
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
|
|
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[
|
|
54
|
-
block_scene_failure: Callable[
|
|
55
|
-
recover_missing_pr_scene: Callable[
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
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,
|
|
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) -> 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
|
-
|
|
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
|
-
"
|
|
174
|
-
"
|
|
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
|
-
"
|
|
178
|
-
f"`{ready_label}` label again:\n\n"
|
|
183
|
+
f"{message('clarify.repair', ready_label=ready_label)}\n\n"
|
|
179
184
|
"```\n"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"unfinished one.\n"
|
|
185
|
+
f"{message('clarify.repair_outcome')}\n"
|
|
186
|
+
f"{message('clarify.repair_acceptance')}\n"
|
|
183
187
|
"```\n\n"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
f"
|
|
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
|
|
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
|