@opengsd/gsd-path 1.4.0 → 1.5.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 (347) hide show
  1. package/CHANGELOG.md +325 -344
  2. package/DOCS.md +12 -2
  3. package/FULL.md +3 -2
  4. package/HOOKS.md +95 -8
  5. package/QUICK.md +1 -1
  6. package/README.md +78 -110
  7. package/RUNTIME.md +1 -1
  8. package/UPDATE.md +4 -3
  9. package/WORKFLOW.md +22 -1
  10. package/package.json +5 -2
  11. package/platforms/shared-agents/dispatch.md +13 -0
  12. package/scripts/_common.py +149 -1
  13. package/scripts/archive_milestone.py +14 -13
  14. package/scripts/build_recovery.py +96 -31
  15. package/scripts/build_state.py +79 -3
  16. package/scripts/check_docs_audit.py +77 -4
  17. package/scripts/check_handoffs.py +546 -30
  18. package/scripts/check_task_briefs.py +279 -23
  19. package/scripts/detect_project.py +243 -18
  20. package/scripts/discussion_validate.py +86 -29
  21. package/scripts/dispatch_driver.py +392 -22
  22. package/scripts/git_guard.py +3 -3
  23. package/scripts/guard_hook.py +1416 -165
  24. package/scripts/install.mjs +23 -8
  25. package/scripts/install.py +49 -9
  26. package/scripts/integration.py +146 -1
  27. package/scripts/isolation.py +392 -60
  28. package/scripts/loop_run.py +16 -4
  29. package/scripts/members.py +11 -9
  30. package/scripts/path_config.py +3 -3
  31. package/scripts/pipeline_git.py +63 -12
  32. package/scripts/pipeline_state.py +111 -33
  33. package/scripts/pipeline_undo.py +101 -8
  34. package/scripts/runtime_store.py +20 -2
  35. package/scripts/skill-resources.json +9 -2
  36. package/scripts/state_checkpoint.py +9 -27
  37. package/scripts/status_runtime.py +22 -4
  38. package/scripts/task_context.py +12 -3
  39. package/scripts/wizard.mjs +1 -0
  40. package/scripts/workflow_run.py +5 -2
  41. package/scripts/worktree_paths.py +54 -2
  42. package/skills/gsd-path/DEFINE.md +28 -3
  43. package/skills/gsd-path/PLAN.md +11 -3
  44. package/skills/gsd-path/RESEARCH.md +11 -0
  45. package/skills/gsd-path/SHIP.md +6 -1
  46. package/skills/gsd-path/references/build-native.md +16 -1
  47. package/skills/gsd-path/references/dispatch.md +13 -0
  48. package/skills/gsd-path/references/docs-auditor.md +5 -0
  49. package/skills/gsd-path/references/planner.md +14 -4
  50. package/skills/gsd-path/references/reviewer-final.md +20 -2
  51. package/skills/gsd-path/references/reviewer-wave.md +15 -4
  52. package/skills/gsd-path/references/reviewer.md +3 -1
  53. package/skills/gsd-path/references/spec-probes.md +150 -0
  54. package/skills/gsd-path/scripts/_common.py +149 -1
  55. package/skills/gsd-path/scripts/archive_milestone.py +14 -13
  56. package/skills/gsd-path/scripts/build_recovery.py +96 -31
  57. package/skills/gsd-path/scripts/build_state.py +79 -3
  58. package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
  59. package/skills/gsd-path/scripts/check_handoffs.py +546 -30
  60. package/skills/gsd-path/scripts/check_task_briefs.py +279 -23
  61. package/skills/gsd-path/scripts/detect_project.py +243 -18
  62. package/skills/gsd-path/scripts/discussion_validate.py +86 -29
  63. package/skills/gsd-path/scripts/dispatch_driver.py +392 -22
  64. package/skills/gsd-path/scripts/git_guard.py +3 -3
  65. package/skills/gsd-path/scripts/integration.py +146 -1
  66. package/skills/gsd-path/scripts/isolation.py +392 -60
  67. package/skills/gsd-path/scripts/loop_run.py +16 -4
  68. package/skills/gsd-path/scripts/members.py +11 -9
  69. package/skills/gsd-path/scripts/path_config.py +3 -3
  70. package/skills/gsd-path/scripts/pipeline_git.py +63 -12
  71. package/skills/gsd-path/scripts/pipeline_state.py +111 -33
  72. package/skills/gsd-path/scripts/state_checkpoint.py +9 -27
  73. package/skills/gsd-path/scripts/status_runtime.py +22 -4
  74. package/skills/gsd-path/scripts/task_context.py +12 -3
  75. package/skills/gsd-path/scripts/workflow_run.py +5 -2
  76. package/skills/gsd-path/scripts/worktree_paths.py +54 -2
  77. package/skills/gsd-path/templates/docs-audit.md +5 -1
  78. package/skills/gsd-path/templates/final-review.md +11 -2
  79. package/skills/gsd-path/templates/intent.md +30 -0
  80. package/skills/gsd-path/templates/patch-findings.md +3 -3
  81. package/skills/gsd-path/templates/plan.md +17 -2
  82. package/skills/gsd-path/templates/research-handoff.md +14 -3
  83. package/skills/gsd-path/templates/state.md +1 -1
  84. package/skills/gsd-path/templates/wave-review.md +16 -6
  85. package/skills/gsd-path-build/references/build-native.md +16 -1
  86. package/skills/gsd-path-build/references/dispatch.md +13 -0
  87. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  88. package/skills/gsd-path-build/references/reviewer-wave.md +15 -4
  89. package/skills/gsd-path-build/references/reviewer.md +3 -1
  90. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  91. package/skills/gsd-path-build/scripts/_common.py +149 -1
  92. package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
  93. package/skills/gsd-path-build/scripts/build_recovery.py +96 -31
  94. package/skills/gsd-path-build/scripts/build_state.py +79 -3
  95. package/skills/gsd-path-build/scripts/check_handoffs.py +546 -30
  96. package/skills/gsd-path-build/scripts/check_task_briefs.py +279 -23
  97. package/skills/gsd-path-build/scripts/detect_project.py +243 -18
  98. package/skills/gsd-path-build/scripts/discussion_validate.py +86 -29
  99. package/skills/gsd-path-build/scripts/dispatch_driver.py +392 -22
  100. package/skills/gsd-path-build/scripts/integration.py +146 -1
  101. package/skills/gsd-path-build/scripts/isolation.py +392 -60
  102. package/skills/gsd-path-build/scripts/loop_run.py +16 -4
  103. package/skills/gsd-path-build/scripts/members.py +11 -9
  104. package/skills/gsd-path-build/scripts/path_config.py +3 -3
  105. package/skills/gsd-path-build/scripts/pipeline_git.py +63 -12
  106. package/skills/gsd-path-build/scripts/pipeline_state.py +111 -33
  107. package/skills/gsd-path-build/scripts/state_checkpoint.py +9 -27
  108. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  109. package/skills/gsd-path-build/scripts/workflow_run.py +5 -2
  110. package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
  111. package/skills/gsd-path-build/templates/wave-review.md +16 -6
  112. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  113. package/skills/gsd-path-decide/scripts/_common.py +149 -1
  114. package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
  115. package/skills/gsd-path-decide/scripts/build_recovery.py +96 -31
  116. package/skills/gsd-path-decide/scripts/check_handoffs.py +546 -30
  117. package/skills/gsd-path-decide/scripts/check_task_briefs.py +279 -23
  118. package/skills/gsd-path-decide/scripts/detect_project.py +243 -18
  119. package/skills/gsd-path-decide/scripts/discussion_validate.py +86 -29
  120. package/skills/gsd-path-decide/scripts/integration.py +146 -1
  121. package/skills/gsd-path-decide/scripts/isolation.py +392 -60
  122. package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
  123. package/skills/gsd-path-decide/scripts/members.py +11 -9
  124. package/skills/gsd-path-decide/scripts/path_config.py +3 -3
  125. package/skills/gsd-path-decide/scripts/pipeline_git.py +63 -12
  126. package/skills/gsd-path-decide/scripts/pipeline_state.py +111 -33
  127. package/skills/gsd-path-decide/scripts/state_checkpoint.py +9 -27
  128. package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
  129. package/skills/gsd-path-define/SKILL.md +28 -3
  130. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  131. package/skills/gsd-path-define/scripts/_common.py +149 -1
  132. package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
  133. package/skills/gsd-path-define/scripts/build_recovery.py +96 -31
  134. package/skills/gsd-path-define/scripts/check_handoffs.py +546 -30
  135. package/skills/gsd-path-define/scripts/check_task_briefs.py +279 -23
  136. package/skills/gsd-path-define/scripts/detect_project.py +243 -18
  137. package/skills/gsd-path-define/scripts/discussion_validate.py +86 -29
  138. package/skills/gsd-path-define/scripts/integration.py +146 -1
  139. package/skills/gsd-path-define/scripts/isolation.py +392 -60
  140. package/skills/gsd-path-define/scripts/loop_run.py +16 -4
  141. package/skills/gsd-path-define/scripts/members.py +11 -9
  142. package/skills/gsd-path-define/scripts/path_config.py +3 -3
  143. package/skills/gsd-path-define/scripts/pipeline_git.py +63 -12
  144. package/skills/gsd-path-define/scripts/pipeline_state.py +111 -33
  145. package/skills/gsd-path-define/scripts/state_checkpoint.py +9 -27
  146. package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
  147. package/skills/gsd-path-define/templates/intent.md +30 -0
  148. package/skills/gsd-path-define/templates/state.md +1 -1
  149. package/skills/gsd-path-discuss/scripts/_common.py +149 -1
  150. package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
  151. package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
  152. package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
  153. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  154. package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
  155. package/skills/gsd-path-docs-audit/scripts/_common.py +149 -1
  156. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
  157. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +96 -31
  158. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
  159. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +546 -30
  160. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +279 -23
  161. package/skills/gsd-path-docs-audit/scripts/detect_project.py +243 -18
  162. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +86 -29
  163. package/skills/gsd-path-docs-audit/scripts/integration.py +146 -1
  164. package/skills/gsd-path-docs-audit/scripts/isolation.py +392 -60
  165. package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
  166. package/skills/gsd-path-docs-audit/scripts/members.py +11 -9
  167. package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
  168. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +63 -12
  169. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +111 -33
  170. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +9 -27
  171. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
  172. package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
  173. package/skills/gsd-path-forensics/scripts/_common.py +149 -1
  174. package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
  175. package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
  176. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +101 -8
  177. package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
  178. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  179. package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
  180. package/skills/gsd-path-inspect/scripts/_common.py +149 -1
  181. package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
  182. package/skills/gsd-path-inspect/scripts/build_recovery.py +96 -31
  183. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
  184. package/skills/gsd-path-inspect/scripts/check_handoffs.py +546 -30
  185. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +279 -23
  186. package/skills/gsd-path-inspect/scripts/detect_project.py +243 -18
  187. package/skills/gsd-path-inspect/scripts/discussion_validate.py +86 -29
  188. package/skills/gsd-path-inspect/scripts/integration.py +146 -1
  189. package/skills/gsd-path-inspect/scripts/isolation.py +392 -60
  190. package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
  191. package/skills/gsd-path-inspect/scripts/members.py +11 -9
  192. package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
  193. package/skills/gsd-path-inspect/scripts/pipeline_git.py +63 -12
  194. package/skills/gsd-path-inspect/scripts/pipeline_state.py +111 -33
  195. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +9 -27
  196. package/skills/gsd-path-inspect/scripts/workflow_run.py +5 -2
  197. package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
  198. package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
  199. package/skills/gsd-path-inspect/templates/state.md +1 -1
  200. package/skills/gsd-path-loop/scripts/_common.py +149 -1
  201. package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
  202. package/skills/gsd-path-loop/scripts/path_config.py +3 -3
  203. package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
  204. package/skills/gsd-path-migrate/scripts/_common.py +149 -1
  205. package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
  206. package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
  207. package/skills/gsd-path-plan/SKILL.md +11 -3
  208. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  209. package/skills/gsd-path-plan/references/planner.md +14 -4
  210. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  211. package/skills/gsd-path-plan/scripts/_common.py +149 -1
  212. package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
  213. package/skills/gsd-path-plan/scripts/build_recovery.py +96 -31
  214. package/skills/gsd-path-plan/scripts/check_handoffs.py +546 -30
  215. package/skills/gsd-path-plan/scripts/check_task_briefs.py +279 -23
  216. package/skills/gsd-path-plan/scripts/detect_project.py +243 -18
  217. package/skills/gsd-path-plan/scripts/discussion_validate.py +86 -29
  218. package/skills/gsd-path-plan/scripts/integration.py +146 -1
  219. package/skills/gsd-path-plan/scripts/isolation.py +392 -60
  220. package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
  221. package/skills/gsd-path-plan/scripts/members.py +11 -9
  222. package/skills/gsd-path-plan/scripts/path_config.py +3 -3
  223. package/skills/gsd-path-plan/scripts/pipeline_git.py +63 -12
  224. package/skills/gsd-path-plan/scripts/pipeline_state.py +111 -33
  225. package/skills/gsd-path-plan/scripts/state_checkpoint.py +9 -27
  226. package/skills/gsd-path-plan/scripts/workflow_run.py +5 -2
  227. package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
  228. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  229. package/skills/gsd-path-plan/templates/plan.md +17 -2
  230. package/skills/gsd-path-research/SKILL.md +11 -0
  231. package/skills/gsd-path-research/references/dispatch.md +13 -0
  232. package/skills/gsd-path-research/scripts/_common.py +149 -1
  233. package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
  234. package/skills/gsd-path-research/scripts/build_recovery.py +96 -31
  235. package/skills/gsd-path-research/scripts/check_handoffs.py +546 -30
  236. package/skills/gsd-path-research/scripts/check_task_briefs.py +279 -23
  237. package/skills/gsd-path-research/scripts/detect_project.py +243 -18
  238. package/skills/gsd-path-research/scripts/discussion_validate.py +86 -29
  239. package/skills/gsd-path-research/scripts/integration.py +146 -1
  240. package/skills/gsd-path-research/scripts/isolation.py +392 -60
  241. package/skills/gsd-path-research/scripts/loop_run.py +16 -4
  242. package/skills/gsd-path-research/scripts/members.py +11 -9
  243. package/skills/gsd-path-research/scripts/path_config.py +3 -3
  244. package/skills/gsd-path-research/scripts/pipeline_git.py +63 -12
  245. package/skills/gsd-path-research/scripts/pipeline_state.py +111 -33
  246. package/skills/gsd-path-research/scripts/state_checkpoint.py +9 -27
  247. package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
  248. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  249. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  250. package/skills/gsd-path-roadmap/scripts/_common.py +149 -1
  251. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
  252. package/skills/gsd-path-roadmap/scripts/build_recovery.py +96 -31
  253. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +546 -30
  254. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +279 -23
  255. package/skills/gsd-path-roadmap/scripts/detect_project.py +243 -18
  256. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +86 -29
  257. package/skills/gsd-path-roadmap/scripts/integration.py +146 -1
  258. package/skills/gsd-path-roadmap/scripts/isolation.py +392 -60
  259. package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
  260. package/skills/gsd-path-roadmap/scripts/members.py +11 -9
  261. package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
  262. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +63 -12
  263. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +111 -33
  264. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +9 -27
  265. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
  266. package/skills/gsd-path-ship/SKILL.md +6 -1
  267. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  268. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  269. package/skills/gsd-path-ship/references/reviewer-wave.md +15 -4
  270. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  271. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  272. package/skills/gsd-path-ship/scripts/_common.py +149 -1
  273. package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
  274. package/skills/gsd-path-ship/scripts/build_recovery.py +96 -31
  275. package/skills/gsd-path-ship/scripts/build_state.py +79 -3
  276. package/skills/gsd-path-ship/scripts/check_handoffs.py +546 -30
  277. package/skills/gsd-path-ship/scripts/check_task_briefs.py +279 -23
  278. package/skills/gsd-path-ship/scripts/detect_project.py +243 -18
  279. package/skills/gsd-path-ship/scripts/discussion_validate.py +86 -29
  280. package/skills/gsd-path-ship/scripts/integration.py +146 -1
  281. package/skills/gsd-path-ship/scripts/isolation.py +392 -60
  282. package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
  283. package/skills/gsd-path-ship/scripts/members.py +11 -9
  284. package/skills/gsd-path-ship/scripts/path_config.py +3 -3
  285. package/skills/gsd-path-ship/scripts/pipeline_git.py +63 -12
  286. package/skills/gsd-path-ship/scripts/pipeline_state.py +111 -33
  287. package/skills/gsd-path-ship/scripts/state_checkpoint.py +9 -27
  288. package/skills/gsd-path-ship/scripts/workflow_run.py +5 -2
  289. package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
  290. package/skills/gsd-path-ship/templates/final-review.md +11 -2
  291. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  292. package/skills/gsd-path-ship/templates/wave-review.md +16 -6
  293. package/skills/gsd-path-undo/scripts/_common.py +149 -1
  294. package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
  295. package/skills/gsd-path-undo/scripts/path_config.py +3 -3
  296. package/skills/gsd-path-undo/scripts/pipeline_undo.py +101 -8
  297. package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
  298. package/skills/path/DEFINE.md +28 -3
  299. package/skills/path/PLAN.md +11 -3
  300. package/skills/path/RESEARCH.md +11 -0
  301. package/skills/path/SHIP.md +6 -1
  302. package/skills/path/references/build-native.md +16 -1
  303. package/skills/path/references/dispatch.md +13 -0
  304. package/skills/path/references/docs-auditor.md +5 -0
  305. package/skills/path/references/planner.md +14 -4
  306. package/skills/path/references/reviewer-final.md +20 -2
  307. package/skills/path/references/reviewer-wave.md +15 -4
  308. package/skills/path/references/reviewer.md +3 -1
  309. package/skills/path/references/spec-probes.md +150 -0
  310. package/skills/path/scripts/_common.py +149 -1
  311. package/skills/path/scripts/archive_milestone.py +14 -13
  312. package/skills/path/scripts/build_recovery.py +96 -31
  313. package/skills/path/scripts/build_state.py +79 -3
  314. package/skills/path/scripts/check_docs_audit.py +77 -4
  315. package/skills/path/scripts/check_handoffs.py +546 -30
  316. package/skills/path/scripts/check_task_briefs.py +279 -23
  317. package/skills/path/scripts/detect_project.py +243 -18
  318. package/skills/path/scripts/discussion_validate.py +86 -29
  319. package/skills/path/scripts/dispatch_driver.py +392 -22
  320. package/skills/path/scripts/git_guard.py +3 -3
  321. package/skills/path/scripts/integration.py +146 -1
  322. package/skills/path/scripts/isolation.py +392 -60
  323. package/skills/path/scripts/loop_run.py +16 -4
  324. package/skills/path/scripts/members.py +11 -9
  325. package/skills/path/scripts/path_config.py +3 -3
  326. package/skills/path/scripts/pipeline_git.py +63 -12
  327. package/skills/path/scripts/pipeline_state.py +111 -33
  328. package/skills/path/scripts/state_checkpoint.py +9 -27
  329. package/skills/path/scripts/status_runtime.py +22 -4
  330. package/skills/path/scripts/task_context.py +12 -3
  331. package/skills/path/scripts/workflow_run.py +5 -2
  332. package/skills/path/scripts/worktree_paths.py +54 -2
  333. package/skills/path/templates/docs-audit.md +5 -1
  334. package/skills/path/templates/final-review.md +11 -2
  335. package/skills/path/templates/intent.md +30 -0
  336. package/skills/path/templates/patch-findings.md +3 -3
  337. package/skills/path/templates/plan.md +17 -2
  338. package/skills/path/templates/research-handoff.md +14 -3
  339. package/skills/path/templates/state.md +1 -1
  340. package/skills/path/templates/wave-review.md +16 -6
  341. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  342. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  343. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  344. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  345. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  346. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  347. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -65,6 +65,49 @@ VERIFY_RESULTS = ("pass", "fail")
65
65
  VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
66
66
  VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
67
67
 
68
+ REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
69
+ REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
70
+
71
+
72
+ def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
73
+ return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
74
+
75
+
76
+ def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
77
+ if inline:
78
+ return inline
79
+ collected: list[str] = []
80
+ for line in lines[index + 1 :]:
81
+ if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
82
+ line
83
+ ):
84
+ break
85
+ if line and not line[0].isspace():
86
+ break
87
+ stripped = line.strip()
88
+ if stripped:
89
+ collected.append(stripped)
90
+ return "\n".join(collected)
91
+
92
+
93
+ def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
94
+ pattern = review_bullet_field_label_pattern(field)
95
+ values: list[str] = []
96
+ for index, line in enumerate(lines):
97
+ match = pattern.fullmatch(line)
98
+ if match is None:
99
+ continue
100
+ values.append(_review_bullet_field_value(lines, index, match.group(1)))
101
+ return values
102
+
103
+
104
+ def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
105
+ """Parse one `- **Field**:` review bullet, including indented continuations."""
106
+ values = find_review_bullet_field_values(lines, field)
107
+ if len(values) != 1:
108
+ raise ValueError(field)
109
+ return values[0]
110
+
68
111
 
69
112
  # Sample paths the pipeline must commit. A product rule such as an unanchored
70
113
  # `build/` would otherwise drop them silently from every commit.
@@ -150,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
150
193
  return parse_verify_ledger(path.read_text(encoding="utf-8"))
151
194
 
152
195
 
196
+ def read_user_text(path: Path) -> str:
197
+ """Read a user-edited text file with CRLF normalized to LF."""
198
+ return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
199
+
200
+
153
201
  def parse_verify_ledger(text: str) -> list:
154
202
  entries = []
155
203
  for number, line in enumerate(text.splitlines(), start=1):
@@ -204,7 +252,43 @@ def run_command(
204
252
  def run_git(
205
253
  repo: Path, *arguments: str, input: Optional[str] = None
206
254
  ) -> subprocess.CompletedProcess[str]:
207
- return _run_exact(("git", "-C", str(repo), *arguments), input=input)
255
+ return _run_exact(git_command(repo, *arguments), input=input)
256
+
257
+
258
+ def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
259
+ """Git argv for repo, including WSL drvfs workarounds when needed."""
260
+ return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
261
+
262
+
263
+ def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
264
+ """Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
265
+ if os.name != "posix":
266
+ return ()
267
+ try:
268
+ root = repo.resolve()
269
+ mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
270
+ except OSError:
271
+ return ()
272
+ best = ""
273
+ fstype = ""
274
+ for line in mounts:
275
+ parts = line.split()
276
+ if len(parts) < 3:
277
+ continue
278
+ mount_point, mount_type = parts[1], parts[2]
279
+ if mount_type not in {"9p", "drvfs"}:
280
+ continue
281
+ if root == Path(mount_point) or root.is_relative_to(mount_point):
282
+ if len(mount_point) >= len(best):
283
+ best, fstype = mount_point, mount_type
284
+ if not fstype:
285
+ return ()
286
+ return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
287
+
288
+
289
+ def git_index_lock_retryable(stderr: str) -> bool:
290
+ text = stderr.casefold()
291
+ return "index.lock" in text and "file exists" in text
208
292
 
209
293
 
210
294
  # OS files that are never pipeline artifacts when git ignores them.
@@ -227,6 +311,70 @@ def git_visible_entries(directory: Path) -> set:
227
311
  return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
228
312
 
229
313
 
314
+ RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
315
+ RUNTIME_PIN_ALLOWANCE = (
316
+ "only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
317
+ )
318
+
319
+
320
+ def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
321
+ """True when a review of `reviewed` still covers `head`.
322
+
323
+ That is the same commit, or `reviewed` is an ancestor of `head` and every
324
+ commit on the first-parent path in ``reviewed..head`` is a non-merge commit
325
+ whose only change is ``.gsd-path/runtime.json``. An empty commit does not
326
+ qualify. Any other path, any merge commit, an unresolvable SHA, or a
327
+ non-ancestor returns False.
328
+ """
329
+ if not reviewed or not head:
330
+ return False
331
+ resolved_repo = Path(repo)
332
+ resolved: List[str] = []
333
+ for value in (reviewed, head):
334
+ result = run_git(
335
+ resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
336
+ )
337
+ if result.returncode != 0 or not result.stdout.strip():
338
+ return False
339
+ resolved.append(result.stdout.strip())
340
+ reviewed_sha, head_sha = resolved
341
+ if reviewed_sha == head_sha:
342
+ return True
343
+ if (
344
+ run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
345
+ != 0
346
+ ):
347
+ return False
348
+ listing = run_git(
349
+ resolved_repo,
350
+ "rev-list",
351
+ "--first-parent",
352
+ "--parents",
353
+ f"{reviewed_sha}..{head_sha}",
354
+ )
355
+ if listing.returncode != 0:
356
+ return False
357
+ for line in listing.stdout.splitlines():
358
+ fields = line.split()
359
+ if len(fields) != 2:
360
+ return False
361
+ commit_sha, parent_sha = fields[0], fields[1]
362
+ changed = run_git(
363
+ resolved_repo,
364
+ "diff",
365
+ "--name-only",
366
+ "--no-renames",
367
+ "--no-ext-diff",
368
+ parent_sha,
369
+ commit_sha,
370
+ )
371
+ if changed.returncode != 0:
372
+ return False
373
+ if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
374
+ return False
375
+ return True
376
+
377
+
230
378
  def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
231
379
  temporary_path.parent.mkdir(parents=True, exist_ok=True)
232
380
  descriptor = None
@@ -514,16 +514,28 @@ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
514
514
  """Run one spec command; a hung command counts as failed (exit None).
515
515
 
516
516
  POSIX runs /bin/sh as shell=True did; Windows runs Git for Windows bash, not
517
- cmd.exe. A timeout kills the whole process group, so a grandchild cannot
518
- keep the output pipes open.
517
+ cmd.exe. A timeout kills the whole process group when possible; if output
518
+ pipes stay open, drain them with a short grace timeout and then close them.
519
519
  """
520
520
  argv = _common.bash_argv(command) if os.name == "nt" else ["/bin/sh", "-c", command]
521
521
  process = _common.popen_group(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
522
+ stdout = b""
523
+ stderr = b""
522
524
  try:
523
525
  stdout, stderr = process.communicate(timeout=timeout)
524
- except subprocess.TimeoutExpired:
526
+ except subprocess.TimeoutExpired as expired:
525
527
  _common.kill_tree(process)
526
- stdout, stderr = process.communicate()
528
+ try:
529
+ stdout, stderr = process.communicate(timeout=5)
530
+ except subprocess.TimeoutExpired as drained:
531
+ stdout = drained.stdout or expired.stdout or b""
532
+ stderr = drained.stderr or expired.stderr or b""
533
+ if process.stdout:
534
+ process.stdout.close()
535
+ if process.stderr:
536
+ process.stderr.close()
537
+ process.kill()
538
+ process.wait(timeout=5)
527
539
  output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
528
540
  return None, f"{output}\ntimed out at the admitted wall-clock deadline"
529
541
  return process.returncode, (stdout + stderr).decode("utf-8", errors="replace")
@@ -13,7 +13,7 @@ sys.dont_write_bytecode = True
13
13
  ROLES = ('coder', 'reviewer', 'review_panel', 'skeptic', 'inspect_codebase',
14
14
  'inspect_docs', 'docs_audit', 'research', 'decide', 'roadmap', 'plan', 'plan_patch')
15
15
  HOSTS = ('codex', 'claude', 'grok', 'opencode', 'copilot', 'qwen', 'antigravity',
16
- 'cursor', 'zed', 'kiro', 'kimi')
16
+ 'cursor', 'zed', 'kiro', 'kimi', 'muse')
17
17
 
18
18
 
19
19
  def sibling(name):
@@ -73,8 +73,8 @@ def validate_value(key, value):
73
73
  if not isinstance(value, str) or not value or value != value.strip():
74
74
  raise ValueError(f'{key} requires a nonempty string')
75
75
  if key == 'integration':
76
- if value not in ('direct', 'pull-request'):
77
- raise ValueError('integration must be direct or pull-request')
76
+ if value not in ('direct', 'pull-request', 'external-landing'):
77
+ raise ValueError('integration must be direct, pull-request, or external-landing')
78
78
  elif key == 'review_panel':
79
79
  if any(char.isspace() for char in value):
80
80
  raise ValueError('use off, detected, or comma-separated families without spaces')
@@ -22,6 +22,27 @@ else:
22
22
  from scripts import _common
23
23
 
24
24
 
25
+ def _worktree_list_porcelain(primary: Path) -> tuple[str, bool]:
26
+ try:
27
+ if __package__:
28
+ from . import pipeline_git as pg
29
+ else:
30
+ from scripts import pipeline_git as pg
31
+ except ImportError:
32
+ try:
33
+ import pipeline_git as pg
34
+ except ImportError:
35
+ pg = None
36
+ if pg is not None:
37
+ return pg.worktree_list_porcelain(primary)
38
+ try:
39
+ output = _git(primary, "worktree", "list", "--porcelain", "-z")
40
+ return output, True
41
+ except ValueError:
42
+ output = _git(primary, "worktree", "list", "--porcelain")
43
+ return output, False
44
+
45
+
25
46
  def _git(primary: Path, *args: str) -> str:
26
47
  result = _common.run_git(primary, *args)
27
48
  if result.returncode:
@@ -82,6 +103,33 @@ def _workspace(primary: Path, pin: bool) -> Path:
82
103
  return workspace
83
104
 
84
105
 
106
+ def release_workspace(primary: Path) -> bool:
107
+ """Forget the pinned placement receipt once no linked worktrees remain.
108
+
109
+ Lets GSD_PATH_WORKTREE_ROOT (or a new default) apply again at the next
110
+ milestone's first allocation. Returns True when a receipt was removed;
111
+ any remaining linked worktree (including one only still registered) keeps
112
+ the pin.
113
+ """
114
+ primary = primary.resolve()
115
+ common = Path(_git(primary, "rev-parse", "--git-common-dir"))
116
+ common = (primary / common).resolve()
117
+ receipt = common / "gsd-path" / "workspaces" / f"{_identity(primary)}.json"
118
+ if not receipt.is_file():
119
+ return False
120
+ output, nul_separated = _worktree_list_porcelain(primary)
121
+ records = output.split("\0\0") if nul_separated else [
122
+ block for block in output.strip().split("\n\n") if block.strip()
123
+ ]
124
+ for record in records:
125
+ fields = record.split("\0") if nul_separated else record.splitlines()
126
+ for field in fields:
127
+ if field.startswith("worktree ") and Path(field[len("worktree "):]).resolve() != primary:
128
+ return False
129
+ receipt.unlink()
130
+ return True
131
+
132
+
85
133
  def worktree_path(primary: Path, kind: str, name: str, *, pin: bool = False) -> Path:
86
134
  primary = primary.resolve()
87
135
  if kind not in {"task", "verify", "integrate", "bound"} or not name or Path(name).name != name or name in {".", ".."}:
@@ -91,9 +139,13 @@ def worktree_path(primary: Path, kind: str, name: str, *, pin: bool = False) ->
91
139
  legacy = (primary.parent / f".{primary.name}-gsd-path-integrate-{name}"
92
140
  if kind == "integrate" else primary.parent / f"{primary.name}.gsd-path" / kind / name)
93
141
  branch = f"refs/heads/gsd-path-{kind}/{name}"
94
- records = _git(primary, "worktree", "list", "--porcelain", "-z").split("\0\0")
142
+ output, nul_separated = _worktree_list_porcelain(primary)
143
+ if nul_separated:
144
+ records = output.split("\0\0")
145
+ else:
146
+ records = [block for block in output.strip().split("\n\n") if block.strip()]
95
147
  for record in records:
96
- fields = record.split("\0")
148
+ fields = record.split("\0") if nul_separated else record.splitlines()
97
149
  # Compare as paths: git prints forward slashes on Windows.
98
150
  worktrees = [Path(field[len("worktree "):]) for field in fields if field.startswith("worktree ")]
99
151
  if legacy in worktrees and f"branch {branch}" in fields:
@@ -44,6 +44,19 @@ use exactly one matching branch:
44
44
  create another worktree. Encode task dependencies explicitly; launch
45
45
  independent children concurrently up to the host's advertised
46
46
  concurrent-subagent capacity.
47
+ - A `muse.subagent_spawn` tool (with `muse.subagent_wait`,
48
+ `muse.subagent_read_result`, and `muse.subagent_send_message` siblings) is
49
+ Muse Code. Omit `subagent_type` (general-purpose, full capability), pass the
50
+ logical task name as `task_name` and `role`, and the complete brief as
51
+ `objective`. Never set `worktree_isolation`. Keep the returned
52
+ `subagent_id`; send a correction to a still-live child with
53
+ `muse.subagent_send_message` mode `followup` and the complete new prompt,
54
+ otherwise start a fresh child. Collect with `muse.subagent_wait`
55
+ (`wait_for: task_terminal`) then `muse.subagent_read_result`; on
56
+ `root_capacity_exhausted`, wait for a child to finish and retry. Spawn has no
57
+ model or effort argument, so record no such capability. If only
58
+ `muse.workflow` is exposed (untrusted workspace), do not script or inline
59
+ the work; stop and report that the workspace must be trusted.
47
60
 
48
61
  For every branch:
49
62
 
@@ -59,6 +59,11 @@ actually does. Read-only: change nothing outside your single output file.
59
59
  5. **Classify remediation** for every non-verified claim: `fix-doc`,
60
60
  `fix-code`, or `NEEDS-USER` when the right side of the conflict is not
61
61
  yours to decide.
62
+ Number queue rows with unique, strictly increasing positive integers; they
63
+ need not be contiguous. A row that persists from the prior audit keeps its
64
+ number. A new row takes a number greater than every prior queue number and
65
+ every `## User rulings` Queue #. Never reuse a retired number: a ruling
66
+ stays bound to the item it was made for.
62
67
 
63
68
  ## Rules
64
69
 
@@ -65,6 +65,49 @@ VERIFY_RESULTS = ("pass", "fail")
65
65
  VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
66
66
  VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
67
67
 
68
+ REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
69
+ REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
70
+
71
+
72
+ def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
73
+ return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
74
+
75
+
76
+ def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
77
+ if inline:
78
+ return inline
79
+ collected: list[str] = []
80
+ for line in lines[index + 1 :]:
81
+ if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
82
+ line
83
+ ):
84
+ break
85
+ if line and not line[0].isspace():
86
+ break
87
+ stripped = line.strip()
88
+ if stripped:
89
+ collected.append(stripped)
90
+ return "\n".join(collected)
91
+
92
+
93
+ def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
94
+ pattern = review_bullet_field_label_pattern(field)
95
+ values: list[str] = []
96
+ for index, line in enumerate(lines):
97
+ match = pattern.fullmatch(line)
98
+ if match is None:
99
+ continue
100
+ values.append(_review_bullet_field_value(lines, index, match.group(1)))
101
+ return values
102
+
103
+
104
+ def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
105
+ """Parse one `- **Field**:` review bullet, including indented continuations."""
106
+ values = find_review_bullet_field_values(lines, field)
107
+ if len(values) != 1:
108
+ raise ValueError(field)
109
+ return values[0]
110
+
68
111
 
69
112
  # Sample paths the pipeline must commit. A product rule such as an unanchored
70
113
  # `build/` would otherwise drop them silently from every commit.
@@ -150,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
150
193
  return parse_verify_ledger(path.read_text(encoding="utf-8"))
151
194
 
152
195
 
196
+ def read_user_text(path: Path) -> str:
197
+ """Read a user-edited text file with CRLF normalized to LF."""
198
+ return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
199
+
200
+
153
201
  def parse_verify_ledger(text: str) -> list:
154
202
  entries = []
155
203
  for number, line in enumerate(text.splitlines(), start=1):
@@ -204,7 +252,43 @@ def run_command(
204
252
  def run_git(
205
253
  repo: Path, *arguments: str, input: Optional[str] = None
206
254
  ) -> subprocess.CompletedProcess[str]:
207
- return _run_exact(("git", "-C", str(repo), *arguments), input=input)
255
+ return _run_exact(git_command(repo, *arguments), input=input)
256
+
257
+
258
+ def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
259
+ """Git argv for repo, including WSL drvfs workarounds when needed."""
260
+ return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
261
+
262
+
263
+ def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
264
+ """Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
265
+ if os.name != "posix":
266
+ return ()
267
+ try:
268
+ root = repo.resolve()
269
+ mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
270
+ except OSError:
271
+ return ()
272
+ best = ""
273
+ fstype = ""
274
+ for line in mounts:
275
+ parts = line.split()
276
+ if len(parts) < 3:
277
+ continue
278
+ mount_point, mount_type = parts[1], parts[2]
279
+ if mount_type not in {"9p", "drvfs"}:
280
+ continue
281
+ if root == Path(mount_point) or root.is_relative_to(mount_point):
282
+ if len(mount_point) >= len(best):
283
+ best, fstype = mount_point, mount_type
284
+ if not fstype:
285
+ return ()
286
+ return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
287
+
288
+
289
+ def git_index_lock_retryable(stderr: str) -> bool:
290
+ text = stderr.casefold()
291
+ return "index.lock" in text and "file exists" in text
208
292
 
209
293
 
210
294
  # OS files that are never pipeline artifacts when git ignores them.
@@ -227,6 +311,70 @@ def git_visible_entries(directory: Path) -> set:
227
311
  return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
228
312
 
229
313
 
314
+ RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
315
+ RUNTIME_PIN_ALLOWANCE = (
316
+ "only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
317
+ )
318
+
319
+
320
+ def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
321
+ """True when a review of `reviewed` still covers `head`.
322
+
323
+ That is the same commit, or `reviewed` is an ancestor of `head` and every
324
+ commit on the first-parent path in ``reviewed..head`` is a non-merge commit
325
+ whose only change is ``.gsd-path/runtime.json``. An empty commit does not
326
+ qualify. Any other path, any merge commit, an unresolvable SHA, or a
327
+ non-ancestor returns False.
328
+ """
329
+ if not reviewed or not head:
330
+ return False
331
+ resolved_repo = Path(repo)
332
+ resolved: List[str] = []
333
+ for value in (reviewed, head):
334
+ result = run_git(
335
+ resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
336
+ )
337
+ if result.returncode != 0 or not result.stdout.strip():
338
+ return False
339
+ resolved.append(result.stdout.strip())
340
+ reviewed_sha, head_sha = resolved
341
+ if reviewed_sha == head_sha:
342
+ return True
343
+ if (
344
+ run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
345
+ != 0
346
+ ):
347
+ return False
348
+ listing = run_git(
349
+ resolved_repo,
350
+ "rev-list",
351
+ "--first-parent",
352
+ "--parents",
353
+ f"{reviewed_sha}..{head_sha}",
354
+ )
355
+ if listing.returncode != 0:
356
+ return False
357
+ for line in listing.stdout.splitlines():
358
+ fields = line.split()
359
+ if len(fields) != 2:
360
+ return False
361
+ commit_sha, parent_sha = fields[0], fields[1]
362
+ changed = run_git(
363
+ resolved_repo,
364
+ "diff",
365
+ "--name-only",
366
+ "--no-renames",
367
+ "--no-ext-diff",
368
+ parent_sha,
369
+ commit_sha,
370
+ )
371
+ if changed.returncode != 0:
372
+ return False
373
+ if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
374
+ return False
375
+ return True
376
+
377
+
230
378
  def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
231
379
  temporary_path.parent.mkdir(parents=True, exist_ok=True)
232
380
  descriptor = None
@@ -426,10 +426,10 @@ def frontmatter_value(content: str, key: str) -> Optional[str]:
426
426
  for line in lines[1:]:
427
427
  if line == "---":
428
428
  break
429
- match = re.match(rf"^{re.escape(key)}:\s*([^#]*?)\s*(?:#.*)?$", line)
429
+ match = re.match(rf"^{re.escape(key)}:(.*)$", line)
430
430
  if match:
431
- value = match.group(1).strip().strip('"').strip("'")
432
- return value
431
+ value = _common.strip_yaml_comment(match.group(1))
432
+ return value.strip().strip('"').strip("'")
433
433
  return None
434
434
 
435
435
 
@@ -586,14 +586,11 @@ def resolved_archive_target(
586
586
  expected_number = milestone_number(branch)
587
587
  configured = parsed_state.archive
588
588
  entries = archive_sequence_entries(project)
589
- missing_prior = [
590
- number for number in range(1, expected_number) if number not in entries
591
- ]
592
589
  later = sorted(number for number in entries if number > expected_number)
593
- if missing_prior or later:
590
+ if later:
594
591
  raise ArchiveError(
595
592
  f"archive sequence does not match bound branch {branch}: "
596
- f"missing={missing_prior}, later={later}"
593
+ f"later={later}"
597
594
  )
598
595
 
599
596
  if configured is not None:
@@ -1065,9 +1062,10 @@ def prepare_locked(project: Path, active_root: Path, slug: str) -> dict:
1065
1062
  final = active_root / "review" / "FINAL.md"
1066
1063
  reviewed = re.search(r"(?m)^Reviewed HEAD:\s*(\S+)", final.read_text(encoding="utf-8")) if final.is_file() else None
1067
1064
  head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve HEAD").strip()
1068
- if reviewed and reviewed.group(1) != head:
1065
+ if reviewed and not _common.reviewed_head_covers(project, reviewed.group(1), head):
1069
1066
  raise ArchiveError(
1070
1067
  f"FINAL.md Reviewed HEAD {reviewed.group(1)} does not match current HEAD {head}; "
1068
+ f"{_common.RUNTIME_PIN_ALLOWANCE}; "
1071
1069
  "re-run the final review on the current commit before archiving"
1072
1070
  )
1073
1071
  target = resolved_archive_target(project, slug, parsed_state)
@@ -1680,6 +1678,7 @@ def abandon_locked(
1680
1678
 
1681
1679
 
1682
1680
  run_git = _common.run_git
1681
+ reviewed_head_covers = _common.reviewed_head_covers
1683
1682
  run_command = _common.run_command
1684
1683
 
1685
1684
 
@@ -1899,9 +1898,10 @@ def preflight(repo: Path) -> dict:
1899
1898
  require_stageable_carry_forward(project, archive)
1900
1899
  require_clean_older_archives(project, configured)
1901
1900
  head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve preflight HEAD")
1902
- if reviewed_head != head:
1901
+ if not reviewed_head_covers(project, reviewed_head, head):
1903
1902
  raise ArchiveError(
1904
- f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}"
1903
+ f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}; "
1904
+ f"{_common.RUNTIME_PIN_ALLOWANCE}"
1905
1905
  )
1906
1906
  return {
1907
1907
  "archive": configured,
@@ -2093,9 +2093,10 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2093
2093
 
2094
2094
  members = member_ship_rows(project, archive, ship_commit)
2095
2095
  ship_parent = require_git_success(run_git(project, "rev-parse", f"{ship_commit}^"), "resolve ship parent")
2096
- if reviewed_head != ship_parent:
2096
+ if not reviewed_head_covers(project, reviewed_head, ship_parent):
2097
2097
  raise ArchiveError(
2098
- f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}"
2098
+ f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}; "
2099
+ f"{_common.RUNTIME_PIN_ALLOWANCE}"
2099
2100
  )
2100
2101
  require_canonical_commit_body(
2101
2102
  project,