agentic-project-kit 1.0.1__py3-none-any.whl
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.
- agentic_project_kit/__init__.py +1 -0
- agentic_project_kit/absolute_path_portability_audit.py +241 -0
- agentic_project_kit/access_level_policy.py +14 -0
- agentic_project_kit/access_levels.py +22 -0
- agentic_project_kit/action_registry.py +195 -0
- agentic_project_kit/action_specs.py +286 -0
- agentic_project_kit/agent_command_runner.py +516 -0
- agentic_project_kit/audit_test_matrix.py +36 -0
- agentic_project_kit/audit_test_matrix_cli.py +43 -0
- agentic_project_kit/chat_bootloader.py +257 -0
- agentic_project_kit/chat_entrypoint_contract.py +297 -0
- agentic_project_kit/checks.py +511 -0
- agentic_project_kit/ci_readiness.py +211 -0
- agentic_project_kit/cli.py +147 -0
- agentic_project_kit/cli_commands/__init__.py +1 -0
- agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
- agentic_project_kit/cli_commands/actions.py +21 -0
- agentic_project_kit/cli_commands/artifact_gc.py +240 -0
- agentic_project_kit/cli_commands/boot.py +55 -0
- agentic_project_kit/cli_commands/chat.py +34 -0
- agentic_project_kit/cli_commands/checks.py +305 -0
- agentic_project_kit/cli_commands/cockpit.py +103 -0
- agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
- agentic_project_kit/cli_commands/commands.py +129 -0
- agentic_project_kit/cli_commands/dev.py +23 -0
- agentic_project_kit/cli_commands/direction.py +92 -0
- agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
- agentic_project_kit/cli_commands/doc_registry.py +126 -0
- agentic_project_kit/cli_commands/docs.py +223 -0
- agentic_project_kit/cli_commands/dpa.py +1338 -0
- agentic_project_kit/cli_commands/evidence.py +251 -0
- agentic_project_kit/cli_commands/github.py +34 -0
- agentic_project_kit/cli_commands/governance.py +14 -0
- agentic_project_kit/cli_commands/gui.py +23 -0
- agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
- agentic_project_kit/cli_commands/handoff.py +183 -0
- agentic_project_kit/cli_commands/human_workflows.py +430 -0
- agentic_project_kit/cli_commands/init.py +132 -0
- agentic_project_kit/cli_commands/instruction.py +75 -0
- agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
- agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
- agentic_project_kit/cli_commands/pass_already_done.py +66 -0
- agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
- agentic_project_kit/cli_commands/patch_preflight.py +37 -0
- agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
- agentic_project_kit/cli_commands/patterns.py +40 -0
- agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
- agentic_project_kit/cli_commands/pr.py +133 -0
- agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
- agentic_project_kit/cli_commands/profiles.py +26 -0
- agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
- agentic_project_kit/cli_commands/project_direction.py +25 -0
- agentic_project_kit/cli_commands/release.py +392 -0
- agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
- agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
- agentic_project_kit/cli_commands/remote_next.py +57 -0
- agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
- agentic_project_kit/cli_commands/rule_registry.py +129 -0
- agentic_project_kit/cli_commands/rules.py +222 -0
- agentic_project_kit/cli_commands/scaffold.py +46 -0
- agentic_project_kit/cli_commands/slice.py +21 -0
- agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
- agentic_project_kit/cli_commands/state.py +51 -0
- agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
- agentic_project_kit/cli_commands/todo.py +53 -0
- agentic_project_kit/cli_commands/transfer.py +19 -0
- agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
- agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
- agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
- agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
- agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
- agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
- agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
- agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
- agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
- agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
- agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
- agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
- agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
- agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
- agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
- agentic_project_kit/cli_commands/transfer_shared.py +274 -0
- agentic_project_kit/cli_commands/validation.py +200 -0
- agentic_project_kit/cli_commands/version.py +27 -0
- agentic_project_kit/cli_commands/work_orders.py +219 -0
- agentic_project_kit/cli_commands/workflow.py +471 -0
- agentic_project_kit/cli_commands/workflow_guard.py +26 -0
- agentic_project_kit/cli_commands/workspace.py +252 -0
- agentic_project_kit/cockpit.py +608 -0
- agentic_project_kit/cockpit_readiness.py +67 -0
- agentic_project_kit/command_authority_audit.py +192 -0
- agentic_project_kit/command_inbox_check.py +103 -0
- agentic_project_kit/command_manifest.py +591 -0
- agentic_project_kit/command_queue_contract.py +161 -0
- agentic_project_kit/command_selector.py +245 -0
- agentic_project_kit/command_taxonomy.py +264 -0
- agentic_project_kit/commit_guard.py +67 -0
- agentic_project_kit/communication_artifact_gc.py +495 -0
- agentic_project_kit/communication_rule_context.py +437 -0
- agentic_project_kit/communication_state.py +115 -0
- agentic_project_kit/composed_wrapper_invariants.py +97 -0
- agentic_project_kit/composite_short_circuit.py +140 -0
- agentic_project_kit/contract.py +251 -0
- agentic_project_kit/control_state.py +83 -0
- agentic_project_kit/doc_currency_audit.py +536 -0
- agentic_project_kit/doc_lifecycle.py +1179 -0
- agentic_project_kit/doc_lifecycle_signals.py +446 -0
- agentic_project_kit/doc_lifecycle_sweep.py +639 -0
- agentic_project_kit/doc_mesh.py +491 -0
- agentic_project_kit/doctor.py +379 -0
- agentic_project_kit/documentation_registry.py +1122 -0
- agentic_project_kit/documentation_system_audit.py +310 -0
- agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
- agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
- agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
- agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
- agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
- agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
- agentic_project_kit/dpa_final_closeout.py +627 -0
- agentic_project_kit/dpa_fixture_evidence.py +680 -0
- agentic_project_kit/dpa_maintainer_record_check.py +711 -0
- agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
- agentic_project_kit/dpa_probe_002_readiness.py +509 -0
- agentic_project_kit/dpa_probe_003_readiness.py +417 -0
- agentic_project_kit/dpa_probe_004_readiness.py +437 -0
- agentic_project_kit/dpa_readiness.py +505 -0
- agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
- agentic_project_kit/dpa_registry_contracts.py +312 -0
- agentic_project_kit/dpa_renderer_readiness.py +407 -0
- agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
- agentic_project_kit/dpa_stable_readiness.py +743 -0
- agentic_project_kit/dpa_successor_projection.py +168 -0
- agentic_project_kit/dpa_workspace_init_projection.py +93 -0
- agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
- agentic_project_kit/entrypoint_slice_runner.py +153 -0
- agentic_project_kit/evidence_clean.py +152 -0
- agentic_project_kit/evidence_commit_paths.py +214 -0
- agentic_project_kit/evidence_finalize_log.py +204 -0
- agentic_project_kit/evidence_guard.py +114 -0
- agentic_project_kit/evidence_inspector.py +507 -0
- agentic_project_kit/evidence_state_contract.py +99 -0
- agentic_project_kit/execution_mode_state.py +148 -0
- agentic_project_kit/final_summary_contract.py +85 -0
- agentic_project_kit/finalize_guard.py +139 -0
- agentic_project_kit/gatekeeper_core.py +232 -0
- agentic_project_kit/github.py +25 -0
- agentic_project_kit/governance.py +59 -0
- agentic_project_kit/gui_action_execution.py +105 -0
- agentic_project_kit/gui_action_renderer.py +81 -0
- agentic_project_kit/gui_action_run_intent.py +98 -0
- agentic_project_kit/gui_action_views.py +195 -0
- agentic_project_kit/gui_activity_log.py +76 -0
- agentic_project_kit/gui_button_catalog.py +605 -0
- agentic_project_kit/gui_cockpit.py +365 -0
- agentic_project_kit/gui_cockpit_actions.py +695 -0
- agentic_project_kit/gui_cockpit_common.py +172 -0
- agentic_project_kit/gui_cockpit_header.py +501 -0
- agentic_project_kit/gui_cockpit_sidebar.py +387 -0
- agentic_project_kit/gui_cockpit_task.py +748 -0
- agentic_project_kit/gui_command_projection.py +355 -0
- agentic_project_kit/gui_communication_modes.py +130 -0
- agentic_project_kit/gui_dry_run.py +219 -0
- agentic_project_kit/gui_gatekeeper_status.py +259 -0
- agentic_project_kit/gui_layout_plan.py +148 -0
- agentic_project_kit/gui_open_folder.py +18 -0
- agentic_project_kit/gui_output_status_panel.py +61 -0
- agentic_project_kit/gui_panel_state.py +47 -0
- agentic_project_kit/gui_presenter.py +62 -0
- agentic_project_kit/gui_readiness_gate.py +206 -0
- agentic_project_kit/gui_release_flow.py +59 -0
- agentic_project_kit/gui_task_editor.py +1077 -0
- agentic_project_kit/gui_tk_widgets.py +177 -0
- agentic_project_kit/gui_tkinter_renderer.py +87 -0
- agentic_project_kit/gui_tkinter_shell.py +1176 -0
- agentic_project_kit/gui_transfer_contract.py +28 -0
- agentic_project_kit/gui_viewmodel.py +698 -0
- agentic_project_kit/gui_window_guard.py +46 -0
- agentic_project_kit/handoff_freshness.py +390 -0
- agentic_project_kit/handoff_prompt.py +180 -0
- agentic_project_kit/handoff_state.py +254 -0
- agentic_project_kit/instruction_lint.py +455 -0
- agentic_project_kit/llm_context_carriers.py +66 -0
- agentic_project_kit/llm_execution_context.py +285 -0
- agentic_project_kit/local_command_stack.py +133 -0
- agentic_project_kit/local_feature_gate.py +82 -0
- agentic_project_kit/local_garbage_collector.py +269 -0
- agentic_project_kit/models.py +18 -0
- agentic_project_kit/mutation_lock_audit.py +421 -0
- agentic_project_kit/next_turn_evidence.py +253 -0
- agentic_project_kit/next_turn_merge_if_green.py +459 -0
- agentic_project_kit/next_turn_pr_status.py +255 -0
- agentic_project_kit/next_turn_result.py +183 -0
- agentic_project_kit/next_turn_runner.py +159 -0
- agentic_project_kit/next_turn_slot.py +107 -0
- agentic_project_kit/next_turn_status.py +274 -0
- agentic_project_kit/ns_legacy_reference_audit.py +258 -0
- agentic_project_kit/ns_slice_runner.py +16 -0
- agentic_project_kit/operational_handoff_projection.py +124 -0
- agentic_project_kit/output_contract.py +80 -0
- agentic_project_kit/output_repair.py +61 -0
- agentic_project_kit/pass_already_done.py +216 -0
- agentic_project_kit/patch_artifact_preflight.py +147 -0
- agentic_project_kit/patch_cycle_workflow.py +440 -0
- agentic_project_kit/patch_failure_discipline_audit.py +223 -0
- agentic_project_kit/patch_preflight.py +215 -0
- agentic_project_kit/path_literal_audit.py +612 -0
- agentic_project_kit/patterns.py +99 -0
- agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
- agentic_project_kit/portability_shell_gate.py +40 -0
- agentic_project_kit/post_merge_handoff_refresh.py +56 -0
- agentic_project_kit/post_release.py +252 -0
- agentic_project_kit/post_release_closeout.py +616 -0
- agentic_project_kit/pr_cleanup.py +172 -0
- agentic_project_kit/pr_closeout.py +50 -0
- agentic_project_kit/pr_closeout_complete.py +344 -0
- agentic_project_kit/pr_create_or_skip.py +71 -0
- agentic_project_kit/pr_hygiene.py +210 -0
- agentic_project_kit/program_redundancy_audit.py +251 -0
- agentic_project_kit/project_direction.py +840 -0
- agentic_project_kit/protected_change_planner.py +194 -0
- agentic_project_kit/release.py +401 -0
- agentic_project_kit/release_doi_safety.py +24 -0
- agentic_project_kit/release_gate_core.py +174 -0
- agentic_project_kit/release_metadata_authority_gate.py +328 -0
- agentic_project_kit/release_metadata_prep.py +43 -0
- agentic_project_kit/release_notes.py +823 -0
- agentic_project_kit/release_prep_core.py +204 -0
- agentic_project_kit/release_prepare.py +369 -0
- agentic_project_kit/release_process_guardrails.py +247 -0
- agentic_project_kit/release_publish_core.py +114 -0
- agentic_project_kit/release_publish_orchestration.py +469 -0
- agentic_project_kit/release_state.py +552 -0
- agentic_project_kit/release_verify_core.py +175 -0
- agentic_project_kit/remote_branch_hygiene.py +223 -0
- agentic_project_kit/remote_next.py +89 -0
- agentic_project_kit/remote_next_closeout.py +101 -0
- agentic_project_kit/remote_next_order_contract.py +391 -0
- agentic_project_kit/removed_ns_commands.py +458 -0
- agentic_project_kit/removed_source_audit.py +225 -0
- agentic_project_kit/rendering.py +13 -0
- agentic_project_kit/repair_report.py +79 -0
- agentic_project_kit/repo_identity.py +59 -0
- agentic_project_kit/result_report_classifier.py +57 -0
- agentic_project_kit/rule_ack.py +105 -0
- agentic_project_kit/rule_preservation.py +58 -0
- agentic_project_kit/rule_refresh.py +174 -0
- agentic_project_kit/rule_registry_registration.py +322 -0
- agentic_project_kit/rule_registry_report.py +206 -0
- agentic_project_kit/rule_registry_validator.py +704 -0
- agentic_project_kit/rule_snapshot.py +98 -0
- agentic_project_kit/rule_source_validator.py +133 -0
- agentic_project_kit/run_summary.py +68 -0
- agentic_project_kit/run_summary_renderer.py +238 -0
- agentic_project_kit/runtime_validator.py +68 -0
- agentic_project_kit/safe_file_replace.py +35 -0
- agentic_project_kit/safe_patch.py +47 -0
- agentic_project_kit/safe_push.py +218 -0
- agentic_project_kit/safe_remove_diagnostic.py +51 -0
- agentic_project_kit/scaffold.py +69 -0
- agentic_project_kit/site_claims.py +444 -0
- agentic_project_kit/site_generator.py +1140 -0
- agentic_project_kit/slice_gate.py +225 -0
- agentic_project_kit/slice_runner.py +75 -0
- agentic_project_kit/standard_gates_audit_suite.py +167 -0
- agentic_project_kit/state_freshness.py +135 -0
- agentic_project_kit/status_current_state_audit.py +762 -0
- agentic_project_kit/successor_handoff_package.py +1763 -0
- agentic_project_kit/templates.py +292 -0
- agentic_project_kit/terminal_block_guard.py +166 -0
- agentic_project_kit/terminal_logging.py +275 -0
- agentic_project_kit/todo.py +97 -0
- agentic_project_kit/transfer_closeout.py +136 -0
- agentic_project_kit/transfer_continue.py +215 -0
- agentic_project_kit/transfer_local_runner.py +101 -0
- agentic_project_kit/transfer_operation_monitor.py +236 -0
- agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
- agentic_project_kit/transfer_post_merge_settle.py +383 -0
- agentic_project_kit/transfer_pr_actions.py +164 -0
- agentic_project_kit/transfer_remote_next.py +959 -0
- agentic_project_kit/transfer_repo_actions.py +2253 -0
- agentic_project_kit/transfer_runner.py +347 -0
- agentic_project_kit/transfer_safety_context.py +291 -0
- agentic_project_kit/transfer_state.py +483 -0
- agentic_project_kit/transfer_uplink.py +471 -0
- agentic_project_kit/transfer_workflow_next.py +148 -0
- agentic_project_kit/typed_work_order_evidence.py +58 -0
- agentic_project_kit/typed_work_order_queue.py +166 -0
- agentic_project_kit/typed_work_order_runner.py +168 -0
- agentic_project_kit/work_cycle.py +317 -0
- agentic_project_kit/work_discard_changes.py +129 -0
- agentic_project_kit/work_order_patch.py +199 -0
- agentic_project_kit/work_order_runner.py +120 -0
- agentic_project_kit/work_order_uploader.py +220 -0
- agentic_project_kit/work_order_validator.py +185 -0
- agentic_project_kit/work_orders.py +246 -0
- agentic_project_kit/workflow_guard.py +262 -0
- agentic_project_kit/workflow_summary_runner.py +124 -0
- agentic_project_kit/workspace.py +665 -0
- agentic_project_kit/workspace_adopt.py +471 -0
- agentic_project_kit/workspace_dpa_intake.py +406 -0
- agentic_project_kit/workspace_init.py +487 -0
- agentic_project_kit/workspace_lock.py +129 -0
- agentic_project_kit/workspace_remove.py +381 -0
- agentic_project_kit/workspace_upgrade.py +324 -0
- agentic_project_kit/wrapper_live_status.py +104 -0
- agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
- agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
- agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
- agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
- agentic_project_kit-1.0.1.dist-info/licenses/LICENSE +10 -0
|
@@ -0,0 +1,2253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass, asdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from agentic_project_kit.chat_entrypoint_contract import ensure_command_reference_in_prompt
|
|
13
|
+
from agentic_project_kit.dpa_current_handoff_lifecycle import (
|
|
14
|
+
DEFAULT_READINESS_PATH,
|
|
15
|
+
evaluate_current_handoff_lifecycle,
|
|
16
|
+
render_current_handoff_lifecycle_result,
|
|
17
|
+
)
|
|
18
|
+
from agentic_project_kit.safe_push import SafePushResult
|
|
19
|
+
from agentic_project_kit.safe_push import safe_push
|
|
20
|
+
from agentic_project_kit.transfer_operation_monitor import MonitorDecision
|
|
21
|
+
from agentic_project_kit.transfer_operation_monitor import guard_branch
|
|
22
|
+
from agentic_project_kit.transfer_operation_monitor import guard_pr_create
|
|
23
|
+
from agentic_project_kit.workspace import KitConfig, Workspace, load_workspace
|
|
24
|
+
from agentic_project_kit.workspace_lock import acquire_workspace_lock, workspace_mutation_lock
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RepoActionResult:
|
|
29
|
+
action: str
|
|
30
|
+
result_status: str
|
|
31
|
+
returncode: int
|
|
32
|
+
command: list[str]
|
|
33
|
+
stdout: str
|
|
34
|
+
stderr: str
|
|
35
|
+
next_action: str
|
|
36
|
+
|
|
37
|
+
def as_json_data(self) -> dict:
|
|
38
|
+
return asdict(self)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class SuccessorPackageFreshnessCheck:
|
|
43
|
+
findings: tuple[str, ...]
|
|
44
|
+
notes: tuple[str, ...]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_LEGACY_WORKSPACE = Workspace(root=Path("."), config=KitConfig())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _workspace_path_text(ws: Workspace, path: Path) -> str:
|
|
51
|
+
try:
|
|
52
|
+
return path.relative_to(ws.root).as_posix()
|
|
53
|
+
except ValueError:
|
|
54
|
+
return path.as_posix()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _admin_refresh_paths(ws: Workspace) -> tuple[str, ...]:
|
|
58
|
+
return (
|
|
59
|
+
_workspace_path_text(ws, ws.dpa_current_handoff_acceptance_state_path()),
|
|
60
|
+
_workspace_path_text(ws, ws.handoff_state_path()),
|
|
61
|
+
_workspace_path_text(ws, ws.operational_handoff_state_path()),
|
|
62
|
+
_workspace_path_text(ws, ws.status_path()),
|
|
63
|
+
_workspace_path_text(ws, ws.handoff_file("CURRENT_HANDOFF.md")),
|
|
64
|
+
_workspace_path_text(ws, ws.handoff_file("NEXT_CHAT_BOOTSTRAP.md")),
|
|
65
|
+
_workspace_path_text(ws, ws.handoff_file("START_NEW_CHAT_PROMPT.md")),
|
|
66
|
+
_workspace_path_text(ws, ws.package_file("execution_contract.json")),
|
|
67
|
+
_workspace_path_text(ws, ws.package_file("source_manifest.json")),
|
|
68
|
+
_workspace_path_text(ws, ws.package_file("successor_context.yaml")),
|
|
69
|
+
_workspace_path_text(ws, ws.package_file("successor_prompt.md")),
|
|
70
|
+
_workspace_path_text(ws, ws.package_file("validation_report.json")),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _successor_package_refresh_paths(ws: Workspace) -> tuple[str, ...]:
|
|
75
|
+
return (
|
|
76
|
+
_workspace_path_text(ws, ws.handoff_file("NEXT_CHAT_BOOTSTRAP.md")),
|
|
77
|
+
_workspace_path_text(ws, ws.handoff_file("START_NEW_CHAT_PROMPT.md")),
|
|
78
|
+
_workspace_path_text(ws, ws.handoff_file("CLOSEOUT_BEFORE_CHAT_SWITCH_PROMPT.md")),
|
|
79
|
+
_workspace_path_text(ws, ws.package_file("execution_contract.json")),
|
|
80
|
+
_workspace_path_text(ws, ws.package_file("source_manifest.json")),
|
|
81
|
+
_workspace_path_text(ws, ws.package_file("successor_context.yaml")),
|
|
82
|
+
_workspace_path_text(ws, ws.package_file("successor_prompt.md")),
|
|
83
|
+
_workspace_path_text(ws, ws.package_file("validation_report.json")),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _agentic_kit_command() -> str:
|
|
88
|
+
candidate = Path(sys.executable).parent / "agentic-kit"
|
|
89
|
+
if candidate.exists():
|
|
90
|
+
return str(candidate)
|
|
91
|
+
return "agentic-kit"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _safe_push_runner(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
95
|
+
return _run(command, cwd=cwd)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _safe_push_current_branch(
|
|
99
|
+
*,
|
|
100
|
+
branch: str,
|
|
101
|
+
purpose: str,
|
|
102
|
+
set_upstream: bool = True,
|
|
103
|
+
) -> SafePushResult:
|
|
104
|
+
return safe_push(
|
|
105
|
+
Path("."),
|
|
106
|
+
target_branch=branch,
|
|
107
|
+
purpose=purpose,
|
|
108
|
+
set_upstream=set_upstream,
|
|
109
|
+
run=_safe_push_runner,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _full_sha_guard(action: str, expected_head_sha: str) -> RepoActionResult | None:
|
|
115
|
+
if len(expected_head_sha) != 40:
|
|
116
|
+
completed = subprocess.CompletedProcess(
|
|
117
|
+
[action, "--expected-head-sha"],
|
|
118
|
+
2,
|
|
119
|
+
"",
|
|
120
|
+
"Expected full 40-character head SHA. Short SHAs are refused for guarded PR actions.\n",
|
|
121
|
+
)
|
|
122
|
+
return _result(action, [action, "--expected-head-sha"], completed, "Re-run with the full PR head SHA.")
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _resolve_pr_head_sha(pr_number: int, *, action: str) -> tuple[str, RepoActionResult | None]:
|
|
128
|
+
command = ["gh", "pr", "view", str(pr_number), "--json", "headRefOid", "--jq", ".headRefOid"]
|
|
129
|
+
completed = _run(command)
|
|
130
|
+
if completed.returncode != 0:
|
|
131
|
+
return "", _result(action, command, completed, "Inspect PR head SHA lookup failure before continuing.")
|
|
132
|
+
|
|
133
|
+
head_sha = completed.stdout.strip()
|
|
134
|
+
if len(head_sha) != 40:
|
|
135
|
+
bad = subprocess.CompletedProcess(
|
|
136
|
+
command,
|
|
137
|
+
2,
|
|
138
|
+
completed.stdout,
|
|
139
|
+
f"Resolved PR head SHA is not a full 40-character SHA: {head_sha}\n",
|
|
140
|
+
)
|
|
141
|
+
return "", _result(action, command, bad, "Inspect PR head SHA lookup failure before continuing.")
|
|
142
|
+
|
|
143
|
+
return head_sha, None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _pr_state_lookup(pr_number: int, *, action: str) -> tuple[dict, RepoActionResult | None]:
|
|
147
|
+
command = ["gh", "pr", "view", str(pr_number), "--json", "number,state,mergedAt,mergeCommit,headRefOid,url,title"]
|
|
148
|
+
completed = _run(command)
|
|
149
|
+
if completed.returncode != 0:
|
|
150
|
+
return {}, _result(action, command, completed, "STATE=BLOCKED; NEXT=diagnose_pr_state_lookup")
|
|
151
|
+
try:
|
|
152
|
+
data = json.loads(completed.stdout or "{}")
|
|
153
|
+
except json.JSONDecodeError as exc:
|
|
154
|
+
bad = subprocess.CompletedProcess(
|
|
155
|
+
command,
|
|
156
|
+
2,
|
|
157
|
+
completed.stdout,
|
|
158
|
+
f"Could not parse PR state lookup JSON: {exc}\n",
|
|
159
|
+
)
|
|
160
|
+
return {}, _result(action, command, bad, "STATE=BLOCKED; NEXT=diagnose_pr_state_lookup")
|
|
161
|
+
if not isinstance(data, dict):
|
|
162
|
+
bad = subprocess.CompletedProcess(command, 2, completed.stdout, "PR state lookup did not return an object.\n")
|
|
163
|
+
return {}, _result(action, command, bad, "STATE=BLOCKED; NEXT=diagnose_pr_state_lookup")
|
|
164
|
+
return data, None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _already_merged_pr_result(pr_number: int, *, action: str, command: list[str]) -> RepoActionResult | None:
|
|
168
|
+
data, failure = _pr_state_lookup(pr_number, action=action)
|
|
169
|
+
if failure is not None:
|
|
170
|
+
return None
|
|
171
|
+
if data.get("mergedAt") or str(data.get("state") or "").upper() == "MERGED":
|
|
172
|
+
out = (
|
|
173
|
+
"IDEMPOTENT_PR_RECOVERY\n"
|
|
174
|
+
f"STATE=ALREADY_MERGED\n"
|
|
175
|
+
f"PR={pr_number}\n"
|
|
176
|
+
f"URL={data.get('url', '')}\n"
|
|
177
|
+
f"MERGED_AT={data.get('mergedAt', '')}\n"
|
|
178
|
+
"RESULT=PASS\n"
|
|
179
|
+
)
|
|
180
|
+
completed = subprocess.CompletedProcess(command, 0, out, "")
|
|
181
|
+
return _result(action, command, completed, "STATE=ALREADY_MERGED; NEXT=run_post_merge_check_or_handoff_refresh")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _existing_admin_refresh_pr(refresh_branch: str, *, action: str = "admin-refresh-pr") -> RepoActionResult | None:
|
|
186
|
+
command = [
|
|
187
|
+
"gh",
|
|
188
|
+
"pr",
|
|
189
|
+
"list",
|
|
190
|
+
"--head",
|
|
191
|
+
refresh_branch,
|
|
192
|
+
"--state",
|
|
193
|
+
"open",
|
|
194
|
+
"--json",
|
|
195
|
+
"number,url,headRefName,headRefOid,state,title",
|
|
196
|
+
]
|
|
197
|
+
completed = _run(command)
|
|
198
|
+
if completed.returncode != 0:
|
|
199
|
+
return _result(action, command, completed, "Inspect existing admin refresh PR lookup failure.")
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
prs = json.loads(completed.stdout or "[]")
|
|
203
|
+
except json.JSONDecodeError as exc:
|
|
204
|
+
bad = subprocess.CompletedProcess(
|
|
205
|
+
command,
|
|
206
|
+
2,
|
|
207
|
+
completed.stdout,
|
|
208
|
+
f"Could not parse existing admin refresh PR lookup JSON: {exc}\n",
|
|
209
|
+
)
|
|
210
|
+
return _result(action, command, bad, "Inspect existing admin refresh PR lookup output.")
|
|
211
|
+
|
|
212
|
+
if not isinstance(prs, list):
|
|
213
|
+
bad = subprocess.CompletedProcess(
|
|
214
|
+
command,
|
|
215
|
+
2,
|
|
216
|
+
completed.stdout,
|
|
217
|
+
"Existing admin refresh PR lookup did not return a JSON list.\n",
|
|
218
|
+
)
|
|
219
|
+
return _result(action, command, bad, "Inspect existing admin refresh PR lookup output.")
|
|
220
|
+
|
|
221
|
+
if len(prs) > 1:
|
|
222
|
+
bad = subprocess.CompletedProcess(
|
|
223
|
+
command,
|
|
224
|
+
2,
|
|
225
|
+
completed.stdout,
|
|
226
|
+
f"Multiple open admin refresh PRs found for branch {refresh_branch}.\n",
|
|
227
|
+
)
|
|
228
|
+
return _result(action, command, bad, "Resolve duplicate admin refresh PRs before continuing.")
|
|
229
|
+
|
|
230
|
+
if not prs:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
pr = prs[0]
|
|
234
|
+
number = pr.get("number", "")
|
|
235
|
+
url = pr.get("url", "")
|
|
236
|
+
head_ref_oid = pr.get("headRefOid", "")
|
|
237
|
+
out = (
|
|
238
|
+
"ADMIN_REFRESH_EXISTING_BRANCH_RECOVERY\n"
|
|
239
|
+
f"refresh_branch={refresh_branch}\n"
|
|
240
|
+
f"existing_pr={number}\n"
|
|
241
|
+
f"head_ref_oid={head_ref_oid}\n"
|
|
242
|
+
f"url={url}\n"
|
|
243
|
+
"result=PASS\n"
|
|
244
|
+
)
|
|
245
|
+
ok = subprocess.CompletedProcess(command, 0, out, "")
|
|
246
|
+
return _result(action, command, ok, "Run transfer pr-status on the existing admin refresh PR.")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _existing_successor_package_refresh_pr(refresh_branch: str) -> RepoActionResult | None:
|
|
250
|
+
command = [
|
|
251
|
+
"gh",
|
|
252
|
+
"pr",
|
|
253
|
+
"list",
|
|
254
|
+
"--head",
|
|
255
|
+
refresh_branch,
|
|
256
|
+
"--state",
|
|
257
|
+
"open",
|
|
258
|
+
"--json",
|
|
259
|
+
"number,url,headRefName,headRefOid,state,title",
|
|
260
|
+
]
|
|
261
|
+
completed = _run(command)
|
|
262
|
+
if completed.returncode != 0:
|
|
263
|
+
return _result(
|
|
264
|
+
"successor-package-refresh-pr",
|
|
265
|
+
command,
|
|
266
|
+
completed,
|
|
267
|
+
"Inspect existing successor package refresh PR lookup failure.",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
prs = json.loads(completed.stdout or "[]")
|
|
272
|
+
except json.JSONDecodeError as exc:
|
|
273
|
+
bad = subprocess.CompletedProcess(
|
|
274
|
+
command,
|
|
275
|
+
2,
|
|
276
|
+
completed.stdout,
|
|
277
|
+
f"Could not parse existing successor package refresh PR lookup JSON: {exc}\n",
|
|
278
|
+
)
|
|
279
|
+
return _result(
|
|
280
|
+
"successor-package-refresh-pr",
|
|
281
|
+
command,
|
|
282
|
+
bad,
|
|
283
|
+
"Inspect existing successor package refresh PR lookup output.",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if not isinstance(prs, list):
|
|
287
|
+
bad = subprocess.CompletedProcess(
|
|
288
|
+
command,
|
|
289
|
+
2,
|
|
290
|
+
completed.stdout,
|
|
291
|
+
"Existing successor package refresh PR lookup did not return a JSON list.\n",
|
|
292
|
+
)
|
|
293
|
+
return _result(
|
|
294
|
+
"successor-package-refresh-pr",
|
|
295
|
+
command,
|
|
296
|
+
bad,
|
|
297
|
+
"Inspect existing successor package refresh PR lookup output.",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if len(prs) > 1:
|
|
301
|
+
bad = subprocess.CompletedProcess(
|
|
302
|
+
command,
|
|
303
|
+
2,
|
|
304
|
+
completed.stdout,
|
|
305
|
+
f"Multiple open successor package refresh PRs found for branch {refresh_branch}.\n",
|
|
306
|
+
)
|
|
307
|
+
return _result(
|
|
308
|
+
"successor-package-refresh-pr",
|
|
309
|
+
command,
|
|
310
|
+
bad,
|
|
311
|
+
"Resolve duplicate successor package refresh PRs before continuing.",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if not prs:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
pr = prs[0]
|
|
318
|
+
out = (
|
|
319
|
+
"SUCCESSOR_PACKAGE_REFRESH_EXISTING_BRANCH_RECOVERY\n"
|
|
320
|
+
f"refresh_branch={refresh_branch}\n"
|
|
321
|
+
f"existing_pr={pr.get('number', '')}\n"
|
|
322
|
+
f"head_ref_oid={pr.get('headRefOid', '')}\n"
|
|
323
|
+
f"url={pr.get('url', '')}\n"
|
|
324
|
+
"result=PASS\n"
|
|
325
|
+
)
|
|
326
|
+
ok = subprocess.CompletedProcess(command, 0, out, "")
|
|
327
|
+
return _result(
|
|
328
|
+
"successor-package-refresh-pr",
|
|
329
|
+
command,
|
|
330
|
+
ok,
|
|
331
|
+
"Run transfer pr-status on the existing successor package refresh PR.",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _run(command: list[str], *, cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
|
|
336
|
+
return subprocess.run(
|
|
337
|
+
command,
|
|
338
|
+
cwd=cwd,
|
|
339
|
+
check=False,
|
|
340
|
+
text=True,
|
|
341
|
+
stdout=subprocess.PIPE,
|
|
342
|
+
stderr=subprocess.PIPE,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _result(action: str, command: list[str], completed: subprocess.CompletedProcess[str], next_action: str) -> RepoActionResult:
|
|
347
|
+
status = "PASS" if completed.returncode == 0 else "FAIL"
|
|
348
|
+
return RepoActionResult(
|
|
349
|
+
action=action,
|
|
350
|
+
result_status=status,
|
|
351
|
+
returncode=completed.returncode,
|
|
352
|
+
command=command,
|
|
353
|
+
stdout=completed.stdout,
|
|
354
|
+
stderr=completed.stderr,
|
|
355
|
+
next_action=next_action,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _is_refresh_only_successor_package_head(generated_head: str, current_head: str, ws: Workspace) -> bool:
|
|
361
|
+
"""Return true when commits after generated_head only touch refresh artifacts.
|
|
362
|
+
|
|
363
|
+
Successor package files are generated before they are committed. Therefore a
|
|
364
|
+
package-refresh commit naturally has validation_report.generated_head set to
|
|
365
|
+
the parent commit rather than to the package-refresh commit itself. Treat
|
|
366
|
+
that state as fresh only when the range since generated_head contains
|
|
367
|
+
generated refresh artifacts and no product/source/governance code changes.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
if not generated_head or not current_head or generated_head == current_head:
|
|
371
|
+
return generated_head == current_head
|
|
372
|
+
|
|
373
|
+
root = ws.root
|
|
374
|
+
merge_base = _run(["git", "merge-base", "--is-ancestor", generated_head, current_head], cwd=root)
|
|
375
|
+
if merge_base.returncode != 0:
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
diff = _run(["git", "diff", "--name-only", f"{generated_head}..{current_head}"], cwd=root)
|
|
379
|
+
if diff.returncode != 0:
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
changed = {line.strip() for line in diff.stdout.splitlines() if line.strip()}
|
|
383
|
+
if not changed:
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
fixed_refresh_paths = set(_admin_refresh_paths(ws))
|
|
387
|
+
fixed_refresh_paths.add(_workspace_path_text(ws, ws.handoff_file("CLOSEOUT_BEFORE_CHAT_SWITCH_PROMPT.md")))
|
|
388
|
+
fixed_refresh_paths.add(_workspace_path_text(ws, ws.dpa_current_handoff_acceptance_state_path()))
|
|
389
|
+
package_prefix = _workspace_path_text(ws, ws.handoff_packages_latest()) + "/"
|
|
390
|
+
|
|
391
|
+
def allowed(path: str) -> bool:
|
|
392
|
+
return (
|
|
393
|
+
path in fixed_refresh_paths
|
|
394
|
+
or path.startswith(package_prefix)
|
|
395
|
+
or path.startswith(ws.post_pr_successor_chat_handoff_prefix())
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return all(allowed(path) for path in changed)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _successor_package_freshness_check(repo_root: Path | None = None) -> SuccessorPackageFreshnessCheck:
|
|
403
|
+
"""Return deterministic successor handoff package freshness state."""
|
|
404
|
+
|
|
405
|
+
root = repo_root or Path.cwd()
|
|
406
|
+
ws = load_workspace(root)
|
|
407
|
+
findings: list[str] = []
|
|
408
|
+
notes: list[str] = []
|
|
409
|
+
|
|
410
|
+
def read(path: Path) -> str:
|
|
411
|
+
rel = _workspace_path_text(ws, path)
|
|
412
|
+
if not path.exists():
|
|
413
|
+
findings.append(f"missing {rel}")
|
|
414
|
+
return ""
|
|
415
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
416
|
+
|
|
417
|
+
package_root = ws.handoff_packages_latest()
|
|
418
|
+
canonical_start_prompt = ws.handoff_file("START_NEW_CHAT_PROMPT.md")
|
|
419
|
+
project_markers = [
|
|
420
|
+
ws.agentic_root(),
|
|
421
|
+
ws.source_root(),
|
|
422
|
+
ws.pyproject_path(),
|
|
423
|
+
]
|
|
424
|
+
if not package_root.exists() and not canonical_start_prompt.exists() and not all(marker.exists() for marker in project_markers):
|
|
425
|
+
return SuccessorPackageFreshnessCheck(findings=(), notes=())
|
|
426
|
+
|
|
427
|
+
head = _run(["git", "rev-parse", "HEAD"]).stdout.strip()
|
|
428
|
+
|
|
429
|
+
validation_text = read(ws.package_file("validation_report.json"))
|
|
430
|
+
execution_text = read(ws.package_file("execution_contract.json"))
|
|
431
|
+
successor_prompt = read(ws.package_file("successor_prompt.md"))
|
|
432
|
+
start_prompt = read(ws.handoff_file("START_NEW_CHAT_PROMPT.md"))
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
validation = json.loads(validation_text) if validation_text else {}
|
|
436
|
+
except json.JSONDecodeError as exc:
|
|
437
|
+
findings.append(f"invalid validation_report.json: {exc}")
|
|
438
|
+
validation = {}
|
|
439
|
+
|
|
440
|
+
if validation.get("status") != "PASS":
|
|
441
|
+
findings.append("validation_report.json status is not PASS")
|
|
442
|
+
generated_head = str(validation.get("generated_head") or "")
|
|
443
|
+
if head and generated_head:
|
|
444
|
+
if generated_head == head:
|
|
445
|
+
notes.append("successor_package_head_status=exact")
|
|
446
|
+
elif _is_refresh_only_successor_package_head(generated_head, head, ws):
|
|
447
|
+
notes.extend(
|
|
448
|
+
[
|
|
449
|
+
"successor_package_head_status=refresh_only_descendant",
|
|
450
|
+
f"successor_package_generated_head={generated_head}",
|
|
451
|
+
f"successor_package_current_head={head}",
|
|
452
|
+
]
|
|
453
|
+
)
|
|
454
|
+
else:
|
|
455
|
+
findings.append("validation_report.json generated_head does not match HEAD or refresh-only ancestry")
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
execution_contract = json.loads(execution_text) if execution_text else {}
|
|
459
|
+
except json.JSONDecodeError as exc:
|
|
460
|
+
findings.append(f"invalid execution_contract.json: {exc}")
|
|
461
|
+
execution_contract = {}
|
|
462
|
+
|
|
463
|
+
if execution_contract.get("kind") != "successor_execution_contract":
|
|
464
|
+
findings.append("execution_contract.json kind is not successor_execution_contract")
|
|
465
|
+
|
|
466
|
+
rules = execution_contract.get("rules", [])
|
|
467
|
+
if not isinstance(rules, list):
|
|
468
|
+
rules = []
|
|
469
|
+
rule_ids = {rule.get("rule_id") for rule in rules if isinstance(rule, dict)}
|
|
470
|
+
required = {
|
|
471
|
+
"local-copy-paste-protocol",
|
|
472
|
+
"strict-start-decision",
|
|
473
|
+
"protected-file-preservation",
|
|
474
|
+
"bootstrap_acceptance_gate",
|
|
475
|
+
}
|
|
476
|
+
missing = sorted(required - rule_ids)
|
|
477
|
+
if missing:
|
|
478
|
+
findings.append("execution_contract.json missing rule IDs: " + ", ".join(missing))
|
|
479
|
+
|
|
480
|
+
combined = "\n".join([successor_prompt, start_prompt, execution_text])
|
|
481
|
+
for needle in (
|
|
482
|
+
"Zusätzliche Startbremse nach dem Bootstrap",
|
|
483
|
+
"RESULT=NEW_CHAT_BOOTSTRAP_DONE",
|
|
484
|
+
"Übergabe akzeptiert, keine Admin-Arbeit nötig",
|
|
485
|
+
):
|
|
486
|
+
if needle not in combined:
|
|
487
|
+
findings.append(f"successor handoff package missing bootstrap acceptance marker: {needle}")
|
|
488
|
+
|
|
489
|
+
if "\\n## Operational documentation refresh state" in combined or "\\n\\n## Operational documentation refresh state" in combined:
|
|
490
|
+
findings.append("successor handoff package contains literal newline artifacts")
|
|
491
|
+
|
|
492
|
+
return SuccessorPackageFreshnessCheck(findings=tuple(findings), notes=tuple(notes))
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _successor_package_freshness_findings(repo_root: Path | None = None) -> list[str]:
|
|
496
|
+
"""Return deterministic successor handoff package freshness findings."""
|
|
497
|
+
|
|
498
|
+
return list(_successor_package_freshness_check(repo_root).findings)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _successor_package_freshness_notes(repo_root: Path | None = None) -> list[str]:
|
|
502
|
+
"""Return machine-readable successor handoff package freshness evidence lines."""
|
|
503
|
+
|
|
504
|
+
return list(_successor_package_freshness_check(repo_root).notes)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _successor_package_freshness_summary() -> str:
|
|
508
|
+
findings = _successor_package_freshness_findings()
|
|
509
|
+
if not findings:
|
|
510
|
+
return "successor package fresh"
|
|
511
|
+
return "successor package stale: " + "; ".join(findings)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def final_signal(result: RepoActionResult) -> str:
|
|
515
|
+
return "d" if result.result_status == "PASS" and result.returncode == 0 else "f"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _summary_rule(label: str, *, end: bool = False) -> str:
|
|
519
|
+
side = " END SUMMARY " if end else f" {label} "
|
|
520
|
+
width = 80
|
|
521
|
+
remaining = max(width - len(side), 0)
|
|
522
|
+
left = remaining // 2
|
|
523
|
+
right = remaining - left
|
|
524
|
+
return "*" * left + side + "*" * right
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _summary_line(label: str, value: object, *, indent: int = 0) -> str:
|
|
528
|
+
prefix = " " * indent
|
|
529
|
+
text = "" if value is None else str(value)
|
|
530
|
+
return f"{prefix}{label}:".ljust(24) + text
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _compact_output_label(value: str, *, max_lines: int = 8) -> list[str]:
|
|
534
|
+
lines = [line for line in value.rstrip().splitlines() if line.strip()]
|
|
535
|
+
if not lines:
|
|
536
|
+
return ["none"]
|
|
537
|
+
if len(lines) <= max_lines:
|
|
538
|
+
return lines
|
|
539
|
+
return [*lines[:max_lines], f"... truncated {len(lines) - max_lines} more line(s); use --json for full output"]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def result_terminal(result: RepoActionResult) -> str:
|
|
543
|
+
signal = final_signal(result)
|
|
544
|
+
lines = [
|
|
545
|
+
_summary_rule("START SUMMARY"),
|
|
546
|
+
f"TRANSFER_{result.action.upper().replace('-', '_')}",
|
|
547
|
+
"",
|
|
548
|
+
_summary_line("STATE", result.result_status),
|
|
549
|
+
_summary_line("RETURNCODE", result.returncode),
|
|
550
|
+
"",
|
|
551
|
+
"COMMAND",
|
|
552
|
+
_summary_line("- ACTION", result.action),
|
|
553
|
+
_summary_line("- ARGV", " ".join(result.command)),
|
|
554
|
+
"",
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
stdout_lines = _compact_output_label(result.stdout)
|
|
558
|
+
stderr_lines = _compact_output_label(result.stderr)
|
|
559
|
+
if stdout_lines != ["none"]:
|
|
560
|
+
lines.append("STDOUT")
|
|
561
|
+
lines.extend(_summary_line("- LINE", item) for item in stdout_lines)
|
|
562
|
+
lines.append("")
|
|
563
|
+
if stderr_lines != ["none"]:
|
|
564
|
+
lines.append("STDERR")
|
|
565
|
+
lines.extend(_summary_line("- LINE", item) for item in stderr_lines)
|
|
566
|
+
lines.append("")
|
|
567
|
+
|
|
568
|
+
lines.extend(
|
|
569
|
+
[
|
|
570
|
+
_summary_line("NEXT", result.next_action),
|
|
571
|
+
_summary_line("CHAT_REPLY", f"{signal} | NEXT={result.next_action}"),
|
|
572
|
+
_summary_rule("END SUMMARY", end=True),
|
|
573
|
+
]
|
|
574
|
+
)
|
|
575
|
+
return "\n".join(lines)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _monitor_block_result(
|
|
579
|
+
*,
|
|
580
|
+
action: str,
|
|
581
|
+
command_kind: str,
|
|
582
|
+
required_branch: str,
|
|
583
|
+
monitor,
|
|
584
|
+
next_action: str,
|
|
585
|
+
) -> RepoActionResult:
|
|
586
|
+
completed = subprocess.CompletedProcess(
|
|
587
|
+
["transfer-monitor", command_kind, "--branch", required_branch],
|
|
588
|
+
2,
|
|
589
|
+
"",
|
|
590
|
+
(
|
|
591
|
+
f"Transfer operation monitor blocked {command_kind}: "
|
|
592
|
+
f"{monitor.reason}; actual_branch={monitor.actual_branch}; "
|
|
593
|
+
f"required_branch={monitor.required_branch}\n"
|
|
594
|
+
),
|
|
595
|
+
)
|
|
596
|
+
return _result(action, list(completed.args), completed, next_action)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _recover_known_volatile_branch_switch_block(
|
|
601
|
+
*,
|
|
602
|
+
command_kind: str,
|
|
603
|
+
required_branch: str,
|
|
604
|
+
monitor,
|
|
605
|
+
allow_main_mutation: bool,
|
|
606
|
+
auto_switch: bool = True,
|
|
607
|
+
):
|
|
608
|
+
if monitor.reason != "dirty_worktree_blocks_branch_switch":
|
|
609
|
+
return monitor
|
|
610
|
+
|
|
611
|
+
repair_command = [
|
|
612
|
+
_agentic_kit_command(),
|
|
613
|
+
"transfer",
|
|
614
|
+
"normalize-session",
|
|
615
|
+
"--repair-known-volatile",
|
|
616
|
+
]
|
|
617
|
+
repair = _run(repair_command)
|
|
618
|
+
if repair.returncode != 0:
|
|
619
|
+
return monitor
|
|
620
|
+
|
|
621
|
+
return guard_branch(
|
|
622
|
+
command_kind=command_kind,
|
|
623
|
+
required_branch=required_branch,
|
|
624
|
+
allow_main_mutation=allow_main_mutation,
|
|
625
|
+
auto_switch=auto_switch,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _verify_current_branch(action: str, expected_branch: str, *, command: list[str]) -> RepoActionResult | None:
|
|
630
|
+
branch_command = ["git", "branch", "--show-current"]
|
|
631
|
+
branch_completed = _run(branch_command)
|
|
632
|
+
if branch_completed.returncode != 0:
|
|
633
|
+
return _result(action, branch_command, branch_completed, "Inspect repository branch state.")
|
|
634
|
+
|
|
635
|
+
actual_branch = branch_completed.stdout.strip()
|
|
636
|
+
if actual_branch != expected_branch:
|
|
637
|
+
completed = subprocess.CompletedProcess(
|
|
638
|
+
command,
|
|
639
|
+
2,
|
|
640
|
+
branch_completed.stdout,
|
|
641
|
+
(
|
|
642
|
+
f"Refusing to continue {action} because the current branch drifted: "
|
|
643
|
+
f"expected {expected_branch}, got {actual_branch}\n"
|
|
644
|
+
),
|
|
645
|
+
)
|
|
646
|
+
return _result(action, command, completed, "Inspect branch drift before continuing.")
|
|
647
|
+
return None
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _remote_mutation_preflight(
|
|
651
|
+
*,
|
|
652
|
+
action: str,
|
|
653
|
+
mutation: str,
|
|
654
|
+
required_branch: str = "",
|
|
655
|
+
) -> RepoActionResult | None:
|
|
656
|
+
command = ["remote-preflight", mutation]
|
|
657
|
+
if required_branch:
|
|
658
|
+
branch_command = ["git", "branch", "--show-current"]
|
|
659
|
+
branch_completed = _run(branch_command)
|
|
660
|
+
if branch_completed.returncode != 0:
|
|
661
|
+
return _result(action, branch_command, branch_completed, "STATE=BLOCKED; NEXT=diagnose_branch_state")
|
|
662
|
+
|
|
663
|
+
actual_branch = branch_completed.stdout.strip()
|
|
664
|
+
if actual_branch != required_branch:
|
|
665
|
+
completed = subprocess.CompletedProcess(
|
|
666
|
+
command,
|
|
667
|
+
2,
|
|
668
|
+
branch_completed.stdout,
|
|
669
|
+
(
|
|
670
|
+
"Remote mutation preflight blocked mutation due to branch drift: "
|
|
671
|
+
f"mutation={mutation}; expected_branch={required_branch}; actual_branch={actual_branch}\n"
|
|
672
|
+
),
|
|
673
|
+
)
|
|
674
|
+
return _result(action, command, completed, "STATE=REMOTE_DRIFT; NEXT=sync_or_regenerate_command")
|
|
675
|
+
|
|
676
|
+
remote_command = ["git", "ls-remote", "--exit-code", "origin", "HEAD"]
|
|
677
|
+
remote_completed = _run(remote_command)
|
|
678
|
+
if remote_completed.returncode != 0:
|
|
679
|
+
completed = subprocess.CompletedProcess(
|
|
680
|
+
command,
|
|
681
|
+
2,
|
|
682
|
+
remote_completed.stdout,
|
|
683
|
+
(
|
|
684
|
+
"Remote mutation preflight could not verify origin reachability: "
|
|
685
|
+
f"mutation={mutation}\n"
|
|
686
|
+
+ remote_completed.stderr
|
|
687
|
+
),
|
|
688
|
+
)
|
|
689
|
+
return _result(action, command, completed, "STATE=REMOTE_UNREACHABLE; NEXT=retry_remote_check_later")
|
|
690
|
+
return None
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def branch_create(branch: str, *, start_point: str = "main", push: bool = False) -> RepoActionResult:
|
|
694
|
+
with workspace_mutation_lock(Path("."), 'branch_create'):
|
|
695
|
+
monitor = guard_branch(
|
|
696
|
+
command_kind="branch-create",
|
|
697
|
+
required_branch=start_point,
|
|
698
|
+
allow_main_mutation=True,
|
|
699
|
+
auto_switch=True,
|
|
700
|
+
)
|
|
701
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
702
|
+
return _monitor_block_result(
|
|
703
|
+
action="branch-create",
|
|
704
|
+
command_kind="branch-create",
|
|
705
|
+
required_branch=start_point,
|
|
706
|
+
monitor=monitor,
|
|
707
|
+
next_action="Inspect transfer operation monitor block before creating branch.",
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
command = ["git", "switch", "-c", branch, start_point]
|
|
711
|
+
completed = _run(command)
|
|
712
|
+
if completed.returncode != 0:
|
|
713
|
+
return _result("branch-create", command, completed, "Inspect branch state before continuing.")
|
|
714
|
+
|
|
715
|
+
drift = _verify_current_branch("branch-create", branch, command=command)
|
|
716
|
+
if drift is not None:
|
|
717
|
+
return drift
|
|
718
|
+
|
|
719
|
+
if push:
|
|
720
|
+
preflight = _remote_mutation_preflight(
|
|
721
|
+
action="branch-create",
|
|
722
|
+
mutation="push-new-branch",
|
|
723
|
+
)
|
|
724
|
+
if preflight is not None:
|
|
725
|
+
return preflight
|
|
726
|
+
pushed = _safe_push_current_branch(
|
|
727
|
+
branch=branch,
|
|
728
|
+
purpose="transfer branch-create publish new branch",
|
|
729
|
+
)
|
|
730
|
+
push_command = list(pushed.command)
|
|
731
|
+
if not pushed.ok:
|
|
732
|
+
return _result(
|
|
733
|
+
"branch-create",
|
|
734
|
+
push_command,
|
|
735
|
+
pushed.as_completed_process(),
|
|
736
|
+
"Inspect safe push block or branch push failure before continuing.",
|
|
737
|
+
)
|
|
738
|
+
drift = _verify_current_branch("branch-create", branch, command=push_command)
|
|
739
|
+
if drift is not None:
|
|
740
|
+
return drift
|
|
741
|
+
remote_drift = _verify_remote_branch_matches_local(
|
|
742
|
+
action="branch-create",
|
|
743
|
+
branch=branch,
|
|
744
|
+
command=push_command,
|
|
745
|
+
next_action="Resolve remote branch mismatch before continuing with queued work.",
|
|
746
|
+
)
|
|
747
|
+
if remote_drift is not None:
|
|
748
|
+
return remote_drift
|
|
749
|
+
return _result("branch-create", push_command, pushed, "Run transfer state or continue with queued work.")
|
|
750
|
+
|
|
751
|
+
return _result("branch-create", command, completed, "Run transfer state or continue with queued work.")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def branch_switch(branch: str, *, pull: bool = False) -> RepoActionResult:
|
|
755
|
+
with workspace_mutation_lock(Path("."), "branch_switch"):
|
|
756
|
+
monitor = guard_branch(
|
|
757
|
+
command_kind="branch-switch",
|
|
758
|
+
required_branch=branch,
|
|
759
|
+
allow_main_mutation=True,
|
|
760
|
+
auto_switch=True,
|
|
761
|
+
)
|
|
762
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
763
|
+
return _monitor_block_result(
|
|
764
|
+
action="branch-switch",
|
|
765
|
+
command_kind="branch-switch",
|
|
766
|
+
required_branch=branch,
|
|
767
|
+
monitor=monitor,
|
|
768
|
+
next_action="Inspect transfer operation monitor block before switching branch.",
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
command = ["git", "switch", branch]
|
|
772
|
+
completed = _run(command)
|
|
773
|
+
if completed.returncode != 0:
|
|
774
|
+
return _result("branch-switch", command, completed, "Inspect branch state before continuing.")
|
|
775
|
+
|
|
776
|
+
drift = _verify_current_branch("branch-switch", branch, command=command)
|
|
777
|
+
if drift is not None:
|
|
778
|
+
return drift
|
|
779
|
+
|
|
780
|
+
if pull:
|
|
781
|
+
pull_command = ["git", "pull", "--ff-only", "origin", branch]
|
|
782
|
+
pulled = _run(pull_command)
|
|
783
|
+
if pulled.returncode != 0:
|
|
784
|
+
return _result("branch-switch", pull_command, pulled, "Inspect pull failure before continuing.")
|
|
785
|
+
drift = _verify_current_branch("branch-switch", branch, command=pull_command)
|
|
786
|
+
if drift is not None:
|
|
787
|
+
return drift
|
|
788
|
+
return _result("branch-switch", pull_command, pulled, "Run transfer state or continue with queued work.")
|
|
789
|
+
|
|
790
|
+
return _result("branch-switch", command, completed, "Run transfer state or continue with queued work.")
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _current_branch_result(action: str = "commit") -> tuple[str, RepoActionResult | None]:
|
|
794
|
+
branch_command = ["git", "branch", "--show-current"]
|
|
795
|
+
branch_completed = _run(branch_command)
|
|
796
|
+
if branch_completed.returncode != 0:
|
|
797
|
+
return "", _result(
|
|
798
|
+
action,
|
|
799
|
+
branch_command,
|
|
800
|
+
branch_completed,
|
|
801
|
+
"Inspect repository branch state before committing.",
|
|
802
|
+
)
|
|
803
|
+
return branch_completed.stdout.strip(), None
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _main_commit_refusal(message: str) -> RepoActionResult:
|
|
807
|
+
completed = subprocess.CompletedProcess(
|
|
808
|
+
["git", "commit", "-m", message],
|
|
809
|
+
2,
|
|
810
|
+
"",
|
|
811
|
+
"Refusing to commit directly on main. Use a branch or pass --allow-main explicitly.\n",
|
|
812
|
+
)
|
|
813
|
+
return _result("commit", completed.args, completed, "Create a feature/admin branch before committing.")
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def commit_paths(message: str, paths: list[str], *, allow_main: bool = False, required_branch: str = "") -> RepoActionResult:
|
|
817
|
+
with acquire_workspace_lock(Path("."), "commit_paths"):
|
|
818
|
+
return _commit_paths_unlocked(
|
|
819
|
+
message,
|
|
820
|
+
paths,
|
|
821
|
+
allow_main=allow_main,
|
|
822
|
+
required_branch=required_branch,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _commit_paths_unlocked(
|
|
827
|
+
message: str,
|
|
828
|
+
paths: list[str],
|
|
829
|
+
*,
|
|
830
|
+
allow_main: bool = False,
|
|
831
|
+
required_branch: str = "",
|
|
832
|
+
) -> RepoActionResult:
|
|
833
|
+
if not paths:
|
|
834
|
+
completed = subprocess.CompletedProcess(["git", "add"], 2, "", "No paths supplied.\n")
|
|
835
|
+
return _result("commit", ["git", "add"], completed, "Provide explicit paths for commit.")
|
|
836
|
+
|
|
837
|
+
if required_branch:
|
|
838
|
+
monitor = guard_branch(
|
|
839
|
+
command_kind="commit",
|
|
840
|
+
required_branch=required_branch,
|
|
841
|
+
allow_main_mutation=allow_main,
|
|
842
|
+
auto_switch=True,
|
|
843
|
+
)
|
|
844
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
845
|
+
return _monitor_block_result(
|
|
846
|
+
action="commit",
|
|
847
|
+
command_kind="commit",
|
|
848
|
+
required_branch=required_branch,
|
|
849
|
+
monitor=monitor,
|
|
850
|
+
next_action="Inspect transfer operation monitor block before committing.",
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
branch_before_add, branch_error = _current_branch_result()
|
|
854
|
+
if branch_error is not None:
|
|
855
|
+
return branch_error
|
|
856
|
+
|
|
857
|
+
if branch_before_add == "main" and not allow_main:
|
|
858
|
+
return _main_commit_refusal(message)
|
|
859
|
+
|
|
860
|
+
if required_branch and branch_before_add != required_branch:
|
|
861
|
+
completed = subprocess.CompletedProcess(
|
|
862
|
+
["transfer-monitor", "commit", "--branch", required_branch],
|
|
863
|
+
2,
|
|
864
|
+
"",
|
|
865
|
+
(
|
|
866
|
+
"Transfer operation monitor branch mismatch before commit: "
|
|
867
|
+
f"actual_branch={branch_before_add}; required_branch={required_branch}\n"
|
|
868
|
+
),
|
|
869
|
+
)
|
|
870
|
+
return _result(
|
|
871
|
+
"commit",
|
|
872
|
+
list(completed.args),
|
|
873
|
+
completed,
|
|
874
|
+
"Inspect transfer operation monitor branch mismatch before committing.",
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
add_command = ["git", "add", *paths]
|
|
878
|
+
added = _run(add_command)
|
|
879
|
+
if added.returncode != 0:
|
|
880
|
+
return _result("commit", add_command, added, "Inspect path list and worktree before continuing.")
|
|
881
|
+
|
|
882
|
+
branch_before_commit, branch_error = _current_branch_result()
|
|
883
|
+
if branch_error is not None:
|
|
884
|
+
return branch_error
|
|
885
|
+
|
|
886
|
+
if branch_before_commit != branch_before_add:
|
|
887
|
+
completed = subprocess.CompletedProcess(
|
|
888
|
+
["git", "commit", "-m", message],
|
|
889
|
+
2,
|
|
890
|
+
"",
|
|
891
|
+
(
|
|
892
|
+
"Refusing to commit because the current branch changed during transfer commit: "
|
|
893
|
+
f"{branch_before_add} -> {branch_before_commit}\n"
|
|
894
|
+
),
|
|
895
|
+
)
|
|
896
|
+
return _result(
|
|
897
|
+
"commit",
|
|
898
|
+
completed.args,
|
|
899
|
+
completed,
|
|
900
|
+
"Inspect branch drift before committing.",
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
if branch_before_commit == "main" and not allow_main:
|
|
904
|
+
return _main_commit_refusal(message)
|
|
905
|
+
|
|
906
|
+
commit_command = ["git", "commit", "-m", message]
|
|
907
|
+
committed = _run(commit_command)
|
|
908
|
+
return _result("commit", commit_command, committed, "Push current branch or inspect status.")
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def push_current(*, required_branch: str = "") -> RepoActionResult:
|
|
912
|
+
with acquire_workspace_lock(Path("."), "push_current"):
|
|
913
|
+
return _push_current_unlocked(required_branch=required_branch)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _push_current_unlocked(*, required_branch: str = "") -> RepoActionResult:
|
|
917
|
+
if required_branch:
|
|
918
|
+
monitor = guard_branch(
|
|
919
|
+
command_kind="push-current",
|
|
920
|
+
required_branch=required_branch,
|
|
921
|
+
allow_main_mutation=False,
|
|
922
|
+
auto_switch=True,
|
|
923
|
+
)
|
|
924
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
925
|
+
completed = subprocess.CompletedProcess(
|
|
926
|
+
["transfer-monitor", "push-current", "--branch", required_branch],
|
|
927
|
+
2,
|
|
928
|
+
"",
|
|
929
|
+
(
|
|
930
|
+
"Transfer operation monitor blocked push-current: "
|
|
931
|
+
f"{monitor.reason}; actual_branch={monitor.actual_branch}; "
|
|
932
|
+
f"required_branch={monitor.required_branch}\n"
|
|
933
|
+
),
|
|
934
|
+
)
|
|
935
|
+
return _result(
|
|
936
|
+
"push-current",
|
|
937
|
+
list(completed.args),
|
|
938
|
+
completed,
|
|
939
|
+
"Inspect transfer operation monitor block before pushing.",
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
branch_completed = _run(["git", "branch", "--show-current"])
|
|
943
|
+
if branch_completed.returncode != 0:
|
|
944
|
+
return _result("push-current", ["git", "branch", "--show-current"], branch_completed, "Inspect repository state.")
|
|
945
|
+
branch = branch_completed.stdout.strip()
|
|
946
|
+
if not branch:
|
|
947
|
+
command = ["safe-push", "push-current", "--branch", "UNKNOWN"]
|
|
948
|
+
completed = subprocess.CompletedProcess(command, 2, "", "No current branch detected.\n")
|
|
949
|
+
return _result("push-current", command, completed, "Switch to a named branch first.")
|
|
950
|
+
|
|
951
|
+
if branch == "main":
|
|
952
|
+
completed = subprocess.CompletedProcess(
|
|
953
|
+
["transfer-monitor", "push-current", "--branch", branch],
|
|
954
|
+
2,
|
|
955
|
+
"",
|
|
956
|
+
(
|
|
957
|
+
"Transfer operation monitor blocked push-current: "
|
|
958
|
+
"main_mutation_not_allowed; actual_branch=main; required_branch=main\n"
|
|
959
|
+
),
|
|
960
|
+
)
|
|
961
|
+
return _result(
|
|
962
|
+
"push-current",
|
|
963
|
+
list(completed.args),
|
|
964
|
+
completed,
|
|
965
|
+
"Switch to a feature/admin branch before pushing.",
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
if required_branch and branch != required_branch:
|
|
969
|
+
completed = subprocess.CompletedProcess(
|
|
970
|
+
["transfer-monitor", "push-current", "--branch", required_branch],
|
|
971
|
+
2,
|
|
972
|
+
"",
|
|
973
|
+
(
|
|
974
|
+
"Transfer operation monitor branch mismatch after guard: "
|
|
975
|
+
f"actual_branch={branch}; required_branch={required_branch}\n"
|
|
976
|
+
),
|
|
977
|
+
)
|
|
978
|
+
return _result(
|
|
979
|
+
"push-current",
|
|
980
|
+
list(completed.args),
|
|
981
|
+
completed,
|
|
982
|
+
"Inspect transfer operation monitor branch mismatch before pushing.",
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
preflight = _remote_mutation_preflight(
|
|
986
|
+
action="push-current",
|
|
987
|
+
mutation="push-current",
|
|
988
|
+
)
|
|
989
|
+
if preflight is not None:
|
|
990
|
+
return preflight
|
|
991
|
+
|
|
992
|
+
pushed = _safe_push_current_branch(
|
|
993
|
+
branch=branch,
|
|
994
|
+
purpose="transfer push-current publish branch",
|
|
995
|
+
)
|
|
996
|
+
command = list(pushed.command)
|
|
997
|
+
if not pushed.ok:
|
|
998
|
+
return _result(
|
|
999
|
+
"push-current",
|
|
1000
|
+
command,
|
|
1001
|
+
pushed.as_completed_process(),
|
|
1002
|
+
"Inspect safe push block or push failure before continuing.",
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
drift = _verify_current_branch("push-current", branch, command=command)
|
|
1006
|
+
if drift is not None:
|
|
1007
|
+
return drift
|
|
1008
|
+
|
|
1009
|
+
remote_drift = _verify_remote_branch_matches_local(
|
|
1010
|
+
action="push-current",
|
|
1011
|
+
branch=branch,
|
|
1012
|
+
command=command,
|
|
1013
|
+
next_action="Resolve remote branch mismatch before creating or updating a PR.",
|
|
1014
|
+
)
|
|
1015
|
+
if remote_drift is not None:
|
|
1016
|
+
return remote_drift
|
|
1017
|
+
|
|
1018
|
+
return _result("push-current", command, pushed.as_completed_process(), "Create or inspect pull request.")
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _remote_head_sha(head: str) -> tuple[str, subprocess.CompletedProcess[str]]:
|
|
1022
|
+
command = ["git", "ls-remote", "--exit-code", "--heads", "origin", head]
|
|
1023
|
+
completed = _run(command)
|
|
1024
|
+
if completed.returncode != 0:
|
|
1025
|
+
return "", completed
|
|
1026
|
+
for line in completed.stdout.splitlines():
|
|
1027
|
+
parts = line.split()
|
|
1028
|
+
if len(parts) >= 2 and parts[1] == f"refs/heads/{head}":
|
|
1029
|
+
return parts[0], completed
|
|
1030
|
+
return "", subprocess.CompletedProcess(command, 2, completed.stdout, f"Remote branch origin/{head} was not found.\n")
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _verify_remote_branch_matches_local(
|
|
1034
|
+
*,
|
|
1035
|
+
action: str,
|
|
1036
|
+
branch: str,
|
|
1037
|
+
command: list[str],
|
|
1038
|
+
next_action: str,
|
|
1039
|
+
) -> RepoActionResult | None:
|
|
1040
|
+
local_command = ["git", "rev-parse", "HEAD"]
|
|
1041
|
+
local_completed = _run(local_command)
|
|
1042
|
+
if local_completed.returncode != 0:
|
|
1043
|
+
return _result(action, local_command, local_completed, "Inspect local HEAD before verifying remote branch.")
|
|
1044
|
+
local_head = local_completed.stdout.strip()
|
|
1045
|
+
remote_sha, remote_completed = _remote_head_sha(branch)
|
|
1046
|
+
if remote_completed.returncode != 0 or not remote_sha:
|
|
1047
|
+
completed = subprocess.CompletedProcess(
|
|
1048
|
+
remote_completed.args,
|
|
1049
|
+
2,
|
|
1050
|
+
remote_completed.stdout,
|
|
1051
|
+
(
|
|
1052
|
+
f"Remote branch origin/{branch} could not be verified after push.\n"
|
|
1053
|
+
f"{remote_completed.stderr}"
|
|
1054
|
+
),
|
|
1055
|
+
)
|
|
1056
|
+
return _result(action, list(completed.args), completed, "Inspect remote branch verification before continuing.")
|
|
1057
|
+
if remote_sha != local_head:
|
|
1058
|
+
completed = subprocess.CompletedProcess(
|
|
1059
|
+
command,
|
|
1060
|
+
2,
|
|
1061
|
+
remote_completed.stdout,
|
|
1062
|
+
(
|
|
1063
|
+
f"Remote branch origin/{branch} does not match local HEAD after push. "
|
|
1064
|
+
f"remote={remote_sha}; local={local_head}.\n"
|
|
1065
|
+
),
|
|
1066
|
+
)
|
|
1067
|
+
return _result(action, command, completed, next_action)
|
|
1068
|
+
return None
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def ensure_remote_head(
|
|
1072
|
+
head: str,
|
|
1073
|
+
*,
|
|
1074
|
+
auto_push: bool = True,
|
|
1075
|
+
action: str = "ensure-remote-head",
|
|
1076
|
+
) -> RepoActionResult:
|
|
1077
|
+
mutation_lock_contract = "workspace_mutation_lock"
|
|
1078
|
+
_ = mutation_lock_contract
|
|
1079
|
+
"""Verify that the remote head branch exists and matches local HEAD.
|
|
1080
|
+
|
|
1081
|
+
This is intentionally stricter than a successful `gh pr create`: PR creation
|
|
1082
|
+
must not rely on a stale or missing remote branch and must never silently
|
|
1083
|
+
force-push divergent remote state.
|
|
1084
|
+
"""
|
|
1085
|
+
|
|
1086
|
+
branch_command = ["git", "branch", "--show-current"]
|
|
1087
|
+
branch_completed = _run(branch_command)
|
|
1088
|
+
if branch_completed.returncode != 0:
|
|
1089
|
+
return _result(action, branch_command, branch_completed, "Inspect repository branch state.")
|
|
1090
|
+
branch = branch_completed.stdout.strip()
|
|
1091
|
+
if branch != head:
|
|
1092
|
+
completed = subprocess.CompletedProcess(
|
|
1093
|
+
branch_command,
|
|
1094
|
+
2,
|
|
1095
|
+
branch_completed.stdout,
|
|
1096
|
+
f"Current branch {branch!r} does not match PR head {head!r}.\n",
|
|
1097
|
+
)
|
|
1098
|
+
return _result(action, branch_command, completed, "Switch to the PR head branch before creating a PR.")
|
|
1099
|
+
|
|
1100
|
+
local_command = ["git", "rev-parse", "HEAD"]
|
|
1101
|
+
local_completed = _run(local_command)
|
|
1102
|
+
if local_completed.returncode != 0:
|
|
1103
|
+
return _result(action, local_command, local_completed, "Inspect local HEAD before creating a PR.")
|
|
1104
|
+
local_head = local_completed.stdout.strip()
|
|
1105
|
+
|
|
1106
|
+
remote_sha, remote_completed = _remote_head_sha(head)
|
|
1107
|
+
auto_pushed = False
|
|
1108
|
+
if not remote_sha:
|
|
1109
|
+
if not auto_push:
|
|
1110
|
+
completed = subprocess.CompletedProcess(
|
|
1111
|
+
remote_completed.args,
|
|
1112
|
+
2,
|
|
1113
|
+
remote_completed.stdout,
|
|
1114
|
+
f"Remote branch origin/{head} is missing and auto-push is disabled.\n{remote_completed.stderr}",
|
|
1115
|
+
)
|
|
1116
|
+
return _result(action, list(completed.args), completed, "Push the branch through transfer push-current before creating a PR.")
|
|
1117
|
+
|
|
1118
|
+
push_result = push_current(required_branch=head)
|
|
1119
|
+
if push_result.returncode != 0:
|
|
1120
|
+
completed = subprocess.CompletedProcess(
|
|
1121
|
+
push_result.command,
|
|
1122
|
+
push_result.returncode,
|
|
1123
|
+
push_result.stdout,
|
|
1124
|
+
push_result.stderr,
|
|
1125
|
+
)
|
|
1126
|
+
return _result(action, push_result.command, completed, "Inspect push-current failure before creating a PR.")
|
|
1127
|
+
auto_pushed = True
|
|
1128
|
+
|
|
1129
|
+
local_completed = _run(local_command)
|
|
1130
|
+
if local_completed.returncode != 0:
|
|
1131
|
+
return _result(action, local_command, local_completed, "Inspect local HEAD after branch push.")
|
|
1132
|
+
local_head = local_completed.stdout.strip()
|
|
1133
|
+
remote_sha, remote_completed = _remote_head_sha(head)
|
|
1134
|
+
|
|
1135
|
+
if remote_completed.returncode not in {0, 2} and not remote_sha:
|
|
1136
|
+
return _result(action, list(remote_completed.args), remote_completed, "Inspect remote branch lookup before creating a PR.")
|
|
1137
|
+
|
|
1138
|
+
if remote_sha != local_head:
|
|
1139
|
+
completed = subprocess.CompletedProcess(
|
|
1140
|
+
remote_completed.args,
|
|
1141
|
+
2,
|
|
1142
|
+
remote_completed.stdout,
|
|
1143
|
+
(
|
|
1144
|
+
f"Remote branch origin/{head} does not match local HEAD. "
|
|
1145
|
+
f"remote={remote_sha or '<missing>'}; local={local_head}. "
|
|
1146
|
+
"Refusing PR create without force-push.\n"
|
|
1147
|
+
),
|
|
1148
|
+
)
|
|
1149
|
+
return _result(action, list(completed.args), completed, "Resolve remote branch divergence before creating a PR.")
|
|
1150
|
+
|
|
1151
|
+
stdout = (
|
|
1152
|
+
"REMOTE_HEAD_VERIFIED\n"
|
|
1153
|
+
f"head={head}\n"
|
|
1154
|
+
f"local_head={local_head}\n"
|
|
1155
|
+
f"remote_head={remote_sha}\n"
|
|
1156
|
+
f"auto_pushed={str(auto_pushed).lower()}\n"
|
|
1157
|
+
)
|
|
1158
|
+
completed = subprocess.CompletedProcess(list(remote_completed.args), 0, stdout, "")
|
|
1159
|
+
return _result(action, list(completed.args), completed, "Create or inspect pull request.")
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def pr_create(*, base: str, head: str, title: str, body: str) -> RepoActionResult:
|
|
1163
|
+
with workspace_mutation_lock(Path("."), "pr_create"):
|
|
1164
|
+
return _pr_create_unlocked(base=base, head=head, title=title, body=body)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _pr_create_unlocked(*, base: str, head: str, title: str, body: str) -> RepoActionResult:
|
|
1168
|
+
if head == "current":
|
|
1169
|
+
branch_result = _run(["git", "branch", "--show-current"])
|
|
1170
|
+
resolved_head = branch_result.stdout.strip()
|
|
1171
|
+
if branch_result.returncode != 0 or not resolved_head:
|
|
1172
|
+
completed = subprocess.CompletedProcess(
|
|
1173
|
+
["transfer-monitor", "pr-create", "--base", base, "--head", head],
|
|
1174
|
+
2,
|
|
1175
|
+
"",
|
|
1176
|
+
"Transfer operation monitor blocked pr-create: current_branch_missing; actual_branch=; required_branch=current\n",
|
|
1177
|
+
)
|
|
1178
|
+
return _result("pr-create", list(completed.args), completed, "Inspect current branch resolution before creating a PR.")
|
|
1179
|
+
head = resolved_head
|
|
1180
|
+
|
|
1181
|
+
monitor = guard_pr_create(base_branch=base, head_branch=head)
|
|
1182
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
1183
|
+
completed = subprocess.CompletedProcess(
|
|
1184
|
+
["transfer-monitor", "pr-create", "--base", base, "--head", head],
|
|
1185
|
+
2,
|
|
1186
|
+
"",
|
|
1187
|
+
f"Transfer operation monitor blocked pr-create: {monitor.reason}; actual_branch={monitor.actual_branch}; required_branch={monitor.required_branch}\n",
|
|
1188
|
+
)
|
|
1189
|
+
return _result("pr-create", list(completed.args), completed, "Inspect transfer operation monitor block before creating a PR.")
|
|
1190
|
+
|
|
1191
|
+
preflight = _remote_mutation_preflight(
|
|
1192
|
+
action="pr-create",
|
|
1193
|
+
mutation="pr-create",
|
|
1194
|
+
required_branch=head,
|
|
1195
|
+
)
|
|
1196
|
+
if preflight is not None:
|
|
1197
|
+
return preflight
|
|
1198
|
+
|
|
1199
|
+
remote_head = ensure_remote_head(head, auto_push=True, action="pr-create")
|
|
1200
|
+
if remote_head.returncode != 0:
|
|
1201
|
+
return remote_head
|
|
1202
|
+
|
|
1203
|
+
command = [
|
|
1204
|
+
"gh",
|
|
1205
|
+
"pr",
|
|
1206
|
+
"create",
|
|
1207
|
+
"--base",
|
|
1208
|
+
base,
|
|
1209
|
+
"--head",
|
|
1210
|
+
head,
|
|
1211
|
+
"--title",
|
|
1212
|
+
title,
|
|
1213
|
+
"--body",
|
|
1214
|
+
body,
|
|
1215
|
+
]
|
|
1216
|
+
completed = _run(command)
|
|
1217
|
+
if completed.returncode == 0:
|
|
1218
|
+
return _result("pr-create", command, completed, "Run agentic-kit transfer pr-status on the created PR.")
|
|
1219
|
+
|
|
1220
|
+
combined = f"{completed.stdout}\n{completed.stderr}".lower()
|
|
1221
|
+
if "already exists" in combined or "pull request already exists" in combined:
|
|
1222
|
+
lookup_command = [
|
|
1223
|
+
"gh",
|
|
1224
|
+
"pr",
|
|
1225
|
+
"list",
|
|
1226
|
+
"--base",
|
|
1227
|
+
base,
|
|
1228
|
+
"--head",
|
|
1229
|
+
head,
|
|
1230
|
+
"--state",
|
|
1231
|
+
"all",
|
|
1232
|
+
"--limit",
|
|
1233
|
+
"5",
|
|
1234
|
+
"--json",
|
|
1235
|
+
"number,state,url,headRefName,baseRefName,title",
|
|
1236
|
+
]
|
|
1237
|
+
lookup = _run(lookup_command)
|
|
1238
|
+
if lookup.returncode != 0:
|
|
1239
|
+
return _result("pr-create", lookup_command, lookup, "STATE=BLOCKED; NEXT=diagnose_existing_pr_lookup")
|
|
1240
|
+
try:
|
|
1241
|
+
prs = json.loads(lookup.stdout or "[]")
|
|
1242
|
+
except json.JSONDecodeError as exc:
|
|
1243
|
+
bad = subprocess.CompletedProcess(
|
|
1244
|
+
lookup_command,
|
|
1245
|
+
2,
|
|
1246
|
+
lookup.stdout,
|
|
1247
|
+
f"Could not parse existing PR lookup JSON: {exc}\n",
|
|
1248
|
+
)
|
|
1249
|
+
return _result("pr-create", lookup_command, bad, "STATE=BLOCKED; NEXT=diagnose_existing_pr_lookup")
|
|
1250
|
+
if isinstance(prs, list) and len(prs) == 1:
|
|
1251
|
+
pr = prs[0]
|
|
1252
|
+
out = (
|
|
1253
|
+
"IDEMPOTENT_PR_RECOVERY\n"
|
|
1254
|
+
"STATE=PR_EXISTS\n"
|
|
1255
|
+
f"PR={pr.get('number', '')}\n"
|
|
1256
|
+
f"URL={pr.get('url', '')}\n"
|
|
1257
|
+
f"BASE={base}\n"
|
|
1258
|
+
f"HEAD={head}\n"
|
|
1259
|
+
"RESULT=PASS\n"
|
|
1260
|
+
)
|
|
1261
|
+
recovered = subprocess.CompletedProcess(command, 0, out, "")
|
|
1262
|
+
return _result("pr-create", command, recovered, "STATE=PR_EXISTS; NEXT=run_pr_status_or_pr_complete")
|
|
1263
|
+
bad = subprocess.CompletedProcess(
|
|
1264
|
+
lookup_command,
|
|
1265
|
+
2,
|
|
1266
|
+
lookup.stdout,
|
|
1267
|
+
f"Expected exactly one existing PR for {head}->{base}; found {len(prs) if isinstance(prs, list) else 'non-list'}\n",
|
|
1268
|
+
)
|
|
1269
|
+
return _result("pr-create", lookup_command, bad, "STATE=BLOCKED; NEXT=diagnose_existing_pr_lookup")
|
|
1270
|
+
|
|
1271
|
+
return _result("pr-create", command, completed, "Run agentic-kit transfer pr-status on the created PR.")
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def repo_status(*, short: bool = True) -> RepoActionResult:
|
|
1276
|
+
command = ["git", "status", "--short"] if short else ["git", "status"]
|
|
1277
|
+
completed = _run(command)
|
|
1278
|
+
next_action = (
|
|
1279
|
+
"Worktree clean; continue with the next planned slice."
|
|
1280
|
+
if completed.returncode == 0 and not completed.stdout.strip()
|
|
1281
|
+
else "Inspect changes, commit explicit paths, or clean the worktree."
|
|
1282
|
+
)
|
|
1283
|
+
return _result("repo-status", command, completed, next_action)
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def repo_log(limit: int = 5) -> RepoActionResult:
|
|
1287
|
+
command = ["git", "log", f"-{limit}", "--oneline"]
|
|
1288
|
+
completed = _run(command)
|
|
1289
|
+
return _result("repo-log", command, completed, "Use commit SHAs for guarded PR or merge work.")
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def head_sha(*, full: bool = False) -> RepoActionResult:
|
|
1293
|
+
command = ["git", "rev-parse", "HEAD"] if full else ["git", "rev-parse", "--short", "HEAD"]
|
|
1294
|
+
completed = _run(command)
|
|
1295
|
+
return _result("head-sha", command, completed, "Use this SHA for guarded PR or merge work.")
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
def repo_diff(*, cached: bool = False, name_only: bool = False) -> RepoActionResult:
|
|
1299
|
+
command = ["git", "diff"]
|
|
1300
|
+
if cached:
|
|
1301
|
+
command.append("--cached")
|
|
1302
|
+
if name_only:
|
|
1303
|
+
command.append("--name-only")
|
|
1304
|
+
completed = _run(command)
|
|
1305
|
+
return _result("repo-diff", command, completed, "Review the actual diff before committing or protected planning.")
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def fetch_origin(branch: str = "main") -> RepoActionResult:
|
|
1309
|
+
command = ["git", "fetch", "origin", branch]
|
|
1310
|
+
completed = _run(command)
|
|
1311
|
+
return _result("fetch-origin", command, completed, "Fast-forward pull, switch branch, or inspect PR state.")
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def pull_current() -> RepoActionResult:
|
|
1315
|
+
with workspace_mutation_lock(Path("."), "pull_current"):
|
|
1316
|
+
branch_completed = _run(["git", "branch", "--show-current"])
|
|
1317
|
+
if branch_completed.returncode != 0:
|
|
1318
|
+
return _result("pull-current", ["git", "branch", "--show-current"], branch_completed, "Inspect repository state.")
|
|
1319
|
+
branch = branch_completed.stdout.strip()
|
|
1320
|
+
if not branch:
|
|
1321
|
+
completed = subprocess.CompletedProcess(["git", "pull"], 2, "", "No current branch detected.\n")
|
|
1322
|
+
return _result("pull-current", ["git", "pull"], completed, "Switch to a named branch first.")
|
|
1323
|
+
command = ["git", "pull", "--ff-only", "origin", branch]
|
|
1324
|
+
completed = _run(command)
|
|
1325
|
+
return _result("pull-current", command, completed, "Run transfer state or continue with queued work.")
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def branch_delete(branch: str, *, remote: bool = False, force: bool = False) -> RepoActionResult:
|
|
1329
|
+
with workspace_mutation_lock(Path("."), "branch_delete"):
|
|
1330
|
+
if remote:
|
|
1331
|
+
preflight = _remote_mutation_preflight(
|
|
1332
|
+
action="branch-delete",
|
|
1333
|
+
mutation="delete-remote-branch",
|
|
1334
|
+
)
|
|
1335
|
+
if preflight is not None:
|
|
1336
|
+
return preflight
|
|
1337
|
+
command = ["git", "push", "origin", "--delete", branch]
|
|
1338
|
+
completed = _run(command)
|
|
1339
|
+
return _result("branch-delete", command, completed, "Inspect local and remote branch state.")
|
|
1340
|
+
command = ["git", "branch", "-D" if force else "-d", branch]
|
|
1341
|
+
completed = _run(command)
|
|
1342
|
+
return _result("branch-delete", command, completed, "Inspect local branch state.")
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def pr_wait_ci(
|
|
1346
|
+
pr_number: int,
|
|
1347
|
+
*,
|
|
1348
|
+
expected_head_sha: str = "",
|
|
1349
|
+
timeout_seconds: int = 300,
|
|
1350
|
+
poll_seconds: int = 10,
|
|
1351
|
+
) -> RepoActionResult:
|
|
1352
|
+
if expected_head_sha:
|
|
1353
|
+
guarded = _full_sha_guard("pr-wait-ci", expected_head_sha)
|
|
1354
|
+
if guarded is not None:
|
|
1355
|
+
return guarded
|
|
1356
|
+
else:
|
|
1357
|
+
expected_head_sha, failure = _resolve_pr_head_sha(pr_number, action="pr-wait-ci")
|
|
1358
|
+
if failure is not None:
|
|
1359
|
+
return failure
|
|
1360
|
+
|
|
1361
|
+
command = [
|
|
1362
|
+
_agentic_kit_command(),
|
|
1363
|
+
"pr",
|
|
1364
|
+
"wait-ci",
|
|
1365
|
+
str(pr_number),
|
|
1366
|
+
"--timeout-seconds",
|
|
1367
|
+
str(timeout_seconds),
|
|
1368
|
+
"--interval-seconds",
|
|
1369
|
+
str(poll_seconds),
|
|
1370
|
+
"--expected-head-sha",
|
|
1371
|
+
expected_head_sha,
|
|
1372
|
+
]
|
|
1373
|
+
completed = _run(command)
|
|
1374
|
+
return _result("pr-wait-ci", command, completed, "Run transfer pr-status or merge-if-green after CI is green.")
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
def pr_merge_safe(
|
|
1378
|
+
pr_number: int,
|
|
1379
|
+
*,
|
|
1380
|
+
expected_head_sha: str = "",
|
|
1381
|
+
main_branch: str = "main",
|
|
1382
|
+
merge_method: str = "squash",
|
|
1383
|
+
no_verify_main: bool = False,
|
|
1384
|
+
merge_state_timeout_seconds: int = 60,
|
|
1385
|
+
merge_state_poll_seconds: int = 5,
|
|
1386
|
+
) -> RepoActionResult:
|
|
1387
|
+
with workspace_mutation_lock(Path("."), "pr_merge_safe"):
|
|
1388
|
+
return _pr_merge_safe_unlocked(
|
|
1389
|
+
pr_number,
|
|
1390
|
+
expected_head_sha=expected_head_sha,
|
|
1391
|
+
main_branch=main_branch,
|
|
1392
|
+
merge_method=merge_method,
|
|
1393
|
+
no_verify_main=no_verify_main,
|
|
1394
|
+
merge_state_timeout_seconds=merge_state_timeout_seconds,
|
|
1395
|
+
merge_state_poll_seconds=merge_state_poll_seconds,
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def _pr_merge_safe_unlocked(
|
|
1400
|
+
pr_number: int,
|
|
1401
|
+
*,
|
|
1402
|
+
expected_head_sha: str = "",
|
|
1403
|
+
main_branch: str = "main",
|
|
1404
|
+
merge_method: str = "squash",
|
|
1405
|
+
no_verify_main: bool = False,
|
|
1406
|
+
merge_state_timeout_seconds: int = 60,
|
|
1407
|
+
merge_state_poll_seconds: int = 5,
|
|
1408
|
+
) -> RepoActionResult:
|
|
1409
|
+
monitor = guard_branch(
|
|
1410
|
+
command_kind="pr-merge-safe",
|
|
1411
|
+
required_branch=main_branch,
|
|
1412
|
+
allow_main_mutation=True,
|
|
1413
|
+
auto_switch=True,
|
|
1414
|
+
)
|
|
1415
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
1416
|
+
monitor = _recover_known_volatile_branch_switch_block(
|
|
1417
|
+
command_kind="pr-merge-safe",
|
|
1418
|
+
required_branch=main_branch,
|
|
1419
|
+
monitor=monitor,
|
|
1420
|
+
allow_main_mutation=True,
|
|
1421
|
+
auto_switch=True,
|
|
1422
|
+
)
|
|
1423
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
1424
|
+
return _monitor_block_result(
|
|
1425
|
+
action="pr-merge-safe",
|
|
1426
|
+
command_kind="pr-merge-safe",
|
|
1427
|
+
required_branch=main_branch,
|
|
1428
|
+
monitor=monitor,
|
|
1429
|
+
next_action="Inspect transfer operation monitor block before merging PR.",
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
if expected_head_sha:
|
|
1433
|
+
guarded = _full_sha_guard("pr-merge-safe", expected_head_sha)
|
|
1434
|
+
if guarded is not None:
|
|
1435
|
+
return guarded
|
|
1436
|
+
else:
|
|
1437
|
+
expected_head_sha, failure = _resolve_pr_head_sha(pr_number, action="pr-merge-safe")
|
|
1438
|
+
if failure is not None:
|
|
1439
|
+
return failure
|
|
1440
|
+
preflight = _remote_mutation_preflight(
|
|
1441
|
+
action="pr-merge-safe",
|
|
1442
|
+
mutation="pr-merge-safe",
|
|
1443
|
+
)
|
|
1444
|
+
if preflight is not None:
|
|
1445
|
+
return preflight
|
|
1446
|
+
|
|
1447
|
+
command = [
|
|
1448
|
+
_agentic_kit_command(),
|
|
1449
|
+
"pr",
|
|
1450
|
+
"merge-if-green",
|
|
1451
|
+
str(pr_number),
|
|
1452
|
+
"--expected-head-sha",
|
|
1453
|
+
expected_head_sha,
|
|
1454
|
+
"--main-branch",
|
|
1455
|
+
main_branch,
|
|
1456
|
+
"--merge-method",
|
|
1457
|
+
merge_method,
|
|
1458
|
+
"--merge-state-timeout-seconds",
|
|
1459
|
+
str(merge_state_timeout_seconds),
|
|
1460
|
+
"--merge-state-poll-seconds",
|
|
1461
|
+
str(merge_state_poll_seconds),
|
|
1462
|
+
]
|
|
1463
|
+
already_merged = _already_merged_pr_result(pr_number, action="pr-merge-safe", command=command)
|
|
1464
|
+
if already_merged is not None:
|
|
1465
|
+
return already_merged
|
|
1466
|
+
if no_verify_main:
|
|
1467
|
+
command.append("--no-verify-main")
|
|
1468
|
+
completed = _run(command)
|
|
1469
|
+
if completed.returncode != 0:
|
|
1470
|
+
already_merged = _already_merged_pr_result(pr_number, action="pr-merge-safe", command=command)
|
|
1471
|
+
if already_merged is not None:
|
|
1472
|
+
return already_merged
|
|
1473
|
+
return _result("pr-merge-safe", command, completed, "Sync main and run post-merge handoff refresh status.")
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def _post_merge_state_lines(*, state: str, next_action: str) -> str:
|
|
1477
|
+
return f"STATE={state}\nNEXT={next_action}\n"
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def post_merge_check(*, main_branch: str = "main") -> RepoActionResult:
|
|
1481
|
+
branch_command = ["git", "branch", "--show-current"]
|
|
1482
|
+
branch_completed = _run(branch_command)
|
|
1483
|
+
if branch_completed.returncode != 0:
|
|
1484
|
+
return _result("post-merge-check", branch_command, branch_completed, "STATE=BLOCKED; NEXT=diagnose_branch_state")
|
|
1485
|
+
|
|
1486
|
+
branch = branch_completed.stdout.strip()
|
|
1487
|
+
if branch != main_branch:
|
|
1488
|
+
completed = subprocess.CompletedProcess(
|
|
1489
|
+
branch_command,
|
|
1490
|
+
2,
|
|
1491
|
+
branch_completed.stdout,
|
|
1492
|
+
f"Expected branch {main_branch} before post-merge lifecycle check. Current branch: {branch}\n",
|
|
1493
|
+
)
|
|
1494
|
+
return _result("post-merge-check", branch_command, completed, "STATE=BLOCKED; NEXT=switch_to_main_and_sync")
|
|
1495
|
+
|
|
1496
|
+
command = [_agentic_kit_command(), "handoff", "post-merge-refresh-status"]
|
|
1497
|
+
completed = _run(command)
|
|
1498
|
+
if completed.returncode != 0:
|
|
1499
|
+
return _result("post-merge-check", command, completed, "STATE=BLOCKED; NEXT=diagnose_handoff_refresh_status")
|
|
1500
|
+
|
|
1501
|
+
if "result=NOOP" in completed.stdout:
|
|
1502
|
+
state = "READY"
|
|
1503
|
+
next_action = "none"
|
|
1504
|
+
elif "result=REFRESH_REQUIRED" in completed.stdout:
|
|
1505
|
+
state = "NEEDS_HANDOFF_REFRESH"
|
|
1506
|
+
next_action = "refresh_handoff_state"
|
|
1507
|
+
else:
|
|
1508
|
+
state = "BLOCKED"
|
|
1509
|
+
next_action = "diagnose_post_merge_refresh_status_output"
|
|
1510
|
+
|
|
1511
|
+
package_notes = _successor_package_freshness_notes()
|
|
1512
|
+
if package_notes:
|
|
1513
|
+
completed = subprocess.CompletedProcess(
|
|
1514
|
+
completed.args,
|
|
1515
|
+
completed.returncode,
|
|
1516
|
+
completed.stdout + ("\n" if completed.stdout else "") + "\n".join(package_notes),
|
|
1517
|
+
completed.stderr,
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
package_findings = _successor_package_freshness_findings()
|
|
1521
|
+
if package_findings:
|
|
1522
|
+
completed = subprocess.CompletedProcess(
|
|
1523
|
+
completed.args,
|
|
1524
|
+
1,
|
|
1525
|
+
completed.stdout + ("\n" if completed.stdout else "") + "\n".join(package_findings),
|
|
1526
|
+
completed.stderr,
|
|
1527
|
+
)
|
|
1528
|
+
state = "NEEDS_SUCCESSOR_PACKAGE_REFRESH"
|
|
1529
|
+
next_action = "refresh_successor_package"
|
|
1530
|
+
|
|
1531
|
+
return _result(
|
|
1532
|
+
"post-merge-check",
|
|
1533
|
+
command,
|
|
1534
|
+
completed,
|
|
1535
|
+
_post_merge_state_lines(state=state, next_action=next_action).strip(),
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
ADMIN_REFRESH_PATHS = _admin_refresh_paths(_LEGACY_WORKSPACE)
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
def _admin_refresh_successor_prompt_path(after_pr: int, *, ws: Workspace | None = None) -> str:
|
|
1543
|
+
workspace = ws or _LEGACY_WORKSPACE
|
|
1544
|
+
return _workspace_path_text(workspace, workspace.post_pr_successor_chat_handoff_path(after_pr))
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
_CURRENT_GOVERNED_SLICE_REFRESH_TEXT = (
|
|
1548
|
+
"Current governed slice: continue from fresh main with the next planned "
|
|
1549
|
+
"governed slice after post-merge handoff checks pass; do not repeat already "
|
|
1550
|
+
"verified release publication steps."
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
_NEXT_SAFE_STEP_REFRESH_TEXT = (
|
|
1554
|
+
"Next safe step: continue from fresh main with the next planned governed slice."
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def _refresh_current_state_multiline_field(
|
|
1559
|
+
block: str,
|
|
1560
|
+
*,
|
|
1561
|
+
label: str,
|
|
1562
|
+
replacement: str,
|
|
1563
|
+
following_labels: tuple[str, ...] = (),
|
|
1564
|
+
) -> str:
|
|
1565
|
+
label_pattern = re.escape(label)
|
|
1566
|
+
stop_labels = "|".join(re.escape(item) for item in following_labels)
|
|
1567
|
+
stop_pattern = r"\Z"
|
|
1568
|
+
if stop_labels:
|
|
1569
|
+
stop_pattern = rf"\n(?:{stop_labels}):|\Z"
|
|
1570
|
+
pattern = rf"(?ms)^{label_pattern}:.*?(?={stop_pattern})"
|
|
1571
|
+
match = re.search(pattern, block)
|
|
1572
|
+
if not match:
|
|
1573
|
+
return block
|
|
1574
|
+
suffix = ""
|
|
1575
|
+
if not following_labels:
|
|
1576
|
+
suffix_match = re.search(r"(\n*)\Z", match.group(0))
|
|
1577
|
+
suffix = suffix_match.group(1) if suffix_match else ""
|
|
1578
|
+
return block[: match.start()] + replacement + suffix + block[match.end() :]
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def _refresh_status_current_state_block(text: str, *, after_pr: int, short: str, subject: str) -> str:
|
|
1582
|
+
match = re.search(r"(?ms)^## Current State\s*(.*?)(?=^## |\Z)", text)
|
|
1583
|
+
if not match:
|
|
1584
|
+
return text
|
|
1585
|
+
|
|
1586
|
+
block = match.group(0)
|
|
1587
|
+
replacements = (
|
|
1588
|
+
(
|
|
1589
|
+
r"^Current verified main:.*$",
|
|
1590
|
+
f"Current verified main: `{short}` (`{subject}`).",
|
|
1591
|
+
),
|
|
1592
|
+
(
|
|
1593
|
+
r"^Latest substantive .*$",
|
|
1594
|
+
f"Latest substantive work: PR #{after_pr} (`{subject}`).",
|
|
1595
|
+
),
|
|
1596
|
+
(
|
|
1597
|
+
r"^Post-merge handoff status:.*$",
|
|
1598
|
+
f"Post-merge handoff status: PASS/NOOP after PR #{after_pr} administrative refresh.",
|
|
1599
|
+
),
|
|
1600
|
+
)
|
|
1601
|
+
for pattern, replacement in replacements:
|
|
1602
|
+
block = re.sub(pattern, replacement, block, count=1, flags=re.MULTILINE)
|
|
1603
|
+
block = _refresh_current_state_multiline_field(
|
|
1604
|
+
block,
|
|
1605
|
+
label="Current governed slice",
|
|
1606
|
+
replacement=_CURRENT_GOVERNED_SLICE_REFRESH_TEXT,
|
|
1607
|
+
following_labels=("Post-merge handoff status", "Next safe step"),
|
|
1608
|
+
)
|
|
1609
|
+
block = _refresh_current_state_multiline_field(
|
|
1610
|
+
block,
|
|
1611
|
+
label="Next safe step",
|
|
1612
|
+
replacement=_NEXT_SAFE_STEP_REFRESH_TEXT,
|
|
1613
|
+
)
|
|
1614
|
+
return text[: match.start()] + block + text[match.end() :]
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def _yaml_inline_scalar(value: str) -> str:
|
|
1618
|
+
dumped = yaml.safe_dump(
|
|
1619
|
+
value,
|
|
1620
|
+
allow_unicode=True,
|
|
1621
|
+
default_flow_style=True,
|
|
1622
|
+
width=4096,
|
|
1623
|
+
).strip()
|
|
1624
|
+
if dumped.endswith("\n..."):
|
|
1625
|
+
dumped = dumped.removesuffix("\n...").strip()
|
|
1626
|
+
return dumped
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
def _refresh_operational_handoff_docs(after_pr: int, *, ws: Workspace | None = None) -> subprocess.CompletedProcess[str]:
|
|
1630
|
+
command = ["admin-refresh-operational-handoff-docs", "--after-pr", str(after_pr)]
|
|
1631
|
+
workspace = ws or load_workspace(Path("."))
|
|
1632
|
+
try:
|
|
1633
|
+
full = _run(["git", "rev-parse", "HEAD"]).stdout.strip()
|
|
1634
|
+
short = _run(["git", "rev-parse", "--short=8", "HEAD"]).stdout.strip() or full[:8]
|
|
1635
|
+
subject = _run(["git", "log", "-1", "--format=%s"]).stdout.strip()
|
|
1636
|
+
subject_yaml = _yaml_inline_scalar(subject)
|
|
1637
|
+
prompt_path = _admin_refresh_successor_prompt_path(after_pr, ws=workspace)
|
|
1638
|
+
|
|
1639
|
+
touched: list[str] = []
|
|
1640
|
+
successor_prompt_pattern = re.compile(r"post-pr\d+-successor-chat-handoff\.md")
|
|
1641
|
+
successor_instruction_pattern = re.compile(
|
|
1642
|
+
r"Start the next chat from the fresh post-PR\d+ successor handoff"
|
|
1643
|
+
)
|
|
1644
|
+
verify_instruction_pattern = re.compile(
|
|
1645
|
+
r"confirm the post-PR\d+ operational handoff refresh"
|
|
1646
|
+
)
|
|
1647
|
+
|
|
1648
|
+
for file_path in (workspace.handoff_state_path(), workspace.operational_handoff_state_path()):
|
|
1649
|
+
file_name = _workspace_path_text(workspace, file_path)
|
|
1650
|
+
if not file_path.exists():
|
|
1651
|
+
continue
|
|
1652
|
+
current = file_path.read_text(encoding="utf-8")
|
|
1653
|
+
updated = current
|
|
1654
|
+
if file_path == workspace.handoff_state_path():
|
|
1655
|
+
updated = re.sub(r"^(\s*commit:\s*)[0-9a-f]{7,40}\s*$", rf"\g<1>{short}", updated, count=1, flags=re.MULTILINE)
|
|
1656
|
+
updated = re.sub(r"^(\s*current_head:\s*)[0-9a-f]{7,40}\s*$", rf"\g<1>{short}", updated, count=1, flags=re.MULTILINE)
|
|
1657
|
+
updated = re.sub(r"^(\s*commit_subject:\s*).*$", rf"\g<1>{subject_yaml}", updated, count=1, flags=re.MULTILINE)
|
|
1658
|
+
updated = re.sub(r"^(\s*current_head_subject:\s*).*$", rf"\g<1>{subject_yaml}", updated, count=1, flags=re.MULTILINE)
|
|
1659
|
+
updated = re.sub(
|
|
1660
|
+
r"^(\s*latest_successor_prompt:\s*).*$",
|
|
1661
|
+
rf"\g<1>{prompt_path}",
|
|
1662
|
+
updated,
|
|
1663
|
+
flags=re.MULTILINE,
|
|
1664
|
+
)
|
|
1665
|
+
updated = successor_prompt_pattern.sub(
|
|
1666
|
+
f"post-pr{after_pr}-successor-chat-handoff.md",
|
|
1667
|
+
updated,
|
|
1668
|
+
count=4,
|
|
1669
|
+
)
|
|
1670
|
+
updated = successor_instruction_pattern.sub(
|
|
1671
|
+
f"Start the next chat from the fresh post-PR{after_pr} successor handoff",
|
|
1672
|
+
updated,
|
|
1673
|
+
)
|
|
1674
|
+
updated = verify_instruction_pattern.sub(
|
|
1675
|
+
f"confirm the post-PR{after_pr} operational handoff refresh",
|
|
1676
|
+
updated,
|
|
1677
|
+
)
|
|
1678
|
+
updated = re.sub(r"main at [0-9a-f]{7,40}", f"main at {short}", updated)
|
|
1679
|
+
if file_path == workspace.operational_handoff_state_path():
|
|
1680
|
+
updated = re.sub(r"^(\s*full:\s*)[0-9a-f]{40}\s*$", rf"\g<1>{full}", updated, flags=re.MULTILINE)
|
|
1681
|
+
updated = re.sub(r"^(\s*short:\s*)[0-9a-f]{7,40}\s*$", rf"\g<1>{short}", updated, flags=re.MULTILINE)
|
|
1682
|
+
updated = re.sub(r"^(\s*subject:\s*).*$", rf"\g<1>{subject_yaml}", updated, flags=re.MULTILINE)
|
|
1683
|
+
if updated != current:
|
|
1684
|
+
file_path.write_text(updated, encoding="utf-8")
|
|
1685
|
+
touched.append(file_name)
|
|
1686
|
+
|
|
1687
|
+
marker = (
|
|
1688
|
+
f"\n## Operational documentation refresh state after PR #{after_pr}\n\n"
|
|
1689
|
+
f"Current administrative handoff refresh state is `{short}` (`{subject}`). "
|
|
1690
|
+
f"Continue next only after this post-PR{after_pr} refresh is committed and merged; "
|
|
1691
|
+
"the next substantive slice must be created from fresh main.\n"
|
|
1692
|
+
)
|
|
1693
|
+
operational_refresh_marker_pattern = re.compile(
|
|
1694
|
+
r"\n## Operational documentation refresh state after PR #\d+\n\n"
|
|
1695
|
+
r"Current administrative handoff refresh state is `[^`]+` \(`[^`]*`\)\. "
|
|
1696
|
+
r"Continue next only after this post-PR\d+ refresh is committed and merged; "
|
|
1697
|
+
r"the next substantive slice must be created from fresh main\.\n?",
|
|
1698
|
+
flags=re.MULTILINE,
|
|
1699
|
+
)
|
|
1700
|
+
for file_path in (
|
|
1701
|
+
workspace.status_path(),
|
|
1702
|
+
workspace.handoff_file("CURRENT_HANDOFF.md"),
|
|
1703
|
+
workspace.handoff_file("START_NEW_CHAT_PROMPT.md"),
|
|
1704
|
+
):
|
|
1705
|
+
file_name = _workspace_path_text(workspace, file_path)
|
|
1706
|
+
if not file_path.exists():
|
|
1707
|
+
continue
|
|
1708
|
+
current = file_path.read_text(encoding="utf-8").replace("\\n", "\n")
|
|
1709
|
+
if file_path == workspace.status_path():
|
|
1710
|
+
current = _refresh_status_current_state_block(
|
|
1711
|
+
current,
|
|
1712
|
+
after_pr=after_pr,
|
|
1713
|
+
short=short,
|
|
1714
|
+
subject=subject,
|
|
1715
|
+
)
|
|
1716
|
+
refreshed = operational_refresh_marker_pattern.sub("", current).rstrip() + marker
|
|
1717
|
+
elif file_path == workspace.handoff_file("CURRENT_HANDOFF.md"):
|
|
1718
|
+
lifecycle = evaluate_current_handoff_lifecycle(
|
|
1719
|
+
workspace.root,
|
|
1720
|
+
target_path=file_path,
|
|
1721
|
+
acceptance_state_path=workspace.dpa_current_handoff_acceptance_state_path(),
|
|
1722
|
+
validation_ref=full,
|
|
1723
|
+
execute=True,
|
|
1724
|
+
initialize_acceptance=True,
|
|
1725
|
+
require_dp2_authorized=(workspace.root / DEFAULT_READINESS_PATH).exists(),
|
|
1726
|
+
)
|
|
1727
|
+
if not lifecycle.ok:
|
|
1728
|
+
return subprocess.CompletedProcess(
|
|
1729
|
+
command,
|
|
1730
|
+
2,
|
|
1731
|
+
render_current_handoff_lifecycle_result(lifecycle),
|
|
1732
|
+
"",
|
|
1733
|
+
)
|
|
1734
|
+
refreshed = file_path.read_text(encoding="utf-8")
|
|
1735
|
+
current = refreshed
|
|
1736
|
+
else:
|
|
1737
|
+
refreshed = operational_refresh_marker_pattern.sub("", current).rstrip() + marker
|
|
1738
|
+
if file_path == workspace.handoff_file("START_NEW_CHAT_PROMPT.md"):
|
|
1739
|
+
refreshed = ensure_command_reference_in_prompt(refreshed, workspace.root)
|
|
1740
|
+
if refreshed != current:
|
|
1741
|
+
file_path.write_text(refreshed, encoding="utf-8")
|
|
1742
|
+
touched.append(file_name)
|
|
1743
|
+
if file_path == workspace.handoff_file("CURRENT_HANDOFF.md") and file_name not in touched:
|
|
1744
|
+
touched.append(file_name)
|
|
1745
|
+
if file_path == workspace.handoff_file("CURRENT_HANDOFF.md"):
|
|
1746
|
+
acceptance_name = _workspace_path_text(workspace, workspace.dpa_current_handoff_acceptance_state_path())
|
|
1747
|
+
if acceptance_name not in touched:
|
|
1748
|
+
touched.append(acceptance_name)
|
|
1749
|
+
|
|
1750
|
+
package_refresh = _run([_agentic_kit_command(), "transfer", "prepare-successor-handoff", "--render-prompt"])
|
|
1751
|
+
if package_refresh.returncode != 0:
|
|
1752
|
+
return subprocess.CompletedProcess(
|
|
1753
|
+
command,
|
|
1754
|
+
package_refresh.returncode,
|
|
1755
|
+
package_refresh.stdout,
|
|
1756
|
+
package_refresh.stderr,
|
|
1757
|
+
)
|
|
1758
|
+
|
|
1759
|
+
for package_path in (
|
|
1760
|
+
workspace.package_file("execution_contract.json"),
|
|
1761
|
+
workspace.package_file("source_manifest.json"),
|
|
1762
|
+
workspace.package_file("successor_context.yaml"),
|
|
1763
|
+
workspace.package_file("successor_prompt.md"),
|
|
1764
|
+
workspace.package_file("validation_report.json"),
|
|
1765
|
+
):
|
|
1766
|
+
package_name = _workspace_path_text(workspace, package_path)
|
|
1767
|
+
if package_path.exists() and package_name not in touched:
|
|
1768
|
+
touched.append(package_name)
|
|
1769
|
+
|
|
1770
|
+
boot = _run([_agentic_kit_command(), "boot", "write"])
|
|
1771
|
+
if boot.returncode != 0:
|
|
1772
|
+
return subprocess.CompletedProcess(command, boot.returncode, boot.stdout, boot.stderr)
|
|
1773
|
+
bootstrap_path = workspace.handoff_file("NEXT_CHAT_BOOTSTRAP.md")
|
|
1774
|
+
bootstrap_name = _workspace_path_text(workspace, bootstrap_path)
|
|
1775
|
+
if bootstrap_path.exists() and bootstrap_name not in touched:
|
|
1776
|
+
touched.append(bootstrap_name)
|
|
1777
|
+
|
|
1778
|
+
prompt = _run(
|
|
1779
|
+
[
|
|
1780
|
+
_agentic_kit_command(),
|
|
1781
|
+
"handoff",
|
|
1782
|
+
"prompt",
|
|
1783
|
+
"--intended-output",
|
|
1784
|
+
prompt_path,
|
|
1785
|
+
]
|
|
1786
|
+
)
|
|
1787
|
+
if prompt.returncode != 0:
|
|
1788
|
+
return subprocess.CompletedProcess(command, prompt.returncode, prompt.stdout, prompt.stderr)
|
|
1789
|
+
|
|
1790
|
+
prompt_file = workspace.post_pr_successor_chat_handoff_path(after_pr)
|
|
1791
|
+
prompt_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1792
|
+
prompt_file.write_text(prompt.stdout, encoding="utf-8")
|
|
1793
|
+
touched.append(prompt_path)
|
|
1794
|
+
|
|
1795
|
+
freshness = _run([_agentic_kit_command(), "handoff", "post-merge-refresh-status"])
|
|
1796
|
+
if freshness.returncode != 0:
|
|
1797
|
+
return subprocess.CompletedProcess(
|
|
1798
|
+
command,
|
|
1799
|
+
freshness.returncode,
|
|
1800
|
+
"Updated operational handoff docs before freshness check:\\n"
|
|
1801
|
+
+ "\\n".join(f"- {item}" for item in touched)
|
|
1802
|
+
+ "\\n\\n"
|
|
1803
|
+
+ freshness.stdout,
|
|
1804
|
+
freshness.stderr,
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
return subprocess.CompletedProcess(
|
|
1808
|
+
command,
|
|
1809
|
+
0,
|
|
1810
|
+
"Updated operational handoff docs:\\n" + "\\n".join(f"- {item}" for item in touched) + "\\n",
|
|
1811
|
+
"",
|
|
1812
|
+
)
|
|
1813
|
+
except Exception as exc:
|
|
1814
|
+
return subprocess.CompletedProcess(command, 2, "", f"{type(exc).__name__}: {exc}\\n")
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def _is_refresh_only_pr(after_pr: int, *, ws: Workspace | None = None) -> bool:
|
|
1818
|
+
"""Return true when the merged PR is itself an administrative handoff refresh.
|
|
1819
|
+
|
|
1820
|
+
Refresh-only PRs are already the administrative follow-up for an earlier
|
|
1821
|
+
merge. Running another admin refresh after them creates an infinite
|
|
1822
|
+
post-pr<N>-handoff-refresh treadmill.
|
|
1823
|
+
"""
|
|
1824
|
+
|
|
1825
|
+
workspace = ws or load_workspace(Path("."))
|
|
1826
|
+
completed = _run(
|
|
1827
|
+
[
|
|
1828
|
+
"gh",
|
|
1829
|
+
"pr",
|
|
1830
|
+
"view",
|
|
1831
|
+
str(after_pr),
|
|
1832
|
+
"--json",
|
|
1833
|
+
"title,headRefName",
|
|
1834
|
+
"--jq",
|
|
1835
|
+
".title + \"\\n\" + .headRefName",
|
|
1836
|
+
]
|
|
1837
|
+
)
|
|
1838
|
+
if completed.returncode != 0:
|
|
1839
|
+
return False
|
|
1840
|
+
|
|
1841
|
+
lines = completed.stdout.splitlines()
|
|
1842
|
+
title = lines[0].strip() if lines else ""
|
|
1843
|
+
head_ref = lines[1].strip() if len(lines) > 1 else ""
|
|
1844
|
+
|
|
1845
|
+
return (
|
|
1846
|
+
(
|
|
1847
|
+
title.startswith("Refresh successor handoff after PR")
|
|
1848
|
+
or title.startswith("Refresh handoff state after PR")
|
|
1849
|
+
)
|
|
1850
|
+
and head_ref.startswith(workspace.admin_refresh_branch_prefix())
|
|
1851
|
+
and head_ref.endswith("-handoff-refresh")
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
def admin_refresh_pr(after_pr: int, *, main_branch: str = "main") -> RepoActionResult:
|
|
1856
|
+
ws = load_workspace(Path("."))
|
|
1857
|
+
if _is_refresh_only_pr(after_pr, ws=ws):
|
|
1858
|
+
completed = subprocess.CompletedProcess(
|
|
1859
|
+
["admin-refresh-pr", "--after-pr", str(after_pr)],
|
|
1860
|
+
0,
|
|
1861
|
+
f"NOOP: PR #{after_pr} is already a refresh-only handoff PR; skipping chained admin refresh.\\n",
|
|
1862
|
+
"",
|
|
1863
|
+
)
|
|
1864
|
+
return _result(
|
|
1865
|
+
"admin-refresh-pr",
|
|
1866
|
+
completed.args,
|
|
1867
|
+
completed,
|
|
1868
|
+
"admin_refresh_skipped_refresh_only_pr",
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
with workspace_mutation_lock(Path("."), "admin_refresh_pr"):
|
|
1872
|
+
return _admin_refresh_pr_unlocked(after_pr, main_branch=main_branch, ws=ws)
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
def _admin_refresh_pr_unlocked(
|
|
1876
|
+
after_pr: int,
|
|
1877
|
+
*,
|
|
1878
|
+
main_branch: str = "main",
|
|
1879
|
+
ws: Workspace,
|
|
1880
|
+
) -> RepoActionResult:
|
|
1881
|
+
monitor = guard_branch(
|
|
1882
|
+
command_kind="admin-refresh-pr",
|
|
1883
|
+
required_branch=main_branch,
|
|
1884
|
+
allow_main_mutation=True,
|
|
1885
|
+
auto_switch=True,
|
|
1886
|
+
)
|
|
1887
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
1888
|
+
return _monitor_block_result(
|
|
1889
|
+
action="admin-refresh-pr",
|
|
1890
|
+
command_kind="admin-refresh-pr",
|
|
1891
|
+
required_branch=main_branch,
|
|
1892
|
+
monitor=monitor,
|
|
1893
|
+
next_action="Inspect transfer operation monitor block before admin refresh.",
|
|
1894
|
+
)
|
|
1895
|
+
|
|
1896
|
+
status_command = ["git", "status", "--short"]
|
|
1897
|
+
status_completed = _run(status_command)
|
|
1898
|
+
if status_completed.returncode != 0:
|
|
1899
|
+
return _result("admin-refresh-pr", status_command, status_completed, "Inspect worktree status.")
|
|
1900
|
+
if status_completed.stdout.strip():
|
|
1901
|
+
completed = subprocess.CompletedProcess(
|
|
1902
|
+
status_command,
|
|
1903
|
+
2,
|
|
1904
|
+
status_completed.stdout,
|
|
1905
|
+
"Refusing admin refresh with dirty worktree. Commit, clean, or inspect first.\n",
|
|
1906
|
+
)
|
|
1907
|
+
return _result("admin-refresh-pr", completed.args, completed, "Start admin refresh from a clean main worktree.")
|
|
1908
|
+
|
|
1909
|
+
preflight = _remote_mutation_preflight(
|
|
1910
|
+
action="admin-refresh-pr",
|
|
1911
|
+
mutation="admin-refresh-pr",
|
|
1912
|
+
)
|
|
1913
|
+
if preflight is not None:
|
|
1914
|
+
return preflight
|
|
1915
|
+
|
|
1916
|
+
refresh_branch = ws.admin_refresh_branch(after_pr)
|
|
1917
|
+
transcript: list[str] = []
|
|
1918
|
+
|
|
1919
|
+
branch_create_step = ["git", "switch", "-c", refresh_branch, main_branch]
|
|
1920
|
+
completed = _run(branch_create_step)
|
|
1921
|
+
transcript.append(f"$ {' '.join(branch_create_step)}\n{completed.stdout}{completed.stderr}")
|
|
1922
|
+
if completed.returncode != 0:
|
|
1923
|
+
combined = f"{completed.stdout}{completed.stderr}"
|
|
1924
|
+
if "already exists" in combined:
|
|
1925
|
+
existing = _existing_admin_refresh_pr(refresh_branch)
|
|
1926
|
+
if existing is not None:
|
|
1927
|
+
return existing
|
|
1928
|
+
switch_step = ["git", "switch", refresh_branch]
|
|
1929
|
+
switched = _run(switch_step)
|
|
1930
|
+
transcript.append(f"$ {' '.join(switch_step)}\n{switched.stdout}{switched.stderr}")
|
|
1931
|
+
if switched.returncode != 0:
|
|
1932
|
+
return _result(
|
|
1933
|
+
"admin-refresh-pr",
|
|
1934
|
+
switch_step,
|
|
1935
|
+
switched,
|
|
1936
|
+
"Inspect existing admin refresh branch before continuing.",
|
|
1937
|
+
)
|
|
1938
|
+
reset_step = ["git", "reset", "--hard", main_branch]
|
|
1939
|
+
reset = _run(reset_step)
|
|
1940
|
+
transcript.append(f"$ {' '.join(reset_step)}\n{reset.stdout}{reset.stderr}")
|
|
1941
|
+
if reset.returncode != 0:
|
|
1942
|
+
return _result(
|
|
1943
|
+
"admin-refresh-pr",
|
|
1944
|
+
reset_step,
|
|
1945
|
+
reset,
|
|
1946
|
+
"Inspect existing admin refresh branch reset before continuing.",
|
|
1947
|
+
)
|
|
1948
|
+
else:
|
|
1949
|
+
return _result(
|
|
1950
|
+
"admin-refresh-pr",
|
|
1951
|
+
branch_create_step,
|
|
1952
|
+
completed,
|
|
1953
|
+
"Inspect admin refresh branch state or existing PR before continuing.",
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1956
|
+
steps = [
|
|
1957
|
+
["admin-refresh-operational-handoff-docs", "--after-pr", str(after_pr)],
|
|
1958
|
+
[_agentic_kit_command(), "handoff", "check"],
|
|
1959
|
+
[_agentic_kit_command(), "handoff", "post-merge-refresh-status"],
|
|
1960
|
+
[_agentic_kit_command(), "transfer", "protected-diff-plan", "--label", f"post-pr{after_pr}-handoff-refresh"],
|
|
1961
|
+
["git", "status", "--short"],
|
|
1962
|
+
]
|
|
1963
|
+
|
|
1964
|
+
for step in steps:
|
|
1965
|
+
if step[0] == "admin-refresh-operational-handoff-docs":
|
|
1966
|
+
completed = _refresh_operational_handoff_docs(after_pr, ws=ws)
|
|
1967
|
+
else:
|
|
1968
|
+
completed = _run(step)
|
|
1969
|
+
transcript.append(f"$ {' '.join(step)}\n{completed.stdout}{completed.stderr}")
|
|
1970
|
+
if completed.returncode != 0:
|
|
1971
|
+
return _result("admin-refresh-pr", step, completed, "Inspect admin refresh step failure before continuing.")
|
|
1972
|
+
|
|
1973
|
+
final_status = _run(["git", "status", "--short"])
|
|
1974
|
+
changed = tuple(line.strip() for line in final_status.stdout.splitlines() if line.strip())
|
|
1975
|
+
admin_refresh_paths = _admin_refresh_paths(ws)
|
|
1976
|
+
allowed = set(tuple(f"M {path}" for path in admin_refresh_paths) + (
|
|
1977
|
+
f"?? {_admin_refresh_successor_prompt_path(after_pr, ws=ws)}",
|
|
1978
|
+
))
|
|
1979
|
+
changed_set = set(changed)
|
|
1980
|
+
unexpected = sorted(changed_set - allowed)
|
|
1981
|
+
if not changed_set or unexpected:
|
|
1982
|
+
completed = subprocess.CompletedProcess(
|
|
1983
|
+
["git", "status", "--short"],
|
|
1984
|
+
2,
|
|
1985
|
+
final_status.stdout,
|
|
1986
|
+
"Admin refresh must change a non-empty subset of generated administrative handoff refresh paths "
|
|
1987
|
+
"and no unexpected paths.\n",
|
|
1988
|
+
)
|
|
1989
|
+
return _result("admin-refresh-pr", completed.args, completed, "Inspect unexpected admin refresh diff before committing.")
|
|
1990
|
+
|
|
1991
|
+
commit_message = f"Refresh handoff state after PR{after_pr}"
|
|
1992
|
+
commit_result = commit_paths(
|
|
1993
|
+
commit_message,
|
|
1994
|
+
[*admin_refresh_paths, _admin_refresh_successor_prompt_path(after_pr, ws=ws)],
|
|
1995
|
+
required_branch=refresh_branch,
|
|
1996
|
+
)
|
|
1997
|
+
transcript.append(
|
|
1998
|
+
f"$ transfer commit --message {commit_message!r} --path <admin-refresh-paths>\n"
|
|
1999
|
+
f"{commit_result.stdout}{commit_result.stderr}"
|
|
2000
|
+
)
|
|
2001
|
+
if commit_result.returncode != 0:
|
|
2002
|
+
completed = subprocess.CompletedProcess(
|
|
2003
|
+
commit_result.command,
|
|
2004
|
+
commit_result.returncode,
|
|
2005
|
+
commit_result.stdout,
|
|
2006
|
+
commit_result.stderr,
|
|
2007
|
+
)
|
|
2008
|
+
return _result("admin-refresh-pr", commit_result.command, completed, "Inspect admin refresh commit failure before continuing.")
|
|
2009
|
+
|
|
2010
|
+
push_result = push_current(required_branch=refresh_branch)
|
|
2011
|
+
transcript.append(
|
|
2012
|
+
f"$ transfer push-current --branch {refresh_branch}\n"
|
|
2013
|
+
f"{push_result.stdout}{push_result.stderr}"
|
|
2014
|
+
)
|
|
2015
|
+
if push_result.returncode != 0:
|
|
2016
|
+
completed = subprocess.CompletedProcess(
|
|
2017
|
+
push_result.command,
|
|
2018
|
+
push_result.returncode,
|
|
2019
|
+
push_result.stdout,
|
|
2020
|
+
push_result.stderr,
|
|
2021
|
+
)
|
|
2022
|
+
return _result("admin-refresh-pr", push_result.command, completed, "Inspect admin refresh push failure before continuing.")
|
|
2023
|
+
|
|
2024
|
+
pr_body = f"Administrative operational handoff refresh after PR{after_pr}. No product-code changes."
|
|
2025
|
+
pr_result = pr_create(
|
|
2026
|
+
base=main_branch,
|
|
2027
|
+
head=refresh_branch,
|
|
2028
|
+
title=commit_message,
|
|
2029
|
+
body=pr_body,
|
|
2030
|
+
)
|
|
2031
|
+
transcript.append(
|
|
2032
|
+
f"$ transfer pr-create --base {main_branch} --head {refresh_branch}\n"
|
|
2033
|
+
f"{pr_result.stdout}{pr_result.stderr}"
|
|
2034
|
+
)
|
|
2035
|
+
if pr_result.returncode != 0:
|
|
2036
|
+
completed = subprocess.CompletedProcess(
|
|
2037
|
+
pr_result.command,
|
|
2038
|
+
pr_result.returncode,
|
|
2039
|
+
pr_result.stdout,
|
|
2040
|
+
pr_result.stderr,
|
|
2041
|
+
)
|
|
2042
|
+
return _result("admin-refresh-pr", pr_result.command, completed, "Inspect admin refresh PR creation failure.")
|
|
2043
|
+
|
|
2044
|
+
completed = subprocess.CompletedProcess(
|
|
2045
|
+
["agentic-kit", "transfer", "admin-refresh-pr", "--after-pr", str(after_pr)],
|
|
2046
|
+
0,
|
|
2047
|
+
"\n".join(transcript),
|
|
2048
|
+
"",
|
|
2049
|
+
)
|
|
2050
|
+
return _result("admin-refresh-pr", completed.args, completed, "Run transfer pr-status on the created admin refresh PR.")
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
def _successor_package_refresh_branch(after_pr: int, *, ws: Workspace) -> str:
|
|
2054
|
+
return f"{ws.admin_refresh_branch_prefix()}{after_pr}-successor-package-refresh"
|
|
2055
|
+
|
|
2056
|
+
|
|
2057
|
+
def successor_package_refresh_pr(after_pr: int, *, main_branch: str = "main") -> RepoActionResult:
|
|
2058
|
+
"""Create a generated successor-handoff-package refresh PR from clean main."""
|
|
2059
|
+
|
|
2060
|
+
ws = load_workspace(Path("."))
|
|
2061
|
+
with workspace_mutation_lock(Path("."), "successor_package_refresh_pr"):
|
|
2062
|
+
monitor = guard_branch(
|
|
2063
|
+
command_kind="successor-package-refresh-pr",
|
|
2064
|
+
required_branch=main_branch,
|
|
2065
|
+
allow_main_mutation=True,
|
|
2066
|
+
auto_switch=True,
|
|
2067
|
+
)
|
|
2068
|
+
if monitor.decision == MonitorDecision.BLOCK:
|
|
2069
|
+
return _monitor_block_result(
|
|
2070
|
+
action="successor-package-refresh-pr",
|
|
2071
|
+
command_kind="successor-package-refresh-pr",
|
|
2072
|
+
required_branch=main_branch,
|
|
2073
|
+
monitor=monitor,
|
|
2074
|
+
next_action="Inspect transfer operation monitor block before successor package refresh.",
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
status_command = ["git", "status", "--short"]
|
|
2078
|
+
status_completed = _run(status_command)
|
|
2079
|
+
if status_completed.returncode != 0:
|
|
2080
|
+
return _result(
|
|
2081
|
+
"successor-package-refresh-pr",
|
|
2082
|
+
status_command,
|
|
2083
|
+
status_completed,
|
|
2084
|
+
"Inspect worktree status.",
|
|
2085
|
+
)
|
|
2086
|
+
if status_completed.stdout.strip():
|
|
2087
|
+
completed = subprocess.CompletedProcess(
|
|
2088
|
+
status_command,
|
|
2089
|
+
2,
|
|
2090
|
+
status_completed.stdout,
|
|
2091
|
+
"Refusing successor package refresh with dirty worktree. Commit, clean, or inspect first.\n",
|
|
2092
|
+
)
|
|
2093
|
+
return _result(
|
|
2094
|
+
"successor-package-refresh-pr",
|
|
2095
|
+
completed.args,
|
|
2096
|
+
completed,
|
|
2097
|
+
"Start successor package refresh from a clean main worktree.",
|
|
2098
|
+
)
|
|
2099
|
+
|
|
2100
|
+
preflight = _remote_mutation_preflight(
|
|
2101
|
+
action="successor-package-refresh-pr",
|
|
2102
|
+
mutation="successor-package-refresh-pr",
|
|
2103
|
+
)
|
|
2104
|
+
if preflight is not None:
|
|
2105
|
+
return preflight
|
|
2106
|
+
|
|
2107
|
+
refresh_branch = _successor_package_refresh_branch(after_pr, ws=ws)
|
|
2108
|
+
transcript: list[str] = []
|
|
2109
|
+
|
|
2110
|
+
branch_create_step = ["git", "switch", "-c", refresh_branch, main_branch]
|
|
2111
|
+
completed = _run(branch_create_step)
|
|
2112
|
+
transcript.append(f"$ {' '.join(branch_create_step)}\n{completed.stdout}{completed.stderr}")
|
|
2113
|
+
if completed.returncode != 0:
|
|
2114
|
+
combined = f"{completed.stdout}{completed.stderr}"
|
|
2115
|
+
if "already exists" in combined:
|
|
2116
|
+
existing = _existing_successor_package_refresh_pr(refresh_branch)
|
|
2117
|
+
if existing is not None:
|
|
2118
|
+
return existing
|
|
2119
|
+
return _result(
|
|
2120
|
+
"successor-package-refresh-pr",
|
|
2121
|
+
branch_create_step,
|
|
2122
|
+
completed,
|
|
2123
|
+
"Resolve existing successor package refresh branch before continuing.",
|
|
2124
|
+
)
|
|
2125
|
+
return _result(
|
|
2126
|
+
"successor-package-refresh-pr",
|
|
2127
|
+
branch_create_step,
|
|
2128
|
+
completed,
|
|
2129
|
+
"Inspect successor package refresh branch state before continuing.",
|
|
2130
|
+
)
|
|
2131
|
+
|
|
2132
|
+
refresh_command = [
|
|
2133
|
+
_agentic_kit_command(),
|
|
2134
|
+
"transfer",
|
|
2135
|
+
"chat-switch-complete",
|
|
2136
|
+
"--render-prompt",
|
|
2137
|
+
"--json",
|
|
2138
|
+
]
|
|
2139
|
+
refreshed = _run(refresh_command)
|
|
2140
|
+
transcript.append(f"$ {' '.join(refresh_command)}\n{refreshed.stdout}{refreshed.stderr}")
|
|
2141
|
+
if refreshed.returncode != 0:
|
|
2142
|
+
return _result(
|
|
2143
|
+
"successor-package-refresh-pr",
|
|
2144
|
+
refresh_command,
|
|
2145
|
+
refreshed,
|
|
2146
|
+
"Inspect successor package generation failure before continuing.",
|
|
2147
|
+
)
|
|
2148
|
+
|
|
2149
|
+
final_status = _run(["git", "status", "--short"])
|
|
2150
|
+
changed = tuple(line.strip() for line in final_status.stdout.splitlines() if line.strip())
|
|
2151
|
+
allowed_paths = _successor_package_refresh_paths(ws)
|
|
2152
|
+
allowed = {f"M {path}" for path in allowed_paths} | {f"?? {path}" for path in allowed_paths}
|
|
2153
|
+
changed_set = set(changed)
|
|
2154
|
+
unexpected = sorted(changed_set - allowed)
|
|
2155
|
+
if not changed_set or unexpected:
|
|
2156
|
+
completed = subprocess.CompletedProcess(
|
|
2157
|
+
["git", "status", "--short"],
|
|
2158
|
+
2,
|
|
2159
|
+
final_status.stdout,
|
|
2160
|
+
"Successor package refresh must change a non-empty subset of generated successor package paths "
|
|
2161
|
+
"and no unexpected paths.\n",
|
|
2162
|
+
)
|
|
2163
|
+
return _result(
|
|
2164
|
+
"successor-package-refresh-pr",
|
|
2165
|
+
completed.args,
|
|
2166
|
+
completed,
|
|
2167
|
+
"Inspect unexpected successor package refresh diff before committing.",
|
|
2168
|
+
)
|
|
2169
|
+
|
|
2170
|
+
commit_message = f"Refresh successor package after PR{after_pr}"
|
|
2171
|
+
commit_result = commit_paths(
|
|
2172
|
+
commit_message,
|
|
2173
|
+
list(allowed_paths),
|
|
2174
|
+
required_branch=refresh_branch,
|
|
2175
|
+
)
|
|
2176
|
+
transcript.append(
|
|
2177
|
+
f"$ transfer commit --message {commit_message!r} --path <successor-package-refresh-paths>\n"
|
|
2178
|
+
f"{commit_result.stdout}{commit_result.stderr}"
|
|
2179
|
+
)
|
|
2180
|
+
if commit_result.returncode != 0:
|
|
2181
|
+
completed = subprocess.CompletedProcess(
|
|
2182
|
+
commit_result.command,
|
|
2183
|
+
commit_result.returncode,
|
|
2184
|
+
commit_result.stdout,
|
|
2185
|
+
commit_result.stderr,
|
|
2186
|
+
)
|
|
2187
|
+
return _result(
|
|
2188
|
+
"successor-package-refresh-pr",
|
|
2189
|
+
commit_result.command,
|
|
2190
|
+
completed,
|
|
2191
|
+
"Inspect successor package refresh commit failure before continuing.",
|
|
2192
|
+
)
|
|
2193
|
+
|
|
2194
|
+
push_result = push_current(required_branch=refresh_branch)
|
|
2195
|
+
transcript.append(
|
|
2196
|
+
f"$ transfer push-current --branch {refresh_branch}\n"
|
|
2197
|
+
f"{push_result.stdout}{push_result.stderr}"
|
|
2198
|
+
)
|
|
2199
|
+
if push_result.returncode != 0:
|
|
2200
|
+
completed = subprocess.CompletedProcess(
|
|
2201
|
+
push_result.command,
|
|
2202
|
+
push_result.returncode,
|
|
2203
|
+
push_result.stdout,
|
|
2204
|
+
push_result.stderr,
|
|
2205
|
+
)
|
|
2206
|
+
return _result(
|
|
2207
|
+
"successor-package-refresh-pr",
|
|
2208
|
+
push_result.command,
|
|
2209
|
+
completed,
|
|
2210
|
+
"Inspect successor package refresh push failure before continuing.",
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
pr_body = f"Generated successor handoff package refresh after PR{after_pr}. No product-code changes."
|
|
2214
|
+
pr_result = pr_create(
|
|
2215
|
+
base=main_branch,
|
|
2216
|
+
head=refresh_branch,
|
|
2217
|
+
title=commit_message,
|
|
2218
|
+
body=pr_body,
|
|
2219
|
+
)
|
|
2220
|
+
transcript.append(
|
|
2221
|
+
f"$ transfer pr-create --base {main_branch} --head {refresh_branch}\n"
|
|
2222
|
+
f"{pr_result.stdout}{pr_result.stderr}"
|
|
2223
|
+
)
|
|
2224
|
+
if pr_result.returncode != 0:
|
|
2225
|
+
completed = subprocess.CompletedProcess(
|
|
2226
|
+
pr_result.command,
|
|
2227
|
+
pr_result.returncode,
|
|
2228
|
+
pr_result.stdout,
|
|
2229
|
+
pr_result.stderr,
|
|
2230
|
+
)
|
|
2231
|
+
return _result(
|
|
2232
|
+
"successor-package-refresh-pr",
|
|
2233
|
+
pr_result.command,
|
|
2234
|
+
completed,
|
|
2235
|
+
"Inspect successor package refresh PR creation failure.",
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2238
|
+
completed = subprocess.CompletedProcess(
|
|
2239
|
+
["agentic-kit", "transfer", "successor-package-refresh-pr", "--after-pr", str(after_pr)],
|
|
2240
|
+
0,
|
|
2241
|
+
"\n".join(transcript),
|
|
2242
|
+
"",
|
|
2243
|
+
)
|
|
2244
|
+
return _result(
|
|
2245
|
+
"successor-package-refresh-pr",
|
|
2246
|
+
completed.args,
|
|
2247
|
+
completed,
|
|
2248
|
+
"Run transfer pr-status on the created successor package refresh PR.",
|
|
2249
|
+
)
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
def result_json(result: RepoActionResult) -> str:
|
|
2253
|
+
return json.dumps(result.as_json_data(), indent=2, sort_keys=True)
|