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,275 @@
1
+ """Terminal log wrapper utilities for local workflow runs.
2
+
3
+ The module supports a deterministic handoff protocol:
4
+ - run a command while teeing stdout/stderr to a terminal log,
5
+ - keep a pointer to the latest terminal log,
6
+ - inspect upload readiness,
7
+ - upload only terminal-log artifacts on failure handoff.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import datetime as _dt
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from agentic_project_kit.safe_push import safe_push
19
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
20
+
21
+ TERMINAL_DIR = Path(LEGACY_DEFAULTS.terminal_reports_root)
22
+ LATEST_POINTER = TERMINAL_DIR / "LATEST_TERMINAL_LOG.txt"
23
+
24
+
25
+ def _safe_name(name: str) -> str:
26
+ safe = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in name.strip())
27
+ return safe or "run"
28
+
29
+
30
+ def _terminal_dir(root: Path = Path(".")) -> Path:
31
+ if TERMINAL_DIR != Path(LEGACY_DEFAULTS.terminal_reports_root):
32
+ return TERMINAL_DIR
33
+ return load_workspace(root).terminal_reports_dir()
34
+
35
+
36
+ def _latest_pointer(root: Path = Path(".")) -> Path:
37
+ if LATEST_POINTER != TERMINAL_DIR / "LATEST_TERMINAL_LOG.txt":
38
+ return LATEST_POINTER
39
+ return load_workspace(root).latest_terminal_log_pointer()
40
+
41
+
42
+ def make_log_path(name: str, now: _dt.datetime | None = None) -> Path:
43
+ current = now or _dt.datetime.now()
44
+ stamp = current.strftime("%Y%m%d-%H%M%S")
45
+ return _terminal_dir() / f"{stamp}_{_safe_name(name)}.log"
46
+
47
+
48
+ def write_latest_pointer(log_path: Path) -> None:
49
+ pointer = _latest_pointer()
50
+ pointer.parent.mkdir(parents=True, exist_ok=True)
51
+ pointer.write_text(log_path.as_posix() + "\n", encoding="utf-8")
52
+
53
+
54
+ def read_latest_pointer() -> Path | None:
55
+ pointer = _latest_pointer()
56
+ if not pointer.exists():
57
+ return None
58
+ value = pointer.read_text(encoding="utf-8").strip()
59
+ if not value:
60
+ return None
61
+ return Path(value)
62
+
63
+
64
+ def _is_allowed_terminal_artifact(path_text: str) -> bool:
65
+ workspace = load_workspace(Path("."))
66
+ latest = workspace.path_text(_latest_pointer())
67
+ terminal_dir = workspace.path_text(_terminal_dir())
68
+ return path_text == latest or (path_text.startswith(terminal_dir + "/") and path_text.endswith(".log"))
69
+
70
+
71
+ def git_dirty_paths() -> list[str]:
72
+ proc = subprocess.run(["git", "status", "--porcelain"], text=True, capture_output=True, check=False)
73
+ paths: list[str] = []
74
+ for line in proc.stdout.splitlines():
75
+ if not line.strip():
76
+ continue
77
+ paths.append(line[3:])
78
+ return paths
79
+
80
+
81
+
82
+
83
+ def terminal_clean_check() -> tuple[str, str]:
84
+ """Classify dirty state while allowing terminal-log artifacts."""
85
+
86
+ dirty = git_dirty_paths()
87
+ if not dirty:
88
+ return "PASS_CLEAN", "Working tree is clean."
89
+ forbidden = [path for path in dirty if not _is_allowed_terminal_artifact(path)]
90
+ if forbidden:
91
+ return "FAIL_DIRTY_NON_LOG_FILES", chr(10).join(forbidden)
92
+ return "PASS_ONLY_TERMINAL_LOG_DIRTY", chr(10).join(dirty)
93
+
94
+
95
+ def remote_mutation_preflight() -> tuple[str, str]:
96
+ """Require a fully clean worktree before remote mutations or merge/sync verification."""
97
+
98
+ dirty = git_dirty_paths()
99
+ if dirty:
100
+ return "FAIL_DIRTY_WORKTREE_BEFORE_REMOTE_MUTATION", chr(10).join(dirty)
101
+ return "PASS_CLEAN_FOR_REMOTE_MUTATION", "Working tree is clean for remote mutation or merge verification."
102
+ def terminal_status() -> tuple[str, str]:
103
+ latest = read_latest_pointer()
104
+ if latest is None:
105
+ return "PASS_NO_LOG", "No latest terminal log pointer exists."
106
+ if not latest.exists():
107
+ return "FAIL_INVALID_POINTER", f"Latest terminal log pointer targets missing file: {latest.as_posix()}"
108
+ return "PASS_LOG_READY", f"Latest terminal log: {latest.as_posix()}"
109
+
110
+
111
+ def _has_result_marker(text: str) -> bool:
112
+ return "### RESULT: PASS ###" in text or "### RESULT: FAIL ###" in text or "### RESULT: PENDING ###" in text
113
+
114
+
115
+ def finalize_terminal_log(tmp_log: Path, name: str) -> tuple[str, str]:
116
+ source = Path(tmp_log)
117
+ if not source.exists():
118
+ return "FAIL_SOURCE_MISSING", source.as_posix()
119
+ source_text = source.read_text(encoding="utf-8")
120
+ if not _has_result_marker(source_text):
121
+ return "FAIL_MISSING_RESULT_MARKER", source.as_posix()
122
+ source_resolved = source.resolve()
123
+ terminal_dir = _terminal_dir()
124
+ terminal_resolved = terminal_dir.resolve()
125
+ try:
126
+ source_resolved.relative_to(terminal_resolved)
127
+ except ValueError:
128
+ source_inside_terminal_dir = False
129
+ else:
130
+ source_inside_terminal_dir = True
131
+ if source_inside_terminal_dir:
132
+ return "FAIL_SOURCE_INSIDE_TERMINAL_DIR", source.as_posix()
133
+ terminal_dir.mkdir(parents=True, exist_ok=True)
134
+ target = make_log_path(name)
135
+ target.write_text(source_text, encoding="utf-8")
136
+ write_latest_pointer(target)
137
+ return "PASS_FINALIZED", target.as_posix()
138
+
139
+
140
+ def run_logged(name: str, command: list[str]) -> int:
141
+ if not command:
142
+ print("FAIL_NO_COMMAND")
143
+ return 2
144
+ _terminal_dir().mkdir(parents=True, exist_ok=True)
145
+ log_path = make_log_path(name)
146
+ write_latest_pointer(log_path)
147
+ with log_path.open("w", encoding="utf-8") as log:
148
+ def emit(line: str) -> None:
149
+ print(line)
150
+ log.write(line + "\n")
151
+ log.flush()
152
+ emit(f"terminal_log={log_path.as_posix()}")
153
+ emit("### RUN: " + " ".join(command))
154
+ proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=os.environ.copy())
155
+ assert proc.stdout is not None
156
+ for raw in proc.stdout:
157
+ line = raw.rstrip("\n")
158
+ print(line)
159
+ log.write(line + "\n")
160
+ code = proc.wait()
161
+ emit(f"### EXIT: {code}")
162
+ emit("### RESULT: PASS ###" if code == 0 else "### RESULT: FAIL ###")
163
+ return code
164
+
165
+
166
+ def _current_branch() -> str:
167
+ proc = subprocess.run(["git", "branch", "--show-current"], text=True, capture_output=True, check=False)
168
+ return proc.stdout.strip() if proc.returncode == 0 else ""
169
+
170
+
171
+ def _refuse_unsafe_mutation_branch(required_branch: str = "", allow_main: bool = False) -> tuple[bool, str]:
172
+ branch = _current_branch()
173
+ if required_branch and branch != required_branch:
174
+ return False, f"FAIL_BRANCH_MISMATCH actual={branch or 'UNKNOWN'} required={required_branch}"
175
+ if branch == "main" and not allow_main:
176
+ return False, "FAIL_MAIN_MUTATION_NOT_ALLOWED"
177
+ if not branch:
178
+ return False, "FAIL_NO_CURRENT_BRANCH"
179
+ return True, branch
180
+
181
+
182
+ def upload_terminal_output(required_branch: str = "", allow_main: bool = False) -> int:
183
+ branch_ok, branch_message = _refuse_unsafe_mutation_branch(required_branch=required_branch, allow_main=allow_main)
184
+ if not branch_ok:
185
+ print(branch_message)
186
+ return 1
187
+ outcome, message = terminal_status()
188
+ print(outcome)
189
+ print(message)
190
+ if outcome != "PASS_LOG_READY":
191
+ return 1
192
+ dirty = git_dirty_paths()
193
+ forbidden = [p for p in dirty if not _is_allowed_terminal_artifact(p)]
194
+ if forbidden:
195
+ print("FAIL_DIRTY_NON_LOG_FILES")
196
+ for item in forbidden:
197
+ print(item)
198
+ return 1
199
+ push_preflight = safe_push(
200
+ Path("."),
201
+ target_branch=branch_message,
202
+ purpose="upload terminal output log",
203
+ dry_run=True,
204
+ )
205
+ if not push_preflight.ok:
206
+ print("FAIL_PUSH_PREFLIGHT_BLOCKED")
207
+ for reason in push_preflight.reasons:
208
+ print(reason)
209
+ return push_preflight.returncode
210
+ latest = read_latest_pointer()
211
+ assert latest is not None
212
+ subprocess.run(["git", "add", latest.as_posix(), _latest_pointer().as_posix()], check=True)
213
+ diff = subprocess.run(["git", "diff", "--cached", "--quiet"], check=False)
214
+ if diff.returncode == 0:
215
+ print("PASS_ALREADY_UPLOADED")
216
+ return 0
217
+ subprocess.run(["git", "commit", "-m", "Upload terminal output log"], check=True)
218
+ pushed = safe_push(
219
+ Path("."),
220
+ target_branch=branch_message,
221
+ purpose="upload terminal output log",
222
+ )
223
+ if not pushed.ok:
224
+ print("FAIL_PUSH_FAILED")
225
+ return pushed.returncode
226
+ print("PASS_UPLOADED")
227
+ return 0
228
+
229
+
230
+ def main(argv: list[str] | None = None) -> int:
231
+ args = list(sys.argv[1:] if argv is None else argv)
232
+ if not args:
233
+ print("Usage: terminal_logging.py run-logged <name> -- <command...> | terminal-status | terminal-upload")
234
+ return 2
235
+ mode = args.pop(0)
236
+ if mode == "terminal-status":
237
+ outcome, message = terminal_status()
238
+ print(outcome)
239
+ print(message)
240
+ return 0 if outcome.startswith("PASS") else 1
241
+ if mode == "terminal-clean-check":
242
+ outcome, message = terminal_clean_check()
243
+ print(outcome)
244
+ print(message)
245
+ return 0 if outcome.startswith("PASS") else 1
246
+ if mode == "terminal-remote-preflight":
247
+ outcome, message = remote_mutation_preflight()
248
+ print(outcome)
249
+ print(message)
250
+ return 0 if outcome.startswith("PASS") else 1
251
+ if mode == "terminal-upload":
252
+ return upload_terminal_output()
253
+ if mode == "terminal-finalize":
254
+ if len(args) != 2:
255
+ print("FAIL_USAGE")
256
+ print("Usage: terminal-finalize <tmp-log> <name>")
257
+ return 2
258
+ outcome, message = finalize_terminal_log(Path(args[0]), args[1])
259
+ print(outcome)
260
+ print(message)
261
+ return 0 if outcome == "PASS_FINALIZED" else 1
262
+ if mode == "run-logged":
263
+ if "--" not in args:
264
+ print("FAIL_MISSING_SEPARATOR")
265
+ return 2
266
+ sep = args.index("--")
267
+ name = args[0] if sep > 0 else "run"
268
+ command = args[sep + 1:]
269
+ return run_logged(name, command)
270
+ print("FAIL_UNKNOWN_MODE")
271
+ return 2
272
+
273
+
274
+ if __name__ == "__main__":
275
+ raise SystemExit(main())
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
10
+
11
+ DEFAULT_TODO_PATH = Path(".agentic/todo.yaml")
12
+ DEFAULT_RENDER_PATH = Path(LEGACY_DEFAULTS.docs_root) / "TODO.md"
13
+
14
+
15
+ def load_todo(path: Path = DEFAULT_TODO_PATH) -> dict[str, Any]:
16
+ if not path.exists():
17
+ raise FileNotFoundError(f"TODO file not found: {path}")
18
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
19
+ items = data.get("items")
20
+ if not isinstance(items, list):
21
+ raise ValueError(f"TODO file must contain an 'items' list: {path}")
22
+ return data
23
+
24
+
25
+ def save_todo(data: dict[str, Any], path: Path = DEFAULT_TODO_PATH) -> None:
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ path.write_text(
28
+ yaml.safe_dump(data, sort_keys=False, allow_unicode=True),
29
+ encoding="utf-8",
30
+ )
31
+
32
+
33
+ def list_items(data: dict[str, Any], include_done: bool = False) -> list[dict[str, Any]]:
34
+ items = data.get("items", [])
35
+ if include_done:
36
+ return list(items)
37
+ return [item for item in items if item.get("status", "open") != "done"]
38
+
39
+
40
+ def complete_item(
41
+ item_id: str,
42
+ evidence: str,
43
+ path: Path = DEFAULT_TODO_PATH,
44
+ ) -> dict[str, Any]:
45
+ data = load_todo(path)
46
+ for item in data["items"]:
47
+ if item.get("id") == item_id:
48
+ item["status"] = "done"
49
+ item["evidence"] = evidence
50
+ item["completed_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
51
+ save_todo(data, path)
52
+ return item
53
+ raise KeyError(f"TODO item not found: {item_id}")
54
+
55
+
56
+ def render_markdown(
57
+ todo_path: Path = DEFAULT_TODO_PATH,
58
+ output_path: Path = DEFAULT_RENDER_PATH,
59
+ ) -> str:
60
+ data = load_todo(todo_path)
61
+ if output_path == DEFAULT_RENDER_PATH:
62
+ output_path = load_workspace(Path(".")).docs_file("TODO.md")
63
+ lines: list[str] = [
64
+ "# TODO",
65
+ "",
66
+ "This file is generated from `.agentic/todo.yaml`.",
67
+ "",
68
+ "## Items",
69
+ "",
70
+ ]
71
+
72
+ for item in data["items"]:
73
+ done = item.get("status") == "done"
74
+ marker = "x" if done else " "
75
+ item_id = item.get("id", "UNKNOWN")
76
+ title = item.get("title", "")
77
+ owner = item.get("owner", "unknown")
78
+ priority = item.get("priority", "normal")
79
+ evidence_required = item.get("evidence_required", "")
80
+ evidence = item.get("evidence", "")
81
+ completed_at = item.get("completed_at", "")
82
+
83
+ lines.append(f"- [{marker}] **{item_id}** {title} ")
84
+ lines.append(f" Owner: {owner} ")
85
+ lines.append(f" Priority: {priority} ")
86
+ if evidence_required:
87
+ lines.append(f" Evidence required: `{evidence_required}` ")
88
+ if evidence:
89
+ lines.append(f" Evidence: `{evidence}` ")
90
+ if completed_at:
91
+ lines.append(f" Completed at: `{completed_at}` ")
92
+ lines.append("")
93
+
94
+ output_path.parent.mkdir(parents=True, exist_ok=True)
95
+ text = "\n".join(lines).rstrip() + "\n"
96
+ output_path.write_text(text, encoding="utf-8")
97
+ return text
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from agentic_project_kit.transfer_state import build_transfer_state
11
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
12
+
13
+
14
+ LATEST_COMMAND_RUN = Path(LEGACY_DEFAULTS.command_runs_root) / "LATEST_COMMAND_RUN.txt"
15
+ TRANSFER_ROOT = Path(".agentic/transfer")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class TransferCloseout:
20
+ schema_version: int
21
+ removed_transfer_dir: bool
22
+ latest_command_run_path: str | None
23
+ latest_report_exists: bool
24
+ allowed_dirty_paths: list[str]
25
+ blocked_dirty_paths: list[str]
26
+ state: dict[str, Any]
27
+ result_status: str
28
+ returncode: int
29
+ next_action: str
30
+
31
+ def as_json_data(self) -> dict[str, Any]:
32
+ return {
33
+ "schema_version": self.schema_version,
34
+ "removed_transfer_dir": self.removed_transfer_dir,
35
+ "latest_command_run_path": self.latest_command_run_path,
36
+ "latest_report_exists": self.latest_report_exists,
37
+ "allowed_dirty_paths": self.allowed_dirty_paths,
38
+ "blocked_dirty_paths": self.blocked_dirty_paths,
39
+ "state": self.state,
40
+ "result_status": self.result_status,
41
+ "returncode": self.returncode,
42
+ "next_action": self.next_action,
43
+ }
44
+
45
+
46
+ def _git_status_short(root: Path) -> list[str]:
47
+ process = subprocess.run(
48
+ ["git", "status", "--short", "--untracked-files=all"],
49
+ cwd=root,
50
+ text=True,
51
+ stdout=subprocess.PIPE,
52
+ stderr=subprocess.PIPE,
53
+ check=False,
54
+ )
55
+ if process.returncode != 0:
56
+ raise RuntimeError(f"git status failed: {process.stderr.strip()}")
57
+ return [line.rstrip() for line in process.stdout.splitlines() if line.strip()]
58
+
59
+
60
+ def _status_path(line: str) -> str:
61
+ # Handles normal short status lines. Rename lines are intentionally treated by their final token.
62
+ return line[3:].strip().split(" -> ")[-1]
63
+
64
+
65
+ def _is_allowed_dirty(path: str, latest_report_path: str | None, latest_command_run: str) -> bool:
66
+ if path == latest_command_run:
67
+ return True
68
+ if latest_report_path and path == latest_report_path:
69
+ return True
70
+ return False
71
+
72
+
73
+ def _read_latest_report_path(root: Path) -> tuple[str | None, bool]:
74
+ latest = load_workspace(root).latest_command_run_pointer()
75
+ if not latest.exists():
76
+ return None, False
77
+ value = latest.read_text(encoding="utf-8").strip()
78
+ if not value:
79
+ return None, False
80
+ report_path = value.splitlines()[-1].strip()
81
+ return report_path, (root / report_path).exists()
82
+
83
+
84
+ def closeout_transfer(project_root: Path = Path("."), remove_transfer_dir: bool = True) -> TransferCloseout:
85
+ root = project_root.resolve()
86
+ removed = False
87
+
88
+ if remove_transfer_dir and (root / TRANSFER_ROOT).exists():
89
+ shutil.rmtree(root / TRANSFER_ROOT)
90
+ removed = True
91
+
92
+ latest_report_path, latest_report_exists = _read_latest_report_path(root)
93
+ workspace = load_workspace(root)
94
+ latest_command_run = workspace.path_text(workspace.latest_command_run_pointer())
95
+
96
+ status_lines = _git_status_short(root)
97
+ allowed: list[str] = []
98
+ blocked: list[str] = []
99
+ for line in status_lines:
100
+ path = _status_path(line)
101
+ if _is_allowed_dirty(path, latest_report_path, latest_command_run):
102
+ allowed.append(path)
103
+ else:
104
+ blocked.append(path)
105
+
106
+ state = build_transfer_state(root).as_json_data()
107
+
108
+ if blocked:
109
+ result_status = "BLOCKED"
110
+ returncode = 1
111
+ next_action = "Review blocked dirty paths before committing or running another transfer."
112
+ else:
113
+ result_status = "PASS"
114
+ returncode = 0
115
+ next_action = "Review allowed dirty evidence paths, then run project gates before commit."
116
+
117
+ return TransferCloseout(
118
+ schema_version=1,
119
+ removed_transfer_dir=removed,
120
+ latest_command_run_path=latest_report_path,
121
+ latest_report_exists=latest_report_exists,
122
+ allowed_dirty_paths=allowed,
123
+ blocked_dirty_paths=blocked,
124
+ state=state,
125
+ result_status=result_status,
126
+ returncode=returncode,
127
+ next_action=next_action,
128
+ )
129
+
130
+
131
+ def closeout_transfer_json(project_root: Path = Path("."), remove_transfer_dir: bool = True) -> str:
132
+ return json.dumps(
133
+ closeout_transfer(project_root, remove_transfer_dir=remove_transfer_dir).as_json_data(),
134
+ indent=2,
135
+ sort_keys=True,
136
+ )