orbi-cli 0.7.28__tar.gz → 0.7.30__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.28 → orbi_cli-0.7.30}/PKG-INFO +1 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/pyproject.toml +1 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/__init__.py +1 -1
- orbi_cli-0.7.28/src/orbi/criteria_command.py → orbi_cli-0.7.30/src/orbi/amend_command.py +9 -9
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/base_ci_red.py +35 -24
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/check_permission.py +1 -20
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/checks.py +29 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/claim.py +8 -8
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_commands.py +3 -3
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_source.py +18 -15
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_ticket_command.py +10 -12
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/comment_kind.py +7 -1
- orbi_cli-0.7.30/src/orbi/comment_rest_id.py +29 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/config.py +16 -6
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/failure_report.py +5 -13
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/git_transport.py +109 -3
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/github.py +33 -33
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/human_review.py +2 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/language_terms.py +2 -2
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/merge_exec.py +21 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/milestone.py +3 -2
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/milestone_command.py +3 -7
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_command.py +44 -10
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_process.py +28 -29
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_recovery.py +42 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_session.py +38 -8
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_stream_log.py +34 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pilot_setup.py +6 -4
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress.py +2 -2
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_closeout.py +1 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner.py +15 -13
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner_source.py +6 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/setup_checks.py +35 -10
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/source_pool.py +6 -12
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/split_command.py +9 -58
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/ticket_command.py +80 -28
- orbi_cli-0.7.30/src/orbi/ticket_command_registry.py +86 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/PKG-INFO +1 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/SOURCES.txt +7 -2
- orbi_cli-0.7.28/tests/test_criteria_command.py → orbi_cli-0.7.30/tests/test_amend_command.py +137 -35
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_base_ci_red.py +133 -10
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_delivery.py +13 -10
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_release.py +21 -12
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_runner.py +68 -7
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_install.py +1 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_packaging.py +2 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_source.py +118 -7
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_ticket_commands.py +3 -3
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_comment_identity.py +22 -0
- orbi_cli-0.7.30/tests/test_comment_rest_id.py +47 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_i18n.py +8 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_site.py +5 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_transport.py +66 -2
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_github.py +101 -5
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_github_fakes.py +42 -6
- orbi_cli-0.7.30/tests/test_pi_child_reap.py +181 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_command.py +67 -7
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_session.py +82 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pilot_setup.py +85 -2
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_prerequisite_check.py +56 -4
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_progress.py +36 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_resume_continue_e2e.py +4 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_review_merge.py +80 -4
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_runner_config.py +97 -1
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_runner_source_freshness.py +28 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_split_command.py +0 -6
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_steering_review.py +136 -18
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_suggest.py +27 -15
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ticket_command.py +11 -3
- orbi_cli-0.7.30/tests/test_ticket_command_registration.py +221 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/LICENSE +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/README.md +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/docs/licenses/sustainable-use-license.md +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/setup.cfg +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/blocked_comment.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/branch_reclaim.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/clarify.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_doctor.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_parser.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/cli_status.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/comment_trust.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/delivery_comment.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/delivery_labels.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/delivery_scene.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/engine_source.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/example_config.toml +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/extensions/clarify_verdict.ts +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/failure.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/gh_response.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/git_identity.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/github_feedback.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/gitops.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/journal.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/language.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/launchd_deploy.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/merge_gate.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/merge_handoff.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/notify.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_activity.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pi_idle.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pilot_slots.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/pr_verification.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress_display.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress_outcome.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/progress_summary.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_comments.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_declaration.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_git.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_notes.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/release_run.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/repo_config.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/resume_scan.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/resume_verification.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/review_merge.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/review_rounds.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/review_verdict.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/run_state.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner_health.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/runner_marker.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/scene.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/scheduler.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/scheduler_units.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/session_command.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/setup_config.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/status_observe.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/suggest.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/systemd_deploy.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi/user_summary.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/entry_points.txt +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/src/orbi_cli.egg-info/top_level.txt +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_agents_md.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_apply_label_patch.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_base_branch_failure.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_blocked_comment.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_adapters.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_pi.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_bootstrap_preflight.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_branch_reclaim.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_check_permission.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_failure_issue_workflow.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_failure_triage.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_macos_workflow.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ci_workflow.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_clarify.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_claude_plugin.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_golden.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_cli_milestone.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_command_receipt_contract.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_comment_readers_contract.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_comment_trust.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_concurrency_e2e.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_constitution.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_coverage_gate.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_deliver_pr_fakes.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_delivery_branch_delete.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_delivery_labels.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_delivery_scene.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docker_docs.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docker_entrypoint.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docker_image_workflow.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_issue_shapes.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_labels.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_mermaid.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_releases.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_docs_single_source.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_engine_source.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_failure.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_failure_report.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_fix_needed_loop.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gh_read_retry.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gh_response.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_base_smoke.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_merge_smoke.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_git_network_retry.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gitops.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_gitops_fakes.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_human_review.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_import_graph.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_install_sh.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_issue_1367.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_issue_461.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_issue_template_config.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_journal.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_journal_events.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_launchd_deploy.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_license.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_merge_exec_extraction.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_message_language.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_milestone_command.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_notify.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_ops_e2e.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_orbi.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_orbi_dashboard.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_orbi_exporter.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_patch_ratchet.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_activity.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_extensions.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_provider_templates.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pi_recovery.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pilot_slots.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pr_issue_link_workflow.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_pr_verification.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_progress_wiring.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_prompt_contract.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_public_contracts.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_public_helpers.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_publish_workflow.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_release_docs_workflow.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_release_extraction.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_release_ticket_templates.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_repo_config.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_resume_e2e.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_resume_pr.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_review_verdict.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_run_artifacts.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_run_id_e2e.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_run_id_fixture.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_runner_health.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_scene.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_scheduler.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_seam.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_setup_output_golden.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_size_ratchet.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_src_layout.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_status_observe.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_suite_hygiene.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_systemd_deploy.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_systemd_timer.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_user_acceptance_contract.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_user_summary.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/tests/test_versioned_comment_block.py +0 -0
- {orbi_cli-0.7.28 → orbi_cli-0.7.30}/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.30
|
|
4
4
|
Summary: Orbi — local AI development worker: claims GitHub Issues, runs Pi in isolated worktrees, and delivers PRs (GitHub Issues + labels are the only state store).
|
|
5
5
|
Author: xqliu
|
|
6
6
|
License-Expression: AGPL-3.0-only OR LicenseRef-Sustainable-Use-1.0
|
|
@@ -44,7 +44,7 @@ build-backend = "setuptools.build_meta"
|
|
|
44
44
|
|
|
45
45
|
[project]
|
|
46
46
|
name = "orbi-cli"
|
|
47
|
-
version = "0.7.
|
|
47
|
+
version = "0.7.30"
|
|
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"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""`/orbi amend`: put the proposed acceptance criteria on the ticket.
|
|
2
2
|
|
|
3
3
|
The clarification gate stops a thin ticket and may attach a `kind=criteria`
|
|
4
|
-
proposal to its stop comment (Issue #1926). This module
|
|
4
|
+
proposal to its stop comment (Issue #1926). This module defines the one
|
|
5
5
|
command that acts on it: a person with write permission replies
|
|
6
6
|
`/orbi amend` and the proposed conditions — **as the comment reads at
|
|
7
7
|
that moment**, so editing it first changes what lands — are appended to the
|
|
@@ -30,7 +30,9 @@ still scanned on the next tick and the same command continues from what is
|
|
|
30
30
|
already on the ticket; a ticket that already carries this command's success
|
|
31
31
|
receipt only gets step 4. The success receipt uses the command layer's own
|
|
32
32
|
marker keyed on the command comment, so "has this command already
|
|
33
|
-
succeeded?" is a marker lookup over the raw bodies, never prose
|
|
33
|
+
succeeded?" is a marker lookup over the raw bodies, never prose — and only
|
|
34
|
+
a receipt THIS runner wrote counts (`runner_receipt`): the marker is public
|
|
35
|
+
text, so a copied one must never stand in for the work (Issue #1992).
|
|
34
36
|
|
|
35
37
|
The two steps that change what the next tick reads — the append and the
|
|
36
38
|
dispatch label — are wrapped in the shared `ticket_command.run_step`, so
|
|
@@ -53,8 +55,8 @@ from orbi.ticket_command import (
|
|
|
53
55
|
TicketCommandError,
|
|
54
56
|
command_comment_key,
|
|
55
57
|
command_receipt_marker,
|
|
56
|
-
register_command,
|
|
57
58
|
run_step,
|
|
59
|
+
runner_receipt,
|
|
58
60
|
)
|
|
59
61
|
|
|
60
62
|
# The heading the conditions land under follows the message language
|
|
@@ -189,10 +191,10 @@ def _criteria_apply(
|
|
|
189
191
|
language.text(language_code, "criteria.reason_no_suggestion"),
|
|
190
192
|
)
|
|
191
193
|
receipt = (
|
|
192
|
-
f"<!-- {command_receipt_marker(
|
|
194
|
+
f"<!-- {command_receipt_marker(AMEND_COMMAND)} "
|
|
193
195
|
f"comment={command_comment_key(command_comment)} -->"
|
|
194
196
|
)
|
|
195
|
-
if not
|
|
197
|
+
if not runner_receipt(comments, receipt):
|
|
196
198
|
current = issue.get("body") or ""
|
|
197
199
|
if appended_marker() not in current:
|
|
198
200
|
section = render_acceptance(suggestion.items, language_code)
|
|
@@ -229,8 +231,8 @@ def _criteria_apply(
|
|
|
229
231
|
edit_issue(issue_number, repo=repo, remove=NEEDS_DETAIL_LABEL)
|
|
230
232
|
|
|
231
233
|
|
|
232
|
-
|
|
233
|
-
name="
|
|
234
|
+
AMEND_COMMAND = CommandSpec(
|
|
235
|
+
name="amend",
|
|
234
236
|
subcommand="amend",
|
|
235
237
|
arity=0,
|
|
236
238
|
permission="write",
|
|
@@ -242,5 +244,3 @@ ADD_CRITERIA_COMMAND = CommandSpec(
|
|
|
242
244
|
),
|
|
243
245
|
context=_criteria_context,
|
|
244
246
|
)
|
|
245
|
-
|
|
246
|
-
register_command(ADD_CRITERIA_COMMAND)
|
|
@@ -38,13 +38,14 @@ from __future__ import annotations
|
|
|
38
38
|
import dataclasses
|
|
39
39
|
import json
|
|
40
40
|
import logging
|
|
41
|
-
import re
|
|
42
41
|
from datetime import datetime, timezone
|
|
43
42
|
|
|
44
43
|
from orbi import comment_trust, failure, language, user_summary
|
|
45
44
|
from orbi.comment_kind import (
|
|
46
45
|
COMMENT_KIND_FAILURE, comment_kind, parse_versioned_block,
|
|
46
|
+
versioned_block_pattern,
|
|
47
47
|
)
|
|
48
|
+
from orbi.comment_rest_id import rest_comment_id
|
|
48
49
|
from orbi.delivery_labels import (
|
|
49
50
|
BLOCKED_LABEL, FIX_NEEDED_LABEL, P0_LABEL, READY_LABEL,
|
|
50
51
|
)
|
|
@@ -58,7 +59,7 @@ from orbi.pi_activity import parse_iso_utc
|
|
|
58
59
|
from orbi.progress import fenced
|
|
59
60
|
from orbi.ticket_command import (
|
|
60
61
|
CommandSpec, TicketCommandError, command_comment_key,
|
|
61
|
-
command_receipt_marker, process_commands,
|
|
62
|
+
command_receipt_marker, process_commands, runner_receipt,
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
SCHEMA_VERSION = 1
|
|
@@ -159,10 +160,27 @@ def _validate(record: BaseCIRed) -> None:
|
|
|
159
160
|
)
|
|
160
161
|
|
|
161
162
|
|
|
163
|
+
def _serialize(payload: dict) -> str:
|
|
164
|
+
"""One block payload: compact JSON with the HTML delimiters escaped.
|
|
165
|
+
|
|
166
|
+
The block is an HTML comment and the record carries GitHub's own
|
|
167
|
+
text: a check name may hold the comment terminator (a workflow named
|
|
168
|
+
after a shell pipeline) and the base branch is a user string.
|
|
169
|
+
Unescaped, a terminator closes the comment early, the next tick's
|
|
170
|
+
`parse` reads a truncated payload as corruption, and `paused_scene`
|
|
171
|
+
abandons the recovery (Issue #1998). The JSON \\u003c / \\u003e escapes
|
|
172
|
+
decode back to the exact same value, exactly the way the progress
|
|
173
|
+
block serializes its summary (`progress_summary.serialize_summary`).
|
|
174
|
+
"""
|
|
175
|
+
return json.dumps(
|
|
176
|
+
payload, separators=(",", ":"), ensure_ascii=False,
|
|
177
|
+
).replace("<", "\\u003c").replace(">", "\\u003e")
|
|
178
|
+
|
|
179
|
+
|
|
162
180
|
def render(record: BaseCIRed) -> str:
|
|
163
181
|
"""Render the single hidden block for one record (deterministic)."""
|
|
164
182
|
_validate(record)
|
|
165
|
-
payload =
|
|
183
|
+
payload = _serialize(
|
|
166
184
|
{
|
|
167
185
|
"schema": record.schema,
|
|
168
186
|
"state": record.state,
|
|
@@ -171,7 +189,6 @@ def render(record: BaseCIRed) -> str:
|
|
|
171
189
|
"pr": record.pr,
|
|
172
190
|
"failing_checks": list(record.failing_checks),
|
|
173
191
|
},
|
|
174
|
-
separators=(",", ":"), ensure_ascii=False,
|
|
175
192
|
)
|
|
176
193
|
return BLOCK_TEMPLATE.format(payload=payload)
|
|
177
194
|
|
|
@@ -474,6 +491,12 @@ def rewrite_state(body: str, state: str, *, language_code: str) -> str:
|
|
|
474
491
|
The state line is APPENDED above the block, so the comment keeps a
|
|
475
492
|
short timeline (paused -> green again -> merged) instead of
|
|
476
493
|
rewriting history; the block itself is replaced, never duplicated.
|
|
494
|
+
|
|
495
|
+
The block is located by its marker ENVELOPE, never by the exact text
|
|
496
|
+
today's `render` produces: a comment written before #1998 escaped the
|
|
497
|
+
HTML delimiters carries a raw `<`/`>` (a check name such as
|
|
498
|
+
`tests (>=3.10)`) and still parses, so a text-exact replace would
|
|
499
|
+
silently no-op and leave it `waiting` for good.
|
|
477
500
|
"""
|
|
478
501
|
record = parse(body)
|
|
479
502
|
if record is None:
|
|
@@ -500,8 +523,8 @@ def rewrite_state(body: str, state: str, *, language_code: str) -> str:
|
|
|
500
523
|
)
|
|
501
524
|
if line.strip() not in (text.strip() for text in lines[:marker_index]):
|
|
502
525
|
lines[marker_index:marker_index] = [line, ""]
|
|
503
|
-
return
|
|
504
|
-
render(
|
|
526
|
+
return versioned_block_pattern(_MARKER).sub(
|
|
527
|
+
lambda _match: render(updated), "\n".join(lines), count=1,
|
|
505
528
|
).rstrip() + "\n"
|
|
506
529
|
|
|
507
530
|
|
|
@@ -509,19 +532,6 @@ def rewrite_state(body: str, state: str, *, language_code: str) -> str:
|
|
|
509
532
|
# The in-place edit of the one comment
|
|
510
533
|
# --------------------------------------------------------------------------
|
|
511
534
|
|
|
512
|
-
_COMMENT_URL_ID_RE = re.compile(r"#issuecomment-(\d+)$")
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def _rest_comment_id(comment: dict) -> int | None:
|
|
516
|
-
"""The REST id of one comment, from the id field or its permalink."""
|
|
517
|
-
comment_id = comment.get("id")
|
|
518
|
-
if isinstance(comment_id, int) and not isinstance(comment_id, bool):
|
|
519
|
-
return comment_id
|
|
520
|
-
url = comment.get("url")
|
|
521
|
-
match = _COMMENT_URL_ID_RE.search(url) if isinstance(url, str) else None
|
|
522
|
-
return int(match.group(1)) if match else None
|
|
523
|
-
|
|
524
|
-
|
|
525
535
|
def _located(comments: list[dict]) -> tuple[BaseCIRed, dict] | None:
|
|
526
536
|
"""The newest comment carrying a valid block, and its record.
|
|
527
537
|
|
|
@@ -548,7 +558,7 @@ def _edit_state(repo: str, issue_number: int, comment: dict,
|
|
|
548
558
|
body = comment.get("body")
|
|
549
559
|
if not isinstance(body, str):
|
|
550
560
|
return False
|
|
551
|
-
comment_id =
|
|
561
|
+
comment_id = rest_comment_id(comment)
|
|
552
562
|
if comment_id is None:
|
|
553
563
|
LOGGER.warning(
|
|
554
564
|
"base CI red comment has no REST id issue=%s repo=%s",
|
|
@@ -894,7 +904,10 @@ def _fix_base_apply(
|
|
|
894
904
|
Idempotent per COMMENT: the reply carries the command layer's own
|
|
895
905
|
receipt marker, so a second tick that still sees the same command
|
|
896
906
|
comment does nothing (the block stays `waiting` until the base turns
|
|
897
|
-
green, so the comment is visible for several ticks).
|
|
907
|
+
green, so the comment is visible for several ticks). Only a receipt
|
|
908
|
+
THIS runner wrote counts (`runner_receipt`): the marker is public
|
|
909
|
+
text, so a copied one must never read as "the fix Issue was opened"
|
|
910
|
+
(Issue #1992).
|
|
898
911
|
"""
|
|
899
912
|
if not isinstance(record, BaseCIRed):
|
|
900
913
|
# The layer's contract: a handler that cannot run raises its own
|
|
@@ -908,7 +921,7 @@ def _fix_base_apply(
|
|
|
908
921
|
f"<!-- {command_receipt_marker(FIX_BASE_COMMAND)} "
|
|
909
922
|
f"comment={command_comment_key(command_comment)} -->"
|
|
910
923
|
)
|
|
911
|
-
if
|
|
924
|
+
if runner_receipt(comments, receipt):
|
|
912
925
|
return
|
|
913
926
|
checks = failing_checks(repo, list(record.failing_checks), record.base_sha)
|
|
914
927
|
if not checks:
|
|
@@ -967,5 +980,3 @@ FIX_BASE_COMMAND = CommandSpec(
|
|
|
967
980
|
),
|
|
968
981
|
context=_fix_base_context,
|
|
969
982
|
)
|
|
970
|
-
|
|
971
|
-
register_command(FIX_BASE_COMMAND)
|
|
@@ -49,6 +49,7 @@ from orbi.comment_kind import (
|
|
|
49
49
|
COMMENT_KIND_FAILURE, comment_kind, comment_kind_marker,
|
|
50
50
|
parse_versioned_block,
|
|
51
51
|
)
|
|
52
|
+
from orbi.comment_rest_id import rest_comment_id
|
|
52
53
|
from orbi.delivery_labels import (
|
|
53
54
|
BLOCKED_LABEL, FIX_NEEDED_LABEL, READY_LABEL,
|
|
54
55
|
)
|
|
@@ -81,9 +82,6 @@ _MARKER = "orbi:check-permission:v1"
|
|
|
81
82
|
BLOCK_TEMPLATE = f"<!-- {_MARKER} {{payload}} -->"
|
|
82
83
|
_FIELDS = ("schema", "pr", "state")
|
|
83
84
|
|
|
84
|
-
# The REST id carried by a comment's permalink (see `rest_comment_id`).
|
|
85
|
-
_COMMENT_URL_ID_RE = re.compile(r"#issuecomment-(\d+)$")
|
|
86
|
-
|
|
87
85
|
# One bounded page per recovered scan: `ai-blocked` Issues are few, and an
|
|
88
86
|
# unbounded scan would multiply the per-tick read cost.
|
|
89
87
|
_SCAN_LIMIT = 200
|
|
@@ -369,23 +367,6 @@ def _waiting_block(comments: list[dict], *,
|
|
|
369
367
|
return record, comment
|
|
370
368
|
|
|
371
369
|
|
|
372
|
-
def rest_comment_id(comment: dict) -> int | None:
|
|
373
|
-
"""The REST id of one Issue comment, from its id field or its permalink.
|
|
374
|
-
|
|
375
|
-
`gh issue view --json comments` reports the GraphQL node id — an
|
|
376
|
-
`"IC_kw..."` string no REST route accepts — next to the permalink, and
|
|
377
|
-
the permalink's `#issuecomment-<digits>` suffix IS the REST id the
|
|
378
|
-
in-place edit's `PATCH /repos/{repo}/issues/comments/{id}` takes (the
|
|
379
|
-
same rule `base_ci_red` and the failure reporter follow).
|
|
380
|
-
"""
|
|
381
|
-
comment_id = comment.get("id")
|
|
382
|
-
if isinstance(comment_id, int) and not isinstance(comment_id, bool):
|
|
383
|
-
return comment_id
|
|
384
|
-
url = comment.get("url")
|
|
385
|
-
match = _COMMENT_URL_ID_RE.search(url) if isinstance(url, str) else None
|
|
386
|
-
return int(match.group(1)) if match else None
|
|
387
|
-
|
|
388
|
-
|
|
389
370
|
def _edit_state(repo: str, issue_number: int, comment: dict,
|
|
390
371
|
record: CheckPermission) -> None:
|
|
391
372
|
"""Patch the ONE comment in place to the record's next state."""
|
|
@@ -12,6 +12,8 @@ stays the I/O leaf and no domain module owns a private copy.
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import json
|
|
16
|
+
|
|
15
17
|
# A status a check reports once it has concluded (the rollup's CheckRun
|
|
16
18
|
# `COMPLETED`, a legacy status context's `SUCCESS`/`FAILURE`/`ERROR`);
|
|
17
19
|
# anything else is still running. GitHub recomputes mergeability and
|
|
@@ -21,6 +23,33 @@ _FINAL_STATUSES = ("COMPLETED", "SUCCESS", "FAILURE", "ERROR")
|
|
|
21
23
|
_PASSING_CONCLUSIONS = ("SUCCESS", "NEUTRAL", "SKIPPED")
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def parse_paginated_check_runs(raw: str) -> list:
|
|
27
|
+
"""Flatten the check-run pages emitted by ``gh api --paginate --slurp``.
|
|
28
|
+
|
|
29
|
+
The list-check-runs REST response is one object per page —
|
|
30
|
+
``{"total_count": N, "check_runs": [...]}`` — so the slurped output is
|
|
31
|
+
an array of those objects, one per page, and the caller receives every
|
|
32
|
+
run in page order (Issue #1993: GitHub serves 30 runs per page, so an
|
|
33
|
+
unpaginated read hid the 31st). Any other shape fails fast: a body that
|
|
34
|
+
cannot be read as pages must never pass for a complete — or passing —
|
|
35
|
+
check list.
|
|
36
|
+
"""
|
|
37
|
+
pages = json.loads(raw)
|
|
38
|
+
if not isinstance(pages, list):
|
|
39
|
+
raise ValueError("paginated check-runs response must be an array of pages")
|
|
40
|
+
runs: list = []
|
|
41
|
+
for page in pages:
|
|
42
|
+
if not isinstance(page, dict) or not isinstance(page.get("check_runs"), list):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"paginated check-runs page must carry a check_runs array"
|
|
45
|
+
)
|
|
46
|
+
page_runs = page["check_runs"]
|
|
47
|
+
if any(not isinstance(run, dict) for run in page_runs):
|
|
48
|
+
raise ValueError("paginated check-runs page contains a non-object item")
|
|
49
|
+
runs.extend(page_runs)
|
|
50
|
+
return runs
|
|
51
|
+
|
|
52
|
+
|
|
24
53
|
def render_check(entry: dict) -> str:
|
|
25
54
|
"""Render one classified check entry for humans, from the data.
|
|
26
55
|
|
|
@@ -15,12 +15,10 @@ import os
|
|
|
15
15
|
from collections.abc import Sequence
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
-
from orbi import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# registry by then.
|
|
23
|
-
from orbi import criteria_command, split_command # noqa: F401
|
|
18
|
+
from orbi import (
|
|
19
|
+
base_ci_red, check_permission, gitops, ticket_command,
|
|
20
|
+
ticket_command_registry,
|
|
21
|
+
)
|
|
24
22
|
from orbi import milestone as milestone_bookkeeping
|
|
25
23
|
from orbi.config import RunnerConfig
|
|
26
24
|
from orbi.delivery_labels import (
|
|
@@ -216,7 +214,7 @@ def reconcile_open_epics(repo: str, run_id: str, language_code: str) -> list[str
|
|
|
216
214
|
comments = issue_comments(int(number), repo=repo)
|
|
217
215
|
if not epic_audit_posted(comments, child_evidence):
|
|
218
216
|
comment_issue(int(number), repo=repo,
|
|
219
|
-
body=f"
|
|
217
|
+
body=f"{run_marker(run_id)}\n{audit}\nrun_id={run_id}")
|
|
220
218
|
close_issue(int(number), repo=repo)
|
|
221
219
|
event("epic_closed", issue=number, repo=repo)
|
|
222
220
|
evidence.append(f"Epic #{number} closed after verification ({'; '.join(child_evidence)})")
|
|
@@ -798,7 +796,9 @@ def pick_next_delivery(
|
|
|
798
796
|
# recovery scans above.
|
|
799
797
|
try:
|
|
800
798
|
ticket_command.process_labeled_commands(
|
|
801
|
-
repo, NEEDS_DETAIL_LABEL,
|
|
799
|
+
repo, NEEDS_DETAIL_LABEL,
|
|
800
|
+
commands=ticket_command_registry.ticket_commands(),
|
|
801
|
+
dispatch_label=dispatch_label,
|
|
802
802
|
language_code=repo_language,
|
|
803
803
|
)
|
|
804
804
|
except Exception:
|
|
@@ -26,7 +26,7 @@ from orbi import (
|
|
|
26
26
|
scheduler,
|
|
27
27
|
status_observe,
|
|
28
28
|
suggest,
|
|
29
|
-
|
|
29
|
+
ticket_command_registry,
|
|
30
30
|
)
|
|
31
31
|
from orbi.cli_doctor import doctor_report
|
|
32
32
|
from orbi.cli_ticket_command import command_ticket_command
|
|
@@ -73,7 +73,7 @@ READ_ONLY_COMMANDS = frozenset({"status", "doctor", "session", "check"})
|
|
|
73
73
|
# actions like the maintenance ones: no model call, so the same rule
|
|
74
74
|
# applies (Issue #1930).
|
|
75
75
|
_TICKET_COMMANDS = frozenset(
|
|
76
|
-
spec.subcommand for spec in
|
|
76
|
+
spec.subcommand for spec in ticket_command_registry.cli_specs()
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
# A missing provider key must never block these deployment maintenance
|
|
@@ -388,6 +388,6 @@ COMMAND_HANDLERS = {
|
|
|
388
388
|
# of `/orbi fix-base`, so the table is derived from the registry.
|
|
389
389
|
**{
|
|
390
390
|
spec.subcommand: command_ticket_command
|
|
391
|
-
for spec in
|
|
391
|
+
for spec in ticket_command_registry.cli_specs()
|
|
392
392
|
},
|
|
393
393
|
}
|
|
@@ -46,7 +46,7 @@ import subprocess
|
|
|
46
46
|
from pathlib import Path
|
|
47
47
|
|
|
48
48
|
import orbi
|
|
49
|
-
from orbi.journal import quote_value
|
|
49
|
+
from orbi.journal import quote_value, run_command
|
|
50
50
|
|
|
51
51
|
# The Python floor (Issue #861): the same floor as the PEP 621
|
|
52
52
|
# `requires-python` — pinned together by tests/test_cli_packaging.py
|
|
@@ -70,20 +70,21 @@ PROBE_TIMEOUT_SECONDS = 10
|
|
|
70
70
|
def system_python3_compatible(exe: str) -> bool:
|
|
71
71
|
"""Whether one concrete interpreter satisfies REQUIRED_PYTHON.
|
|
72
72
|
|
|
73
|
-
Runs the :data:`PYTHON_PROBE_CODE` probe on ``exe``
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
Runs the :data:`PYTHON_PROBE_CODE` probe on ``exe`` through the
|
|
74
|
+
single subprocess seam (:func:`orbi.journal.run_command`,
|
|
75
|
+
CONSTITUTION Article 3.4), with a timeout — a blocking command
|
|
76
|
+
never runs bare. Any failure (missing executable, non-zero probe
|
|
77
|
+
exit, timeout) is NOT compatible: the selection fails safe to the
|
|
78
|
+
uv-provisioned interpreter, which uv resolves against system AND
|
|
79
|
+
managed interpreters either way.
|
|
79
80
|
"""
|
|
80
81
|
if not exe:
|
|
81
82
|
return False
|
|
82
83
|
try:
|
|
83
|
-
probe =
|
|
84
|
+
probe = run_command(
|
|
84
85
|
[exe, "-S", "-c", PYTHON_PROBE_CODE],
|
|
85
|
-
capture_output=True,
|
|
86
86
|
timeout=PROBE_TIMEOUT_SECONDS,
|
|
87
|
+
check=False,
|
|
87
88
|
)
|
|
88
89
|
except (OSError, subprocess.SubprocessError):
|
|
89
90
|
return False
|
|
@@ -99,6 +100,11 @@ def select_python_interpreter() -> str:
|
|
|
99
100
|
ships 3.12.3; on a fresh uv-only host there is no system python3
|
|
100
101
|
at all). ``orbi setup``, the Runner pre-start refresh and the
|
|
101
102
|
systemd self-heal template all apply this one rule.
|
|
103
|
+
|
|
104
|
+
The rule runs a subprocess probe, so it is decided ONLY when a
|
|
105
|
+
reinstall (or the fix string that asks for one) is actually about
|
|
106
|
+
to happen — never at import (Issue #2001: importing the Runner
|
|
107
|
+
must not spawn a probe before logging is configured).
|
|
102
108
|
"""
|
|
103
109
|
exe = shutil.which("python3")
|
|
104
110
|
if exe is not None and system_python3_compatible(exe):
|
|
@@ -106,10 +112,6 @@ def select_python_interpreter() -> str:
|
|
|
106
112
|
return "3.14"
|
|
107
113
|
|
|
108
114
|
|
|
109
|
-
# The selection result the reinstall argv (and every fix string)
|
|
110
|
-
# embeds — computed once per process.
|
|
111
|
-
PYTHON_INTERPRETER = select_python_interpreter()
|
|
112
|
-
|
|
113
115
|
# The runtime package directory inside a checkout (the src
|
|
114
116
|
# layout): the editable install maps this WHOLE directory, so a newly
|
|
115
117
|
# added package module is importable without regenerating any module
|
|
@@ -127,11 +129,12 @@ def reinstall_args(repo_dir: Path) -> list[str]:
|
|
|
127
129
|
reinstallation") and ``--python`` carries the compatible
|
|
128
130
|
interpreter selection (Issue #861: the system ``python3`` when it
|
|
129
131
|
satisfies the floor, otherwise the bare ``3.14`` so uv provisions
|
|
130
|
-
or selects one)
|
|
132
|
+
or selects one) — decided HERE, when the reinstall is about to
|
|
133
|
+
happen, so importing this module never runs the probe (Issue #2001).
|
|
131
134
|
"""
|
|
132
135
|
return [
|
|
133
136
|
"uv", "tool", "install", "--force", "--reinstall", "--editable",
|
|
134
|
-
"--python",
|
|
137
|
+
"--python", select_python_interpreter(), str(Path(repo_dir)),
|
|
135
138
|
]
|
|
136
139
|
|
|
137
140
|
|
|
@@ -5,8 +5,8 @@ A ticket command has two spellings and ONE execution path: a
|
|
|
5
5
|
<owner/name> --issue <N>` on the host. This module is the terminal one:
|
|
6
6
|
|
|
7
7
|
- `register_cli` adds one subcommand per registered arity-0 command
|
|
8
|
-
(`
|
|
9
|
-
spellings and there is nothing to edit here. A command that needs an
|
|
8
|
+
(`ticket_command_registry.cli_specs`), so registering a command gives
|
|
9
|
+
both spellings and there is nothing to edit here. A command that needs an
|
|
10
10
|
argument (`/orbi milestone <version>`) keeps its own explicit CLI command
|
|
11
11
|
(`orbi milestone set`) and is never generated;
|
|
12
12
|
- `command_ticket_command` runs the SAME `ticket_command.execute_command`
|
|
@@ -24,14 +24,12 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import sys
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
from orbi import
|
|
33
|
-
base_ci_red, criteria_command, split_command, ticket_command,
|
|
34
|
-
)
|
|
27
|
+
# Both the tree below and the CLI dispatch table derive from the ONE
|
|
28
|
+
# explicit list of registrants,
|
|
29
|
+
# `ticket_command_registry.ticket_commands()` (Issues #1930, #2000): this
|
|
30
|
+
# module names no command module, so a command can never be missing from
|
|
31
|
+
# the terminal spelling because an import somewhere else changed.
|
|
32
|
+
from orbi import ticket_command, ticket_command_registry
|
|
35
33
|
from orbi.claim import _repo_scan_keys
|
|
36
34
|
from orbi.github import authenticated_github_login
|
|
37
35
|
from orbi.repo_config import merged_repo_language
|
|
@@ -39,7 +37,7 @@ from orbi.repo_config import merged_repo_language
|
|
|
39
37
|
|
|
40
38
|
def register_cli(subparsers, common) -> None:
|
|
41
39
|
"""Add one subcommand per registered arity-0 ticket command."""
|
|
42
|
-
for spec in
|
|
40
|
+
for spec in ticket_command_registry.cli_specs():
|
|
43
41
|
command = subparsers.add_parser(
|
|
44
42
|
spec.subcommand, parents=[common],
|
|
45
43
|
help=f"run the registered `{spec.canonical}` ticket command on "
|
|
@@ -58,7 +56,7 @@ def register_cli(subparsers, common) -> None:
|
|
|
58
56
|
|
|
59
57
|
def command_ticket_command(config, args, parser) -> int:
|
|
60
58
|
"""Run one registered ticket command on one Issue (Issue #1930)."""
|
|
61
|
-
spec =
|
|
59
|
+
spec = ticket_command_registry.command_by_name(args.ticket_command)
|
|
62
60
|
repo = args.repo
|
|
63
61
|
milestone, dispatch_label = _repo_scan_keys(
|
|
64
62
|
config, repo, config.active_milestone,
|
|
@@ -33,6 +33,11 @@ import re
|
|
|
33
33
|
from collections.abc import Callable, Sequence
|
|
34
34
|
from typing import Any
|
|
35
35
|
|
|
36
|
+
# The run-id format belongs to `journal.validate_run_id` (Issue #2003):
|
|
37
|
+
# the anchored prefix below interpolates that pattern instead of spelling
|
|
38
|
+
# the id out, so a reader can never accept an id the writer refuses.
|
|
39
|
+
from orbi.journal import RUN_ID_PATTERN
|
|
40
|
+
|
|
36
41
|
COMMENT_KIND_STARTED = "started"
|
|
37
42
|
COMMENT_KIND_FAILURE = "failure"
|
|
38
43
|
COMMENT_KIND_REVIEW_ROUND = "review-round"
|
|
@@ -49,7 +54,8 @@ COMMENT_KIND_PATTERN = re.compile(r"<!-- orbi:comment=([a-z-]+) -->")
|
|
|
49
54
|
# the line immediately after. Anchoring means a marker-shaped string
|
|
50
55
|
# quoted in prose, a raw log or a code fence is never read as metadata.
|
|
51
56
|
_KIND_PREFIX_RE = re.compile(
|
|
52
|
-
r"\A<!-- orbi:run=
|
|
57
|
+
r"\A<!-- orbi:run=" + RUN_ID_PATTERN.pattern
|
|
58
|
+
+ r" -->\r?\n<!-- orbi:comment=([a-z-]+) -->"
|
|
53
59
|
)
|
|
54
60
|
|
|
55
61
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The REST id of one Issue comment record (Issue #2004).
|
|
2
|
+
|
|
3
|
+
`gh issue view --json comments` reports the GraphQL node id — an
|
|
4
|
+
`"IC_kw..."` string no REST route accepts — next to the comment's
|
|
5
|
+
permalink, and the permalink's fragment carries the REST id that the
|
|
6
|
+
in-place edit's `PATCH /repos/{repo}/issues/comments/{id}` route takes. A
|
|
7
|
+
numeric `id` field (an `gh api` read) is that id already.
|
|
8
|
+
|
|
9
|
+
One rule, one owner: the base-CI red recovery (`base_ci_red`), the
|
|
10
|
+
check-permission recovery (`check_permission`) and the failure reporter
|
|
11
|
+
(`failure_report`) all read the id from here, so the pattern lives in this
|
|
12
|
+
module only. `github.issue_comment_rest_id` stays the neighbouring rule
|
|
13
|
+
that resolves a GraphQL node id to a REST id with an API read.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
_COMMENT_URL_ID_RE = re.compile(r"#issuecomment-(\d+)$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def rest_comment_id(comment: dict) -> int | None:
|
|
23
|
+
"""The REST id of one comment, from the id field or its permalink."""
|
|
24
|
+
comment_id = comment.get("id")
|
|
25
|
+
if isinstance(comment_id, int) and not isinstance(comment_id, bool):
|
|
26
|
+
return comment_id
|
|
27
|
+
url = comment.get("url")
|
|
28
|
+
match = _COMMENT_URL_ID_RE.search(url) if isinstance(url, str) else None
|
|
29
|
+
return int(match.group(1)) if match else None
|
|
@@ -190,6 +190,20 @@ class RunnerConfig:
|
|
|
190
190
|
repositories: tuple[dict, ...] = ()
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
def parse_source_repos(value: object) -> tuple[str, ...]:
|
|
194
|
+
"""Validate the task-pool declaration; the one ``source_repos`` parser.
|
|
195
|
+
|
|
196
|
+
``load_config`` parses the whole config through it, and the source pool
|
|
197
|
+
uses it on a sibling deployment's plain TOML value (Issue #2002): the
|
|
198
|
+
rule is stated once, so the two callers can never drift apart.
|
|
199
|
+
"""
|
|
200
|
+
if not isinstance(value, list) or not value:
|
|
201
|
+
raise ValueError("source_repos must be a non-empty list")
|
|
202
|
+
if not all(isinstance(repo, str) and repo for repo in value):
|
|
203
|
+
raise ValueError("source_repos must contain non-empty strings")
|
|
204
|
+
return tuple(value)
|
|
205
|
+
|
|
206
|
+
|
|
193
207
|
class ConfigFileMissingError(FileNotFoundError):
|
|
194
208
|
"""The configured ``orbi.toml`` itself does not exist."""
|
|
195
209
|
|
|
@@ -209,11 +223,7 @@ def load_config(path: Path, *, check_provider_api_keys: bool = True,
|
|
|
209
223
|
if not path.exists():
|
|
210
224
|
raise ConfigFileMissingError(path)
|
|
211
225
|
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
212
|
-
source_repos = data.get("source_repos")
|
|
213
|
-
if not isinstance(source_repos, list) or not source_repos:
|
|
214
|
-
raise ValueError("source_repos must be a non-empty list")
|
|
215
|
-
if not all(isinstance(repo, str) and repo for repo in source_repos):
|
|
216
|
-
raise ValueError("source_repos must contain non-empty strings")
|
|
226
|
+
source_repos = parse_source_repos(data.get("source_repos"))
|
|
217
227
|
unit_name = data.get("unit_name")
|
|
218
228
|
if unit_name is not None and (
|
|
219
229
|
not isinstance(unit_name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", unit_name)
|
|
@@ -488,7 +498,7 @@ def load_config(path: Path, *, check_provider_api_keys: bool = True,
|
|
|
488
498
|
}
|
|
489
499
|
return RunnerConfig(
|
|
490
500
|
config_path=path.resolve(),
|
|
491
|
-
source_repos=
|
|
501
|
+
source_repos=source_repos,
|
|
492
502
|
repo_dir=repo_dir,
|
|
493
503
|
deploy_home=deploy_home,
|
|
494
504
|
unit_name=unit_name,
|
|
@@ -34,6 +34,8 @@ from orbi.comment_kind import (
|
|
|
34
34
|
COMMENT_KIND_FAILURE,
|
|
35
35
|
comment_kind_marker,
|
|
36
36
|
)
|
|
37
|
+
from orbi.comment_rest_id import rest_comment_id
|
|
38
|
+
from orbi.comment_trust import latest_run_marker, trusted_comments
|
|
37
39
|
from orbi.delivery_labels import (
|
|
38
40
|
EVENT_FIX_NEEDED, FIX_NEEDED_LABEL, IN_PROGRESS_LABEL, READY_LABEL,
|
|
39
41
|
)
|
|
@@ -41,7 +43,6 @@ from orbi.failure import (
|
|
|
41
43
|
failure_comment_body, failure_detail, failure_summary,
|
|
42
44
|
redact_local_paths, terminal_action,
|
|
43
45
|
)
|
|
44
|
-
from orbi.comment_trust import latest_run_marker, trusted_comments
|
|
45
46
|
from orbi.github import (
|
|
46
47
|
pr_number, apply_label_patch, comment_issue, issue_comments,
|
|
47
48
|
issue_labels, issue_priority, pr_comments, run_gh_write_command,
|
|
@@ -875,17 +876,8 @@ def report_delivery_failure(
|
|
|
875
876
|
# The #825 dedup: `gh issue view --json comments` returns a GraphQL
|
|
876
877
|
# node id, but the PATCH route requires the REST id. The comment URL
|
|
877
878
|
# already carries that id, so do not make a second API request.
|
|
878
|
-
|
|
879
|
-
if
|
|
880
|
-
rest_comment_id = comment_id
|
|
881
|
-
else:
|
|
882
|
-
url = reported_failure.get("url")
|
|
883
|
-
match = (
|
|
884
|
-
re.search(r"#issuecomment-(\d+)$", url)
|
|
885
|
-
if isinstance(url, str) else None
|
|
886
|
-
)
|
|
887
|
-
rest_comment_id = int(match.group(1)) if match is not None else None
|
|
888
|
-
if rest_comment_id is None:
|
|
879
|
+
comment_rest_id = rest_comment_id(reported_failure)
|
|
880
|
+
if comment_rest_id is None:
|
|
889
881
|
# A malformed or missing URL is data loss in the dedup metadata,
|
|
890
882
|
# not a delivery failure. Preserve the pre-#825 behavior so the
|
|
891
883
|
# current failure is still recorded and the tick continues.
|
|
@@ -907,7 +899,7 @@ def report_delivery_failure(
|
|
|
907
899
|
post_new_comment = False
|
|
908
900
|
try:
|
|
909
901
|
update_issue_comment(
|
|
910
|
-
|
|
902
|
+
comment_rest_id, repo=source_repo,
|
|
911
903
|
# Apply the increment to the newly assembled body, not the
|
|
912
904
|
# stale first report. This preserves scene/retry fields
|
|
913
905
|
# added by this attempt while retaining the dedup counter.
|