@opengsd/gsd-path 1.0.0 → 1.1.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 (395) hide show
  1. package/AGENTS.md +28 -11
  2. package/DOCS.md +6 -5
  3. package/FULL.md +9 -13
  4. package/QUICK.md +3 -3
  5. package/README.md +151 -14
  6. package/RUNTIME.md +7 -1
  7. package/WORKFLOW.md +30 -14
  8. package/package.json +15 -1
  9. package/platforms/shared-agents/dispatch.md +6 -0
  10. package/scripts/archive_milestone.py +10 -118
  11. package/scripts/bootstrap_repository.py +25 -3
  12. package/scripts/build_recovery.py +287 -0
  13. package/scripts/check_docs_audit.py +5 -3
  14. package/scripts/check_handoffs.py +1 -1
  15. package/scripts/detect_project.py +11 -2
  16. package/scripts/dispatch_driver.py +4 -2
  17. package/scripts/git_guard.py +2 -1
  18. package/scripts/install.mjs +2 -0
  19. package/scripts/install.py +2 -0
  20. package/scripts/integration.py +28 -22
  21. package/scripts/isolation.py +9 -1
  22. package/scripts/lean_verification.py +1 -1
  23. package/scripts/pipeline_diagnose.py +12 -3
  24. package/scripts/pipeline_state.py +114 -186
  25. package/scripts/pipeline_undo.py +11 -7
  26. package/scripts/promote_lookahead.py +30 -162
  27. package/scripts/roadmap.py +321 -0
  28. package/scripts/skill-resources.json +79 -1
  29. package/scripts/state_checkpoint.py +109 -72
  30. package/scripts/state_promote.py +42 -44
  31. package/scripts/task_context.py +73 -0
  32. package/scripts/token_budget.py +40 -1
  33. package/scripts/workflow_run.py +116 -7
  34. package/skills/gsd-path/BUILD.md +47 -575
  35. package/skills/gsd-path/DECIDE.md +2 -6
  36. package/skills/gsd-path/DEFINE.md +10 -14
  37. package/skills/gsd-path/INSPECT.md +30 -8
  38. package/skills/gsd-path/PLAN.md +25 -17
  39. package/skills/gsd-path/RESEARCH.md +3 -6
  40. package/skills/gsd-path/ROADMAP.md +22 -58
  41. package/skills/gsd-path/SHIP.md +5 -4
  42. package/skills/gsd-path/SKILL.md +24 -14
  43. package/skills/gsd-path/references/build-abandon.md +33 -0
  44. package/skills/gsd-path/references/build-native.md +494 -0
  45. package/skills/gsd-path/references/build-recovery.md +78 -0
  46. package/skills/gsd-path/references/coder.md +8 -3
  47. package/skills/gsd-path/references/dispatch.md +6 -0
  48. package/skills/gsd-path/references/docs-auditor.md +14 -0
  49. package/skills/gsd-path/references/reviewer-final.md +21 -0
  50. package/skills/gsd-path/references/reviewer-panel.md +17 -0
  51. package/skills/gsd-path/references/reviewer-skeptic.md +20 -0
  52. package/skills/gsd-path/references/reviewer-wave.md +89 -0
  53. package/skills/gsd-path/references/reviewer.md +12 -150
  54. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  55. package/skills/gsd-path/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  56. package/skills/gsd-path/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  57. package/skills/gsd-path/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  58. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  59. package/skills/gsd-path/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  60. package/skills/gsd-path/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  61. package/skills/gsd-path/scripts/__pycache__/discussion_records.cpython-312.pyc +0 -0
  62. package/skills/gsd-path/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  63. package/skills/gsd-path/scripts/__pycache__/integration.cpython-312.pyc +0 -0
  64. package/skills/gsd-path/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  65. package/skills/gsd-path/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
  66. package/skills/gsd-path/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  67. package/skills/gsd-path/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  68. package/skills/gsd-path/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
  69. package/skills/gsd-path/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  70. package/skills/gsd-path/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  71. package/skills/gsd-path/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  72. package/skills/gsd-path/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
  73. package/skills/gsd-path/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
  74. package/skills/gsd-path/scripts/archive_milestone.py +10 -118
  75. package/skills/gsd-path/scripts/bootstrap_repository.py +25 -3
  76. package/skills/gsd-path/scripts/build_recovery.py +287 -0
  77. package/skills/gsd-path/scripts/check_docs_audit.py +5 -3
  78. package/skills/gsd-path/scripts/check_handoffs.py +1 -1
  79. package/skills/gsd-path/scripts/detect_project.py +11 -2
  80. package/skills/gsd-path/scripts/dispatch_driver.py +4 -2
  81. package/skills/gsd-path/scripts/integration.py +28 -22
  82. package/skills/gsd-path/scripts/isolation.py +9 -1
  83. package/skills/gsd-path/scripts/lean_verification.py +1 -1
  84. package/skills/gsd-path/scripts/pipeline_state.py +114 -186
  85. package/skills/gsd-path/scripts/promote_lookahead.py +30 -162
  86. package/skills/gsd-path/scripts/roadmap.py +321 -0
  87. package/skills/gsd-path/scripts/state_checkpoint.py +109 -72
  88. package/skills/gsd-path/scripts/state_promote.py +42 -44
  89. package/skills/gsd-path/scripts/task_context.py +73 -0
  90. package/skills/gsd-path/scripts/token_budget.py +40 -1
  91. package/skills/gsd-path/scripts/workflow_run.py +116 -7
  92. package/skills/gsd-path/templates/wave-review.md +1 -1
  93. package/skills/gsd-path-build/SKILL.md +47 -575
  94. package/skills/gsd-path-build/references/build-abandon.md +33 -0
  95. package/skills/gsd-path-build/references/build-native.md +494 -0
  96. package/skills/gsd-path-build/references/build-recovery.md +78 -0
  97. package/skills/gsd-path-build/references/coder.md +8 -3
  98. package/skills/gsd-path-build/references/dispatch.md +6 -0
  99. package/skills/gsd-path-build/references/reviewer-final.md +21 -0
  100. package/skills/gsd-path-build/references/reviewer-panel.md +17 -0
  101. package/skills/gsd-path-build/references/reviewer-skeptic.md +20 -0
  102. package/skills/gsd-path-build/references/reviewer-wave.md +89 -0
  103. package/skills/gsd-path-build/references/reviewer.md +12 -150
  104. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  105. package/skills/gsd-path-build/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  106. package/skills/gsd-path-build/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  107. package/skills/gsd-path-build/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  108. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  109. package/skills/gsd-path-build/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  110. package/skills/gsd-path-build/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  111. package/skills/gsd-path-build/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  112. package/skills/gsd-path-build/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  113. package/skills/gsd-path-build/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
  114. package/skills/gsd-path-build/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  115. package/skills/gsd-path-build/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  116. package/skills/gsd-path-build/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
  117. package/skills/gsd-path-build/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  118. package/skills/gsd-path-build/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  119. package/skills/gsd-path-build/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  120. package/skills/gsd-path-build/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
  121. package/skills/gsd-path-build/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
  122. package/skills/gsd-path-build/scripts/archive_milestone.py +10 -118
  123. package/skills/gsd-path-build/scripts/build_recovery.py +287 -0
  124. package/skills/gsd-path-build/scripts/check_handoffs.py +1 -1
  125. package/skills/gsd-path-build/scripts/detect_project.py +1526 -0
  126. package/skills/gsd-path-build/scripts/dispatch_driver.py +4 -2
  127. package/skills/gsd-path-build/scripts/integration.py +28 -22
  128. package/skills/gsd-path-build/scripts/isolation.py +9 -1
  129. package/skills/gsd-path-build/scripts/pipeline_state.py +114 -186
  130. package/skills/gsd-path-build/scripts/promote_lookahead.py +283 -0
  131. package/skills/gsd-path-build/scripts/roadmap.py +321 -0
  132. package/skills/gsd-path-build/scripts/state_checkpoint.py +109 -72
  133. package/skills/gsd-path-build/scripts/state_promote.py +42 -44
  134. package/skills/gsd-path-build/scripts/task_context.py +73 -0
  135. package/skills/gsd-path-build/scripts/token_budget.py +40 -1
  136. package/skills/gsd-path-build/scripts/workflow_run.py +116 -7
  137. package/skills/gsd-path-build/templates/wave-review.md +1 -1
  138. package/skills/gsd-path-decide/SKILL.md +2 -6
  139. package/skills/gsd-path-decide/references/dispatch.md +6 -0
  140. package/skills/gsd-path-decide/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  141. package/skills/gsd-path-decide/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  142. package/skills/gsd-path-decide/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  143. package/skills/gsd-path-decide/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  144. package/skills/gsd-path-decide/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  145. package/skills/gsd-path-decide/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  146. package/skills/gsd-path-decide/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  147. package/skills/gsd-path-decide/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  148. package/skills/gsd-path-decide/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  149. package/skills/gsd-path-decide/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  150. package/skills/gsd-path-decide/scripts/archive_milestone.py +2157 -0
  151. package/skills/gsd-path-decide/scripts/build_recovery.py +287 -0
  152. package/skills/gsd-path-decide/scripts/check_handoffs.py +1 -1
  153. package/skills/gsd-path-decide/scripts/detect_project.py +1526 -0
  154. package/skills/gsd-path-decide/scripts/discussion_validate.py +1321 -0
  155. package/skills/gsd-path-decide/scripts/integration.py +1439 -0
  156. package/skills/gsd-path-decide/scripts/isolation.py +9 -1
  157. package/skills/gsd-path-decide/scripts/pipeline_state.py +114 -186
  158. package/skills/gsd-path-decide/scripts/promote_lookahead.py +283 -0
  159. package/skills/gsd-path-decide/scripts/review_panel.py +553 -0
  160. package/skills/gsd-path-decide/scripts/roadmap.py +321 -0
  161. package/skills/gsd-path-decide/scripts/state_checkpoint.py +109 -72
  162. package/skills/gsd-path-decide/scripts/state_promote.py +42 -44
  163. package/skills/gsd-path-define/SKILL.md +10 -14
  164. package/skills/gsd-path-define/references/build-recovery.md +78 -0
  165. package/skills/gsd-path-define/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  166. package/skills/gsd-path-define/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  167. package/skills/gsd-path-define/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  168. package/skills/gsd-path-define/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  169. package/skills/gsd-path-define/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  170. package/skills/gsd-path-define/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  171. package/skills/gsd-path-define/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  172. package/skills/gsd-path-define/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  173. package/skills/gsd-path-define/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  174. package/skills/gsd-path-define/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  175. package/skills/gsd-path-define/scripts/archive_milestone.py +2157 -0
  176. package/skills/gsd-path-define/scripts/build_recovery.py +287 -0
  177. package/skills/gsd-path-define/scripts/check_handoffs.py +1 -1
  178. package/skills/gsd-path-define/scripts/detect_project.py +11 -2
  179. package/skills/gsd-path-define/scripts/discussion_validate.py +1321 -0
  180. package/skills/gsd-path-define/scripts/integration.py +1439 -0
  181. package/skills/gsd-path-define/scripts/isolation.py +9 -1
  182. package/skills/gsd-path-define/scripts/pipeline_state.py +114 -186
  183. package/skills/gsd-path-define/scripts/promote_lookahead.py +283 -0
  184. package/skills/gsd-path-define/scripts/review_panel.py +553 -0
  185. package/skills/gsd-path-define/scripts/roadmap.py +321 -0
  186. package/skills/gsd-path-define/scripts/state_checkpoint.py +109 -72
  187. package/skills/gsd-path-define/scripts/state_promote.py +42 -44
  188. package/skills/gsd-path-docs-audit/references/dispatch.md +6 -0
  189. package/skills/gsd-path-docs-audit/references/docs-auditor.md +14 -0
  190. package/skills/gsd-path-docs-audit/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  191. package/skills/gsd-path-docs-audit/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  192. package/skills/gsd-path-docs-audit/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  193. package/skills/gsd-path-docs-audit/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  194. package/skills/gsd-path-docs-audit/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  195. package/skills/gsd-path-docs-audit/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  196. package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  197. package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  198. package/skills/gsd-path-docs-audit/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  199. package/skills/gsd-path-docs-audit/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  200. package/skills/gsd-path-docs-audit/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  201. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +2157 -0
  202. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +287 -0
  203. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +5 -3
  204. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +1 -1
  205. package/skills/gsd-path-docs-audit/scripts/detect_project.py +1526 -0
  206. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +1321 -0
  207. package/skills/gsd-path-docs-audit/scripts/integration.py +1439 -0
  208. package/skills/gsd-path-docs-audit/scripts/isolation.py +9 -1
  209. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +114 -186
  210. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +283 -0
  211. package/skills/gsd-path-docs-audit/scripts/review_panel.py +553 -0
  212. package/skills/gsd-path-docs-audit/scripts/roadmap.py +321 -0
  213. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +109 -72
  214. package/skills/gsd-path-docs-audit/scripts/state_promote.py +42 -44
  215. package/skills/gsd-path-forensics/scripts/__pycache__/pipeline_undo.cpython-312.pyc +0 -0
  216. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +12 -3
  217. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +11 -7
  218. package/skills/gsd-path-inspect/SKILL.md +30 -8
  219. package/skills/gsd-path-inspect/references/dispatch.md +6 -0
  220. package/skills/gsd-path-inspect/references/docs-auditor.md +14 -0
  221. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  222. package/skills/gsd-path-inspect/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  223. package/skills/gsd-path-inspect/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  224. package/skills/gsd-path-inspect/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  225. package/skills/gsd-path-inspect/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  226. package/skills/gsd-path-inspect/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  227. package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  228. package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  229. package/skills/gsd-path-inspect/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  230. package/skills/gsd-path-inspect/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  231. package/skills/gsd-path-inspect/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  232. package/skills/gsd-path-inspect/scripts/archive_milestone.py +2157 -0
  233. package/skills/gsd-path-inspect/scripts/build_recovery.py +287 -0
  234. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +5 -3
  235. package/skills/gsd-path-inspect/scripts/check_handoffs.py +1 -1
  236. package/skills/gsd-path-inspect/scripts/detect_project.py +11 -2
  237. package/skills/gsd-path-inspect/scripts/discussion_validate.py +1321 -0
  238. package/skills/gsd-path-inspect/scripts/integration.py +1439 -0
  239. package/skills/gsd-path-inspect/scripts/isolation.py +9 -1
  240. package/skills/gsd-path-inspect/scripts/pipeline_state.py +114 -186
  241. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +283 -0
  242. package/skills/gsd-path-inspect/scripts/review_panel.py +553 -0
  243. package/skills/gsd-path-inspect/scripts/roadmap.py +321 -0
  244. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +109 -72
  245. package/skills/gsd-path-inspect/scripts/state_promote.py +42 -44
  246. package/skills/gsd-path-inspect/scripts/workflow_run.py +226 -0
  247. package/skills/gsd-path-plan/SKILL.md +25 -17
  248. package/skills/gsd-path-plan/references/build-recovery.md +78 -0
  249. package/skills/gsd-path-plan/references/dispatch.md +6 -0
  250. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  251. package/skills/gsd-path-plan/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  252. package/skills/gsd-path-plan/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  253. package/skills/gsd-path-plan/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  254. package/skills/gsd-path-plan/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  255. package/skills/gsd-path-plan/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  256. package/skills/gsd-path-plan/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  257. package/skills/gsd-path-plan/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  258. package/skills/gsd-path-plan/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  259. package/skills/gsd-path-plan/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  260. package/skills/gsd-path-plan/scripts/archive_milestone.py +2157 -0
  261. package/skills/gsd-path-plan/scripts/build_recovery.py +287 -0
  262. package/skills/gsd-path-plan/scripts/check_handoffs.py +1 -1
  263. package/skills/gsd-path-plan/scripts/detect_project.py +1526 -0
  264. package/skills/gsd-path-plan/scripts/discussion_validate.py +1321 -0
  265. package/skills/gsd-path-plan/scripts/integration.py +1439 -0
  266. package/skills/gsd-path-plan/scripts/isolation.py +9 -1
  267. package/skills/gsd-path-plan/scripts/pipeline_state.py +114 -186
  268. package/skills/gsd-path-plan/scripts/promote_lookahead.py +283 -0
  269. package/skills/gsd-path-plan/scripts/roadmap.py +321 -0
  270. package/skills/gsd-path-plan/scripts/state_checkpoint.py +109 -72
  271. package/skills/gsd-path-plan/scripts/state_promote.py +42 -44
  272. package/skills/gsd-path-plan/scripts/workflow_run.py +116 -7
  273. package/skills/gsd-path-research/SKILL.md +3 -6
  274. package/skills/gsd-path-research/references/dispatch.md +6 -0
  275. package/skills/gsd-path-research/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  276. package/skills/gsd-path-research/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  277. package/skills/gsd-path-research/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  278. package/skills/gsd-path-research/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  279. package/skills/gsd-path-research/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  280. package/skills/gsd-path-research/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  281. package/skills/gsd-path-research/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  282. package/skills/gsd-path-research/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  283. package/skills/gsd-path-research/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  284. package/skills/gsd-path-research/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  285. package/skills/gsd-path-research/scripts/archive_milestone.py +2157 -0
  286. package/skills/gsd-path-research/scripts/build_recovery.py +287 -0
  287. package/skills/gsd-path-research/scripts/check_handoffs.py +1 -1
  288. package/skills/gsd-path-research/scripts/detect_project.py +1526 -0
  289. package/skills/gsd-path-research/scripts/discussion_validate.py +1321 -0
  290. package/skills/gsd-path-research/scripts/integration.py +1439 -0
  291. package/skills/gsd-path-research/scripts/isolation.py +9 -1
  292. package/skills/gsd-path-research/scripts/pipeline_state.py +114 -186
  293. package/skills/gsd-path-research/scripts/promote_lookahead.py +283 -0
  294. package/skills/gsd-path-research/scripts/review_panel.py +553 -0
  295. package/skills/gsd-path-research/scripts/roadmap.py +321 -0
  296. package/skills/gsd-path-research/scripts/state_checkpoint.py +109 -72
  297. package/skills/gsd-path-research/scripts/state_promote.py +42 -44
  298. package/skills/gsd-path-roadmap/SKILL.md +22 -58
  299. package/skills/gsd-path-roadmap/references/dispatch.md +6 -0
  300. package/skills/gsd-path-roadmap/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  301. package/skills/gsd-path-roadmap/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  302. package/skills/gsd-path-roadmap/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  303. package/skills/gsd-path-roadmap/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  304. package/skills/gsd-path-roadmap/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  305. package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  306. package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  307. package/skills/gsd-path-roadmap/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  308. package/skills/gsd-path-roadmap/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  309. package/skills/gsd-path-roadmap/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  310. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +10 -118
  311. package/skills/gsd-path-roadmap/scripts/build_recovery.py +287 -0
  312. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +1 -1
  313. package/skills/gsd-path-roadmap/scripts/detect_project.py +11 -2
  314. package/skills/gsd-path-roadmap/scripts/integration.py +28 -22
  315. package/skills/gsd-path-roadmap/scripts/isolation.py +9 -1
  316. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +114 -186
  317. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +30 -162
  318. package/skills/gsd-path-roadmap/scripts/roadmap.py +321 -0
  319. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +109 -72
  320. package/skills/gsd-path-roadmap/scripts/state_promote.py +42 -44
  321. package/skills/gsd-path-ship/SKILL.md +5 -4
  322. package/skills/gsd-path-ship/references/dispatch.md +6 -0
  323. package/skills/gsd-path-ship/references/reviewer-final.md +21 -0
  324. package/skills/gsd-path-ship/references/reviewer-panel.md +17 -0
  325. package/skills/gsd-path-ship/references/reviewer-skeptic.md +20 -0
  326. package/skills/gsd-path-ship/references/reviewer-wave.md +89 -0
  327. package/skills/gsd-path-ship/references/reviewer.md +12 -150
  328. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  329. package/skills/gsd-path-ship/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  330. package/skills/gsd-path-ship/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  331. package/skills/gsd-path-ship/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  332. package/skills/gsd-path-ship/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  333. package/skills/gsd-path-ship/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  334. package/skills/gsd-path-ship/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  335. package/skills/gsd-path-ship/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  336. package/skills/gsd-path-ship/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  337. package/skills/gsd-path-ship/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  338. package/skills/gsd-path-ship/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  339. package/skills/gsd-path-ship/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  340. package/skills/gsd-path-ship/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  341. package/skills/gsd-path-ship/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  342. package/skills/gsd-path-ship/scripts/archive_milestone.py +10 -118
  343. package/skills/gsd-path-ship/scripts/build_recovery.py +287 -0
  344. package/skills/gsd-path-ship/scripts/check_handoffs.py +1 -1
  345. package/skills/gsd-path-ship/scripts/detect_project.py +1526 -0
  346. package/skills/gsd-path-ship/scripts/integration.py +28 -22
  347. package/skills/gsd-path-ship/scripts/isolation.py +9 -1
  348. package/skills/gsd-path-ship/scripts/lean_verification.py +1 -1
  349. package/skills/gsd-path-ship/scripts/pipeline_state.py +114 -186
  350. package/skills/gsd-path-ship/scripts/promote_lookahead.py +283 -0
  351. package/skills/gsd-path-ship/scripts/roadmap.py +321 -0
  352. package/skills/gsd-path-ship/scripts/state_checkpoint.py +109 -72
  353. package/skills/gsd-path-ship/scripts/state_promote.py +42 -44
  354. package/skills/gsd-path-ship/scripts/workflow_run.py +116 -7
  355. package/skills/gsd-path-ship/templates/wave-review.md +1 -1
  356. package/skills/gsd-path-undo/scripts/pipeline_undo.py +11 -7
  357. package/skills/path/BUILD.md +47 -575
  358. package/skills/path/DECIDE.md +2 -6
  359. package/skills/path/DEFINE.md +10 -14
  360. package/skills/path/INSPECT.md +30 -8
  361. package/skills/path/PLAN.md +25 -17
  362. package/skills/path/RESEARCH.md +3 -6
  363. package/skills/path/ROADMAP.md +22 -58
  364. package/skills/path/SHIP.md +5 -4
  365. package/skills/path/SKILL.md +24 -14
  366. package/skills/path/references/build-abandon.md +33 -0
  367. package/skills/path/references/build-native.md +494 -0
  368. package/skills/path/references/build-recovery.md +78 -0
  369. package/skills/path/references/coder.md +8 -3
  370. package/skills/path/references/dispatch.md +6 -0
  371. package/skills/path/references/docs-auditor.md +14 -0
  372. package/skills/path/references/reviewer-final.md +21 -0
  373. package/skills/path/references/reviewer-panel.md +17 -0
  374. package/skills/path/references/reviewer-skeptic.md +20 -0
  375. package/skills/path/references/reviewer-wave.md +89 -0
  376. package/skills/path/references/reviewer.md +12 -150
  377. package/skills/path/scripts/archive_milestone.py +10 -118
  378. package/skills/path/scripts/bootstrap_repository.py +25 -3
  379. package/skills/path/scripts/build_recovery.py +287 -0
  380. package/skills/path/scripts/check_docs_audit.py +5 -3
  381. package/skills/path/scripts/check_handoffs.py +1 -1
  382. package/skills/path/scripts/detect_project.py +11 -2
  383. package/skills/path/scripts/dispatch_driver.py +4 -2
  384. package/skills/path/scripts/integration.py +28 -22
  385. package/skills/path/scripts/isolation.py +9 -1
  386. package/skills/path/scripts/lean_verification.py +1 -1
  387. package/skills/path/scripts/pipeline_state.py +114 -186
  388. package/skills/path/scripts/promote_lookahead.py +30 -162
  389. package/skills/path/scripts/roadmap.py +321 -0
  390. package/skills/path/scripts/state_checkpoint.py +109 -72
  391. package/skills/path/scripts/state_promote.py +42 -44
  392. package/skills/path/scripts/task_context.py +73 -0
  393. package/skills/path/scripts/token_budget.py +40 -1
  394. package/skills/path/scripts/workflow_run.py +116 -7
  395. package/skills/path/templates/wave-review.md +1 -1
@@ -0,0 +1,1439 @@
1
+ #!/usr/bin/env python3
2
+ # gsd-path project runtime
3
+ """Own milestone integration, publication, tags, and completion proof."""
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path, PurePosixPath
8
+ from typing import Optional
9
+
10
+ try:
11
+ from pipeline_git import (
12
+ bound_branch_name,
13
+ default_branch_name,
14
+ integrate_commit_body,
15
+ integrate_subject,
16
+ is_bound_branch,
17
+ milestone_id,
18
+ milestone_number,
19
+ ship_subject,
20
+ )
21
+ from pipeline_state import PipelineState
22
+ from isolation import publication_base
23
+ import _common
24
+ except ImportError: # pragma: no cover - package import used by tests
25
+ from scripts.pipeline_git import (
26
+ bound_branch_name,
27
+ default_branch_name,
28
+ integrate_commit_body,
29
+ integrate_subject,
30
+ is_bound_branch,
31
+ milestone_id,
32
+ milestone_number,
33
+ ship_subject,
34
+ )
35
+ from scripts.pipeline_state import PipelineState
36
+ from scripts.isolation import publication_base
37
+ from scripts import _common
38
+
39
+
40
+ if __package__:
41
+ from . import archive_milestone
42
+ from .archive_milestone import ArchiveError
43
+ else:
44
+ import archive_milestone
45
+ from archive_milestone import ArchiveError
46
+
47
+
48
+ PR_CREDIT_LINE = (
49
+ "PR prepared with [GSD Path](https://github.com/open-gsd/gsd-path)."
50
+ )
51
+ GITHUB_HOST = "github.com"
52
+
53
+
54
+ run_git = _common.run_git
55
+ run_command = _common.run_command
56
+
57
+
58
+ def resolve_remote_default(project: Path) -> str:
59
+ result = run_git(project, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
60
+ remote_ref = result.stdout.strip()
61
+ prefix = "refs/remotes/"
62
+ if result.returncode != 0 or not remote_ref.startswith(prefix):
63
+ raise ArchiveError("origin/HEAD is unresolved; fetch before validating integration")
64
+ return remote_ref.removeprefix(prefix)
65
+
66
+
67
+ def live_remote_ref(project: Path, ref: str) -> Optional[str]:
68
+ result = run_git(
69
+ project,
70
+ "ls-remote",
71
+ "--exit-code",
72
+ "--refs",
73
+ "origin",
74
+ ref,
75
+ )
76
+ if result.returncode == 2:
77
+ return None
78
+ if result.returncode != 0:
79
+ raise ArchiveError(f"could not inspect {ref} on origin: git ls-remote failed")
80
+ rows = [line.split("\t", 1) for line in result.stdout.splitlines() if line]
81
+ if len(rows) != 1 or len(rows[0]) != 2 or rows[0][1] != ref:
82
+ raise ArchiveError(f"origin returned ambiguous ref data for {ref}")
83
+ return rows[0][0]
84
+
85
+
86
+ def publishable_bound_branch(project: Path, branch: str, ship_commit: str) -> Optional[str]:
87
+ """The live origin SHA the bound ref may move from: absent, the ship
88
+ commit, or an owner-adopted rebased head proven by its archived receipt."""
89
+ remote_sha = live_remote_ref(project, f"refs/heads/{branch}")
90
+ if remote_sha is None or remote_sha == ship_commit:
91
+ return remote_sha
92
+ try:
93
+ approved_base = publication_base(project, branch, ship_commit)
94
+ except (RuntimeError, OSError) as error:
95
+ raise ArchiveError(f"rebase publication proof failed: {error}") from error
96
+ if remote_sha != approved_base:
97
+ raise ArchiveError(f"origin/{branch} moved or collides: {remote_sha} != {ship_commit}")
98
+ return remote_sha
99
+
100
+
101
+ def publish_bound_branch(project: Path, branch: str, ship_commit: str) -> None:
102
+ remote_ref = f"refs/heads/{branch}"
103
+ remote_sha = publishable_bound_branch(project, branch, ship_commit)
104
+ if remote_sha != ship_commit:
105
+ # An absent ref carries an empty lease; an adopted head leases its exact SHA.
106
+ archive_milestone.require_git_success(
107
+ run_git(
108
+ project,
109
+ "push",
110
+ f"--force-with-lease={remote_ref}:{remote_sha or ''}",
111
+ "origin",
112
+ f"{ship_commit}:{remote_ref}",
113
+ ),
114
+ "publish bound branch under its observed lease",
115
+ )
116
+
117
+ confirmed_sha = live_remote_ref(project, remote_ref)
118
+ if confirmed_sha != ship_commit:
119
+ raise ArchiveError(f"origin/{branch} does not equal the ship commit after publish")
120
+ archive_milestone.require_git_success(
121
+ run_git(
122
+ project,
123
+ "update-ref",
124
+ f"refs/remotes/origin/{branch}",
125
+ ship_commit,
126
+ ),
127
+ "refresh local bound-branch ref",
128
+ )
129
+
130
+
131
+ def refresh_origin(repo: Path) -> dict:
132
+ """Fetch origin, refresh origin/HEAD, and mirror milestone tags."""
133
+ project = repo.resolve()
134
+ archive_milestone.require_git_success(run_git(project, "fetch", "--prune", "origin"), "fetch origin")
135
+ archive_milestone.require_git_success(
136
+ run_git(project, "remote", "set-head", "origin", "--auto"),
137
+ "refresh origin/HEAD",
138
+ )
139
+ mirrored = run_git(
140
+ project,
141
+ "fetch",
142
+ "--prune",
143
+ "origin",
144
+ "refs/tags/milestone/*:refs/remotes/origin/tags/milestone/*",
145
+ )
146
+ if mirrored.returncode != 0:
147
+ detail = (mirrored.stderr or mirrored.stdout).strip()
148
+ if "couldn't find remote ref" not in detail.casefold() and detail:
149
+ raise ArchiveError(f"could not refresh published milestone tags: {detail}")
150
+ remote_default = resolve_remote_default(project)
151
+ return {"remote_default": remote_default}
152
+
153
+
154
+ def optional_ref(project: Path, ref: str) -> Optional[str]:
155
+ result = run_git(project, "rev-parse", "--verify", "--quiet", ref)
156
+ return result.stdout.strip() if result.returncode == 0 else None
157
+
158
+
159
+ def integration_names(project: Path, archive_name: str) -> tuple[str, Path]:
160
+ identifier = milestone_id(milestone_number(archive_name))
161
+ branch = f"gsd-path-integrate/{identifier}"
162
+ worktree = project.parent / f".{project.name}-gsd-path-integrate-{identifier}"
163
+ return branch, worktree
164
+
165
+
166
+ def registered_worktree(project: Path, branch: str) -> Optional[Path]:
167
+ output = archive_milestone.require_git_success(
168
+ run_git(project, "worktree", "list", "--porcelain"),
169
+ "inspect integration worktrees",
170
+ )
171
+ branch_ref = f"branch refs/heads/{branch}"
172
+ matches = []
173
+ for record in output.split("\n\n"):
174
+ lines = record.splitlines()
175
+ if branch_ref not in lines:
176
+ continue
177
+ worktree_lines = [
178
+ line.removeprefix("worktree ")
179
+ for line in lines
180
+ if line.startswith("worktree ")
181
+ ]
182
+ if len(worktree_lines) != 1:
183
+ raise ArchiveError(
184
+ f"integration branch {branch} has a malformed worktree record"
185
+ )
186
+ matches.append(Path(worktree_lines[0]).resolve())
187
+ if len(matches) > 1:
188
+ raise ArchiveError(f"integration branch {branch} is checked out more than once")
189
+ return matches[0] if matches else None
190
+
191
+
192
+ def remove_registered_worktree(project: Path, branch: str, expected_path: Path) -> None:
193
+ registered = registered_worktree(project, branch)
194
+ if registered is None:
195
+ if expected_path.exists() or expected_path.is_symlink():
196
+ raise ArchiveError(f"unregistered integration worktree path exists: {expected_path}")
197
+ return
198
+ if registered != expected_path.resolve():
199
+ raise ArchiveError(
200
+ f"integration branch {branch} is checked out at unexpected path: {registered}"
201
+ )
202
+ status = archive_milestone.require_git_success(
203
+ run_git(registered, "status", "--porcelain", "--untracked-files=all"),
204
+ "inspect integration worktree status",
205
+ )
206
+ if status:
207
+ raise ArchiveError("integration worktree is dirty; refusing cleanup")
208
+ archive_milestone.require_git_success(
209
+ run_git(project, "worktree", "remove", str(registered)),
210
+ "remove integration worktree",
211
+ )
212
+
213
+
214
+ def delete_integration_branch(project: Path, branch: str, expected: str) -> None:
215
+ ref = f"refs/heads/{branch}"
216
+ actual = optional_ref(project, ref)
217
+ if actual is None:
218
+ return
219
+ if actual != expected:
220
+ raise ArchiveError(f"integration branch {branch} moved unexpectedly")
221
+ archive_milestone.require_git_success(
222
+ run_git(project, "update-ref", "-d", ref, expected),
223
+ "delete integration branch",
224
+ )
225
+
226
+
227
+ def require_generated_integration_commit(
228
+ project: Path,
229
+ commit: str,
230
+ archive_path: str,
231
+ archive_name: str,
232
+ ship_commit: str,
233
+ default_name: str,
234
+ bound_branch: str,
235
+ first_parent: Optional[str] = None,
236
+ ) -> None:
237
+ subject = archive_milestone.require_git_success(
238
+ run_git(project, "show", "-s", "--format=%s", commit),
239
+ "inspect integration commit subject",
240
+ )
241
+ expected_subject = integrate_subject(archive_name, default_name)
242
+ if subject != expected_subject:
243
+ raise ArchiveError(f"integration commit subject must be {expected_subject!r}")
244
+ archive_milestone.require_canonical_commit_body(
245
+ project,
246
+ commit,
247
+ expected_subject,
248
+ integrate_commit_body(archive_path, ship_commit, default_name, bound_branch),
249
+ "integration",
250
+ )
251
+ parents = archive_milestone.require_git_success(
252
+ run_git(project, "rev-list", "--parents", "-n", "1", commit),
253
+ "inspect integration merge parents",
254
+ ).split()
255
+ if len(parents) != 3 or parents[2] != ship_commit:
256
+ raise ArchiveError("integration commit is not the canonical merge of the ship commit")
257
+ if first_parent is not None and parents[1] != first_parent:
258
+ raise ArchiveError("integration merge was created from a stale remote default")
259
+
260
+
261
+ def discard_unpublished_stale_integration(
262
+ project: Path,
263
+ branch: str,
264
+ merge_commit: str,
265
+ remote_default_sha: str,
266
+ default_name: str,
267
+ archive_name: str,
268
+ bound_branch: str,
269
+ ) -> None:
270
+ live_default = live_remote_ref(project, f"refs/heads/{default_name}")
271
+ if live_default != remote_default_sha:
272
+ raise ArchiveError(f"origin/{default_name} advanced again; rerun integrate")
273
+ published = run_git(
274
+ project, "merge-base", "--is-ancestor", merge_commit, remote_default_sha
275
+ )
276
+ if published.returncode == 0:
277
+ raise ArchiveError("stale integration merge is already published on origin/main")
278
+ if published.returncode != 1:
279
+ archive_milestone.require_git_success(published, "inspect stale integration publication")
280
+
281
+ bound_ref = f"refs/heads/{bound_branch}"
282
+ if live_remote_ref(project, bound_ref) is not None:
283
+ raise ArchiveError(
284
+ "stale integration recovery requires the remote bound branch to be absent"
285
+ )
286
+ tag_name = f"milestone/{archive_name}"
287
+ published_tag_ref = f"refs/tags/{tag_name}"
288
+ if live_remote_ref(project, published_tag_ref) is not None:
289
+ raise ArchiveError(
290
+ "stale integration recovery requires the remote milestone tag to be absent"
291
+ )
292
+
293
+ local_tag_ref = f"refs/tags/{tag_name}"
294
+ local_tag_object = optional_ref(project, local_tag_ref)
295
+ if local_tag_object is not None:
296
+ require_annotated_tag(
297
+ project,
298
+ local_tag_ref,
299
+ merge_commit,
300
+ f"stale milestone tag {tag_name}",
301
+ )
302
+ archive_milestone.require_git_success(
303
+ run_git(project, "update-ref", "-d", local_tag_ref, local_tag_object),
304
+ "delete unpublished stale milestone tag",
305
+ )
306
+
307
+ tracking_tag_ref = f"refs/remotes/origin/tags/{tag_name}"
308
+ tracking_tag_object = optional_ref(project, tracking_tag_ref)
309
+ if tracking_tag_object is not None:
310
+ if local_tag_object is None or tracking_tag_object != local_tag_object:
311
+ raise ArchiveError("local published-tag tracking ref is not safely recoverable")
312
+ archive_milestone.require_git_success(
313
+ run_git(
314
+ project,
315
+ "update-ref",
316
+ "-d",
317
+ tracking_tag_ref,
318
+ tracking_tag_object,
319
+ ),
320
+ "delete stale published-tag tracking ref",
321
+ )
322
+ delete_integration_branch(project, branch, merge_commit)
323
+
324
+
325
+ def create_integration_merge(
326
+ project: Path,
327
+ worktree: Path,
328
+ branch: str,
329
+ remote_default_sha: str,
330
+ archive_path: str,
331
+ archive_name: str,
332
+ ship_commit: str,
333
+ default_name: str,
334
+ bound_branch: str,
335
+ ) -> tuple[str, bool]:
336
+ if worktree.exists() or worktree.is_symlink():
337
+ raise ArchiveError(f"integration worktree path already exists: {worktree}")
338
+ branch_ref = f"refs/heads/{branch}"
339
+ branch_tip = optional_ref(project, branch_ref)
340
+ if branch_tip is None:
341
+ add = run_git(
342
+ project,
343
+ "worktree",
344
+ "add",
345
+ "--quiet",
346
+ "-b",
347
+ branch,
348
+ str(worktree),
349
+ remote_default_sha,
350
+ )
351
+ elif branch_tip == remote_default_sha:
352
+ add = run_git(project, "worktree", "add", "--quiet", str(worktree), branch)
353
+ else:
354
+ require_generated_integration_commit(
355
+ project,
356
+ branch_tip,
357
+ archive_path,
358
+ archive_name,
359
+ ship_commit,
360
+ default_name,
361
+ bound_branch,
362
+ )
363
+ parents = archive_milestone.require_git_success(
364
+ run_git(project, "rev-list", "--parents", "-n", "1", branch_tip),
365
+ "inspect existing integration merge parents",
366
+ ).split()
367
+ if parents[1] == remote_default_sha:
368
+ return branch_tip, False
369
+ discard_unpublished_stale_integration(
370
+ project,
371
+ branch,
372
+ branch_tip,
373
+ remote_default_sha,
374
+ default_name,
375
+ archive_name,
376
+ bound_branch,
377
+ )
378
+ add = run_git(
379
+ project,
380
+ "worktree",
381
+ "add",
382
+ "--quiet",
383
+ "-b",
384
+ branch,
385
+ str(worktree),
386
+ remote_default_sha,
387
+ )
388
+ archive_milestone.require_git_success(add, "create integration worktree")
389
+
390
+ merge = run_git(
391
+ worktree,
392
+ "merge",
393
+ "--no-ff",
394
+ "-m",
395
+ integrate_subject(archive_name, default_name),
396
+ "-m",
397
+ integrate_commit_body(archive_path, ship_commit, default_name, bound_branch),
398
+ ship_commit,
399
+ )
400
+ if merge.returncode != 0:
401
+ conflicted = optional_ref(worktree, "MERGE_HEAD") is not None
402
+ detail = (merge.stderr or merge.stdout).strip()
403
+ conflicts = ""
404
+ if conflicted:
405
+ conflicts = run_git(
406
+ worktree, "diff", "--name-only", "--diff-filter=U"
407
+ ).stdout.strip()
408
+ archive_milestone.require_git_success(
409
+ run_git(worktree, "merge", "--abort"),
410
+ "abort integration merge",
411
+ )
412
+ remove_registered_worktree(project, branch, worktree)
413
+ delete_integration_branch(project, branch, remote_default_sha)
414
+ if conflicted:
415
+ raise ArchiveError(
416
+ "integration merge conflicted; Git aborted it without resolving files: "
417
+ + ", ".join(conflicts.splitlines())
418
+ + "; closed milestone remains unchanged; run gsd-path-forensics and "
419
+ "obtain a user-approved integration recovery plan; do not merge into or re-ship the bound branch"
420
+ )
421
+ raise ArchiveError(f"create integration merge failed: {detail}")
422
+
423
+ merge_commit = archive_milestone.require_git_success(
424
+ run_git(worktree, "rev-parse", "HEAD"), "resolve integration merge"
425
+ )
426
+ require_generated_integration_commit(
427
+ project,
428
+ merge_commit,
429
+ archive_path,
430
+ archive_name,
431
+ ship_commit,
432
+ default_name,
433
+ bound_branch,
434
+ remote_default_sha,
435
+ )
436
+ return merge_commit, True
437
+
438
+
439
+ def require_annotated_tag(
440
+ project: Path,
441
+ ref: str,
442
+ merge_commit: str,
443
+ label: str,
444
+ message: Optional[str] = None,
445
+ ) -> str:
446
+ object_id = optional_ref(project, ref)
447
+ if object_id is None:
448
+ raise ArchiveError(f"missing {label}")
449
+ tag_type = archive_milestone.require_git_success(
450
+ run_git(project, "cat-file", "-t", ref), f"inspect {label}"
451
+ )
452
+ if tag_type != "tag":
453
+ raise ArchiveError(f"{label} must be annotated")
454
+ target = archive_milestone.require_git_success(
455
+ run_git(project, "rev-parse", f"{ref}^{{commit}}"), f"resolve {label} target"
456
+ )
457
+ if target != merge_commit:
458
+ raise ArchiveError(f"{label} does not point at the integration merge")
459
+ if message is not None:
460
+ actual_message = archive_milestone.require_git_success(
461
+ run_git(project, "for-each-ref", "--format=%(contents)", ref),
462
+ f"inspect {label} message",
463
+ )
464
+ if actual_message != message.strip():
465
+ raise ArchiveError(f"{label} has unexpected metadata")
466
+ return object_id
467
+
468
+
469
+ def ensure_integration_tag(
470
+ project: Path,
471
+ tag_name: str,
472
+ merge_commit: str,
473
+ message: Optional[str] = None,
474
+ ) -> tuple[str, bool]:
475
+ tag_ref = f"refs/tags/{tag_name}"
476
+ tracking_ref = f"refs/remotes/origin/tags/{tag_name}"
477
+ local_object = optional_ref(project, tag_ref)
478
+ published_object = live_remote_ref(project, tag_ref)
479
+ tracking_object = optional_ref(project, tracking_ref)
480
+ if tracking_object != published_object:
481
+ if published_object is None:
482
+ archive_milestone.require_git_success(
483
+ run_git(project, "update-ref", "-d", tracking_ref, tracking_object),
484
+ "prune local milestone-tag ref",
485
+ )
486
+ else:
487
+ archive_milestone.require_git_success(
488
+ run_git(project, "update-ref", tracking_ref, published_object),
489
+ "refresh local milestone-tag ref",
490
+ )
491
+ if published_object is not None:
492
+ published_object = require_annotated_tag(
493
+ project,
494
+ tracking_ref,
495
+ merge_commit,
496
+ f"published milestone tag {tag_name}",
497
+ message,
498
+ )
499
+ if local_object is None:
500
+ if published_object is not None:
501
+ archive_milestone.require_git_success(
502
+ run_git(project, "update-ref", tag_ref, published_object),
503
+ "restore local milestone tag",
504
+ )
505
+ else:
506
+ tag_message = message or f"milestone {tag_name.removeprefix('milestone/')}"
507
+ archive_milestone.require_git_success(
508
+ run_git(
509
+ project,
510
+ "tag",
511
+ "--no-sign",
512
+ "-a",
513
+ "-m",
514
+ tag_message,
515
+ tag_name,
516
+ merge_commit,
517
+ ),
518
+ "create milestone tag",
519
+ )
520
+ local_object = require_annotated_tag(
521
+ project,
522
+ tag_ref,
523
+ merge_commit,
524
+ f"milestone tag {tag_name}",
525
+ message,
526
+ )
527
+ if published_object is not None and published_object != local_object:
528
+ raise ArchiveError(f"local and published milestone tag {tag_name} differ")
529
+ return local_object, published_object is not None
530
+
531
+
532
+ def github_repository(project: Path) -> str:
533
+ remote = archive_milestone.require_git_success(
534
+ run_git(project, "remote", "get-url", "origin"),
535
+ "resolve origin URL",
536
+ )
537
+ patterns = (
538
+ r"https://github\.com/([^/]+/[^/]+?)(?:\.git)?$",
539
+ r"git@github\.com:([^/]+/[^/]+?)(?:\.git)?$",
540
+ r"ssh://git@github\.com/([^/]+/[^/]+?)(?:\.git)?$",
541
+ )
542
+ for pattern in patterns:
543
+ match = re.fullmatch(pattern, remote)
544
+ if match:
545
+ return match.group(1)
546
+ raise ArchiveError("pull-request integration requires a GitHub.com origin")
547
+
548
+
549
+ def require_github_authentication() -> None:
550
+ archive_milestone.require_git_success(
551
+ run_command("gh", "auth", "status", "--hostname", GITHUB_HOST),
552
+ "verify GitHub authentication",
553
+ )
554
+
555
+
556
+ def github_api_json(*arguments: str) -> object:
557
+ output = archive_milestone.require_git_success(
558
+ run_command("gh", "api", "--hostname", GITHUB_HOST, *arguments),
559
+ "call GitHub API",
560
+ )
561
+ try:
562
+ return json.loads(output)
563
+ except json.JSONDecodeError as error:
564
+ raise ArchiveError("GitHub API returned invalid JSON") from error
565
+
566
+
567
+ def require_pull_request_shape(value: object) -> dict:
568
+ if not isinstance(value, dict):
569
+ raise ArchiveError("GitHub pull request response is not an object")
570
+ required = {
571
+ "number",
572
+ "state",
573
+ "html_url",
574
+ "merged_at",
575
+ "merge_commit_sha",
576
+ "base",
577
+ "head",
578
+ }
579
+ if not required.issubset(value):
580
+ raise ArchiveError("GitHub pull request response is missing fields")
581
+ if not isinstance(value["base"], dict) or not isinstance(value["head"], dict):
582
+ raise ArchiveError("GitHub pull request refs are invalid")
583
+ number = value["number"]
584
+ if not isinstance(number, int) or isinstance(number, bool) or number < 1:
585
+ raise ArchiveError("GitHub pull request response has invalid fields")
586
+ if value["state"] not in {"open", "closed"}:
587
+ raise ArchiveError("GitHub pull request response has invalid fields")
588
+ html_url = value["html_url"]
589
+ if not isinstance(html_url, str) or re.fullmatch(
590
+ rf"https://github\.com/[^/]+/[^/]+/pull/{number}", html_url
591
+ ) is None:
592
+ raise ArchiveError("GitHub pull request response has invalid fields")
593
+ merged_at = value["merged_at"]
594
+ if merged_at is not None and not isinstance(merged_at, str):
595
+ raise ArchiveError("GitHub pull request response has invalid fields")
596
+ merge_commit = value["merge_commit_sha"]
597
+ if merge_commit is not None and (
598
+ not isinstance(merge_commit, str)
599
+ or re.fullmatch(r"[0-9a-f]{40}", merge_commit) is None
600
+ ):
601
+ raise ArchiveError("GitHub pull request response has invalid fields")
602
+ if not isinstance(value["base"].get("ref"), str):
603
+ raise ArchiveError("GitHub pull request response has invalid fields")
604
+ if not isinstance(value["head"].get("ref"), str):
605
+ raise ArchiveError("GitHub pull request response has invalid fields")
606
+ head_sha = value["head"].get("sha")
607
+ if not isinstance(head_sha, str) or re.fullmatch(r"[0-9a-f]{40}", head_sha) is None:
608
+ raise ArchiveError("GitHub pull request response has invalid fields")
609
+ return value
610
+
611
+
612
+ def require_pull_request_pages(value: object) -> list[dict]:
613
+ if not isinstance(value, list) or any(
614
+ not isinstance(page, list) for page in value
615
+ ):
616
+ raise ArchiveError("GitHub pull request pages are invalid")
617
+ return [require_pull_request_shape(item) for page in value for item in page]
618
+
619
+
620
+ def pull_request_head_matches_repository(pull: dict, repository: str) -> bool:
621
+ head_repository = pull["head"].get("repo")
622
+ full_name = (
623
+ head_repository.get("full_name")
624
+ if isinstance(head_repository, dict)
625
+ else None
626
+ )
627
+ return isinstance(full_name, str) and full_name.casefold() == repository.casefold()
628
+
629
+
630
+ def require_pull_request_identity(
631
+ repository: str,
632
+ pull: dict,
633
+ branch: str,
634
+ ship_commit: str,
635
+ ) -> None:
636
+ if pull["base"].get("ref") != "main":
637
+ raise ArchiveError("GitHub pull request base is not main")
638
+ if pull["head"].get("ref") != branch:
639
+ raise ArchiveError("GitHub pull request head is not the bound branch")
640
+ if pull["head"].get("sha") != ship_commit:
641
+ raise ArchiveError("GitHub pull request head is not the ship commit")
642
+ if not pull_request_head_matches_repository(pull, repository):
643
+ raise ArchiveError("GitHub pull request head repository is not origin")
644
+
645
+
646
+ def find_pull_request(repository: str, branch: str, ship_commit: str) -> Optional[dict]:
647
+ pulls = require_pull_request_pages(
648
+ github_api_json(
649
+ f"repos/{repository}/pulls",
650
+ "--method",
651
+ "GET",
652
+ "-f",
653
+ "state=all",
654
+ "-f",
655
+ "base=main",
656
+ "--paginate",
657
+ "--slurp",
658
+ )
659
+ )
660
+ pulls = [
661
+ pull
662
+ for pull in pulls
663
+ if pull["base"].get("ref") == "main"
664
+ and (
665
+ pull["head"].get("sha") == ship_commit
666
+ or (
667
+ pull["head"].get("ref") == branch
668
+ and pull_request_head_matches_repository(pull, repository)
669
+ )
670
+ )
671
+ ]
672
+ if len(pulls) > 1:
673
+ raise ArchiveError("multiple pull requests target main from the ship commit")
674
+ if not pulls:
675
+ return None
676
+ pull = pulls[0]
677
+ require_pull_request_identity(repository, pull, branch, ship_commit)
678
+ return pull
679
+
680
+
681
+ def pull_request_body(archive_path: str, ship_commit: str, branch: str) -> str:
682
+ return (
683
+ f"{integrate_commit_body(archive_path, ship_commit, 'main', branch)}"
684
+ f"\n\n---\n{PR_CREDIT_LINE}"
685
+ )
686
+
687
+
688
+ def update_pull_request_body(
689
+ repository: str,
690
+ pull: dict,
691
+ archive_path: str,
692
+ ship_commit: str,
693
+ branch: str,
694
+ ) -> dict:
695
+ expected = pull_request_body(archive_path, ship_commit, branch)
696
+ current = pull.get("body")
697
+ if isinstance(current, str) and current.rstrip().endswith(PR_CREDIT_LINE):
698
+ return pull
699
+ response = github_api_json(
700
+ f"repos/{repository}/pulls/{pull['number']}",
701
+ "--method",
702
+ "PATCH",
703
+ "-f",
704
+ f"body={expected}",
705
+ )
706
+ updated = require_pull_request_shape(response)
707
+ require_pull_request_identity(repository, updated, branch, ship_commit)
708
+ if updated.get("body") != expected:
709
+ raise ArchiveError("updated GitHub pull request is missing the credit footer")
710
+ return updated
711
+
712
+
713
+ def create_pull_request(
714
+ repository: str,
715
+ branch: str,
716
+ archive_path: str,
717
+ archive_name: str,
718
+ ship_commit: str,
719
+ ) -> dict:
720
+ body = pull_request_body(archive_path, ship_commit, branch)
721
+ response = github_api_json(
722
+ f"repos/{repository}/pulls",
723
+ "--method",
724
+ "POST",
725
+ "-f",
726
+ f"title={integrate_subject(archive_name, 'main')}",
727
+ "-f",
728
+ f"head={branch}",
729
+ "-f",
730
+ "base=main",
731
+ "-f",
732
+ f"body={body}",
733
+ )
734
+ pull = require_pull_request_shape(response)
735
+ require_pull_request_identity(repository, pull, branch, ship_commit)
736
+ return pull
737
+
738
+
739
+ def pull_request_tag_message(
740
+ archive_name: str,
741
+ pull_request: str,
742
+ ship_commit: str,
743
+ landing: str,
744
+ ) -> str:
745
+ return (
746
+ f"milestone {archive_name}\n\n"
747
+ "Mode: pull-request\n"
748
+ f"Pull-Request: {pull_request}\n"
749
+ f"Ship: {ship_commit}\n"
750
+ f"Landing: {landing}"
751
+ )
752
+
753
+
754
+ def pull_request_tag_metadata(project: Path, tag_ref: str) -> dict[str, str]:
755
+ message = archive_milestone.require_git_success(
756
+ run_git(project, "for-each-ref", "--format=%(contents)", tag_ref),
757
+ "inspect pull-request milestone tag",
758
+ )
759
+ fields: dict[str, str] = {}
760
+ for line in message.splitlines():
761
+ match = re.fullmatch(r"(Mode|Pull-Request|Ship|Landing): (.+)", line)
762
+ if match:
763
+ if match.group(1) in fields:
764
+ raise ArchiveError("pull-request milestone tag repeats metadata")
765
+ fields[match.group(1)] = match.group(2)
766
+ if set(fields) != {"Mode", "Pull-Request", "Ship", "Landing"}:
767
+ raise ArchiveError("pull-request milestone tag is missing metadata")
768
+ if fields["Mode"] != "pull-request":
769
+ raise ArchiveError("pull-request milestone tag has the wrong mode")
770
+ return fields
771
+
772
+
773
+ def require_pull_request_merge(
774
+ project: Path,
775
+ merge_commit: str,
776
+ ship_commit: str,
777
+ remote_default: str,
778
+ ) -> None:
779
+ resolved = optional_ref(project, merge_commit)
780
+ if resolved != merge_commit:
781
+ raise ArchiveError("GitHub merge commit is unavailable after fetch")
782
+ parents = archive_milestone.require_git_success(
783
+ run_git(project, "rev-list", "--parents", "-n", "1", merge_commit),
784
+ "inspect pull-request merge parents",
785
+ ).split()
786
+ if len(parents) != 3:
787
+ raise ArchiveError("pull-request integration must use a two-parent merge commit")
788
+ if parents[2] != ship_commit:
789
+ raise ArchiveError("pull-request merge second parent is not the ship commit")
790
+ first_parent = archive_milestone.require_git_success(
791
+ run_git(project, "rev-list", "--first-parent", remote_default),
792
+ "inspect remote-default first-parent history",
793
+ ).splitlines()
794
+ if merge_commit not in first_parent:
795
+ raise ArchiveError("pull-request merge is not on origin/main first-parent history")
796
+
797
+
798
+ def require_pull_request_merge_provenance(
799
+ repository: str,
800
+ number: int,
801
+ merge_commit: Optional[str] = None,
802
+ ) -> None:
803
+ owner, name = repository.split("/", 1)
804
+ query = """query($owner:String!,$name:String!,$number:Int!){
805
+ repository(owner:$owner,name:$name){
806
+ pullRequest(number:$number){
807
+ mergeQueue:timelineItems(first:1,itemTypes:[ADDED_TO_MERGE_QUEUE_EVENT]){
808
+ nodes{__typename}
809
+ }
810
+ autoMerge:timelineItems(first:1,itemTypes:[AUTO_MERGE_ENABLED_EVENT]){
811
+ nodes{__typename}
812
+ }
813
+ mergeAction:timelineItems(last:1,itemTypes:[MERGED_EVENT]){
814
+ nodes{__typename ... on MergedEvent{actor{__typename login} commit{oid}}}
815
+ }
816
+ }
817
+ }
818
+ }"""
819
+ payload = github_api_json(
820
+ "graphql",
821
+ "-f",
822
+ f"query={query}",
823
+ "-F",
824
+ f"owner={owner}",
825
+ "-F",
826
+ f"name={name}",
827
+ "-F",
828
+ f"number={number}",
829
+ )
830
+ data = payload.get("data") if isinstance(payload, dict) else None
831
+ repository_data = data.get("repository") if isinstance(data, dict) else None
832
+ pull_request = (
833
+ repository_data.get("pullRequest")
834
+ if isinstance(repository_data, dict)
835
+ else None
836
+ )
837
+ checks = (
838
+ (
839
+ "mergeQueue",
840
+ "AddedToMergeQueueEvent",
841
+ "GitHub merge-queue provenance is invalid",
842
+ "pull-request integration cannot use a merge queue",
843
+ ),
844
+ (
845
+ "autoMerge",
846
+ "AutoMergeEnabledEvent",
847
+ "GitHub auto-merge provenance is invalid",
848
+ "pull-request integration cannot use auto-merge",
849
+ ),
850
+ )
851
+ for field, typename, invalid_message, rejection_message in checks:
852
+ timeline = pull_request.get(field) if isinstance(pull_request, dict) else None
853
+ nodes = timeline.get("nodes") if isinstance(timeline, dict) else None
854
+ if not isinstance(nodes, list):
855
+ raise ArchiveError(invalid_message)
856
+ if nodes:
857
+ if any(
858
+ not isinstance(node, dict) or node.get("__typename") != typename
859
+ for node in nodes
860
+ ):
861
+ raise ArchiveError(invalid_message)
862
+ raise ArchiveError(rejection_message)
863
+ merge_timeline = (
864
+ pull_request.get("mergeAction")
865
+ if isinstance(pull_request, dict)
866
+ else None
867
+ )
868
+ merge_nodes = (
869
+ merge_timeline.get("nodes")
870
+ if isinstance(merge_timeline, dict)
871
+ else None
872
+ )
873
+ if not isinstance(merge_nodes, list):
874
+ raise ArchiveError("GitHub pull-request merge provenance is invalid")
875
+ if merge_commit is None:
876
+ if merge_nodes:
877
+ raise ArchiveError("open pull request has invalid merge provenance")
878
+ return
879
+ if len(merge_nodes) != 1:
880
+ raise ArchiveError("pull request was not merged by a GitHub merge action")
881
+ merge_node = merge_nodes[0]
882
+ commit = merge_node.get("commit") if isinstance(merge_node, dict) else None
883
+ actor = merge_node.get("actor") if isinstance(merge_node, dict) else None
884
+ if (
885
+ not isinstance(merge_node, dict)
886
+ or merge_node.get("__typename") != "MergedEvent"
887
+ or not isinstance(commit, dict)
888
+ or commit.get("oid") != merge_commit
889
+ ):
890
+ raise ArchiveError("GitHub pull-request merge action does not match landing")
891
+ if (
892
+ not isinstance(actor, dict)
893
+ or actor.get("__typename") != "User"
894
+ or not isinstance(actor.get("login"), str)
895
+ or not actor["login"]
896
+ ):
897
+ raise ArchiveError("pull request must be merged by a human GitHub user")
898
+
899
+
900
+ def integrate_pull_request(
901
+ project: Path,
902
+ state: PipelineState,
903
+ archive_path: str,
904
+ archive_name: str,
905
+ ship_commit: str,
906
+ ) -> dict:
907
+ branch = state.branch
908
+ if branch is None:
909
+ raise ArchiveError("pull-request integration requires a bound branch")
910
+ require_github_authentication()
911
+ repository = github_repository(project)
912
+ pull = find_pull_request(repository, branch, ship_commit)
913
+ if pull is None:
914
+ publish_bound_branch(project, branch, ship_commit)
915
+ pull = create_pull_request(
916
+ repository,
917
+ branch,
918
+ archive_path,
919
+ archive_name,
920
+ ship_commit,
921
+ )
922
+ elif pull["merged_at"] is None:
923
+ if pull["state"] != "open":
924
+ raise ArchiveError("GitHub pull request was closed without merging")
925
+ pull = update_pull_request_body(
926
+ repository,
927
+ pull,
928
+ archive_path,
929
+ ship_commit,
930
+ branch,
931
+ )
932
+ if pull["merged_at"] is None:
933
+ if pull["state"] != "open":
934
+ raise ArchiveError("GitHub pull request was closed without merging")
935
+ publish_bound_branch(project, branch, ship_commit)
936
+ require_pull_request_merge_provenance(repository, pull["number"])
937
+ return {
938
+ "status": "awaiting-merge",
939
+ "mode": "pull-request",
940
+ "archive": archive_path,
941
+ "commit": ship_commit,
942
+ "pull_request": pull["html_url"],
943
+ }
944
+
945
+ merge_commit = pull["merge_commit_sha"]
946
+ if pull["state"] != "closed" or not isinstance(merge_commit, str):
947
+ raise ArchiveError("GitHub pull request merge metadata is invalid")
948
+ require_pull_request_merge_provenance(
949
+ repository,
950
+ pull["number"],
951
+ merge_commit,
952
+ )
953
+ remote_default = refresh_origin(project)["remote_default"]
954
+ require_pull_request_merge(project, merge_commit, ship_commit, remote_default)
955
+ pull = update_pull_request_body(
956
+ repository,
957
+ pull,
958
+ archive_path,
959
+ ship_commit,
960
+ branch,
961
+ )
962
+ tag_name = f"milestone/{archive_name}"
963
+ message = pull_request_tag_message(
964
+ archive_name,
965
+ pull["html_url"],
966
+ ship_commit,
967
+ merge_commit,
968
+ )
969
+ tag_object, tag_published = ensure_integration_tag(
970
+ project,
971
+ tag_name,
972
+ merge_commit,
973
+ message,
974
+ )
975
+ if not tag_published:
976
+ archive_milestone.require_git_success(
977
+ run_git(
978
+ project,
979
+ "push",
980
+ "origin",
981
+ f"refs/tags/{tag_name}:refs/tags/{tag_name}",
982
+ ),
983
+ "push milestone tag",
984
+ )
985
+ archive_milestone.require_git_success(
986
+ run_git(
987
+ project,
988
+ "update-ref",
989
+ f"refs/remotes/origin/tags/{tag_name}",
990
+ tag_object,
991
+ ),
992
+ "refresh local milestone-tag ref",
993
+ )
994
+ return validate_integrated(project, archive_milestone.milestone_slug(state))
995
+
996
+
997
+ def integrate(repo: Path, slug: str) -> dict:
998
+ project = repo.resolve()
999
+ active_root = archive_milestone.require_project_layout(project)
1000
+ state, _, loaded_state_path = archive_milestone.strict_state(project)
1001
+ if loaded_state_path.resolve() != (active_root / "STATE.md").resolve():
1002
+ raise ArchiveError("strict STATE.md loader returned an unexpected path")
1003
+ expected_slug = archive_milestone.milestone_slug(state)
1004
+ if archive_milestone.normalized_slug(slug) != expected_slug:
1005
+ raise ArchiveError(
1006
+ f"--slug {slug!r} does not match normalized STATE.milestone {expected_slug!r}"
1007
+ )
1008
+
1009
+ shipped = archive_milestone.validate(project)
1010
+ archive_path = shipped["archive"]
1011
+ ship_commit = shipped["commit"]
1012
+ archive_name = PurePosixPath(archive_path).name
1013
+ bound_branch = state.branch
1014
+ expected_branch = bound_branch_name(milestone_number(archive_name))
1015
+ if bound_branch != expected_branch:
1016
+ raise ArchiveError(f"integration requires canonical bound branch {expected_branch}")
1017
+ head = archive_milestone.require_git_success(
1018
+ run_git(project, "rev-parse", "HEAD"), "resolve bound branch HEAD"
1019
+ )
1020
+ if head != ship_commit:
1021
+ raise ArchiveError("bound worktree must remain at the ship commit during integration")
1022
+ subject = archive_milestone.require_git_success(
1023
+ run_git(project, "show", "-s", "--format=%s", ship_commit),
1024
+ "inspect ship commit subject",
1025
+ )
1026
+ if subject != ship_subject(archive_name):
1027
+ raise ArchiveError("integration only generates canonical M00N shipment history")
1028
+
1029
+ remote_default = refresh_origin(project)["remote_default"]
1030
+ default_name = default_branch_name(remote_default)
1031
+ if default_name != "main":
1032
+ raise ArchiveError(f"remote default must be main, got {default_name!r}")
1033
+ if bound_branch == default_name:
1034
+ raise ArchiveError("bound branch must not be the remote default")
1035
+ remote_default_sha = archive_milestone.require_git_success(
1036
+ run_git(project, "rev-parse", remote_default), "resolve remote default"
1037
+ )
1038
+ if state.integration == "pull-request":
1039
+ return integrate_pull_request(
1040
+ project,
1041
+ state,
1042
+ archive_path,
1043
+ archive_name,
1044
+ ship_commit,
1045
+ )
1046
+ publishable_bound_branch(project, bound_branch, ship_commit)
1047
+ integration_branch, worktree = integration_names(project, archive_name)
1048
+ interrupted_worktree = registered_worktree(project, integration_branch)
1049
+ if (
1050
+ interrupted_worktree is not None
1051
+ and optional_ref(interrupted_worktree, "MERGE_HEAD") is not None
1052
+ ):
1053
+ if interrupted_worktree != worktree.resolve():
1054
+ raise ArchiveError(
1055
+ f"integration branch {integration_branch} is checked out at unexpected path: "
1056
+ f"{interrupted_worktree}"
1057
+ )
1058
+ conflicts = run_git(
1059
+ interrupted_worktree, "diff", "--name-only", "--diff-filter=U"
1060
+ ).stdout.strip()
1061
+ archive_milestone.require_git_success(
1062
+ run_git(interrupted_worktree, "merge", "--abort"),
1063
+ "abort interrupted integration merge",
1064
+ )
1065
+ remove_registered_worktree(project, integration_branch, worktree)
1066
+ interrupted_tip = optional_ref(project, f"refs/heads/{integration_branch}")
1067
+ if interrupted_tip is not None:
1068
+ delete_integration_branch(project, integration_branch, interrupted_tip)
1069
+ raise ArchiveError(
1070
+ "interrupted integration merge conflicted; Git aborted it without resolving files: "
1071
+ + ", ".join(conflicts.splitlines())
1072
+ + "; closed milestone remains unchanged; run gsd-path-forensics and "
1073
+ "obtain a user-approved integration recovery plan; do not merge into or re-ship the bound branch"
1074
+ )
1075
+ remove_registered_worktree(project, integration_branch, worktree)
1076
+ worktree_active = False
1077
+
1078
+ contains_ship = run_git(
1079
+ project, "merge-base", "--is-ancestor", ship_commit, remote_default
1080
+ )
1081
+ if contains_ship.returncode == 0:
1082
+ merge_commit = find_integrate_commit(
1083
+ project, remote_default, archive_name, ship_commit
1084
+ )
1085
+ require_generated_integration_commit(
1086
+ project,
1087
+ merge_commit,
1088
+ archive_path,
1089
+ archive_name,
1090
+ ship_commit,
1091
+ default_name,
1092
+ bound_branch,
1093
+ )
1094
+ elif contains_ship.returncode == 1:
1095
+ merge_commit, worktree_active = create_integration_merge(
1096
+ project,
1097
+ worktree,
1098
+ integration_branch,
1099
+ remote_default_sha,
1100
+ archive_path,
1101
+ archive_name,
1102
+ ship_commit,
1103
+ default_name,
1104
+ bound_branch,
1105
+ )
1106
+ else:
1107
+ archive_milestone.require_git_success(contains_ship, "inspect ship ancestry on the remote default")
1108
+ raise AssertionError("unreachable")
1109
+
1110
+ action_error = None
1111
+ try:
1112
+ tag_name = f"milestone/{archive_name}"
1113
+ tag_object, tag_published = ensure_integration_tag(
1114
+ project, tag_name, merge_commit
1115
+ )
1116
+
1117
+ published_merge = run_git(
1118
+ project, "merge-base", "--is-ancestor", merge_commit, remote_default
1119
+ )
1120
+ if published_merge.returncode == 1:
1121
+ archive_milestone.require_git_success(
1122
+ run_git(
1123
+ project,
1124
+ "push",
1125
+ "origin",
1126
+ f"{merge_commit}:refs/heads/{default_name}",
1127
+ ),
1128
+ "push integration merge to main",
1129
+ )
1130
+ archive_milestone.require_git_success(
1131
+ run_git(project, "update-ref", f"refs/remotes/{remote_default}", merge_commit),
1132
+ "refresh local remote-default ref",
1133
+ )
1134
+ elif published_merge.returncode != 0:
1135
+ archive_milestone.require_git_success(published_merge, "inspect published integration merge")
1136
+
1137
+ publish_bound_branch(project, bound_branch, ship_commit)
1138
+ if not tag_published:
1139
+ archive_milestone.require_git_success(
1140
+ run_git(
1141
+ project,
1142
+ "push",
1143
+ "origin",
1144
+ f"refs/tags/{tag_name}:refs/tags/{tag_name}",
1145
+ ),
1146
+ "push milestone tag",
1147
+ )
1148
+ archive_milestone.require_git_success(
1149
+ run_git(
1150
+ project,
1151
+ "update-ref",
1152
+ f"refs/remotes/origin/tags/{tag_name}",
1153
+ tag_object,
1154
+ ),
1155
+ "refresh local milestone-tag ref",
1156
+ )
1157
+ except (ArchiveError, OSError) as error:
1158
+ action_error = error
1159
+
1160
+ try:
1161
+ if worktree_active:
1162
+ remove_registered_worktree(project, integration_branch, worktree)
1163
+ except (ArchiveError, OSError) as cleanup_error:
1164
+ if action_error is not None:
1165
+ raise ArchiveError(
1166
+ f"{action_error}; integration worktree cleanup failed: {cleanup_error}"
1167
+ )
1168
+ raise
1169
+ if action_error is not None:
1170
+ raise action_error
1171
+
1172
+ branch_tip = optional_ref(project, f"refs/heads/{integration_branch}")
1173
+ if branch_tip is not None:
1174
+ if branch_tip != merge_commit:
1175
+ ancestor = run_git(
1176
+ project, "merge-base", "--is-ancestor", branch_tip, merge_commit
1177
+ )
1178
+ if ancestor.returncode != 0:
1179
+ raise ArchiveError(f"integration branch {integration_branch} moved unexpectedly")
1180
+ delete_integration_branch(project, integration_branch, branch_tip)
1181
+ return validate_integrated(project, slug)
1182
+
1183
+
1184
+ def find_integrate_commit(
1185
+ project: Path,
1186
+ remote_default: str,
1187
+ archive_name: str,
1188
+ ship_commit: str,
1189
+ ) -> str:
1190
+ default_name = default_branch_name(remote_default)
1191
+ expected_subject = integrate_subject(archive_name, default_name)
1192
+ log = archive_milestone.require_git_success(
1193
+ run_git(project, "log", "--first-parent", "--format=%H%x00%s", remote_default),
1194
+ f"inspect {remote_default} first-parent history for the integration merge",
1195
+ )
1196
+ matches = []
1197
+ for record in log.splitlines():
1198
+ if "\x00" not in record:
1199
+ continue
1200
+ commit, subject = record.split("\x00", 1)
1201
+ if subject == expected_subject:
1202
+ matches.append(commit)
1203
+ if not matches:
1204
+ raise ArchiveError(
1205
+ f"no commit with exact subject {expected_subject!r} "
1206
+ f"in {remote_default} first-parent history"
1207
+ )
1208
+ if len(matches) != 1:
1209
+ raise ArchiveError(
1210
+ f"multiple commits with exact subject {expected_subject!r} "
1211
+ f"in {remote_default} first-parent history"
1212
+ )
1213
+ merge_commit = matches[0]
1214
+ parents = archive_milestone.require_git_success(
1215
+ run_git(project, "rev-list", "--parents", "-n", "1", merge_commit),
1216
+ "inspect integration merge parents",
1217
+ ).split()
1218
+ if len(parents) != 3:
1219
+ raise ArchiveError(f"integration commit for {archive_name} is not a merge commit")
1220
+ if parents[2] != ship_commit:
1221
+ raise ArchiveError("integration merge second parent is not the ship commit")
1222
+ return merge_commit
1223
+
1224
+
1225
+ def validate_integrated(repo: Path, slug: str, *, refresh: bool = True) -> dict:
1226
+ project = repo.resolve()
1227
+ active_root = archive_milestone.require_project_layout(project)
1228
+ state, _, loaded_state_path = archive_milestone.strict_state(project)
1229
+ if loaded_state_path.resolve() != (active_root / "STATE.md").resolve():
1230
+ raise ArchiveError("strict STATE.md loader returned an unexpected path")
1231
+ expected_slug = archive_milestone.milestone_slug(state)
1232
+ if archive_milestone.normalized_slug(slug) != expected_slug:
1233
+ raise ArchiveError(
1234
+ f"--slug {slug!r} does not match normalized STATE.milestone {expected_slug!r}"
1235
+ )
1236
+
1237
+ # (a) The shipped transaction itself must still validate.
1238
+ shipped = archive_milestone.validate(repo)
1239
+ configured = shipped["archive"]
1240
+ ship_commit = shipped["commit"]
1241
+ archive_name = PurePosixPath(configured).name
1242
+
1243
+ # Refresh whenever origin exists so direct mode cannot miss a published tag.
1244
+ # Fixtures that only plant refs/remotes/origin/* keep resolve_remote_default.
1245
+ has_origin = run_git(project, "remote", "get-url", "origin").returncode == 0
1246
+ if has_origin and refresh:
1247
+ remote_default = refresh_origin(project)["remote_default"]
1248
+ else:
1249
+ remote_default = resolve_remote_default(project)
1250
+ default_name = default_branch_name(remote_default)
1251
+ bound_branch = state.branch
1252
+ if bound_branch is None:
1253
+ raise ArchiveError("STATE.md does not name a bound build branch")
1254
+ expected_branch = bound_branch_name(milestone_number(archive_name))
1255
+ ship_subject_text = archive_milestone.require_git_success(
1256
+ run_git(project, "log", "-1", "--format=%s", ship_commit),
1257
+ "read ship commit subject",
1258
+ )
1259
+ if ship_subject_text != ship_subject(archive_name):
1260
+ raise ArchiveError("current milestone ship commit subject is not canonical")
1261
+ if not is_bound_branch(bound_branch):
1262
+ raise ArchiveError(f"bound branch {bound_branch!r} is not gsd-path/M00N")
1263
+ if is_bound_branch(bound_branch) and bound_branch != expected_branch:
1264
+ expected_milestone = expected_branch.removeprefix("gsd-path/")
1265
+ raise ArchiveError(
1266
+ f"bound branch {bound_branch} does not match archive milestone {expected_milestone}"
1267
+ )
1268
+ if bound_branch == default_name:
1269
+ raise ArchiveError(
1270
+ f"bound branch {bound_branch!r} is the remote default; "
1271
+ "ship merges onto main, never onto the work branch"
1272
+ )
1273
+ if default_name != "main":
1274
+ raise ArchiveError(f"remote default must be main, got {default_name!r}")
1275
+
1276
+ # (b) Resolve the integration proof selected before build.
1277
+ tag_name = f"milestone/{archive_name}"
1278
+ tag_ref = f"refs/tags/{tag_name}"
1279
+ pull_request = None
1280
+ if state.integration == "pull-request":
1281
+ if run_git(project, "rev-parse", "--verify", "--quiet", tag_ref).returncode != 0:
1282
+ raise ArchiveError(f"missing milestone tag: {tag_name}")
1283
+ metadata = pull_request_tag_metadata(project, tag_ref)
1284
+ if metadata["Ship"] != ship_commit:
1285
+ raise ArchiveError("pull-request milestone tag names the wrong ship commit")
1286
+ merge_commit = metadata["Landing"]
1287
+ pull_request = metadata["Pull-Request"]
1288
+ require_github_authentication()
1289
+ repository = github_repository(project)
1290
+ pull = find_pull_request(repository, bound_branch, ship_commit)
1291
+ if pull is None:
1292
+ raise ArchiveError("GitHub pull request for the ship commit is missing")
1293
+ if pull["html_url"] != pull_request:
1294
+ raise ArchiveError("milestone tag names the wrong pull request")
1295
+ if (
1296
+ pull["state"] != "closed"
1297
+ or pull["merged_at"] is None
1298
+ or pull["merge_commit_sha"] != merge_commit
1299
+ ):
1300
+ raise ArchiveError("GitHub pull request is not merged at the tagged landing")
1301
+ require_pull_request_merge_provenance(
1302
+ repository,
1303
+ pull["number"],
1304
+ merge_commit,
1305
+ )
1306
+ require_pull_request_merge(project, merge_commit, ship_commit, remote_default)
1307
+ require_annotated_tag(
1308
+ project,
1309
+ tag_ref,
1310
+ merge_commit,
1311
+ f"milestone tag {tag_name}",
1312
+ pull_request_tag_message(
1313
+ archive_name,
1314
+ pull_request,
1315
+ ship_commit,
1316
+ merge_commit,
1317
+ ),
1318
+ )
1319
+ else:
1320
+ merge_commit = find_integrate_commit(
1321
+ project, remote_default, archive_name, ship_commit
1322
+ )
1323
+ archive_milestone.require_canonical_commit_body(
1324
+ project,
1325
+ merge_commit,
1326
+ integrate_subject(archive_name, default_name),
1327
+ integrate_commit_body(configured, ship_commit, default_name, bound_branch),
1328
+ "integration",
1329
+ )
1330
+ if run_git(project, "rev-parse", "--verify", "--quiet", tag_ref).returncode != 0:
1331
+ raise ArchiveError(f"missing milestone tag: {tag_name}")
1332
+ require_annotated_tag(
1333
+ project,
1334
+ tag_ref,
1335
+ merge_commit,
1336
+ f"milestone tag {tag_name}",
1337
+ )
1338
+
1339
+ # (c) The remote default must contain the merge commit.
1340
+ contains = run_git(project, "merge-base", "--is-ancestor", merge_commit, remote_default)
1341
+ if contains.returncode != 0:
1342
+ raise ArchiveError(f"{remote_default} does not contain the integration merge")
1343
+
1344
+ # (e) The bound branch tip and annotated tag must be published on origin.
1345
+ require_published_integration(
1346
+ project,
1347
+ bound_branch,
1348
+ ship_commit,
1349
+ tag_name,
1350
+ merge_commit,
1351
+ allow_missing_bound=state.integration == "pull-request",
1352
+ )
1353
+
1354
+ result = {
1355
+ "archive": configured,
1356
+ "commit": ship_commit,
1357
+ "integrate": merge_commit,
1358
+ "landing": merge_commit,
1359
+ "base": archive_milestone.require_git_success(
1360
+ run_git(project, "rev-parse", remote_default),
1361
+ "resolve integrated remote default",
1362
+ ),
1363
+ "mode": state.integration,
1364
+ "tag": tag_name,
1365
+ }
1366
+ if has_origin and not refresh:
1367
+ # Status must not fetch or write refs. Missing/stale local evidence is
1368
+ # unverified; the normal ship validator remains the refresh owner.
1369
+ refs = [(f"refs/heads/{default_name}", result["base"]),
1370
+ (f"refs/tags/{tag_name}", optional_ref(project, tag_ref))]
1371
+ if state.integration == "direct":
1372
+ refs.append((f"refs/heads/{bound_branch}", ship_commit))
1373
+ for ref, expected in refs:
1374
+ if live_remote_ref(project, ref) != expected:
1375
+ raise ArchiveError(f"published {ref} differs from local integration evidence")
1376
+ if pull_request is not None:
1377
+ result["pull_request"] = pull_request
1378
+ return result
1379
+
1380
+
1381
+ def require_published_integration(
1382
+ project: Path,
1383
+ bound_branch: str,
1384
+ ship_commit: str,
1385
+ tag_name: str,
1386
+ merge_commit: str,
1387
+ allow_missing_bound: bool = False,
1388
+ ) -> None:
1389
+ if allow_missing_bound:
1390
+ published_ship = live_remote_ref(project, f"refs/heads/{bound_branch}")
1391
+ if published_ship is not None and published_ship != ship_commit:
1392
+ raise ArchiveError(
1393
+ f"origin/{bound_branch} is {published_ship}, expected ship commit {ship_commit}"
1394
+ )
1395
+ published_tag = live_remote_ref(project, f"refs/tags/{tag_name}")
1396
+ if published_tag is None:
1397
+ raise ArchiveError(f"missing published milestone tag: origin/tags/{tag_name}")
1398
+ local_tag = optional_ref(project, f"refs/tags/{tag_name}")
1399
+ if local_tag != published_tag:
1400
+ raise ArchiveError(f"local and published milestone tag {tag_name} differ")
1401
+ require_annotated_tag(
1402
+ project,
1403
+ published_tag,
1404
+ merge_commit,
1405
+ f"published milestone tag {tag_name}",
1406
+ )
1407
+ return
1408
+
1409
+ bound_ref = f"refs/remotes/origin/{bound_branch}"
1410
+ if run_git(project, "rev-parse", "--verify", "--quiet", bound_ref).returncode != 0:
1411
+ if not allow_missing_bound:
1412
+ raise ArchiveError(f"missing published bound branch: origin/{bound_branch}")
1413
+ else:
1414
+ published_ship = archive_milestone.require_git_success(
1415
+ run_git(project, "rev-parse", bound_ref),
1416
+ "resolve published bound branch",
1417
+ )
1418
+ if published_ship != ship_commit:
1419
+ raise ArchiveError(
1420
+ f"origin/{bound_branch} is {published_ship}, expected ship commit {ship_commit}"
1421
+ )
1422
+
1423
+ remote_tag = f"refs/remotes/origin/tags/{tag_name}"
1424
+ if run_git(project, "rev-parse", "--verify", "--quiet", remote_tag).returncode != 0:
1425
+ raise ArchiveError(f"missing published milestone tag: origin/tags/{tag_name}")
1426
+ remote_tag_type = archive_milestone.require_git_success(
1427
+ run_git(project, "cat-file", "-t", remote_tag),
1428
+ "inspect published milestone tag type",
1429
+ )
1430
+ if remote_tag_type != "tag":
1431
+ raise ArchiveError(f"published milestone tag {tag_name} must be annotated")
1432
+ remote_tag_target = archive_milestone.require_git_success(
1433
+ run_git(project, "rev-parse", f"{remote_tag}^{{commit}}"),
1434
+ "resolve published milestone tag target",
1435
+ )
1436
+ if remote_tag_target != merge_commit:
1437
+ raise ArchiveError(
1438
+ f"published milestone tag {tag_name} does not point at the integration merge"
1439
+ )