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,1077 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from datetime import datetime, timezone
5
+ from enum import StrEnum
6
+ import hashlib
7
+ import os
8
+ from pathlib import Path
9
+ from agentic_project_kit.chat_entrypoint_contract import ensure_command_reference_in_prompt
10
+ import platform
11
+ import shlex
12
+ import subprocess
13
+ from typing import Callable
14
+
15
+ import yaml
16
+
17
+ from agentic_project_kit.command_manifest import JSON_PATH as COMMAND_MANIFEST_JSON_PATH
18
+ from agentic_project_kit.command_manifest import load_manifest
19
+ from agentic_project_kit.communication_rule_context import REQUIRED_LOADED_SECTIONS
20
+ from agentic_project_kit import transfer_repo_actions
21
+ from agentic_project_kit.gui_transfer_contract import (
22
+ CANONICAL_REMOTE_TRANSFER_REPORT_PATH,
23
+ CANONICAL_TRANSFER_INBOX_PATH,
24
+ CANONICAL_TRANSFER_OUTBOX_PATH as CANONICAL_TRANSFER_OUTBOX_PATH,
25
+ CANONICAL_TRANSFER_PAYLOAD_PATH,
26
+ CURRENT_USER_TASK_PATH,
27
+ GUI_TRANSFER_TASK_REF,
28
+ fetch_gui_transfer_ref_args,
29
+ remote_gui_task_spec,
30
+ )
31
+ REMOTE_STATUS_COMMAND_ARGS = ("transfer", "patch-cycle-status", "--json")
32
+ FILE_TRANSFER_CONTINUE_COMMAND_ARGS = ("transfer", "continue", "--json")
33
+ POSIX_AGENTIC_KIT = "./.venv/bin/agentic-kit"
34
+ WINDOWS_AGENTIC_KIT = r".venv\Scripts\agentic-kit.exe"
35
+ REMOTE_STATUS_COMMAND = (POSIX_AGENTIC_KIT, *REMOTE_STATUS_COMMAND_ARGS)
36
+ TRANSFER_CONTINUE_COMMAND = (POSIX_AGENTIC_KIT, *FILE_TRANSFER_CONTINUE_COMMAND_ARGS)
37
+ COMMUNICATION_MODE_ALIASES = {
38
+ "a": "remote",
39
+ "remote": "remote",
40
+ "github": "remote",
41
+ "github_pr_ci": "remote",
42
+ "b": "file_transfer",
43
+ "file": "file_transfer",
44
+ "file_transfer": "file_transfer",
45
+ "transfer": "file_transfer",
46
+ "transfer_files": "file_transfer",
47
+ "c": "copy_paste",
48
+ "copy": "copy_paste",
49
+ "copy_paste": "copy_paste",
50
+ "copy-and-paste": "copy_paste",
51
+ }
52
+ COMMAND_MANIFEST_RELATIVE_PATH = COMMAND_MANIFEST_JSON_PATH.as_posix()
53
+ INITIAL_LLM_PROMPT_RELATIVE_PATH = Path(".agentic/INITIAL_LLM_PROMPT.md")
54
+
55
+
56
+ class TaskEditorState(StrEnum):
57
+ IDLE = "IDLE"
58
+ SENT = "SENT"
59
+ READ = "READ"
60
+ BLOCKED = "BLOCKED"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class InitialLlmPrompt:
65
+ result_status: str
66
+ kind: str
67
+ prompt_text: str
68
+ copy_paste_instruction: str
69
+ task_path: str
70
+ task_ref: str
71
+
72
+ def as_json_data(self) -> dict[str, object]:
73
+ return asdict(self)
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class SubmittedUserTask:
78
+ result_status: str
79
+ kind: str
80
+ task_id: str
81
+ title: str
82
+ remote_path: str
83
+ task_path: str
84
+ published_ref: str | None
85
+ next_reply: str
86
+ next_action: str
87
+ button_next_state: str
88
+ body_sha256: str
89
+ created_at_utc: str
90
+ head_sha: str
91
+ blob_sha: str
92
+ commit_status: str
93
+ push_status: str
94
+ local_only: bool
95
+ remote_readable: bool
96
+ reason: str
97
+ communication_mode: str = "file_transfer"
98
+ communication_mode_code: str = "b"
99
+ communication_mode_label: str = "File Transfer"
100
+ local_execution_command: tuple[str, ...] = TRANSFER_CONTINUE_COMMAND
101
+ reply_contract: dict[str, object] = field(default_factory=dict)
102
+
103
+ def as_json_data(self) -> dict[str, object]:
104
+ return asdict(self)
105
+
106
+
107
+ def normalize_communication_mode(mode: str) -> str:
108
+ key = str(mode or "").strip().lower().replace(" ", "_")
109
+ return COMMUNICATION_MODE_ALIASES.get(key, "file_transfer")
110
+
111
+
112
+ def communication_mode_code(mode: str) -> str:
113
+ normalized = normalize_communication_mode(mode)
114
+ return {"remote": "a", "file_transfer": "b", "copy_paste": "c"}[normalized]
115
+
116
+
117
+ def communication_mode_label(mode: str) -> str:
118
+ normalized = normalize_communication_mode(mode)
119
+ return {
120
+ "remote": "Remote: GitHub/PR/CI",
121
+ "file_transfer": "File Transfer: Transfer files",
122
+ "copy_paste": "Copy-and-Paste: Recovery/Fallback",
123
+ }[normalized]
124
+
125
+
126
+ def standard_command_args_for_communication_mode(mode: str) -> tuple[str, ...]:
127
+ normalized = normalize_communication_mode(mode)
128
+ if normalized == "remote":
129
+ return REMOTE_STATUS_COMMAND_ARGS
130
+ if normalized == "file_transfer":
131
+ return FILE_TRANSFER_CONTINUE_COMMAND_ARGS
132
+ return ()
133
+
134
+
135
+ def standard_command_for_communication_mode(
136
+ mode: str,
137
+ *,
138
+ platform_name: str | None = None,
139
+ ) -> tuple[str, ...]:
140
+ args = standard_command_args_for_communication_mode(mode)
141
+ if not args:
142
+ return ()
143
+ executable = WINDOWS_AGENTIC_KIT if platform_name == "Windows" else POSIX_AGENTIC_KIT
144
+ return (executable, *args)
145
+
146
+
147
+ def standard_command_label_for_communication_mode(mode: str) -> str:
148
+ normalized = normalize_communication_mode(mode)
149
+ return {
150
+ "remote": "Run mode-a standard",
151
+ "file_transfer": "Run mode-b standard",
152
+ "copy_paste": "No standard command",
153
+ }[normalized]
154
+
155
+
156
+ def standard_command_description_for_communication_mode(mode: str) -> str:
157
+ normalized = normalize_communication_mode(mode)
158
+ return {
159
+ "remote": (
160
+ "Mode a standard command: agentic-kit transfer patch-cycle-status --json "
161
+ "for remote GitHub/PR/CI status and evidence orientation."
162
+ ),
163
+ "file_transfer": (
164
+ "Mode b standard command: agentic-kit transfer continue --json reads and "
165
+ "executes the canonical repo-backed transfer reply."
166
+ ),
167
+ "copy_paste": (
168
+ "Mode c has no standard agentic-kit execution command; open a terminal "
169
+ "and paste the LLM-provided recovery block."
170
+ ),
171
+ }[normalized]
172
+
173
+
174
+ def _command_manifest_sha_for_carrier(project_root: Path) -> str:
175
+ for candidate in (project_root, Path.cwd()):
176
+ try:
177
+ manifest = load_manifest(candidate.resolve())
178
+ except Exception:
179
+ continue
180
+ meta = manifest.get("meta") if isinstance(manifest.get("meta"), dict) else {}
181
+ manifest_sha = str(meta.get("manifest_sha") or "")
182
+ if manifest_sha:
183
+ return manifest_sha
184
+ return ""
185
+
186
+
187
+ def communication_reply_contract(mode: str) -> dict[str, object]:
188
+ normalized = normalize_communication_mode(mode)
189
+ code = communication_mode_code(normalized)
190
+ selected_command = standard_command_for_communication_mode(normalized)
191
+ mode_map = {
192
+ "a": {
193
+ "mode": "remote",
194
+ "label": "Remote: GitHub/PR/CI",
195
+ "standard_local_command": list(REMOTE_STATUS_COMMAND),
196
+ "llm_response": (
197
+ "Finish the remote work through repo-backed PR/CI/evidence mechanisms, "
198
+ "then reply with a compact completion status and evidence pointers."
199
+ ),
200
+ "local_followup": (
201
+ "The local user/GUI may inspect the remote workflow state through: "
202
+ + " ".join(REMOTE_STATUS_COMMAND)
203
+ ),
204
+ },
205
+ "b": {
206
+ "mode": "file_transfer",
207
+ "label": "File Transfer: Transfer files",
208
+ "standard_local_command": list(TRANSFER_CONTINUE_COMMAND),
209
+ "llm_response": (
210
+ "Publish a fresh active transfer order through the existing agentic-kit "
211
+ "transfer protocol, then tell the local user to run the standard command. "
212
+ "Do not paste a terminal block as the normal answer."
213
+ ),
214
+ "local_followup": (
215
+ "The local user/GUI executes the reply through: "
216
+ + " ".join(TRANSFER_CONTINUE_COMMAND)
217
+ ),
218
+ "response_order_contract": {
219
+ "submitted_gui_task_status": "submitted",
220
+ "active_order_ref": GUI_TRANSFER_TASK_REF,
221
+ "active_order_path": CANONICAL_TRANSFER_INBOX_PATH.as_posix(),
222
+ "active_order_required_status": "active",
223
+ "active_order_required_branch": GUI_TRANSFER_TASK_REF,
224
+ "python_payload_path": CANONICAL_TRANSFER_PAYLOAD_PATH.as_posix(),
225
+ "local_user_command": list(TRANSFER_CONTINUE_COMMAND),
226
+ "forbidden": [
227
+ "using_submitted_gui_task_as_executable_transfer_order",
228
+ "inventing_transfer_files_or_refs",
229
+ "asking_user_to_run_python_payload_directly",
230
+ ],
231
+ },
232
+ },
233
+ "c": {
234
+ "mode": "copy_paste",
235
+ "label": "Copy-and-Paste: Recovery/Fallback",
236
+ "standard_local_command": [],
237
+ "llm_response": (
238
+ "Reply with one complete copy-and-paste terminal block and no hidden side protocol. "
239
+ "Use this only as recovery/fallback."
240
+ ),
241
+ "local_followup": "The local user may open an OS terminal and paste the provided block.",
242
+ },
243
+ }
244
+ return {
245
+ "schema_version": 1,
246
+ "kind": "gui_task_reply_contract",
247
+ "selected_code": code,
248
+ "selected_mode": normalized,
249
+ "selected_label": communication_mode_label(normalized),
250
+ "mode_map": mode_map,
251
+ "selected_response": mode_map[code]["llm_response"],
252
+ "local_execution_command": list(selected_command),
253
+ "rules": {
254
+ "g_go_reads_repo_backed_state_first": True,
255
+ "forbidden": "answering_g_go_from_chat_memory",
256
+ "compare_task_id_and_body_sha256": True,
257
+ "discard_previous_task_context_when_identity_changes": True,
258
+ "do_not_invent_result_files_branches_refs_or_protocols": True,
259
+ "latest_remote_transfer_report_path": CANONICAL_REMOTE_TRANSFER_REPORT_PATH.as_posix(),
260
+ "gui_task_carrier_ref": GUI_TRANSFER_TASK_REF,
261
+ "gui_task_carrier_path": CANONICAL_TRANSFER_INBOX_PATH.as_posix(),
262
+ "gui_task_carrier_status": "submitted",
263
+ "active_transfer_order_status": "active",
264
+ "g_go_priority": [
265
+ "read_latest_remote_transfer_report_or_outbox_first",
266
+ "if_no_fresh_result_read_gui_task_carrier",
267
+ ],
268
+ },
269
+ }
270
+
271
+
272
+ def _loaded_sections_json_block() -> str:
273
+ lines = [f' "{section}"' for section in REQUIRED_LOADED_SECTIONS]
274
+ return "[\n" + ",\n".join(lines) + "\n ]"
275
+
276
+
277
+ def _initial_prompt_bootstrap_block() -> str:
278
+ return """You are working in the repository vfi64/agentic-project-kit.
279
+
280
+ Before doing anything else, read these files from the repository in order:
281
+
282
+ 1. docs/handoff/NEXT_CHAT_BOOTSTRAP.md
283
+ 2. docs/reports/handoff-packages/latest/successor_prompt.md
284
+ 3. docs/reports/handoff-packages/latest/validation_report.json
285
+ 4. docs/reports/handoff-packages/latest/source_manifest.json
286
+ 5. docs/reports/handoff-packages/latest/execution_contract.json
287
+
288
+ Bootstrap is accepted only after all required bootstrap files have been read
289
+ from the repository and validation_report.json has result_status/status PASS.
290
+ If any required bootstrap file is missing, unreadable, stale, or internally
291
+ inconsistent, reply exactly BOOTSTRAP_BLOCKED and do not mutate anything.
292
+ If HEAD differs from the handoff package without explanation, reply exactly
293
+ BOOTSTRAP_BLOCKED and do not mutate anything.
294
+ """
295
+
296
+
297
+ def _initial_prompt_file_transfer_block(
298
+ task_path: Path = CURRENT_USER_TASK_PATH,
299
+ task_ref: str = GUI_TRANSFER_TASK_REF,
300
+ ) -> str:
301
+ path = task_path.as_posix()
302
+ return f"""GUI task-transfer dialog:
303
+
304
+ When the user writes "g" or "go":
305
+ - Never answer a bare g/go from chat memory or from a previous user question.
306
+ - First inspect the latest repo-backed transfer result, normally
307
+ `{CANONICAL_REMOTE_TRANSFER_REPORT_PATH.as_posix()}`. If that report is fresh
308
+ and belongs to the last local `agentic-kit transfer continue --json` run,
309
+ continue from the report/outbox evidence.
310
+ - If no fresh transfer result exists, read the GUI task carrier `{path}` from
311
+ the remote ref `{task_ref}`.
312
+ Do not read this GUI task carrier from `main` unless the send result
313
+ explicitly says it was published to `main`.
314
+ - If neither a fresh transfer result nor the GUI task carrier exists
315
+ (HTTP 404 or missing): reply exactly
316
+ TASK_NOT_FOUND and do not mutate anything. The user must click Send
317
+ in the GUI first.
318
+ - If the GUI task carrier exists: treat it as a submitted user task for the LLM,
319
+ not as an executable local transfer order. Its status is `submitted`.
320
+ Read the task carrier first every time before planning from it.
321
+ Extract task_id and user_task.body_sha256 from the task carrier.
322
+ Compare them to the last task_id/body_sha256 you handled in this chat.
323
+ If they differ, discard prior task context and handle the newly read task.
324
+ If they match, still use the freshly read task carrier as the source of truth.
325
+ The user task is in `user_task.body`.
326
+ The communication mode is in `reply_contract.selected_code`:
327
+ a = remote GitHub/PR/CI work; answer with a compact completion status.
328
+ The local standard command is `{" ".join(REMOTE_STATUS_COMMAND)}`.
329
+ b = transfer files; publish a fresh active transfer order to remote ref
330
+ `{task_ref}` at `{path}` and tell the user to run
331
+ `{" ".join(TRANSFER_CONTINUE_COMMAND)}`. The active reply order must
332
+ have `status: active` and `branch: {task_ref}`. If a Python payload is
333
+ needed, use `{CANONICAL_TRANSFER_PAYLOAD_PATH.as_posix()}` as the payload
334
+ lane and reference it from the active transfer order; do not ask the user
335
+ to run the payload directly.
336
+ c = copy-and-paste fallback; answer with one complete terminal block.
337
+ Work according to repo rules, gates, protected-file policy, and
338
+ existing agentic-kit wrappers.
339
+ Use existing agentic-kit transfer/result wrappers for result publication.
340
+ Do not invent new result files, branches, refs, or ad-hoc protocols.
341
+ If no suitable wrapper exists, reply exactly REQUIRED_RESULT_WRAPPER_MISSING
342
+ and do not mutate anything.
343
+ Reply with compact machine-readable status and evidence pointers.
344
+
345
+ Copy-and-paste terminal evidence is recovery/fallback only.
346
+ It is not the normal operating path.
347
+ """
348
+
349
+
350
+ def _initial_prompt_d2_block() -> str:
351
+ return f"""Communication-rule refresh dialog:
352
+
353
+ When the user writes "d2":
354
+ 1. Read the pending state:
355
+ .agentic/rule_ack/communication_refresh_pending.json
356
+ 2. Read the remote communication rule capsule from the ref specified by
357
+ remote_ref in the pending state, at the pending state's remote_path.
358
+ If remote_ref is absent, read it from `main`.
359
+ If no pending state exists, reply exactly RULE_REFRESH_NOT_PENDING.
360
+ 3. Compute the Git blob SHA exactly as Git does for SHA-1 blob objects:
361
+ sha1("blob " + byte_length + "\\0" + raw_file_bytes).
362
+ Verify it against expected_blob_sha in the pending state.
363
+ 4. Return a RULE_REFRESH_ACK in exactly this JSON format before any
364
+ further mutation:
365
+
366
+ {{
367
+ "kind": "communication_rule_refresh_ack",
368
+ "result_status": "PASS",
369
+ "source": "<remote_path from pending state>",
370
+ "remote": "<remote_ref from pending state, or main if absent>",
371
+ "blob_sha": "<actual blob SHA you computed>",
372
+ "generated_at": "<generated_at from the capsule file>",
373
+ "loaded_sections": {_loaded_sections_json_block()},
374
+ "rules_loaded": true
375
+ }}
376
+
377
+ If blob_sha does not match expected_blob_sha, reply exactly
378
+ RULE_REFRESH_ACK_BLOCKED and do not mutate anything.
379
+ """
380
+
381
+
382
+ def _initial_prompt_stop_rules_block() -> str:
383
+ return """Stop and report without mutating when:
384
+ - Bootstrap not accepted or validation_report.json is not PASS. Reply BOOTSTRAP_BLOCKED.
385
+ - g/go received but neither a fresh transfer result nor the GUI task carrier is available. Reply TASK_NOT_FOUND.
386
+ - d2 received but no pending state exists. Reply RULE_REFRESH_NOT_PENDING.
387
+ - d2 received but blob_sha does not match. Reply RULE_REFRESH_ACK_BLOCKED.
388
+ - Communication rule refresh is pending and no valid ACK exists.
389
+ - No suitable agentic-kit transfer/result wrapper exists for result publication. Reply REQUIRED_RESULT_WRAPPER_MISSING.
390
+ - Worktree is dirty in a way that blocks the required operation.
391
+ - Required command, wrapper, or source file is missing or ambiguous.
392
+
393
+ Do not guess. Do not improvise. Stop and report.
394
+ """
395
+
396
+
397
+ def build_initial_llm_prompt(
398
+ task_path: Path = CURRENT_USER_TASK_PATH,
399
+ task_ref: str = GUI_TRANSFER_TASK_REF,
400
+ project_root: Path | str = Path("."),
401
+ ) -> InitialLlmPrompt:
402
+ root = Path(project_root)
403
+ target_prompt = root / INITIAL_LLM_PROMPT_RELATIVE_PATH
404
+ if target_prompt.exists():
405
+ prompt_text = ensure_command_reference_in_prompt(
406
+ target_prompt.read_text(encoding="utf-8").rstrip() + "\n",
407
+ root,
408
+ )
409
+ return InitialLlmPrompt(
410
+ result_status="PASS",
411
+ kind="initial_llm_prompt",
412
+ prompt_text=prompt_text,
413
+ copy_paste_instruction=(
414
+ "Copy the prompt_text from the selected project's initial prompt "
415
+ "and paste it once into the LLM chat at the start of a new session."
416
+ ),
417
+ task_path=task_path.as_posix(),
418
+ task_ref=task_ref,
419
+ )
420
+ prompt = "\n\n".join(
421
+ (
422
+ _initial_prompt_bootstrap_block().strip(),
423
+ _initial_prompt_file_transfer_block(task_path, task_ref).strip(),
424
+ _initial_prompt_d2_block().strip(),
425
+ _initial_prompt_stop_rules_block().strip(),
426
+ )
427
+ )
428
+ return InitialLlmPrompt(
429
+ result_status="PASS",
430
+ kind="initial_llm_prompt",
431
+ prompt_text=ensure_command_reference_in_prompt(prompt + "\n", root),
432
+ copy_paste_instruction=(
433
+ "Copy the prompt_text and paste it once into the LLM chat at the start "
434
+ "of a new session. The LLM must read the bootstrap files before any work."
435
+ ),
436
+ task_path=task_path.as_posix(),
437
+ task_ref=task_ref,
438
+ )
439
+
440
+
441
+ GitRunner = Callable[[Path, tuple[str, ...]], subprocess.CompletedProcess[str]]
442
+
443
+
444
+ @dataclass(frozen=True)
445
+ class TaskCarrierPublishResult:
446
+ result_status: str
447
+ published_ref: str
448
+ remote_path: str
449
+ remote_readable: bool
450
+ blob_sha: str
451
+ commit_status: str
452
+ push_status: str
453
+ reason: str
454
+
455
+
456
+ def submit_user_task(
457
+ project_root: Path | str,
458
+ *,
459
+ title: str,
460
+ body: str,
461
+ task_path: Path = CURRENT_USER_TASK_PATH,
462
+ communication_mode: str = "file_transfer",
463
+ publish: bool = False,
464
+ created_at_utc: str | None = None,
465
+ git_runner: GitRunner | None = None,
466
+ ) -> SubmittedUserTask:
467
+ root = Path(project_root).resolve()
468
+ normalized_title = title.strip() or "GUI file-transfer task"
469
+ normalized_body = body.strip()
470
+ if not normalized_body:
471
+ raise ValueError("task body must not be empty")
472
+ created = created_at_utc or datetime.now(timezone.utc).replace(microsecond=0).isoformat()
473
+ head_sha = _git_output(root, "rev-parse", "HEAD") or "UNKNOWN"
474
+ short_head = _git_output(root, "rev-parse", "--short", "HEAD") or head_sha[:12]
475
+ branch = _git_output(root, "branch", "--show-current") or "UNKNOWN"
476
+ origin_main = _git_output(root, "rev-parse", "origin/main") or "UNKNOWN"
477
+ body_sha = hashlib.sha256(normalized_body.encode("utf-8")).hexdigest()
478
+ task_id = hashlib.sha256(
479
+ f"{created}\n{head_sha}\n{normalized_title}\n{body_sha}".encode("utf-8")
480
+ ).hexdigest()[:16]
481
+ relative_path = task_path.as_posix()
482
+ normalized_mode = normalize_communication_mode(communication_mode)
483
+ reply_contract = communication_reply_contract(normalized_mode)
484
+ local_execution_command = standard_command_for_communication_mode(normalized_mode)
485
+ local_execution_description = standard_command_description_for_communication_mode(normalized_mode)
486
+ command_manifest_sha = _command_manifest_sha_for_carrier(root)
487
+ payload = {
488
+ "schema_version": 1,
489
+ "kind": "gui_user_task_transfer_order",
490
+ "manifest": COMMAND_MANIFEST_RELATIVE_PATH,
491
+ "manifest_sha": command_manifest_sha,
492
+ "id": task_id,
493
+ "command_id": task_id,
494
+ "task_id": task_id,
495
+ "title": normalized_title,
496
+ "safety": "remote_llm_user_task",
497
+ "status": "submitted",
498
+ "branch": branch,
499
+ "expected_current_branch": branch,
500
+ "expected_head": short_head,
501
+ "expected_origin_main": origin_main,
502
+ "created_for_head": head_sha,
503
+ "report_path": f"docs/reports/command_runs/{task_id}-gui-user-task.md",
504
+ "communication_mode": normalized_mode,
505
+ "communication_mode_code": communication_mode_code(normalized_mode),
506
+ "communication_mode_label": communication_mode_label(normalized_mode),
507
+ "reply_contract": reply_contract,
508
+ "task_identity": {
509
+ "task_id": task_id,
510
+ "body_sha256": body_sha,
511
+ },
512
+ "g_go_handling": {
513
+ "read_repo_backed_state_first": True,
514
+ "latest_remote_transfer_report_path": CANONICAL_REMOTE_TRANSFER_REPORT_PATH.as_posix(),
515
+ "read_order": [
516
+ "latest_remote_transfer_report_or_outbox_first",
517
+ "gui_task_carrier_if_no_fresh_result_exists",
518
+ ],
519
+ "forbidden": "answering_g_go_from_chat_memory",
520
+ "compare_task_id_and_body_sha256": True,
521
+ "discard_previous_task_context_when_identity_changes": True,
522
+ },
523
+ "task_carrier_contract": {
524
+ "role": "gui_user_task_for_llm",
525
+ "status": "submitted",
526
+ "not_executable_by_transfer_continue": True,
527
+ "authoritative_ref": GUI_TRANSFER_TASK_REF,
528
+ "authoritative_path": relative_path,
529
+ "same_file_active_reply_contract": {
530
+ "submitted_gui_task_status": "submitted",
531
+ "active_order_ref": GUI_TRANSFER_TASK_REF,
532
+ "active_order_path": relative_path,
533
+ "active_order_required_status": "active",
534
+ "active_order_required_branch": GUI_TRANSFER_TASK_REF,
535
+ "python_payload_path": CANONICAL_TRANSFER_PAYLOAD_PATH.as_posix(),
536
+ "local_user_command": list(TRANSFER_CONTINUE_COMMAND),
537
+ "forbidden": [
538
+ "using_submitted_gui_task_as_executable_transfer_order",
539
+ "inventing_transfer_files_or_refs",
540
+ "asking_user_to_run_python_payload_directly",
541
+ ],
542
+ },
543
+ },
544
+ "local_execution": {
545
+ "standard_command": list(local_execution_command),
546
+ "description": local_execution_description,
547
+ },
548
+ "actions": [
549
+ {
550
+ "type": "run_command",
551
+ "command": ["./.venv/bin/agentic-kit", "transfer", "state", "--json"],
552
+ }
553
+ ],
554
+ "user_task": {
555
+ "title": normalized_title,
556
+ "body": normalized_body,
557
+ "body_sha256": body_sha,
558
+ },
559
+ "created_at_utc": created,
560
+ "head_sha": head_sha,
561
+ "task_status": "submitted",
562
+ "next_reply": "g",
563
+ "next_action": (
564
+ "Send g/go to the LLM; assistant must read "
565
+ f"the GUI task carrier {relative_path} from remote ref {GUI_TRANSFER_TASK_REF}."
566
+ if publish
567
+ else (
568
+ "GUI task carrier written locally only. Publish through the guarded "
569
+ "agentic-kit transfer path before sending g/go to the LLM."
570
+ )
571
+ ),
572
+ "remote_path": relative_path,
573
+ "task_path": relative_path,
574
+ "published_ref": GUI_TRANSFER_TASK_REF if publish else None,
575
+ "local_only": not publish,
576
+ }
577
+ publish_result = None
578
+ if publish:
579
+ publish_result = _publish_task_carrier(
580
+ root,
581
+ task_path,
582
+ payload,
583
+ git_runner=git_runner or _run_git,
584
+ )
585
+ else:
586
+ _write_task_payload(root / task_path, payload)
587
+
588
+ remote_readable = bool(publish_result and publish_result.remote_readable)
589
+ result_status = "PASS" if not publish or remote_readable else "FAIL"
590
+ commit_status = publish_result.commit_status if publish_result else "SKIPPED"
591
+ push_status = publish_result.push_status if publish_result else "SKIPPED"
592
+ blob_sha = publish_result.blob_sha if publish_result else ""
593
+ reason = (
594
+ publish_result.reason
595
+ if publish_result
596
+ else "task_carrier_local_only"
597
+ )
598
+ next_action = (
599
+ "Send g/go to the LLM; assistant must read "
600
+ f"the GUI task carrier {relative_path} from remote ref {GUI_TRANSFER_TASK_REF}."
601
+ if remote_readable
602
+ else (
603
+ "GUI task carrier written locally only. Publish through the guarded "
604
+ "agentic-kit transfer path before sending g/go to the LLM."
605
+ if not publish
606
+ else "Inspect GUI task carrier publish failure before sending g/go."
607
+ )
608
+ )
609
+ return SubmittedUserTask(
610
+ result_status=result_status,
611
+ kind="gui_file_transfer_user_task_submission",
612
+ task_id=task_id,
613
+ title=normalized_title,
614
+ remote_path=relative_path,
615
+ task_path=relative_path,
616
+ published_ref=GUI_TRANSFER_TASK_REF if publish else None,
617
+ next_reply="g",
618
+ next_action=next_action,
619
+ button_next_state=TaskEditorState.SENT.value if remote_readable else TaskEditorState.BLOCKED.value,
620
+ body_sha256=body_sha,
621
+ created_at_utc=created,
622
+ head_sha=head_sha,
623
+ blob_sha=blob_sha,
624
+ commit_status=commit_status,
625
+ push_status=push_status,
626
+ local_only=not remote_readable,
627
+ remote_readable=remote_readable,
628
+ reason=reason,
629
+ communication_mode=normalized_mode,
630
+ communication_mode_code=communication_mode_code(normalized_mode),
631
+ communication_mode_label=communication_mode_label(normalized_mode),
632
+ local_execution_command=local_execution_command,
633
+ reply_contract=reply_contract,
634
+ )
635
+
636
+
637
+ def _write_task_payload(target: Path, payload: dict[str, object]) -> None:
638
+ target.parent.mkdir(parents=True, exist_ok=True)
639
+ target.write_text(_render_task_payload(payload), encoding="utf-8")
640
+
641
+
642
+ def _render_task_payload(payload: dict[str, object]) -> str:
643
+ return yaml.safe_dump(payload, sort_keys=False, allow_unicode=False)
644
+
645
+
646
+ def _publish_task_carrier(
647
+ root: Path,
648
+ task_path: Path,
649
+ payload: dict[str, object],
650
+ *,
651
+ git_runner: GitRunner,
652
+ ) -> TaskCarrierPublishResult:
653
+ payload_text = _render_task_payload(payload)
654
+ with _working_directory(root):
655
+ original_branch = _git_runner_output(root, git_runner, ("branch", "--show-current")) or ""
656
+ ensure_result = _ensure_task_carrier_branch(root, git_runner=git_runner)
657
+ if ensure_result is not None:
658
+ return _restore_branch_after_publish(root, original_branch, ensure_result)
659
+
660
+ _write_task_payload(root / task_path, payload)
661
+ commit_paths = [task_path.as_posix()]
662
+ commit_result = transfer_repo_actions.commit_paths(
663
+ "Publish GUI transfer order",
664
+ commit_paths,
665
+ required_branch=GUI_TRANSFER_TASK_REF,
666
+ )
667
+ if commit_result.returncode != 0:
668
+ result = TaskCarrierPublishResult(
669
+ result_status="FAIL",
670
+ published_ref=GUI_TRANSFER_TASK_REF,
671
+ remote_path=task_path.as_posix(),
672
+ remote_readable=False,
673
+ blob_sha="",
674
+ commit_status=commit_result.result_status,
675
+ push_status="SKIPPED",
676
+ reason="commit_failed",
677
+ )
678
+ return _restore_branch_after_publish(root, original_branch, result)
679
+
680
+ push_result = transfer_repo_actions.push_current(required_branch=GUI_TRANSFER_TASK_REF)
681
+ if push_result.returncode != 0:
682
+ result = TaskCarrierPublishResult(
683
+ result_status="FAIL",
684
+ published_ref=GUI_TRANSFER_TASK_REF,
685
+ remote_path=task_path.as_posix(),
686
+ remote_readable=False,
687
+ blob_sha="",
688
+ commit_status=commit_result.result_status,
689
+ push_status=push_result.result_status,
690
+ reason="push_failed",
691
+ )
692
+ return _restore_branch_after_publish(root, original_branch, result)
693
+
694
+ verification = _verify_remote_task_carrier(
695
+ root,
696
+ task_path,
697
+ expected_payload_text=payload_text,
698
+ git_runner=git_runner,
699
+ )
700
+ result = TaskCarrierPublishResult(
701
+ result_status="PASS" if verification.remote_readable else "FAIL",
702
+ published_ref=GUI_TRANSFER_TASK_REF,
703
+ remote_path=task_path.as_posix(),
704
+ remote_readable=verification.remote_readable,
705
+ blob_sha=verification.blob_sha,
706
+ commit_status=commit_result.result_status,
707
+ push_status=push_result.result_status,
708
+ reason=verification.reason,
709
+ )
710
+ return _restore_branch_after_publish(root, original_branch, result)
711
+
712
+
713
+ def _ensure_task_carrier_branch(
714
+ root: Path,
715
+ *,
716
+ git_runner: GitRunner,
717
+ ) -> TaskCarrierPublishResult | None:
718
+ fetch = git_runner(root, _fetch_task_ref_args())
719
+ local_exists = git_runner(root, ("rev-parse", "--verify", "--quiet", GUI_TRANSFER_TASK_REF)).returncode == 0
720
+ remote_exists = git_runner(root, ("rev-parse", "--verify", "--quiet", f"origin/{GUI_TRANSFER_TASK_REF}")).returncode == 0
721
+ if local_exists or remote_exists:
722
+ switch_result = transfer_repo_actions.branch_switch(GUI_TRANSFER_TASK_REF, pull=remote_exists)
723
+ if switch_result.returncode != 0:
724
+ return TaskCarrierPublishResult(
725
+ result_status="FAIL",
726
+ published_ref=GUI_TRANSFER_TASK_REF,
727
+ remote_path=CURRENT_USER_TASK_PATH.as_posix(),
728
+ remote_readable=False,
729
+ blob_sha="",
730
+ commit_status="SKIPPED",
731
+ push_status="SKIPPED",
732
+ reason="branch_switch_failed",
733
+ )
734
+ return None
735
+
736
+ create_result = transfer_repo_actions.branch_create(GUI_TRANSFER_TASK_REF, start_point="main")
737
+ if create_result.returncode != 0:
738
+ return TaskCarrierPublishResult(
739
+ result_status="FAIL",
740
+ published_ref=GUI_TRANSFER_TASK_REF,
741
+ remote_path=CURRENT_USER_TASK_PATH.as_posix(),
742
+ remote_readable=False,
743
+ blob_sha="",
744
+ commit_status="SKIPPED",
745
+ push_status="SKIPPED",
746
+ reason="branch_create_failed" if fetch.returncode != 0 else "branch_create_failed_after_fetch",
747
+ )
748
+ return None
749
+
750
+
751
+ @dataclass(frozen=True)
752
+ class RemoteTaskCarrierVerification:
753
+ remote_readable: bool
754
+ blob_sha: str
755
+ reason: str
756
+
757
+
758
+ @dataclass(frozen=True)
759
+ class TerminalLaunchPlan:
760
+ result_status: str
761
+ platform_name: str
762
+ script_path: str
763
+ command: tuple[str, ...]
764
+ launch_argv: tuple[str, ...]
765
+ reason: str
766
+
767
+ def as_json_data(self) -> dict[str, object]:
768
+ return asdict(self)
769
+
770
+
771
+ def _terminal_script_command_for_platform(
772
+ platform_name: str,
773
+ communication_mode: str,
774
+ ) -> tuple[str, ...]:
775
+ return standard_command_for_communication_mode(
776
+ communication_mode,
777
+ platform_name=platform_name,
778
+ )
779
+
780
+
781
+ def _terminal_script_path(root: Path, platform_name: str, communication_mode: str) -> Path:
782
+ suffix = ".cmd" if platform_name == "Windows" else ".command" if platform_name == "Darwin" else ".sh"
783
+ mode_name = normalize_communication_mode(communication_mode).replace("_", "-")
784
+ return root / "tmp" / f"agentic-kit-{mode_name}-standard{suffix}"
785
+
786
+
787
+ def _terminal_script_text(
788
+ root: Path,
789
+ command: tuple[str, ...],
790
+ platform_name: str,
791
+ communication_mode: str,
792
+ ) -> str:
793
+ description = standard_command_description_for_communication_mode(communication_mode)
794
+ if platform_name == "Windows":
795
+ command_text = subprocess.list2cmdline(command) if command else ""
796
+ command_line = f"{command_text}\r\n" if command_text else "echo Paste the LLM recovery block here.\r\n"
797
+ return (
798
+ "@echo off\r\n"
799
+ f"cd /d {subprocess.list2cmdline([str(root)])}\r\n"
800
+ f"echo [agentic-kit] {description}\r\n"
801
+ f"{command_line}"
802
+ "echo.\r\n"
803
+ "echo [agentic-kit] command finished with RC=%ERRORLEVEL%\r\n"
804
+ "cmd /k\r\n"
805
+ )
806
+ command_text = " ".join(shlex.quote(part) for part in command)
807
+ command_block = (
808
+ f"{command_text}\n"
809
+ "rc=$?\n"
810
+ "printf '\\n[agentic-kit] command finished with RC=%s\\n' \"$rc\"\n"
811
+ if command_text
812
+ else "printf '%s\\n' '[agentic-kit] Paste the LLM recovery block here.'\n"
813
+ )
814
+ return (
815
+ "#!/bin/sh\n"
816
+ f"cd {shlex.quote(str(root))}\n"
817
+ f"printf '%s\\n' {shlex.quote('[agentic-kit] ' + description)}\n"
818
+ f"{command_block}"
819
+ "exec ${SHELL:-/bin/sh}\n"
820
+ )
821
+
822
+
823
+ def build_terminal_launch_plan(
824
+ project_root: Path | str,
825
+ *,
826
+ communication_mode: str = "file_transfer",
827
+ platform_name: str | None = None,
828
+ ) -> TerminalLaunchPlan:
829
+ root = Path(project_root).resolve()
830
+ detected = platform_name or platform.system()
831
+ command = _terminal_script_command_for_platform(detected, communication_mode)
832
+ script_path = _terminal_script_path(root, detected, communication_mode)
833
+ script_path.parent.mkdir(parents=True, exist_ok=True)
834
+ script_path.write_text(
835
+ _terminal_script_text(root, command, detected, communication_mode),
836
+ encoding="utf-8",
837
+ )
838
+ if detected != "Windows":
839
+ script_path.chmod(0o755)
840
+
841
+ if detected == "Darwin":
842
+ argv = ("open", "-a", "Terminal", str(script_path))
843
+ elif detected == "Windows":
844
+ argv = ("cmd.exe", "/c", "start", "Agentic Kit Transfer", str(script_path))
845
+ elif detected == "Linux":
846
+ terminal = _first_available_terminal()
847
+ if not terminal:
848
+ return TerminalLaunchPlan(
849
+ "FAIL",
850
+ detected,
851
+ str(script_path),
852
+ command,
853
+ (),
854
+ "terminal_not_found",
855
+ )
856
+ if terminal.endswith("gnome-terminal") or terminal.endswith("konsole"):
857
+ argv = (terminal, "--", str(script_path))
858
+ else:
859
+ argv = (terminal, "-e", str(script_path))
860
+ else:
861
+ return TerminalLaunchPlan("FAIL", detected, str(script_path), command, (), "unsupported_platform")
862
+ return TerminalLaunchPlan("PASS", detected, str(script_path), command, argv, "terminal_launcher_ready")
863
+
864
+
865
+ def _first_available_terminal() -> str:
866
+ candidates = (
867
+ "x-terminal-emulator",
868
+ "gnome-terminal",
869
+ "konsole",
870
+ "xfce4-terminal",
871
+ "xterm",
872
+ )
873
+ for candidate in candidates:
874
+ resolved = subprocess.run(
875
+ ["which", candidate],
876
+ text=True,
877
+ stdout=subprocess.PIPE,
878
+ stderr=subprocess.DEVNULL,
879
+ check=False,
880
+ )
881
+ if resolved.returncode == 0 and resolved.stdout.strip():
882
+ return resolved.stdout.strip()
883
+ return ""
884
+
885
+
886
+ def open_transfer_terminal(
887
+ project_root: Path | str,
888
+ *,
889
+ communication_mode: str = "file_transfer",
890
+ ) -> TerminalLaunchPlan:
891
+ plan = build_terminal_launch_plan(project_root, communication_mode=communication_mode)
892
+ if plan.result_status != "PASS":
893
+ return plan
894
+ try:
895
+ subprocess.Popen(list(plan.launch_argv))
896
+ except OSError as exc:
897
+ return TerminalLaunchPlan(
898
+ "FAIL",
899
+ plan.platform_name,
900
+ plan.script_path,
901
+ plan.command,
902
+ plan.launch_argv,
903
+ f"terminal_launch_failed: {exc}",
904
+ )
905
+ return plan
906
+
907
+
908
+ def _verify_remote_task_carrier(
909
+ root: Path,
910
+ task_path: Path,
911
+ *,
912
+ expected_payload_text: str,
913
+ git_runner: GitRunner,
914
+ ) -> RemoteTaskCarrierVerification:
915
+ fetch = git_runner(root, _fetch_task_ref_args())
916
+ if fetch.returncode != 0:
917
+ return RemoteTaskCarrierVerification(False, "", "remote_fetch_failed")
918
+ remote_spec = remote_gui_task_spec(task_path)
919
+ blob = git_runner(root, ("rev-parse", "--verify", remote_spec))
920
+ if blob.returncode != 0:
921
+ return RemoteTaskCarrierVerification(False, "", "remote_blob_missing")
922
+ show = git_runner(root, ("show", remote_spec))
923
+ if show.returncode != 0:
924
+ return RemoteTaskCarrierVerification(False, blob.stdout.strip(), "remote_file_missing")
925
+ if show.stdout != expected_payload_text:
926
+ return RemoteTaskCarrierVerification(False, blob.stdout.strip(), "remote_content_mismatch")
927
+ return RemoteTaskCarrierVerification(True, blob.stdout.strip(), "remote_carrier_verified")
928
+
929
+
930
+ def _fetch_task_ref_args() -> tuple[str, str, str]:
931
+ return fetch_gui_transfer_ref_args()
932
+
933
+
934
+ def _restore_branch_after_publish(
935
+ root: Path,
936
+ original_branch: str,
937
+ result: TaskCarrierPublishResult,
938
+ ) -> TaskCarrierPublishResult:
939
+ if not original_branch or original_branch == GUI_TRANSFER_TASK_REF:
940
+ return result
941
+ restore = transfer_repo_actions.branch_switch(original_branch)
942
+ if restore.returncode == 0:
943
+ return result
944
+ return TaskCarrierPublishResult(
945
+ result_status="FAIL",
946
+ published_ref=result.published_ref,
947
+ remote_path=result.remote_path,
948
+ remote_readable=False,
949
+ blob_sha=result.blob_sha,
950
+ commit_status=result.commit_status,
951
+ push_status=result.push_status,
952
+ reason=f"{result.reason}; restore_branch_failed",
953
+ )
954
+
955
+
956
+ def task_editor_visible_for_mode(mode: str) -> bool:
957
+ return normalize_communication_mode(mode) in {"remote", "file_transfer", "copy_paste"}
958
+
959
+
960
+ def task_editor_send_enabled(
961
+ body: str,
962
+ *,
963
+ traffic_light_state: str,
964
+ communication_context_fresh: bool,
965
+ required_next_reply: str | None,
966
+ ) -> bool:
967
+ return (
968
+ bool(body.strip())
969
+ and traffic_light_state == "READY"
970
+ and communication_context_fresh
971
+ and required_next_reply is None
972
+ )
973
+
974
+
975
+ def task_editor_state_after_send(result_status: str, *, remote_readable: bool = False) -> TaskEditorState:
976
+ return TaskEditorState.SENT if result_status == "PASS" and remote_readable else TaskEditorState.BLOCKED
977
+
978
+
979
+ def read_user_task(project_root: Path | str, task_path: Path = CURRENT_USER_TASK_PATH) -> dict[str, object]:
980
+ root = Path(project_root).resolve()
981
+ target = root / task_path
982
+ if not target.exists():
983
+ return {
984
+ "schema_version": 1,
985
+ "kind": "gui_file_transfer_user_task_read",
986
+ "result_status": "FAIL",
987
+ "reason": "TASK_NOT_FOUND",
988
+ "task_path": task_path.as_posix(),
989
+ "next_action": "Click Send in the GUI first, then publish the transfer order.",
990
+ }
991
+ try:
992
+ payload = yaml.safe_load(target.read_text(encoding="utf-8")) or {}
993
+ except yaml.YAMLError as exc:
994
+ return {
995
+ "schema_version": 1,
996
+ "kind": "gui_file_transfer_user_task_read",
997
+ "result_status": "FAIL",
998
+ "reason": "TASK_YAML_INVALID",
999
+ "task_path": task_path.as_posix(),
1000
+ "error": str(exc),
1001
+ "next_action": "Inspect the transfer order YAML before continuing.",
1002
+ }
1003
+ if not isinstance(payload, dict):
1004
+ return {
1005
+ "schema_version": 1,
1006
+ "kind": "gui_file_transfer_user_task_read",
1007
+ "result_status": "FAIL",
1008
+ "reason": "TASK_YAML_NOT_OBJECT",
1009
+ "task_path": task_path.as_posix(),
1010
+ "next_action": "Inspect the transfer order YAML before continuing.",
1011
+ }
1012
+ return {
1013
+ "schema_version": 1,
1014
+ "kind": "gui_file_transfer_user_task_read",
1015
+ "result_status": "PASS",
1016
+ "reason": "TASK_FOUND",
1017
+ "task_path": task_path.as_posix(),
1018
+ "task": payload,
1019
+ "next_action": "Inspect the transfer order or publish it through the guarded transfer path.",
1020
+ }
1021
+
1022
+
1023
+ def task_editor_state_after_read(result_status: str) -> TaskEditorState:
1024
+ return TaskEditorState.IDLE if result_status == "PASS" else TaskEditorState.BLOCKED
1025
+
1026
+
1027
+ def transfer_state_has_canonical_outbox_result(payload: dict[str, object]) -> bool:
1028
+ transfer_files = payload.get("transfer_files")
1029
+ if isinstance(transfer_files, dict):
1030
+ outbox = transfer_files.get("outbox")
1031
+ else:
1032
+ outbox = payload.get("outbox")
1033
+ if not isinstance(outbox, dict):
1034
+ return False
1035
+ last_result = outbox.get("last_result")
1036
+ if not isinstance(last_result, dict):
1037
+ return False
1038
+ return bool(last_result.get("exists"))
1039
+
1040
+
1041
+ def _git_output(root: Path, *args: str) -> str:
1042
+ completed = _run_git(root, args)
1043
+ if completed.returncode != 0:
1044
+ return ""
1045
+ return completed.stdout.strip()
1046
+
1047
+
1048
+ def _git_runner_output(root: Path, git_runner: GitRunner, args: tuple[str, ...]) -> str:
1049
+ completed = git_runner(root, args)
1050
+ if completed.returncode != 0:
1051
+ return ""
1052
+ return completed.stdout.strip()
1053
+
1054
+
1055
+ def _run_git(root: Path, args: tuple[str, ...]) -> subprocess.CompletedProcess[str]:
1056
+ return subprocess.run(
1057
+ ["git", *args],
1058
+ cwd=root,
1059
+ text=True,
1060
+ stdout=subprocess.PIPE,
1061
+ stderr=subprocess.PIPE,
1062
+ check=False,
1063
+ )
1064
+
1065
+
1066
+ class _working_directory:
1067
+ def __init__(self, path: Path) -> None:
1068
+ self.path = path
1069
+ self.previous: Path | None = None
1070
+
1071
+ def __enter__(self) -> None:
1072
+ self.previous = Path.cwd()
1073
+ os.chdir(self.path)
1074
+
1075
+ def __exit__(self, *_exc: object) -> None:
1076
+ if self.previous is not None:
1077
+ os.chdir(self.previous)