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,254 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+ from pathlib import Path
5
+ import re
6
+ import subprocess
7
+ from typing import Any
8
+
9
+ import yaml
10
+
11
+ DEFAULT_HANDOFF_STATE_PATH = Path(".agentic/handoff_state.yaml")
12
+ REQUIRED_TOP_LEVEL_FIELDS = [
13
+ "schema_version",
14
+ "updated",
15
+ "repo",
16
+ "safe_state",
17
+ "release",
18
+ "open_items",
19
+ "completed_since_previous_handoff",
20
+ "current_capabilities",
21
+ "rules",
22
+ "recent_failure_patterns",
23
+ "next_allowed_tasks",
24
+ "blocked_until_closeout",
25
+ "first_instruction",
26
+ "handoff_maintenance",
27
+ ]
28
+ VALID_RULE_STATUSES = {"active", "superseded", "historical"}
29
+ VALID_SAFE_STATE_SEMANTICS = {"last_substantive_work_state", "current_main_head"}
30
+ HANDOFF_STATE_PRESERVATION_ANCHORS = (
31
+ "# preservation-anchor: use d for log-backed PASS and f for log-backed FAIL",
32
+ "# preservation-anchor: nested shell/Python quote layers",
33
+ )
34
+ ADMINISTRATIVE_EVIDENCE_SUBJECT_PREFIXES = (
35
+ "record ",
36
+ "refresh handoff state",
37
+ "refresh operational handoff",
38
+ "accept administrative handoff",
39
+ "align handoff state",
40
+ "finalize handoff state",
41
+ "finalize post-pr",
42
+ "recover post-pr",
43
+ "record final post-pr",
44
+ "recover post-",
45
+ )
46
+
47
+ PR_REFERENCE_RE = re.compile(r"post-PR(?P<number>\d+)|after-pr(?P<after_number>\d+)", re.IGNORECASE)
48
+
49
+
50
+ def _referenced_pr_numbers(text: str) -> set[str]:
51
+ numbers: set[str] = set()
52
+ for match in PR_REFERENCE_RE.finditer(text):
53
+ number = match.group("number") or match.group("after_number")
54
+ if number:
55
+ numbers.add(number)
56
+ return numbers
57
+
58
+
59
+ def _validate_handoff_next_reference_consistency(data: dict[str, Any]) -> list[str]:
60
+ first_instruction = str(data.get("first_instruction", ""))
61
+ maintenance = data.get("handoff_maintenance", {})
62
+ if not isinstance(maintenance, dict):
63
+ return []
64
+ latest_prompt = str(maintenance.get("latest_successor_prompt", ""))
65
+ instruction_refs = _referenced_pr_numbers(first_instruction)
66
+ prompt_refs = _referenced_pr_numbers(latest_prompt)
67
+ if not instruction_refs or not prompt_refs:
68
+ return []
69
+ if instruction_refs == prompt_refs:
70
+ return []
71
+ return [
72
+ "first_instruction successor prompt reference does not match "
73
+ "handoff_maintenance.latest_successor_prompt: "
74
+ f"first_instruction PRs={sorted(instruction_refs)}, "
75
+ f"latest_successor_prompt PRs={sorted(prompt_refs)}"
76
+ ]
77
+
78
+ def load_handoff_state(path: str | Path = DEFAULT_HANDOFF_STATE_PATH) -> dict[str, Any]:
79
+ state_path = Path(path)
80
+ if not state_path.exists():
81
+ raise FileNotFoundError(f"handoff state file not found: {state_path}")
82
+ data = yaml.safe_load(state_path.read_text(encoding="utf-8"))
83
+ if not isinstance(data, dict):
84
+ raise ValueError("handoff state must be a YAML mapping")
85
+ return data
86
+
87
+ def _is_empty(value: Any) -> bool:
88
+ return value is None or value == "" or value == [] or value == {}
89
+
90
+ def validate_handoff_state(data: dict[str, Any]) -> list[str]:
91
+ errors: list[str] = []
92
+ for field in REQUIRED_TOP_LEVEL_FIELDS:
93
+ if field not in data:
94
+ errors.append(f"missing required field: {field}")
95
+ elif _is_empty(data[field]) and field not in {"open_items", "blocked_until_closeout"}:
96
+ errors.append(f"empty required field: {field}")
97
+ rules = data.get("rules", [])
98
+ if not isinstance(rules, list):
99
+ errors.append("rules must be a list")
100
+ rules = []
101
+ active_ids: set[str] = set()
102
+ for index, rule in enumerate(rules):
103
+ if not isinstance(rule, dict):
104
+ errors.append(f"rule {index} must be a mapping")
105
+ continue
106
+ rule_id = rule.get("id")
107
+ status = rule.get("status")
108
+ text = rule.get("text")
109
+ if not rule_id:
110
+ errors.append(f"rule {index} missing id")
111
+ if status not in VALID_RULE_STATUSES:
112
+ errors.append(f"rule {rule_id or index} has invalid status: {status}")
113
+ if not text:
114
+ errors.append(f"rule {rule_id or index} missing text")
115
+ if status == "active" and rule_id:
116
+ if rule_id in active_ids:
117
+ errors.append(f"duplicate active rule id: {rule_id}")
118
+ active_ids.add(rule_id)
119
+ if status == "superseded" and not (rule.get("superseded_by") or rule.get("reason")):
120
+ errors.append(f"superseded rule {rule_id or index} needs superseded_by or reason")
121
+ safe_state = data.get("safe_state", {})
122
+ if isinstance(safe_state, dict):
123
+ semantics = safe_state.get("semantics", "last_substantive_work_state")
124
+ if semantics not in VALID_SAFE_STATE_SEMANTICS:
125
+ errors.append(f"safe_state.semantics must be one of: {sorted(VALID_SAFE_STATE_SEMANTICS)}")
126
+ refresh_prs = safe_state.get("administrative_refresh_prs", [])
127
+ if refresh_prs is None:
128
+ refresh_prs = []
129
+ if not isinstance(refresh_prs, list):
130
+ errors.append("safe_state.administrative_refresh_prs must be a list when present")
131
+ else:
132
+ for pr in refresh_prs:
133
+ if not isinstance(pr, int):
134
+ errors.append("safe_state.administrative_refresh_prs entries must be integers")
135
+ admin_state = data.get("administrative_evidence_state")
136
+ if admin_state is not None:
137
+ if not isinstance(admin_state, dict):
138
+ errors.append("administrative_evidence_state must be a mapping")
139
+ elif admin_state.get("allowed_after_safe_state") is not True:
140
+ errors.append("administrative_evidence_state.allowed_after_safe_state must be true")
141
+ for pattern in data.get("recent_failure_patterns", []) or []:
142
+ if isinstance(pattern, dict) and not pattern.get("prevention"):
143
+ pattern_id = pattern.get("id", "<unknown>")
144
+ errors.append(f"failure pattern {pattern_id} missing prevention")
145
+ blocked = set(data.get("blocked_until_closeout", []) or [])
146
+ first_instruction = str(data.get("first_instruction", ""))
147
+ for blocked_item in blocked:
148
+ if blocked_item and blocked_item in first_instruction:
149
+ errors.append(f"first_instruction mentions blocked work: {blocked_item}")
150
+ errors.extend(_validate_handoff_next_reference_consistency(data))
151
+ return errors
152
+
153
+ def active_rules(data: dict[str, Any]) -> list[dict[str, Any]]:
154
+ return [
155
+ rule
156
+ for rule in data.get("rules", [])
157
+ if isinstance(rule, dict) and rule.get("status") == "active"
158
+ ]
159
+
160
+ def summarize_handoff_state(data: dict[str, Any]) -> str:
161
+ repo = data.get("repo", {})
162
+ release = data.get("release", {})
163
+ safe_state = data.get("safe_state", {})
164
+ lines = [
165
+ f"Repo: {repo.get('name', '<unknown>')}",
166
+ f"Path: {repo.get('local_path', '<unknown>')}",
167
+ f"Remote: {repo.get('remote', '<unknown>')}",
168
+ f"Version: {release.get('current_version', '<unknown>')}",
169
+ f"Safe state: {safe_state.get('branch', '<unknown>')} @ {safe_state.get('commit', '<unknown>')}",
170
+ f"Next: {data.get('first_instruction', '<none>')}",
171
+ ]
172
+ return "\n".join(lines)
173
+
174
+
175
+ def current_git_safe_state() -> dict[str, str]:
176
+ commit = subprocess.check_output([
177
+ "git",
178
+ "rev-parse",
179
+ "--short",
180
+ "HEAD",
181
+ ], text=True).strip()
182
+ subject = subprocess.check_output([
183
+ "git",
184
+ "log",
185
+ "-1",
186
+ "--pretty=%s",
187
+ ], text=True).strip()
188
+ return {"commit": commit, "commit_subject": subject}
189
+
190
+ def is_administrative_evidence_subject(commit_subject: str) -> bool:
191
+ lowered = commit_subject.lower().strip()
192
+ return (
193
+ lowered.startswith(ADMINISTRATIVE_EVIDENCE_SUBJECT_PREFIXES)
194
+ or lowered.startswith("refresh handoff after pr")
195
+ or (
196
+ lowered.startswith("refresh post-pr")
197
+ and ("handoff" in lowered or "bootstrap" in lowered)
198
+ )
199
+ )
200
+
201
+
202
+ def is_administrative_refresh_subject(commit_subject: str) -> bool:
203
+ return is_administrative_evidence_subject(commit_subject)
204
+
205
+
206
+ def build_administrative_evidence_state(commit: str, commit_subject: str) -> dict[str, Any]:
207
+ return {
208
+ "current_head": commit,
209
+ "current_head_subject": commit_subject,
210
+ "allowed_after_safe_state": True,
211
+ "reason": "administrative evidence/log/handoff commit after substantive safe_state",
212
+ }
213
+
214
+
215
+ def refresh_handoff_safe_state(
216
+ data: dict[str, Any],
217
+ commit: str,
218
+ commit_subject: str,
219
+ reason: str = "Refresh handoff state to last substantive work commit",
220
+ updated_date: str | None = None,
221
+ ) -> dict[str, Any]:
222
+ refreshed = dict(data)
223
+ safe_state = dict(refreshed.get("safe_state", {}))
224
+ safe_state.setdefault("semantics", "last_substantive_work_state")
225
+ refresh_date = updated_date or date.today().isoformat()
226
+ if is_administrative_evidence_subject(commit_subject):
227
+ safe_state["semantics"] = "last_substantive_work_state"
228
+ refreshed["safe_state"] = safe_state
229
+ refreshed["administrative_evidence_state"] = build_administrative_evidence_state(commit, commit_subject)
230
+ refreshed["updated"] = {
231
+ "date": refresh_date,
232
+ "reason": "administrative handoff refresh: Current HEAD is administrative evidence; safe_state remains on last substantive work state",
233
+ "source": "agentic-kit handoff refresh",
234
+ }
235
+ return refreshed
236
+ safe_state["branch"] = "main"
237
+ safe_state["commit"] = commit
238
+ safe_state["commit_subject"] = commit_subject
239
+ refreshed["safe_state"] = safe_state
240
+ refreshed.pop("administrative_evidence_state", None)
241
+ refreshed["updated"] = {
242
+ "date": refresh_date,
243
+ "reason": reason,
244
+ "source": "agentic-kit handoff refresh",
245
+ }
246
+ return refreshed
247
+
248
+ def save_handoff_state(data: dict[str, Any], path: str | Path = DEFAULT_HANDOFF_STATE_PATH) -> None:
249
+ state_path = Path(path)
250
+ text = yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
251
+ missing_anchors = [anchor for anchor in HANDOFF_STATE_PRESERVATION_ANCHORS if anchor not in text]
252
+ if missing_anchors:
253
+ text = text.rstrip() + "\n" + "\n".join(missing_anchors) + "\n"
254
+ state_path.write_text(text, encoding="utf-8")