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.
Files changed (310) hide show
  1. agentic_project_kit/__init__.py +1 -0
  2. agentic_project_kit/absolute_path_portability_audit.py +241 -0
  3. agentic_project_kit/access_level_policy.py +14 -0
  4. agentic_project_kit/access_levels.py +22 -0
  5. agentic_project_kit/action_registry.py +195 -0
  6. agentic_project_kit/action_specs.py +286 -0
  7. agentic_project_kit/agent_command_runner.py +516 -0
  8. agentic_project_kit/audit_test_matrix.py +36 -0
  9. agentic_project_kit/audit_test_matrix_cli.py +43 -0
  10. agentic_project_kit/chat_bootloader.py +257 -0
  11. agentic_project_kit/chat_entrypoint_contract.py +297 -0
  12. agentic_project_kit/checks.py +511 -0
  13. agentic_project_kit/ci_readiness.py +211 -0
  14. agentic_project_kit/cli.py +147 -0
  15. agentic_project_kit/cli_commands/__init__.py +1 -0
  16. agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
  17. agentic_project_kit/cli_commands/actions.py +21 -0
  18. agentic_project_kit/cli_commands/artifact_gc.py +240 -0
  19. agentic_project_kit/cli_commands/boot.py +55 -0
  20. agentic_project_kit/cli_commands/chat.py +34 -0
  21. agentic_project_kit/cli_commands/checks.py +305 -0
  22. agentic_project_kit/cli_commands/cockpit.py +103 -0
  23. agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
  24. agentic_project_kit/cli_commands/commands.py +129 -0
  25. agentic_project_kit/cli_commands/dev.py +23 -0
  26. agentic_project_kit/cli_commands/direction.py +92 -0
  27. agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
  28. agentic_project_kit/cli_commands/doc_registry.py +126 -0
  29. agentic_project_kit/cli_commands/docs.py +223 -0
  30. agentic_project_kit/cli_commands/dpa.py +1338 -0
  31. agentic_project_kit/cli_commands/evidence.py +251 -0
  32. agentic_project_kit/cli_commands/github.py +34 -0
  33. agentic_project_kit/cli_commands/governance.py +14 -0
  34. agentic_project_kit/cli_commands/gui.py +23 -0
  35. agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
  36. agentic_project_kit/cli_commands/handoff.py +183 -0
  37. agentic_project_kit/cli_commands/human_workflows.py +430 -0
  38. agentic_project_kit/cli_commands/init.py +132 -0
  39. agentic_project_kit/cli_commands/instruction.py +75 -0
  40. agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
  41. agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
  42. agentic_project_kit/cli_commands/pass_already_done.py +66 -0
  43. agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
  44. agentic_project_kit/cli_commands/patch_preflight.py +37 -0
  45. agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
  46. agentic_project_kit/cli_commands/patterns.py +40 -0
  47. agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
  48. agentic_project_kit/cli_commands/pr.py +133 -0
  49. agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
  50. agentic_project_kit/cli_commands/profiles.py +26 -0
  51. agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
  52. agentic_project_kit/cli_commands/project_direction.py +25 -0
  53. agentic_project_kit/cli_commands/release.py +392 -0
  54. agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
  55. agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
  56. agentic_project_kit/cli_commands/remote_next.py +57 -0
  57. agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
  58. agentic_project_kit/cli_commands/rule_registry.py +129 -0
  59. agentic_project_kit/cli_commands/rules.py +222 -0
  60. agentic_project_kit/cli_commands/scaffold.py +46 -0
  61. agentic_project_kit/cli_commands/slice.py +21 -0
  62. agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
  63. agentic_project_kit/cli_commands/state.py +51 -0
  64. agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
  65. agentic_project_kit/cli_commands/todo.py +53 -0
  66. agentic_project_kit/cli_commands/transfer.py +19 -0
  67. agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
  68. agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
  69. agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
  70. agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
  71. agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
  72. agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
  73. agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
  74. agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
  75. agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
  76. agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
  77. agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
  78. agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
  79. agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
  80. agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
  81. agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
  82. agentic_project_kit/cli_commands/transfer_shared.py +274 -0
  83. agentic_project_kit/cli_commands/validation.py +200 -0
  84. agentic_project_kit/cli_commands/version.py +27 -0
  85. agentic_project_kit/cli_commands/work_orders.py +219 -0
  86. agentic_project_kit/cli_commands/workflow.py +471 -0
  87. agentic_project_kit/cli_commands/workflow_guard.py +26 -0
  88. agentic_project_kit/cli_commands/workspace.py +252 -0
  89. agentic_project_kit/cockpit.py +608 -0
  90. agentic_project_kit/cockpit_readiness.py +67 -0
  91. agentic_project_kit/command_authority_audit.py +192 -0
  92. agentic_project_kit/command_inbox_check.py +103 -0
  93. agentic_project_kit/command_manifest.py +591 -0
  94. agentic_project_kit/command_queue_contract.py +161 -0
  95. agentic_project_kit/command_selector.py +245 -0
  96. agentic_project_kit/command_taxonomy.py +264 -0
  97. agentic_project_kit/commit_guard.py +67 -0
  98. agentic_project_kit/communication_artifact_gc.py +495 -0
  99. agentic_project_kit/communication_rule_context.py +437 -0
  100. agentic_project_kit/communication_state.py +115 -0
  101. agentic_project_kit/composed_wrapper_invariants.py +97 -0
  102. agentic_project_kit/composite_short_circuit.py +140 -0
  103. agentic_project_kit/contract.py +251 -0
  104. agentic_project_kit/control_state.py +83 -0
  105. agentic_project_kit/doc_currency_audit.py +536 -0
  106. agentic_project_kit/doc_lifecycle.py +1179 -0
  107. agentic_project_kit/doc_lifecycle_signals.py +446 -0
  108. agentic_project_kit/doc_lifecycle_sweep.py +639 -0
  109. agentic_project_kit/doc_mesh.py +491 -0
  110. agentic_project_kit/doctor.py +379 -0
  111. agentic_project_kit/documentation_registry.py +1122 -0
  112. agentic_project_kit/documentation_system_audit.py +310 -0
  113. agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
  114. agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
  115. agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
  116. agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
  117. agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
  118. agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
  119. agentic_project_kit/dpa_final_closeout.py +627 -0
  120. agentic_project_kit/dpa_fixture_evidence.py +680 -0
  121. agentic_project_kit/dpa_maintainer_record_check.py +711 -0
  122. agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
  123. agentic_project_kit/dpa_probe_002_readiness.py +509 -0
  124. agentic_project_kit/dpa_probe_003_readiness.py +417 -0
  125. agentic_project_kit/dpa_probe_004_readiness.py +437 -0
  126. agentic_project_kit/dpa_readiness.py +505 -0
  127. agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
  128. agentic_project_kit/dpa_registry_contracts.py +312 -0
  129. agentic_project_kit/dpa_renderer_readiness.py +407 -0
  130. agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
  131. agentic_project_kit/dpa_stable_readiness.py +743 -0
  132. agentic_project_kit/dpa_successor_projection.py +168 -0
  133. agentic_project_kit/dpa_workspace_init_projection.py +93 -0
  134. agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
  135. agentic_project_kit/entrypoint_slice_runner.py +153 -0
  136. agentic_project_kit/evidence_clean.py +152 -0
  137. agentic_project_kit/evidence_commit_paths.py +214 -0
  138. agentic_project_kit/evidence_finalize_log.py +204 -0
  139. agentic_project_kit/evidence_guard.py +114 -0
  140. agentic_project_kit/evidence_inspector.py +507 -0
  141. agentic_project_kit/evidence_state_contract.py +99 -0
  142. agentic_project_kit/execution_mode_state.py +148 -0
  143. agentic_project_kit/final_summary_contract.py +85 -0
  144. agentic_project_kit/finalize_guard.py +139 -0
  145. agentic_project_kit/gatekeeper_core.py +232 -0
  146. agentic_project_kit/github.py +25 -0
  147. agentic_project_kit/governance.py +59 -0
  148. agentic_project_kit/gui_action_execution.py +105 -0
  149. agentic_project_kit/gui_action_renderer.py +81 -0
  150. agentic_project_kit/gui_action_run_intent.py +98 -0
  151. agentic_project_kit/gui_action_views.py +195 -0
  152. agentic_project_kit/gui_activity_log.py +76 -0
  153. agentic_project_kit/gui_button_catalog.py +605 -0
  154. agentic_project_kit/gui_cockpit.py +365 -0
  155. agentic_project_kit/gui_cockpit_actions.py +695 -0
  156. agentic_project_kit/gui_cockpit_common.py +172 -0
  157. agentic_project_kit/gui_cockpit_header.py +501 -0
  158. agentic_project_kit/gui_cockpit_sidebar.py +387 -0
  159. agentic_project_kit/gui_cockpit_task.py +748 -0
  160. agentic_project_kit/gui_command_projection.py +355 -0
  161. agentic_project_kit/gui_communication_modes.py +130 -0
  162. agentic_project_kit/gui_dry_run.py +219 -0
  163. agentic_project_kit/gui_gatekeeper_status.py +259 -0
  164. agentic_project_kit/gui_layout_plan.py +148 -0
  165. agentic_project_kit/gui_open_folder.py +18 -0
  166. agentic_project_kit/gui_output_status_panel.py +61 -0
  167. agentic_project_kit/gui_panel_state.py +47 -0
  168. agentic_project_kit/gui_presenter.py +62 -0
  169. agentic_project_kit/gui_readiness_gate.py +206 -0
  170. agentic_project_kit/gui_release_flow.py +59 -0
  171. agentic_project_kit/gui_task_editor.py +1077 -0
  172. agentic_project_kit/gui_tk_widgets.py +177 -0
  173. agentic_project_kit/gui_tkinter_renderer.py +87 -0
  174. agentic_project_kit/gui_tkinter_shell.py +1176 -0
  175. agentic_project_kit/gui_transfer_contract.py +28 -0
  176. agentic_project_kit/gui_viewmodel.py +698 -0
  177. agentic_project_kit/gui_window_guard.py +46 -0
  178. agentic_project_kit/handoff_freshness.py +390 -0
  179. agentic_project_kit/handoff_prompt.py +180 -0
  180. agentic_project_kit/handoff_state.py +254 -0
  181. agentic_project_kit/instruction_lint.py +455 -0
  182. agentic_project_kit/llm_context_carriers.py +66 -0
  183. agentic_project_kit/llm_execution_context.py +285 -0
  184. agentic_project_kit/local_command_stack.py +133 -0
  185. agentic_project_kit/local_feature_gate.py +82 -0
  186. agentic_project_kit/local_garbage_collector.py +269 -0
  187. agentic_project_kit/models.py +18 -0
  188. agentic_project_kit/mutation_lock_audit.py +421 -0
  189. agentic_project_kit/next_turn_evidence.py +253 -0
  190. agentic_project_kit/next_turn_merge_if_green.py +459 -0
  191. agentic_project_kit/next_turn_pr_status.py +255 -0
  192. agentic_project_kit/next_turn_result.py +183 -0
  193. agentic_project_kit/next_turn_runner.py +159 -0
  194. agentic_project_kit/next_turn_slot.py +107 -0
  195. agentic_project_kit/next_turn_status.py +274 -0
  196. agentic_project_kit/ns_legacy_reference_audit.py +258 -0
  197. agentic_project_kit/ns_slice_runner.py +16 -0
  198. agentic_project_kit/operational_handoff_projection.py +124 -0
  199. agentic_project_kit/output_contract.py +80 -0
  200. agentic_project_kit/output_repair.py +61 -0
  201. agentic_project_kit/pass_already_done.py +216 -0
  202. agentic_project_kit/patch_artifact_preflight.py +147 -0
  203. agentic_project_kit/patch_cycle_workflow.py +440 -0
  204. agentic_project_kit/patch_failure_discipline_audit.py +223 -0
  205. agentic_project_kit/patch_preflight.py +215 -0
  206. agentic_project_kit/path_literal_audit.py +612 -0
  207. agentic_project_kit/patterns.py +99 -0
  208. agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
  209. agentic_project_kit/portability_shell_gate.py +40 -0
  210. agentic_project_kit/post_merge_handoff_refresh.py +56 -0
  211. agentic_project_kit/post_release.py +252 -0
  212. agentic_project_kit/post_release_closeout.py +616 -0
  213. agentic_project_kit/pr_cleanup.py +172 -0
  214. agentic_project_kit/pr_closeout.py +50 -0
  215. agentic_project_kit/pr_closeout_complete.py +344 -0
  216. agentic_project_kit/pr_create_or_skip.py +71 -0
  217. agentic_project_kit/pr_hygiene.py +210 -0
  218. agentic_project_kit/program_redundancy_audit.py +251 -0
  219. agentic_project_kit/project_direction.py +840 -0
  220. agentic_project_kit/protected_change_planner.py +194 -0
  221. agentic_project_kit/release.py +401 -0
  222. agentic_project_kit/release_doi_safety.py +24 -0
  223. agentic_project_kit/release_gate_core.py +174 -0
  224. agentic_project_kit/release_metadata_authority_gate.py +328 -0
  225. agentic_project_kit/release_metadata_prep.py +43 -0
  226. agentic_project_kit/release_notes.py +823 -0
  227. agentic_project_kit/release_prep_core.py +204 -0
  228. agentic_project_kit/release_prepare.py +369 -0
  229. agentic_project_kit/release_process_guardrails.py +247 -0
  230. agentic_project_kit/release_publish_core.py +114 -0
  231. agentic_project_kit/release_publish_orchestration.py +469 -0
  232. agentic_project_kit/release_state.py +552 -0
  233. agentic_project_kit/release_verify_core.py +175 -0
  234. agentic_project_kit/remote_branch_hygiene.py +223 -0
  235. agentic_project_kit/remote_next.py +89 -0
  236. agentic_project_kit/remote_next_closeout.py +101 -0
  237. agentic_project_kit/remote_next_order_contract.py +391 -0
  238. agentic_project_kit/removed_ns_commands.py +458 -0
  239. agentic_project_kit/removed_source_audit.py +225 -0
  240. agentic_project_kit/rendering.py +13 -0
  241. agentic_project_kit/repair_report.py +79 -0
  242. agentic_project_kit/repo_identity.py +59 -0
  243. agentic_project_kit/result_report_classifier.py +57 -0
  244. agentic_project_kit/rule_ack.py +105 -0
  245. agentic_project_kit/rule_preservation.py +58 -0
  246. agentic_project_kit/rule_refresh.py +174 -0
  247. agentic_project_kit/rule_registry_registration.py +322 -0
  248. agentic_project_kit/rule_registry_report.py +206 -0
  249. agentic_project_kit/rule_registry_validator.py +704 -0
  250. agentic_project_kit/rule_snapshot.py +98 -0
  251. agentic_project_kit/rule_source_validator.py +133 -0
  252. agentic_project_kit/run_summary.py +68 -0
  253. agentic_project_kit/run_summary_renderer.py +238 -0
  254. agentic_project_kit/runtime_validator.py +68 -0
  255. agentic_project_kit/safe_file_replace.py +35 -0
  256. agentic_project_kit/safe_patch.py +47 -0
  257. agentic_project_kit/safe_push.py +218 -0
  258. agentic_project_kit/safe_remove_diagnostic.py +51 -0
  259. agentic_project_kit/scaffold.py +69 -0
  260. agentic_project_kit/site_claims.py +444 -0
  261. agentic_project_kit/site_generator.py +1140 -0
  262. agentic_project_kit/slice_gate.py +225 -0
  263. agentic_project_kit/slice_runner.py +75 -0
  264. agentic_project_kit/standard_gates_audit_suite.py +167 -0
  265. agentic_project_kit/state_freshness.py +135 -0
  266. agentic_project_kit/status_current_state_audit.py +762 -0
  267. agentic_project_kit/successor_handoff_package.py +1763 -0
  268. agentic_project_kit/templates.py +292 -0
  269. agentic_project_kit/terminal_block_guard.py +166 -0
  270. agentic_project_kit/terminal_logging.py +275 -0
  271. agentic_project_kit/todo.py +97 -0
  272. agentic_project_kit/transfer_closeout.py +136 -0
  273. agentic_project_kit/transfer_continue.py +215 -0
  274. agentic_project_kit/transfer_local_runner.py +101 -0
  275. agentic_project_kit/transfer_operation_monitor.py +236 -0
  276. agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
  277. agentic_project_kit/transfer_post_merge_settle.py +383 -0
  278. agentic_project_kit/transfer_pr_actions.py +164 -0
  279. agentic_project_kit/transfer_remote_next.py +959 -0
  280. agentic_project_kit/transfer_repo_actions.py +2253 -0
  281. agentic_project_kit/transfer_runner.py +347 -0
  282. agentic_project_kit/transfer_safety_context.py +291 -0
  283. agentic_project_kit/transfer_state.py +483 -0
  284. agentic_project_kit/transfer_uplink.py +471 -0
  285. agentic_project_kit/transfer_workflow_next.py +148 -0
  286. agentic_project_kit/typed_work_order_evidence.py +58 -0
  287. agentic_project_kit/typed_work_order_queue.py +166 -0
  288. agentic_project_kit/typed_work_order_runner.py +168 -0
  289. agentic_project_kit/work_cycle.py +317 -0
  290. agentic_project_kit/work_discard_changes.py +129 -0
  291. agentic_project_kit/work_order_patch.py +199 -0
  292. agentic_project_kit/work_order_runner.py +120 -0
  293. agentic_project_kit/work_order_uploader.py +220 -0
  294. agentic_project_kit/work_order_validator.py +185 -0
  295. agentic_project_kit/work_orders.py +246 -0
  296. agentic_project_kit/workflow_guard.py +262 -0
  297. agentic_project_kit/workflow_summary_runner.py +124 -0
  298. agentic_project_kit/workspace.py +665 -0
  299. agentic_project_kit/workspace_adopt.py +471 -0
  300. agentic_project_kit/workspace_dpa_intake.py +406 -0
  301. agentic_project_kit/workspace_init.py +487 -0
  302. agentic_project_kit/workspace_lock.py +129 -0
  303. agentic_project_kit/workspace_remove.py +381 -0
  304. agentic_project_kit/workspace_upgrade.py +324 -0
  305. agentic_project_kit/wrapper_live_status.py +104 -0
  306. agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
  307. agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
  308. agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
  309. agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
  310. agentic_project_kit-1.0.1.dist-info/licenses/LICENSE +10 -0
@@ -0,0 +1,743 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ # ruff: noqa: F403,F405
5
+
6
+ from agentic_project_kit.cli_commands.transfer_shared import *
7
+ from agentic_project_kit.cli_commands.transfer_context_helpers import *
8
+ from agentic_project_kit.cli_commands.transfer_pr_merge_flow import _resolve_expected_head_sha_alias
9
+ from agentic_project_kit.workspace import load_workspace
10
+
11
+
12
+
13
+ def _auto_preflight_pr_create_complete(*, root: Path) -> None:
14
+ """Prepare rule/context carriers before pr-create-complete mutates GitHub state."""
15
+
16
+ # Isolated CliRunner fixtures may not be real Git worktrees. In that case,
17
+ # do not shadow the canonical fresh-LLM-context gate with rule/context
18
+ # repair blockers from the auto-preflight.
19
+ if not (root / ".git").exists():
20
+ return
21
+
22
+ def run_step(
23
+ name: str,
24
+ command: list[str],
25
+ *,
26
+ allow_nonzero: bool = False,
27
+ ) -> subprocess.CompletedProcess[str]:
28
+ completed = subprocess.run(
29
+ command,
30
+ cwd=root,
31
+ text=True,
32
+ stdout=subprocess.PIPE,
33
+ stderr=subprocess.PIPE,
34
+ )
35
+ if completed.returncode != 0 and not allow_nonzero:
36
+ typer.echo(
37
+ {
38
+ "result_status": "BLOCKED",
39
+ "blocker": name,
40
+ "returncode": completed.returncode,
41
+ "stdout": completed.stdout,
42
+ "stderr": completed.stderr,
43
+ "next_action": "Fix the automatic pr-create-complete preflight blocker before PR creation.",
44
+ }
45
+ )
46
+ raise typer.Exit(code=2)
47
+ return completed
48
+
49
+ # Rule acknowledgement can be unavailable in isolated test fixtures. Do not let it
50
+ # shadow the canonical fresh-LLM-context gate; the later gate remains authoritative.
51
+ run_step(
52
+ "rules_acknowledge",
53
+ [
54
+ sys.executable,
55
+ "-m",
56
+ "agentic_project_kit.cli",
57
+ "rules",
58
+ "acknowledge",
59
+ "--json",
60
+ ],
61
+ allow_nonzero=True,
62
+ )
63
+ run_step(
64
+ "refresh_llm_context_carriers",
65
+ [
66
+ sys.executable,
67
+ "-m",
68
+ "agentic_project_kit.cli",
69
+ "transfer",
70
+ "refresh-llm-context-carriers",
71
+ "--json",
72
+ ],
73
+ allow_nonzero=True,
74
+ )
75
+ run_step(
76
+ "require_fresh_llm_context",
77
+ [
78
+ sys.executable,
79
+ "-m",
80
+ "agentic_project_kit.cli",
81
+ "transfer",
82
+ "require-fresh-llm-context",
83
+ "--json",
84
+ ],
85
+ allow_nonzero=True,
86
+ )
87
+
88
+ workspace = load_workspace(root)
89
+ volatile_carriers = {
90
+ workspace.path_text(workspace.transfer_handoff_report_file("latest-transfer-handoff-report.json")),
91
+ workspace.path_text(workspace.transfer_handoff_report_file("latest-transfer-handoff-report.log")),
92
+ }
93
+ for rel_path in sorted(volatile_carriers):
94
+ run_step(
95
+ "restore_known_volatile_tracked_carrier",
96
+ ["git", "restore", "--", rel_path],
97
+ allow_nonzero=True,
98
+ )
99
+
100
+ status_result = run_step("git_status", ["git", "status", "--porcelain"], allow_nonzero=True)
101
+ if status_result.returncode != 0:
102
+ return
103
+ status = status_result.stdout.splitlines()
104
+ unexpected_dirt: list[str] = []
105
+ for line in status:
106
+ if not line:
107
+ continue
108
+ marker = line[:2]
109
+ rel = line[3:]
110
+ if marker == "??" and (rel.startswith(workspace.path_text(workspace.tmp()) + "/") or rel == ".agentic/transfer/outbox/last_result.txt"):
111
+ continue
112
+ if rel in volatile_carriers:
113
+ continue
114
+ unexpected_dirt.append(line)
115
+
116
+ if unexpected_dirt:
117
+ typer.echo(
118
+ {
119
+ "result_status": "BLOCKED",
120
+ "blocker": "unexpected_dirt_after_pr_create_complete_preflight",
121
+ "unexpected_dirt": unexpected_dirt,
122
+ "next_action": "Review unexpected dirt before PR creation.",
123
+ }
124
+ )
125
+ raise typer.Exit(code=2)
126
+
127
+
128
+
129
+ @transfer_app.command("pr-create")
130
+ def pr_create_command(
131
+ base: str = typer.Option("main", "--base", help="Base branch."),
132
+ head: str = typer.Option(..., "--head", help="Head branch."),
133
+ title: str = typer.Option(..., "--title", help="Pull request title."),
134
+ body: str = typer.Option("", "--body", help="Pull request body."),
135
+ json_output: bool = typer.Option(False, "--json", help="Print JSON instead of text."),
136
+ skip_llm_context_gate: bool = typer.Option(
137
+ False,
138
+ "--skip-llm-context-gate",
139
+ help="Recovery-only: create PR without requiring fresh generated LLM context.",
140
+ ),
141
+ ) -> None:
142
+ if not skip_llm_context_gate:
143
+ require_fresh = _public_transfer_attr("_require_fresh_llm_context_or_exit", _require_fresh_llm_context_or_exit)
144
+ require_fresh(max_age_minutes=60, json_output=json_output)
145
+ resolved_head = head
146
+ if head == "current":
147
+ import subprocess
148
+
149
+ completed = subprocess.run(["git", "branch", "--show-current"], text=True, capture_output=True)
150
+ resolved_head = completed.stdout.strip()
151
+ if completed.returncode != 0 or not resolved_head:
152
+ payload = {
153
+ "action": "pr-create",
154
+ "result_status": "BLOCKED",
155
+ "final_signal": "f",
156
+ "next_action": "Resolve current branch before creating a PR.",
157
+ "blockers": ["current_branch_missing"],
158
+ "stderr": completed.stderr,
159
+ }
160
+ typer.echo(json.dumps(payload, indent=2, sort_keys=True) if json_output else "PR_CREATE_BLOCKED\nreason=current_branch_missing")
161
+ raise typer.Exit(code=2)
162
+ require_capability = _public_transfer_attr("_require_transfer_capability", _require_transfer_capability)
163
+ require_capability("rules_confirmed")
164
+ create_pr = _public_transfer_attr("pr_create", pr_create)
165
+ result = create_pr(base=base, head=resolved_head, title=title, body=body)
166
+ echo_repo_result = _public_transfer_attr("_echo_repo_result", _echo_repo_result)
167
+ echo_repo_result(result, json_output)
168
+ if result.returncode != 0:
169
+ raise typer.Exit(code=result.returncode)
170
+
171
+ @transfer_app.command("pr-existing-for-branch")
172
+ def pr_existing_for_branch_command(
173
+ head: str = typer.Option(
174
+ "current",
175
+ "--head",
176
+ help="Head branch to look up. Use current to resolve git branch --show-current.",
177
+ ),
178
+ base: str = typer.Option("main", "--base", help="Base branch to match."),
179
+ state: str = typer.Option("all", "--state", help="GitHub PR state filter."),
180
+ json_output: bool = typer.Option(False, "--json", help="Print JSON instead of text report."),
181
+ ) -> None:
182
+ import subprocess
183
+ from datetime import datetime, timezone
184
+
185
+ steps: list[dict[str, object]] = []
186
+ blockers: list[str] = []
187
+ resolved_head = head
188
+
189
+ def run_step(name: str, argv: list[str]) -> subprocess.CompletedProcess[str]:
190
+ completed = subprocess.run(argv, text=True, capture_output=True, check=False)
191
+ steps.append(
192
+ {
193
+ "name": name,
194
+ "argv": argv,
195
+ "returncode": completed.returncode,
196
+ "stdout": completed.stdout,
197
+ "stderr": completed.stderr,
198
+ "ok": completed.returncode == 0,
199
+ }
200
+ )
201
+ return completed
202
+
203
+ if head == "current":
204
+ branch_result = run_step("resolve-current-branch", ["git", "branch", "--show-current"])
205
+ resolved_head = branch_result.stdout.strip()
206
+ if branch_result.returncode != 0 or not resolved_head:
207
+ blockers.append("current_branch_missing")
208
+
209
+ prs: list[dict[str, object]] = []
210
+ parse_error = ""
211
+ if not blockers:
212
+ lookup_result = run_step(
213
+ "gh-pr-list",
214
+ [
215
+ "gh",
216
+ "pr",
217
+ "list",
218
+ "--head",
219
+ resolved_head,
220
+ "--base",
221
+ base,
222
+ "--state",
223
+ state,
224
+ "--json",
225
+ "number,url,state,headRefName,baseRefName,isDraft,mergeStateStatus",
226
+ ],
227
+ )
228
+ if lookup_result.returncode != 0:
229
+ blockers.append("gh_pr_list_failed")
230
+ else:
231
+ try:
232
+ raw = json.loads(lookup_result.stdout or "[]")
233
+ if isinstance(raw, list):
234
+ prs = [item for item in raw if isinstance(item, dict)]
235
+ else:
236
+ blockers.append("pr_lookup_output_not_list")
237
+ except json.JSONDecodeError as exc:
238
+ parse_error = str(exc)
239
+ blockers.append("pr_lookup_json_invalid")
240
+
241
+ if blockers:
242
+ result_status = "FAIL" if "gh_pr_list_failed" in blockers else "BLOCKED"
243
+ returncode = 1 if result_status == "FAIL" else 2
244
+ pr_number = None
245
+ elif len(prs) == 1:
246
+ result_status = "PASS"
247
+ returncode = 0
248
+ pr_number = prs[0].get("number")
249
+ elif len(prs) == 0:
250
+ result_status = "MISS"
251
+ returncode = 2
252
+ pr_number = None
253
+ blockers.append("existing_pr_not_found")
254
+ else:
255
+ result_status = "BLOCKED"
256
+ returncode = 2
257
+ pr_number = None
258
+ blockers.append("multiple_existing_prs_found")
259
+
260
+ blockers = list(dict.fromkeys(blockers))
261
+ final_signal = "d" if result_status == "PASS" else "f"
262
+ if result_status == "PASS":
263
+ next_action = "Existing PR found; run transfer pr-status with the reported PR number if needed."
264
+ elif result_status == "MISS":
265
+ next_action = "No existing PR found for the requested branch."
266
+ else:
267
+ next_action = "Inspect pr-existing-for-branch lookup result before continuing: " + ", ".join(blockers)
268
+
269
+ payload = {
270
+ "schema_version": 1,
271
+ "kind": "transfer_pr_existing_for_branch_result",
272
+ "action": "pr-existing-for-branch",
273
+ "created_at_utc": datetime.now(timezone.utc).isoformat(),
274
+ "result_status": result_status,
275
+ "final_signal": final_signal,
276
+ "next_action": next_action,
277
+ "base": base,
278
+ "head": resolved_head,
279
+ "state": state,
280
+ "pr_number": pr_number,
281
+ "prs": prs,
282
+ "blockers": blockers,
283
+ "parse_error": parse_error,
284
+ "steps": steps,
285
+ }
286
+
287
+ if json_output:
288
+ typer.echo(json.dumps(payload, indent=2, ensure_ascii=False))
289
+ else:
290
+ _echo_transfer_payload_summary(
291
+ title="TRANSFER_PR_EXISTING_FOR_BRANCH",
292
+ result_status=result_status,
293
+ final_signal=final_signal,
294
+ next_action=next_action,
295
+ fields={
296
+ "HEAD": resolved_head,
297
+ "BASE": base,
298
+ "STATE_FILTER": state,
299
+ "PR": pr_number if pr_number is not None else "none",
300
+ "MATCHES": len(prs),
301
+ "BLOCKERS": len(blockers),
302
+ },
303
+ )
304
+
305
+ if returncode != 0:
306
+ raise typer.Exit(code=returncode)
307
+
308
+ @transfer_app.command(
309
+ "pr-create-complete",
310
+ help="Create, validate, merge, and optionally close out a PR without manual PR/SHA copying.",
311
+ )
312
+ def pr_create_complete_command(
313
+ title: str = typer.Option(..., "--title", help="Pull request title."),
314
+ body: str = typer.Option("", "--body", help="Pull request body."),
315
+ base: str = typer.Option("main", "--base", help="Base branch."),
316
+ head: str = typer.Option(
317
+ "current",
318
+ "--head",
319
+ help="Head branch. Use current to resolve git branch --show-current.",
320
+ ),
321
+ merge_method: str = typer.Option("squash", "--merge-method", help="GitHub merge method."),
322
+ timeout_seconds: int = typer.Option(300, "--timeout-seconds", min=1, help="Maximum CI wait time."),
323
+ poll_seconds: int = typer.Option(
324
+ 10,
325
+ "--interval-seconds",
326
+ "--poll-seconds",
327
+ min=1,
328
+ help="CI polling interval.",
329
+ ),
330
+ post_merge_complete: bool = typer.Option(
331
+ False,
332
+ "--post-merge-complete",
333
+ help="After pr-complete, run visible post-merge closeout using the concrete PR number.",
334
+ ),
335
+ json_output: bool = typer.Option(False, "--json", help="Print JSON instead of text."),
336
+ skip_llm_context_gate: bool = typer.Option(
337
+ False,
338
+ "--skip-llm-context-gate",
339
+ help="Recovery-only: run PR create/complete without requiring fresh generated LLM context.",
340
+ ),
341
+ ) -> None:
342
+ mutation_lock_contract = "workspace_mutation_lock"
343
+ _ = mutation_lock_contract
344
+ """Create a PR and complete it without requiring manual PR-number or SHA copying."""
345
+ if not skip_llm_context_gate:
346
+ _auto_preflight_pr_create_complete(root=Path("."))
347
+ require_fresh = _public_transfer_attr("_require_fresh_llm_context_or_exit", _require_fresh_llm_context_or_exit)
348
+ require_fresh(max_age_minutes=60, json_output=json_output)
349
+ require_communication_context = _public_transfer_attr(
350
+ "_require_current_communication_context_or_exit",
351
+ _require_current_communication_context_or_exit,
352
+ )
353
+ require_communication_context(
354
+ json_output=json_output,
355
+ allow_rule_carrier_publish=True,
356
+ )
357
+
358
+ import os
359
+ import re
360
+ import subprocess
361
+ from datetime import datetime, timezone
362
+
363
+ require_capability = _public_transfer_attr("_require_transfer_capability", _require_transfer_capability)
364
+ require_capability("rules_confirmed")
365
+
366
+ agentic_kit = "./.venv/bin/agentic-kit"
367
+ steps: list[dict[str, object]] = []
368
+ blockers: list[str] = []
369
+ resolved_head = "" if head == "current" else head
370
+ expected_head_sha = ""
371
+ pr_number: int | None = None
372
+
373
+ def update_live_status(
374
+ phase: wrapper_live_status.WrapperPhase,
375
+ *,
376
+ result_status: str = "RUNNING",
377
+ step: str = "",
378
+ message: str = "",
379
+ ) -> None:
380
+ wrapper_live_status.write_wrapper_live_status(
381
+ Path("."),
382
+ wrapper="pr-create-complete",
383
+ phase=phase,
384
+ result_status=result_status,
385
+ base=base,
386
+ head=resolved_head,
387
+ expected_head_sha=expected_head_sha,
388
+ pr_number=pr_number,
389
+ blockers=blockers,
390
+ step=step,
391
+ message=message,
392
+ )
393
+
394
+ update_live_status("starting", step="start")
395
+
396
+ def run_step(
397
+ name: str,
398
+ argv: list[str],
399
+ *,
400
+ env: dict[str, str] | None = None,
401
+ ) -> subprocess.CompletedProcess[str]:
402
+ completed = subprocess.run(argv, text=True, capture_output=True, env=env)
403
+ step_payload_status = ""
404
+ try:
405
+ from agentic_project_kit.release_process_guardrails import (
406
+ is_transfer_result_payload,
407
+ rc_from_result_payload,
408
+ )
409
+
410
+ parsed = json.loads(completed.stdout or "{}")
411
+ if isinstance(parsed, dict) and is_transfer_result_payload(parsed):
412
+ derived_rc = rc_from_result_payload(parsed)
413
+ step_payload_status = str(parsed.get("result_status", parsed.get("status", "")))
414
+ if completed.returncode == 0 and derived_rc != 0:
415
+ blockers.append(name + "_blocked_payload")
416
+ except Exception as exc: # noqa: BLE001 - diagnostic parsing must not hide the wrapped command result.
417
+ step_payload_status = f"payload_parse_unavailable:{type(exc).__name__}"
418
+
419
+ steps.append(
420
+ {
421
+ "name": name,
422
+ "argv": argv,
423
+ "returncode": completed.returncode,
424
+ "stdout": completed.stdout,
425
+ "stderr": completed.stderr,
426
+ "ok": completed.returncode == 0,
427
+ "payload_status": step_payload_status,
428
+ }
429
+ )
430
+ if completed.returncode != 0:
431
+ blockers.append(name + "_failed")
432
+ return completed
433
+
434
+ def post_merge_complete_verified_by_inner_pr_complete(stdout: str) -> bool:
435
+ try:
436
+ payload = json.loads(stdout or "{}")
437
+ except json.JSONDecodeError:
438
+ return False
439
+ if not isinstance(payload, dict):
440
+ return False
441
+ return (
442
+ payload.get("result_status") == "PASS"
443
+ and payload.get("post_merge_complete_followup_required") is True
444
+ and "created, completed, and verified" in str(payload.get("next_action", ""))
445
+ )
446
+
447
+ def pr_is_merged_for_outer_followup() -> bool:
448
+ if pr_number is None:
449
+ return False
450
+ command = ["gh", "pr", "list", str(pr_number), "--json", "state,mergedAt,mergeCommit"]
451
+ completed = subprocess.run(command, text=True, capture_output=True)
452
+ steps.append(
453
+ {
454
+ "name": "outer-followup-pr-merged-check",
455
+ "argv": command,
456
+ "returncode": completed.returncode,
457
+ "stdout": completed.stdout,
458
+ "stderr": completed.stderr,
459
+ "ok": completed.returncode == 0,
460
+ }
461
+ )
462
+ if completed.returncode != 0:
463
+ return False
464
+ try:
465
+ payload = json.loads(completed.stdout or "{}")
466
+ except json.JSONDecodeError:
467
+ return False
468
+ if not isinstance(payload, dict):
469
+ return False
470
+ return bool(payload.get("mergedAt")) or str(payload.get("state", "")).upper() == "MERGED"
471
+
472
+ def post_merge_check_is_green_for_outer_followup() -> bool:
473
+ sync_command = [agentic_kit, "transfer", "sync-main"]
474
+ sync = subprocess.run(sync_command, text=True, capture_output=True)
475
+ steps.append(
476
+ {
477
+ "name": "outer-followup-sync-main-before-post-merge-check",
478
+ "argv": sync_command,
479
+ "returncode": sync.returncode,
480
+ "stdout": sync.stdout,
481
+ "stderr": sync.stderr,
482
+ "ok": sync.returncode == 0,
483
+ }
484
+ )
485
+ if sync.returncode != 0:
486
+ return False
487
+
488
+ command = [agentic_kit, "transfer", "post-merge-check"]
489
+ completed = subprocess.run(command, text=True, capture_output=True)
490
+ steps.append(
491
+ {
492
+ "name": "outer-followup-post-merge-check-green-check",
493
+ "argv": command,
494
+ "returncode": completed.returncode,
495
+ "stdout": completed.stdout,
496
+ "stderr": completed.stderr,
497
+ "ok": completed.returncode == 0,
498
+ }
499
+ )
500
+ combined = f"{completed.stdout}\n{completed.stderr}"
501
+ if completed.returncode != 0:
502
+ return False
503
+ return any(
504
+ marker in combined
505
+ for marker in (
506
+ "STATE: PASS",
507
+ '"result_status": "PASS"',
508
+ "result=NOOP",
509
+ "STATE=PASS",
510
+ )
511
+ )
512
+
513
+ def outer_followup_false_red_is_safe_to_clear() -> bool:
514
+ return pr_is_merged_for_outer_followup() and post_merge_check_is_green_for_outer_followup()
515
+
516
+ if head == "current":
517
+ branch_result = run_step("resolve-current-branch", ["git", "branch", "--show-current"])
518
+ resolved_head = branch_result.stdout.strip()
519
+ if not resolved_head:
520
+ blockers.append("current_branch_missing")
521
+ else:
522
+ resolved_head = head
523
+
524
+ sha_result = run_step("resolve-current-head-sha", ["git", "rev-parse", "HEAD"])
525
+ expected_head_sha = sha_result.stdout.strip()
526
+ if not re.fullmatch(r"[0-9a-fA-F]{40}", expected_head_sha):
527
+ blockers.append("current_head_sha_invalid")
528
+
529
+ inner_post_merge_followup_verified = False
530
+ if not blockers:
531
+ update_live_status("creating_pr", step="pr-create")
532
+ create_result = run_step(
533
+ "pr-create",
534
+ [
535
+ agentic_kit,
536
+ "transfer",
537
+ "pr-create",
538
+ "--title",
539
+ title,
540
+ "--body",
541
+ body,
542
+ "--base",
543
+ base,
544
+ "--head",
545
+ resolved_head,
546
+ "--skip-llm-context-gate",
547
+ "--json",
548
+ ],
549
+ )
550
+
551
+ if create_result.returncode == 0:
552
+ match = re.search(r"/pull/(\d+)", create_result.stdout)
553
+ if match:
554
+ pr_number = int(match.group(1))
555
+ else:
556
+ blockers.append("pr_number_not_found_in_pr_create_output")
557
+ else:
558
+ existing = run_step(
559
+ "pr-existing-for-branch",
560
+ [
561
+ "gh",
562
+ "pr",
563
+ "view",
564
+ "--head",
565
+ resolved_head,
566
+ "--base",
567
+ base,
568
+ "--json",
569
+ "number",
570
+ "--jq",
571
+ ".[0].number // empty",
572
+ ],
573
+ )
574
+ if existing.returncode == 0 and existing.stdout.strip().isdigit():
575
+ pr_number = int(existing.stdout.strip())
576
+ blockers = [b for b in blockers if b != "pr-create_failed"]
577
+ else:
578
+ blockers.append("existing_pr_number_not_found")
579
+
580
+ if pr_number is not None and not blockers:
581
+ update_live_status("waiting_ci", step="pr-complete")
582
+ complete_argv = [
583
+ agentic_kit,
584
+ "transfer",
585
+ "pr-complete",
586
+ str(pr_number),
587
+ "--expected-head-sha",
588
+ expected_head_sha,
589
+ "--merge-method",
590
+ merge_method,
591
+ "--timeout-seconds",
592
+ str(timeout_seconds),
593
+ "--interval-seconds",
594
+ str(poll_seconds),
595
+ ]
596
+ if skip_llm_context_gate:
597
+ complete_argv.append("--skip-llm-context-gate")
598
+ complete_argv.append("--json")
599
+ child_env = os.environ.copy()
600
+ child_env.update(
601
+ {
602
+ "AGENTIC_KIT_WRAPPER_LIVE_STATUS_PARENT": "pr-create-complete",
603
+ "AGENTIC_KIT_WRAPPER_LIVE_STATUS_BASE": base,
604
+ "AGENTIC_KIT_WRAPPER_LIVE_STATUS_HEAD": resolved_head,
605
+ "AGENTIC_KIT_WRAPPER_LIVE_STATUS_EXPECTED_HEAD_SHA": expected_head_sha,
606
+ }
607
+ )
608
+ complete_result = run_step("pr-complete", complete_argv, env=child_env)
609
+ inner_post_merge_followup_verified = post_merge_complete_verified_by_inner_pr_complete(
610
+ complete_result.stdout
611
+ )
612
+ if not blockers:
613
+ run_step(
614
+ "restore-known-volatile-after-pr-complete",
615
+ [agentic_kit, "transfer", "restore-known-volatile", "--json"],
616
+ )
617
+
618
+ if (
619
+ pr_number is not None
620
+ and post_merge_complete
621
+ and not blockers
622
+ and not inner_post_merge_followup_verified
623
+ ):
624
+ # pr-complete already runs the concrete post-merge-complete lifecycle after
625
+ # a successful merge. The outer pr-create-complete wrapper must not run it
626
+ # a second time, because a successful admin-refresh PR can make the repeated
627
+ # closeout look like a new failure even though the lifecycle is already done.
628
+ update_live_status("post_merge", step="outer-post-merge-followup")
629
+ post_merge_steps = [
630
+ ("post-pr-sync-main-after-complete", [agentic_kit, "transfer", "sync-main"]),
631
+ ("post-pr-post-merge-check", [agentic_kit, "transfer", "post-merge-check"]),
632
+ ("post-pr-repo-status", [agentic_kit, "transfer", "repo-status"]),
633
+ ]
634
+ for name, argv in post_merge_steps:
635
+ run_step(name, argv)
636
+ if blockers:
637
+ break
638
+ if not blockers:
639
+ run_step(
640
+ "restore-known-volatile-after-post-merge-followup",
641
+ [agentic_kit, "transfer", "restore-known-volatile", "--json"],
642
+ )
643
+
644
+ blockers = list(dict.fromkeys(blockers))
645
+ outer_followup_false_red_cleared = False
646
+ if blockers and post_merge_complete and outer_followup_false_red_is_safe_to_clear():
647
+ blockers = []
648
+ outer_followup_false_red_cleared = True
649
+
650
+ result_status = "PASS" if not blockers else "BLOCKED"
651
+ update_live_status(
652
+ "done" if result_status == "PASS" else "blocked",
653
+ result_status=result_status,
654
+ step="complete",
655
+ )
656
+ final_signal = "d" if result_status == "PASS" else "f"
657
+ if result_status == "PASS" and inner_post_merge_followup_verified:
658
+ next_action = (
659
+ "PR create-complete lifecycle is complete; inner pr-complete already "
660
+ "created, completed, and verified the administrative handoff refresh."
661
+ )
662
+ elif result_status == "PASS" and outer_followup_false_red_cleared:
663
+ next_action = (
664
+ "PR create-complete lifecycle is complete; outer post-merge follow-up "
665
+ "had a non-fatal sync/restore failure, but the PR is merged and "
666
+ "post-merge-check is green."
667
+ )
668
+ else:
669
+ next_action = (
670
+ "PR create-complete lifecycle is complete."
671
+ if result_status == "PASS"
672
+ else "Inspect pr-create-complete step failure before continuing: " + ", ".join(blockers)
673
+ )
674
+
675
+ payload = {
676
+ "schema_version": 1,
677
+ "kind": "transfer_pr_create_complete_result",
678
+ "action": "pr-create-complete",
679
+ "created_at_utc": datetime.now(timezone.utc).isoformat(),
680
+ "result_status": result_status,
681
+ "final_signal": final_signal,
682
+ "next_action": next_action,
683
+ "base": base,
684
+ "head": resolved_head,
685
+ "expected_head_sha": expected_head_sha,
686
+ "pr_number": pr_number,
687
+ "post_merge_complete_verified_by_inner_pr_complete": inner_post_merge_followup_verified,
688
+ "outer_followup_false_red_cleared": outer_followup_false_red_cleared,
689
+ "blockers": blockers,
690
+ "steps": steps,
691
+ }
692
+
693
+ if json_output:
694
+ typer.echo(json.dumps(payload, indent=2, ensure_ascii=False))
695
+ else:
696
+ _echo_transfer_payload_summary(
697
+ title="TRANSFER_PR_CREATE_COMPLETE",
698
+ result_status=result_status,
699
+ final_signal=final_signal,
700
+ next_action=next_action,
701
+ fields={
702
+ "PR": pr_number if pr_number is not None else "none",
703
+ "HEAD": resolved_head,
704
+ "BASE": base,
705
+ "STEPS": len(steps),
706
+ "BLOCKERS": len(blockers),
707
+ },
708
+ )
709
+
710
+ if blockers:
711
+ raise typer.Exit(code=2)
712
+
713
+ @transfer_app.command("pr-status")
714
+ def pr_status_command(
715
+ pr_number: int = typer.Argument(
716
+ ..., help="Pull request number to inspect through the transfer wrapper."
717
+ ),
718
+ json_output: bool = typer.Option(False, "--json", help="Print JSON instead of text report."),
719
+ no_failed_log_fetch: bool = typer.Option(
720
+ False,
721
+ "--no-failed-log-fetch",
722
+ help="Do not fetch failed GitHub Actions logs for red checks.",
723
+ ),
724
+ failed_log_lines: int = typer.Option(120, min=0, help="Maximum failed-log excerpt lines."),
725
+ expected_head_sha: str = typer.Option(
726
+ "", "--expected-head-sha", help="Expected full PR head SHA."
727
+ ),
728
+ ) -> None:
729
+ result = pr_status_transfer(
730
+ pr_number,
731
+ no_failed_log_fetch=no_failed_log_fetch,
732
+ failed_log_lines=failed_log_lines,
733
+ expected_head_sha=_resolve_expected_head_sha_alias(expected_head_sha),
734
+ )
735
+ if json_output:
736
+ typer.echo(json.dumps(result.as_json_data(), indent=2, sort_keys=True))
737
+ else:
738
+ typer.echo(result.report)
739
+ if result.returncode != 0:
740
+ raise typer.Exit(code=result.returncode)
741
+
742
+
743
+ __all__ = [name for name in globals() if not name.startswith("__")]