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,437 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import subprocess
9
+ from typing import Any
10
+
11
+ from agentic_project_kit.rule_refresh import COMMUNICATION_RULES_OUTPUT, RuleRefreshResult
12
+
13
+
14
+ PENDING_STATE_PATH = Path(".agentic/rule_ack/communication_refresh_pending.json")
15
+ ACK_STATE_PATH = Path(".agentic/rule_ack/communication_refresh_ack.json")
16
+ COMMUNICATION_RULES_REMOTE_REF = "main"
17
+
18
+ REQUIRED_LOADED_SECTIONS = (
19
+ "NO_COPY_TERMINAL_EVIDENCE",
20
+ "CHAT_COMMUNICATION_CONTRACT",
21
+ "PORTABLE_CHAT_EXECUTION_CONTRACT",
22
+ "CHAT_BOOTSTRAP_AND_DRIFT_CONTRACT",
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class CommunicationRuleCapsule:
28
+ result_status: str
29
+ kind: str
30
+ next_reply: str
31
+ local_path: str
32
+ remote_path: str
33
+ commit: str
34
+ blob_sha: str
35
+ generated_at: str
36
+ must_read_before_continue: bool
37
+ local_only: bool
38
+ remote_readable: bool
39
+ pending_state_path: str | None = None
40
+ next_action: str = ""
41
+ remote_ref: str = COMMUNICATION_RULES_REMOTE_REF
42
+
43
+ def as_json_data(self) -> dict[str, object]:
44
+ return {
45
+ "schema_version": 1,
46
+ "result_status": self.result_status,
47
+ "kind": self.kind,
48
+ "next_reply": self.next_reply,
49
+ "local_path": self.local_path,
50
+ "remote_path": self.remote_path,
51
+ "commit": self.commit,
52
+ "blob_sha": self.blob_sha,
53
+ "generated_at": self.generated_at,
54
+ "must_read_before_continue": self.must_read_before_continue,
55
+ "local_only": self.local_only,
56
+ "remote_readable": self.remote_readable,
57
+ "pending_state_path": self.pending_state_path,
58
+ "next_action": self.next_action,
59
+ "remote_ref": self.remote_ref,
60
+ }
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class CommunicationContextGateResult:
65
+ result_status: str
66
+ reason: str
67
+ communication_context_fresh: bool
68
+ required_next_reply: str | None
69
+ remote_path: str
70
+ remote_ref: str
71
+ expected_blob_sha: str | None
72
+ generated_at: str | None
73
+ ack_source: str | None = None
74
+ ack_blob_sha: str | None = None
75
+ next_action: str = ""
76
+
77
+ def as_json_data(self) -> dict[str, object]:
78
+ return {
79
+ "schema_version": 1,
80
+ "kind": "communication_context_gate",
81
+ "result_status": self.result_status,
82
+ "reason": self.reason,
83
+ "communication_context_fresh": self.communication_context_fresh,
84
+ "required_next_reply": self.required_next_reply,
85
+ "remote_path": self.remote_path,
86
+ "remote_ref": self.remote_ref,
87
+ "expected_blob_sha": self.expected_blob_sha,
88
+ "generated_at": self.generated_at,
89
+ "ack_source": self.ack_source,
90
+ "ack_blob_sha": self.ack_blob_sha,
91
+ "next_action": self.next_action,
92
+ }
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class CommunicationRefreshAckResult:
97
+ result_status: str
98
+ reason: str
99
+ ack_path: str
100
+ source: str | None
101
+ blob_sha: str | None
102
+ generated_at: str | None
103
+ loaded_sections: tuple[str, ...]
104
+ rules_loaded: bool
105
+ written_state_path: str | None = None
106
+ next_action: str = ""
107
+
108
+ def as_json_data(self) -> dict[str, object]:
109
+ return {
110
+ "schema_version": 1,
111
+ "kind": "communication_rule_refresh_ack_result",
112
+ "result_status": self.result_status,
113
+ "reason": self.reason,
114
+ "ack_path": self.ack_path,
115
+ "source": self.source,
116
+ "blob_sha": self.blob_sha,
117
+ "generated_at": self.generated_at,
118
+ "loaded_sections": list(self.loaded_sections),
119
+ "rules_loaded": self.rules_loaded,
120
+ "written_state_path": self.written_state_path,
121
+ "next_action": self.next_action,
122
+ }
123
+
124
+
125
+ def build_communication_rule_capsule(
126
+ project_root: Path | str,
127
+ result: RuleRefreshResult,
128
+ *,
129
+ publish: bool,
130
+ write_pending: bool = False,
131
+ ) -> CommunicationRuleCapsule:
132
+ root = Path(project_root).resolve()
133
+ local_path = Path(result.output_path)
134
+ absolute = root / local_path
135
+ data = absolute.read_bytes()
136
+ blob_sha = git_blob_sha(data)
137
+ commit = _git_output(root, "rev-parse", "HEAD") or "UNKNOWN"
138
+ tracked_blob_sha = _git_output(root, "rev-parse", f"HEAD:{local_path.as_posix()}")
139
+ path_dirty = bool(_git_output(root, "status", "--porcelain", "--", local_path.as_posix()))
140
+ remote_readable = bool(publish and tracked_blob_sha == blob_sha and not path_dirty)
141
+ generated_at = _extract_generated_at(absolute)
142
+ pending_path: str | None = None
143
+ next_action = "Rule capsule generated locally."
144
+ if publish:
145
+ next_action = (
146
+ "Send d2 only after the rule capsule is committed and remote-readable."
147
+ if not remote_readable
148
+ else "Send d2; assistant must read the remote rule capsule and ACK it before mutation."
149
+ )
150
+ if write_pending:
151
+ pending = pending_state_from_capsule(
152
+ CommunicationRuleCapsule(
153
+ result_status="PASS",
154
+ kind="communication_rule_refresh",
155
+ next_reply=result.next_reply,
156
+ local_path=result.output_path,
157
+ remote_path=local_path.as_posix(),
158
+ commit=commit,
159
+ blob_sha=blob_sha,
160
+ generated_at=generated_at,
161
+ must_read_before_continue=publish,
162
+ local_only=not publish,
163
+ remote_readable=remote_readable,
164
+ next_action=next_action,
165
+ )
166
+ )
167
+ target = root / PENDING_STATE_PATH
168
+ target.parent.mkdir(parents=True, exist_ok=True)
169
+ target.write_text(json.dumps(pending, indent=2, sort_keys=True) + "\n", encoding="utf-8")
170
+ pending_path = PENDING_STATE_PATH.as_posix()
171
+ return CommunicationRuleCapsule(
172
+ result_status="PASS",
173
+ kind="communication_rule_refresh",
174
+ next_reply=result.next_reply,
175
+ local_path=result.output_path,
176
+ remote_path=local_path.as_posix(),
177
+ commit=commit,
178
+ blob_sha=blob_sha,
179
+ generated_at=generated_at,
180
+ must_read_before_continue=publish,
181
+ local_only=not publish,
182
+ remote_readable=remote_readable,
183
+ pending_state_path=pending_path,
184
+ next_action=next_action,
185
+ )
186
+
187
+
188
+ def pending_state_from_capsule(capsule: CommunicationRuleCapsule) -> dict[str, object]:
189
+ return {
190
+ "schema_version": 1,
191
+ "required_next_reply": "d2",
192
+ "required_action": "read_remote_communication_rules",
193
+ "remote_path": capsule.remote_path,
194
+ "remote_ref": capsule.remote_ref,
195
+ "local_path": capsule.local_path,
196
+ "expected_blob_sha": capsule.blob_sha,
197
+ "generated_at": capsule.generated_at,
198
+ "commit": capsule.commit,
199
+ "blocks_normal_go": True,
200
+ "local_only": capsule.local_only,
201
+ "remote_readable": capsule.remote_readable,
202
+ "next_action": capsule.next_action,
203
+ }
204
+
205
+
206
+ def require_current_communication_context(
207
+ project_root: Path | str = ".",
208
+ ) -> CommunicationContextGateResult:
209
+ root = Path(project_root).resolve()
210
+ pending = _read_json(root / PENDING_STATE_PATH)
211
+ if not pending:
212
+ ack = _read_json(root / ACK_STATE_PATH)
213
+ return CommunicationContextGateResult(
214
+ result_status="PASS",
215
+ reason="communication_context_current",
216
+ communication_context_fresh=True,
217
+ required_next_reply=None,
218
+ remote_path=str(ack.get("source", COMMUNICATION_RULES_OUTPUT.as_posix())) if ack else COMMUNICATION_RULES_OUTPUT.as_posix(),
219
+ remote_ref=str(ack.get("remote", COMMUNICATION_RULES_REMOTE_REF)) if ack else COMMUNICATION_RULES_REMOTE_REF,
220
+ expected_blob_sha=None,
221
+ generated_at=str(ack.get("generated_at")) if ack else None,
222
+ ack_source=str(ack.get("source")) if ack else None,
223
+ ack_blob_sha=str(ack.get("blob_sha")) if ack else None,
224
+ next_action="Communication context is current; mutation may continue.",
225
+ )
226
+
227
+ remote_path = str(pending.get("remote_path", COMMUNICATION_RULES_OUTPUT.as_posix()))
228
+ remote_ref = str(pending.get("remote_ref", COMMUNICATION_RULES_REMOTE_REF))
229
+ expected_blob_sha = str(pending.get("expected_blob_sha", ""))
230
+ generated_at = str(pending.get("generated_at", ""))
231
+ remote_readable = bool(pending.get("remote_readable", False)) or _pending_capsule_is_remote_readable(
232
+ root,
233
+ remote_path=remote_path,
234
+ remote_ref=remote_ref,
235
+ expected_blob_sha=expected_blob_sha,
236
+ )
237
+ if not remote_readable:
238
+ return CommunicationContextGateResult(
239
+ result_status="BLOCK",
240
+ reason="communication_rule_refresh_not_remote_readable",
241
+ communication_context_fresh=False,
242
+ required_next_reply="d2",
243
+ remote_path=remote_path,
244
+ remote_ref=remote_ref,
245
+ expected_blob_sha=expected_blob_sha,
246
+ generated_at=generated_at,
247
+ next_action="Commit and publish the rule capsule before sending d2.",
248
+ )
249
+ return CommunicationContextGateResult(
250
+ result_status="BLOCK",
251
+ reason="communication_rule_refresh_pending",
252
+ communication_context_fresh=False,
253
+ required_next_reply="d2",
254
+ remote_path=remote_path,
255
+ remote_ref=remote_ref,
256
+ expected_blob_sha=expected_blob_sha,
257
+ generated_at=generated_at,
258
+ next_action="Assistant must read the remote rule capsule and provide RULE_REFRESH_ACK before mutation.",
259
+ )
260
+
261
+
262
+ def _pending_capsule_is_remote_readable(
263
+ root: Path,
264
+ *,
265
+ remote_path: str,
266
+ remote_ref: str,
267
+ expected_blob_sha: str,
268
+ ) -> bool:
269
+ if not remote_path or not expected_blob_sha:
270
+ return False
271
+ if _git_output(root, "status", "--porcelain", "--", remote_path):
272
+ return False
273
+ candidates = [_remote_tracking_ref(remote_ref)]
274
+ upstream = _git_output(root, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
275
+ if upstream:
276
+ candidates.insert(0, upstream)
277
+ for ref in dict.fromkeys(candidates):
278
+ if _git_output(root, "rev-parse", f"{ref}:{remote_path}") == expected_blob_sha:
279
+ return True
280
+ return False
281
+
282
+
283
+ def _remote_tracking_ref(ref: str) -> str:
284
+ normalized = ref.strip() or COMMUNICATION_RULES_REMOTE_REF
285
+ if normalized.startswith(("origin/", "refs/")):
286
+ return normalized
287
+ return f"origin/{normalized}"
288
+
289
+
290
+ def acknowledge_communication_refresh(
291
+ project_root: Path | str,
292
+ ack_file: Path | str,
293
+ ) -> CommunicationRefreshAckResult:
294
+ root = Path(project_root).resolve()
295
+ ack_path = Path(ack_file)
296
+ if not ack_path.is_absolute():
297
+ ack_path = root / ack_path
298
+ try:
299
+ ack = parse_ack_text(ack_path.read_text(encoding="utf-8"))
300
+ except OSError as exc:
301
+ return _ack_block(str(ack_path), "ack_file_missing", str(exc))
302
+ pending = _read_json(root / PENDING_STATE_PATH)
303
+ if not pending:
304
+ return _ack_block(str(ack_path), "pending_state_missing", "No communication refresh pending state exists.")
305
+
306
+ problems = _ack_problems(ack, pending)
307
+ if problems:
308
+ return _ack_block(str(ack_path), ",".join(problems), "ACK does not match pending communication refresh.", ack=ack)
309
+
310
+ state = {
311
+ "schema_version": 1,
312
+ "kind": "communication_rule_refresh_ack",
313
+ "result_status": "PASS",
314
+ "source": ack["source"],
315
+ "remote": ack.get("remote", pending.get("remote_ref", COMMUNICATION_RULES_REMOTE_REF)),
316
+ "blob_sha": ack["blob_sha"],
317
+ "generated_at": ack["generated_at"],
318
+ "loaded_sections": list(ack["loaded_sections"]),
319
+ "rules_loaded": True,
320
+ "acknowledged_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
321
+ "communication_context_fresh": True,
322
+ }
323
+ target = root / ACK_STATE_PATH
324
+ target.parent.mkdir(parents=True, exist_ok=True)
325
+ target.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8")
326
+ (root / PENDING_STATE_PATH).unlink(missing_ok=True)
327
+ return CommunicationRefreshAckResult(
328
+ result_status="PASS",
329
+ reason="communication_rule_refresh_acknowledged",
330
+ ack_path=str(ack_path),
331
+ source=str(ack["source"]),
332
+ blob_sha=str(ack["blob_sha"]),
333
+ generated_at=str(ack["generated_at"]),
334
+ loaded_sections=tuple(str(item) for item in ack["loaded_sections"]),
335
+ rules_loaded=True,
336
+ written_state_path=ACK_STATE_PATH.as_posix(),
337
+ next_action="Communication context ACK accepted; mutation may continue.",
338
+ )
339
+
340
+
341
+ def parse_ack_text(text: str) -> dict[str, Any]:
342
+ stripped = text.strip()
343
+ if stripped.startswith("{"):
344
+ data = json.loads(stripped)
345
+ else:
346
+ data: dict[str, Any] = {}
347
+ for line in stripped.splitlines():
348
+ if not line or line == "RULE_REFRESH_ACK" or "=" not in line:
349
+ continue
350
+ key, value = line.split("=", 1)
351
+ data[key.strip()] = value.strip()
352
+ loaded = data.get("loaded_sections", [])
353
+ if isinstance(loaded, str):
354
+ data["loaded_sections"] = [item.strip() for item in loaded.split(",") if item.strip()]
355
+ data["rules_loaded"] = str(data.get("rules_loaded", "")).lower() == "true" or data.get("rules_loaded") is True
356
+ return data
357
+
358
+
359
+ def git_blob_sha(data: bytes) -> str:
360
+ return hashlib.sha1(b"blob " + str(len(data)).encode("ascii") + b"\0" + data).hexdigest()
361
+
362
+
363
+ def _ack_problems(ack: dict[str, Any], pending: dict[str, object]) -> list[str]:
364
+ problems: list[str] = []
365
+ if ack.get("source") != pending.get("remote_path"):
366
+ problems.append("source_mismatch")
367
+ expected_remote = str(pending.get("remote_ref", COMMUNICATION_RULES_REMOTE_REF))
368
+ if str(ack.get("remote", expected_remote)) != expected_remote:
369
+ problems.append("remote_ref_mismatch")
370
+ if ack.get("blob_sha") != pending.get("expected_blob_sha"):
371
+ problems.append("blob_sha_mismatch")
372
+ if ack.get("generated_at") != pending.get("generated_at"):
373
+ problems.append("generated_at_mismatch")
374
+ if not ack.get("rules_loaded"):
375
+ problems.append("rules_not_loaded")
376
+ loaded = set(str(item) for item in ack.get("loaded_sections", []))
377
+ missing_sections = [section for section in REQUIRED_LOADED_SECTIONS if section not in loaded]
378
+ if missing_sections:
379
+ problems.append("missing_loaded_sections")
380
+ if ack.get("result_status") != "PASS":
381
+ problems.append("result_status_not_pass")
382
+ if ack.get("kind") not in {None, "communication_rule_refresh_ack"}:
383
+ problems.append("kind_mismatch")
384
+ return problems
385
+
386
+
387
+ def _ack_block(
388
+ ack_path: str,
389
+ reason: str,
390
+ next_action: str,
391
+ *,
392
+ ack: dict[str, Any] | None = None,
393
+ ) -> CommunicationRefreshAckResult:
394
+ ack = ack or {}
395
+ loaded = ack.get("loaded_sections", ())
396
+ if isinstance(loaded, str):
397
+ loaded = tuple(item.strip() for item in loaded.split(",") if item.strip())
398
+ return CommunicationRefreshAckResult(
399
+ result_status="BLOCK",
400
+ reason=reason,
401
+ ack_path=ack_path,
402
+ source=str(ack.get("source")) if ack.get("source") is not None else None,
403
+ blob_sha=str(ack.get("blob_sha")) if ack.get("blob_sha") is not None else None,
404
+ generated_at=str(ack.get("generated_at")) if ack.get("generated_at") is not None else None,
405
+ loaded_sections=tuple(str(item) for item in loaded),
406
+ rules_loaded=bool(ack.get("rules_loaded", False)),
407
+ next_action=next_action,
408
+ )
409
+
410
+
411
+ def _extract_generated_at(path: Path) -> str:
412
+ for line in path.read_text(encoding="utf-8").splitlines():
413
+ if line.startswith("Generated at:"):
414
+ return line.split(":", 1)[1].strip()
415
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
416
+
417
+
418
+ def _git_output(root: Path, *args: str) -> str:
419
+ completed = subprocess.run(
420
+ ["git", *args],
421
+ cwd=root,
422
+ text=True,
423
+ stdout=subprocess.PIPE,
424
+ stderr=subprocess.PIPE,
425
+ check=False,
426
+ )
427
+ if completed.returncode != 0:
428
+ return ""
429
+ return completed.stdout.strip()
430
+
431
+
432
+ def _read_json(path: Path) -> dict[str, Any]:
433
+ try:
434
+ data = json.loads(path.read_text(encoding="utf-8"))
435
+ except (OSError, json.JSONDecodeError):
436
+ return {}
437
+ return data if isinstance(data, dict) else {}
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ DEFAULT_STATE_PATH = Path(".agentic/communication_state.json")
13
+ VALID_ORIGINS = {"local", "remote", "mixed"}
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class CommunicationStateCheck:
18
+ ok: bool
19
+ findings: tuple[str, ...]
20
+
21
+
22
+ def format_summary_id(value: int) -> str:
23
+ if value < 0:
24
+ raise ValueError("summary id must be non-negative")
25
+ return f"COMM-{value:05d}"
26
+
27
+
28
+ def current_branch() -> str:
29
+ result = subprocess.run(["git", "branch", "--show-current"], text=True, capture_output=True, check=False)
30
+ return result.stdout.strip() or "unknown"
31
+
32
+
33
+ def load_state(path: Path = DEFAULT_STATE_PATH) -> dict[str, Any]:
34
+ return json.loads(path.read_text(encoding="utf-8"))
35
+
36
+
37
+ def write_state(state: dict[str, Any], path: Path = DEFAULT_STATE_PATH) -> None:
38
+ path.parent.mkdir(parents=True, exist_ok=True)
39
+ path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8")
40
+
41
+
42
+ def check_state(path: Path = DEFAULT_STATE_PATH) -> CommunicationStateCheck:
43
+ findings: list[str] = []
44
+ if not path.exists():
45
+ return CommunicationStateCheck(False, ("missing_state_file",))
46
+ try:
47
+ state = load_state(path)
48
+ except json.JSONDecodeError:
49
+ return CommunicationStateCheck(False, ("invalid_json",))
50
+ if state.get("schema_version") != 1:
51
+ findings.append("invalid_schema_version")
52
+ summary_id = state.get("last_summary_id")
53
+ if not isinstance(summary_id, int) or summary_id < 0:
54
+ findings.append("invalid_last_summary_id")
55
+ origin = state.get("last_origin")
56
+ if origin is not None and origin not in VALID_ORIGINS:
57
+ findings.append("invalid_last_origin")
58
+ return CommunicationStateCheck(not findings, tuple(findings) if findings else ("none",))
59
+
60
+
61
+ def next_summary_header(
62
+ path: Path = DEFAULT_STATE_PATH,
63
+ origin: str = "local",
64
+ branch: str | None = None,
65
+ slice_name: str = "",
66
+ write: bool = True,
67
+ ) -> str:
68
+ if origin not in VALID_ORIGINS:
69
+ raise ValueError(f"invalid origin: {origin}")
70
+ if write and not slice_name.strip():
71
+ raise ValueError("slice_name must not be empty when writing communication summary state")
72
+ state = load_state(path)
73
+ next_id = int(state.get("last_summary_id", 0)) + 1
74
+ now = datetime.now().astimezone()
75
+ branch_name = branch or current_branch()
76
+ header = f"SUMMARY {format_summary_id(next_id)} | {now:%Y-%m-%d %H:%M:%S %z}"
77
+ if write:
78
+ state["last_summary_id"] = next_id
79
+ state["updated_at"] = now.isoformat(timespec="seconds")
80
+ state["last_origin"] = origin
81
+ state["last_branch"] = branch_name
82
+ state["last_slice"] = slice_name
83
+ write_state(state, path)
84
+ return header
85
+
86
+
87
+ def main(argv: list[str] | None = None) -> int:
88
+ parser = argparse.ArgumentParser(prog="communication-state")
89
+ sub = parser.add_subparsers(dest="command", required=True)
90
+ check_parser = sub.add_parser("check")
91
+ check_parser.add_argument("--state-path", default=str(DEFAULT_STATE_PATH))
92
+ next_parser = sub.add_parser("next-summary")
93
+ next_parser.add_argument("--state-path", default=str(DEFAULT_STATE_PATH))
94
+ next_parser.add_argument("--origin", choices=sorted(VALID_ORIGINS), default="local")
95
+ next_parser.add_argument("--branch", default=None)
96
+ next_parser.add_argument("--slice", default="")
97
+ next_parser.add_argument("--no-write", action="store_true")
98
+ args = parser.parse_args(argv)
99
+ path = Path(args.state_path)
100
+ if args.command == "check":
101
+ result = check_state(path)
102
+ print("COMMUNICATION STATE CHECK")
103
+ print(f"state_path={path}")
104
+ print(f"state={'PASS' if result.ok else 'FAIL'}")
105
+ print("findings:")
106
+ for finding in result.findings:
107
+ print(f" - {finding}")
108
+ print(f"### RESULT: {'PASS' if result.ok else 'FAIL'} ###")
109
+ return 0 if result.ok else 1
110
+ print(next_summary_header(path, args.origin, args.branch, args.slice, not args.no_write))
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__":
115
+ raise SystemExit(main())
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Iterable, Protocol
5
+
6
+
7
+ class StepLike(Protocol):
8
+ name: str
9
+ result: object
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class WrapperInvariantViolation:
14
+ wrapper: str
15
+ step_name: str
16
+ step_status: str
17
+ wrapper_status: str
18
+ allowed_recovery_states: tuple[str, ...]
19
+ message: str
20
+
21
+
22
+ def _status_of(result: object) -> str:
23
+ value = getattr(result, "result_status", "")
24
+ return str(value or "").upper()
25
+
26
+
27
+ def _state_of(result: object) -> str:
28
+ value = getattr(result, "lifecycle_state", "")
29
+ if not value:
30
+ value = getattr(result, "state", "")
31
+ return str(value or "").upper()
32
+
33
+
34
+ def validate_no_failed_required_step_promoted_to_pass(
35
+ *,
36
+ wrapper: str,
37
+ wrapper_status: str,
38
+ steps: Iterable[StepLike],
39
+ allowed_recovery_states: Iterable[str] = (),
40
+ ) -> list[WrapperInvariantViolation]:
41
+ """Validate the central composed-wrapper safety invariant.
42
+
43
+ A composed wrapper must not report PASS when a required substep reported
44
+ FAIL/BLOCKED unless that substep state is explicitly allowlisted as a
45
+ recovery state by the wrapper.
46
+
47
+ The helper is intentionally small and pure so lifecycle tests can use it
48
+ without shelling out or creating repository state.
49
+ """
50
+
51
+ normalized_wrapper_status = str(wrapper_status or "").upper()
52
+ if normalized_wrapper_status != "PASS":
53
+ return []
54
+
55
+ allowed = tuple(str(item or "").upper() for item in allowed_recovery_states)
56
+ violations: list[WrapperInvariantViolation] = []
57
+
58
+ for step in steps:
59
+ result = getattr(step, "result", None)
60
+ step_status = _status_of(result)
61
+ step_state = _state_of(result)
62
+ if step_status not in {"FAIL", "BLOCKED"}:
63
+ continue
64
+ if step_state and step_state in allowed:
65
+ continue
66
+ violations.append(
67
+ WrapperInvariantViolation(
68
+ wrapper=wrapper,
69
+ step_name=str(getattr(step, "name", "")),
70
+ step_status=step_status,
71
+ wrapper_status=normalized_wrapper_status,
72
+ allowed_recovery_states=allowed,
73
+ message=(
74
+ f"{wrapper} returned PASS although required step "
75
+ f"{getattr(step, 'name', '')!s} returned {step_status}."
76
+ ),
77
+ )
78
+ )
79
+
80
+ return violations
81
+
82
+
83
+ def assert_no_failed_required_step_promoted_to_pass(
84
+ *,
85
+ wrapper: str,
86
+ wrapper_status: str,
87
+ steps: Iterable[StepLike],
88
+ allowed_recovery_states: Iterable[str] = (),
89
+ ) -> None:
90
+ violations = validate_no_failed_required_step_promoted_to_pass(
91
+ wrapper=wrapper,
92
+ wrapper_status=wrapper_status,
93
+ steps=steps,
94
+ allowed_recovery_states=allowed_recovery_states,
95
+ )
96
+ if violations:
97
+ raise AssertionError("; ".join(item.message for item in violations))