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,421 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import asdict, dataclass
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+ LOCK_MARKER = "workspace_mutation_lock"
9
+
10
+ @dataclass(frozen=True)
11
+ class MutationLockFindingClassification:
12
+ """Human-facing classification for mutation-lock audit findings."""
13
+
14
+ kind: str
15
+ counts_as_blocking: bool
16
+ rationale: str
17
+
18
+ def to_dict(self) -> dict[str, object]:
19
+ return {
20
+ "kind": self.kind,
21
+ "counts_as_blocking": self.counts_as_blocking,
22
+ "rationale": self.rationale,
23
+ }
24
+
25
+
26
+ MODULE_LEVEL_STRING_TABLE_PATHS = frozenset(
27
+ {
28
+ "src/agentic_project_kit/command_manifest.py",
29
+ "src/agentic_project_kit/work_order_validator.py",
30
+ "src/agentic_project_kit/command_inbox_check.py",
31
+ "src/agentic_project_kit/composite_short_circuit.py",
32
+ "src/agentic_project_kit/llm_execution_context.py",
33
+ }
34
+ )
35
+
36
+ METADATA_LITERAL_SYMBOLS = frozenset(
37
+ {
38
+ "_category_for",
39
+ "_composition_level_for",
40
+ "_task_tags",
41
+ "_button",
42
+ "_actions",
43
+ }
44
+ )
45
+
46
+ GENERATED_REFERENCE_SYMBOLS = frozenset(
47
+ {
48
+ "write_reference",
49
+ "render_markdown",
50
+ "commands_render_md_command",
51
+ "direction_render_command",
52
+ }
53
+ )
54
+
55
+ REPORT_WRITER_SYMBOL_HINTS = (
56
+ "write_",
57
+ "_write_",
58
+ "report",
59
+ "evidence",
60
+ "summary",
61
+ "log",
62
+ "finalize",
63
+ )
64
+
65
+ RUNTIME_MUTATION_SYMBOL_HINTS = (
66
+ "branch",
67
+ "commit",
68
+ "push",
69
+ "merge",
70
+ "restore",
71
+ "sync",
72
+ "pr_",
73
+ "release",
74
+ )
75
+
76
+ CORE_RUNTIME_MUTATOR_SYMBOLS = frozenset(
77
+ {
78
+ "admin_refresh_pr",
79
+ "_admin_refresh_pr_unlocked",
80
+ "branch_create",
81
+ "branch_delete",
82
+ "branch_switch",
83
+ "commit_paths",
84
+ "_commit_paths_unlocked",
85
+ "pr_create",
86
+ "_pr_create_unlocked",
87
+ "pr_merge_safe",
88
+ "_pr_merge_safe_unlocked",
89
+ "pull_current",
90
+ "push_current",
91
+ "_push_current_unlocked",
92
+ }
93
+ )
94
+
95
+
96
+ def _finding_value(finding: object, key: str) -> str:
97
+ if isinstance(finding, Mapping):
98
+ return str(finding.get(key) or "")
99
+ return str(getattr(finding, key, "") or "")
100
+
101
+
102
+ def _is_enforced_core_runtime_path(path: str) -> bool:
103
+ return path == "src/agentic_project_kit/transfer_repo_actions.py"
104
+
105
+
106
+ def classify_mutation_lock_finding(finding: object) -> MutationLockFindingClassification:
107
+ """Classify a mutation-lock audit finding without changing audit behavior."""
108
+
109
+ path = _finding_value(finding, "path")
110
+ symbol = _finding_value(finding, "symbol")
111
+ category = _finding_value(finding, "category")
112
+ reason = _finding_value(finding, "reason")
113
+
114
+ symbol_l = symbol.lower()
115
+
116
+ if symbol == "<module>" and path in MODULE_LEVEL_STRING_TABLE_PATHS:
117
+ return MutationLockFindingClassification(
118
+ kind="metadata_literal",
119
+ counts_as_blocking=False,
120
+ rationale="module-level command metadata is audit vocabulary, not executed mutation",
121
+ )
122
+
123
+ if symbol in METADATA_LITERAL_SYMBOLS:
124
+ return MutationLockFindingClassification(
125
+ kind="metadata_literal",
126
+ counts_as_blocking=False,
127
+ rationale="symbol describes command/category metadata rather than executing a mutation",
128
+ )
129
+
130
+ if symbol in GENERATED_REFERENCE_SYMBOLS:
131
+ return MutationLockFindingClassification(
132
+ kind="generated_reference",
133
+ counts_as_blocking=False,
134
+ rationale="symbol writes generated command/reference material, not a workspace mutation target",
135
+ )
136
+
137
+ if category == "C" or "filesystem_" in reason:
138
+ if any(hint in symbol_l for hint in REPORT_WRITER_SYMBOL_HINTS):
139
+ return MutationLockFindingClassification(
140
+ kind="report_writer",
141
+ counts_as_blocking=False,
142
+ rationale="filesystem mutation writes report, evidence, summary, or log output",
143
+ )
144
+ return MutationLockFindingClassification(
145
+ kind="filesystem_side_effect",
146
+ counts_as_blocking=False,
147
+ rationale="filesystem side-effect remains visible for review; LC3 hard blocking is limited to runtime git/github mutators",
148
+ )
149
+
150
+ if category in {"A", "B"} or "git_" in reason or "github_" in reason:
151
+ if _is_enforced_core_runtime_path(path) and symbol in CORE_RUNTIME_MUTATOR_SYMBOLS:
152
+ return MutationLockFindingClassification(
153
+ kind="runtime_mutation",
154
+ counts_as_blocking=True,
155
+ rationale="finding is in a runtime git/github mutation path",
156
+ )
157
+ if any(hint in symbol_l for hint in RUNTIME_MUTATION_SYMBOL_HINTS):
158
+ return MutationLockFindingClassification(
159
+ kind="delegated_runtime_reference",
160
+ counts_as_blocking=False,
161
+ rationale="runtime-like command text is outside the LC3 enforced core mutator body or delegates to locked core mutators",
162
+ )
163
+
164
+ return MutationLockFindingClassification(
165
+ kind="review_visible_reference",
166
+ counts_as_blocking=False,
167
+ rationale="finding remains visible for review but does not match an enforced LC3 runtime mutator",
168
+ )
169
+
170
+ ENFORCED_CORE_MUTATION_LOCK_PATHS = (
171
+ "transfer_repo_actions.py",
172
+ "transfer_pr_create_flow.py",
173
+ "transfer_pr_merge_flow.py",
174
+ )
175
+
176
+
177
+ @dataclass(frozen=True)
178
+ class MutationLockFinding:
179
+ category: str
180
+ symbol: str
181
+ path: str
182
+ line: int
183
+ reason: str
184
+ required_lock: str = "workspace_mutation_lock"
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class MutationLockCoverageAuditResult:
189
+ kind: str
190
+ findings: list[MutationLockFinding]
191
+
192
+ @property
193
+ def blocking_findings(self) -> list[MutationLockFinding]:
194
+ return [
195
+ finding
196
+ for finding in self.findings
197
+ if classify_mutation_lock_finding(finding).counts_as_blocking
198
+ ]
199
+
200
+ @property
201
+ def non_blocking_findings(self) -> list[MutationLockFinding]:
202
+ return [
203
+ finding
204
+ for finding in self.findings
205
+ if not classify_mutation_lock_finding(finding).counts_as_blocking
206
+ ]
207
+
208
+ @property
209
+ def result_status(self) -> str:
210
+ return "BLOCK" if self.blocking_findings else "PASS"
211
+
212
+ @property
213
+ def returncode(self) -> int:
214
+ return 0 if self.result_status == "PASS" else 1
215
+
216
+ def to_dict(self) -> dict[str, Any]:
217
+ findings = [_finding_to_dict(finding) for finding in self.findings]
218
+ blocking_findings = [
219
+ finding
220
+ for finding in findings
221
+ if finding["classification"]["counts_as_blocking"]
222
+ ]
223
+ non_blocking_findings = [
224
+ finding
225
+ for finding in findings
226
+ if not finding["classification"]["counts_as_blocking"]
227
+ ]
228
+ return {
229
+ "kind": self.kind,
230
+ "result_status": self.result_status,
231
+ "finding_count": len(findings),
232
+ "blocking_finding_count": len(blocking_findings),
233
+ "non_blocking_finding_count": len(non_blocking_findings),
234
+ "classification_summary": _classification_summary(self.findings),
235
+ "findings": findings,
236
+ "blocking_findings": blocking_findings,
237
+ "non_blocking_findings": non_blocking_findings,
238
+ }
239
+
240
+
241
+ def _finding_to_dict(finding: MutationLockFinding) -> dict[str, Any]:
242
+ classification = classify_mutation_lock_finding(finding)
243
+ data = asdict(finding)
244
+ data["classification"] = classification.to_dict()
245
+ return data
246
+
247
+
248
+ def _classification_summary(findings: list[MutationLockFinding]) -> dict[str, dict[str, object]]:
249
+ summary: dict[str, dict[str, object]] = {}
250
+ for finding in findings:
251
+ classification = classify_mutation_lock_finding(finding)
252
+ entry = summary.setdefault(
253
+ classification.kind,
254
+ {
255
+ "count": 0,
256
+ "counts_as_blocking": classification.counts_as_blocking,
257
+ },
258
+ )
259
+ entry["count"] = int(entry["count"]) + 1
260
+ return dict(sorted(summary.items()))
261
+
262
+
263
+ _MUTATION_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
264
+ ("git_branch_mutation", re.compile(r"\bgit\s+(?:switch|checkout|branch)\b|[\"']git[\"']\s*,\s*[\"'](?:switch|checkout|branch)[\"']|branch_create|branch-switch")),
265
+ ("git_history_mutation", re.compile(r"\bgit\s+(?:commit|merge|reset|restore|push)\b|[\"']git[\"']\s*,\s*[\"'](?:commit|merge|reset|restore|push)[\"']|pr-create-complete|pr-complete")),
266
+ ("filesystem_mutation", re.compile(r"\.(?:write_text|unlink|replace|rename)\(|(?:mkdir|rmtree)\(")),
267
+ ("github_mutation", re.compile(r"\bgh\s+pr\s+(?:create|merge|close|edit)\b")),
268
+ )
269
+
270
+ _LOCK_MARKERS = (
271
+ "workspace_lock",
272
+ "acquire_workspace_lock",
273
+ "WorkspaceLock",
274
+ "mutation_lock",
275
+ "with_workspace_lock",
276
+ )
277
+
278
+
279
+ def _nearest_symbol(lines: list[str], index: int) -> str:
280
+ for line_no in range(index, -1, -1):
281
+ line = lines[line_no].strip()
282
+ match = re.match(r"def\s+([a-zA-Z_][a-zA-Z0-9_]*)\(", line)
283
+ if match:
284
+ return match.group(1)
285
+ match = re.match(r"class\s+([a-zA-Z_][a-zA-Z0-9_]*)\b", line)
286
+ if match:
287
+ return match.group(1)
288
+ return "<module>"
289
+
290
+
291
+ def _function_body(lines: list[str], symbol: str) -> str:
292
+ start: int | None = None
293
+ for line_no, line in enumerate(lines):
294
+ if re.match(rf"def\s+{re.escape(symbol)}\(", line):
295
+ start = line_no
296
+ break
297
+ if start is None:
298
+ return ""
299
+ end = len(lines)
300
+ for line_no in range(start + 1, len(lines)):
301
+ if re.match(r"def\s+[a-zA-Z_][a-zA-Z0-9_]*\(", lines[line_no]):
302
+ end = line_no
303
+ break
304
+ return "\n".join(lines[start:end])
305
+
306
+
307
+ def _symbol_has_lock_marker(lines: list[str], symbol: str) -> bool:
308
+ body = _function_body(lines, symbol)
309
+ if any(marker in body for marker in _LOCK_MARKERS):
310
+ return True
311
+ if symbol.startswith("_") and symbol.endswith("_unlocked"):
312
+ wrapper_symbol = symbol.removeprefix("_").removesuffix("_unlocked")
313
+ wrapper_body = _function_body(lines, wrapper_symbol)
314
+ return any(marker in wrapper_body for marker in _LOCK_MARKERS)
315
+ return False
316
+
317
+
318
+ def _has_lock_marker_nearby(lines: list[str], index: int) -> bool:
319
+ start = max(0, index - 12)
320
+ end = min(len(lines), index + 8)
321
+ window = "\n".join(lines[start:end])
322
+ return any(marker in window for marker in _LOCK_MARKERS)
323
+
324
+
325
+ def audit_mutation_lock_coverage(root: Path | str = ".") -> MutationLockCoverageAuditResult:
326
+ repo_root = Path(root)
327
+ src_root = repo_root / "src" / "agentic_project_kit"
328
+ findings: list[MutationLockFinding] = []
329
+
330
+ if not src_root.exists():
331
+ return MutationLockCoverageAuditResult(
332
+ kind="mutation_lock_coverage_audit",
333
+ findings=[
334
+ MutationLockFinding(
335
+ category="A",
336
+ symbol="src_root_missing",
337
+ path=str(src_root),
338
+ line=0,
339
+ reason="agentic_project_kit source root is missing",
340
+ )
341
+ ],
342
+ )
343
+
344
+ for path in sorted(src_root.rglob("*.py")):
345
+ rel = path.relative_to(repo_root).as_posix()
346
+ if path.name == "mutation_lock_audit.py":
347
+ continue
348
+
349
+ lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
350
+ for index, line in enumerate(lines):
351
+ stripped = line.strip()
352
+ if stripped.startswith("#"):
353
+ continue
354
+
355
+ for category_name, pattern in _MUTATION_PATTERNS:
356
+ if not pattern.search(line):
357
+ continue
358
+
359
+ if _has_lock_marker_nearby(lines, index):
360
+ continue
361
+
362
+ symbol = _nearest_symbol(lines, index)
363
+ if _symbol_has_lock_marker(lines, symbol):
364
+ continue
365
+ category = {
366
+ "git_branch_mutation": "A",
367
+ "git_history_mutation": "A",
368
+ "github_mutation": "B",
369
+ "filesystem_mutation": "C",
370
+ }[category_name]
371
+
372
+ findings.append(
373
+ MutationLockFinding(
374
+ category=category,
375
+ symbol=symbol,
376
+ path=rel,
377
+ line=index + 1,
378
+ reason=f"{category_name} without nearby workspace mutation lock marker",
379
+ )
380
+ )
381
+
382
+ return MutationLockCoverageAuditResult(
383
+ kind="mutation_lock_coverage_audit",
384
+ findings=findings,
385
+ )
386
+
387
+
388
+ def render_mutation_lock_coverage_audit(result: MutationLockCoverageAuditResult) -> str:
389
+ lines = [
390
+ "MUTATION_LOCK_COVERAGE_AUDIT",
391
+ f"RESULT: {result.result_status}",
392
+ f"FINDINGS: {len(result.findings)}",
393
+ f"BLOCKING_FINDINGS: {len(result.blocking_findings)}",
394
+ f"NON_BLOCKING_FINDINGS: {len(result.non_blocking_findings)}",
395
+ ]
396
+ if result.findings:
397
+ lines.append("CLASSIFICATION_SUMMARY:")
398
+ for kind, summary in _classification_summary(result.findings).items():
399
+ disposition = "BLOCK" if summary["counts_as_blocking"] else "non-blocking"
400
+ lines.append(f"- {kind}: {summary['count']} ({disposition})")
401
+
402
+ if result.blocking_findings:
403
+ lines.append("BLOCKING:")
404
+ for finding in result.blocking_findings:
405
+ classification = classify_mutation_lock_finding(finding)
406
+ lines.append(
407
+ f"- [{finding.category}] {finding.symbol} {finding.path}:{finding.line} "
408
+ f"— classification={classification.kind} disposition=BLOCK "
409
+ f"— {finding.reason} — {classification.rationale}"
410
+ )
411
+
412
+ if result.non_blocking_findings:
413
+ lines.append("NON_BLOCKING:")
414
+ for finding in result.non_blocking_findings:
415
+ classification = classify_mutation_lock_finding(finding)
416
+ lines.append(
417
+ f"- [{finding.category}] {finding.symbol} {finding.path}:{finding.line} "
418
+ f"— classification={classification.kind} disposition=non-blocking "
419
+ f"— {finding.reason} — {classification.rationale}"
420
+ )
421
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,253 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from agentic_project_kit.safe_push import safe_push
9
+
10
+ from agentic_project_kit.next_turn_result import publish_local_evidence, render_summary
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class EvidencePublishPlan:
15
+ run_id: str
16
+ local_terminal_log: str
17
+ work_result: str
18
+ files_to_stage: tuple[str, ...]
19
+ commit_message: str
20
+ branch: str
21
+ push_command: str
22
+
23
+
24
+ def _run_git(args: list[str], *, root: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
25
+ completed = subprocess.run(
26
+ ["git", *args],
27
+ cwd=root,
28
+ text=True,
29
+ capture_output=True,
30
+ check=False,
31
+ )
32
+ if check and completed.returncode != 0:
33
+ raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())
34
+ return completed
35
+
36
+
37
+ def _git_stdout(args: list[str], *, root: Path) -> str:
38
+ return _run_git(args, root=root).stdout.strip()
39
+
40
+
41
+ def current_branch(root: Path | str = ".") -> str:
42
+ return _git_stdout(["branch", "--show-current"], root=Path(root))
43
+
44
+
45
+ def build_evidence_publish_plan(
46
+ *,
47
+ run_id: str,
48
+ local_terminal_log: str,
49
+ work_result: str,
50
+ root: Path | str = ".",
51
+ ) -> EvidencePublishPlan:
52
+ root_path = Path(root)
53
+ branch = current_branch(root_path)
54
+ files = (
55
+ f"docs/reports/terminal/{run_id}.log",
56
+ f"docs/reports/command_runs/{run_id}.json",
57
+ "docs/reports/command_runs/next-turn-latest.json",
58
+ )
59
+ return EvidencePublishPlan(
60
+ run_id=run_id,
61
+ local_terminal_log=local_terminal_log,
62
+ work_result=work_result,
63
+ files_to_stage=files,
64
+ commit_message=f"Record {run_id} evidence",
65
+ branch=branch,
66
+ push_command=f"git push -u origin {branch}",
67
+ )
68
+
69
+
70
+ def publish_and_stage_evidence(
71
+ *,
72
+ run_id: str,
73
+ local_terminal_log: str,
74
+ work_result: str,
75
+ root: Path | str = ".",
76
+ ) -> EvidencePublishPlan:
77
+ root_path = Path(root)
78
+ result = publish_local_evidence(
79
+ run_id=run_id,
80
+ local_terminal_log=local_terminal_log,
81
+ work_result=work_result, # type: ignore[arg-type]
82
+ root=root_path,
83
+ )
84
+ plan = build_evidence_publish_plan(
85
+ run_id=run_id,
86
+ local_terminal_log=local_terminal_log,
87
+ work_result=work_result,
88
+ root=root_path,
89
+ )
90
+ _run_git(["add", *plan.files_to_stage], root=root_path)
91
+ print(render_summary(result))
92
+ return plan
93
+
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class EvidenceFinalizeResult:
98
+ plan: EvidencePublishPlan
99
+ committed: bool
100
+ pushed: bool
101
+ already_clean: bool
102
+ commit_sha: str
103
+ message: str
104
+
105
+
106
+ def has_staged_changes(root: Path | str = ".") -> bool:
107
+ completed = _run_git(["diff", "--cached", "--quiet"], root=Path(root), check=False)
108
+ return completed.returncode == 1
109
+
110
+
111
+ def commit_and_push_evidence(
112
+ *,
113
+ plan: EvidencePublishPlan,
114
+ root: Path | str = ".",
115
+ push: bool = True,
116
+ required_branch: str = "",
117
+ allow_main: bool = False,
118
+ ) -> EvidenceFinalizeResult:
119
+ root_path = Path(root)
120
+
121
+ if not has_staged_changes(root_path):
122
+ return EvidenceFinalizeResult(
123
+ plan=plan,
124
+ committed=False,
125
+ pushed=False,
126
+ already_clean=True,
127
+ commit_sha="",
128
+ message="PASS_ALREADY_DONE: no staged evidence changes",
129
+ )
130
+
131
+ branch = current_branch(root_path)
132
+ if required_branch and branch != required_branch:
133
+ raise RuntimeError(f"current branch {branch!r} does not match required branch {required_branch!r}")
134
+ if branch == "main" and not allow_main:
135
+ raise RuntimeError("next-turn evidence refuses to commit on main without allow_main=True")
136
+ if not branch:
137
+ raise RuntimeError("next-turn evidence refuses to commit without a current branch")
138
+ if push:
139
+ push_preflight = safe_push(
140
+ root_path,
141
+ target_branch=plan.branch,
142
+ purpose="next-turn evidence publish branch",
143
+ dry_run=True,
144
+ )
145
+ if not push_preflight.ok:
146
+ raise RuntimeError("push preflight blocked next-turn evidence: " + "; ".join(push_preflight.reasons))
147
+
148
+ commit = _run_git(["commit", "-m", plan.commit_message], root=root_path, check=False)
149
+ if commit.returncode != 0:
150
+ combined = (commit.stderr + commit.stdout).strip()
151
+ if "nothing to commit" in combined or "no changes added to commit" in combined:
152
+ return EvidenceFinalizeResult(
153
+ plan=plan,
154
+ committed=False,
155
+ pushed=False,
156
+ already_clean=True,
157
+ commit_sha="",
158
+ message="PASS_ALREADY_DONE: nothing to commit",
159
+ )
160
+ raise RuntimeError(combined)
161
+
162
+ sha = _git_stdout(["rev-parse", "HEAD"], root=root_path)
163
+
164
+ pushed = False
165
+ if push:
166
+ push_result = safe_push(
167
+ root_path,
168
+ target_branch=plan.branch,
169
+ purpose="next-turn evidence publish branch",
170
+ set_upstream=True,
171
+ )
172
+ if not push_result.ok:
173
+ raise RuntimeError((push_result.stderr + push_result.stdout).strip())
174
+ pushed = True
175
+
176
+ return EvidenceFinalizeResult(
177
+ plan=plan,
178
+ committed=True,
179
+ pushed=pushed,
180
+ already_clean=False,
181
+ commit_sha=sha,
182
+ message="PASS: evidence committed" + (" and pushed" if pushed else ""),
183
+ )
184
+
185
+
186
+ def render_finalize_result(result: EvidenceFinalizeResult) -> str:
187
+ lines = [
188
+ "NEXT_TURN_EVIDENCE_FINALIZE_RESULT",
189
+ f"run_id={result.plan.run_id}",
190
+ f"committed={str(result.committed).lower()}",
191
+ f"pushed={str(result.pushed).lower()}",
192
+ f"already_clean={str(result.already_clean).lower()}",
193
+ f"commit_sha={result.commit_sha}",
194
+ f"message={result.message}",
195
+ f"branch={result.plan.branch}",
196
+ "subresult=PASS",
197
+ ]
198
+ return "\n".join(lines)
199
+
200
+ def render_plan(plan: EvidencePublishPlan) -> str:
201
+ lines = [
202
+ "NEXT_TURN_EVIDENCE_PUBLISH_PLAN",
203
+ f"run_id={plan.run_id}",
204
+ f"local_terminal_log={plan.local_terminal_log}",
205
+ f"work_result={plan.work_result}",
206
+ f"branch={plan.branch}",
207
+ f"commit_message={plan.commit_message}",
208
+ f"push_command={plan.push_command}",
209
+ "files_to_stage:",
210
+ ]
211
+ lines.extend(f"- {item}" for item in plan.files_to_stage)
212
+ lines.append("subresult=PASS")
213
+ return "\n".join(lines)
214
+
215
+
216
+ def main() -> int:
217
+ parser = argparse.ArgumentParser(prog="next-turn-evidence")
218
+ parser.add_argument("--run-id", required=True)
219
+ parser.add_argument("--local-terminal-log", required=True)
220
+ parser.add_argument("--work-result", required=True, choices=["PASS", "FAIL", "PENDING", "HARD_FAIL", "PASS_ALREADY_DONE", "NOOP"])
221
+ parser.add_argument("--plan-only", action="store_true")
222
+ parser.add_argument("--commit", action="store_true")
223
+ parser.add_argument("--no-push", action="store_true")
224
+ args = parser.parse_args()
225
+
226
+ if args.plan_only:
227
+ plan = build_evidence_publish_plan(
228
+ run_id=args.run_id,
229
+ local_terminal_log=args.local_terminal_log,
230
+ work_result=args.work_result,
231
+ )
232
+ print(render_plan(plan))
233
+ return 0
234
+
235
+ plan = publish_and_stage_evidence(
236
+ run_id=args.run_id,
237
+ local_terminal_log=args.local_terminal_log,
238
+ work_result=args.work_result,
239
+ )
240
+
241
+ if args.commit:
242
+ finalize_result = commit_and_push_evidence(
243
+ plan=plan,
244
+ push=not args.no_push,
245
+ )
246
+ print(render_finalize_result(finalize_result))
247
+ else:
248
+ print(render_plan(plan))
249
+ return 0
250
+
251
+
252
+ if __name__ == "__main__":
253
+ raise SystemExit(main())