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,67 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import subprocess
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CommitGuardResult:
10
+ ok: bool
11
+ branch: str
12
+ dirty: bool
13
+ messages: tuple[str, ...]
14
+
15
+
16
+ def _run_git(repo_root: Path, args: list[str]) -> tuple[int, str]:
17
+ completed = subprocess.run(
18
+ ["git", *args],
19
+ cwd=repo_root,
20
+ text=True,
21
+ stdout=subprocess.PIPE,
22
+ stderr=subprocess.STDOUT,
23
+ check=False,
24
+ )
25
+ return completed.returncode, completed.stdout.strip()
26
+
27
+
28
+ def evaluate_commit_guard(repo_root: Path) -> CommitGuardResult:
29
+ branch_rc, branch_output = _run_git(repo_root, ["branch", "--show-current"])
30
+ if branch_rc != 0:
31
+ return CommitGuardResult(False, "", False, (branch_output or "ERROR: unable to determine current branch.",))
32
+
33
+ branch = branch_output.strip()
34
+ if branch == "main":
35
+ return CommitGuardResult(
36
+ False,
37
+ branch,
38
+ False,
39
+ (
40
+ "ERROR: refusing commit/PR workflow on main.",
41
+ "Create a feature or docs branch first, then rerun the commit/PR workflow.",
42
+ "Expected action: create a feature or docs branch first.",
43
+ ),
44
+ )
45
+
46
+ status_rc, status_output = _run_git(repo_root, ["status", "--porcelain"])
47
+ if status_rc != 0:
48
+ return CommitGuardResult(False, branch, False, (status_output or "ERROR: unable to read git status.",))
49
+
50
+ dirty = bool(status_output.strip())
51
+ if dirty:
52
+ messages = (f"Commit/PR guard passed on branch: {branch}",)
53
+ else:
54
+ messages = (f"No local changes detected on branch: {branch}",)
55
+ return CommitGuardResult(True, branch, dirty, messages)
56
+
57
+
58
+ def main(argv: list[str] | None = None) -> int:
59
+ _ = argv
60
+ result = evaluate_commit_guard(Path(".").resolve())
61
+ for message in result.messages:
62
+ print(message)
63
+ return 0 if result.ok else 1
64
+
65
+
66
+ if __name__ == "__main__":
67
+ raise SystemExit(main())
@@ -0,0 +1,495 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ from collections import defaultdict
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ArtifactRule:
14
+ id: str
15
+ path: str
16
+ kind: str
17
+ reason: str
18
+
19
+
20
+ TRANSIENT_RULES = (
21
+ ArtifactRule("agent-current-yaml", ".agentic/commands/current.yaml", "stale-transient-command", "transient compatibility file after agent-next or agent-run"),
22
+ ArtifactRule("agent-current-sh", ".agentic/commands/current.sh", "stale-transient-command", "transient compatibility file after agent-next or agent-run"),
23
+ ArtifactRule(
24
+ "next-turn-latest-terminal-log",
25
+ "docs/reports/terminal/next-turn-latest.log",
26
+ "stale-next-turn-working-copy",
27
+ "untracked fixed-slot working log; durable evidence must be uploaded explicitly",
28
+ ),
29
+ ArtifactRule(
30
+ "next-turn-latest-command-report",
31
+ "docs/reports/command_runs/next-turn-latest.json",
32
+ "stale-next-turn-working-copy",
33
+ "untracked fixed-slot working report; durable evidence must be uploaded explicitly",
34
+ ),
35
+ )
36
+
37
+ PROTECTED_ARTIFACTS = (
38
+ "docs/reports/terminal/LATEST_TERMINAL_LOG.txt",
39
+ )
40
+
41
+ ALLOWED_PREFIXES = (".agentic/commands/",)
42
+ ALLOWED_EXACT_PATHS = (
43
+ "docs/reports/terminal/next-turn-latest.log",
44
+ "docs/reports/command_runs/next-turn-latest.json",
45
+ )
46
+ DEFAULT_TMP_LOG_TTL_SECONDS = 24 * 60 * 60
47
+ DEFAULT_KEEP_LAST_SLICE_LOGS = 12
48
+
49
+ TRANSFER_RUNS_KEEP_NAMES = frozenset(
50
+ {
51
+ "latest-transfer-report.log",
52
+ "latest-transfer-report.json",
53
+ "latest-remote-next-report.log",
54
+ "latest-remote-next-report.json",
55
+ }
56
+ )
57
+
58
+ REPORT_RETENTION_ROOTS = (
59
+ "docs/terminal",
60
+ "docs/reports/terminal",
61
+ "docs/reports/command_runs",
62
+ "docs/reports/branch_cleanup",
63
+ "docs/reports/ns-migration",
64
+ )
65
+
66
+ REPORT_RETENTION_KEEP_NAME_FRAGMENTS = frozenset(
67
+ {
68
+ "latest",
69
+ "current",
70
+ "manifest",
71
+ "source_manifest",
72
+ "validation_report",
73
+ "execution_contract",
74
+ "summary",
75
+ "index",
76
+ "readme",
77
+ }
78
+ )
79
+
80
+ REPORT_RETENTION_AUTO_DELETE_SUFFIXES = frozenset({".log", ".json"})
81
+
82
+ REPORT_RETENTION_AUTO_DELETE_MARKDOWN_PATTERNS = frozenset(
83
+ {
84
+ r"docs/reports/terminal/post-pr[0-9]+-successor-chat-handoff[.]md",
85
+ r"docs/reports/terminal/post-pr[0-9]+-successor-handoff[.]md",
86
+ r"docs/reports/terminal/v[0-9]+-successor-chat-handoff-after-pr[0-9]+[.]md",
87
+ r"docs/reports/terminal/v[0-9]+-handoff-after-pr[0-9]+[.]md",
88
+ }
89
+ )
90
+
91
+ REPORT_RETENTION_REFERENCE_EXCLUDED_PREFIXES = (
92
+ ".git/",
93
+ ".venv/",
94
+ "tmp/",
95
+ "__pycache__/",
96
+ "docs/terminal/",
97
+ "docs/reports/terminal/",
98
+ "docs/reports/command_runs/",
99
+ "docs/reports/branch_cleanup/",
100
+ "docs/reports/ns-migration/",
101
+ "docs/reports/transfer_runs/",
102
+ "docs/reference/agentic-kit-commands.json",
103
+ )
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class ReportRetentionCandidate:
108
+ path: Path
109
+ reason: str
110
+
111
+ PROTECTED_LOCAL_NAMES = frozenset(
112
+ {
113
+ "local-gc-last.json",
114
+ "local-command-stack-state.json",
115
+ "local-gc-last-run-id.txt",
116
+ "next-turn-latest.log",
117
+ }
118
+ )
119
+
120
+ LOCAL_LOG_PATTERNS = (
121
+ "*slice*.log",
122
+ "*handoff*.log",
123
+ "*release*.log",
124
+ "*gc*.log",
125
+ )
126
+
127
+
128
+ def _is_allowed(path: Path) -> bool:
129
+ text = path.as_posix()
130
+ if text in PROTECTED_ARTIFACTS:
131
+ return False
132
+ return text in ALLOWED_EXACT_PATHS or any(text.startswith(prefix) for prefix in ALLOWED_PREFIXES)
133
+
134
+
135
+ def _is_git_untracked(base: Path, rel: Path) -> bool:
136
+ completed = subprocess.run(
137
+ ["git", "status", "--porcelain", "--", rel.as_posix()],
138
+ cwd=base,
139
+ text=True,
140
+ stdout=subprocess.PIPE,
141
+ stderr=subprocess.DEVNULL,
142
+ check=False,
143
+ )
144
+ if completed.returncode != 0:
145
+ return False
146
+ return any(line.startswith("?? ") for line in completed.stdout.splitlines())
147
+
148
+
149
+ def collect_candidates(root: Path | str = ".") -> list[tuple[ArtifactRule, Path]]:
150
+ base = Path(root)
151
+ found: list[tuple[ArtifactRule, Path]] = []
152
+ for rule in TRANSIENT_RULES:
153
+ rel = Path(rule.path)
154
+ if not (base / rel).exists():
155
+ continue
156
+ if rel.as_posix() in ALLOWED_EXACT_PATHS and not _is_git_untracked(base, rel):
157
+ continue
158
+ found.append((rule, rel))
159
+ return found
160
+
161
+
162
+ def render_plan(candidates: list[tuple[ArtifactRule, Path]]) -> str:
163
+ if not candidates:
164
+ return "PASS_NOTHING_TO_COLLECT"
165
+ lines = ["PENDING_COMMUNICATION_ARTIFACTS"]
166
+ for rule, path in candidates:
167
+ lines.append(f"{rule.id}\t{rule.kind}\t{path.as_posix()}\t{rule.reason}")
168
+ return "\n".join(lines)
169
+
170
+
171
+ def execute_gc(root: Path | str = ".") -> tuple[str, str]:
172
+ base = Path(root)
173
+ candidates = collect_candidates(base)
174
+ if not candidates:
175
+ return "PASS_NOTHING_TO_COLLECT", ""
176
+ removed: list[str] = []
177
+ for rule, rel in candidates:
178
+ if not _is_allowed(rel):
179
+ return "FAIL_UNREGISTERED_PATH", rel.as_posix()
180
+ target = base / rel
181
+ if target.is_symlink():
182
+ return "FAIL_SYMLINK_ARTIFACT", rel.as_posix()
183
+ if not target.is_file():
184
+ return "FAIL_NOT_A_FILE", rel.as_posix()
185
+ target.unlink()
186
+ removed.append(rel.as_posix())
187
+ return "PASS_COLLECTED", "\n".join(removed)
188
+
189
+
190
+ def collect_expired_tmp_logs(
191
+ tmp_root: Path | str = "/tmp",
192
+ now: float | None = None,
193
+ ttl_seconds: int = DEFAULT_TMP_LOG_TTL_SECONDS,
194
+ keep_last: int = DEFAULT_KEEP_LAST_SLICE_LOGS,
195
+ ) -> list[Path]:
196
+ root = Path(tmp_root)
197
+ if not root.exists() or not root.is_dir():
198
+ return []
199
+ now_value = time.time() if now is None else now
200
+ expired_agentic_logs: list[Path] = []
201
+ for path in root.glob("agentic-project-kit-*.log"):
202
+ if path.is_symlink() or not path.is_file():
203
+ continue
204
+ if path.name in PROTECTED_LOCAL_NAMES:
205
+ continue
206
+ age_seconds = now_value - path.stat().st_mtime
207
+ if age_seconds >= ttl_seconds:
208
+ expired_agentic_logs.append(path)
209
+
210
+ expired_slice_logs: list[Path] = []
211
+ for pattern in LOCAL_LOG_PATTERNS:
212
+ for path in root.glob(pattern):
213
+ if path.is_symlink() or not path.is_file():
214
+ continue
215
+ if path.name in PROTECTED_LOCAL_NAMES:
216
+ continue
217
+ age_seconds = now_value - path.stat().st_mtime
218
+ if age_seconds >= ttl_seconds:
219
+ expired_slice_logs.append(path)
220
+
221
+ unique_slice_logs = sorted(set(expired_slice_logs), key=lambda item: item.stat().st_mtime, reverse=True)
222
+ if keep_last > 0:
223
+ unique_slice_logs = unique_slice_logs[keep_last:]
224
+ return sorted(set(expired_agentic_logs).union(unique_slice_logs))
225
+
226
+
227
+ def execute_tmp_log_gc(
228
+ tmp_root: Path | str = "/tmp",
229
+ execute: bool = False,
230
+ now: float | None = None,
231
+ ttl_seconds: int = DEFAULT_TMP_LOG_TTL_SECONDS,
232
+ keep_last: int = DEFAULT_KEEP_LAST_SLICE_LOGS,
233
+ ) -> tuple[str, str]:
234
+ candidates = collect_expired_tmp_logs(tmp_root, now=now, ttl_seconds=ttl_seconds, keep_last=keep_last)
235
+ if not candidates:
236
+ return "PASS_NOTHING_TO_COLLECT", ""
237
+ message = "\n".join(path.as_posix() for path in candidates)
238
+ if not execute:
239
+ return "PENDING_EXPIRED_TMP_LOGS", message
240
+ removed: list[str] = []
241
+ root = Path(tmp_root)
242
+ for path in candidates:
243
+ if path.is_symlink():
244
+ return "FAIL_SYMLINK_ARTIFACT", path.as_posix()
245
+ if path.parent != root:
246
+ return "FAIL_UNREGISTERED_PATH", path.as_posix()
247
+ if not path.is_file():
248
+ return "FAIL_NOT_A_FILE", path.as_posix()
249
+ path.unlink()
250
+ removed.append(path.as_posix())
251
+ return "PASS_COLLECTED", "\n".join(removed)
252
+
253
+
254
+
255
+ def collect_expired_transfer_run_reports(
256
+ root: Path | str = ".",
257
+ now: float | None = None,
258
+ ttl_seconds: int = DEFAULT_TMP_LOG_TTL_SECONDS,
259
+ ) -> list[Path]:
260
+ base = Path(root) / "docs" / "reports" / "transfer_runs"
261
+ if not base.exists() or not base.is_dir():
262
+ return []
263
+ now_value = time.time() if now is None else now
264
+ expired: list[Path] = []
265
+ for path in sorted(base.rglob("*")):
266
+ if path.is_symlink() or not path.is_file():
267
+ continue
268
+ if path.name in TRANSFER_RUNS_KEEP_NAMES:
269
+ continue
270
+ age_seconds = now_value - path.stat().st_mtime
271
+ if age_seconds >= ttl_seconds:
272
+ expired.append(path)
273
+ return expired
274
+
275
+
276
+
277
+ def _is_report_keep_name(name: str) -> bool:
278
+ lowered = name.lower()
279
+ return any(fragment in lowered for fragment in REPORT_RETENTION_KEEP_NAME_FRAGMENTS)
280
+
281
+
282
+ def _is_auto_delete_report_retention_path(rel: Path) -> bool:
283
+ suffix = rel.suffix.lower()
284
+ if suffix in REPORT_RETENTION_AUTO_DELETE_SUFFIXES:
285
+ return True
286
+ if suffix != ".md":
287
+ return False
288
+ text = rel.as_posix()
289
+ return any(re.fullmatch(pattern, text) for pattern in REPORT_RETENTION_AUTO_DELETE_MARKDOWN_PATTERNS)
290
+
291
+
292
+ def _is_report_retention_excluded_reference_file(rel: Path) -> bool:
293
+ text = rel.as_posix()
294
+ return any(text == prefix.rstrip("/") or text.startswith(prefix) for prefix in REPORT_RETENTION_REFERENCE_EXCLUDED_PREFIXES)
295
+
296
+
297
+ def _iter_report_retention_reference_files(base: Path):
298
+ for candidate in base.rglob("*"):
299
+ if not candidate.is_file() or candidate.is_symlink():
300
+ continue
301
+ try:
302
+ candidate_rel = candidate.relative_to(base)
303
+ except ValueError:
304
+ continue
305
+ if _is_report_retention_excluded_reference_file(candidate_rel):
306
+ continue
307
+ yield candidate
308
+
309
+
310
+ def _referenced_report_retention_paths(base: Path, rels: list[Path]) -> set[Path]:
311
+ if not rels:
312
+ return set()
313
+ needles = {rel.as_posix(): rel for rel in rels}
314
+ referenced: set[Path] = set()
315
+ for candidate in _iter_report_retention_reference_files(base):
316
+ try:
317
+ content = candidate.read_text(encoding="utf-8")
318
+ except UnicodeDecodeError:
319
+ continue
320
+ except OSError:
321
+ continue
322
+ for needle, rel in needles.items():
323
+ if rel in referenced:
324
+ continue
325
+ if needle in content:
326
+ referenced.add(rel)
327
+ if len(referenced) == len(needles):
328
+ break
329
+ return referenced
330
+
331
+
332
+ def _report_retention_roots(base: Path) -> list[Path]:
333
+ return [base / item for item in REPORT_RETENTION_ROOTS if (base / item).exists() and (base / item).is_dir()]
334
+
335
+
336
+ def collect_report_retention_candidates(
337
+ root: Path | str = ".",
338
+ now: float | None = None,
339
+ ttl_seconds: int = DEFAULT_TMP_LOG_TTL_SECONDS,
340
+ keep_last_per_parent: int = 2,
341
+ ) -> list[ReportRetentionCandidate]:
342
+ base = Path(root)
343
+ now_value = time.time() if now is None else now
344
+ files: list[Path] = []
345
+ for report_root in _report_retention_roots(base):
346
+ for path in sorted(report_root.rglob("*")):
347
+ if path.is_symlink() or not path.is_file():
348
+ continue
349
+ files.append(path)
350
+
351
+ newest_by_parent: dict[Path, set[Path]] = defaultdict(set)
352
+ if keep_last_per_parent > 0:
353
+ grouped: dict[Path, list[Path]] = defaultdict(list)
354
+ for path in files:
355
+ grouped[path.parent].append(path)
356
+ for parent, items in grouped.items():
357
+ ordered = sorted(items, key=lambda item: item.stat().st_mtime, reverse=True)
358
+ newest_by_parent[parent].update(ordered[:keep_last_per_parent])
359
+
360
+ unreferenced_check: list[Path] = []
361
+ for path in files:
362
+ rel = path.relative_to(base)
363
+ if not _is_auto_delete_report_retention_path(rel):
364
+ continue
365
+ if _is_report_keep_name(path.name):
366
+ continue
367
+ if path in newest_by_parent.get(path.parent, set()):
368
+ continue
369
+ age_seconds = now_value - path.stat().st_mtime
370
+ if age_seconds < ttl_seconds:
371
+ continue
372
+ unreferenced_check.append(rel)
373
+
374
+ referenced = _referenced_report_retention_paths(base, unreferenced_check)
375
+ candidates: list[ReportRetentionCandidate] = []
376
+ for rel in unreferenced_check:
377
+ if rel in referenced:
378
+ continue
379
+ candidates.append(
380
+ ReportRetentionCandidate(
381
+ path=rel,
382
+ reason="expired report-retention artifact; unreferenced outside report surfaces",
383
+ )
384
+ )
385
+ return candidates
386
+
387
+
388
+ def execute_report_retention_gc(
389
+ root: Path | str = ".",
390
+ execute: bool = False,
391
+ now: float | None = None,
392
+ ttl_seconds: int = DEFAULT_TMP_LOG_TTL_SECONDS,
393
+ keep_last_per_parent: int = 2,
394
+ ) -> tuple[str, str]:
395
+ base = Path(root)
396
+ candidates = collect_report_retention_candidates(
397
+ base,
398
+ now=now,
399
+ ttl_seconds=ttl_seconds,
400
+ keep_last_per_parent=keep_last_per_parent,
401
+ )
402
+ if not candidates:
403
+ return "PASS_NOTHING_TO_COLLECT", ""
404
+
405
+ message = "\n".join(f"{item.path.as_posix()}\t{item.reason}" for item in candidates)
406
+ if not execute:
407
+ return "PENDING_EXPIRED_REPORT_RETENTION_FILES", message
408
+
409
+ allowed_roots = [root_path.resolve() for root_path in _report_retention_roots(base)]
410
+ removed: list[str] = []
411
+ referenced = _referenced_report_retention_paths(base, [item.path for item in candidates])
412
+ for item in candidates:
413
+ target = base / item.path
414
+ resolved = target.resolve()
415
+ if target.is_symlink():
416
+ return "FAIL_SYMLINK_ARTIFACT", item.path.as_posix()
417
+ if not any(root_path == resolved or root_path in resolved.parents for root_path in allowed_roots):
418
+ return "FAIL_UNREGISTERED_PATH", item.path.as_posix()
419
+ if not _is_auto_delete_report_retention_path(item.path):
420
+ return "FAIL_UNREGISTERED_REPORT_RETENTION_FILE", item.path.as_posix()
421
+ if _is_report_keep_name(target.name):
422
+ return "FAIL_PROTECTED_REPORT_RETENTION_FILE", item.path.as_posix()
423
+ if item.path in referenced:
424
+ return "FAIL_REFERENCED_REPORT_RETENTION_FILE", item.path.as_posix()
425
+ if not target.is_file():
426
+ return "FAIL_NOT_A_FILE", item.path.as_posix()
427
+ target.unlink()
428
+ removed.append(item.path.as_posix())
429
+ return "PASS_COLLECTED", "\n".join(removed)
430
+
431
+
432
+ def execute_transfer_run_report_gc(
433
+ root: Path | str = ".",
434
+ execute: bool = False,
435
+ now: float | None = None,
436
+ ttl_seconds: int = DEFAULT_TMP_LOG_TTL_SECONDS,
437
+ ) -> tuple[str, str]:
438
+ base = Path(root)
439
+ candidates = collect_expired_transfer_run_reports(base, now=now, ttl_seconds=ttl_seconds)
440
+ if not candidates:
441
+ return "PASS_NOTHING_TO_COLLECT", ""
442
+ message = "\n".join(path.relative_to(base).as_posix() for path in candidates)
443
+ if not execute:
444
+ return "PENDING_EXPIRED_TRANSFER_RUN_REPORTS", message
445
+
446
+ removed: list[str] = []
447
+ transfer_root = (base / "docs" / "reports" / "transfer_runs").resolve()
448
+ for path in candidates:
449
+ resolved = path.resolve()
450
+ if path.is_symlink():
451
+ return "FAIL_SYMLINK_ARTIFACT", path.as_posix()
452
+ if transfer_root not in resolved.parents:
453
+ return "FAIL_UNREGISTERED_PATH", path.as_posix()
454
+ if path.name in TRANSFER_RUNS_KEEP_NAMES:
455
+ return "FAIL_PROTECTED_LATEST_REPORT", path.as_posix()
456
+ if not path.is_file():
457
+ return "FAIL_NOT_A_FILE", path.as_posix()
458
+ path.unlink()
459
+ removed.append(path.relative_to(base).as_posix())
460
+ return "PASS_COLLECTED", "\n".join(removed)
461
+
462
+ def main(argv: list[str] | None = None) -> int:
463
+ args = list(sys.argv[1:] if argv is None else argv)
464
+ execute = "--execute" in args
465
+ if "--tmp-logs" in args:
466
+ tmp_root = "tmp" if "--local-tmp" in args else "/tmp"
467
+ outcome, message = execute_tmp_log_gc(tmp_root, execute=execute)
468
+ print(outcome)
469
+ if message:
470
+ print(message)
471
+ return 0 if outcome.startswith("PASS") or outcome.startswith("PENDING") else 1
472
+ if "--transfer-runs" in args:
473
+ outcome, message = execute_transfer_run_report_gc(".", execute=execute)
474
+ print(outcome)
475
+ if message:
476
+ print(message)
477
+ return 0 if outcome.startswith("PASS") or outcome.startswith("PENDING") else 1
478
+ if "--report-retention" in args:
479
+ outcome, message = execute_report_retention_gc(".", execute=execute)
480
+ print(outcome)
481
+ if message:
482
+ print(message)
483
+ return 0 if outcome.startswith("PASS") or outcome.startswith("PENDING") else 1
484
+ if execute:
485
+ outcome, message = execute_gc(".")
486
+ print(outcome)
487
+ if message:
488
+ print(message)
489
+ return 0 if outcome.startswith("PASS") else 1
490
+ print(render_plan(collect_candidates(".")))
491
+ return 0
492
+
493
+
494
+ if __name__ == "__main__":
495
+ raise SystemExit(main())