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,392 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date as date_cls
4
+ import json
5
+ import re
6
+ from typing import Annotated
7
+ from pathlib import Path
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from agentic_project_kit.post_release import build_post_release_report, render_post_release_report
13
+ from agentic_project_kit.release_prepare import prepare_release_state
14
+ from agentic_project_kit.release_metadata_authority_gate import (
15
+ evaluate_release_metadata_authority_gate,
16
+ render_release_metadata_authority_gate_result,
17
+ )
18
+ from agentic_project_kit.post_release_closeout import post_release_doi_closeout, render_post_release_doi_closeout_result
19
+ from agentic_project_kit.release import (
20
+ ReleaseCheckStatus,
21
+ build_release_plan,
22
+ build_release_preflight_report,
23
+ build_release_state_report,
24
+ render_release_plan,
25
+ render_release_preflight_report,
26
+ render_release_state_report,
27
+ )
28
+ from agentic_project_kit.release_state import build_release_lifecycle_status, render_release_lifecycle_status
29
+ from agentic_project_kit.release_notes import (
30
+ build_release_notes_report,
31
+ check_release_notes_outputs,
32
+ render_release_notes_markdown,
33
+ write_release_notes_outputs,
34
+ )
35
+
36
+ console = Console()
37
+
38
+
39
+ def _print_json(payload: dict[str, object]) -> None:
40
+ typer.echo(json.dumps(payload, indent=2, sort_keys=True))
41
+
42
+
43
+ def register_release_commands(app: typer.Typer) -> None:
44
+ app.command("release-plan")(release_plan_command)
45
+ app.command("release-preflight")(release_preflight_command)
46
+ app.command("release-prep")(release_prep_command)
47
+ app.command("release-status")(release_status_command)
48
+ app.command("release-notes-generate")(release_notes_generate_command)
49
+ app.command("release-metadata-authority-gate")(release_metadata_authority_gate_command)
50
+ app.command("release-check")(release_check_command)
51
+ app.command("post-release-check")(post_release_check_command)
52
+ app.command(
53
+ "post-release-doi-closeout",
54
+ help="Record verified DOI metadata after GitHub release and Zenodo publication.",
55
+ )(post_release_doi_closeout_command)
56
+
57
+
58
+ def release_plan_command(
59
+ project_root: Path = typer.Option(Path("."), "--root"),
60
+ version: str | None = typer.Option(None, "--version", help="Release version without leading v."),
61
+ ) -> None:
62
+ """Print a release preparation checklist for the current project."""
63
+ root = project_root.resolve()
64
+ if version is None:
65
+ current_version = build_release_plan(root).version
66
+ current_report = build_release_preflight_report(root, version=current_version)
67
+ already_released = any(
68
+ check.status == ReleaseCheckStatus.FAIL
69
+ for check in current_report.checks
70
+ if check.name in {"remote tag unused", "GitHub release unused"}
71
+ )
72
+ if already_released:
73
+ console.print(
74
+ "Current package version is already released. "
75
+ "Pass the next target explicitly, for example: "
76
+ "agentic-kit release-plan --version <next-version>",
77
+ markup=False,
78
+ )
79
+ raise typer.Exit(code=2)
80
+ plan = build_release_plan(root, version=version)
81
+ console.print(render_release_plan(plan), markup=False)
82
+
83
+
84
+ def release_preflight_command(
85
+ version: str = typer.Option(..., "--version", help="Target release version without leading v."),
86
+ ) -> None:
87
+ """Validate before-metadata release readiness for a target version."""
88
+ raise typer.Exit(code=run_release_preflight(version))
89
+
90
+
91
+ def run_release_preflight(version: str) -> int:
92
+ """Validate the before-metadata release phase for a target version.
93
+
94
+ This intentionally does not require project files to already contain the
95
+ target version. That is the job of release-check after metadata is patched.
96
+ """
97
+ report = build_release_preflight_report(Path(".").resolve(), version=version)
98
+ console.print(render_release_preflight_report(report), markup=False)
99
+ return 0 if report.ok else 1
100
+
101
+
102
+ def release_prep_command(
103
+ project_root: Path = typer.Option(Path("."), "--root"),
104
+ version: str = typer.Option(..., "--version", help="Target release version without leading v."),
105
+ release_date: str | None = typer.Option(
106
+ None,
107
+ "--date",
108
+ help="Release metadata date in YYYY-MM-DD format. Defaults to today.",
109
+ ),
110
+ summary_lines: Annotated[
111
+ list[str] | None,
112
+ typer.Option(
113
+ "--summary-line",
114
+ help="Release changelog summary line. Repeat for multiple lines; required to avoid stale template reuse.",
115
+ ),
116
+ ] = None,
117
+ summary_lines_from: Path | None = typer.Option(
118
+ None,
119
+ "--summary-lines-from",
120
+ help="Read explicit release changelog summary lines from a JSON artifact generated from release notes.",
121
+ ),
122
+ dry_run: bool = typer.Option(False, "--dry-run", help="Report changed paths without writing files."),
123
+ json_output: bool = typer.Option(False, "--json", help="Print a machine-readable result."),
124
+ ) -> None:
125
+ """Prepare release metadata through the supported agentic-kit route.
126
+
127
+ This edits only local release metadata files. It does not tag, publish,
128
+ push, create GitHub releases, or write Zenodo DOI metadata.
129
+ """
130
+ plain_version = version.removeprefix("v")
131
+ date_value = release_date or date_cls.today().isoformat()
132
+ if not re.fullmatch(r"\d+\.\d+\.\d+", plain_version):
133
+ message = f"Invalid release version: {version!r}; expected MAJOR.MINOR.PATCH"
134
+ if json_output:
135
+ _print_json(
136
+ {
137
+ "ok": False,
138
+ "version": plain_version,
139
+ "date": date_value,
140
+ "dry_run": dry_run,
141
+ "error": message,
142
+ }
143
+ )
144
+ else:
145
+ console.print(f"ERROR: {message}", markup=False)
146
+ raise typer.Exit(code=2)
147
+
148
+ try:
149
+ effective_summary_lines = _combine_release_prep_summary_lines(
150
+ explicit_lines=summary_lines or [],
151
+ summary_lines_from=summary_lines_from,
152
+ )
153
+ result = prepare_release_state(
154
+ project_root.resolve(),
155
+ version=plain_version,
156
+ date=date_value,
157
+ summary_lines=effective_summary_lines,
158
+ dry_run=dry_run,
159
+ )
160
+ except (FileNotFoundError, ValueError) as exc:
161
+ if json_output:
162
+ _print_json(
163
+ {
164
+ "ok": False,
165
+ "version": plain_version,
166
+ "date": date_value,
167
+ "dry_run": dry_run,
168
+ "error": str(exc),
169
+ }
170
+ )
171
+ else:
172
+ console.print(f"ERROR: {exc}", markup=False)
173
+ raise typer.Exit(code=2) from exc
174
+
175
+ if json_output:
176
+ _print_json(result.as_dict())
177
+ else:
178
+ mode = "DRY-RUN" if dry_run else "WRITE"
179
+ if result.changed_paths:
180
+ console.print(
181
+ f"{mode}: prepared release metadata for v{result.version} on {result.date}",
182
+ markup=False,
183
+ )
184
+ for changed_path in result.changed_paths:
185
+ console.print(f"CHANGED: {changed_path}", markup=False)
186
+ else:
187
+ console.print(f"{mode}: release metadata already prepared for v{result.version}", markup=False)
188
+
189
+
190
+ def release_status_command(
191
+ project_root: Path = typer.Option(Path("."), "--root"),
192
+ version: str | None = typer.Option(None, "--version", help="Release version without leading v."),
193
+ include_remote: bool = typer.Option(False, "--include-remote", help="Include remote tag, GitHub Release, and DOI/Zenodo checks."),
194
+ json_output: bool = typer.Option(False, "--json", help="Print a machine-readable result."),
195
+ ) -> None:
196
+ """Render the local release lifecycle state without mutating release files."""
197
+ result = build_release_lifecycle_status(project_root.resolve(), version=version, include_remote=include_remote)
198
+ if json_output:
199
+ _print_json(result.as_dict())
200
+ else:
201
+ console.print(render_release_lifecycle_status(result), markup=False)
202
+ if result.blockers:
203
+ raise typer.Exit(code=2)
204
+
205
+
206
+ def release_notes_generate_command(
207
+ project_root: Path = typer.Option(Path("."), "--root"),
208
+ version: str = typer.Option(..., "--version", help="Target release version without leading v."),
209
+ from_tag: str = typer.Option(..., "--from-tag", help="Previous release tag used as the lower diff bound."),
210
+ to_ref: str = typer.Option("HEAD", "--to-ref", help="Upper git ref used for release-note evidence."),
211
+ json_out: Path | None = typer.Option(None, "--json-out", help="Write generated JSON release notes here."),
212
+ out: Path | None = typer.Option(None, "--out", help="Write generated Markdown release notes here."),
213
+ summary_lines_json: Path | None = typer.Option(
214
+ None,
215
+ "--summary-lines-json",
216
+ help="Write a compact JSON artifact containing explicit release-prep summary lines.",
217
+ ),
218
+ write: bool = typer.Option(False, "--write", help="Write --json-out and --out files."),
219
+ check: bool = typer.Option(False, "--check", help="Check existing --json-out and --out files for generated-output drift."),
220
+ include_github_metadata: bool = typer.Option(False, "--include-github-metadata", help="Augment PR evidence with optional gh pr view metadata."),
221
+ json_output: bool = typer.Option(False, "--json", help="Print a machine-readable result."),
222
+ ) -> None:
223
+ """Generate deterministic evidence-backed release notes from a local git tag diff."""
224
+ result = build_release_notes_report(
225
+ project_root.resolve(),
226
+ version=version,
227
+ from_tag=from_tag,
228
+ to_ref=to_ref,
229
+ include_github_metadata=include_github_metadata,
230
+ )
231
+ if summary_lines_json is not None:
232
+ _write_release_summary_lines_artifact(result, summary_lines_json=summary_lines_json)
233
+ if write:
234
+ if json_out is None or out is None:
235
+ message = "--write requires both --json-out and --out"
236
+ if json_output:
237
+ _print_json({"ok": False, "status": "BLOCK", "message": message})
238
+ else:
239
+ console.print(f"BLOCK: {message}", markup=False)
240
+ raise typer.Exit(code=2)
241
+ write_release_notes_outputs(result, json_out=json_out, markdown_out=out)
242
+ drift_check: dict[str, object] | None = None
243
+ if check:
244
+ if json_out is None or out is None:
245
+ message = "--check requires both --json-out and --out"
246
+ if json_output:
247
+ _print_json({"ok": False, "status": "BLOCK", "message": message})
248
+ else:
249
+ console.print(f"BLOCK: {message}", markup=False)
250
+ raise typer.Exit(code=2)
251
+ drift_check = check_release_notes_outputs(result, json_out=json_out, markdown_out=out)
252
+ if json_output:
253
+ payload = result.as_dict()
254
+ if drift_check is not None:
255
+ payload["drift_check"] = drift_check
256
+ _print_json(payload)
257
+ else:
258
+ console.print(render_release_notes_markdown(result), markup=False)
259
+ if drift_check is not None:
260
+ console.print(f"DRIFT_CHECK={drift_check['status']}", markup=False)
261
+ if result.validation.status != "PASS":
262
+ raise typer.Exit(code=2)
263
+ if drift_check is not None and drift_check["status"] != "PASS":
264
+ raise typer.Exit(code=2)
265
+
266
+
267
+ def _combine_release_prep_summary_lines(*, explicit_lines: list[str], summary_lines_from: Path | None) -> list[str]:
268
+ lines = list(explicit_lines)
269
+ if summary_lines_from is not None:
270
+ lines.extend(_read_release_summary_lines_artifact(summary_lines_from))
271
+ return lines
272
+
273
+
274
+ def _read_release_summary_lines_artifact(path: Path) -> list[str]:
275
+ payload = json.loads(path.read_text(encoding="utf-8"))
276
+ if isinstance(payload, list):
277
+ raw_lines = payload
278
+ elif isinstance(payload, dict) and isinstance(payload.get("summary_lines"), list):
279
+ raw_lines = payload["summary_lines"]
280
+ elif isinstance(payload, dict) and isinstance(payload.get("items"), list):
281
+ raw_lines = [item.get("title") for item in payload["items"] if isinstance(item, dict)]
282
+ else:
283
+ raise ValueError("--summary-lines-from must point to a JSON list, a {summary_lines: [...]} artifact, or a release-notes JSON report")
284
+ lines = [str(line).strip() for line in raw_lines if str(line).strip()]
285
+ if not lines:
286
+ raise ValueError("--summary-lines-from did not contain any usable release changelog summary lines")
287
+ return lines
288
+
289
+
290
+ def _release_summary_lines_from_report(report: object) -> list[str]:
291
+ items = getattr(report, "items")
292
+ return [str(item.title).strip() for item in items if str(item.title).strip()]
293
+
294
+
295
+ def _write_release_summary_lines_artifact(report: object, *, summary_lines_json: Path) -> None:
296
+ lines = _release_summary_lines_from_report(report)
297
+ payload: dict[str, object] = {
298
+ "schema_version": 1,
299
+ "kind": "release_prep_summary_lines",
300
+ "version": getattr(report, "version"),
301
+ "from_tag": getattr(report, "from_tag"),
302
+ "to_ref": getattr(report, "to_ref"),
303
+ "source": "agentic-kit release-notes-generate",
304
+ "summary_lines": lines,
305
+ }
306
+ summary_lines_json.parent.mkdir(parents=True, exist_ok=True)
307
+ summary_lines_json.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
308
+
309
+
310
+ def release_metadata_authority_gate_command(
311
+ project_root: Path = typer.Option(Path("."), "--root"),
312
+ base_ref: str = typer.Option("origin/main", "--base-ref", help="Git ref used as the diff base."),
313
+ version: str | None = typer.Option(None, "--version", help="Expected release version without leading v."),
314
+ evidence: Annotated[
315
+ list[Path] | None,
316
+ typer.Option(
317
+ "--evidence",
318
+ help="Authoritative release-prep evidence file. May be passed more than once.",
319
+ ),
320
+ ] = None,
321
+ json_output: bool = typer.Option(False, "--json", help="Print a machine-readable result."),
322
+ ) -> None:
323
+ """Block manual release metadata anchor edits without release-prep evidence."""
324
+ try:
325
+ result = evaluate_release_metadata_authority_gate(
326
+ project_root.resolve(),
327
+ base_ref=base_ref,
328
+ version=version,
329
+ evidence_paths=evidence or [],
330
+ )
331
+ except (RuntimeError, ValueError) as exc:
332
+ result = None
333
+ if json_output:
334
+ _print_json(
335
+ {
336
+ "ok": False,
337
+ "status": "BLOCK",
338
+ "version": version,
339
+ "base_ref": base_ref,
340
+ "changed_release_anchor_paths": [],
341
+ "evidence_paths": [path.as_posix() for path in evidence or []],
342
+ "message": str(exc),
343
+ }
344
+ )
345
+ else:
346
+ console.print(f"BLOCK: {exc}", markup=False)
347
+ raise typer.Exit(code=2) from exc
348
+
349
+ if json_output:
350
+ _print_json(result.as_dict())
351
+ else:
352
+ console.print(render_release_metadata_authority_gate_result(result), markup=False)
353
+
354
+ if not result.ok:
355
+ raise typer.Exit(code=1)
356
+
357
+
358
+ def release_check_command(
359
+ project_root: Path = typer.Option(Path("."), "--root"),
360
+ version: str | None = typer.Option(None, "--version", help="Release version without leading v."),
361
+ ) -> None:
362
+ """Validate release state for a target version."""
363
+ report = build_release_state_report(project_root.resolve(), version=version)
364
+ console.print(render_release_state_report(report), markup=False)
365
+ if not report.ok:
366
+ raise typer.Exit(code=1)
367
+
368
+
369
+ def post_release_check_command(
370
+ project_root: Path = typer.Option(Path("."), "--root"),
371
+ version: str | None = typer.Option(None, "--version", help="Release version without leading v."),
372
+ ) -> None:
373
+ """Validate post-release GitHub and Zenodo state without guessing DOI metadata."""
374
+ report = build_post_release_report(project_root.resolve(), version=version)
375
+ console.print(render_post_release_report(report), markup=False)
376
+ if not report.ok:
377
+ raise typer.Exit(code=1)
378
+
379
+
380
+ def post_release_doi_closeout_command(
381
+ project_root: Path = typer.Option(Path("."), "--root"),
382
+ version: str = typer.Option(..., "--version", help="Release version without leading v."),
383
+ write: bool = typer.Option(False, "--write", help="Write verified DOI metadata updates."),
384
+ json_output: bool = typer.Option(False, "--json", help="Print a machine-readable result."),
385
+ ) -> None:
386
+ result = post_release_doi_closeout(project_root.resolve(), version=version, write=write)
387
+ if json_output:
388
+ _print_json(result.as_dict())
389
+ else:
390
+ console.print(render_post_release_doi_closeout_result(result), markup=False)
391
+ if not result.ok:
392
+ raise typer.Exit(code=result.returncode)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from agentic_project_kit import __version__ as PACKAGE_VERSION
9
+ from agentic_project_kit.release_publish_orchestration import (
10
+ evaluate_release_publish_plan,
11
+ render_release_publish_plan,
12
+ )
13
+
14
+
15
+ def release_publish_command(
16
+ version: str = typer.Option(PACKAGE_VERSION, "--version"),
17
+ dry_run: bool = typer.Option(True, "--dry-run/--no-dry-run"),
18
+ execute: bool = typer.Option(False, "--execute"),
19
+ allow_execute: bool = typer.Option(False, "--allow-execute"),
20
+ root: Path = typer.Option(Path("."), "--root"),
21
+ json_output: bool = typer.Option(False, "--json"),
22
+ ) -> None:
23
+ """Plan release publishing without live tag/release side effects."""
24
+ plan = evaluate_release_publish_plan(
25
+ root.resolve(),
26
+ version=version,
27
+ dry_run=dry_run,
28
+ execute=execute,
29
+ allow_execute=allow_execute,
30
+ )
31
+ if json_output:
32
+ typer.echo(json.dumps(plan.as_dict(), indent=2, sort_keys=True))
33
+ else:
34
+ typer.echo(render_release_publish_plan(plan), nl=False)
35
+ if not plan.ok:
36
+ raise typer.Exit(code=plan.returncode)
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from agentic_project_kit.remote_branch_hygiene import (
9
+ analyze_remote_branch_hygiene,
10
+ build_remote_branch_hygiene_evidence_report_payload,
11
+ collect_remote_branch_hygiene_inputs,
12
+ delete_remote_branch,
13
+ render_remote_branch_hygiene_report,
14
+ report_as_json_data,
15
+ write_remote_branch_hygiene_evidence_report,
16
+ )
17
+
18
+
19
+ def register_remote_branch_hygiene_command(app: typer.Typer) -> None:
20
+ app.command("remote-branch-hygiene")(remote_branch_hygiene_command)
21
+ app.command("remote-branch-hygiene-report")(remote_branch_hygiene_report_command)
22
+ app.command("remote-branch-hygiene-apply")(remote_branch_hygiene_apply_command)
23
+
24
+
25
+ def remote_branch_hygiene_command(
26
+ project_root: Path = typer.Option(Path("."), "--root"),
27
+ json_output: bool = typer.Option(False, "--json"),
28
+ ) -> None:
29
+ """Dry-run remote branch hygiene classification for K3."""
30
+ remote_branches, open_pr_heads = collect_remote_branch_hygiene_inputs(project_root.resolve())
31
+ report = analyze_remote_branch_hygiene(remote_branches, open_pr_heads)
32
+ if json_output:
33
+ typer.echo(json.dumps(report_as_json_data(report), indent=2, sort_keys=True))
34
+ else:
35
+ typer.echo(render_remote_branch_hygiene_report(report), nl=False)
36
+
37
+ def _assert_safe_output_path(project_root: Path, output_path: Path) -> Path:
38
+ resolved_root = project_root.resolve()
39
+ resolved = output_path.resolve()
40
+ safe_roots = (
41
+ resolved_root / "tmp",
42
+ resolved_root / "docs" / "reports",
43
+ resolved_root / ".agentic",
44
+ )
45
+ if not any(resolved == root or root in resolved.parents for root in safe_roots):
46
+ safe = ", ".join(str(root) for root in safe_roots)
47
+ raise typer.BadParameter(f"output path must be below one of: {safe}")
48
+ return resolved
49
+
50
+
51
+ def remote_branch_hygiene_report_command(
52
+ project_root: Path = typer.Option(Path("."), "--root"),
53
+ output_path: Path = typer.Option(..., "--output"),
54
+ execute: bool = typer.Option(False, "--execute"),
55
+ json_output: bool = typer.Option(False, "--json"),
56
+ ) -> None:
57
+ """Write a K3 remote branch hygiene evidence report only with --execute."""
58
+ root = project_root.resolve()
59
+ remote_branches, open_pr_heads = collect_remote_branch_hygiene_inputs(root)
60
+ report = analyze_remote_branch_hygiene(remote_branches, open_pr_heads)
61
+ safe_output = _assert_safe_output_path(root, output_path)
62
+
63
+ if execute:
64
+ write_remote_branch_hygiene_evidence_report(safe_output, report)
65
+ mode = "execute"
66
+ mutation = "evidence-report-write"
67
+ written = True
68
+ else:
69
+ mode = "dry-run"
70
+ mutation = "none"
71
+ written = False
72
+
73
+ payload = {
74
+ "schema_version": 1,
75
+ "kind": "k3_remote_branch_hygiene_report_write_result",
76
+ "mode": mode,
77
+ "mutation": mutation,
78
+ "result_status": "PASS",
79
+ "written": written,
80
+ "output_path": str(safe_output),
81
+ "report": build_remote_branch_hygiene_evidence_report_payload(report),
82
+ }
83
+ if json_output:
84
+ typer.echo(json.dumps(payload, indent=2, sort_keys=True))
85
+ else:
86
+ typer.echo("K3_REMOTE_BRANCH_HYGIENE_REPORT")
87
+ typer.echo(f"MODE: {mode}")
88
+ typer.echo(f"MUTATION: {mutation}")
89
+ typer.echo(f"WRITTEN: {str(written).lower()}")
90
+ typer.echo(f"OUTPUT: {safe_output}")
91
+
92
+ def _branch_name_is_single_remote_ref(branch: str) -> bool:
93
+ return branch.startswith("origin/") and branch not in {"origin", "origin/HEAD", "origin/main"} and "," not in branch
94
+
95
+
96
+ def _find_safe_delete_candidate(report, branch: str):
97
+ if not _branch_name_is_single_remote_ref(branch):
98
+ raise typer.BadParameter("only must be one non-protected origin/<name> remote branch")
99
+ for finding in report.findings:
100
+ if finding.branch == branch:
101
+ if finding.proposed_action != "candidate-delete-merged-remote-branch":
102
+ raise typer.BadParameter(f"{branch} is not a safe delete candidate")
103
+ if finding.has_open_pr:
104
+ raise typer.BadParameter(f"{branch} is not a safe delete candidate")
105
+ if not finding.merged_to_origin_main:
106
+ raise typer.BadParameter(f"{branch} is not a safe delete candidate")
107
+ return finding
108
+ raise typer.BadParameter(f"branch not found in current hygiene report: {branch}")
109
+
110
+
111
+ def remote_branch_hygiene_apply_command(
112
+ project_root: Path = typer.Option(Path("."), "--root"),
113
+ only: str = typer.Option(..., "--only"),
114
+ execute: bool = typer.Option(False, "--execute"),
115
+ json_output: bool = typer.Option(False, "--json"),
116
+ ) -> None:
117
+ """Safely apply exactly one remote branch deletion candidate."""
118
+ root = project_root.resolve()
119
+ remote_branches, open_pr_heads = collect_remote_branch_hygiene_inputs(root)
120
+ report = analyze_remote_branch_hygiene(remote_branches, open_pr_heads)
121
+ finding = _find_safe_delete_candidate(report, only)
122
+
123
+ if execute:
124
+ delete_remote_branch(root, finding.branch)
125
+ mode = "execute"
126
+ mutation = "delete-single-remote-branch"
127
+ deleted = True
128
+ else:
129
+ mode = "dry-run"
130
+ mutation = "none"
131
+ deleted = False
132
+
133
+ payload = {
134
+ "schema_version": 1,
135
+ "kind": "k3_remote_branch_hygiene_apply_result",
136
+ "mode": mode,
137
+ "mutation": mutation,
138
+ "result_status": "PASS",
139
+ "branch": finding.branch,
140
+ "short_name": finding.short_name,
141
+ "deleted": deleted,
142
+ "safety_class": finding.safety_class,
143
+ "reason": finding.reason,
144
+ }
145
+ if json_output:
146
+ typer.echo(json.dumps(payload, indent=2, sort_keys=True))
147
+ else:
148
+ typer.echo("K3_REMOTE_BRANCH_HYGIENE_APPLY")
149
+ typer.echo(f"MODE: {mode}")
150
+ typer.echo(f"MUTATION: {mutation}")
151
+ typer.echo(f"BRANCH: {finding.branch}")
152
+ typer.echo(f"DELETED: {str(deleted).lower()}")
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from agentic_project_kit.remote_next import (
10
+ remote_next_result_as_json_data,
11
+ render_remote_next_result,
12
+ run_remote_next,
13
+ )
14
+ from agentic_project_kit.remote_next_closeout import (
15
+ render_remote_next_closeout_result,
16
+ run_remote_next_closeout,
17
+ )
18
+
19
+
20
+ def register_remote_next_command(app: typer.Typer) -> None:
21
+ @app.command("remote-next")
22
+ def remote_next_command(
23
+ project_root: Annotated[Path, typer.Option("--root")] = Path("."),
24
+ json_output: Annotated[bool, typer.Option("--json", help="Print machine-readable JSON result.")] = False,
25
+ ) -> None:
26
+ result = run_remote_next(project_root)
27
+ if json_output:
28
+ typer.echo(json.dumps(remote_next_result_as_json_data(result), indent=2, sort_keys=True))
29
+ else:
30
+ typer.echo(render_remote_next_result(result))
31
+ if result.returncode != 0:
32
+ raise typer.Exit(code=result.returncode)
33
+
34
+ @app.command("rn")
35
+ def rn_command(
36
+ project_root: Annotated[Path, typer.Option("--root")] = Path("."),
37
+ json_output: Annotated[bool, typer.Option("--json", help="Print machine-readable JSON result.")] = False,
38
+ ) -> None:
39
+ result = run_remote_next(project_root)
40
+ if json_output:
41
+ typer.echo(json.dumps(remote_next_result_as_json_data(result), indent=2, sort_keys=True))
42
+ else:
43
+ typer.echo(render_remote_next_result(result))
44
+ if result.returncode != 0:
45
+ raise typer.Exit(code=result.returncode)
46
+
47
+ @app.command("rnc")
48
+ def rnc_command(
49
+ project_root: Annotated[Path, typer.Option("--root")] = Path("."),
50
+ no_push: Annotated[bool, typer.Option("--no-push", help="Commit locally without pushing.")] = False,
51
+ ) -> None:
52
+ result = run_remote_next_closeout(project_root, push=not no_push)
53
+ typer.echo(render_remote_next_closeout_result(result))
54
+ if result.status == "no_closeout":
55
+ raise typer.Exit(code=2)
56
+ if not result.success:
57
+ raise typer.Exit(code=1)