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,507 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from pathlib import Path
7
+
8
+ from agentic_project_kit.run_summary_renderer import SummaryPayload
9
+ from agentic_project_kit.run_summary_renderer import validate_summary_data
10
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
11
+
12
+ PASS_MARKER = "### RESULT: PASS ###"
13
+ FAIL_MARKER = "### RESULT: FAIL ###"
14
+ PENDING_MARKER = "### RESULT: PENDING ###"
15
+ HARD_FAIL_MARKER = "### RESULT: HARD-FAIL ###"
16
+ LATEST_TERMINAL_POINTER = Path(LEGACY_DEFAULTS.terminal_reports_root) / "LATEST_TERMINAL_LOG.txt"
17
+ STRUCTURED_SUMMARY_HEADER = "SUMMARY COMM-"
18
+ SUMMARY_BOUNDARY = "================================================================"
19
+ EXPECTED_NEGATIVE_SMOKE_START = "### EXPECTED NEGATIVE SMOKE START ###"
20
+ EXPECTED_NEGATIVE_SMOKE_DONE = "### EXPECTED NEGATIVE SMOKE DONE ###"
21
+ EXPECTED_NEGATIVE_SMOKE_MARKERS = (
22
+ "PASS: false-pass log was rejected",
23
+ "PASS: ns evidence-guard rejected false-PASS log",
24
+ )
25
+
26
+
27
+ class EvidenceVerdict(str, Enum):
28
+ PASS_CONTINUE = "PASS_CONTINUE"
29
+ FAIL_DIAGNOSE = "FAIL_DIAGNOSE"
30
+ MISSING_EVIDENCE_UPLOAD_FIRST = "MISSING_EVIDENCE_UPLOAD_FIRST"
31
+ AMBIGUOUS_SUMMARY_REVIEW_REQUIRED = "AMBIGUOUS_SUMMARY_REVIEW_REQUIRED"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ParsedSummary:
36
+ found: bool
37
+ valid: bool
38
+ payload: SummaryPayload = SummaryPayload()
39
+ result_marker: str = ""
40
+ findings: tuple[str, ...] = ()
41
+
42
+ @property
43
+ def work(self) -> str:
44
+ return self.payload.work
45
+
46
+ @property
47
+ def evidence(self) -> str:
48
+ return self.payload.evidence
49
+
50
+ @property
51
+ def overall(self) -> str:
52
+ return self.payload.overall
53
+
54
+ @property
55
+ def remote_evidence(self) -> str:
56
+ return self.payload.remote_evidence
57
+
58
+ @property
59
+ def chat_reply(self) -> str:
60
+ return self.payload.chat_reply or self.payload.next
61
+
62
+ @property
63
+ def overall_pass(self) -> bool:
64
+ return self.valid and self.overall == "PASS" and self.result_marker == "PASS"
65
+
66
+ @property
67
+ def overall_fail(self) -> bool:
68
+ return self.valid and self.overall in {"FAIL", "HARD-FAIL"}
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class EvidenceInspection:
73
+ path: Path | None
74
+ exists: bool
75
+ verdict: EvidenceVerdict
76
+ final_marker: str | None = None
77
+ pass_markers: int = 0
78
+ fail_markers: int = 0
79
+ hidden_fail_before_final_pass: bool = False
80
+ structured_summary: ParsedSummary = ParsedSummary(found=False, valid=False)
81
+ require_summary: bool = False
82
+ git_branch: str = "UNKNOWN"
83
+ git_status: tuple[str, ...] = ()
84
+
85
+ @property
86
+ def success(self) -> bool:
87
+ return self.verdict == EvidenceVerdict.PASS_CONTINUE
88
+
89
+
90
+ def _run_git(args: list[str], root: Path) -> tuple[str, ...]:
91
+ completed = subprocess.run(["git", *args], cwd=root, text=True, capture_output=True, check=False)
92
+ if completed.returncode != 0:
93
+ return ("UNKNOWN",)
94
+ text = completed.stdout.strip()
95
+ return tuple(text.splitlines()) if text else ()
96
+
97
+
98
+ def resolve_latest_evidence(root: Path | str = ".") -> Path | None:
99
+ root_path = Path(root)
100
+ pointer = load_workspace(root_path).latest_terminal_log_pointer()
101
+ if not pointer.exists():
102
+ return None
103
+ target = pointer.read_text(encoding="utf-8").strip()
104
+ if not target:
105
+ return None
106
+ candidate = Path(target)
107
+ return candidate if candidate.is_absolute() else root_path / candidate
108
+
109
+
110
+ def _last_structured_summary_block(text: str) -> str | None:
111
+ header_index = text.rfind(STRUCTURED_SUMMARY_HEADER)
112
+ if header_index < 0:
113
+ return None
114
+ start = text.rfind(SUMMARY_BOUNDARY, 0, header_index)
115
+ if start < 0:
116
+ start = header_index
117
+ end = text.find(SUMMARY_BOUNDARY, header_index)
118
+ if end < 0:
119
+ return text[start:]
120
+ return text[start : end + len(SUMMARY_BOUNDARY)]
121
+
122
+
123
+ def _summary_field(block: str, label: str) -> str:
124
+ prefix = f" {label}:"
125
+ for line in block.splitlines():
126
+ if line.startswith(prefix):
127
+ return line.split(":", 1)[1].strip()
128
+ return ""
129
+
130
+
131
+ def _summary_paragraph(block: str, section: str) -> str:
132
+ lines = block.splitlines()
133
+ for index, line in enumerate(lines):
134
+ if line == section and index + 1 < len(lines) and lines[index + 1].startswith(" "):
135
+ return lines[index + 1].strip()
136
+ return ""
137
+
138
+
139
+ def parse_structured_summary(text: str) -> ParsedSummary:
140
+ block = _last_structured_summary_block(text)
141
+ if block is None:
142
+ return ParsedSummary(found=False, valid=False)
143
+ lines = block.splitlines()
144
+ comm_header = next((line for line in lines if line.startswith(STRUCTURED_SUMMARY_HEADER)), "")
145
+ marker_values = [line.replace("### RESULT:", "").replace("###", "").strip() for line in lines if line.startswith("### RESULT:")]
146
+ result_marker = marker_values[0] if len(marker_values) == 1 else ""
147
+ payload = SummaryPayload(
148
+ comm_header=comm_header,
149
+ slice=_summary_field(block, "NAME"),
150
+ scope=_summary_field(block, "SCOPE"),
151
+ branch=_summary_field(block, "BRANCH"),
152
+ origin=_summary_field(block, "ORIGIN"),
153
+ state_mode=_summary_field(block, "STATE_MODE"),
154
+ mode_check=_summary_field(block, "MODE_CHECK"),
155
+ work=_summary_field(block, "WORK"),
156
+ evidence=_summary_field(block, "EVIDENCE"),
157
+ overall=_summary_field(block, "OVERALL"),
158
+ remote_evidence=_summary_field(block, "REMOTE_EVIDENCE"),
159
+ pr=_summary_field(block, "PR"),
160
+ head_sha=_summary_field(block, "HEAD_SHA"),
161
+ ci=_summary_field(block, "CI"),
162
+ merge=_summary_field(block, "MERGE"),
163
+ terminal_log=_summary_field(block, "terminal_log"),
164
+ terminal_log_remote=_summary_field(block, "terminal_log_remote"),
165
+ terminal_log_local=_summary_field(block, "terminal_log_local"),
166
+ command_report=_summary_field(block, "command_report"),
167
+ interpretation=_summary_paragraph(block, "INTERPRETATION"),
168
+ safe_step=_summary_field(block, "SAFE_STEP"),
169
+ chat_reply=_summary_field(block, "CHAT_REPLY"),
170
+ )
171
+ findings = list(validate_summary_data(payload))
172
+ if len(marker_values) != 1:
173
+ findings.append(f"invalid result marker count: {len(marker_values)}")
174
+ if payload.overall and result_marker and payload.overall != result_marker:
175
+ findings.append("contradictory summary marker")
176
+ for label, value in (
177
+ ("BRANCH", payload.branch),
178
+ ("ORIGIN", payload.origin),
179
+ ("STATE_MODE", payload.state_mode),
180
+ ("MODE_CHECK", payload.mode_check),
181
+ ("terminal_log_remote", payload.terminal_log_remote),
182
+ ("terminal_log_local", payload.terminal_log_local),
183
+ ("SAFE_STEP", payload.safe_step),
184
+ ):
185
+ if not value:
186
+ findings.append(f"missing summary field: {label}")
187
+ if not payload.interpretation:
188
+ findings.append("missing summary section: INTERPRETATION")
189
+ return ParsedSummary(found=True, valid=not findings, payload=payload, result_marker=result_marker, findings=tuple(findings))
190
+
191
+
192
+ def _marker_verdict(text: str) -> tuple[EvidenceVerdict, str | None, int, int, bool]:
193
+ pass_count = text.count(PASS_MARKER)
194
+ fail_count = text.count(FAIL_MARKER)
195
+ marker_positions = [(text.rfind(PASS_MARKER), PASS_MARKER), (text.rfind(FAIL_MARKER), FAIL_MARKER)]
196
+ marker_positions = [(index, marker) for index, marker in marker_positions if index >= 0]
197
+ if not marker_positions:
198
+ return EvidenceVerdict.AMBIGUOUS_SUMMARY_REVIEW_REQUIRED, None, pass_count, fail_count, False
199
+ final_index, final_marker = max(marker_positions, key=lambda item: item[0])
200
+ hidden_fail = fail_count > 0 and final_marker == PASS_MARKER and text.find(FAIL_MARKER) < final_index
201
+ if final_marker == FAIL_MARKER:
202
+ verdict = EvidenceVerdict.FAIL_DIAGNOSE
203
+ elif hidden_fail:
204
+ verdict = EvidenceVerdict.AMBIGUOUS_SUMMARY_REVIEW_REQUIRED
205
+ else:
206
+ verdict = EvidenceVerdict.PASS_CONTINUE
207
+ return verdict, final_marker, pass_count, fail_count, hidden_fail
208
+
209
+
210
+ def inspect_evidence(path: Path | str | None = None, *, root: Path | str = ".", require_summary: bool = False) -> EvidenceInspection:
211
+ root_path = Path(root)
212
+ evidence_path = Path(path) if path is not None else resolve_latest_evidence(root_path)
213
+ branch_lines = _run_git(["branch", "--show-current"], root_path)
214
+ status_lines = _run_git(["status", "--short"], root_path)
215
+ branch = branch_lines[0] if branch_lines else "UNKNOWN"
216
+ if evidence_path is None:
217
+ return EvidenceInspection(path=None, exists=False, verdict=EvidenceVerdict.MISSING_EVIDENCE_UPLOAD_FIRST, require_summary=require_summary, git_branch=branch, git_status=status_lines)
218
+ if not evidence_path.exists():
219
+ return EvidenceInspection(path=evidence_path, exists=False, verdict=EvidenceVerdict.MISSING_EVIDENCE_UPLOAD_FIRST, require_summary=require_summary, git_branch=branch, git_status=status_lines)
220
+ text = evidence_path.read_text(encoding="utf-8")
221
+ summary = parse_structured_summary(text)
222
+ marker_verdict, final_marker, pass_count, fail_count, hidden_fail = _marker_verdict(text)
223
+ if summary.found:
224
+ if not summary.valid:
225
+ verdict = EvidenceVerdict.AMBIGUOUS_SUMMARY_REVIEW_REQUIRED
226
+ elif summary.overall_pass:
227
+ verdict = EvidenceVerdict.PASS_CONTINUE
228
+ elif summary.overall_fail:
229
+ verdict = EvidenceVerdict.FAIL_DIAGNOSE
230
+ else:
231
+ verdict = EvidenceVerdict.AMBIGUOUS_SUMMARY_REVIEW_REQUIRED
232
+ elif require_summary:
233
+ verdict = EvidenceVerdict.AMBIGUOUS_SUMMARY_REVIEW_REQUIRED
234
+ else:
235
+ verdict = marker_verdict
236
+ return EvidenceInspection(path=evidence_path, exists=True, verdict=verdict, final_marker=final_marker, pass_markers=pass_count, fail_markers=fail_count, hidden_fail_before_final_pass=hidden_fail, structured_summary=summary, require_summary=require_summary, git_branch=branch, git_status=status_lines)
237
+
238
+
239
+ def render_evidence_inspection(result: EvidenceInspection) -> str:
240
+ summary = result.structured_summary
241
+ payload = summary.payload
242
+ lines = [
243
+ "EVIDENCE_INSPECTION",
244
+ f"verdict: {result.verdict.value}",
245
+ f"evidence_exists: {'yes' if result.exists else 'no'}",
246
+ f"require_summary: {'yes' if result.require_summary else 'no'}",
247
+ f"git_branch: {result.git_branch}",
248
+ f"git_status_clean: {'yes' if not result.git_status else 'no'}",
249
+ f"pass_markers: {result.pass_markers}",
250
+ f"fail_markers: {result.fail_markers}",
251
+ f"hidden_fail_before_final_pass: {'yes' if result.hidden_fail_before_final_pass else 'no'}",
252
+ f"structured_summary_found: {'yes' if summary.found else 'no'}",
253
+ f"structured_summary_valid: {'yes' if summary.valid else 'no'}",
254
+ ]
255
+ if result.require_summary and not summary.found:
256
+ lines.append("summary_findings:")
257
+ lines.append("- missing structured summary block")
258
+ if summary.found:
259
+ lines.extend([
260
+ f"summary_name: {payload.slice or payload.slice_name}",
261
+ f"summary_scope: {payload.scope}",
262
+ f"summary_branch: {payload.branch}",
263
+ f"summary_origin: {payload.origin}",
264
+ f"summary_state_mode: {payload.state_mode}",
265
+ f"summary_mode_check: {payload.mode_check}",
266
+ f"summary_work: {payload.work}",
267
+ f"summary_evidence: {payload.evidence}",
268
+ f"summary_overall: {payload.overall}",
269
+ f"summary_remote_evidence: {payload.remote_evidence}",
270
+ f"summary_pr: {payload.pr}",
271
+ f"summary_head_sha: {payload.head_sha}",
272
+ f"summary_ci: {payload.ci}",
273
+ f"summary_merge: {payload.merge}",
274
+ f"summary_terminal_log: {payload.terminal_log}",
275
+ f"summary_terminal_log_remote: {payload.terminal_log_remote}",
276
+ f"summary_terminal_log_local: {payload.terminal_log_local}",
277
+ f"summary_command_report: {payload.command_report}",
278
+ f"summary_safe_step: {payload.safe_step}",
279
+ f"summary_chat_reply: {summary.chat_reply}",
280
+ f"summary_result_marker: {summary.result_marker}",
281
+ ])
282
+ if summary.findings:
283
+ lines.append("summary_findings:")
284
+ lines.extend(f"- {finding}" for finding in summary.findings)
285
+ if result.path is not None:
286
+ lines.append(f"evidence_path: {result.path}")
287
+ if result.final_marker is not None:
288
+ lines.append(f"final_marker: {result.final_marker}")
289
+ if result.git_status:
290
+ lines.append("git_status:")
291
+ lines.extend(result.git_status)
292
+ if result.verdict == EvidenceVerdict.PASS_CONTINUE:
293
+ lines.append("next_action: continue with the next safe slice after reviewing evidence")
294
+ elif result.verdict == EvidenceVerdict.FAIL_DIAGNOSE:
295
+ lines.append("next_action: diagnose the failing evidence before continuing")
296
+ elif result.verdict == EvidenceVerdict.MISSING_EVIDENCE_UPLOAD_FIRST:
297
+ lines.append("next_action: locate remote or repo evidence, or run an evidence upload slice before requesting pasted output")
298
+ else:
299
+ lines.append("next_action: review ambiguous evidence manually before continuing")
300
+ lines.append(f"### RESULT: {'PASS' if result.success else 'FAIL'} ###")
301
+ return "\n".join(lines)
302
+
303
+
304
+ class LogClassificationState(str, Enum):
305
+ READY_TO_CONTINUE = "READY_TO_CONTINUE"
306
+ BLOCKED_BY_FAIL = "BLOCKED_BY_FAIL"
307
+ BLOCKED_BY_PENDING = "BLOCKED_BY_PENDING"
308
+ BLOCKED_BY_MISSING_SUMMARY = "BLOCKED_BY_MISSING_SUMMARY"
309
+ BLOCKED_BY_BAD_SUMMARY = "BLOCKED_BY_BAD_SUMMARY"
310
+ BLOCKED_BY_CONTRADICTORY_SUMMARY = "BLOCKED_BY_CONTRADICTORY_SUMMARY"
311
+ BLOCKED_BY_UNSCOPED_NEGATIVE_SMOKE = "BLOCKED_BY_UNSCOPED_NEGATIVE_SMOKE"
312
+ BLOCKED_BY_HIDDEN_FAIL = "BLOCKED_BY_HIDDEN_FAIL"
313
+ BLOCKED_BY_TEST_FAILURE = "BLOCKED_BY_TEST_FAILURE"
314
+ BLOCKED_BY_RUFF_FAILURE = "BLOCKED_BY_RUFF_FAILURE"
315
+ BLOCKED_BY_TRACEBACK = "BLOCKED_BY_TRACEBACK"
316
+ BLOCKED_BY_SHELL_SYNTAX = "BLOCKED_BY_SHELL_SYNTAX"
317
+ BLOCKED_BY_DIRTY_WORKTREE = "BLOCKED_BY_DIRTY_WORKTREE"
318
+ AMBIGUOUS = "AMBIGUOUS"
319
+
320
+
321
+ @dataclass(frozen=True)
322
+ class LogClassification:
323
+ path: Path | None
324
+ exists: bool
325
+ state: LogClassificationState
326
+ decision: str
327
+ summary_found: bool
328
+ summary_valid: bool
329
+ hidden_fail: bool
330
+ final_marker: str
331
+ reasons: tuple[str, ...] = ()
332
+
333
+ @property
334
+ def success(self) -> bool:
335
+ return self.state == LogClassificationState.READY_TO_CONTINUE
336
+
337
+
338
+ def _is_expected_negative_smoke_log(text: str) -> bool:
339
+ if EXPECTED_NEGATIVE_SMOKE_START not in text or EXPECTED_NEGATIVE_SMOKE_DONE not in text:
340
+ return False
341
+ if text.count(EXPECTED_NEGATIVE_SMOKE_START) != text.count(EXPECTED_NEGATIVE_SMOKE_DONE):
342
+ return False
343
+ return any(marker in text for marker in EXPECTED_NEGATIVE_SMOKE_MARKERS)
344
+
345
+
346
+ def _has_unscoped_expected_negative_smoke_marker(text: str) -> bool:
347
+ has_marker = any(marker in text for marker in EXPECTED_NEGATIVE_SMOKE_MARKERS)
348
+ if not has_marker:
349
+ return False
350
+ return EXPECTED_NEGATIVE_SMOKE_START not in text or EXPECTED_NEGATIVE_SMOKE_DONE not in text
351
+
352
+
353
+ def _last_any_result_marker(text: str) -> str:
354
+ marker_values = (
355
+ (text.rfind(PASS_MARKER), "PASS"),
356
+ (text.rfind(FAIL_MARKER), "FAIL"),
357
+ (text.rfind(PENDING_MARKER), "PENDING"),
358
+ (text.rfind(HARD_FAIL_MARKER), "HARD-FAIL"),
359
+ )
360
+ marker_values = tuple(item for item in marker_values if item[0] >= 0)
361
+ if not marker_values:
362
+ return "UNKNOWN"
363
+ return max(marker_values, key=lambda item: item[0])[1]
364
+
365
+
366
+ def _contains_any(text: str, markers: tuple[str, ...]) -> bool:
367
+ return any(marker in text for marker in markers)
368
+
369
+
370
+ def classify_log(
371
+ path: Path | str | None = None,
372
+ *,
373
+ root: Path | str = ".",
374
+ require_summary: bool = False,
375
+ check_git_status: bool = True,
376
+ ) -> LogClassification:
377
+ root_path = Path(root)
378
+ evidence_path = Path(path) if path is not None else resolve_latest_evidence(root_path)
379
+ if evidence_path is None:
380
+ return LogClassification(
381
+ path=None,
382
+ exists=False,
383
+ state=LogClassificationState.AMBIGUOUS,
384
+ decision="review",
385
+ summary_found=False,
386
+ summary_valid=False,
387
+ hidden_fail=False,
388
+ final_marker="UNKNOWN",
389
+ reasons=("missing evidence log",),
390
+ )
391
+ if not evidence_path.exists():
392
+ return LogClassification(
393
+ path=evidence_path,
394
+ exists=False,
395
+ state=LogClassificationState.AMBIGUOUS,
396
+ decision="review",
397
+ summary_found=False,
398
+ summary_valid=False,
399
+ hidden_fail=False,
400
+ final_marker="UNKNOWN",
401
+ reasons=("missing evidence log",),
402
+ )
403
+
404
+ text = evidence_path.read_text(encoding="utf-8")
405
+ inspection = inspect_evidence(evidence_path, root=root_path, require_summary=require_summary)
406
+ summary = inspection.structured_summary
407
+ final_marker = _last_any_result_marker(text)
408
+ reasons: list[str] = []
409
+
410
+ test_failure = _contains_any(
411
+ text,
412
+ (
413
+ "FAILED tests/",
414
+ "short test summary info",
415
+ "AssertionError",
416
+ "ERROR tests/",
417
+ "FAILED ",
418
+ "= FAILURES =",
419
+ ),
420
+ )
421
+ ruff_failure = _contains_any(text, ("ruff failed", "Ruff failed", "ruff check", "would reformat"))
422
+ traceback = _contains_any(text, ("Traceback (most recent call last):", "ModuleNotFoundError:", "ERROR collecting"))
423
+ shell_syntax = _contains_any(text, ("syntax error", "parse error", "unexpected EOF", "zsh: parse error", "bash: syntax error"))
424
+
425
+ if _has_unscoped_expected_negative_smoke_marker(text):
426
+ reasons.append("expected negative smoke marker is not scoped with START/DONE markers")
427
+ state = LogClassificationState.BLOCKED_BY_UNSCOPED_NEGATIVE_SMOKE
428
+ elif require_summary and not summary.found:
429
+ reasons.append("missing structured summary block")
430
+ state = LogClassificationState.BLOCKED_BY_MISSING_SUMMARY
431
+ elif summary.found and "contradictory summary marker" in summary.findings:
432
+ reasons.extend(summary.findings)
433
+ state = LogClassificationState.BLOCKED_BY_CONTRADICTORY_SUMMARY
434
+ elif summary.found and not summary.valid:
435
+ reasons.extend(summary.findings or ("invalid structured summary block",))
436
+ state = LogClassificationState.BLOCKED_BY_BAD_SUMMARY
437
+ elif inspection.hidden_fail_before_final_pass and not _is_expected_negative_smoke_log(text):
438
+ reasons.append("hidden fail before final PASS")
439
+ state = LogClassificationState.BLOCKED_BY_HIDDEN_FAIL
440
+ elif final_marker in {"FAIL", "HARD-FAIL"}:
441
+ reasons.append(f"final result marker is {final_marker}")
442
+ state = LogClassificationState.BLOCKED_BY_FAIL
443
+ elif final_marker == "PENDING":
444
+ reasons.append("final result marker is PENDING")
445
+ state = LogClassificationState.BLOCKED_BY_PENDING
446
+ elif test_failure:
447
+ reasons.append("test failure marker found")
448
+ state = LogClassificationState.BLOCKED_BY_TEST_FAILURE
449
+ elif ruff_failure:
450
+ reasons.append("ruff failure marker found")
451
+ state = LogClassificationState.BLOCKED_BY_RUFF_FAILURE
452
+ elif traceback:
453
+ reasons.append("python traceback or collection error found")
454
+ state = LogClassificationState.BLOCKED_BY_TRACEBACK
455
+ elif shell_syntax:
456
+ reasons.append("shell syntax failure marker found")
457
+ state = LogClassificationState.BLOCKED_BY_SHELL_SYNTAX
458
+ elif check_git_status and summary.found and summary.valid and summary.overall_pass and inspection.git_status:
459
+ reasons.append("current git status is dirty while summary claims PASS")
460
+ state = LogClassificationState.BLOCKED_BY_DIRTY_WORKTREE
461
+ elif final_marker == "PASS":
462
+ reasons.append("clean final PASS")
463
+ state = LogClassificationState.READY_TO_CONTINUE
464
+ else:
465
+ reasons.append("missing or unknown final result marker")
466
+ state = LogClassificationState.AMBIGUOUS
467
+
468
+ if state == LogClassificationState.READY_TO_CONTINUE:
469
+ decision = "continue"
470
+ elif state == LogClassificationState.BLOCKED_BY_PENDING:
471
+ decision = "pending"
472
+ elif state == LogClassificationState.AMBIGUOUS:
473
+ decision = "review"
474
+ else:
475
+ decision = "fail"
476
+
477
+ return LogClassification(
478
+ path=evidence_path,
479
+ exists=True,
480
+ state=state,
481
+ decision=decision,
482
+ summary_found=summary.found,
483
+ summary_valid=summary.valid,
484
+ hidden_fail=inspection.hidden_fail_before_final_pass,
485
+ final_marker=final_marker,
486
+ reasons=tuple(reasons),
487
+ )
488
+
489
+
490
+ def render_log_classification(result: LogClassification) -> str:
491
+ lines = [
492
+ "LOG_CLASSIFICATION",
493
+ f"state={result.state.value}",
494
+ f"decision={result.decision}",
495
+ f"evidence_exists={str(result.exists).lower()}",
496
+ f"summary_found={str(result.summary_found).lower()}",
497
+ f"summary_valid={str(result.summary_valid).lower()}",
498
+ f"hidden_fail={str(result.hidden_fail).lower()}",
499
+ f"final_marker={result.final_marker}",
500
+ ]
501
+ if result.path is not None:
502
+ lines.append(f"path={result.path}")
503
+ if result.reasons:
504
+ lines.append("reasons:")
505
+ lines.extend(f"- {reason}" for reason in result.reasons)
506
+ lines.append(f"### RESULT: {'PASS' if result.success else 'FAIL'} ###")
507
+ return "\n".join(lines)
@@ -0,0 +1,99 @@
1
+ """Machine-readable evidence state contract for GUI readiness.
2
+
3
+ The contract is read-only. It classifies whether a terminal or command-run
4
+ result has durable repo-backed evidence, only local tmp evidence, missing
5
+ evidence, a stale latest pointer, and/or a command report.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, Workspace, load_workspace
14
+
15
+ EVIDENCE_REMOTE_PRESENT = "REMOTE_EVIDENCE_PRESENT"
16
+ EVIDENCE_LOCAL_TMP_ONLY = "LOCAL_TMP_ONLY"
17
+ EVIDENCE_MISSING = "MISSING_EVIDENCE"
18
+ EVIDENCE_STALE_LATEST_POINTER = "STALE_LATEST_POINTER"
19
+
20
+ TERMINAL_DIR = Path(LEGACY_DEFAULTS.terminal_reports_root)
21
+ COMMAND_RUN_DIR = Path(LEGACY_DEFAULTS.command_runs_root)
22
+ LATEST_TERMINAL_LOG = TERMINAL_DIR / "LATEST_TERMINAL_LOG.txt"
23
+ LATEST_COMMAND_RUN = COMMAND_RUN_DIR / "LATEST_COMMAND_RUN.txt"
24
+
25
+ @dataclass(frozen=True)
26
+ class EvidenceState:
27
+ schema_version: int
28
+ evidence_state: str
29
+ remote_evidence_present: bool
30
+ local_tmp_only: bool
31
+ missing_evidence: bool
32
+ stale_latest_pointer: bool
33
+ terminal_log: str | None
34
+ latest_terminal_log: str | None
35
+ command_report: str | None
36
+ command_report_available: bool
37
+ reason: str
38
+ next_allowed_actions: tuple[str, ...]
39
+
40
+ def _root(root: Path) -> Path:
41
+ return root.resolve()
42
+
43
+ def _read_pointer(path: Path) -> str | None:
44
+ if not path.exists():
45
+ return None
46
+ value = path.read_text(encoding="utf-8").strip()
47
+ return value or None
48
+
49
+ def _is_repo_evidence_path(value: str | None, workspace: Workspace) -> bool:
50
+ if value is None:
51
+ return False
52
+ return value.startswith(workspace.path_text(workspace.terminal_reports_dir()) + "/") or value.startswith(
53
+ workspace.path_text(workspace.command_runs_dir()) + "/"
54
+ )
55
+
56
+ def _exists(root: Path, value: str | None) -> bool:
57
+ if value is None:
58
+ return False
59
+ path = Path(value)
60
+ if path.is_absolute():
61
+ return path.exists()
62
+ return (_root(root) / path).exists()
63
+
64
+ def inspect_evidence_state(root: Path = Path("."), terminal_log: str | None = None, command_report: str | None = None) -> EvidenceState:
65
+ root = _root(root)
66
+ workspace = load_workspace(root)
67
+ latest = _read_pointer(workspace.latest_terminal_log_pointer())
68
+ latest_command_report = _read_pointer(workspace.latest_command_run_pointer())
69
+ effective_report = command_report or latest_command_report
70
+ effective_terminal = terminal_log or latest
71
+ command_report_available = _exists(root, effective_report)
72
+ if effective_terminal is None:
73
+ return EvidenceState(1, EVIDENCE_MISSING, False, False, True, False, None, latest, effective_report, command_report_available, "no terminal evidence path is available", ("paste-output",))
74
+ terminal_exists = _exists(root, effective_terminal)
75
+ repo_path = _is_repo_evidence_path(effective_terminal, workspace)
76
+ stale = latest is not None and terminal_log is not None and latest != terminal_log
77
+ if stale:
78
+ return EvidenceState(1, EVIDENCE_STALE_LATEST_POINTER, repo_path and terminal_exists, False, not terminal_exists, True, effective_terminal, latest, effective_report, command_report_available, "latest terminal pointer does not match requested terminal log", ("terminal-upload", "paste-output"))
79
+ if repo_path and terminal_exists:
80
+ return EvidenceState(1, EVIDENCE_REMOTE_PRESENT, True, False, False, False, effective_terminal, latest, effective_report, command_report_available, "repo-backed evidence is present", ("d", "f"))
81
+ if Path(effective_terminal).is_absolute() and terminal_exists:
82
+ return EvidenceState(1, EVIDENCE_LOCAL_TMP_ONLY, False, True, False, False, effective_terminal, latest, effective_report, command_report_available, "only local tmp evidence is available", ("terminal-upload", "paste-output"))
83
+ return EvidenceState(1, EVIDENCE_MISSING, False, False, True, False, effective_terminal, latest, effective_report, command_report_available, "evidence path is missing or not repo-backed", ("paste-output",))
84
+
85
+ def evidence_state_as_json_data(state: EvidenceState) -> dict[str, object]:
86
+ return {
87
+ "schema_version": state.schema_version,
88
+ "evidence_state": state.evidence_state,
89
+ "remote_evidence_present": state.remote_evidence_present,
90
+ "local_tmp_only": state.local_tmp_only,
91
+ "missing_evidence": state.missing_evidence,
92
+ "stale_latest_pointer": state.stale_latest_pointer,
93
+ "terminal_log": state.terminal_log,
94
+ "latest_terminal_log": state.latest_terminal_log,
95
+ "command_report": state.command_report,
96
+ "command_report_available": state.command_report_available,
97
+ "reason": state.reason,
98
+ "next_allowed_actions": list(state.next_allowed_actions),
99
+ }