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,153 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import re
6
+ import subprocess
7
+ from typing import Iterable, Literal
8
+
9
+ StepClassification = Literal["PASS", "PENDING", "FAIL"]
10
+
11
+ FAIL_RE = re.compile(r"### RESULT: (FAIL|NEEDS_HUMAN_REVIEW) ###|STATUS: (FAIL|NEEDS_HUMAN_REVIEW)|RESULT: (FAIL|NEEDS_HUMAN_REVIEW)")
12
+ PENDING_RE = re.compile(r"### RESULT: (PENDING|WAIT) ###|STATUS: (PENDING|WAIT)|RESULT: (PENDING|WAIT)")
13
+ PASS_RE = re.compile(
14
+ r"### RESULT: (PASS|DONE|NOOP|ALREADY_ON_MAIN|ALREADY_MERGED|ALREADY_RELEASED|DOI_VERIFIED|SUPERSEDED) ###"
15
+ r"|STATUS: (PASS|DONE|NOOP|ALREADY_ON_MAIN|ALREADY_MERGED|ALREADY_RELEASED|DOI_VERIFIED|SUPERSEDED)"
16
+ r"|RESULT: (PASS|DONE|NOOP|ALREADY_ON_MAIN|ALREADY_MERGED|ALREADY_RELEASED|DOI_VERIFIED|SUPERSEDED)"
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CommandStepResult:
22
+ number: int
23
+ command: str
24
+ exit_status: int
25
+ classification: StepClassification
26
+ output: str
27
+
28
+
29
+ def classify_step_output(output: str, exit_status: int) -> StepClassification:
30
+ if FAIL_RE.search(output):
31
+ return "FAIL"
32
+ if PENDING_RE.search(output):
33
+ return "PENDING"
34
+ if PASS_RE.search(output):
35
+ return "PASS"
36
+ return "PASS" if exit_status == 0 else "FAIL"
37
+
38
+
39
+ def load_plan(path: Path) -> list[str]:
40
+ if not path.exists():
41
+ raise FileNotFoundError(f"plan file not found: {path}")
42
+ commands: list[str] = []
43
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
44
+ line = raw_line.strip()
45
+ if not line or line.startswith("#"):
46
+ continue
47
+ commands.append(raw_line)
48
+ return commands
49
+
50
+
51
+ def run_command(repo_root: Path, command: str) -> tuple[int, str]:
52
+ completed = subprocess.run(
53
+ ["sh", "-c", command],
54
+ cwd=repo_root,
55
+ text=True,
56
+ stdout=subprocess.PIPE,
57
+ stderr=subprocess.STDOUT,
58
+ check=False,
59
+ )
60
+ return completed.returncode, completed.stdout
61
+
62
+
63
+ def run_steps(repo_root: Path, commands: Iterable[str]) -> tuple[int, list[CommandStepResult]]:
64
+ status = 0
65
+ results: list[CommandStepResult] = []
66
+ for index, command in enumerate(commands, start=1):
67
+ exit_status, output = run_command(repo_root, command)
68
+ classification = classify_step_output(output, exit_status)
69
+ results.append(CommandStepResult(index, command, exit_status, classification, output))
70
+ if classification == "PASS":
71
+ continue
72
+ status = 2 if classification == "PENDING" else 1
73
+ break
74
+ return status, results
75
+
76
+
77
+ def _git(repo_root: Path, args: list[str]) -> tuple[int, str]:
78
+ completed = subprocess.run(
79
+ ["git", *args],
80
+ cwd=repo_root,
81
+ text=True,
82
+ stdout=subprocess.PIPE,
83
+ stderr=subprocess.STDOUT,
84
+ check=False,
85
+ )
86
+ return completed.returncode, completed.stdout.strip()
87
+
88
+
89
+ def render_run(plan_file: Path, results: list[CommandStepResult], final_status: int, branch: str, git_status: str) -> str:
90
+ lines = [
91
+ "",
92
+ "",
93
+ "",
94
+ "--------------------------------------------------------------------------------",
95
+ "--------------------------------------------------------------------------------",
96
+ "--------------------------------------------------------------------------------",
97
+ "",
98
+ "",
99
+ "",
100
+ "NS SLICE RUNNER",
101
+ "",
102
+ "### SAFETY ###",
103
+ "Safety: deterministic sequential runner; advances only after target-state PASS and stops immediately on retryable or failing states.",
104
+ f"plan={plan_file.as_posix()}",
105
+ ]
106
+ for result in results:
107
+ lines.extend(["", f"### STEP {result.number} ###", result.command])
108
+ if result.output:
109
+ lines.append(result.output.rstrip())
110
+ lines.append(f"STEP {result.number} RESULT: {result.classification}")
111
+ if result.classification == "PENDING":
112
+ lines.append("Stopping slice runner at retryable state; no dependent follow-up actions were run.")
113
+ elif result.classification == "FAIL":
114
+ lines.append("Stopping slice runner at first failing step.")
115
+ lines.extend(["", "### FINAL STATE ###", branch])
116
+ if git_status:
117
+ lines.append(git_status)
118
+ if final_status == 0:
119
+ lines.append("\n### RESULT: PASS ###")
120
+ elif final_status == 2:
121
+ lines.append("\n### RESULT: PENDING ###")
122
+ else:
123
+ lines.append("\n### RESULT: FAIL ###")
124
+ return "\n".join(lines)
125
+
126
+
127
+ def run_slice(plan_file: Path, repo_root: Path) -> int:
128
+ try:
129
+ commands = load_plan(plan_file)
130
+ except FileNotFoundError:
131
+ print(f"ERROR: plan file not found: {plan_file}")
132
+ return 1
133
+ status, results = run_steps(repo_root, commands)
134
+ branch_rc, branch = _git(repo_root, ["branch", "--show-current"])
135
+ status_rc, git_status = _git(repo_root, ["status", "--short"])
136
+ final_status = status if branch_rc == 0 and status_rc == 0 else 1
137
+ print(render_run(plan_file, results, final_status, branch, git_status))
138
+ return final_status
139
+
140
+
141
+ def main(argv: list[str] | None = None) -> int:
142
+ args = argv if argv is not None else []
143
+ if not args:
144
+ print("ERROR: missing plan file. Usage: ./ns slice-runner <plan-file>")
145
+ print("Plan format: one shell command per line; blank lines and lines starting with # are ignored.")
146
+ return 1
147
+ return run_slice(Path(args[0]), Path(".").resolve())
148
+
149
+
150
+ if __name__ == "__main__":
151
+ import sys
152
+
153
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import shutil
6
+ import subprocess
7
+
8
+
9
+ TRACKED_WORKFLOW_EVIDENCE: tuple[str, ...] = (
10
+ ".agentic/workflow_state",
11
+ "docs/reports/CURRENT_WORKFLOW_OUTPUT.md",
12
+ )
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class CleanCheckResult:
17
+ ok: bool
18
+ expected_log: str
19
+ unexpected_lines: tuple[str, ...]
20
+ raw_lines: tuple[str, ...]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class EvidenceCleanResult:
25
+ ok: bool
26
+ before_status: tuple[str, ...]
27
+ restore_lines: tuple[str, ...]
28
+ removed_tmp_evidence: bool
29
+ untracked_doc_reports: tuple[str, ...]
30
+ after_status: tuple[str, ...]
31
+ errors: tuple[str, ...]
32
+
33
+
34
+ def normalize_expected_log(repo_root: Path, expected_log: str | Path) -> str:
35
+ expected = Path(expected_log)
36
+ if expected.is_absolute():
37
+ expected = expected.resolve().relative_to(repo_root.resolve())
38
+ return expected.as_posix()
39
+
40
+
41
+ def unexpected_status_lines(status_lines: list[str] | tuple[str, ...], expected_log: str) -> tuple[str, ...]:
42
+ allowed = "?? " + expected_log
43
+ unexpected: list[str] = []
44
+ for line in status_lines:
45
+ stripped = line.rstrip()
46
+ if stripped == "":
47
+ continue
48
+ if stripped == allowed:
49
+ continue
50
+ unexpected.append(stripped)
51
+ return tuple(unexpected)
52
+
53
+
54
+ def _run_git(repo_root: Path, args: list[str]) -> tuple[int, str]:
55
+ completed = subprocess.run(
56
+ ["git", *args],
57
+ cwd=repo_root,
58
+ text=True,
59
+ stdout=subprocess.PIPE,
60
+ stderr=subprocess.STDOUT,
61
+ check=False,
62
+ )
63
+ return completed.returncode, completed.stdout.strip()
64
+
65
+
66
+ def read_git_status(repo_root: Path, *paths: str) -> tuple[str, ...]:
67
+ completed = subprocess.run(
68
+ ["git", "status", "--short", *paths],
69
+ cwd=repo_root,
70
+ text=True,
71
+ stdout=subprocess.PIPE,
72
+ stderr=subprocess.STDOUT,
73
+ check=False,
74
+ )
75
+ if completed.returncode != 0:
76
+ return (completed.stdout.strip() or "git status failed",)
77
+ return tuple(line for line in completed.stdout.splitlines() if line.strip())
78
+
79
+
80
+ def check_clean_except_expected_log(repo_root: Path, expected_log: str | Path) -> CleanCheckResult:
81
+ normalized = normalize_expected_log(repo_root, expected_log)
82
+ raw = read_git_status(repo_root)
83
+ unexpected = unexpected_status_lines(raw, normalized)
84
+ return CleanCheckResult(
85
+ ok=len(unexpected) == 0,
86
+ expected_log=normalized,
87
+ unexpected_lines=unexpected,
88
+ raw_lines=raw,
89
+ )
90
+
91
+
92
+ def _path_is_tracked(repo_root: Path, path: str) -> bool:
93
+ returncode, _ = _run_git(repo_root, ["ls-files", "--error-unmatch", path])
94
+ return returncode == 0
95
+
96
+
97
+ def _path_is_dirty(repo_root: Path, path: str) -> bool:
98
+ worktree_rc, _ = _run_git(repo_root, ["diff", "--quiet", "--", path])
99
+ index_rc, _ = _run_git(repo_root, ["diff", "--cached", "--quiet", "--", path])
100
+ return worktree_rc != 0 or index_rc != 0
101
+
102
+
103
+ def _restore_known_tracked_workflow_evidence(repo_root: Path) -> tuple[tuple[str, ...], tuple[str, ...]]:
104
+ lines: list[str] = []
105
+ errors: list[str] = []
106
+ for path in TRACKED_WORKFLOW_EVIDENCE:
107
+ if not _path_is_tracked(repo_root, path):
108
+ lines.append(f"skip_untracked_path={path}")
109
+ continue
110
+ if not _path_is_dirty(repo_root, path):
111
+ lines.append(f"clean_tracked={path}")
112
+ continue
113
+ lines.append(f"restore_tracked={path}")
114
+ returncode, output = _run_git(repo_root, ["restore", "--staged", "--worktree", path])
115
+ if returncode != 0:
116
+ errors.append(output or f"git restore failed for {path}")
117
+ return tuple(lines), tuple(errors)
118
+
119
+
120
+ def _remove_ignored_tmp_evidence(repo_root: Path) -> bool:
121
+ target = repo_root / "tmp" / "agent-evidence"
122
+ if not target.exists():
123
+ return False
124
+ shutil.rmtree(target)
125
+ return True
126
+
127
+
128
+ def _untracked_doc_reports(repo_root: Path) -> tuple[str, ...]:
129
+ return tuple(line for line in read_git_status(repo_root, "docs/reports") if line.startswith("?? "))
130
+
131
+
132
+ def clean_local_evidence(repo_root: Path) -> EvidenceCleanResult:
133
+ before = read_git_status(repo_root)
134
+ restore_lines, restore_errors = _restore_known_tracked_workflow_evidence(repo_root)
135
+ errors = list(restore_errors)
136
+ try:
137
+ removed_tmp = _remove_ignored_tmp_evidence(repo_root)
138
+ except OSError as exc:
139
+ removed_tmp = False
140
+ errors.append(str(exc))
141
+ untracked_reports = _untracked_doc_reports(repo_root)
142
+ after = read_git_status(repo_root)
143
+ ok = not errors and not untracked_reports
144
+ return EvidenceCleanResult(
145
+ ok=ok,
146
+ before_status=before,
147
+ restore_lines=restore_lines,
148
+ removed_tmp_evidence=removed_tmp,
149
+ untracked_doc_reports=untracked_reports,
150
+ after_status=after,
151
+ errors=tuple(errors),
152
+ )
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import subprocess
6
+
7
+ from agentic_project_kit.safe_push import safe_push
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class EvidenceCommitPathsResult:
12
+ success: bool
13
+ branch: str
14
+ head_before: str
15
+ commit_sha: str
16
+ paths: tuple[str, ...]
17
+ log_path: str | None
18
+ findings: tuple[str, ...]
19
+
20
+
21
+ def _run_git(root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
22
+ return subprocess.run(["git", *args], cwd=root, text=True, capture_output=True, check=False)
23
+
24
+
25
+ def _git_stdout(root: Path, args: list[str]) -> str:
26
+ result = _run_git(root, args)
27
+ if result.returncode != 0:
28
+ return ""
29
+ return result.stdout.strip()
30
+
31
+
32
+ def _status_lines(root: Path) -> tuple[str, ...]:
33
+ result = _run_git(root, ["status", "--short", "--untracked-files=all"])
34
+ if result.returncode != 0:
35
+ return ()
36
+ return tuple(line for line in result.stdout.splitlines() if line.strip())
37
+
38
+
39
+ def _path_is_dirty(status_line: str, rel_path: str) -> bool:
40
+ return status_line.strip().endswith(rel_path)
41
+
42
+
43
+ def commit_paths(
44
+ *,
45
+ root: Path | str = Path("."),
46
+ paths: tuple[str, ...],
47
+ message: str,
48
+ log_path: str | None = None,
49
+ push: bool = False,
50
+ required_branch: str = "",
51
+ allow_main: bool = False,
52
+ ) -> EvidenceCommitPathsResult:
53
+ root_path = Path(root).resolve()
54
+ if not paths:
55
+ raise ValueError("at least one path is required")
56
+ if log_path:
57
+ if Path(log_path).is_absolute():
58
+ raise ValueError("log_path must be repository-relative")
59
+ if not log_path.startswith("docs/reports/terminal/"):
60
+ raise ValueError("log_path must start with docs/reports/terminal/")
61
+ if log_path not in paths:
62
+ paths = (*paths, log_path)
63
+
64
+ normalized_paths = tuple(dict.fromkeys(paths))
65
+ for rel in normalized_paths:
66
+ if Path(rel).is_absolute():
67
+ raise ValueError(f"path must be repository-relative: {rel}")
68
+ if rel.startswith(".git/") or rel == ".git":
69
+ raise ValueError(".git paths are not allowed")
70
+ if rel.startswith(".venv/") or rel == ".venv":
71
+ raise ValueError(".venv paths are not allowed")
72
+
73
+ branch = _git_stdout(root_path, ["branch", "--show-current"]) or "UNKNOWN"
74
+ if required_branch and branch != required_branch:
75
+ raise ValueError(f"current branch {branch!r} does not match required branch {required_branch!r}")
76
+ if branch == "main" and not allow_main:
77
+ raise ValueError("evidence commit-paths refuses to commit on main without --allow-main")
78
+ head_before = _git_stdout(root_path, ["rev-parse", "HEAD"]) or "UNKNOWN"
79
+ status_before = _status_lines(root_path)
80
+
81
+ if push:
82
+ push_preflight = safe_push(
83
+ root_path,
84
+ target_branch=branch,
85
+ purpose="evidence commit-paths publish branch",
86
+ dry_run=True,
87
+ )
88
+ if not push_preflight.ok:
89
+ return EvidenceCommitPathsResult(
90
+ False,
91
+ branch,
92
+ head_before,
93
+ "",
94
+ normalized_paths,
95
+ log_path,
96
+ tuple(["push preflight blocked evidence commit", *push_preflight.reasons]),
97
+ )
98
+
99
+ unexpected = [
100
+ line for line in status_before
101
+ if not any(_path_is_dirty(line, rel) for rel in normalized_paths)
102
+ ]
103
+ if unexpected:
104
+ return EvidenceCommitPathsResult(
105
+ False,
106
+ branch,
107
+ head_before,
108
+ "",
109
+ normalized_paths,
110
+ log_path,
111
+ tuple(["unexpected dirty paths before commit", *unexpected]),
112
+ )
113
+
114
+ deletion_lines = {line[3:] for line in status_before if line.startswith(" D ") or line.startswith("D ")}
115
+ missing = [
116
+ rel for rel in normalized_paths
117
+ if not (root_path / rel).exists() and rel not in deletion_lines
118
+ ]
119
+ if missing:
120
+ return EvidenceCommitPathsResult(
121
+ False,
122
+ branch,
123
+ head_before,
124
+ "",
125
+ normalized_paths,
126
+ log_path,
127
+ tuple(f"missing path: {rel}" for rel in missing),
128
+ )
129
+
130
+ add_result = _run_git(root_path, ["add", "-A", "--", *normalized_paths])
131
+ if add_result.returncode != 0:
132
+ return EvidenceCommitPathsResult(
133
+ False,
134
+ branch,
135
+ head_before,
136
+ "",
137
+ normalized_paths,
138
+ log_path,
139
+ (add_result.stderr.strip() or add_result.stdout.strip() or "git add failed",),
140
+ )
141
+
142
+ diff_result = _run_git(root_path, ["diff", "--cached", "--quiet"])
143
+ if diff_result.returncode == 0:
144
+ return EvidenceCommitPathsResult(
145
+ True,
146
+ branch,
147
+ head_before,
148
+ head_before,
149
+ normalized_paths,
150
+ log_path,
151
+ ("no staged diff; no commit created",),
152
+ )
153
+
154
+ commit_result = _run_git(root_path, ["commit", "-m", message])
155
+ if commit_result.returncode != 0:
156
+ return EvidenceCommitPathsResult(
157
+ False,
158
+ branch,
159
+ head_before,
160
+ "",
161
+ normalized_paths,
162
+ log_path,
163
+ (commit_result.stderr.strip() or commit_result.stdout.strip() or "git commit failed",),
164
+ )
165
+
166
+ commit_sha = _git_stdout(root_path, ["rev-parse", "HEAD"]) or "UNKNOWN"
167
+ if push:
168
+ push_result = safe_push(
169
+ root_path,
170
+ target_branch=branch,
171
+ purpose="evidence commit-paths publish branch",
172
+ )
173
+ if not push_result.ok:
174
+ return EvidenceCommitPathsResult(
175
+ False,
176
+ branch,
177
+ head_before,
178
+ commit_sha,
179
+ normalized_paths,
180
+ log_path,
181
+ (push_result.stderr.strip() or push_result.stdout.strip() or "git push failed",),
182
+ )
183
+
184
+ status_after = _status_lines(root_path)
185
+ if status_after:
186
+ return EvidenceCommitPathsResult(
187
+ False,
188
+ branch,
189
+ head_before,
190
+ commit_sha,
191
+ normalized_paths,
192
+ log_path,
193
+ tuple(["dirty worktree after commit", *status_after]),
194
+ )
195
+
196
+ return EvidenceCommitPathsResult(True, branch, head_before, commit_sha, normalized_paths, log_path, ())
197
+
198
+
199
+ def render_commit_paths_result(result: EvidenceCommitPathsResult) -> str:
200
+ lines = [
201
+ "EVIDENCE_COMMIT_PATHS",
202
+ f"success={'yes' if result.success else 'no'}",
203
+ f"branch={result.branch}",
204
+ f"head_before={result.head_before}",
205
+ f"commit_sha={result.commit_sha or 'NONE'}",
206
+ ]
207
+ if result.log_path:
208
+ lines.append(f"log_path={result.log_path}")
209
+ for path in result.paths:
210
+ lines.append(f"path={path}")
211
+ for finding in result.findings:
212
+ lines.append(f"finding={finding}")
213
+ lines.append(f"### RESULT: {'PASS' if result.success else 'FAIL'} ###")
214
+ return "\n".join(lines)