@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
@@ -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")
@@ -255,6 +255,7 @@ def check_member(
255
255
  coordinator_name: str,
256
256
  checkout: Path,
257
257
  recorded_remote: Optional[str] = None,
258
+ refuse_reserved_refs: bool = True,
258
259
  ) -> str:
259
260
  """Refuse a checkout that cannot join; return its origin URL."""
260
261
  if checkout.is_symlink() or not checkout.is_dir():
@@ -294,13 +295,14 @@ def check_member(
294
295
  raise MembersError(f"member STATE.md is unreadable: {error}") from error
295
296
  if state.milestone is not None and (state.phase, state.status) != ("shipped", "done"):
296
297
  raise MembersError(f"member has an active milestone: {state.milestone}")
297
- prefixes = member_ref_prefixes(coordinator_name) + tuple(
298
- "refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
299
- )
300
- refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
301
- colliding = [ref for ref in refs if ref.startswith(prefixes)]
302
- if colliding:
303
- raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
298
+ if refuse_reserved_refs:
299
+ prefixes = member_ref_prefixes(coordinator_name) + tuple(
300
+ "refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
301
+ )
302
+ refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
303
+ colliding = [ref for ref in refs if ref.startswith(prefixes)]
304
+ if colliding:
305
+ raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
304
306
  return remote
305
307
 
306
308
 
@@ -620,7 +622,7 @@ def validate_members(repo: Path) -> list[dict[str, str]]:
620
622
  members = read_members(root)
621
623
  for member in members:
622
624
  checkout = Path(member["checkout"])
623
- check_member(root, state.project, checkout, member["remote"])
625
+ check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
624
626
  try:
625
627
  role = member_role(checkout)
626
628
  except MembersError as error:
@@ -639,7 +641,7 @@ def repair_members(repo: Path) -> list[dict[str, str]]:
639
641
  members = read_members(root)
640
642
  for member in members:
641
643
  checkout = Path(member["checkout"])
642
- check_member(root, state.project, checkout, member["remote"])
644
+ check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
643
645
  _refuse_foreign_marker(checkout.resolve(), root, state.project, member["name"])
644
646
  for member in members:
645
647
  checkout = Path(member["checkout"]).resolve()
@@ -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')
@@ -146,6 +146,29 @@ def ship_commit_body(archive_path: str, reviewed_head: str, members: Sequence[di
146
146
  )
147
147
 
148
148
 
149
+ _TRAILER_LINE = re.compile(r"^[A-Za-z-]+:\s")
150
+
151
+
152
+ def ship_body_matches(body: str, expected_body: str) -> bool:
153
+ """True when body equals the contract or adds only optional git trailers after it."""
154
+ normalized = body.rstrip("\n")
155
+ expected = expected_body.rstrip("\n")
156
+ if normalized == expected:
157
+ return True
158
+ prefix = expected + "\n"
159
+ if not normalized.startswith(prefix):
160
+ return False
161
+ remainder = normalized[len(prefix):]
162
+ if not remainder:
163
+ return True
164
+ for line in remainder.splitlines():
165
+ if not line.strip():
166
+ return False
167
+ if not _TRAILER_LINE.match(line):
168
+ return False
169
+ return True
170
+
171
+
149
172
  def integrate_commit_body(
150
173
  archive_path: str,
151
174
  ship_commit: str,
@@ -270,22 +293,50 @@ def _symbolic_branch(repo: Path) -> str:
270
293
  return result.stdout.strip()
271
294
 
272
295
 
273
- def _branch_worktrees(repo: Path) -> dict[str, Path]:
274
- """Return every checked-out local branch from Git's worktree registry."""
275
- output = _run_git(repo, "worktree", "list", "--porcelain", "-z").stdout
296
+ def worktree_list_porcelain(repo: Path) -> tuple[str, bool]:
297
+ """Return worktree list output and whether it is NUL-separated."""
298
+ result = _run_git(repo, "worktree", "list", "--porcelain", "-z", check=False)
299
+ if result.returncode == 0:
300
+ return result.stdout, True
301
+ result = _run_git(repo, "worktree", "list", "--porcelain", check=False)
302
+ if result.returncode != 0:
303
+ message = (result.stderr or result.stdout).strip() or "could not list worktrees"
304
+ raise PipelineGitError(message)
305
+ return result.stdout, False
306
+
307
+
308
+ def branch_worktrees_from_porcelain(output: str, *, nul_separated: bool) -> dict[str, Path]:
276
309
  worktrees: dict[str, Path] = {}
277
- current_path: Optional[Path] = None
278
- for field in output.split("\0"):
279
- if field.startswith("worktree "):
280
- current_path = Path(field.removeprefix("worktree ")).resolve()
281
- elif field.startswith("branch ") and current_path is not None:
282
- ref = field.removeprefix("branch ")
283
- prefix = "refs/heads/"
284
- if ref.startswith(prefix):
285
- worktrees[ref.removeprefix(prefix)] = current_path
310
+ if nul_separated:
311
+ current_path: Optional[Path] = None
312
+ for field in output.split("\0"):
313
+ if not field:
314
+ continue
315
+ if field.startswith("worktree "):
316
+ current_path = Path(field.removeprefix("worktree ")).resolve()
317
+ elif field.startswith("branch ") and current_path is not None:
318
+ ref = field.removeprefix("branch ")
319
+ prefix = "refs/heads/"
320
+ if ref.startswith(prefix):
321
+ worktrees[ref.removeprefix(prefix)] = current_path
322
+ return worktrees
323
+ for block in output.strip().split("\n\n"):
324
+ fields = block.splitlines()
325
+ if not fields or not fields[0].startswith("worktree "):
326
+ continue
327
+ current_path = Path(fields[0].removeprefix("worktree ")).resolve()
328
+ for line in fields[1:]:
329
+ if line.startswith("branch refs/heads/"):
330
+ worktrees[line.removeprefix("branch refs/heads/")] = current_path
286
331
  return worktrees
287
332
 
288
333
 
334
+ def _branch_worktrees(repo: Path) -> dict[str, Path]:
335
+ """Return every checked-out local branch from Git's worktree registry."""
336
+ output, nul_separated = worktree_list_porcelain(repo)
337
+ return branch_worktrees_from_porcelain(output, nul_separated=nul_separated)
338
+
339
+
289
340
  def _remote_ref_sha(repo: Path, ref: str) -> Optional[str]:
290
341
  if not _origin_remote_exists(repo):
291
342
  raise PipelineGitError("origin remote is not configured")
@@ -91,7 +91,7 @@ STATE_FIELDS = (
91
91
  "integration_source",
92
92
  )
93
93
  LEGACY_STATE_FIELDS = STATE_FIELDS[:7]
94
- INTEGRATION_MODES = ("direct", "pull-request")
94
+ INTEGRATION_MODES = ("direct", "pull-request", "external-landing")
95
95
  INTEGRATION_SOURCES = ("default", "milestone")
96
96
  NULL = "null"
97
97
  SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
@@ -141,6 +141,8 @@ CROSS_PHASE_TRANSITIONS = {
141
141
  ("decide", "done", "plan", "active"),
142
142
  ("roadmap", "done", "define", "active"),
143
143
  ("plan", "done", "build", "active"),
144
+ ("plan", "active", "define", "active"),
145
+ ("plan", "blocked", "define", "active"),
144
146
  ("build", "blocked", "define", "active"),
145
147
  ("build", "blocked", "plan", "active"),
146
148
  ("build", "active", "ship", "active"),
@@ -193,11 +195,24 @@ def _run_git(
193
195
  *arguments: str,
194
196
  check: bool = True,
195
197
  ) -> subprocess.CompletedProcess[str]:
198
+ command = _common.git_command(repo, *arguments)
196
199
  result = subprocess.run(
197
- ["git", "-C", str(repo), *arguments],
200
+ command,
198
201
  capture_output=True,
199
202
  encoding="utf-8", errors="replace",
200
203
  )
204
+ if (
205
+ check
206
+ and result.returncode != 0
207
+ and arguments
208
+ and arguments[0] == "commit"
209
+ and _common.git_index_lock_retryable(result.stderr)
210
+ ):
211
+ result = subprocess.run(
212
+ command,
213
+ capture_output=True,
214
+ encoding="utf-8", errors="replace",
215
+ )
201
216
  if check and result.returncode != 0:
202
217
  detail = result.stderr.strip() or result.stdout.strip()
203
218
  raise PipelineStateError(f"git {' '.join(arguments)} failed: {detail}")
@@ -911,36 +926,13 @@ def _legacy_bind_next_landing(repo: Path, state: PipelineState) -> str:
911
926
  return landing
912
927
 
913
928
 
914
- def _bind_next_recovery(
929
+ def _bind_next_journal_recovery(
915
930
  repo: Path,
916
931
  state: PipelineState,
932
+ path: Path,
933
+ transaction: dict[str, object],
934
+ current: Optional[str],
917
935
  ) -> Optional[dict[str, object]]:
918
- journal_root = _git_path(repo, BIND_NEXT_JOURNAL_DIR)
919
- if not journal_root.exists() and not journal_root.is_symlink():
920
- return None
921
- if journal_root.is_symlink() or not journal_root.is_dir():
922
- raise PipelineStateError(f"bind-next journal root must be a real directory: {journal_root}")
923
-
924
- matches: list[tuple[Path, dict[str, object]]] = []
925
- for path in sorted(journal_root.glob("*.json")):
926
- transaction = _read_json(path)
927
- if transaction.get("schema") not in {
928
- LEGACY_BIND_NEXT_JOURNAL_SCHEMA,
929
- BIND_NEXT_JOURNAL_SCHEMA,
930
- }:
931
- raise PipelineStateError(f"bind-next journal has invalid schema: {path}")
932
- if transaction.get("repo") != str(repo):
933
- raise PipelineStateError(f"bind-next journal belongs to another worktree: {path}")
934
- if transaction.get("previous_branch") == state.branch:
935
- matches.append((path, transaction))
936
- if not matches:
937
- return None
938
- if len(matches) != 1:
939
- raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
940
- if state.phase != "shipped" or state.status != "done":
941
- raise PipelineStateError("bind-next journal requires STATE shipped/done")
942
-
943
- path, transaction = matches[0]
944
936
  required = (
945
937
  "branch",
946
938
  "previous_branch",
@@ -1022,7 +1014,32 @@ def _bind_next_recovery(
1022
1014
  if stage not in {"prepared", "switched", "retired"}:
1023
1015
  raise PipelineStateError("bind-next journal has invalid stage")
1024
1016
 
1025
- current = _current_branch(repo)
1017
+ stale_retired = False
1018
+ if stage == "retired":
1019
+ target_ref = _run_git(
1020
+ repo,
1021
+ "show-ref",
1022
+ "--verify",
1023
+ "--quiet",
1024
+ f"refs/heads/{branch}",
1025
+ check=False,
1026
+ )
1027
+ if target_ref.returncode not in {0, 1}:
1028
+ detail = (target_ref.stderr or target_ref.stdout).strip()
1029
+ raise PipelineStateError(
1030
+ f"could not check bind-next target branch {branch}: {detail}"
1031
+ )
1032
+ target_exists = target_ref.returncode == 0
1033
+ stale_retired = current not in {previous, branch} or not target_exists
1034
+ if stale_retired:
1035
+ # If the completed target is gone while the previous branch is checked
1036
+ # out, still prove that branch has not moved away from the recorded ship.
1037
+ if current == previous:
1038
+ head = _run_git(repo, "rev-parse", "HEAD").stdout.strip()
1039
+ if head != ship:
1040
+ raise PipelineStateError("bind-next previous branch is not at journal ship")
1041
+ return None
1042
+
1026
1043
  if current not in {previous, branch}:
1027
1044
  raise PipelineStateError("bind-next journal does not own the current branch")
1028
1045
  if current == previous and stage != "prepared":
@@ -1057,6 +1074,52 @@ def _bind_next_recovery(
1057
1074
  return result
1058
1075
 
1059
1076
 
1077
+ def _bind_next_recovery(
1078
+ repo: Path,
1079
+ state: PipelineState,
1080
+ ) -> Optional[dict[str, object]]:
1081
+ journal_root = _git_path(repo, BIND_NEXT_JOURNAL_DIR)
1082
+ if not journal_root.exists() and not journal_root.is_symlink():
1083
+ return None
1084
+ if journal_root.is_symlink() or not journal_root.is_dir():
1085
+ raise PipelineStateError(f"bind-next journal root must be a real directory: {journal_root}")
1086
+
1087
+ matches: list[tuple[Path, dict[str, object]]] = []
1088
+ for path in sorted(journal_root.glob("*.json")):
1089
+ transaction = _read_json(path)
1090
+ if transaction.get("schema") not in {
1091
+ LEGACY_BIND_NEXT_JOURNAL_SCHEMA,
1092
+ BIND_NEXT_JOURNAL_SCHEMA,
1093
+ }:
1094
+ raise PipelineStateError(f"bind-next journal has invalid schema: {path}")
1095
+ if transaction.get("repo") != str(repo):
1096
+ raise PipelineStateError(f"bind-next journal belongs to another worktree: {path}")
1097
+ if transaction.get("previous_branch") == state.branch:
1098
+ matches.append((path, transaction))
1099
+ if not matches:
1100
+ return None
1101
+ if state.phase != "shipped" or state.status != "done":
1102
+ if len(matches) != 1:
1103
+ raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
1104
+ raise PipelineStateError("bind-next journal requires STATE shipped/done")
1105
+
1106
+ current = _current_branch(repo)
1107
+ recoveries: list[dict[str, object]] = []
1108
+ for path, transaction in matches:
1109
+ recovery = _bind_next_journal_recovery(
1110
+ repo,
1111
+ state,
1112
+ path,
1113
+ transaction,
1114
+ current,
1115
+ )
1116
+ if recovery is not None:
1117
+ recoveries.append(recovery)
1118
+ if len(recoveries) > 1:
1119
+ raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
1120
+ return recoveries[0] if recoveries else None
1121
+
1122
+
1060
1123
  def _pending_discussion_block(repo: Path, phase: Optional[str]) -> Optional[str]:
1061
1124
  pending, error = _pending_answers(repo)
1062
1125
  if error:
@@ -1077,7 +1140,7 @@ def route_state(repo: Path, project_dir: str = ".project") -> dict[str, object]:
1077
1140
  result["recovery"] = recovery
1078
1141
  if route.get("phase") == "define":
1079
1142
  route["mode"] = "corrections"
1080
- elif route.get("phase") == "plan":
1143
+ elif route.get("phase") == "plan" and recovery.get("source", "build") == "build":
1081
1144
  route["mode"] = "build-repair"
1082
1145
  if route["action"] == "run-phase" and result["state"]["archive"] is None:
1083
1146
  reason = _pending_discussion_block(_repo_root(repo), route.get("phase"))
@@ -1668,6 +1731,8 @@ def _validate_transition(
1668
1731
  },
1669
1732
  ("define", "done", "plan", "active"): {"planning started"},
1670
1733
  ("decide", "done", "plan", "active"): {"planning started"},
1734
+ ("plan", "active", "define", "active"): {"plan intent corrections requested"},
1735
+ ("plan", "blocked", "define", "active"): {"plan intent corrections requested"},
1671
1736
  ("ship", "blocked", "plan", "active"): {"patch plan reopened"},
1672
1737
  ("ship", "active", "shipped", "done"): {
1673
1738
  "archive preflight passed; shipment recorded",
@@ -1720,9 +1785,10 @@ def transition_state(
1720
1785
  )
1721
1786
  if reason:
1722
1787
  raise PipelineStateError(reason)
1723
- if state.phase in {"research", "decide"} and (
1788
+ leaving_define = (state.phase, after.phase, after.status) == ("define", "define", "done")
1789
+ if leaving_define or (state.phase in {"research", "decide"} and (
1724
1790
  after.phase != state.phase or after.status == "done"
1725
- ):
1791
+ )):
1726
1792
  if __package__:
1727
1793
  from . import check_handoffs
1728
1794
  else:
@@ -1731,6 +1797,8 @@ def transition_state(
1731
1797
  except ImportError: # pragma: no cover - package imports used by tests
1732
1798
  from scripts import check_handoffs
1733
1799
  validator = {
1800
+ # Approved intent carries its spec-reach probe tables complete.
1801
+ "define": check_handoffs.validate_intent_probes,
1734
1802
  "research": check_handoffs.validate_research_artifacts,
1735
1803
  "decide": check_handoffs.validate_decide_artifacts,
1736
1804
  }[state.phase]
@@ -1747,6 +1815,16 @@ def transition_state(
1747
1815
  rendered, after.phase,
1748
1816
  _build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
1749
1817
  )
1818
+ elif (
1819
+ state.phase == "plan"
1820
+ and state.status in {"active", "blocked"}
1821
+ and (after.phase, after.status) == ("define", "active")
1822
+ ):
1823
+ recovery = _build_recovery().begin(resolved, state, after, event)
1824
+ rendered = _append_event(
1825
+ rendered, after.phase,
1826
+ _build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
1827
+ )
1750
1828
  _atomic_write(path, rendered)
1751
1829
  return {
1752
1830
  "schema": STATE_SCHEMA,
@@ -18,14 +18,14 @@ from pathlib import Path, PurePosixPath
18
18
  from typing import Mapping, Optional, Sequence
19
19
 
20
20
  if __package__:
21
- from .isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
21
+ from .isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
22
22
  else:
23
23
  try:
24
- from isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
24
+ from isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
25
25
  except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
26
26
  if error.name != "isolation":
27
27
  raise
28
- from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
28
+ from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
29
29
 
30
30
 
31
31
  if __package__:
@@ -444,14 +444,14 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
444
444
  # Pre-Git approval defers base-dependent checks to build.
445
445
  return
446
446
  if __package__:
447
- from .check_task_briefs import BriefError, _member_bases, validate_task_briefs
447
+ from .check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
448
448
  else:
449
449
  try:
450
- from check_task_briefs import BriefError, _member_bases, validate_task_briefs
450
+ from check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
451
451
  except ModuleNotFoundError as error: # pragma: no cover - package imports
452
452
  if error.name != "check_task_briefs":
453
453
  raise
454
- from scripts.check_task_briefs import BriefError, _member_bases, validate_task_briefs
454
+ from scripts.check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
455
455
  if __package__:
456
456
  from . import check_handoffs
457
457
  else:
@@ -462,28 +462,10 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
462
462
  raise
463
463
  from scripts import check_handoffs
464
464
  try:
465
- tasks, dependency_files = check_handoffs.plan_brief_inputs(repo, project_dir)
466
- landed_bases = {}
467
465
  member_base = _member_bases(repo)
468
- for task_id, text in tasks.items():
469
- if check_handoffs._task_scalar(text, task_id, "status") != "done":
470
- continue
471
- agent = check_handoffs._task_scalar(text, task_id, "agent")
472
- if agent in {"", "null"}:
473
- raise BriefError(f"{task_id} landed task has no recorded agent")
474
- member = check_handoffs._strict_frontmatter(text, task_id).get("repo")
475
- base_repo = repo
476
- if member is not None:
477
- located = member_base(member) if isinstance(member, str) and member else None
478
- if located is None:
479
- raise BriefError(f"repo: names no member in MEMBERS.md: {member}")
480
- base_repo = located[0]
481
- recorded_base = check_handoffs._task_scalar(text, task_id, "base")
482
- try:
483
- landed_bases[task_id] = require_commit(base_repo, require_full_sha(recorded_base))
484
- except IsolationError as error:
485
- raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
486
- dependency_files[task_id] = set()
466
+ tasks, dependency_files, landed_bases = plan_validation_bases(
467
+ repo, project_dir, member_base, initial=True
468
+ )
487
469
  validate_task_briefs(
488
470
  repo, head.stdout.strip(), f"{project_dir}/tasks",
489
471
  dependency_files=dependency_files, landed_bases=landed_bases,
@@ -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:
@@ -100,6 +100,10 @@ Obtain a confident answer in every area before finishing:
100
100
  give every named surface at least one success criterion observable
101
101
  there. A criterion satisfied by a passing test alone does not cover a
102
102
  surface.
103
+ 9. **Edges and prohibitions** — once the success criteria are drafted, run
104
+ the [spec-reach probes](references/spec-probes.md): every applicable edge
105
+ and every kept prohibition gets the user's ruling in INTENT.md
106
+ `## Edge coverage` and `## Prohibitions`.
103
107
 
104
108
  ## Program mode
105
109
 
@@ -149,7 +153,10 @@ State ownership. There is no re-interview of charter or roadmap scope:
149
153
  `Lane: milestone`, `Surfaces:` copied verbatim from the entry, and
150
154
  `Review panel:` copied from CHARTER.md (default
151
155
  the configured preference when CHARTER omits it). The confirmation may override the copied
152
- panel value; do not invent `detected` or a named list. When brownfield,
156
+ panel value; do not invent `detected` or a named list. Run the
157
+ [spec-reach probes](references/spec-probes.md) over the derived criteria
158
+ and draft `## Edge coverage` and `## Prohibitions` with recommended
159
+ rulings. When brownfield,
153
160
  fill `## Current state` from the map, record doc-vs-code rulings as in
154
161
  Brownfield mode, and add protected existing behavior under Scope out.
155
162
  Write Ground truth paths from the track root: `.project` normally, or
@@ -157,7 +164,9 @@ State ownership. There is no re-interview of charter or roadmap scope:
157
164
  active `research/` paths.
158
165
  3. Present one confirmation, not an interview: playback the derivation (and
159
166
  brownfield ground truth when present), link the resolved absolute INTENT.md
160
- path, and ask approve or adjust. A requested change that contradicts the
167
+ path, and ask approve or adjust. The confirmation lists each proposed edge
168
+ and prohibition ruling for the user to accept or change; it never rules on
169
+ them silently. A requested change that contradicts the
161
170
  charter or the resolved roadmap entry is a scope change — this phase never
162
171
  edits the roadmap; surface it to the user for a `$gsd-path-roadmap` re-slice
163
172
  at the next milestone boundary.
@@ -254,7 +263,15 @@ question. The rules change for every primary mode:
254
263
  the brownfield map already established. Milestone mode skips this step.
255
264
  5. Stop when coverage is complete or the user says `enough`. Record remaining
256
265
  uncertainty under `## Open questions` with `RESEARCH` or `NEEDS-USER`.
257
- 6. Classify the proposed milestone lane before writing the approval draft.
266
+ `enough` does not skip the next step.
267
+ 6. In standard mode, run the [spec-reach probes](references/spec-probes.md)
268
+ over the drafted success criteria: one question round for edges, then one
269
+ for kept prohibitions, each with a recommended ruling first. A ruling
270
+ observable where the user sees the work becomes or amends a success
271
+ criterion; write every ruling into `## Edge coverage` and
272
+ `## Prohibitions`. Program mode skips this step (each milestone runs it
273
+ in milestone mode); milestone mode runs it inside its confirmation.
274
+ 7. Classify the proposed milestone lane before writing the approval draft.
258
275
  Milestone mode keeps `Lane: milestone` and skips this classification.
259
276
  Program mode writes CHARTER.md instead of INTENT.md. Otherwise choose
260
277
  `quick` when scope fits at most two deliverable-sized tasks (project
@@ -275,6 +292,11 @@ question. The rules change for every primary mode:
275
292
  ## Output contract
276
293
 
277
294
  At approval, retain the already reviewed `Lane:` value and its one-line reason.
295
+ Before every approval question on INTENT.md, run the bundled `python3
296
+ <absolute-bundled-script> intent --repo <absolute-root>` helper
297
+ (`scripts/check_handoffs.py`, plus `--project-dir .project/next` in
298
+ lookahead). It checks the success criteria, Surfaces, and both probe tables;
299
+ fix a failure in the draft before asking. It approves nothing.
278
300
  Before presenting an approval draft without a CHARTER or explicit panel choice,
279
301
  read `settings.review_panel.value` from the selected runtime's `path_config.py
280
302
  show --repo <absolute-root>` (bundle for legacy projects; off for older selected
@@ -302,5 +324,8 @@ Use the executable phase handoff in AGENTS.md for the next action.
302
324
  - In brownfield mode, state mapped facts for correction; never re-ask them.
303
325
  - Treat solution-shaped problems as hypotheses and uncover the underlying pain.
304
326
  - Preserve vetoes and corrections verbatim.
327
+ - Never rule on an edge's behavior or a prohibition yourself; recommend,
328
+ then record the user's ruling. An edge the user has not ruled on is a
329
+ required answer, including under pre-approval.
305
330
  - Wrap up when every coverage-checklist area is answered or the user signals
306
331
  done, and tag remaining gaps.