@opengsd/gsd-path 1.1.0 → 1.3.0

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 (578) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +87 -0
  3. package/DOCS.md +73 -1
  4. package/FULL.md +3 -2
  5. package/HOOKS.md +18 -9
  6. package/QUICK.md +3 -4
  7. package/README.md +72 -5
  8. package/RUNTIME.md +9 -4
  9. package/UPDATE.md +56 -11
  10. package/WORKFLOW.md +5 -2
  11. package/package.json +9 -1
  12. package/platforms/claude/dispatch.md +4 -4
  13. package/platforms/copilot/dispatch.md +4 -4
  14. package/platforms/cursor/dispatch.md +5 -10
  15. package/platforms/grok/dispatch.md +4 -6
  16. package/platforms/kimi/dispatch.md +4 -4
  17. package/platforms/kiro/dispatch.md +4 -6
  18. package/platforms/opencode/dispatch.md +4 -4
  19. package/platforms/qwen/dispatch.md +4 -4
  20. package/platforms/shared-agents/dispatch.md +6 -19
  21. package/scripts/_common.py +5 -0
  22. package/scripts/archive_milestone.py +20 -6
  23. package/scripts/bootstrap_repository.py +8 -1
  24. package/scripts/build_recovery.py +5 -0
  25. package/scripts/build_state.py +2 -0
  26. package/scripts/check_docs_audit.py +2 -0
  27. package/scripts/check_handoffs.py +6 -0
  28. package/scripts/check_task_briefs.py +3 -0
  29. package/scripts/detect_project.py +11 -1
  30. package/scripts/discussion_records.py +3 -0
  31. package/scripts/discussion_validate.py +8 -3
  32. package/scripts/dispatch_driver.py +230 -21
  33. package/scripts/git_guard.py +12 -4
  34. package/scripts/guard_hook.py +11 -1
  35. package/scripts/install.mjs +74 -1448
  36. package/scripts/install.py +165 -201
  37. package/scripts/integration.py +28 -8
  38. package/scripts/isolation.py +15 -5
  39. package/scripts/jev_review.py +138 -0
  40. package/scripts/lean_verification.py +2 -0
  41. package/scripts/loop_run.py +2 -0
  42. package/scripts/model_policy.py +279 -0
  43. package/scripts/path_config.py +248 -0
  44. package/scripts/pipeline_diagnose.py +3 -0
  45. package/scripts/pipeline_git.py +3 -0
  46. package/scripts/pipeline_state.py +4 -1
  47. package/scripts/pipeline_undo.py +3 -0
  48. package/scripts/promote_lookahead.py +3 -0
  49. package/scripts/review_panel.py +11 -0
  50. package/scripts/roadmap.py +5 -0
  51. package/scripts/runtime_store.py +281 -0
  52. package/scripts/skill-resources.json +104 -4
  53. package/scripts/state_checkpoint.py +5 -0
  54. package/scripts/state_promote.py +5 -0
  55. package/scripts/status_runtime.py +92 -6
  56. package/scripts/wizard.mjs +8 -2
  57. package/scripts/workflow_run.py +12 -1
  58. package/scripts/worktree_paths.py +94 -0
  59. package/skills/gsd-path/BUILD.md +7 -0
  60. package/skills/gsd-path/DECIDE.md +2 -0
  61. package/skills/gsd-path/DEFINE.md +13 -7
  62. package/skills/gsd-path/DOCS-AUDIT.md +2 -0
  63. package/skills/gsd-path/INSPECT.md +2 -0
  64. package/skills/gsd-path/PLAN.md +4 -2
  65. package/skills/gsd-path/RESEARCH.md +2 -0
  66. package/skills/gsd-path/ROADMAP.md +2 -0
  67. package/skills/gsd-path/SHIP.md +24 -13
  68. package/skills/gsd-path/SKILL.md +23 -9
  69. package/skills/gsd-path/references/build-native.md +15 -7
  70. package/skills/gsd-path/references/config.md +57 -0
  71. package/skills/gsd-path/references/dispatch.md +6 -19
  72. package/skills/gsd-path/references/jev-review.md +82 -0
  73. package/skills/gsd-path/references/model-policy.md +139 -0
  74. package/skills/gsd-path/references/reviewer-panel.md +1 -1
  75. package/skills/gsd-path/references/reviewer.md +2 -0
  76. package/skills/gsd-path/references/runtime-selection.md +25 -0
  77. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  78. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  79. package/skills/gsd-path/scripts/_common.py +5 -0
  80. package/skills/gsd-path/scripts/archive_milestone.py +20 -6
  81. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -1
  82. package/skills/gsd-path/scripts/build_recovery.py +5 -0
  83. package/skills/gsd-path/scripts/build_state.py +2 -0
  84. package/skills/gsd-path/scripts/check_docs_audit.py +2 -0
  85. package/skills/gsd-path/scripts/check_handoffs.py +6 -0
  86. package/skills/gsd-path/scripts/check_task_briefs.py +3 -0
  87. package/skills/gsd-path/scripts/detect_project.py +11 -1
  88. package/skills/gsd-path/scripts/discussion_records.py +3 -0
  89. package/skills/gsd-path/scripts/discussion_validate.py +8 -3
  90. package/skills/gsd-path/scripts/dispatch_driver.py +230 -21
  91. package/skills/gsd-path/scripts/git_guard.py +768 -0
  92. package/skills/gsd-path/scripts/integration.py +28 -8
  93. package/skills/gsd-path/scripts/isolation.py +15 -5
  94. package/skills/gsd-path/scripts/jev_review.py +138 -0
  95. package/skills/gsd-path/scripts/lean_verification.py +2 -0
  96. package/skills/gsd-path/scripts/loop_run.py +2 -0
  97. package/skills/gsd-path/scripts/model_policy.py +279 -0
  98. package/skills/gsd-path/scripts/path_config.py +248 -0
  99. package/skills/gsd-path/scripts/pipeline_git.py +3 -0
  100. package/skills/gsd-path/scripts/pipeline_state.py +4 -1
  101. package/skills/gsd-path/scripts/promote_lookahead.py +3 -0
  102. package/skills/gsd-path/scripts/review_panel.py +11 -0
  103. package/skills/gsd-path/scripts/roadmap.py +5 -0
  104. package/skills/gsd-path/scripts/state_checkpoint.py +5 -0
  105. package/skills/gsd-path/scripts/state_promote.py +5 -0
  106. package/skills/gsd-path/scripts/status_runtime.py +208 -0
  107. package/skills/gsd-path/scripts/workflow_run.py +12 -1
  108. package/skills/gsd-path/scripts/worktree_paths.py +94 -0
  109. package/skills/gsd-path/templates/skeptic.md +1 -1
  110. package/skills/gsd-path/templates/task.md +2 -0
  111. package/skills/gsd-path/templates/wave-panel.md +1 -1
  112. package/skills/gsd-path-build/SKILL.md +7 -0
  113. package/skills/gsd-path-build/references/build-native.md +15 -7
  114. package/skills/gsd-path-build/references/dispatch.md +6 -19
  115. package/skills/gsd-path-build/references/jev-review.md +82 -0
  116. package/skills/gsd-path-build/references/model-policy.md +139 -0
  117. package/skills/gsd-path-build/references/reviewer-panel.md +1 -1
  118. package/skills/gsd-path-build/references/reviewer.md +2 -0
  119. package/skills/gsd-path-build/references/runtime-selection.md +25 -0
  120. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  121. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  122. package/skills/gsd-path-build/scripts/_common.py +5 -0
  123. package/skills/gsd-path-build/scripts/archive_milestone.py +20 -6
  124. package/skills/gsd-path-build/scripts/build_recovery.py +5 -0
  125. package/skills/gsd-path-build/scripts/build_state.py +2 -0
  126. package/skills/gsd-path-build/scripts/check_handoffs.py +6 -0
  127. package/skills/gsd-path-build/scripts/check_task_briefs.py +3 -0
  128. package/skills/gsd-path-build/scripts/detect_project.py +11 -1
  129. package/skills/gsd-path-build/scripts/discussion_records.py +3 -0
  130. package/skills/gsd-path-build/scripts/discussion_validate.py +8 -3
  131. package/skills/gsd-path-build/scripts/dispatch_driver.py +230 -21
  132. package/skills/gsd-path-build/scripts/integration.py +28 -8
  133. package/skills/gsd-path-build/scripts/isolation.py +15 -5
  134. package/skills/gsd-path-build/scripts/jev_review.py +138 -0
  135. package/skills/gsd-path-build/scripts/loop_run.py +2 -0
  136. package/skills/gsd-path-build/scripts/model_policy.py +279 -0
  137. package/skills/gsd-path-build/scripts/path_config.py +248 -0
  138. package/skills/gsd-path-build/scripts/pipeline_git.py +3 -0
  139. package/skills/gsd-path-build/scripts/pipeline_state.py +4 -1
  140. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -0
  141. package/skills/gsd-path-build/scripts/review_panel.py +11 -0
  142. package/skills/gsd-path-build/scripts/roadmap.py +5 -0
  143. package/skills/gsd-path-build/scripts/state_checkpoint.py +5 -0
  144. package/skills/gsd-path-build/scripts/state_promote.py +5 -0
  145. package/skills/gsd-path-build/scripts/workflow_run.py +12 -1
  146. package/skills/gsd-path-build/scripts/worktree_paths.py +94 -0
  147. package/skills/gsd-path-build/templates/skeptic.md +1 -1
  148. package/skills/gsd-path-build/templates/task.md +2 -0
  149. package/skills/gsd-path-build/templates/wave-panel.md +1 -1
  150. package/skills/gsd-path-decide/SKILL.md +2 -0
  151. package/skills/gsd-path-decide/references/dispatch.md +6 -19
  152. package/skills/gsd-path-decide/references/model-policy.md +139 -0
  153. package/skills/gsd-path-decide/references/runtime-selection.md +25 -0
  154. package/skills/gsd-path-decide/scripts/_common.py +5 -0
  155. package/skills/gsd-path-decide/scripts/archive_milestone.py +20 -6
  156. package/skills/gsd-path-decide/scripts/build_recovery.py +5 -0
  157. package/skills/gsd-path-decide/scripts/check_handoffs.py +6 -0
  158. package/skills/gsd-path-decide/scripts/check_task_briefs.py +3 -0
  159. package/skills/gsd-path-decide/scripts/detect_project.py +11 -1
  160. package/skills/gsd-path-decide/scripts/discussion_records.py +3 -0
  161. package/skills/gsd-path-decide/scripts/discussion_validate.py +8 -3
  162. package/skills/gsd-path-decide/scripts/integration.py +28 -8
  163. package/skills/gsd-path-decide/scripts/isolation.py +15 -5
  164. package/skills/gsd-path-decide/scripts/loop_run.py +839 -0
  165. package/skills/gsd-path-decide/scripts/model_policy.py +279 -0
  166. package/skills/gsd-path-decide/scripts/path_config.py +248 -0
  167. package/skills/gsd-path-decide/scripts/pipeline_git.py +3 -0
  168. package/skills/gsd-path-decide/scripts/pipeline_state.py +4 -1
  169. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -0
  170. package/skills/gsd-path-decide/scripts/review_panel.py +11 -0
  171. package/skills/gsd-path-decide/scripts/roadmap.py +5 -0
  172. package/skills/gsd-path-decide/scripts/state_checkpoint.py +5 -0
  173. package/skills/gsd-path-decide/scripts/state_promote.py +5 -0
  174. package/skills/gsd-path-decide/scripts/worktree_paths.py +94 -0
  175. package/skills/gsd-path-define/SKILL.md +13 -7
  176. package/skills/gsd-path-define/references/runtime-selection.md +25 -0
  177. package/skills/gsd-path-define/scripts/_common.py +5 -0
  178. package/skills/gsd-path-define/scripts/archive_milestone.py +20 -6
  179. package/skills/gsd-path-define/scripts/build_recovery.py +5 -0
  180. package/skills/gsd-path-define/scripts/check_handoffs.py +6 -0
  181. package/skills/gsd-path-define/scripts/check_task_briefs.py +3 -0
  182. package/skills/gsd-path-define/scripts/detect_project.py +11 -1
  183. package/skills/gsd-path-define/scripts/discussion_records.py +3 -0
  184. package/skills/gsd-path-define/scripts/discussion_validate.py +8 -3
  185. package/skills/gsd-path-define/scripts/integration.py +28 -8
  186. package/skills/gsd-path-define/scripts/isolation.py +15 -5
  187. package/skills/gsd-path-define/scripts/loop_run.py +839 -0
  188. package/skills/gsd-path-define/scripts/model_policy.py +279 -0
  189. package/skills/gsd-path-define/scripts/path_config.py +248 -0
  190. package/skills/gsd-path-define/scripts/pipeline_git.py +3 -0
  191. package/skills/gsd-path-define/scripts/pipeline_state.py +4 -1
  192. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -0
  193. package/skills/gsd-path-define/scripts/review_panel.py +11 -0
  194. package/skills/gsd-path-define/scripts/roadmap.py +5 -0
  195. package/skills/gsd-path-define/scripts/state_checkpoint.py +5 -0
  196. package/skills/gsd-path-define/scripts/state_promote.py +5 -0
  197. package/skills/gsd-path-define/scripts/worktree_paths.py +94 -0
  198. package/skills/gsd-path-discuss/SKILL.md +2 -0
  199. package/skills/gsd-path-discuss/references/runtime-selection.md +25 -0
  200. package/skills/gsd-path-discuss/scripts/_common.py +5 -0
  201. package/skills/gsd-path-discuss/scripts/discussion_records.py +3 -0
  202. package/skills/gsd-path-discuss/scripts/loop_run.py +839 -0
  203. package/skills/gsd-path-discuss/scripts/model_policy.py +279 -0
  204. package/skills/gsd-path-discuss/scripts/path_config.py +248 -0
  205. package/skills/gsd-path-discuss/scripts/review_panel.py +564 -0
  206. package/skills/gsd-path-discuss/scripts/worktree_paths.py +94 -0
  207. package/skills/gsd-path-docs-audit/SKILL.md +2 -0
  208. package/skills/gsd-path-docs-audit/references/dispatch.md +6 -19
  209. package/skills/gsd-path-docs-audit/references/model-policy.md +139 -0
  210. package/skills/gsd-path-docs-audit/references/runtime-selection.md +25 -0
  211. package/skills/gsd-path-docs-audit/scripts/_common.py +5 -0
  212. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +20 -6
  213. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +5 -0
  214. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +2 -0
  215. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +6 -0
  216. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +3 -0
  217. package/skills/gsd-path-docs-audit/scripts/detect_project.py +11 -1
  218. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +3 -0
  219. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +8 -3
  220. package/skills/gsd-path-docs-audit/scripts/integration.py +28 -8
  221. package/skills/gsd-path-docs-audit/scripts/isolation.py +15 -5
  222. package/skills/gsd-path-docs-audit/scripts/loop_run.py +839 -0
  223. package/skills/gsd-path-docs-audit/scripts/model_policy.py +279 -0
  224. package/skills/gsd-path-docs-audit/scripts/path_config.py +248 -0
  225. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +3 -0
  226. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +4 -1
  227. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -0
  228. package/skills/gsd-path-docs-audit/scripts/review_panel.py +11 -0
  229. package/skills/gsd-path-docs-audit/scripts/roadmap.py +5 -0
  230. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +5 -0
  231. package/skills/gsd-path-docs-audit/scripts/state_promote.py +5 -0
  232. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +94 -0
  233. package/skills/gsd-path-forensics/SKILL.md +9 -5
  234. package/skills/gsd-path-forensics/references/runtime-selection.md +25 -0
  235. package/skills/gsd-path-forensics/scripts/_common.py +5 -0
  236. package/skills/gsd-path-forensics/scripts/discussion_records.py +3 -0
  237. package/skills/gsd-path-forensics/scripts/loop_run.py +839 -0
  238. package/skills/gsd-path-forensics/scripts/model_policy.py +279 -0
  239. package/skills/gsd-path-forensics/scripts/path_config.py +248 -0
  240. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +3 -0
  241. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +3 -0
  242. package/skills/gsd-path-forensics/scripts/review_panel.py +564 -0
  243. package/skills/gsd-path-forensics/scripts/worktree_paths.py +94 -0
  244. package/skills/gsd-path-inspect/SKILL.md +2 -0
  245. package/skills/gsd-path-inspect/references/dispatch.md +6 -19
  246. package/skills/gsd-path-inspect/references/model-policy.md +139 -0
  247. package/skills/gsd-path-inspect/references/runtime-selection.md +25 -0
  248. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  249. package/skills/gsd-path-inspect/scripts/_common.py +5 -0
  250. package/skills/gsd-path-inspect/scripts/archive_milestone.py +20 -6
  251. package/skills/gsd-path-inspect/scripts/build_recovery.py +5 -0
  252. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +2 -0
  253. package/skills/gsd-path-inspect/scripts/check_handoffs.py +6 -0
  254. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +3 -0
  255. package/skills/gsd-path-inspect/scripts/detect_project.py +11 -1
  256. package/skills/gsd-path-inspect/scripts/discussion_records.py +3 -0
  257. package/skills/gsd-path-inspect/scripts/discussion_validate.py +8 -3
  258. package/skills/gsd-path-inspect/scripts/integration.py +28 -8
  259. package/skills/gsd-path-inspect/scripts/isolation.py +15 -5
  260. package/skills/gsd-path-inspect/scripts/loop_run.py +839 -0
  261. package/skills/gsd-path-inspect/scripts/model_policy.py +279 -0
  262. package/skills/gsd-path-inspect/scripts/path_config.py +248 -0
  263. package/skills/gsd-path-inspect/scripts/pipeline_git.py +3 -0
  264. package/skills/gsd-path-inspect/scripts/pipeline_state.py +4 -1
  265. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -0
  266. package/skills/gsd-path-inspect/scripts/review_panel.py +11 -0
  267. package/skills/gsd-path-inspect/scripts/roadmap.py +5 -0
  268. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +5 -0
  269. package/skills/gsd-path-inspect/scripts/state_promote.py +5 -0
  270. package/skills/gsd-path-inspect/scripts/workflow_run.py +12 -1
  271. package/skills/gsd-path-inspect/scripts/worktree_paths.py +94 -0
  272. package/skills/gsd-path-loop/SKILL.md +2 -0
  273. package/skills/gsd-path-loop/references/runtime-selection.md +25 -0
  274. package/skills/gsd-path-loop/scripts/_common.py +5 -0
  275. package/skills/gsd-path-loop/scripts/discussion_records.py +3 -0
  276. package/skills/gsd-path-loop/scripts/loop_run.py +2 -0
  277. package/skills/gsd-path-loop/scripts/model_policy.py +279 -0
  278. package/skills/gsd-path-loop/scripts/path_config.py +248 -0
  279. package/skills/gsd-path-loop/scripts/review_panel.py +564 -0
  280. package/skills/gsd-path-loop/scripts/worktree_paths.py +94 -0
  281. package/skills/gsd-path-migrate/SKILL.md +2 -0
  282. package/skills/gsd-path-migrate/references/runtime-selection.md +25 -0
  283. package/skills/gsd-path-migrate/scripts/_common.py +185 -0
  284. package/skills/gsd-path-migrate/scripts/discussion_records.py +3 -0
  285. package/skills/gsd-path-migrate/scripts/loop_run.py +839 -0
  286. package/skills/gsd-path-migrate/scripts/model_policy.py +279 -0
  287. package/skills/gsd-path-migrate/scripts/path_config.py +248 -0
  288. package/skills/gsd-path-migrate/scripts/review_panel.py +564 -0
  289. package/skills/gsd-path-plan/SKILL.md +4 -2
  290. package/skills/gsd-path-plan/references/dispatch.md +6 -19
  291. package/skills/gsd-path-plan/references/model-policy.md +139 -0
  292. package/skills/gsd-path-plan/references/runtime-selection.md +25 -0
  293. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  294. package/skills/gsd-path-plan/scripts/_common.py +5 -0
  295. package/skills/gsd-path-plan/scripts/archive_milestone.py +20 -6
  296. package/skills/gsd-path-plan/scripts/build_recovery.py +5 -0
  297. package/skills/gsd-path-plan/scripts/check_handoffs.py +6 -0
  298. package/skills/gsd-path-plan/scripts/check_task_briefs.py +3 -0
  299. package/skills/gsd-path-plan/scripts/detect_project.py +11 -1
  300. package/skills/gsd-path-plan/scripts/discussion_records.py +3 -0
  301. package/skills/gsd-path-plan/scripts/discussion_validate.py +8 -3
  302. package/skills/gsd-path-plan/scripts/integration.py +28 -8
  303. package/skills/gsd-path-plan/scripts/isolation.py +15 -5
  304. package/skills/gsd-path-plan/scripts/loop_run.py +839 -0
  305. package/skills/gsd-path-plan/scripts/model_policy.py +279 -0
  306. package/skills/gsd-path-plan/scripts/path_config.py +248 -0
  307. package/skills/gsd-path-plan/scripts/pipeline_git.py +3 -0
  308. package/skills/gsd-path-plan/scripts/pipeline_state.py +4 -1
  309. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -0
  310. package/skills/gsd-path-plan/scripts/review_panel.py +11 -0
  311. package/skills/gsd-path-plan/scripts/roadmap.py +5 -0
  312. package/skills/gsd-path-plan/scripts/state_checkpoint.py +5 -0
  313. package/skills/gsd-path-plan/scripts/state_promote.py +5 -0
  314. package/skills/gsd-path-plan/scripts/workflow_run.py +12 -1
  315. package/skills/gsd-path-plan/scripts/worktree_paths.py +94 -0
  316. package/skills/gsd-path-plan/templates/task.md +2 -0
  317. package/skills/gsd-path-research/SKILL.md +2 -0
  318. package/skills/gsd-path-research/references/dispatch.md +6 -19
  319. package/skills/gsd-path-research/references/model-policy.md +139 -0
  320. package/skills/gsd-path-research/references/runtime-selection.md +25 -0
  321. package/skills/gsd-path-research/scripts/_common.py +5 -0
  322. package/skills/gsd-path-research/scripts/archive_milestone.py +20 -6
  323. package/skills/gsd-path-research/scripts/build_recovery.py +5 -0
  324. package/skills/gsd-path-research/scripts/check_handoffs.py +6 -0
  325. package/skills/gsd-path-research/scripts/check_task_briefs.py +3 -0
  326. package/skills/gsd-path-research/scripts/detect_project.py +11 -1
  327. package/skills/gsd-path-research/scripts/discussion_records.py +3 -0
  328. package/skills/gsd-path-research/scripts/discussion_validate.py +8 -3
  329. package/skills/gsd-path-research/scripts/integration.py +28 -8
  330. package/skills/gsd-path-research/scripts/isolation.py +15 -5
  331. package/skills/gsd-path-research/scripts/loop_run.py +839 -0
  332. package/skills/gsd-path-research/scripts/model_policy.py +279 -0
  333. package/skills/gsd-path-research/scripts/path_config.py +248 -0
  334. package/skills/gsd-path-research/scripts/pipeline_git.py +3 -0
  335. package/skills/gsd-path-research/scripts/pipeline_state.py +4 -1
  336. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -0
  337. package/skills/gsd-path-research/scripts/review_panel.py +11 -0
  338. package/skills/gsd-path-research/scripts/roadmap.py +5 -0
  339. package/skills/gsd-path-research/scripts/state_checkpoint.py +5 -0
  340. package/skills/gsd-path-research/scripts/state_promote.py +5 -0
  341. package/skills/gsd-path-research/scripts/worktree_paths.py +94 -0
  342. package/skills/gsd-path-roadmap/SKILL.md +2 -0
  343. package/skills/gsd-path-roadmap/references/dispatch.md +6 -19
  344. package/skills/gsd-path-roadmap/references/model-policy.md +139 -0
  345. package/skills/gsd-path-roadmap/references/runtime-selection.md +25 -0
  346. package/skills/gsd-path-roadmap/scripts/_common.py +5 -0
  347. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +20 -6
  348. package/skills/gsd-path-roadmap/scripts/build_recovery.py +5 -0
  349. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +6 -0
  350. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +3 -0
  351. package/skills/gsd-path-roadmap/scripts/detect_project.py +11 -1
  352. package/skills/gsd-path-roadmap/scripts/discussion_records.py +3 -0
  353. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +8 -3
  354. package/skills/gsd-path-roadmap/scripts/integration.py +28 -8
  355. package/skills/gsd-path-roadmap/scripts/isolation.py +15 -5
  356. package/skills/gsd-path-roadmap/scripts/loop_run.py +839 -0
  357. package/skills/gsd-path-roadmap/scripts/model_policy.py +279 -0
  358. package/skills/gsd-path-roadmap/scripts/path_config.py +248 -0
  359. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +3 -0
  360. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +4 -1
  361. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -0
  362. package/skills/gsd-path-roadmap/scripts/review_panel.py +11 -0
  363. package/skills/gsd-path-roadmap/scripts/roadmap.py +5 -0
  364. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +5 -0
  365. package/skills/gsd-path-roadmap/scripts/state_promote.py +5 -0
  366. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +94 -0
  367. package/skills/gsd-path-ship/SKILL.md +24 -13
  368. package/skills/gsd-path-ship/references/dispatch.md +6 -19
  369. package/skills/gsd-path-ship/references/jev-review.md +82 -0
  370. package/skills/gsd-path-ship/references/model-policy.md +139 -0
  371. package/skills/gsd-path-ship/references/reviewer-panel.md +1 -1
  372. package/skills/gsd-path-ship/references/reviewer.md +2 -0
  373. package/skills/gsd-path-ship/references/runtime-selection.md +25 -0
  374. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  375. package/skills/gsd-path-ship/scripts/_common.py +5 -0
  376. package/skills/gsd-path-ship/scripts/archive_milestone.py +20 -6
  377. package/skills/gsd-path-ship/scripts/build_recovery.py +5 -0
  378. package/skills/gsd-path-ship/scripts/build_state.py +2 -0
  379. package/skills/gsd-path-ship/scripts/check_handoffs.py +6 -0
  380. package/skills/gsd-path-ship/scripts/check_task_briefs.py +3 -0
  381. package/skills/gsd-path-ship/scripts/detect_project.py +11 -1
  382. package/skills/gsd-path-ship/scripts/discussion_records.py +3 -0
  383. package/skills/gsd-path-ship/scripts/discussion_validate.py +8 -3
  384. package/skills/gsd-path-ship/scripts/integration.py +28 -8
  385. package/skills/gsd-path-ship/scripts/isolation.py +15 -5
  386. package/skills/gsd-path-ship/scripts/jev_review.py +138 -0
  387. package/skills/gsd-path-ship/scripts/lean_verification.py +2 -0
  388. package/skills/gsd-path-ship/scripts/loop_run.py +839 -0
  389. package/skills/gsd-path-ship/scripts/model_policy.py +279 -0
  390. package/skills/gsd-path-ship/scripts/path_config.py +248 -0
  391. package/skills/gsd-path-ship/scripts/pipeline_git.py +3 -0
  392. package/skills/gsd-path-ship/scripts/pipeline_state.py +4 -1
  393. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -0
  394. package/skills/gsd-path-ship/scripts/review_panel.py +11 -0
  395. package/skills/gsd-path-ship/scripts/roadmap.py +5 -0
  396. package/skills/gsd-path-ship/scripts/state_checkpoint.py +5 -0
  397. package/skills/gsd-path-ship/scripts/state_promote.py +5 -0
  398. package/skills/gsd-path-ship/scripts/workflow_run.py +12 -1
  399. package/skills/gsd-path-ship/scripts/worktree_paths.py +94 -0
  400. package/skills/gsd-path-undo/SKILL.md +2 -0
  401. package/skills/gsd-path-undo/references/runtime-selection.md +25 -0
  402. package/skills/gsd-path-undo/scripts/_common.py +5 -0
  403. package/skills/gsd-path-undo/scripts/discussion_records.py +3 -0
  404. package/skills/gsd-path-undo/scripts/loop_run.py +839 -0
  405. package/skills/gsd-path-undo/scripts/model_policy.py +279 -0
  406. package/skills/gsd-path-undo/scripts/path_config.py +248 -0
  407. package/skills/gsd-path-undo/scripts/pipeline_undo.py +3 -0
  408. package/skills/gsd-path-undo/scripts/review_panel.py +564 -0
  409. package/skills/gsd-path-undo/scripts/worktree_paths.py +94 -0
  410. package/skills/path/BUILD.md +7 -0
  411. package/skills/path/DECIDE.md +2 -0
  412. package/skills/path/DEFINE.md +13 -7
  413. package/skills/path/DOCS-AUDIT.md +2 -0
  414. package/skills/path/INSPECT.md +2 -0
  415. package/skills/path/PLAN.md +4 -2
  416. package/skills/path/RESEARCH.md +2 -0
  417. package/skills/path/ROADMAP.md +2 -0
  418. package/skills/path/SHIP.md +24 -13
  419. package/skills/path/SKILL.md +23 -9
  420. package/skills/path/references/build-native.md +15 -7
  421. package/skills/path/references/config.md +57 -0
  422. package/skills/path/references/dispatch.md +6 -19
  423. package/skills/path/references/jev-review.md +82 -0
  424. package/skills/path/references/model-policy.md +139 -0
  425. package/skills/path/references/reviewer-panel.md +1 -1
  426. package/skills/path/references/reviewer.md +2 -0
  427. package/skills/path/references/runtime-selection.md +25 -0
  428. package/skills/path/scripts/_common.py +5 -0
  429. package/skills/path/scripts/archive_milestone.py +20 -6
  430. package/skills/path/scripts/bootstrap_repository.py +8 -1
  431. package/skills/path/scripts/build_recovery.py +5 -0
  432. package/skills/path/scripts/build_state.py +2 -0
  433. package/skills/path/scripts/check_docs_audit.py +2 -0
  434. package/skills/path/scripts/check_handoffs.py +6 -0
  435. package/skills/path/scripts/check_task_briefs.py +3 -0
  436. package/skills/path/scripts/detect_project.py +11 -1
  437. package/skills/path/scripts/discussion_records.py +3 -0
  438. package/skills/path/scripts/discussion_validate.py +8 -3
  439. package/skills/path/scripts/dispatch_driver.py +230 -21
  440. package/skills/path/scripts/git_guard.py +768 -0
  441. package/skills/path/scripts/integration.py +28 -8
  442. package/skills/path/scripts/isolation.py +15 -5
  443. package/skills/path/scripts/jev_review.py +138 -0
  444. package/skills/path/scripts/lean_verification.py +2 -0
  445. package/skills/path/scripts/loop_run.py +2 -0
  446. package/skills/path/scripts/model_policy.py +279 -0
  447. package/skills/path/scripts/path_config.py +248 -0
  448. package/skills/path/scripts/pipeline_git.py +3 -0
  449. package/skills/path/scripts/pipeline_state.py +4 -1
  450. package/skills/path/scripts/promote_lookahead.py +3 -0
  451. package/skills/path/scripts/review_panel.py +11 -0
  452. package/skills/path/scripts/roadmap.py +5 -0
  453. package/skills/path/scripts/state_checkpoint.py +5 -0
  454. package/skills/path/scripts/state_promote.py +5 -0
  455. package/skills/path/scripts/status_runtime.py +208 -0
  456. package/skills/path/scripts/workflow_run.py +12 -1
  457. package/skills/path/scripts/worktree_paths.py +94 -0
  458. package/skills/path/templates/skeptic.md +1 -1
  459. package/skills/path/templates/task.md +2 -0
  460. package/skills/path/templates/wave-panel.md +1 -1
  461. package/skills/gsd-path/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  462. package/skills/gsd-path/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  463. package/skills/gsd-path/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  464. package/skills/gsd-path/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  465. package/skills/gsd-path/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  466. package/skills/gsd-path/scripts/__pycache__/discussion_records.cpython-312.pyc +0 -0
  467. package/skills/gsd-path/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  468. package/skills/gsd-path/scripts/__pycache__/integration.cpython-312.pyc +0 -0
  469. package/skills/gsd-path/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  470. package/skills/gsd-path/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
  471. package/skills/gsd-path/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  472. package/skills/gsd-path/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  473. package/skills/gsd-path/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
  474. package/skills/gsd-path/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  475. package/skills/gsd-path/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  476. package/skills/gsd-path/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  477. package/skills/gsd-path/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
  478. package/skills/gsd-path/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
  479. package/skills/gsd-path-build/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  480. package/skills/gsd-path-build/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  481. package/skills/gsd-path-build/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  482. package/skills/gsd-path-build/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  483. package/skills/gsd-path-build/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  484. package/skills/gsd-path-build/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  485. package/skills/gsd-path-build/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  486. package/skills/gsd-path-build/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
  487. package/skills/gsd-path-build/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  488. package/skills/gsd-path-build/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  489. package/skills/gsd-path-build/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
  490. package/skills/gsd-path-build/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  491. package/skills/gsd-path-build/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  492. package/skills/gsd-path-build/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  493. package/skills/gsd-path-build/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
  494. package/skills/gsd-path-build/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
  495. package/skills/gsd-path-decide/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  496. package/skills/gsd-path-decide/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  497. package/skills/gsd-path-decide/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  498. package/skills/gsd-path-decide/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  499. package/skills/gsd-path-decide/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  500. package/skills/gsd-path-decide/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  501. package/skills/gsd-path-decide/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  502. package/skills/gsd-path-decide/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  503. package/skills/gsd-path-decide/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  504. package/skills/gsd-path-decide/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  505. package/skills/gsd-path-define/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  506. package/skills/gsd-path-define/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  507. package/skills/gsd-path-define/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  508. package/skills/gsd-path-define/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  509. package/skills/gsd-path-define/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  510. package/skills/gsd-path-define/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  511. package/skills/gsd-path-define/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  512. package/skills/gsd-path-define/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  513. package/skills/gsd-path-define/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  514. package/skills/gsd-path-define/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  515. package/skills/gsd-path-docs-audit/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  516. package/skills/gsd-path-docs-audit/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  517. package/skills/gsd-path-docs-audit/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  518. package/skills/gsd-path-docs-audit/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  519. package/skills/gsd-path-docs-audit/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  520. package/skills/gsd-path-docs-audit/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  521. package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  522. package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  523. package/skills/gsd-path-docs-audit/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  524. package/skills/gsd-path-docs-audit/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  525. package/skills/gsd-path-docs-audit/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  526. package/skills/gsd-path-forensics/scripts/__pycache__/pipeline_undo.cpython-312.pyc +0 -0
  527. package/skills/gsd-path-inspect/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  528. package/skills/gsd-path-inspect/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  529. package/skills/gsd-path-inspect/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  530. package/skills/gsd-path-inspect/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  531. package/skills/gsd-path-inspect/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  532. package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  533. package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  534. package/skills/gsd-path-inspect/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  535. package/skills/gsd-path-inspect/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  536. package/skills/gsd-path-inspect/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  537. package/skills/gsd-path-plan/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  538. package/skills/gsd-path-plan/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  539. package/skills/gsd-path-plan/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  540. package/skills/gsd-path-plan/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  541. package/skills/gsd-path-plan/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  542. package/skills/gsd-path-plan/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  543. package/skills/gsd-path-plan/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  544. package/skills/gsd-path-plan/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  545. package/skills/gsd-path-plan/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  546. package/skills/gsd-path-research/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  547. package/skills/gsd-path-research/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  548. package/skills/gsd-path-research/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  549. package/skills/gsd-path-research/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  550. package/skills/gsd-path-research/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  551. package/skills/gsd-path-research/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  552. package/skills/gsd-path-research/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  553. package/skills/gsd-path-research/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  554. package/skills/gsd-path-research/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  555. package/skills/gsd-path-research/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  556. package/skills/gsd-path-roadmap/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  557. package/skills/gsd-path-roadmap/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  558. package/skills/gsd-path-roadmap/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  559. package/skills/gsd-path-roadmap/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  560. package/skills/gsd-path-roadmap/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  561. package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  562. package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  563. package/skills/gsd-path-roadmap/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  564. package/skills/gsd-path-roadmap/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  565. package/skills/gsd-path-roadmap/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  566. package/skills/gsd-path-ship/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  567. package/skills/gsd-path-ship/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  568. package/skills/gsd-path-ship/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  569. package/skills/gsd-path-ship/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  570. package/skills/gsd-path-ship/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  571. package/skills/gsd-path-ship/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  572. package/skills/gsd-path-ship/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  573. package/skills/gsd-path-ship/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  574. package/skills/gsd-path-ship/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  575. package/skills/gsd-path-ship/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  576. package/skills/gsd-path-ship/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  577. package/skills/gsd-path-ship/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  578. package/skills/gsd-path-ship/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
@@ -0,0 +1,768 @@
1
+ #!/usr/bin/env python3
2
+ """Git pre-commit, commit-msg, and pre-push guard for GSD Path projects.
3
+
4
+ See HOOKS.md for guard rules and scripts/install.py / scripts/install.mjs for
5
+ hook wiring. Inspection failures and violations both exit 1: unreadable Git
6
+ state cannot prove archive immutability or safe publication.
7
+ """
8
+
9
+ import re
10
+ import subprocess
11
+ from functools import lru_cache
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ # Installed layout keeps the pipeline runtime in .gsd-path/runtime/ beside this
16
+ # guard; the repository checkout keeps it as sibling modules under scripts/.
17
+ _HERE = Path(__file__).resolve().parent
18
+ sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py").is_file() else _HERE))
19
+ sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
20
+ try:
21
+ from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
22
+ from pipeline_git import is_ship_subject, task_commit_body
23
+ from pipeline_state import _completion_status
24
+ except ImportError as error: # pragma: no cover - broken install
25
+ print(
26
+ f"gsd-path guard: pipeline runtime is missing ({error}); commit blocked; "
27
+ "rerun the installer with --hooks-refresh",
28
+ file=sys.stderr,
29
+ )
30
+ raise SystemExit(1)
31
+
32
+ ARCHIVE_PREFIX = ".project/archive/"
33
+ GUARD_MARKER = "gsd-path guard"
34
+ INTEGRATE_SUBJECT = re.compile(
35
+ r"^integrate: (?P<milestone>M\d{3,}) — merge "
36
+ r"(?P<branch>gsd-path/M\d{3,}) into (?P<target>\S+)$"
37
+ )
38
+ ABANDON_SUBJECT = re.compile(
39
+ r"^build: abandon milestone (?P<slug>[a-z0-9][a-z0-9-]*)$"
40
+ )
41
+ ROADMAP_HEADING = re.compile(
42
+ r"^### (?P<milestone>M\d{3,}) — (?P<slug>[a-z0-9][a-z0-9-]*)\s*$"
43
+ )
44
+ ARCHIVE_NAME = re.compile(
45
+ r"^\.project/archive/(?P<number>\d{3,})-(?P<slug>[a-z0-9][a-z0-9-]*)$"
46
+ )
47
+ SLUG = re.compile(r"^[a-z0-9][a-z0-9-]*$")
48
+ BOUND_BRANCH = re.compile(r"^gsd-path/M(?P<number>\d{3,})$")
49
+ TASK_BRANCH_PREFIX = "gsd-path-task/"
50
+ TASK_SUBJECT = re.compile(r"^(?P<id>[A-Za-z][A-Za-z0-9._-]*): \S.*$")
51
+ FULL_SHA = re.compile(r"^[0-9a-f]{40}$")
52
+ LANDING_HINT = (
53
+ "during build, changes outside .project/ land only through isolation.py "
54
+ "land; bookkeeping commits may touch only .project/"
55
+ )
56
+ PRODUCT_HINT = (
57
+ f"while this worktree carries unfinished milestone state, {LANDING_HINT}; "
58
+ "findings during ship reopen through the patch plan"
59
+ )
60
+ PUBLICATION_HINT = (
61
+ "unshipped milestone work reaches a remote only as its ship commit "
62
+ "(STATE shipped/done) through archive_milestone.py integrate; findings during "
63
+ "ship reopen through the patch plan (see SHIP.md)"
64
+ )
65
+ LEGACY_STATE_FIELDS = {
66
+ "pipeline",
67
+ "project",
68
+ "milestone",
69
+ "phase",
70
+ "status",
71
+ "branch",
72
+ "archive",
73
+ }
74
+ INTEGRATION_STATE_FIELDS = LEGACY_STATE_FIELDS | {
75
+ "integration_default",
76
+ "integration",
77
+ }
78
+ PROVENANCE_STATE_FIELDS = INTEGRATION_STATE_FIELDS | {"integration_source"}
79
+ INTEGRATION_MODES = {"direct", "pull-request"}
80
+ INTEGRATION_SOURCES = {"default", "milestone"}
81
+
82
+
83
+ def staged_entries():
84
+ output = subprocess.run(
85
+ ["git", "diff", "--cached", "--name-status", "-z", "--find-renames"],
86
+ capture_output=True,
87
+ text=True,
88
+ check=True,
89
+ ).stdout
90
+ tokens = output.split("\0")
91
+ entries = []
92
+ index = 0
93
+ while index < len(tokens):
94
+ status = tokens[index]
95
+ if not status:
96
+ index += 1
97
+ continue
98
+ code = status[0]
99
+ if code in "RC":
100
+ entries.append((code, tokens[index + 1], tokens[index + 2]))
101
+ index += 3
102
+ else:
103
+ entries.append((code, tokens[index + 1], None))
104
+ index += 2
105
+ return entries
106
+
107
+
108
+ def message_of(message_file):
109
+ lines = [
110
+ line.rstrip()
111
+ for line in Path(message_file).read_text(encoding="utf-8").splitlines()
112
+ ]
113
+ lines = [line for index, line in enumerate(lines) if line or (index and lines[index - 1])]
114
+ while lines and not lines[0]:
115
+ lines.pop(0)
116
+ if not lines:
117
+ return "", ""
118
+ subject_end = lines.index("") if "" in lines else len(lines)
119
+ return " ".join(line.strip() for line in lines[:subject_end]), "\n".join(lines[subject_end:]).strip("\n")
120
+
121
+
122
+ def archive_root(path):
123
+ if not path.startswith(ARCHIVE_PREFIX):
124
+ return None
125
+ name = path[len(ARCHIVE_PREFIX):].split("/", 1)[0]
126
+ return f"{ARCHIVE_PREFIX}{name}" if name else None
127
+
128
+
129
+ def destination_path(entry):
130
+ code, old, new = entry
131
+ if code == "A":
132
+ return old
133
+ if code in "CR":
134
+ return new
135
+ return None
136
+
137
+
138
+ def committed_archive_roots():
139
+ head = subprocess.run(
140
+ ["git", "rev-parse", "--verify", "--quiet", "HEAD^{commit}"],
141
+ capture_output=True,
142
+ text=True,
143
+ check=False,
144
+ )
145
+ if head.returncode == 1:
146
+ return set() # unborn HEAD: the first commit has no committed tree
147
+ head.check_returncode()
148
+ output = subprocess.run(
149
+ ["git", "ls-tree", "-r", "--name-only", "HEAD", "--", ARCHIVE_PREFIX],
150
+ capture_output=True,
151
+ text=True,
152
+ check=True,
153
+ ).stdout
154
+ return {
155
+ root
156
+ for path in output.splitlines()
157
+ if (root := archive_root(path)) is not None
158
+ }
159
+
160
+
161
+ def staged_file(path):
162
+ return subprocess.run(
163
+ ["git", "show", f":{path}"],
164
+ capture_output=True,
165
+ text=True,
166
+ check=True,
167
+ ).stdout
168
+
169
+
170
+ def staged_frontmatter():
171
+ return frontmatter_of(staged_file(".project/STATE.md"), "staged STATE.md")
172
+
173
+
174
+ def shown_file(revspec):
175
+ """`git show <revspec>` text, or None when the object does not exist."""
176
+ shown = subprocess.run(
177
+ ["git", "show", revspec],
178
+ capture_output=True,
179
+ text=True,
180
+ check=False,
181
+ )
182
+ return shown.stdout if shown.returncode == 0 else None
183
+
184
+
185
+ def state_at(revision):
186
+ """The STATE.md frontmatter at a revision; None when it has no STATE.md."""
187
+ subprocess.run(
188
+ ["git", "cat-file", "-e", revision],
189
+ capture_output=True,
190
+ text=True,
191
+ check=True,
192
+ )
193
+ content = shown_file(f"{revision}:.project/STATE.md")
194
+ return None if content is None else frontmatter_of(content, f"{revision} STATE.md")
195
+
196
+
197
+ def head_frontmatter():
198
+ content = shown_file("HEAD:.project/STATE.md")
199
+ return None if content is None else frontmatter_of(content, "HEAD STATE.md")
200
+
201
+
202
+ def shipped(state):
203
+ return (state.get("phase"), state.get("status")) == ("shipped", "done")
204
+
205
+
206
+ def unshipped_bound_branch(state):
207
+ """The bound branch a STATE still owes a ship commit to, or None."""
208
+ bound = state.get("branch", "")
209
+ return bound if BOUND_BRANCH.fullmatch(bound) and not shipped(state) else None
210
+
211
+
212
+ def frontmatter_of(content, label):
213
+ lines = content.splitlines()
214
+ if not lines or lines[0] != "---":
215
+ raise ValueError(f"{label} is missing YAML frontmatter")
216
+ values = {}
217
+ closed = False
218
+ for line in lines[1:]:
219
+ if line == "---":
220
+ closed = True
221
+ break
222
+ if not line.strip() or line.lstrip().startswith("#"):
223
+ continue
224
+ match = re.fullmatch(r"([a-z_]+):\s*([^#]*?)(?:\s+#.*)?", line)
225
+ if match is None:
226
+ raise ValueError(f"{label} has malformed frontmatter: {line}")
227
+ key, value = match.groups()
228
+ if key in values:
229
+ raise ValueError(f"{label} repeats field: {key}")
230
+ values[key] = value.strip().strip("\"'")
231
+ if not closed:
232
+ raise ValueError(f"{label} frontmatter is not closed")
233
+ return values
234
+
235
+
236
+ def strict_ship_state(state, archive):
237
+ fields = frozenset(state)
238
+ if fields not in {
239
+ frozenset(LEGACY_STATE_FIELDS),
240
+ frozenset(INTEGRATION_STATE_FIELDS),
241
+ frozenset(PROVENANCE_STATE_FIELDS),
242
+ }:
243
+ return False
244
+ if fields != frozenset(LEGACY_STATE_FIELDS):
245
+ if state.get("integration_default") not in INTEGRATION_MODES:
246
+ return False
247
+ if state.get("integration") not in INTEGRATION_MODES:
248
+ return False
249
+ if fields == frozenset(PROVENANCE_STATE_FIELDS):
250
+ source = state.get("integration_source")
251
+ if source not in INTEGRATION_SOURCES:
252
+ return False
253
+ if source == "default" and state.get("integration") != state.get(
254
+ "integration_default"
255
+ ):
256
+ return False
257
+ archive_match = ARCHIVE_NAME.fullmatch(archive or "")
258
+ branch_match = BOUND_BRANCH.fullmatch(state.get("branch", ""))
259
+ if archive_match is None or branch_match is None:
260
+ return False
261
+ return (
262
+ state.get("pipeline") == "gsd-path/v2"
263
+ and bool(SLUG.fullmatch(state.get("project", "")))
264
+ and state.get("milestone") == archive_match.group("slug")
265
+ and state.get("phase") == "shipped"
266
+ and state.get("status") == "done"
267
+ and state.get("archive") == archive
268
+ and int(branch_match.group("number")) == int(archive_match.group("number"))
269
+ )
270
+
271
+
272
+ def ship_contract_violations(entries, subject, body, new_archives, state):
273
+ if not is_ship_commit(subject):
274
+ return []
275
+ found = []
276
+ if len(new_archives) != 1:
277
+ return ["a ship commit requires exactly one new current archive"]
278
+ archive = next(iter(new_archives))
279
+ match = ARCHIVE_NAME.fullmatch(archive)
280
+ if match is None or state is None or not strict_ship_state(state, archive):
281
+ found.append("ship commit staged STATE.md is not the strict shipped transaction")
282
+ return found
283
+ expected_subject = f"ship: M{int(match.group('number')):03d} — {match.group('slug')}"
284
+ if subject != expected_subject:
285
+ found.append(f"ship commit subject must be {expected_subject!r}")
286
+ if current_branch() != state["branch"]:
287
+ found.append("ship commit must run on staged STATE.branch")
288
+ reviewed_head = subprocess.run(
289
+ ["git", "rev-parse", "--verify", "HEAD"],
290
+ capture_output=True,
291
+ text=True,
292
+ check=True,
293
+ ).stdout.strip()
294
+ expected_body = f"Archive: {archive}\nReviewed-HEAD: {reviewed_head}"
295
+ if body != expected_body:
296
+ found.append("ship commit body does not match Archive and Reviewed-HEAD")
297
+ changed_paths = {
298
+ new if code in "CR" else old
299
+ for code, old, new in entries
300
+ }
301
+ if ".project/STATE.md" not in changed_paths:
302
+ found.append("ship commit must stage .project/STATE.md")
303
+ if f"{archive}/MANIFEST.md" not in changed_paths:
304
+ found.append("ship commit must add the current archive MANIFEST.md")
305
+ return found
306
+
307
+
308
+ def staged_archive_target(values=None):
309
+ values = staged_frontmatter() if values is None else values
310
+ if "archive" not in values:
311
+ raise ValueError("staged STATE.md must contain exactly one archive field")
312
+ value = values["archive"]
313
+ if value in {"", "null"}:
314
+ return None
315
+ root = archive_root(value)
316
+ if root != value.rstrip("/"):
317
+ raise ValueError(f"staged STATE.md archive is invalid: {value}")
318
+ return root
319
+
320
+
321
+ def staged_abandon_target(new_archives, state):
322
+ """Return archive, slug, and reason for a complete abandon commit."""
323
+ if len(new_archives) != 1:
324
+ return None
325
+ target = next(iter(new_archives))
326
+ archive_match = ARCHIVE_NAME.fullmatch(target)
327
+ if archive_match is None:
328
+ return None
329
+
330
+ required_state = {
331
+ "pipeline": "gsd-path/v2",
332
+ "milestone": "null",
333
+ "phase": "roadmap",
334
+ "status": "active",
335
+ "archive": "null",
336
+ }
337
+ if any(state.get(key) != value for key, value in required_state.items()):
338
+ return None
339
+
340
+ sections = []
341
+ lines = staged_file(".project/ROADMAP.md").splitlines()
342
+ for index, line in enumerate(lines):
343
+ heading = ROADMAP_HEADING.fullmatch(line)
344
+ if heading is None:
345
+ continue
346
+ end = next(
347
+ (
348
+ cursor
349
+ for cursor in range(index + 1, len(lines))
350
+ if ROADMAP_HEADING.fullmatch(lines[cursor]) is not None
351
+ ),
352
+ len(lines),
353
+ )
354
+ section = lines[index:end]
355
+ if (
356
+ section.count("Status: abandoned") == 1
357
+ and section.count(f"Archive: {target}")
358
+ + section.count(f"Archive: {target}/")
359
+ == 1
360
+ ):
361
+ sections.append(heading)
362
+ if len(sections) != 1:
363
+ return None
364
+
365
+ heading = sections[0]
366
+ slug = heading.group("slug")
367
+ branch = f"gsd-path/{heading.group('milestone')}"
368
+ if (
369
+ slug != archive_match.group("slug")
370
+ or state.get("branch") != branch
371
+ or current_branch() != branch
372
+ ):
373
+ return None
374
+ reasons = [
375
+ line[len("Reason:") :].strip()
376
+ for line in staged_file(f"{target}/MANIFEST.md").splitlines()
377
+ if line.startswith("Reason:")
378
+ ]
379
+ if len(reasons) != 1 or not reasons[0]:
380
+ return None
381
+ reason = " ".join(reasons[0].split())
382
+ return target, slug, reason
383
+
384
+
385
+ @lru_cache(maxsize=None)
386
+ def current_branch():
387
+ return subprocess.run(
388
+ ["git", "branch", "--show-current"],
389
+ capture_output=True,
390
+ text=True,
391
+ check=True,
392
+ ).stdout.strip()
393
+
394
+
395
+ def closed_milestone_reason():
396
+ branch = current_branch()
397
+ if BOUND_BRANCH.fullmatch(branch) is None:
398
+ return None
399
+ commits = subprocess.run(
400
+ ["git", "log", "--format=%H", "--grep=^ship:", "HEAD"],
401
+ capture_output=True, text=True, check=True,
402
+ ).stdout.splitlines()
403
+ if any(ship_commit_at(sha, branch) for sha in commits):
404
+ return (f"closed milestone {branch} accepts no new work; finish integration "
405
+ "through ship, then use the router's next-milestone handoff")
406
+ return None
407
+
408
+
409
+ def head_descends_from(commit):
410
+ return (
411
+ subprocess.run(
412
+ ["git", "merge-base", "--is-ancestor", commit, "HEAD"],
413
+ capture_output=True,
414
+ text=True,
415
+ check=False,
416
+ ).returncode
417
+ == 0
418
+ )
419
+
420
+
421
+ def task_landed_at_head(task_file):
422
+ content = shown_file(f"HEAD:{task_file}")
423
+ fields, _ = task_frontmatter(content) if content is not None else (None, None)
424
+ return bool(fields) and fields.get("status") == "done"
425
+
426
+
427
+ def repo_root():
428
+ return Path(
429
+ subprocess.run(
430
+ ["git", "rev-parse", "--show-toplevel"],
431
+ capture_output=True,
432
+ text=True,
433
+ check=True,
434
+ ).stdout.strip()
435
+ )
436
+
437
+
438
+ def product_commit_violations(entries, subject, body):
439
+ """Hold product changes until build landing or verified integration."""
440
+ if is_ship_commit(subject):
441
+ return [] # ship commits are held to .project/ by ship_contract_violations
442
+ staged = sorted({path for _, old, new in entries for path in (old, new) if path})
443
+ if all(path.startswith(BOOKKEEPING_PREFIXES) for path in staged):
444
+ return []
445
+ # A commit that enters build must stay .project/-only, so the staged STATE
446
+ # counts as much as the committed one.
447
+ state = staged_frontmatter() if ".project/STATE.md" in staged else head_frontmatter()
448
+ if state is None:
449
+ return []
450
+ if shipped(state):
451
+ if is_integration_merge(subject):
452
+ return []
453
+ completion = _completion_status(repo_root(), state, ".project")
454
+ if completion["status"] == "verified":
455
+ return []
456
+ return [f"milestone integration is unverified: {completion.get('reason')}; {PRODUCT_HINT}"]
457
+ phase = state.get("phase")
458
+ if subject is None and phase == "build":
459
+ return [] # pre-commit has no message; commit-msg checks the landing shape
460
+ bound = unshipped_bound_branch(state)
461
+ if bound is None:
462
+ return []
463
+ if phase != "build":
464
+ return [f"product files are staged while STATE is {phase}; {PRODUCT_HINT}"]
465
+ if TASK_SUBJECT.fullmatch(subject) is None:
466
+ return [f"{subject!r} is not a landing commit; {LANDING_HINT}"]
467
+ fields = {
468
+ line[:4]: line[6:].strip()
469
+ for line in body.splitlines()
470
+ if line.startswith(("Task: ", "Base: "))
471
+ }
472
+ task_file, base = fields.get("Task", ""), fields.get("Base", "")
473
+ problems = []
474
+ if FULL_SHA.fullmatch(base) is None or not head_descends_from(base):
475
+ problems.append("Base: must be a full SHA that HEAD descends from")
476
+ elif task_file not in staged or not task_file.startswith(".project/tasks/"):
477
+ problems.append("the Task: file must be a staged .project/tasks/ file")
478
+ elif task_landed_at_head(task_file):
479
+ problems.append("the Task: file is already done at HEAD; a task lands exactly once")
480
+ else:
481
+ # The same proof isolation.py land and recover apply: subject from the
482
+ # base task, allow-list from its files:, and the landed-state transition.
483
+ contract_paths, error = _landing_state(
484
+ repo_root(), base, task_file, staged_file(task_file), subject, None
485
+ )
486
+ if error:
487
+ problems.append(error)
488
+ if contract_paths is not None:
489
+ stray = sorted(set(staged) - contract_paths)
490
+ if stray:
491
+ problems.append("undeclared paths: " + ", ".join(stray))
492
+ if body != task_commit_body(task_file, staged, base).strip():
493
+ problems.append("body must be exactly the Task:/Base:/Files: block for the staged paths")
494
+ if problems:
495
+ return [f"{subject!r} is not a valid landing commit ({'; '.join(problems)}); {LANDING_HINT}"]
496
+ return []
497
+
498
+
499
+ @lru_cache(maxsize=None)
500
+ def ship_commit_at(sha, bound):
501
+ """True when the commit is the strict ship commit its own STATE.md names."""
502
+ state = state_at(sha)
503
+ if state is None or state.get("branch") != bound:
504
+ return False
505
+ shown = subprocess.run(
506
+ ["git", "show", "--no-patch", "--format=%s", sha],
507
+ capture_output=True,
508
+ text=True,
509
+ check=True,
510
+ )
511
+ archive = state.get("archive") or ""
512
+ return strict_ship_state(state, archive) and is_ship_subject(
513
+ shown.stdout.strip(), archive.rsplit("/", 1)[-1]
514
+ )
515
+
516
+
517
+ def pre_push_violations(lines):
518
+ """Refuse ref updates that publish unshipped milestone work.
519
+
520
+ A bound-named ref moves only to, or away from, its strict ship commit. Any
521
+ other ref may not carry a commit whose STATE.md still owes a ship commit.
522
+ """
523
+ found = []
524
+ for line in lines:
525
+ parts = line.split()
526
+ if len(parts) != 4:
527
+ raise ValueError(f"unexpected pre-push line: {line!r}")
528
+ local_ref, local_sha, remote_ref, remote_sha = parts
529
+ if any(re.fullmatch(r"[0-9a-fA-F]{40}", sha) is None for sha in (local_sha, remote_sha)):
530
+ raise ValueError(f"unexpected pre-push SHA: {line!r}")
531
+ names = [ref.removeprefix("refs/heads/") for ref in (local_ref, remote_ref)]
532
+ bounds = {name for name in names if BOUND_BRANCH.fullmatch(name)}
533
+ if bounds:
534
+ # A deletion is judged by the commit it removes; an absent ref proves nothing.
535
+ sha = remote_sha if local_sha == NULL_SHA else local_sha
536
+ for bound in sorted(bounds):
537
+ if sha != NULL_SHA and not ship_commit_at(sha, bound):
538
+ found.append(
539
+ f"{remote_ref} <- {sha[:12]} moves {bound} off its ship commit; "
540
+ f"{PUBLICATION_HINT}"
541
+ )
542
+ continue
543
+ if local_sha == NULL_SHA:
544
+ continue
545
+ state = state_at(local_sha)
546
+ owed = unshipped_bound_branch(state) if state is not None else None
547
+ if owed is not None:
548
+ found.append(
549
+ f"{remote_ref} <- {local_sha[:12]} carries unshipped {owed} work "
550
+ f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}"
551
+ )
552
+ return found
553
+
554
+
555
+ def is_ship_commit(subject):
556
+ return bool(subject and subject.strip().casefold().startswith("ship:"))
557
+
558
+
559
+ def is_abandon_commit(subject, abandon):
560
+ match = ABANDON_SUBJECT.fullmatch(subject or "")
561
+ return bool(match and abandon and match.group("slug") == abandon[1])
562
+
563
+
564
+ def abandon_contract_violations(subject, body, abandon):
565
+ if not is_abandon_commit(subject, abandon):
566
+ return []
567
+ expected = f"Why: {abandon[2]}"
568
+ if body != expected:
569
+ return ["abandon commit body does not match the archived MANIFEST Reason"]
570
+ return []
571
+
572
+
573
+ def default_branch():
574
+ origin_head = subprocess.run(
575
+ ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
576
+ capture_output=True,
577
+ text=True,
578
+ check=False,
579
+ )
580
+ if origin_head.returncode == 0 and origin_head.stdout.strip().startswith("origin/"):
581
+ return origin_head.stdout.strip()[len("origin/") :]
582
+ configured = subprocess.run(
583
+ ["git", "config", "--get", "init.defaultBranch"],
584
+ capture_output=True,
585
+ text=True,
586
+ check=False,
587
+ )
588
+ if configured.returncode == 0 and configured.stdout.strip():
589
+ return configured.stdout.strip()
590
+ return "main"
591
+
592
+
593
+ def is_integration_merge(subject):
594
+ match = INTEGRATE_SUBJECT.fullmatch(subject or "")
595
+ if not match or match.group("branch") != f"gsd-path/{match.group('milestone')}":
596
+ return False
597
+ target = default_branch()
598
+ if match.group("target") != target:
599
+ return False
600
+ branch = current_branch()
601
+ milestone = match.group("milestone")
602
+ if branch not in {target, f"gsd-path-integrate/{milestone}"}:
603
+ return False
604
+ merge_head = subprocess.run(
605
+ ["git", "rev-parse", "--verify", "--quiet", "MERGE_HEAD^{commit}"],
606
+ capture_output=True,
607
+ text=True,
608
+ check=False,
609
+ )
610
+ bound_head = subprocess.run(
611
+ [
612
+ "git",
613
+ "rev-parse",
614
+ "--verify",
615
+ "--quiet",
616
+ f"refs/heads/{match.group('branch')}^{{commit}}",
617
+ ],
618
+ capture_output=True,
619
+ text=True,
620
+ check=False,
621
+ )
622
+ return (
623
+ merge_head.returncode == 0
624
+ and bound_head.returncode == 0
625
+ and merge_head.stdout.strip() == bound_head.stdout.strip()
626
+ )
627
+
628
+
629
+ def violations(
630
+ entries,
631
+ subject,
632
+ existing_archives=(),
633
+ current_archive=None,
634
+ integration_merge=False,
635
+ abandon_commit=False,
636
+ ):
637
+ found = []
638
+ existing = set(existing_archives)
639
+ for code, old, new in entries:
640
+ if code in "MDTR" and old.startswith(ARCHIVE_PREFIX):
641
+ label = f"{code} {old}" + (f" -> {new}" if new else "")
642
+ found.append(
643
+ f"{label}: committed archives are read-only; "
644
+ f"unstage it with git restore --staged -- {new or old}"
645
+ )
646
+ for entry in entries:
647
+ destination = destination_path(entry)
648
+ root = archive_root(destination or "")
649
+ if root is None:
650
+ continue
651
+ code, old, new = entry
652
+ label = f"{code} {old}" + (f" -> {new}" if new else "")
653
+ if root in existing:
654
+ found.append(
655
+ f"{label}: committed archives are read-only; "
656
+ f"unstage it with git restore --staged -- {destination}"
657
+ )
658
+ elif root != current_archive:
659
+ found.append(
660
+ f"{label}: only the current STATE.archive "
661
+ f"({current_archive or 'unset'}) may be created; "
662
+ f"unstage it with git restore --staged -- {destination}"
663
+ )
664
+ elif (
665
+ subject is not None
666
+ and not is_ship_commit(subject)
667
+ and not integration_merge
668
+ and not abandon_commit
669
+ ):
670
+ milestone, target = root[len(ARCHIVE_PREFIX):], default_branch()
671
+ found.append(
672
+ f"{label}: a new archive requires a ship commit, a "
673
+ "milestone-abandon commit, or an integration merge with the "
674
+ f"subject 'integrate: {milestone} — merge "
675
+ f"gsd-path/{milestone} into {target}'"
676
+ )
677
+ if is_ship_commit(subject):
678
+ for code, old, new in entries:
679
+ for path in (old, new):
680
+ if path and not path.startswith(".project/"):
681
+ found.append(
682
+ f"{code} {path}: a ship commit may only touch .project/; "
683
+ f"unstage it with git restore --staged -- {path}"
684
+ )
685
+ return found
686
+
687
+
688
+ def commit_message(argv):
689
+ if len(argv) <= 1:
690
+ return "", ""
691
+ if argv[1] == "pre-commit":
692
+ # At pre-commit time .git/COMMIT_EDITMSG still holds the PREVIOUS
693
+ # commit's message, so subject rules cannot be enforced here; the
694
+ # commit-msg hook enforces them with the real message.
695
+ return None, None
696
+ if argv[1] == "commit-msg" and len(argv) > 2:
697
+ return message_of(argv[2])
698
+ return message_of(argv[1])
699
+
700
+
701
+ def report(found, action):
702
+ if not found:
703
+ return 0
704
+ print(f"gsd-path guard blocked the {action}:", file=sys.stderr)
705
+ for violation in found:
706
+ print(f" {violation}", file=sys.stderr)
707
+ return 1
708
+
709
+
710
+ def main(argv):
711
+ if len(argv) > 1 and argv[1] == "closed-milestone":
712
+ reason = closed_milestone_reason()
713
+ return report([reason] if reason else [], "mutation")
714
+ if len(argv) > 1 and argv[1] == "pre-push":
715
+ try:
716
+ found = pre_push_violations(sys.stdin.read().splitlines())
717
+ except Exception as error:
718
+ print(f"gsd-path guard: inspection failed; push blocked ({error})", file=sys.stderr)
719
+ return 1
720
+ return report(found, "push")
721
+ try:
722
+ reason = closed_milestone_reason()
723
+ if reason:
724
+ return report([reason], "commit")
725
+ entries = staged_entries()
726
+ subject, body = commit_message(argv)
727
+ existing = committed_archive_roots()
728
+ destinations = {
729
+ root
730
+ for entry in entries
731
+ if (root := archive_root(destination_path(entry) or "")) is not None
732
+ }
733
+ new_archives = destinations - existing
734
+ state = staged_frontmatter() if new_archives else None
735
+ current_archive = staged_archive_target(state) if new_archives else None
736
+ abandon = (
737
+ staged_abandon_target(new_archives, state)
738
+ if new_archives and current_archive is None
739
+ else None
740
+ )
741
+ if abandon is not None:
742
+ current_archive = abandon[0]
743
+ landing = product_commit_violations(entries, subject, body)
744
+ except Exception as error:
745
+ print(
746
+ f"gsd-path guard: inspection failed; commit blocked ({error}); "
747
+ "run git from inside the repository worktree, then retry the commit",
748
+ file=sys.stderr,
749
+ )
750
+ return 1
751
+ integration_merge = is_integration_merge(subject) if new_archives else False
752
+ abandon_commit = is_abandon_commit(subject, abandon) if new_archives else False
753
+ found = violations(
754
+ entries,
755
+ subject,
756
+ existing,
757
+ current_archive,
758
+ integration_merge,
759
+ abandon_commit,
760
+ )
761
+ found.extend(ship_contract_violations(entries, subject, body, new_archives, state))
762
+ found.extend(abandon_contract_violations(subject, body, abandon))
763
+ found.extend(landing)
764
+ return report(found, "commit")
765
+
766
+
767
+ if __name__ == "__main__":
768
+ raise SystemExit(main(sys.argv))