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,255 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from collections.abc import Callable
5
+ from dataclasses import asdict
6
+ import json
7
+ import re
8
+ import subprocess
9
+ from dataclasses import dataclass, replace
10
+ from typing import Any, Literal
11
+
12
+ Decision = Literal["green", "red", "pending", "no-checks", "unknown", "not-open"]
13
+ FailedLogStatus = Literal["not-fetched", "fetched", "unavailable", "missing-run-id"]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class FailedRunDiagnostic:
18
+ check_name: str
19
+ conclusion: str
20
+ run_id: str
21
+ details_url: str
22
+ command: str
23
+ log_status: FailedLogStatus = "not-fetched"
24
+ log_excerpt: str = ""
25
+ error: str = ""
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class PrStatusDecision:
30
+ pr: str
31
+ state: str
32
+ merge_state_status: str
33
+ head_ref_oid: str
34
+ decision: Decision
35
+ successful_checks: tuple[str, ...]
36
+ pending_checks: tuple[str, ...]
37
+ failed_checks: tuple[str, ...]
38
+ unknown_checks: tuple[str, ...]
39
+ failed_run_log_hint: str
40
+ failed_run_diagnostics: tuple[FailedRunDiagnostic, ...]
41
+
42
+
43
+ def _check_name(item: dict[str, Any]) -> str:
44
+ return str(item.get("name") or item.get("workflowName") or item.get("__typename") or "unknown")
45
+
46
+
47
+ def _run_id_from_details_url(details_url: str) -> str:
48
+ match = re.search(r"/actions/runs/([0-9]+)(?:/|$)", details_url)
49
+ return match.group(1) if match else ""
50
+
51
+
52
+ def _failed_run_diagnostic(item: dict[str, Any], *, name: str, conclusion: str) -> FailedRunDiagnostic:
53
+ details_url = str(item.get("detailsUrl") or item.get("details_url") or "")
54
+ run_id = _run_id_from_details_url(details_url)
55
+ command = f"gh run view {run_id} --log-failed" if run_id else ""
56
+ return FailedRunDiagnostic(
57
+ check_name=name,
58
+ conclusion=conclusion,
59
+ run_id=run_id,
60
+ details_url=details_url,
61
+ command=command,
62
+ )
63
+
64
+
65
+ def classify_pr_status(payload: dict[str, Any], *, pr: str = "") -> PrStatusDecision:
66
+ state = str(payload.get("state") or "UNKNOWN")
67
+ checks = payload.get("statusCheckRollup") or []
68
+ successful: list[str] = []
69
+ pending: list[str] = []
70
+ failed: list[str] = []
71
+ unknown: list[str] = []
72
+ failed_diagnostics: list[FailedRunDiagnostic] = []
73
+
74
+ if state != "OPEN":
75
+ decision: Decision = "not-open"
76
+ elif not isinstance(checks, list) or not checks:
77
+ decision = "no-checks"
78
+ else:
79
+ for item in checks:
80
+ if not isinstance(item, dict):
81
+ unknown.append("unknown")
82
+ continue
83
+ name = _check_name(item)
84
+ status = str(item.get("status") or "").upper()
85
+ conclusion = str(item.get("conclusion") or "").upper()
86
+ if status != "COMPLETED":
87
+ pending.append(name)
88
+ elif conclusion == "SUCCESS":
89
+ successful.append(name)
90
+ elif conclusion in {"FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED"}:
91
+ failed.append(name)
92
+ failed_diagnostics.append(_failed_run_diagnostic(item, name=name, conclusion=conclusion))
93
+ else:
94
+ unknown.append(name)
95
+
96
+ if failed:
97
+ decision = "red"
98
+ elif pending:
99
+ decision = "pending"
100
+ elif unknown:
101
+ decision = "unknown"
102
+ else:
103
+ decision = "green"
104
+
105
+ hint = "none"
106
+ if decision == "red":
107
+ commands = [diagnostic.command for diagnostic in failed_diagnostics if diagnostic.command]
108
+ hint = f"run: {commands[0]}" if commands else "run: gh run view <failed-run-id> --log-failed"
109
+
110
+ return PrStatusDecision(
111
+ pr=pr,
112
+ state=state,
113
+ merge_state_status=str(payload.get("mergeStateStatus") or "UNKNOWN"),
114
+ head_ref_oid=str(payload.get("headRefOid") or ""),
115
+ decision=decision,
116
+ successful_checks=tuple(successful),
117
+ pending_checks=tuple(pending),
118
+ failed_checks=tuple(failed),
119
+ unknown_checks=tuple(unknown),
120
+ failed_run_log_hint=hint,
121
+ failed_run_diagnostics=tuple(failed_diagnostics),
122
+ )
123
+
124
+
125
+ def _excerpt(text: str, *, max_lines: int) -> str:
126
+ lines = text.strip().splitlines()
127
+ if max_lines <= 0 or len(lines) <= max_lines:
128
+ return "\n".join(lines)
129
+ omitted = len(lines) - max_lines
130
+ return "\n".join([*lines[:max_lines], f"... ({omitted} lines omitted)"])
131
+
132
+
133
+ def _fetch_failed_run_log(run_id: str) -> tuple[int, str, str]:
134
+ completed = subprocess.run(["gh", "run", "view", run_id, "--log-failed"], text=True, capture_output=True, check=False)
135
+ return completed.returncode, completed.stdout, completed.stderr
136
+
137
+
138
+ def attach_failed_run_logs(
139
+ decision: PrStatusDecision,
140
+ *,
141
+ max_lines: int = 120,
142
+ fetcher: Callable[[str], tuple[int, str, str]] = _fetch_failed_run_log,
143
+ ) -> PrStatusDecision:
144
+ diagnostics: list[FailedRunDiagnostic] = []
145
+ for diagnostic in decision.failed_run_diagnostics:
146
+ if not diagnostic.run_id:
147
+ diagnostics.append(
148
+ replace(
149
+ diagnostic,
150
+ log_status="missing-run-id",
151
+ error="failed check detailsUrl did not contain a GitHub Actions run id",
152
+ )
153
+ )
154
+ continue
155
+ returncode, stdout, stderr = fetcher(diagnostic.run_id)
156
+ if returncode == 0:
157
+ diagnostics.append(replace(diagnostic, log_status="fetched", log_excerpt=_excerpt(stdout, max_lines=max_lines)))
158
+ continue
159
+ diagnostics.append(
160
+ replace(
161
+ diagnostic,
162
+ log_status="unavailable",
163
+ log_excerpt=_excerpt(stdout, max_lines=max_lines),
164
+ error=(stderr.strip() or stdout.strip() or f"gh run view exited with {returncode}"),
165
+ )
166
+ )
167
+ return replace(decision, failed_run_diagnostics=tuple(diagnostics))
168
+
169
+
170
+ def _render_indented_block(text: str, *, indent: str = " ") -> list[str]:
171
+ if not text:
172
+ return [f"{indent}(none)"]
173
+ return [f"{indent}{line}" for line in text.splitlines()]
174
+
175
+
176
+ def render_decision(decision: PrStatusDecision) -> str:
177
+ lines = [
178
+ "NEXT_TURN_PR_STATUS",
179
+ f"pr={decision.pr}",
180
+ f"state={decision.state}",
181
+ f"merge_state_status={decision.merge_state_status}",
182
+ f"head_ref_oid={decision.head_ref_oid}",
183
+ f"decision={decision.decision}",
184
+ "successful_checks:",
185
+ *[f"- {item}" for item in decision.successful_checks],
186
+ "pending_checks:",
187
+ *[f"- {item}" for item in decision.pending_checks],
188
+ "failed_checks:",
189
+ *[f"- {item}" for item in decision.failed_checks],
190
+ "unknown_checks:",
191
+ *[f"- {item}" for item in decision.unknown_checks],
192
+ f"failed_run_log_hint={decision.failed_run_log_hint}",
193
+ "failed_run_diagnostics:",
194
+ ]
195
+ for diagnostic in decision.failed_run_diagnostics:
196
+ lines.extend(
197
+ [
198
+ (
199
+ f"- check={diagnostic.check_name} conclusion={diagnostic.conclusion} "
200
+ f"run_id={diagnostic.run_id or '(missing)'} log_status={diagnostic.log_status}"
201
+ ),
202
+ f" command={diagnostic.command or '(unavailable)'}",
203
+ f" details_url={diagnostic.details_url or '(missing)'}",
204
+ ]
205
+ )
206
+ if diagnostic.error:
207
+ lines.append(f" error={diagnostic.error}")
208
+ lines.append(" log_excerpt:")
209
+ lines.extend(_render_indented_block(diagnostic.log_excerpt, indent=" "))
210
+ lines.append("### RESULT: PASS ###")
211
+ return "\n".join(lines)
212
+
213
+
214
+ def _run_gh(args: list[str]) -> str:
215
+ completed = subprocess.run(["gh", *args], text=True, capture_output=True, check=False)
216
+ if completed.returncode != 0:
217
+ raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())
218
+ return completed.stdout
219
+
220
+
221
+ def fetch_pr_payload(pr: str) -> dict[str, Any]:
222
+ raw = _run_gh([
223
+ "pr",
224
+ "view",
225
+ pr,
226
+ "--json",
227
+ "baseRefName,baseRefOid,headRefName,headRefOid,state,mergeStateStatus,statusCheckRollup,url",
228
+ ])
229
+ payload = json.loads(raw)
230
+ if not isinstance(payload, dict):
231
+ raise RuntimeError("gh pr view did not return a JSON object")
232
+ return payload
233
+
234
+
235
+ def main() -> int:
236
+ parser = argparse.ArgumentParser(prog="next-turn-pr-status")
237
+ parser.add_argument("pr")
238
+ parser.add_argument("--json", action="store_true")
239
+ parser.add_argument("--no-failed-log-fetch", action="store_true")
240
+ parser.add_argument("--failed-log-lines", type=int, default=120)
241
+ args = parser.parse_args()
242
+
243
+ payload = fetch_pr_payload(args.pr)
244
+ decision = classify_pr_status(payload, pr=args.pr)
245
+ if decision.decision == "red" and not args.no_failed_log_fetch:
246
+ decision = attach_failed_run_logs(decision, max_lines=args.failed_log_lines)
247
+ if args.json:
248
+ print(json.dumps(asdict(decision), indent=2, sort_keys=True))
249
+ else:
250
+ print(render_decision(decision))
251
+ return 0
252
+
253
+
254
+ if __name__ == "__main__":
255
+ raise SystemExit(main())
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from dataclasses import asdict, dataclass
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ WorkResult = Literal["PASS", "FAIL", "PENDING", "HARD_FAIL", "PASS_ALREADY_DONE", "NOOP"]
11
+ EvidenceResult = Literal["PASS", "FAIL", "PARTIAL", "CHAT_ONLY", "NOT_REQUIRED", "LOCAL_LOG"]
12
+ RemoteEvidence = Literal["PASS", "FAIL", "PARTIAL", "NOT_REQUIRED"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class NextTurnResult:
17
+ run_id: str
18
+ work_result: WorkResult
19
+ evidence_result: EvidenceResult
20
+ overall_result: WorkResult
21
+ remote_evidence: RemoteEvidence
22
+ terminal_log: str
23
+ remote_terminal_log: str
24
+ command_report: str
25
+ next_chat_reply: str
26
+ timestamp_utc: str
27
+ reason: str = ""
28
+
29
+
30
+ def classify_next_chat_reply(overall_result: str, remote_evidence: str) -> str:
31
+ if overall_result in {"PASS", "PASS_ALREADY_DONE", "NOOP"}:
32
+ return "d"
33
+ if remote_evidence in {"PASS", "PARTIAL"}:
34
+ return "f"
35
+ return "paste-output"
36
+
37
+
38
+ def build_result(
39
+ *,
40
+ run_id: str,
41
+ work_result: WorkResult,
42
+ evidence_result: EvidenceResult,
43
+ remote_evidence: RemoteEvidence,
44
+ terminal_log: str = "NONE",
45
+ remote_terminal_log: str = "NONE",
46
+ command_report: str = "NONE",
47
+ reason: str = "",
48
+ ) -> NextTurnResult:
49
+ next_chat_reply = classify_next_chat_reply(work_result, remote_evidence)
50
+ return NextTurnResult(
51
+ run_id=run_id,
52
+ work_result=work_result,
53
+ evidence_result=evidence_result,
54
+ overall_result=work_result,
55
+ remote_evidence=remote_evidence,
56
+ terminal_log=terminal_log,
57
+ remote_terminal_log=remote_terminal_log,
58
+ command_report=command_report,
59
+ next_chat_reply=next_chat_reply,
60
+ timestamp_utc=datetime.now(UTC).isoformat(),
61
+ reason=reason,
62
+ )
63
+
64
+
65
+ def result_paths(run_id: str, root: Path | str = ".") -> tuple[Path, Path, Path]:
66
+ root_path = Path(root)
67
+ report = root_path / "docs" / "reports" / "command_runs" / f"{run_id}.json"
68
+ latest = root_path / "docs" / "reports" / "command_runs" / "next-turn-latest.json"
69
+ terminal = root_path / "docs" / "reports" / "terminal" / f"{run_id}.log"
70
+ return report, latest, terminal
71
+
72
+
73
+ def write_result(result: NextTurnResult, root: Path | str = ".") -> tuple[Path, Path]:
74
+ report, latest, _terminal = result_paths(result.run_id, root)
75
+ report.parent.mkdir(parents=True, exist_ok=True)
76
+ payload = asdict(result)
77
+ report.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
78
+ latest.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
79
+ return report, latest
80
+
81
+
82
+
83
+ def publish_local_evidence(
84
+ *,
85
+ run_id: str,
86
+ local_terminal_log: Path | str,
87
+ work_result: WorkResult,
88
+ evidence_result: EvidenceResult = "PASS",
89
+ reason: str = "",
90
+ root: Path | str = ".",
91
+ ) -> NextTurnResult:
92
+ root_path = Path(root)
93
+ source = Path(local_terminal_log)
94
+ _report, _latest, remote_terminal = result_paths(run_id, root_path)
95
+ remote_terminal.parent.mkdir(parents=True, exist_ok=True)
96
+
97
+ if not source.exists():
98
+ result = build_result(
99
+ run_id=run_id,
100
+ work_result=work_result,
101
+ evidence_result="FAIL",
102
+ remote_evidence="FAIL",
103
+ terminal_log=str(source),
104
+ remote_terminal_log=str(remote_terminal.relative_to(root_path)),
105
+ reason=f"local terminal log not found: {source}",
106
+ )
107
+ write_result(result, root_path)
108
+ return result
109
+
110
+ remote_terminal.write_text(source.read_text(encoding="utf-8", errors="replace"), encoding="utf-8")
111
+
112
+ result = build_result(
113
+ run_id=run_id,
114
+ work_result=work_result,
115
+ evidence_result=evidence_result,
116
+ remote_evidence="PARTIAL",
117
+ terminal_log=str(source),
118
+ remote_terminal_log=str(remote_terminal.relative_to(root_path)),
119
+ command_report=str((root_path / "docs" / "reports" / "command_runs" / f"{run_id}.json").relative_to(root_path)),
120
+ reason=reason,
121
+ )
122
+ write_result(result, root_path)
123
+ return result
124
+
125
+ def render_summary(result: NextTurnResult) -> str:
126
+ return "\n".join(
127
+ [
128
+ "================================================================",
129
+ "SUMMARY",
130
+ f"WORK RESULT: {result.work_result}",
131
+ f"EVIDENCE RESULT: {result.evidence_result}",
132
+ f"OVERALL RESULT: {result.overall_result}",
133
+ f"REMOTE_EVIDENCE: {result.remote_evidence}",
134
+ f"terminal_log={result.terminal_log}",
135
+ f"remote_terminal_log={result.remote_terminal_log}",
136
+ f"command_report={result.command_report}",
137
+ f"NEXT_CHAT_REPLY: {result.next_chat_reply}",
138
+ f"CHAT_REPLY: {result.next_chat_reply}",
139
+ f"### RESULT: {result.overall_result} ###",
140
+ "================================================================",
141
+ ]
142
+ )
143
+
144
+
145
+ def main() -> int:
146
+ parser = argparse.ArgumentParser(prog="next-turn-result")
147
+ parser.add_argument("--run-id", required=True)
148
+ parser.add_argument("--work-result", required=True, choices=["PASS", "FAIL", "PENDING", "HARD_FAIL", "PASS_ALREADY_DONE", "NOOP"])
149
+ parser.add_argument("--evidence-result", default="LOCAL_LOG")
150
+ parser.add_argument("--remote-evidence", default="NOT_REQUIRED")
151
+ parser.add_argument("--terminal-log", default="NONE")
152
+ parser.add_argument("--remote-terminal-log", default="NONE")
153
+ parser.add_argument("--command-report", default="NONE")
154
+ parser.add_argument("--reason", default="")
155
+ parser.add_argument("--publish-local-log", default="")
156
+ args = parser.parse_args()
157
+
158
+ if args.publish_local_log:
159
+ result = publish_local_evidence(
160
+ run_id=args.run_id,
161
+ local_terminal_log=args.publish_local_log,
162
+ work_result=args.work_result,
163
+ evidence_result=args.evidence_result,
164
+ reason=args.reason,
165
+ )
166
+ else:
167
+ result = build_result(
168
+ run_id=args.run_id,
169
+ work_result=args.work_result,
170
+ evidence_result=args.evidence_result,
171
+ remote_evidence=args.remote_evidence,
172
+ terminal_log=args.terminal_log,
173
+ remote_terminal_log=args.remote_terminal_log,
174
+ command_report=args.command_report,
175
+ reason=args.reason,
176
+ )
177
+ write_result(result)
178
+ print(render_summary(result))
179
+ return 0
180
+
181
+
182
+ if __name__ == "__main__":
183
+ raise SystemExit(main())
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import subprocess
6
+ import sys
7
+ from dataclasses import asdict, dataclass
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ import os
11
+
12
+ from agentic_project_kit.next_turn_evidence import commit_and_push_evidence, publish_and_stage_evidence, render_finalize_result
13
+ from agentic_project_kit.next_turn_slot import FIXED_SLOT_SCRIPT, FIXED_SLOT_YAML
14
+ from agentic_project_kit.work_order_validator import default_local_result_log_path
15
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
16
+
17
+ LATEST_TERMINAL_LOG = Path(
18
+ os.environ.get(
19
+ "AGENTIC_KIT_NEXT_TURN_LOG",
20
+ (Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.log").as_posix(),
21
+ )
22
+ )
23
+ LATEST_COMMAND_REPORT = Path(
24
+ os.environ.get(
25
+ "AGENTIC_KIT_NEXT_TURN_REPORT",
26
+ (Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.json").as_posix(),
27
+ )
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class NextTurnRunResult:
33
+ command_id: str
34
+ outcome: str
35
+ exit_code: int
36
+ script_path: str
37
+ yaml_path: str
38
+ terminal_log: str
39
+ command_report: str
40
+ timestamp_utc: str
41
+
42
+
43
+ def _command_id_from_yaml(path: Path) -> str:
44
+ if not path.exists():
45
+ return "next-turn"
46
+ for line in path.read_text(encoding="utf-8").splitlines():
47
+ if line.startswith("command_id:"):
48
+ return line.split(":", 1)[1].strip() or "next-turn"
49
+ return "next-turn"
50
+
51
+
52
+ def run_fixed_slot(root: Path | str = ".") -> NextTurnRunResult:
53
+ root_path = Path(root)
54
+ yaml_path = root_path / FIXED_SLOT_YAML
55
+ script_path = root_path / FIXED_SLOT_SCRIPT
56
+ terminal_log = (
57
+ root_path / LATEST_TERMINAL_LOG
58
+ if LATEST_TERMINAL_LOG != Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.log"
59
+ else default_local_result_log_path(root_path)
60
+ )
61
+ command_report = (
62
+ root_path / LATEST_COMMAND_REPORT
63
+ if LATEST_COMMAND_REPORT != Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.json"
64
+ else load_workspace(root_path).tmp_file("next-turn-latest.json")
65
+ )
66
+
67
+ terminal_log.parent.mkdir(parents=True, exist_ok=True)
68
+ command_report.parent.mkdir(parents=True, exist_ok=True)
69
+
70
+ command_id = _command_id_from_yaml(yaml_path)
71
+
72
+ if not yaml_path.exists() or not script_path.exists():
73
+ result = NextTurnRunResult(
74
+ command_id=command_id,
75
+ outcome="FAIL_NO_FIXED_SLOT",
76
+ exit_code=2,
77
+ script_path=str(FIXED_SLOT_SCRIPT),
78
+ yaml_path=str(FIXED_SLOT_YAML),
79
+ terminal_log=str(terminal_log),
80
+ command_report=str(command_report),
81
+ timestamp_utc=datetime.now(timezone.utc).isoformat(),
82
+ )
83
+ terminal_log.write_text("NEXT_TURN_RUNNER\noutcome=FAIL_NO_FIXED_SLOT\n### RESULT: FAIL ###\n", encoding="utf-8")
84
+ command_report.write_text(json.dumps(asdict(result), indent=2, sort_keys=True) + "\n", encoding="utf-8")
85
+ return result
86
+
87
+ completed = subprocess.run(
88
+ [sys.executable, str(script_path)],
89
+ cwd=root_path,
90
+ text=True,
91
+ capture_output=True,
92
+ check=False,
93
+ )
94
+ output = (completed.stdout or "") + (completed.stderr or "")
95
+ terminal_log.write_text(output, encoding="utf-8")
96
+
97
+ outcome = "PASS_EXECUTED" if completed.returncode == 0 else "FAIL_EXECUTED"
98
+ result = NextTurnRunResult(
99
+ command_id=command_id,
100
+ outcome=outcome,
101
+ exit_code=completed.returncode,
102
+ script_path=str(FIXED_SLOT_SCRIPT),
103
+ yaml_path=str(FIXED_SLOT_YAML),
104
+ terminal_log=str(terminal_log),
105
+ command_report=str(command_report),
106
+ timestamp_utc=datetime.now(timezone.utc).isoformat(),
107
+ )
108
+ command_report.write_text(json.dumps(asdict(result), indent=2, sort_keys=True) + "\n", encoding="utf-8")
109
+ if outcome == "PASS_EXECUTED":
110
+ yaml_path.unlink(missing_ok=True)
111
+ script_path.unlink(missing_ok=True)
112
+ return result
113
+
114
+
115
+ def render_result(result: NextTurnRunResult) -> str:
116
+ status = "PASS" if result.outcome == "PASS_EXECUTED" else "FAIL"
117
+ return "\n".join(
118
+ [
119
+ "NEXT_TURN_RUNNER",
120
+ f"command_id={result.command_id}",
121
+ f"outcome={result.outcome}",
122
+ f"exit_code={result.exit_code}",
123
+ f"terminal_log={result.terminal_log}",
124
+ f"command_report={result.command_report}",
125
+ f"subresult={status}",
126
+ ]
127
+ )
128
+
129
+
130
+ def work_result_from_outcome(outcome: str) -> str:
131
+ return "PASS" if outcome == "PASS_EXECUTED" else "FAIL"
132
+
133
+
134
+ def publish_run_evidence(result: NextTurnRunResult, *, run_id: str, push: bool = True) -> str:
135
+ plan = publish_and_stage_evidence(
136
+ run_id=run_id,
137
+ local_terminal_log=result.terminal_log,
138
+ work_result=work_result_from_outcome(result.outcome),
139
+ )
140
+ finalize_result = commit_and_push_evidence(plan=plan, push=push)
141
+ return render_finalize_result(finalize_result)
142
+
143
+
144
+ def main() -> int:
145
+ parser = argparse.ArgumentParser(prog="next-turn-run")
146
+ parser.add_argument("--publish-evidence", action="store_true")
147
+ parser.add_argument("--run-id", default="")
148
+ parser.add_argument("--no-push", action="store_true")
149
+ args = parser.parse_args()
150
+ result = run_fixed_slot()
151
+ print(render_result(result))
152
+ if args.publish_evidence:
153
+ run_id = args.run_id or result.command_id
154
+ print(publish_run_evidence(result, run_id=run_id, push=not args.no_push))
155
+ return 0 if result.outcome == "PASS_EXECUTED" else 1
156
+
157
+
158
+ if __name__ == "__main__":
159
+ raise SystemExit(main())