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,46 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib
5
+ import os
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class WindowGuardResult:
10
+ ok: bool
11
+ tkinter_importable: bool
12
+ native_tkinter_importable: bool
13
+ message: str
14
+
15
+
16
+ def check_window_launch_ready() -> WindowGuardResult:
17
+ try:
18
+ importlib.import_module("tkinter")
19
+ tkinter_importable = True
20
+ except Exception as exc:
21
+ return WindowGuardResult(False, False, False, f"tkinter import failed: {exc}")
22
+ try:
23
+ importlib.import_module("_tkinter")
24
+ native_importable = True
25
+ except Exception as exc:
26
+ return WindowGuardResult(False, tkinter_importable, False, f"native _tkinter import failed: {exc}")
27
+ if os.environ.get("AGENTIC_KIT_ALLOW_TK_WINDOW_SMOKE") != "1":
28
+ return WindowGuardResult(
29
+ False,
30
+ tkinter_importable,
31
+ native_importable,
32
+ "real Tk window smoke requires AGENTIC_KIT_ALLOW_TK_WINDOW_SMOKE=1",
33
+ )
34
+ return WindowGuardResult(True, tkinter_importable, native_importable, "window launch guard passed")
35
+
36
+
37
+ def render_window_guard_result(result: WindowGuardResult) -> str:
38
+ status = "READY" if result.ok else "BLOCKED"
39
+ return "\n".join((
40
+ "GUI WINDOW GUARD",
41
+ f"window_launch_ready={str(result.ok).lower()}",
42
+ f"tkinter_importable={str(result.tkinter_importable).lower()}",
43
+ f"native_tkinter_importable={str(result.native_tkinter_importable).lower()}",
44
+ f"message={result.message}",
45
+ f"window_guard_status={status}",
46
+ ))
@@ -0,0 +1,390 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from agentic_project_kit.handoff_state import is_administrative_evidence_subject
8
+
9
+ FRESHNESS_GUARD_FILES = [
10
+ "docs/STATUS.md",
11
+ ".agentic/handoff_state.yaml",
12
+ "docs/handoff/CURRENT_HANDOFF.md",
13
+ ]
14
+ OPERATIONAL_FRESHNESS_FILES = [
15
+ "docs/STATUS.md",
16
+ "docs/handoff/CURRENT_HANDOFF.md",
17
+ "docs/handoff/START_NEW_CHAT_PROMPT.md",
18
+ "docs/handoff/NEXT_CHAT_BOOTSTRAP.md",
19
+ ]
20
+ SUCCESSOR_HANDOFF_GLOBS = [
21
+ "*successor*handoff*.md",
22
+ "*successor*handoff*.yaml",
23
+ "*successor*handoff*.yml",
24
+ ]
25
+
26
+
27
+ def assess_handoff_prompt_freshness(
28
+ data: dict[str, Any],
29
+ handoff_path: str | Path = ".agentic/handoff_state.yaml",
30
+ *,
31
+ current_head: str | None = None,
32
+ current_subject: str | None = None,
33
+ successor_prompt_text: str | None = None,
34
+ intended_successor_prompt_path: str | Path | None = None,
35
+ ) -> list[str]:
36
+ """Return warnings when a successor handoff prompt may be stale.
37
+
38
+ The guard is intentionally warning-based. A stale repository still needs to
39
+ render a repairable prompt; it must not silently present that prompt as
40
+ authoritative.
41
+ """
42
+
43
+ state_path = Path(handoff_path)
44
+ project_root = _project_root_for_handoff_path(state_path)
45
+ warnings: list[str] = []
46
+
47
+ for relative_path in FRESHNESS_GUARD_FILES:
48
+ if not (project_root / relative_path).exists():
49
+ warnings.append(f"required handoff freshness file missing: {relative_path}")
50
+
51
+ detected_head = _short_commit(current_head) if current_head else _git_short_head(project_root)
52
+ detected_subject = current_subject if current_subject is not None else _git_head_subject(project_root)
53
+ detected_message = (
54
+ current_subject
55
+ if current_subject is not None
56
+ else _git_head_message(project_root)
57
+ )
58
+ expected_commits = _expected_handoff_commits(data)
59
+ admin_refresh_head = bool(
60
+ detected_head
61
+ and expected_commits
62
+ and not _commit_matches(detected_head, expected_commits)
63
+ and (
64
+ is_administrative_evidence_subject(detected_subject)
65
+ or _is_administrative_merge_commit(
66
+ project_root,
67
+ detected_head,
68
+ detected_message,
69
+ expected_commits,
70
+ )
71
+ )
72
+ )
73
+ if (
74
+ detected_head
75
+ and expected_commits
76
+ and not _commit_matches(detected_head, expected_commits)
77
+ and not admin_refresh_head
78
+ ):
79
+ joined = ", ".join(expected_commits)
80
+ warnings.append(
81
+ "current git HEAD "
82
+ f"{detected_head} is not represented by handoff safe/admin state ({joined})"
83
+ )
84
+
85
+ successor_prompt_path = _latest_successor_prompt_path(data, project_root)
86
+ if successor_prompt_path is None:
87
+ warnings.append("no successor handoff prompt was found under docs/reports/terminal")
88
+ return warnings
89
+
90
+ if not successor_prompt_path.exists():
91
+ intended_path = _resolve_project_path(project_root, intended_successor_prompt_path)
92
+ if successor_prompt_text is None or intended_path != successor_prompt_path:
93
+ relative_prompt = _relative_to_project(successor_prompt_path, project_root)
94
+ warnings.append(f"latest successor handoff prompt is missing: {relative_prompt}")
95
+ return warnings
96
+
97
+ marker_commits = expected_commits if admin_refresh_head else ([detected_head] if detected_head else expected_commits)
98
+ if successor_prompt_text is None:
99
+ prompt_text = successor_prompt_path.read_text(encoding="utf-8")
100
+ else:
101
+ prompt_text = successor_prompt_text
102
+ if marker_commits and not any(commit and commit in prompt_text for commit in marker_commits):
103
+ relative_prompt = _relative_to_project(successor_prompt_path, project_root)
104
+ joined = ", ".join(marker_commits)
105
+ warnings.append(
106
+ "latest successor handoff prompt "
107
+ f"{relative_prompt} does not mention current handoff commit marker(s): {joined}"
108
+ )
109
+
110
+ warnings.extend(
111
+ _assess_operational_document_freshness(
112
+ project_root,
113
+ expected_commits,
114
+ )
115
+ )
116
+
117
+ return warnings
118
+
119
+
120
+
121
+ def _assess_operational_document_freshness(
122
+ project_root: Path,
123
+ marker_commits: list[str],
124
+ ) -> list[str]:
125
+ """Return warnings when live operational docs do not mention current markers.
126
+
127
+ This intentionally reuses the existing handoff-freshness mechanism. It does
128
+ not introduce a new rule layer: STATUS, CURRENT_HANDOFF, chat bootstrap
129
+ prompts, and the active roadmap are human-facing projections of the
130
+ machine-readable handoff state and must mention at least one current
131
+ safe/admin commit marker when they exist. Stable authority sources such as
132
+ PROJECT_DIRECTION.yaml are intentionally excluded; they are required
133
+ context, not per-PR refresh projections.
134
+ """
135
+
136
+ markers = [marker for marker in marker_commits if marker]
137
+ if not markers:
138
+ return []
139
+
140
+ warnings: list[str] = []
141
+ for relative_path in OPERATIONAL_FRESHNESS_FILES:
142
+ path = project_root / relative_path
143
+ if not path.exists():
144
+ continue
145
+ content = path.read_text(encoding="utf-8", errors="replace")
146
+ if any(marker in content for marker in markers):
147
+ continue
148
+ joined = ", ".join(markers)
149
+ warnings.append(
150
+ f"operational handoff document {relative_path} does not mention "
151
+ f"current handoff commit marker(s): {joined}"
152
+ )
153
+ return warnings
154
+
155
+ def render_freshness_guard(warnings: list[str]) -> str:
156
+ if not warnings:
157
+ return ""
158
+ lines = [
159
+ "## Handoff Freshness Guard",
160
+ "",
161
+ "WARNING: this successor handoff prompt may be stale.",
162
+ "Refresh `docs/STATUS.md`, `.agentic/handoff_state.yaml`, "
163
+ "`docs/handoff/CURRENT_HANDOFF.md`, and the successor prompt before treating "
164
+ "this prompt as authoritative.",
165
+ "",
166
+ ]
167
+ lines.extend(f"- {warning}" for warning in warnings)
168
+ return "\n".join(lines).rstrip() + "\n"
169
+
170
+
171
+ def _short_commit(value: Any) -> str:
172
+ return str(value or "").strip()[:7]
173
+
174
+
175
+ def _project_root_for_handoff_path(path: Path) -> Path:
176
+ if path.name == "handoff_state.yaml" and path.parent.name == ".agentic":
177
+ return path.parent.parent
178
+ return Path.cwd()
179
+
180
+
181
+ def _resolve_project_path(project_root: Path, path: str | Path | None) -> Path | None:
182
+ if path is None:
183
+ return None
184
+ candidate = Path(path)
185
+ if candidate.is_absolute():
186
+ return candidate
187
+ return project_root / candidate
188
+
189
+
190
+ def _git_short_head(project_root: Path) -> str:
191
+ try:
192
+ return subprocess.check_output(
193
+ ["git", "rev-parse", "--short", "HEAD"],
194
+ cwd=project_root,
195
+ stderr=subprocess.DEVNULL,
196
+ text=True,
197
+ ).strip()
198
+ except (OSError, subprocess.CalledProcessError):
199
+ return ""
200
+
201
+
202
+ def _git_head_subject(project_root: Path) -> str:
203
+ try:
204
+ return subprocess.check_output(
205
+ ["git", "log", "-1", "--pretty=%s"],
206
+ cwd=project_root,
207
+ stderr=subprocess.DEVNULL,
208
+ text=True,
209
+ ).strip()
210
+ except (OSError, subprocess.CalledProcessError):
211
+ return ""
212
+
213
+
214
+ def _git_head_message(project_root: Path) -> str:
215
+ try:
216
+ return subprocess.check_output(
217
+ ["git", "log", "-1", "--pretty=%B"],
218
+ cwd=project_root,
219
+ stderr=subprocess.DEVNULL,
220
+ text=True,
221
+ ).strip()
222
+ except (OSError, subprocess.CalledProcessError):
223
+ return ""
224
+
225
+
226
+ def _git_commit_subject(project_root: Path, commit: str) -> str:
227
+ try:
228
+ return subprocess.check_output(
229
+ ["git", "log", "-1", "--pretty=%s", commit],
230
+ cwd=project_root,
231
+ stderr=subprocess.DEVNULL,
232
+ text=True,
233
+ ).strip()
234
+ except (OSError, subprocess.CalledProcessError):
235
+ return ""
236
+
237
+
238
+ def _git_commit_message(project_root: Path, commit: str) -> str:
239
+ try:
240
+ return subprocess.check_output(
241
+ ["git", "log", "-1", "--pretty=%B", commit],
242
+ cwd=project_root,
243
+ stderr=subprocess.DEVNULL,
244
+ text=True,
245
+ ).strip()
246
+ except (OSError, subprocess.CalledProcessError):
247
+ return ""
248
+
249
+
250
+ def _is_administrative_merge_message(message: str) -> bool:
251
+ lowered = message.lower()
252
+ if not lowered.startswith("merge pull request #"):
253
+ return False
254
+ administrative_terms = (
255
+ "handoff",
256
+ "successor",
257
+ "closeout",
258
+ "evidence",
259
+ "refresh",
260
+ "freshness",
261
+ )
262
+ return any(term in lowered for term in administrative_terms)
263
+
264
+
265
+ def _git_parent_commits(project_root: Path, commit: str) -> list[str]:
266
+ try:
267
+ parents = subprocess.check_output(
268
+ ["git", "show", "-s", "--pretty=%P", commit],
269
+ cwd=project_root,
270
+ stderr=subprocess.DEVNULL,
271
+ text=True,
272
+ ).strip()
273
+ except (OSError, subprocess.CalledProcessError):
274
+ return []
275
+ return [_short_commit(parent) for parent in parents.split() if parent.strip()]
276
+
277
+
278
+ def _is_administrative_merge_commit(
279
+ project_root: Path,
280
+ detected_head: str,
281
+ detected_message: str,
282
+ expected_commits: list[str],
283
+ ) -> bool:
284
+ """Return true for GitHub merge commits that only merge administrative handoff/evidence work.
285
+
286
+ A normal product merge must still trigger the freshness guard. Therefore the
287
+ check requires both a GitHub-style merge message and administrative wording
288
+ in the full merge message, plus a parent already represented by handoff
289
+ safe/admin state.
290
+ """
291
+
292
+ message = detected_message.lower()
293
+ if not _is_administrative_merge_message(message):
294
+ return False
295
+ parents = _git_parent_commits(project_root, detected_head)
296
+ if any(_commit_matches(parent, expected_commits) for parent in parents):
297
+ return True
298
+ return _has_expected_commit_in_first_parent_admin_chain(
299
+ project_root,
300
+ detected_head,
301
+ expected_commits,
302
+ )
303
+
304
+
305
+ def _has_expected_commit_in_first_parent_admin_chain(
306
+ project_root: Path,
307
+ start_commit: str,
308
+ expected_commits: list[str],
309
+ *,
310
+ max_depth: int = 20,
311
+ ) -> bool:
312
+ """Accept bounded chains of administrative GitHub merge commits.
313
+
314
+ This prevents a loop where every administrative merge commit requires a new
315
+ handoff-state refresh. The walk is deliberately first-parent and bounded:
316
+ product merges still warn unless every merge on the path is itself
317
+ administrative and the chain reaches an already represented commit.
318
+ """
319
+
320
+ current = start_commit
321
+ for _ in range(max_depth):
322
+ parents = _git_parent_commits(project_root, current)
323
+ if not parents:
324
+ return False
325
+ first_parent = parents[0]
326
+ if _commit_matches(first_parent, expected_commits):
327
+ return True
328
+ subject = _git_commit_subject(project_root, first_parent)
329
+ message = _git_commit_message(project_root, first_parent)
330
+ if not _is_administrative_merge_message(message or subject):
331
+ return False
332
+ current = first_parent
333
+ return False
334
+
335
+
336
+ def _expected_handoff_commits(data: dict[str, Any]) -> list[str]:
337
+ commits: list[str] = []
338
+ safe_state = data.get("safe_state", {})
339
+ if isinstance(safe_state, dict):
340
+ safe_commit = _short_commit(safe_state.get("commit"))
341
+ if safe_commit:
342
+ commits.append(safe_commit)
343
+ admin_state = data.get("administrative_evidence_state", {})
344
+ if isinstance(admin_state, dict):
345
+ admin_commit = _short_commit(admin_state.get("current_head"))
346
+ if admin_commit and admin_commit not in commits:
347
+ commits.append(admin_commit)
348
+ return commits
349
+
350
+
351
+ def _commit_matches(candidate: str, expected: list[str]) -> bool:
352
+ candidate = _short_commit(candidate)
353
+ return any(
354
+ candidate.startswith(commit) or commit.startswith(candidate)
355
+ for commit in expected
356
+ if commit
357
+ )
358
+
359
+
360
+ def _configured_successor_prompt_path(data: dict[str, Any], project_root: Path) -> Path | None:
361
+ handoff_maintenance = data.get("handoff_maintenance", {})
362
+ if isinstance(handoff_maintenance, dict):
363
+ configured = handoff_maintenance.get("latest_successor_prompt")
364
+ if configured:
365
+ return project_root / str(configured)
366
+ return None
367
+
368
+
369
+ def _latest_successor_prompt_path(data: dict[str, Any], project_root: Path) -> Path | None:
370
+ configured = _configured_successor_prompt_path(data, project_root)
371
+ if configured is not None:
372
+ return configured
373
+ terminal_dir = project_root / "docs" / "reports" / "terminal"
374
+ if not terminal_dir.exists():
375
+ return None
376
+ candidates = sorted(
377
+ candidate
378
+ for pattern in SUCCESSOR_HANDOFF_GLOBS
379
+ for candidate in terminal_dir.glob(pattern)
380
+ )
381
+ if not candidates:
382
+ return None
383
+ return candidates[-1]
384
+
385
+
386
+ def _relative_to_project(path: Path, project_root: Path) -> Path:
387
+ try:
388
+ return path.relative_to(project_root)
389
+ except ValueError:
390
+ return path
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agentic_project_kit.chat_bootloader import MANDATORY_BOOT_SOURCES
6
+ from agentic_project_kit.handoff_state import active_rules
7
+
8
+ MANDATORY_SUCCESSOR_CHAT_SOURCES = list(MANDATORY_BOOT_SOURCES) + [
9
+ "docs/TEST_GATES.md",
10
+ "relevant source files and tests for the requested slice",
11
+ ]
12
+
13
+ COMMUNICATION_SHORTCUTS = [
14
+ "d/D: local block appears finished; verify evidence before treating it as success",
15
+ "f/F: failure reported; inspect or upload evidence before asking for pasted output",
16
+ "w/W: continue within the current governance and evidence rules",
17
+ "paste-output: manual paste only when repo-backed or local evidence is unavailable or unusable",
18
+ "stop: no further mutation or terminal instructions",
19
+ ]
20
+
21
+ FORBIDDEN_PATTERNS = [
22
+ "mutating before reading the mandatory successor-chat sources",
23
+ "treating d as proof of success",
24
+ "asking for pasted output while usable local or remote evidence exists",
25
+ "using shell-only snippets as canonical cross-platform execution",
26
+ "using POSIX tools as correctness dependencies for portable workflows",
27
+ "printing REMOTE_EVIDENCE: PENDING in a final summary",
28
+ "adding handwritten legacy summary footers after the summary renderer",
29
+ "continuing product work after unresolved contract or evidence drift",
30
+ ]
31
+
32
+ SUMMARY_VOCABULARY = [
33
+ "WORK RESULT: PASS|FAIL|PENDING|HARD-FAIL|NO-COMMAND",
34
+ "EVIDENCE RESULT: PASS|FAIL|PARTIAL|CHAT_ONLY|NOT_REQUIRED",
35
+ "OVERALL RESULT: PASS|FAIL|PENDING|HARD-FAIL|NO-COMMAND",
36
+ "REMOTE_EVIDENCE: PASS|FAIL|PARTIAL|NOT_REQUIRED",
37
+ "NEXT_CHAT_REPLY: p|f|paste-output|continue|stop",
38
+ ]
39
+
40
+
41
+ def _bullet_lines(items: list[Any]) -> list[str]:
42
+ return [f"- {item}" for item in items]
43
+
44
+
45
+ def _rule_texts(data: dict[str, Any]) -> list[str]:
46
+ return [f"`{rule.get('id')}`: {rule.get('text')}" for rule in active_rules(data)]
47
+
48
+
49
+ def render_handoff_prompt(data: dict[str, Any]) -> str:
50
+ repo = data.get("repo", {})
51
+ safe_state = data.get("safe_state", {})
52
+ release = data.get("release", {})
53
+ admin_state = data.get("administrative_evidence_state", {})
54
+ if isinstance(data.get("open_items"), dict):
55
+ open_prs = data.get("open_items", {}).get("prs", [])
56
+ else:
57
+ open_prs = []
58
+ lines: list[str] = []
59
+ lines.extend([
60
+ "# Übergabeprompt",
61
+ "",
62
+ "## 1. Arbeitsumgebung",
63
+ "",
64
+ f"Local path: `{repo.get('local_path', '')}`",
65
+ f"Remote: `{repo.get('remote', '')}`",
66
+ f"Default branch: `{repo.get('default_branch', 'main')}`",
67
+ ])
68
+ lines.extend([
69
+ "",
70
+ "## 2. Sicherer Stand",
71
+ "",
72
+ f"Branch: `{safe_state.get('branch', '')}`",
73
+ f"Commit: `{safe_state.get('commit', '')}`",
74
+ f"Subject: {safe_state.get('commit_subject', '')}",
75
+ f"Semantics: `{safe_state.get('semantics', 'last_substantive_work_state')}`",
76
+ f"Working tree expected clean: `{safe_state.get('working_tree_expected_clean', '')}`",
77
+ ])
78
+ if isinstance(admin_state, dict) and admin_state:
79
+ admin_current_head = admin_state.get("current_head", "")
80
+ admin_current_head_subject = admin_state.get("current_head_subject", "")
81
+ admin_allowed_after_safe_state = admin_state.get("allowed_after_safe_state", "")
82
+ admin_reason = admin_state.get("reason", "")
83
+ lines.extend([
84
+ "",
85
+ "## 2a. Administrative Evidence State",
86
+ "",
87
+ "Administrative Evidence Commits nach dem fachlichen Safe-State sind erlaubt, wenn sie nur Logs, Handoff, Summary oder Evidence aktualisieren. Sie ändern den fachlichen Safe-State nicht.",
88
+ "",
89
+ f"Current HEAD at generation time: `{admin_current_head}`",
90
+ f"HEAD subject: {admin_current_head_subject}",
91
+ f"Allowed after safe state: `{admin_allowed_after_safe_state}`",
92
+ f"Reason: {admin_reason}",
93
+ ])
94
+ lines.extend([
95
+ "",
96
+ "## 3. Release- und Produktstand",
97
+ "",
98
+ f"Current version: `{release.get('current_version', '')}`",
99
+ f"Previous version: `{release.get('previous_version', '')}`",
100
+ f"Tag: `{release.get('tag', '')}`",
101
+ f"Zenodo concept DOI: `{release.get('zenodo_concept_doi', '')}`",
102
+ f"Zenodo version DOI: `{release.get('zenodo_version_doi', '')}`",
103
+ f"Post-release check: `{release.get('post_release_check', '')}`",
104
+ ])
105
+ lines.extend([
106
+ "",
107
+ "## 4. Pflichtquellen vor jeder Mutation",
108
+ "",
109
+ "Lies diese Quellen zuerst. Wenn eine Quelle fehlt, widersprüchlich ist oder nicht gelesen werden kann, melde Drift und mutiere nicht außer zur Drift-Reparatur.",
110
+ "",
111
+ ])
112
+ lines.extend(_bullet_lines(MANDATORY_SUCCESSOR_CHAT_SOURCES))
113
+ lines.extend([
114
+ "",
115
+ "## 4a. Bootloader",
116
+ "",
117
+ "Beim Chatwechsel zuerst den Bootloader ausführen: `agentic-kit boot prompt` oder `./ns chat-bootloader`.",
118
+ "",
119
+ "## 5. Kommunikations- und Summary-Regeln",
120
+ "",
121
+ "User-Kürzel sind Kommunikationssignale, keine Evidence:",
122
+ "",
123
+ ])
124
+ lines.extend(_bullet_lines(COMMUNICATION_SHORTCUTS))
125
+ lines.extend(["", "Final-Summary-Vokabular:", ""])
126
+ lines.extend(_bullet_lines(SUMMARY_VOCABULARY))
127
+ lines.extend([
128
+ "",
129
+ "## 6. Aktive Regeln aus handoff_state",
130
+ "",
131
+ ])
132
+ lines.extend(_bullet_lines(_rule_texts(data)))
133
+ lines.extend(["", "## 7. Offene Punkte", ""])
134
+ if open_prs:
135
+ for pr in open_prs:
136
+ lines.append(f"- PR #{pr.get('number')}: {pr.get('title', '')}")
137
+ else:
138
+ lines.append("- Keine offenen PRs im handoff_state.")
139
+ lines.extend(["", "## 8. Abgeschlossen seit letzter Übergabe", ""])
140
+ completed = data.get("completed_since_previous_handoff", [])
141
+ if completed:
142
+ lines.extend(_bullet_lines(completed))
143
+ else:
144
+ lines.append("- Keine Einträge.")
145
+ lines.extend(["", "## 9. Letzte bekannte Fehler- und Driftmuster", ""])
146
+ for item in data.get("recent_failure_patterns", []):
147
+ if isinstance(item, dict):
148
+ lines.append(f"- `{item.get('id')}`: {item.get('prevention') or item.get('description', '')}")
149
+ lines.extend(["", "## 10. Verbotene Muster", ""])
150
+ lines.extend(_bullet_lines(FORBIDDEN_PATTERNS))
151
+ lines.extend(["", "## 11. Nächste erlaubte Aufgaben", ""])
152
+ tasks = data.get("next_allowed_tasks", [])
153
+ if tasks:
154
+ for task in tasks:
155
+ if isinstance(task, dict):
156
+ lines.append(f"- {task.get('priority', '?')}. `{task.get('id', '')}` — {task.get('title', '')}")
157
+ else:
158
+ lines.append("- Keine Aufgaben im handoff_state eingetragen.")
159
+ lines.extend(["", "## 12. Gesperrte Aufgaben", ""])
160
+ blocked = data.get("blocked_until_closeout", [])
161
+ if blocked:
162
+ lines.extend(_bullet_lines(blocked))
163
+ else:
164
+ lines.append("- Keine gesperrten Aufgaben im handoff_state eingetragen.")
165
+ lines.extend([
166
+ "",
167
+ "## 13. Erste Arbeitsanweisung",
168
+ "",
169
+ data.get("first_instruction", ""),
170
+ "",
171
+ "## 14. Arbeitsmodus für den Nachfolge-Chat",
172
+ "",
173
+ "1. Führe zuerst den Bootloader aus.",
174
+ "2. Lies danach alle Pflichtquellen aus Abschnitt 4.",
175
+ "3. Rekonstruiere den aktuellen Stand aus Repo, PR/CI, Logs und Summary, nicht aus Chat-Erinnerung.",
176
+ "4. Prüfe Drift zwischen Regeln, Tests, Summary-Renderer, Status und Handoff.",
177
+ "5. Bei Drift: warnen, keine Produktmutation, Handoff-Prompt oder Drift-Fix anbieten.",
178
+ "6. Arbeite nur in kleinen, testbaren Slices mit ehrlicher Evidence.",
179
+ ])
180
+ return "\n".join(lines).rstrip() + "\n"