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,148 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import shutil
6
+ import subprocess
7
+ from typing import Literal
8
+
9
+ import yaml
10
+
11
+ STATE_FILE = Path(".agentic/execution_mode_state.yaml")
12
+ ModeTarget = Literal["local", "remote"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ModeCheckResult:
17
+ ok: bool
18
+ state: str
19
+ target: str
20
+ branch: str
21
+ dirty: bool
22
+ venv_available: bool
23
+ required_tools: dict[str, str]
24
+ findings: tuple[str, ...]
25
+
26
+
27
+ def _run_git(repo_root: Path, args: list[str]) -> tuple[int, str]:
28
+ completed = subprocess.run(
29
+ ["git", *args],
30
+ cwd=repo_root,
31
+ text=True,
32
+ stdout=subprocess.PIPE,
33
+ stderr=subprocess.STDOUT,
34
+ check=False,
35
+ )
36
+ return completed.returncode, completed.stdout.strip()
37
+
38
+
39
+ def _current_branch(repo_root: Path) -> str:
40
+ rc, output = _run_git(repo_root, ["branch", "--show-current"])
41
+ if rc != 0:
42
+ return "unknown"
43
+ return output.strip() or "unknown"
44
+
45
+
46
+ def _is_dirty(repo_root: Path) -> bool:
47
+ rc, output = _run_git(repo_root, ["status", "--short"])
48
+ if rc != 0:
49
+ return True
50
+ return bool(output.strip())
51
+
52
+
53
+ def _tool_status(tool: str, repo_root: Path | None = None) -> str:
54
+ if shutil.which(tool):
55
+ return "ok"
56
+ if repo_root is not None and (repo_root / ".venv" / "bin" / tool).exists():
57
+ return "ok"
58
+ return "missing"
59
+
60
+
61
+ def _venv_available(repo_root: Path) -> bool:
62
+ return (repo_root / ".venv" / "bin" / "python").exists() or (repo_root / ".venv" / "bin" / "python3").exists()
63
+
64
+
65
+ def evaluate_mode_switch(
66
+ repo_root: Path,
67
+ target: ModeTarget,
68
+ expected_branch: str | None = None,
69
+ require_clean: bool = True,
70
+ ) -> ModeCheckResult:
71
+ repo_root = repo_root.resolve()
72
+ branch = _current_branch(repo_root)
73
+ dirty = _is_dirty(repo_root)
74
+ venv_available = _venv_available(repo_root)
75
+ required_tools = {tool: _tool_status(tool, repo_root) for tool in ("git", "gh")}
76
+ if target == "local":
77
+ required_tools["ruff"] = _tool_status("ruff", repo_root)
78
+ findings: list[str] = []
79
+
80
+ if expected_branch and branch != expected_branch:
81
+ findings.append(f"branch_mismatch expected={expected_branch} actual={branch}")
82
+ if require_clean and dirty:
83
+ findings.append("dirty_worktree")
84
+ for tool, status in required_tools.items():
85
+ if status != "ok":
86
+ findings.append(f"missing_tool={tool}")
87
+ if target == "local" and not venv_available:
88
+ findings.append("missing_venv_python")
89
+
90
+ ok = not findings
91
+ if ok:
92
+ state = "LOCAL_READY" if target == "local" else "REMOTE_READY"
93
+ elif dirty:
94
+ state = "DIRTY_LOCAL_BLOCKED"
95
+ elif any(item.startswith("missing_tool=") or item == "missing_venv_python" for item in findings):
96
+ state = "TOOLING_BLOCKED"
97
+ else:
98
+ state = "MODE_SWITCH_BLOCKED"
99
+
100
+ return ModeCheckResult(
101
+ ok=ok,
102
+ state=state,
103
+ target=target,
104
+ branch=branch,
105
+ dirty=dirty,
106
+ venv_available=venv_available,
107
+ required_tools=required_tools,
108
+ findings=tuple(findings),
109
+ )
110
+
111
+
112
+ def render_mode_check(result: ModeCheckResult) -> str:
113
+ lines = [
114
+ "EXECUTION MODE STATE CHECK",
115
+ f"target={result.target}",
116
+ f"state={result.state}",
117
+ f"branch={result.branch}",
118
+ f"dirty={str(result.dirty).lower()}",
119
+ f"venv_available={str(result.venv_available).lower()}",
120
+ "required_tools:",
121
+ ]
122
+ for tool, status in sorted(result.required_tools.items()):
123
+ lines.append(f" {tool}: {status}")
124
+ lines.append("findings:")
125
+ if result.findings:
126
+ lines.extend(f" - {finding}" for finding in result.findings)
127
+ else:
128
+ lines.append(" - none")
129
+ lines.append("### RESULT: PASS ###" if result.ok else "### RESULT: FAIL ###")
130
+ return "\n".join(lines)
131
+
132
+
133
+ def write_mode_state(repo_root: Path, result: ModeCheckResult, reason: str) -> Path:
134
+ path = repo_root.resolve() / STATE_FILE
135
+ path.parent.mkdir(parents=True, exist_ok=True)
136
+ payload = {
137
+ "schema_version": 1,
138
+ "mode_state": result.state,
139
+ "target": result.target,
140
+ "branch": result.branch,
141
+ "dirty": result.dirty,
142
+ "venv_available": result.venv_available,
143
+ "required_tools": result.required_tools,
144
+ "findings": list(result.findings),
145
+ "reason": reason,
146
+ }
147
+ path.write_text(yaml.safe_dump(payload, sort_keys=True), encoding="utf-8")
148
+ return path
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ SUMMARY_FIELDS = (
6
+ "WORK RESULT",
7
+ "EVIDENCE RESULT",
8
+ "OVERALL RESULT",
9
+ "REMOTE_EVIDENCE",
10
+ "terminal_log",
11
+ "command_report",
12
+ "NEXT_CHAT_REPLY",
13
+ )
14
+
15
+ VALID_WORK = {"PASS", "FAIL", "PENDING", "HARD-FAIL"}
16
+ VALID_EVIDENCE = {"PASS", "FAIL", "PARTIAL", "CHAT_ONLY", "NOT_REQUIRED"}
17
+ VALID_REMOTE = {"PASS", "FAIL", "PARTIAL", "NOT_REQUIRED"}
18
+ VALID_REPLY = {"p", "f", "paste-output", "continue", "stop"}
19
+
20
+ BLOCK_RE = re.compile(r"(?ms)^={64}\nSUMMARY\n(?P<body>.*?)^={64}\s*$")
21
+ RESULT_RE = re.compile(r"^### RESULT: (PASS|FAIL|PENDING|HARD-FAIL) ###$")
22
+
23
+ def parse_final_summary(text: str) -> dict[str, str]:
24
+ matches = list(BLOCK_RE.finditer(text.rstrip()))
25
+ if len(matches) != 1:
26
+ raise ValueError("expected exactly one final SUMMARY block at end of text")
27
+ body = matches[0].group("body").strip().splitlines()
28
+ values: dict[str, str] = {}
29
+ for line in body:
30
+ if line.startswith("### RESULT:"):
31
+ if not RESULT_RE.match(line):
32
+ raise ValueError("invalid final result marker")
33
+ values["RESULT_MARKER"] = line.removeprefix("### RESULT: ").removesuffix(" ###")
34
+ elif "=" in line:
35
+ key, value = line.split("=", 1)
36
+ values[key.strip()] = value.strip()
37
+ elif ":" in line:
38
+ key, value = line.split(":", 1)
39
+ values[key.strip()] = value.strip()
40
+ missing = [field for field in SUMMARY_FIELDS if field not in values]
41
+ if missing:
42
+ raise ValueError(f"missing summary fields: {missing}")
43
+ if "RESULT_MARKER" not in values:
44
+ raise ValueError("missing final result marker")
45
+ return values
46
+
47
+ def validate_final_summary(text: str) -> list[str]:
48
+ errors: list[str] = []
49
+ try:
50
+ values = parse_final_summary(text)
51
+ except ValueError as exc:
52
+ return [str(exc)]
53
+ work = values["WORK RESULT"]
54
+ evidence = values["EVIDENCE RESULT"]
55
+ overall = values["OVERALL RESULT"]
56
+ remote = values["REMOTE_EVIDENCE"]
57
+ reply = values["NEXT_CHAT_REPLY"]
58
+ marker = values["RESULT_MARKER"]
59
+ if work not in VALID_WORK:
60
+ errors.append("invalid WORK RESULT")
61
+ if evidence not in VALID_EVIDENCE:
62
+ errors.append("invalid EVIDENCE RESULT")
63
+ if overall not in VALID_WORK:
64
+ errors.append("invalid OVERALL RESULT")
65
+ if remote not in VALID_REMOTE:
66
+ errors.append("invalid REMOTE_EVIDENCE")
67
+ if reply not in VALID_REPLY:
68
+ errors.append("invalid NEXT_CHAT_REPLY")
69
+ if marker != overall:
70
+ errors.append("final result marker must match OVERALL RESULT")
71
+ if overall == "PASS" and work != "PASS":
72
+ errors.append("OVERALL RESULT: PASS requires WORK RESULT: PASS")
73
+ if overall == "PASS" and evidence not in {"PASS", "NOT_REQUIRED"}:
74
+ errors.append("OVERALL RESULT: PASS requires sufficient evidence")
75
+ if overall == "PASS" and remote not in {"PASS", "NOT_REQUIRED"}:
76
+ errors.append("OVERALL RESULT: PASS requires REMOTE_EVIDENCE: PASS or NOT_REQUIRED")
77
+ if remote != "PASS" and reply == "p":
78
+ errors.append("NEXT_CHAT_REPLY: p requires REMOTE_EVIDENCE: PASS")
79
+ final_start = text.rstrip().rfind("================================================================\nSUMMARY")
80
+ previous = text[:final_start]
81
+ if "### RESULT: FAIL ###" in previous and overall == "PASS":
82
+ errors.append("previous inner FAIL cannot be relabeled as OVERALL RESULT: PASS")
83
+ if "### RESULT: HARD-FAIL ###" in previous and overall == "PASS":
84
+ errors.append("previous inner HARD-FAIL cannot be relabeled as OVERALL RESULT: PASS")
85
+ return errors
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from dataclasses import dataclass
5
+ from enum import StrEnum
6
+
7
+
8
+ class FinalizeGuardStatus(StrEnum):
9
+ PASS_ALREADY_ON_MAIN = "PASS_ALREADY_ON_MAIN"
10
+ PASS_NOOP_BRANCH = "PASS_NOOP_BRANCH"
11
+ PASS_NEEDS_PR = "PASS_NEEDS_PR"
12
+ PASS_SUPERSEDED = "PASS_SUPERSEDED"
13
+ FAIL_CONFLICT_RELEVANT = "FAIL_CONFLICT_RELEVANT"
14
+ FAIL_NEEDS_HUMAN_REVIEW = "FAIL_NEEDS_HUMAN_REVIEW"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class FinalizeGuardDecision:
19
+ status: FinalizeGuardStatus
20
+ result: str
21
+ message: str
22
+ needs_pr: bool = False
23
+ needs_human_review: bool = False
24
+
25
+
26
+ def classify_finalize_guard_state(
27
+ *,
28
+ marker_requested: bool,
29
+ marker_on_main: bool,
30
+ local_branch_exists: bool,
31
+ remote_branch_exists: bool,
32
+ commits_ahead_of_main: int | None,
33
+ merge_conflict: bool | None,
34
+ ) -> FinalizeGuardDecision:
35
+ if not local_branch_exists and not remote_branch_exists:
36
+ return FinalizeGuardDecision(
37
+ status=FinalizeGuardStatus.PASS_NOOP_BRANCH,
38
+ result="PASS",
39
+ message="Finalization branch does not exist; no cleanup or PR action is needed.",
40
+ )
41
+
42
+ if commits_ahead_of_main is None:
43
+ return FinalizeGuardDecision(
44
+ status=FinalizeGuardStatus.FAIL_NEEDS_HUMAN_REVIEW,
45
+ result="FAIL",
46
+ message="Cannot classify branch without ahead/behind information.",
47
+ needs_human_review=True,
48
+ )
49
+
50
+ if commits_ahead_of_main == 0:
51
+ status = FinalizeGuardStatus.PASS_ALREADY_ON_MAIN if marker_requested and marker_on_main else FinalizeGuardStatus.PASS_NOOP_BRANCH
52
+ message = "Idempotent completion: requested finalization marker is already present on main." if status == FinalizeGuardStatus.PASS_ALREADY_ON_MAIN else "Idempotent completion: finalization branch has no commits ahead of main."
53
+ return FinalizeGuardDecision(status=status, result="PASS", message=message)
54
+
55
+ if merge_conflict is None:
56
+ return FinalizeGuardDecision(
57
+ status=FinalizeGuardStatus.FAIL_NEEDS_HUMAN_REVIEW,
58
+ result="FAIL",
59
+ message="Cannot classify relevant branch without merge conflict information.",
60
+ needs_human_review=True,
61
+ )
62
+
63
+ if not merge_conflict:
64
+ return FinalizeGuardDecision(
65
+ status=FinalizeGuardStatus.PASS_NEEDS_PR,
66
+ result="PASS",
67
+ message="Finalization branch has relevant commits and appears mergeable; continue normal PR path.",
68
+ needs_pr=True,
69
+ )
70
+
71
+ if marker_requested and marker_on_main:
72
+ return FinalizeGuardDecision(
73
+ status=FinalizeGuardStatus.PASS_SUPERSEDED,
74
+ result="PASS",
75
+ message="Finalization branch conflicts but requested marker is already represented on main.",
76
+ )
77
+
78
+ return FinalizeGuardDecision(
79
+ status=FinalizeGuardStatus.FAIL_CONFLICT_RELEVANT,
80
+ result="FAIL",
81
+ message="Finalization branch has commits ahead of main and conflicts; human review is required.",
82
+ needs_human_review=True,
83
+ )
84
+
85
+
86
+ def render_finalize_guard_decision(decision: FinalizeGuardDecision) -> str:
87
+ return "\n".join((
88
+ f"STATUS: {decision.status.value}",
89
+ decision.message,
90
+ f"### RESULT: {decision.result} ###",
91
+ ))
92
+
93
+
94
+ def _parse_bool(value: str) -> bool:
95
+ normalized = value.strip().lower()
96
+ if normalized in {"1", "true", "yes", "y"}:
97
+ return True
98
+ if normalized in {"0", "false", "no", "n"}:
99
+ return False
100
+ raise argparse.ArgumentTypeError(f"expected boolean value, got {value!r}")
101
+
102
+
103
+ def _parse_optional_int(value: str) -> int | None:
104
+ normalized = value.strip().lower()
105
+ if normalized in {"none", "unknown", ""}:
106
+ return None
107
+ return int(value)
108
+
109
+
110
+ def _parse_optional_bool(value: str) -> bool | None:
111
+ normalized = value.strip().lower()
112
+ if normalized in {"none", "unknown", ""}:
113
+ return None
114
+ return _parse_bool(value)
115
+
116
+
117
+ def main(argv: list[str] | None = None) -> int:
118
+ parser = argparse.ArgumentParser(description="Classify finalize-guard state.")
119
+ parser.add_argument("--marker-requested", required=True, type=_parse_bool)
120
+ parser.add_argument("--marker-on-main", required=True, type=_parse_bool)
121
+ parser.add_argument("--local-branch-exists", required=True, type=_parse_bool)
122
+ parser.add_argument("--remote-branch-exists", required=True, type=_parse_bool)
123
+ parser.add_argument("--commits-ahead-of-main", required=True, type=_parse_optional_int)
124
+ parser.add_argument("--merge-conflict", required=True, type=_parse_optional_bool)
125
+ args = parser.parse_args(argv)
126
+ decision = classify_finalize_guard_state(
127
+ marker_requested=args.marker_requested,
128
+ marker_on_main=args.marker_on_main,
129
+ local_branch_exists=args.local_branch_exists,
130
+ remote_branch_exists=args.remote_branch_exists,
131
+ commits_ahead_of_main=args.commits_ahead_of_main,
132
+ merge_conflict=args.merge_conflict,
133
+ )
134
+ print(render_finalize_guard_decision(decision))
135
+ return 0 if decision.result == "PASS" else 1
136
+
137
+
138
+ if __name__ == "__main__":
139
+ raise SystemExit(main())
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from agentic_project_kit.transfer_state import (
7
+ PRIMARY_BLOCKED,
8
+ PRIMARY_FAILED,
9
+ PRIMARY_READY,
10
+ PRIMARY_WAIT,
11
+ TransferStateSnapshot,
12
+ )
13
+
14
+ ACTION_DIAGNOSE = "diagnose"
15
+ ACTION_REFRESH_RULES = "refresh_rules"
16
+ ACTION_ACKNOWLEDGE_RULES = "acknowledge_rules"
17
+ ACTION_INSPECT_TRANSFER = "inspect_transfer"
18
+ ACTION_APPLY_TRANSFER = "apply_transfer"
19
+ ACTION_CLOSEOUT_LAST_RUN = "closeout_last_run"
20
+ ACTION_QUEUE_TRANSFER_ORDER = "queue_transfer_order"
21
+
22
+ SAFE_READ_ONLY_ACTIONS = frozenset({ACTION_DIAGNOSE, ACTION_REFRESH_RULES})
23
+ MUTATING_ACTIONS = frozenset(
24
+ {
25
+ ACTION_ACKNOWLEDGE_RULES,
26
+ ACTION_INSPECT_TRANSFER,
27
+ ACTION_APPLY_TRANSFER,
28
+ ACTION_CLOSEOUT_LAST_RUN,
29
+ ACTION_QUEUE_TRANSFER_ORDER,
30
+ }
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class GatekeeperAction:
36
+ """Machine-readable action decision for a future GUI control."""
37
+
38
+ name: str
39
+ label: str
40
+ enabled: bool
41
+ category: str
42
+ required_capability: str | None = None
43
+ blocked_reason: str = ""
44
+ next_action: str = ""
45
+
46
+ def as_json_data(self) -> dict[str, Any]:
47
+ return {
48
+ "name": self.name,
49
+ "label": self.label,
50
+ "enabled": self.enabled,
51
+ "category": self.category,
52
+ "required_capability": self.required_capability,
53
+ "blocked_reason": self.blocked_reason,
54
+ "next_action": self.next_action,
55
+ }
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class GatekeeperActionContract:
60
+ """State/action contract consumed by CLI, tests, and later GUI code."""
61
+
62
+ schema_version: int
63
+ primary_state: str
64
+ next_action: str
65
+ reasons: tuple[str, ...]
66
+ actions: tuple[GatekeeperAction, ...]
67
+
68
+ def action(self, name: str) -> GatekeeperAction:
69
+ for candidate in self.actions:
70
+ if candidate.name == name:
71
+ return candidate
72
+ raise KeyError(name)
73
+
74
+ def as_json_data(self) -> dict[str, Any]:
75
+ return {
76
+ "schema_version": self.schema_version,
77
+ "primary_state": self.primary_state,
78
+ "next_action": self.next_action,
79
+ "reasons": list(self.reasons),
80
+ "actions": [action.as_json_data() for action in self.actions],
81
+ }
82
+
83
+
84
+ def _blocked_reason(snapshot: TransferStateSnapshot, capability: str) -> str:
85
+ if snapshot.capabilities.get(capability, False):
86
+ return ""
87
+ if snapshot.reasons:
88
+ return ",".join(snapshot.reasons)
89
+ if capability == "rules_confirmed":
90
+ return "rules_not_confirmed"
91
+ if capability == "run_next_command":
92
+ return "transfer_not_ready"
93
+ if capability == "closeout_last_run":
94
+ return "closeout_not_ready"
95
+ return "capability_disabled"
96
+
97
+
98
+ def _capability_action(
99
+ snapshot: TransferStateSnapshot,
100
+ *,
101
+ name: str,
102
+ label: str,
103
+ category: str,
104
+ capability: str,
105
+ ) -> GatekeeperAction:
106
+ enabled = snapshot.capabilities.get(capability, False)
107
+ return GatekeeperAction(
108
+ name=name,
109
+ label=label,
110
+ enabled=enabled,
111
+ category=category,
112
+ required_capability=capability,
113
+ blocked_reason=_blocked_reason(snapshot, capability),
114
+ next_action=snapshot.next_action,
115
+ )
116
+
117
+
118
+ def _acknowledge_rules_action(snapshot: TransferStateSnapshot) -> GatekeeperAction:
119
+ rule_snapshot = snapshot.rule_snapshot
120
+ fail_closed = bool(rule_snapshot.get("fail_closed", False)) if isinstance(rule_snapshot, dict) else False
121
+ rules_confirmed = snapshot.capabilities.get("rules_confirmed", False)
122
+ enabled = not fail_closed and not rules_confirmed and snapshot.primary_state != PRIMARY_FAILED
123
+ if enabled:
124
+ blocked_reason = ""
125
+ elif fail_closed:
126
+ blocked_reason = "rule_snapshot_fail_closed"
127
+ elif rules_confirmed:
128
+ blocked_reason = "rules_already_confirmed"
129
+ else:
130
+ blocked_reason = _blocked_reason(snapshot, "rules_confirmed")
131
+ return GatekeeperAction(
132
+ name=ACTION_ACKNOWLEDGE_RULES,
133
+ label="Acknowledge current rule snapshot",
134
+ enabled=enabled,
135
+ category="mutating",
136
+ required_capability="rules_confirmed",
137
+ blocked_reason=blocked_reason,
138
+ next_action=snapshot.next_action,
139
+ )
140
+
141
+
142
+ def _queue_transfer_order_action(snapshot: TransferStateSnapshot) -> GatekeeperAction:
143
+ pending_order = bool(snapshot.closeout.get("pending_transfer_order", False))
144
+ dirty = bool(snapshot.closeout.get("dirty_worktree", False))
145
+ rules_confirmed = snapshot.capabilities.get("rules_confirmed", False)
146
+ enabled = rules_confirmed and not dirty and not pending_order
147
+ if enabled:
148
+ blocked_reason = ""
149
+ elif dirty:
150
+ blocked_reason = "dirty_worktree"
151
+ elif pending_order:
152
+ blocked_reason = "pending_transfer_order_exists"
153
+ elif not rules_confirmed:
154
+ blocked_reason = _blocked_reason(snapshot, "rules_confirmed")
155
+ else:
156
+ blocked_reason = "queue_not_ready"
157
+ return GatekeeperAction(
158
+ name=ACTION_QUEUE_TRANSFER_ORDER,
159
+ label="Queue transfer order",
160
+ enabled=enabled,
161
+ category="mutating",
162
+ required_capability="rules_confirmed",
163
+ blocked_reason=blocked_reason,
164
+ next_action=snapshot.next_action,
165
+ )
166
+
167
+
168
+ def derive_gatekeeper_action_contract(
169
+ snapshot: TransferStateSnapshot,
170
+ ) -> GatekeeperActionContract:
171
+ """Derive the deterministic B11 state/action contract from a transfer snapshot.
172
+
173
+ The contract is deliberately UI-free. A later GUI may render these actions as buttons,
174
+ but must not invent additional enable/disable rules outside this core contract.
175
+ """
176
+
177
+ actions = (
178
+ _capability_action(
179
+ snapshot,
180
+ name=ACTION_DIAGNOSE,
181
+ label="Diagnose state",
182
+ category="read_only",
183
+ capability="diagnose",
184
+ ),
185
+ _capability_action(
186
+ snapshot,
187
+ name=ACTION_REFRESH_RULES,
188
+ label="Refresh rule snapshot",
189
+ category="read_only",
190
+ capability="refresh_rules",
191
+ ),
192
+ _acknowledge_rules_action(snapshot),
193
+ _queue_transfer_order_action(snapshot),
194
+ _capability_action(
195
+ snapshot,
196
+ name=ACTION_INSPECT_TRANSFER,
197
+ label="Inspect transfer order",
198
+ category="mutating",
199
+ capability="run_next_command",
200
+ ),
201
+ _capability_action(
202
+ snapshot,
203
+ name=ACTION_APPLY_TRANSFER,
204
+ label="Apply inspected transfer order",
205
+ category="mutating",
206
+ capability="run_next_command",
207
+ ),
208
+ _capability_action(
209
+ snapshot,
210
+ name=ACTION_CLOSEOUT_LAST_RUN,
211
+ label="Close out last run",
212
+ category="mutating",
213
+ capability="closeout_last_run",
214
+ ),
215
+ )
216
+ return GatekeeperActionContract(
217
+ schema_version=1,
218
+ primary_state=snapshot.primary_state,
219
+ next_action=snapshot.next_action,
220
+ reasons=snapshot.reasons,
221
+ actions=actions,
222
+ )
223
+
224
+
225
+ def derive_gatekeeper_action_contract_json_data(
226
+ snapshot: TransferStateSnapshot,
227
+ ) -> dict[str, Any]:
228
+ return derive_gatekeeper_action_contract(snapshot).as_json_data()
229
+
230
+
231
+ def primary_state_is_supported(primary_state: str) -> bool:
232
+ return primary_state in {PRIMARY_READY, PRIMARY_WAIT, PRIMARY_BLOCKED, PRIMARY_FAILED}
@@ -0,0 +1,25 @@
1
+ from pathlib import Path
2
+ import shutil
3
+ import subprocess
4
+
5
+
6
+ def require_gh() -> None:
7
+ if shutil.which("gh") is None:
8
+ raise RuntimeError("GitHub CLI 'gh' not found. Install gh or run without GitHub creation.")
9
+
10
+
11
+ def check_gh_auth() -> None:
12
+ require_gh()
13
+ result = subprocess.run(["gh", "auth", "status"], text=True, capture_output=True)
14
+ if result.returncode != 0:
15
+ raise RuntimeError("GitHub CLI is not authenticated. Run: gh auth login")
16
+
17
+
18
+ def create_github_repo(project_dir: Path, owner: str | None, visibility: str, push: bool = True) -> None:
19
+ check_gh_auth()
20
+ repo_name = project_dir.name
21
+ repo = f"{owner}/{repo_name}" if owner else repo_name
22
+ cmd = ["gh", "repo", "create", repo, f"--{visibility}", "--source", str(project_dir), "--remote", "origin"]
23
+ if push:
24
+ cmd.append("--push")
25
+ subprocess.run(cmd, cwd=project_dir, check=True)
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from agentic_project_kit.action_specs import (
6
+ built_in_action_specs,
7
+ validate_current_handoff_action_surfaces,
8
+ )
9
+ from agentic_project_kit.handoff_state import load_handoff_state, validate_handoff_state
10
+ from agentic_project_kit.work_orders import check_work_orders
11
+
12
+ CONSTITUTION_FILES = (
13
+ ".agentic/project.yaml",
14
+ "sentinel.yaml",
15
+ "AGENTS.md",
16
+ "README.md",
17
+ "docs/STATUS.md",
18
+ "docs/handoff/CURRENT_HANDOFF.md",
19
+ "docs/architecture/ARCHITECTURE_CONTRACT.md",
20
+ "docs/DOCUMENTATION_COVERAGE.yaml",
21
+ "docs/workflow/HANDOFF_STATE.md",
22
+ "docs/workflow/WORK_ORDERS.md",
23
+ "docs/workflow/PARAMETERIZED_ACTIONS.md",
24
+ )
25
+
26
+ def governance_check() -> list[str]:
27
+ errors: list[str] = []
28
+ for file_name in CONSTITUTION_FILES:
29
+ if not Path(file_name).exists():
30
+ errors.append(f"missing constitution file: {file_name}")
31
+ try:
32
+ handoff_state = load_handoff_state()
33
+ except (FileNotFoundError, ValueError) as exc:
34
+ errors.append(str(exc))
35
+ else:
36
+ errors.extend(f"handoff: {error}" for error in validate_handoff_state(handoff_state))
37
+ errors.extend(f"work-order: {error}" for error in check_work_orders())
38
+ action_specs = built_in_action_specs()
39
+ required_actions = (
40
+ "pr-check-merge",
41
+ "release-verify",
42
+ "release-prepare",
43
+ "doi-record",
44
+ "finalize-release",
45
+ )
46
+ for required in required_actions:
47
+ if required not in action_specs:
48
+ errors.append(f"missing action spec: {required}")
49
+ errors.extend(
50
+ f"action-spec: {error}" for error in validate_current_handoff_action_surfaces(action_specs)
51
+ )
52
+ return errors
53
+
54
+ def render_governance_check(errors: list[str]) -> str:
55
+ if not errors:
56
+ return "Governance check passed"
57
+ lines = ["Governance check failed"]
58
+ lines.extend(f"[FAIL] {error}" for error in errors)
59
+ return "\n".join(lines)