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,516 @@
1
+ """Repository-backed command runner for agent handoff.
2
+
3
+ The runner executes exactly one repo-local command file described by
4
+ .agentic/commands/current.yaml and .agentic/commands/current.sh.
5
+
6
+ It is intentionally conservative:
7
+ - no command file means no-op failure,
8
+ - duplicate command_id is refused,
9
+ - shell syntax is checked before execution,
10
+ - execution is delegated to terminal_logging.run_logged(),
11
+ - reports are written under docs/reports/command_runs/,
12
+ - executed command ids are tracked in .agentic/commands/executed.jsonl.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import dataclasses
18
+ import datetime as _dt
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ from agentic_project_kit.safe_push import safe_push
27
+
28
+ from agentic_project_kit import terminal_logging
29
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
30
+
31
+
32
+ def _clean_git_env() -> dict[str, str]:
33
+ env = os.environ.copy()
34
+ for key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES"):
35
+ env.pop(key, None)
36
+ return env
37
+
38
+ COMMAND_DIR = Path(".agentic/commands")
39
+ CURRENT_YAML = COMMAND_DIR / "current.yaml"
40
+ CURRENT_SCRIPT = COMMAND_DIR / "current.sh"
41
+ INBOX_DIR = COMMAND_DIR / "inbox"
42
+ EXECUTED_JSONL = COMMAND_DIR / "executed.jsonl"
43
+ REPORT_DIR = Path(LEGACY_DEFAULTS.command_runs_root)
44
+ LATEST_COMMAND_RUN_POINTER = REPORT_DIR / "LATEST_COMMAND_RUN.txt"
45
+
46
+ ALLOWED_SAFETY_CLASSES = {
47
+ "read-only",
48
+ "local-only",
49
+ "remote-mutation", "local-verification",
50
+ }
51
+
52
+
53
+ def normalize_safety_class(value: str) -> str:
54
+ if value == "local-verification":
55
+ return "local-only"
56
+ return value
57
+
58
+
59
+ OUTCOME_PASS_EXECUTED = "PASS_EXECUTED"
60
+ OUTCOME_FAIL_NO_COMMAND = "FAIL_NO_COMMAND"
61
+ OUTCOME_FAIL_ALREADY_EXECUTED = "FAIL_ALREADY_EXECUTED"
62
+ OUTCOME_FAIL_INVALID_COMMAND = "FAIL_INVALID_COMMAND"
63
+ OUTCOME_FAIL_SHELL_SYNTAX = "FAIL_SHELL_SYNTAX"
64
+ OUTCOME_FAIL_COMMAND = "FAIL_COMMAND"
65
+ OUTCOME_FAIL_UPLOAD = "FAIL_UPLOAD"
66
+ OUTCOME_FAIL_AMBIGUOUS_COMMANDS = "FAIL_AMBIGUOUS_COMMANDS"
67
+ OUTCOME_FAIL_PULL = "FAIL_PULL"
68
+ OUTCOME_FAIL_POSTCONDITION = "FAIL_POSTCONDITION"
69
+
70
+
71
+ @dataclasses.dataclass(frozen=True)
72
+ class AgentCommand:
73
+ command_id: str
74
+ title: str
75
+ safety_class: str
76
+ expected_branch: str | None = None
77
+ description: str = ""
78
+
79
+
80
+ def _parse_simple_yaml(path: Path) -> dict[str, str]:
81
+ data: dict[str, str] = {}
82
+ for raw in path.read_text(encoding="utf-8").splitlines():
83
+ line = raw.strip()
84
+ if not line or line.startswith("#"):
85
+ continue
86
+ if ":" not in line:
87
+ raise ValueError(f"Invalid YAML line without colon: {raw}")
88
+ key, value = line.split(":", 1)
89
+ key = key.strip()
90
+ value = value.strip()
91
+ if not key:
92
+ raise ValueError(f"Invalid YAML line without key: {raw}")
93
+ if (value.startswith("\'") and value.endswith("\'")) or (value.startswith(chr(34)) and value.endswith(chr(34))):
94
+ value = value[1:-1]
95
+ data[key] = value
96
+ return data
97
+
98
+
99
+
100
+ def inbox_yaml_files() -> list[Path]:
101
+ if not INBOX_DIR.exists():
102
+ return []
103
+ return sorted(INBOX_DIR.glob("*.yaml"))
104
+
105
+
106
+ def pending_inbox_command_pair() -> tuple[Path, Path]:
107
+ pairs: list[tuple[Path, Path]] = []
108
+ for yaml_path in inbox_yaml_files():
109
+ script_path = yaml_path.with_suffix(".sh")
110
+ if script_path.exists():
111
+ pairs.append((yaml_path, script_path))
112
+ if not pairs:
113
+ raise FileNotFoundError("No complete pending command pair in .agentic/commands/inbox")
114
+ if len(pairs) > 1:
115
+ names = ", ".join(path.name for path, _ in pairs)
116
+ raise RuntimeError("Multiple pending commands: " + names)
117
+ return pairs[0]
118
+
119
+
120
+ def prepare_current_from_inbox() -> tuple[Path, Path]:
121
+ yaml_path, script_path = pending_inbox_command_pair()
122
+ COMMAND_DIR.mkdir(parents=True, exist_ok=True)
123
+ CURRENT_YAML.write_text(yaml_path.read_text(encoding="utf-8"), encoding="utf-8")
124
+ CURRENT_SCRIPT.write_text(script_path.read_text(encoding="utf-8"), encoding="utf-8")
125
+ CURRENT_SCRIPT.chmod(0o755)
126
+ yaml_path.unlink()
127
+ script_path.unlink()
128
+ return yaml_path, script_path
129
+
130
+ def remove_current_files() -> None:
131
+ for item in (CURRENT_YAML, CURRENT_SCRIPT):
132
+ if item.exists():
133
+ item.unlink()
134
+
135
+ def load_current_command() -> AgentCommand:
136
+ if not CURRENT_YAML.exists() or not CURRENT_SCRIPT.exists():
137
+ raise FileNotFoundError("Missing .agentic/commands/current.yaml or current.sh")
138
+ data = _parse_simple_yaml(CURRENT_YAML)
139
+ required = ("command_id", "title", "safety_class")
140
+ missing = [key for key in required if not data.get(key)]
141
+ if missing:
142
+ raise ValueError("Missing required command fields: " + ", ".join(missing))
143
+ safety = data["safety_class"]
144
+ safety = normalize_safety_class(safety)
145
+ if safety not in ALLOWED_SAFETY_CLASSES:
146
+ raise ValueError(f"Unsupported safety_class: {safety}")
147
+ return AgentCommand(
148
+ command_id=data["command_id"],
149
+ title=data["title"],
150
+ safety_class=safety,
151
+ expected_branch=data.get("expected_branch") or None,
152
+ description=data.get("description", ""),
153
+ )
154
+
155
+
156
+ def read_executed_ids() -> set[str]:
157
+ if not EXECUTED_JSONL.exists():
158
+ return set()
159
+ ids: set[str] = set()
160
+ for raw in EXECUTED_JSONL.read_text(encoding="utf-8").splitlines():
161
+ if not raw.strip():
162
+ continue
163
+ try:
164
+ item = json.loads(raw)
165
+ except json.JSONDecodeError:
166
+ continue
167
+ command_id = item.get("command_id")
168
+ if isinstance(command_id, str):
169
+ ids.add(command_id)
170
+ return ids
171
+
172
+
173
+ def script_sha256(path: Path = CURRENT_SCRIPT) -> str:
174
+ if not path.exists():
175
+ return "missing"
176
+ return hashlib.sha256(path.read_bytes()).hexdigest()
177
+
178
+
179
+ def current_branch() -> str:
180
+ # Do not let inherited Git environment or an enclosing repository leak into
181
+ # temporary test directories. For this runner, the current directory must
182
+ # itself be a Git worktree root or an unknown branch is safer.
183
+ cwd = Path.cwd().resolve()
184
+ if not (cwd / ".git").exists():
185
+ return "unknown"
186
+
187
+ inside = subprocess.run(
188
+ ["git", "rev-parse", "--is-inside-work-tree"],
189
+ text=True,
190
+ capture_output=True,
191
+ check=False,
192
+ env=_clean_git_env(),
193
+ )
194
+ if inside.returncode != 0 or inside.stdout.strip() != "true":
195
+ return "unknown"
196
+
197
+ top_level = subprocess.run(
198
+ ["git", "rev-parse", "--show-toplevel"],
199
+ text=True,
200
+ capture_output=True,
201
+ check=False,
202
+ env=_clean_git_env(),
203
+ )
204
+ if top_level.returncode != 0 or Path(top_level.stdout.strip()).resolve() != cwd:
205
+ return "unknown"
206
+
207
+ proc = subprocess.run(
208
+ ["git", "branch", "--show-current"],
209
+ text=True,
210
+ capture_output=True,
211
+ check=False,
212
+ env=_clean_git_env(),
213
+ )
214
+ if proc.returncode != 0:
215
+ return "unknown"
216
+ return proc.stdout.strip() or "unknown"
217
+
218
+
219
+ def validate_command(command: AgentCommand) -> tuple[str, str]:
220
+ if command.command_id in read_executed_ids():
221
+ return OUTCOME_FAIL_ALREADY_EXECUTED, command.command_id
222
+ if command.expected_branch and command.expected_branch != current_branch():
223
+ return OUTCOME_FAIL_INVALID_COMMAND, f"Expected branch {command.expected_branch}, got {current_branch()}"
224
+ syntax = subprocess.run(["sh", "-n", CURRENT_SCRIPT.as_posix()], text=True, capture_output=True, check=False)
225
+ if syntax.returncode != 0:
226
+ return OUTCOME_FAIL_SHELL_SYNTAX, syntax.stderr.strip() or syntax.stdout.strip()
227
+ return OUTCOME_PASS_EXECUTED, "valid"
228
+
229
+
230
+ def _report_dir(project_root: Path = Path(".")) -> Path:
231
+ if REPORT_DIR != Path(LEGACY_DEFAULTS.command_runs_root):
232
+ return REPORT_DIR
233
+ return load_workspace(project_root).command_runs_dir()
234
+
235
+
236
+ def _latest_command_run_pointer(project_root: Path = Path(".")) -> Path:
237
+ if LATEST_COMMAND_RUN_POINTER != REPORT_DIR / "LATEST_COMMAND_RUN.txt":
238
+ return LATEST_COMMAND_RUN_POINTER
239
+ return load_workspace(project_root).latest_command_run_pointer()
240
+
241
+
242
+ def report_path(command_id: str, project_root: Path = Path(".")) -> Path:
243
+ safe = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in command_id)
244
+ return _report_dir(project_root) / f"{safe}.md"
245
+
246
+
247
+ def write_report(command: AgentCommand, outcome: str, exit_code: int, log_path: Path | None, detail: str = "") -> Path:
248
+ report_dir = _report_dir()
249
+ report_dir.mkdir(parents=True, exist_ok=True)
250
+ path = report_path(command.command_id)
251
+ lines = [
252
+ f"# Agent command run: {command.command_id}",
253
+ "",
254
+ f"- Title: {command.title}",
255
+ f"- Safety class: {command.safety_class}",
256
+ f"- Outcome: {outcome}",
257
+ f"- Exit code: {exit_code}",
258
+ f"- Branch: {current_branch()}",
259
+ f"- Script SHA256: {script_sha256()}",
260
+ ]
261
+ if log_path is not None:
262
+ lines.append(f"- Terminal log: {log_path.as_posix()}")
263
+ if detail:
264
+ lines.extend(["", "## Detail", "", detail])
265
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
266
+ latest = _latest_command_run_pointer()
267
+ latest.parent.mkdir(parents=True, exist_ok=True)
268
+ latest.write_text(path.as_posix() + "\n", encoding="utf-8")
269
+ return path
270
+
271
+
272
+ def append_executed(command: AgentCommand, outcome: str, exit_code: int) -> None:
273
+ COMMAND_DIR.mkdir(parents=True, exist_ok=True)
274
+ entry = {
275
+ "command_id": command.command_id,
276
+ "outcome": outcome,
277
+ "exit_code": exit_code,
278
+ "script_sha256": script_sha256(),
279
+ "timestamp_utc": _dt.datetime.now(_dt.UTC).isoformat(),
280
+ }
281
+ with EXECUTED_JSONL.open("a", encoding="utf-8") as handle:
282
+ handle.write(json.dumps(entry, sort_keys=True) + "\n")
283
+
284
+
285
+
286
+ def _git_path_is_tracked(path: Path) -> bool:
287
+ result = subprocess.run(
288
+ ["git", "ls-files", "--error-unmatch", path.as_posix()],
289
+ stdout=subprocess.DEVNULL,
290
+ stderr=subprocess.DEVNULL,
291
+ check=False,
292
+ )
293
+ return result.returncode == 0
294
+
295
+
296
+ def stage_commit_push(paths: list[Path], message: str, *, required_branch: str = "", allow_main: bool = False) -> int:
297
+ branch = current_branch()
298
+ if required_branch and branch != required_branch:
299
+ print(f"FAIL_BRANCH_MISMATCH actual={branch} required={required_branch}")
300
+ return 1
301
+ if branch == "main" and not allow_main:
302
+ print("FAIL_MAIN_MUTATION_NOT_ALLOWED")
303
+ return 1
304
+ if branch in {"", "unknown"}:
305
+ print("FAIL_NO_CURRENT_BRANCH")
306
+ return 1
307
+ push_preflight = safe_push(
308
+ Path("."),
309
+ target_branch=branch,
310
+ purpose="agent command runner stage commit push",
311
+ set_upstream=True,
312
+ dry_run=True,
313
+ )
314
+ if not push_preflight.ok:
315
+ print("FAIL_PUSH_PREFLIGHT_BLOCKED")
316
+ for reason in push_preflight.reasons:
317
+ print(reason)
318
+ return push_preflight.returncode
319
+ add_paths = [path for path in paths if path.exists() or _git_path_is_tracked(path)]
320
+ if add_paths:
321
+ subprocess.run(["git", "add", *[path.as_posix() for path in add_paths]], check=True)
322
+ subprocess.run(["git", "commit", "-m", message], check=True)
323
+ push = safe_push(
324
+ Path("."),
325
+ target_branch=branch,
326
+ purpose="agent command runner stage commit push",
327
+ set_upstream=True,
328
+ )
329
+ if not push.ok:
330
+ print(push.stderr or push.stdout or "FAIL_PUSH_FAILED")
331
+ return push.returncode
332
+
333
+ def logged_script_has_fail_result_marker(log_path: Path | None) -> bool:
334
+ if log_path is None or not log_path.exists():
335
+ return False
336
+ text = log_path.read_text(encoding="utf-8", errors="replace")
337
+ fail_pos = text.rfind("### RESULT: FAIL ###")
338
+ pass_pos = text.rfind("### RESULT: PASS ###")
339
+ return fail_pos != -1 and fail_pos > pass_pos
340
+
341
+
342
+ def agent_run(extra_upload_paths: list[Path] | None = None) -> int:
343
+ extra_upload_paths = list(extra_upload_paths or [])
344
+ try:
345
+ command = load_current_command()
346
+ except (FileNotFoundError, ValueError) as exc:
347
+ print(OUTCOME_FAIL_NO_COMMAND if isinstance(exc, FileNotFoundError) else OUTCOME_FAIL_INVALID_COMMAND)
348
+ print(str(exc))
349
+ return 1
350
+
351
+ validation_outcome, validation_detail = validate_command(command)
352
+ if validation_outcome == OUTCOME_FAIL_ALREADY_EXECUTED:
353
+ print(validation_outcome)
354
+ print(validation_detail)
355
+ write_report(command, validation_outcome, 0, None, validation_detail)
356
+ return 0
357
+ if validation_outcome != OUTCOME_PASS_EXECUTED:
358
+ print(validation_outcome)
359
+ print(validation_detail)
360
+ write_report(command, validation_outcome, 1, None, validation_detail)
361
+ return 1
362
+
363
+ print(f"COMMAND_ID={command.command_id}")
364
+ print(f"TITLE={command.title}")
365
+ print(f"SAFETY_CLASS={command.safety_class}")
366
+
367
+ exit_code = terminal_logging.run_logged(command.command_id, ["sh", CURRENT_SCRIPT.as_posix()])
368
+ log_path = terminal_logging.read_latest_pointer()
369
+ fail_marker = logged_script_has_fail_result_marker(log_path)
370
+ outcome = OUTCOME_PASS_EXECUTED if exit_code == 0 and not fail_marker else OUTCOME_FAIL_COMMAND
371
+ if fail_marker and exit_code == 0:
372
+ exit_code = 1
373
+
374
+ report = write_report(command, outcome, exit_code, log_path)
375
+ append_executed(command, outcome, exit_code)
376
+
377
+ upload_paths = [report, _latest_command_run_pointer(), EXECUTED_JSONL, *extra_upload_paths]
378
+ if log_path is not None:
379
+ upload_paths.append(log_path)
380
+ upload_paths.append(terminal_logging.LATEST_POINTER)
381
+
382
+ pushed = stage_commit_push(
383
+ upload_paths,
384
+ f"Record agent command run {command.command_id}",
385
+ required_branch=command.expected_branch or "",
386
+ )
387
+ if pushed != 0:
388
+ print(OUTCOME_FAIL_UPLOAD)
389
+ return pushed
390
+
391
+ print(outcome)
392
+ return 0 if outcome == OUTCOME_PASS_EXECUTED else exit_code or 1
393
+
394
+
395
+
396
+
397
+ def print_agent_next_footer(outcome: str, reply: str, reason: str = "") -> None:
398
+ print("")
399
+ print(f"### AGENT-NEXT RESULT: {outcome} ###")
400
+ print(f"reply={reply}")
401
+ if reply == "p":
402
+ print("### NEXT CHAT REPLY: PASS -> p ###")
403
+ elif reply == "f":
404
+ print("### NEXT CHAT REPLY: FAIL -> f ###")
405
+ elif reply == "paste-output":
406
+ print("### NEXT CHAT REPLY: HARD-FAIL -> paste output ###")
407
+ else:
408
+ print("### NEXT CHAT REPLY: NO-COMMAND -> ask agent to queue command ###")
409
+ if reason:
410
+ print(f"reason={reason}")
411
+ latest_command_run = _latest_command_run_pointer()
412
+ if latest_command_run.exists():
413
+ print(f"latest_command_run={latest_command_run.as_posix()}")
414
+ print("### END AGENT-NEXT RESULT ###")
415
+ if outcome == "PASS":
416
+ work_result = "PASS"
417
+ evidence_result = "PASS"
418
+ overall_result = "PASS"
419
+ elif outcome == "FAIL":
420
+ work_result = "FAIL"
421
+ evidence_result = "PASS"
422
+ overall_result = "FAIL"
423
+ elif outcome == "NO-COMMAND":
424
+ work_result = "NO-COMMAND"
425
+ evidence_result = "PASS"
426
+ overall_result = "NO-COMMAND"
427
+ else:
428
+ work_result = "HARD-FAIL"
429
+ evidence_result = "FAIL"
430
+ overall_result = "HARD-FAIL"
431
+ print("================================================================")
432
+ print("SUMMARY")
433
+ print(f"WORK RESULT: {work_result}")
434
+ print(f"EVIDENCE RESULT: {evidence_result}")
435
+ print(f"OVERALL RESULT: {overall_result}")
436
+ if reason:
437
+ print(f"REASON: {reason}")
438
+ print(f"NEXT CHAT REPLY: {reply}")
439
+ print("================================================================")
440
+
441
+ def git_porcelain_paths() -> list[str]:
442
+ proc = subprocess.run(["git", "status", "--porcelain"], text=True, capture_output=True, check=False)
443
+ paths: list[str] = []
444
+ for raw in proc.stdout.splitlines():
445
+ if raw.strip():
446
+ paths.append(raw[3:])
447
+ return paths
448
+
449
+
450
+ def agent_next_postcondition_failures() -> list[str]:
451
+ failures: list[str] = []
452
+ for item in (CURRENT_YAML, CURRENT_SCRIPT):
453
+ if item.exists():
454
+ failures.append("active current file remains: " + item.as_posix())
455
+ complete_pairs: list[str] = []
456
+ if INBOX_DIR.exists():
457
+ for yaml_path in inbox_yaml_files():
458
+ if yaml_path.with_suffix(".sh").exists():
459
+ complete_pairs.append(yaml_path.name)
460
+ if complete_pairs:
461
+ failures.append("complete inbox command remains: " + ", ".join(complete_pairs))
462
+ unstaged_deletions = [path for path in git_porcelain_paths() if path.startswith(".agentic/commands/inbox/")]
463
+ if unstaged_deletions:
464
+ failures.append("inbox path still dirty after run: " + ", ".join(unstaged_deletions))
465
+ return failures
466
+
467
+
468
+ def git_pull_ff_only() -> int:
469
+ branch = current_branch()
470
+ return subprocess.run(["git", "pull", "--ff-only", "origin", branch], check=False).returncode
471
+
472
+
473
+ def agent_next() -> int:
474
+ pulled = git_pull_ff_only()
475
+ if pulled != 0:
476
+ print(OUTCOME_FAIL_PULL)
477
+ print_agent_next_footer("HARD-FAIL", "paste-output", OUTCOME_FAIL_PULL)
478
+ return pulled
479
+ try:
480
+ consumed_paths = list(prepare_current_from_inbox())
481
+ except FileNotFoundError as exc:
482
+ print(OUTCOME_FAIL_NO_COMMAND)
483
+ print(str(exc))
484
+ print_agent_next_footer("NO-COMMAND", "ask-agent-to-queue-command", str(exc))
485
+ return 1
486
+ except RuntimeError as exc:
487
+ print(OUTCOME_FAIL_AMBIGUOUS_COMMANDS)
488
+ print(str(exc))
489
+ print_agent_next_footer("HARD-FAIL", "paste-output", OUTCOME_FAIL_AMBIGUOUS_COMMANDS)
490
+ return 1
491
+ try:
492
+ result = agent_run(extra_upload_paths=consumed_paths)
493
+ finally:
494
+ remove_current_files()
495
+ failures = agent_next_postcondition_failures()
496
+ if failures:
497
+ print(OUTCOME_FAIL_POSTCONDITION)
498
+ for item in failures:
499
+ print(item)
500
+ print_agent_next_footer("HARD-FAIL", "paste-output", OUTCOME_FAIL_POSTCONDITION)
501
+ return 1
502
+ if result == 0:
503
+ print_agent_next_footer("PASS", "p")
504
+ else:
505
+ print_agent_next_footer("FAIL", "f")
506
+ return result
507
+
508
+ def main(argv: list[str] | None = None) -> int:
509
+ args = list(sys.argv[1:] if argv is None else argv)
510
+ if args and args[0] == "next":
511
+ return agent_next()
512
+ return agent_run()
513
+
514
+
515
+ if __name__ == "__main__":
516
+ raise SystemExit(main())
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ GUI_ENTRY_PATTERNS = ("typed", "work_order", "action_registry", "action_specs", "cockpit")
6
+
7
+ CONTRACT_TEST_CANDIDATES = (
8
+ "tests/test_run_summary_renderer.py",
9
+ "tests/test_v036_summary_and_ack_audit_hardening.py",
10
+ "tests/test_v036_summary_renderer_usage_hardening.py",
11
+ "tests/test_v036_doc_rules_contract_hardening.py",
12
+ "tests/test_v036_zero_ns_shell_adapter_contract.py",
13
+ "tests/test_v036_shell_adapter_inventory_baseline.py",
14
+ "tests/test_v036_release_route_help_safety.py",
15
+ "tests/test_terminal_logging.py",
16
+ "tests/test_evidence_clean.py",
17
+ "tests/test_evidence_clean_cli.py",
18
+ )
19
+
20
+ def discover_gui_entry_tests(root: Path) -> list[str]:
21
+ tests_dir = root / "tests"
22
+ if not tests_dir.exists():
23
+ return []
24
+ return sorted(
25
+ str(path)
26
+ for path in tests_dir.glob("test_*.py")
27
+ if any(pattern in path.name for pattern in GUI_ENTRY_PATTERNS)
28
+ )
29
+
30
+ def existing_contract_tests(root: Path) -> list[str]:
31
+ return [name for name in CONTRACT_TEST_CANDIDATES if (root / name).exists()]
32
+
33
+ def pytest_argv_for(paths: list[str]) -> list[str]:
34
+ if not paths:
35
+ raise ValueError("test matrix is empty")
36
+ return ["-m", "pytest", *paths, "-q"]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from agentic_project_kit.audit_test_matrix import discover_gui_entry_tests, existing_contract_tests, pytest_argv_for
10
+
11
+ def main(argv: list[str] | None = None) -> int:
12
+ parser = argparse.ArgumentParser(description="Run deterministic audit test matrices without shell xargs or hard-coded missing tests.")
13
+ parser.add_argument("--matrix", choices=("gui-entry", "contracts", "all"), default="all")
14
+ parser.add_argument("--json", action="store_true")
15
+ parser.add_argument("--list-only", action="store_true")
16
+ args = parser.parse_args(argv)
17
+ root = Path.cwd()
18
+ matrices: dict[str, list[str]] = {}
19
+ if args.matrix in ("gui-entry", "all"):
20
+ matrices["gui-entry"] = discover_gui_entry_tests(root)
21
+ if args.matrix in ("contracts", "all"):
22
+ matrices["contracts"] = existing_contract_tests(root)
23
+ if args.json:
24
+ print(json.dumps(matrices, indent=2))
25
+ else:
26
+ for name, paths in matrices.items():
27
+ print(f"{name}_test_file_count={len(paths)}")
28
+ for path in paths:
29
+ print(path)
30
+ if args.list_only:
31
+ return 0 if all(matrices.values()) else 1
32
+ status = 0
33
+ for paths in matrices.values():
34
+ try:
35
+ pytest_args = [sys.executable, *pytest_argv_for(paths)]
36
+ except ValueError:
37
+ return 1
38
+ completed = subprocess.run(pytest_args, cwd=root, check=False)
39
+ status |= int(completed.returncode)
40
+ return status
41
+
42
+ if __name__ == "__main__":
43
+ raise SystemExit(main())