@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
@@ -16,9 +16,11 @@ from __future__ import annotations
16
16
  import argparse
17
17
  import errno
18
18
  import hashlib
19
+ import importlib
19
20
  import json
20
21
  import os
21
22
  import re
23
+ import shlex
22
24
  import subprocess
23
25
  import sys
24
26
  import tempfile
@@ -31,6 +33,7 @@ try:
31
33
  from scripts import (_common, archive_milestone, build_state, discussion_validate, isolation,
32
34
  pipeline_state, review_findings, review_panel, workflow_run, task_context, model_policy)
33
35
  from scripts import check_handoffs as contracts
36
+ from scripts.pipeline_git import is_bound_branch
34
37
  except ImportError: # bundled copy inside a skill's scripts directory
35
38
  import _common
36
39
  import archive_milestone
@@ -44,6 +47,7 @@ except ImportError: # bundled copy inside a skill's scripts directory
44
47
  import review_panel
45
48
  import workflow_run
46
49
  import model_policy
50
+ from pipeline_git import is_bound_branch
47
51
 
48
52
  if os.name == "nt":
49
53
  import msvcrt
@@ -71,12 +75,91 @@ class DriverStop(RuntimeError):
71
75
  STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
72
76
  contracts.HandoffError, review_findings.ReviewFindingsError,
73
77
  pipeline_state.PipelineStateError, archive_milestone.ArchiveError, model_policy.PolicyError)
78
+ _RUNTIME_ACTIVATION_ORDER = (
79
+ "_common",
80
+ "pipeline_git",
81
+ "worktree_paths",
82
+ "build_recovery",
83
+ "isolation",
84
+ "pipeline_state",
85
+ "state_checkpoint",
86
+ "check_task_briefs",
87
+ "build_state",
88
+ "check_handoffs",
89
+ "task_context",
90
+ "archive_milestone",
91
+ "review_findings",
92
+ "model_policy",
93
+ )
74
94
  RECOVERY_BLOCKED = "recovery blocked"
75
95
  # The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
76
96
  # the same stops from review, panel, or skeptics stay with the parent.
77
97
  BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
78
98
 
79
99
 
100
+ def _bundled_helpers() -> bool:
101
+ return "scripts.isolation" not in sys.modules
102
+
103
+
104
+ def _refresh_stop_errors() -> None:
105
+ global STOP_ERRORS
106
+ STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
107
+ contracts.HandoffError, review_findings.ReviewFindingsError,
108
+ pipeline_state.PipelineStateError, archive_milestone.ArchiveError,
109
+ model_policy.PolicyError)
110
+
111
+
112
+ def _reload_pinned_modules(runtime_root: Path) -> None:
113
+ global isolation, build_state, task_context, contracts, pipeline_state
114
+ global archive_milestone, review_findings, model_policy
115
+ if _bundled_helpers():
116
+ path = str(runtime_root)
117
+ if path not in sys.path:
118
+ sys.path.insert(0, path)
119
+ for name in _RUNTIME_ACTIVATION_ORDER:
120
+ module = sys.modules.get(name)
121
+ if module is not None:
122
+ importlib.reload(module)
123
+ isolation = sys.modules["isolation"]
124
+ build_state = sys.modules["build_state"]
125
+ task_context = sys.modules["task_context"]
126
+ contracts = sys.modules["check_handoffs"]
127
+ pipeline_state = sys.modules["pipeline_state"]
128
+ archive_milestone = sys.modules["archive_milestone"]
129
+ review_findings = sys.modules["review_findings"]
130
+ model_policy = sys.modules["model_policy"]
131
+ else:
132
+ import scripts
133
+ path = str(runtime_root)
134
+ if path not in scripts.__path__:
135
+ scripts.__path__ = [path, *[entry for entry in scripts.__path__ if entry != path]]
136
+ for name in _RUNTIME_ACTIVATION_ORDER:
137
+ module = sys.modules.get(f"scripts.{name}")
138
+ if module is not None:
139
+ importlib.reload(module)
140
+ isolation = scripts.isolation
141
+ build_state = scripts.build_state
142
+ task_context = scripts.task_context
143
+ contracts = scripts.check_handoffs
144
+ pipeline_state = scripts.pipeline_state
145
+ archive_milestone = scripts.archive_milestone
146
+ review_findings = scripts.review_findings
147
+ model_policy = scripts.model_policy
148
+ _refresh_stop_errors()
149
+
150
+
151
+ def _activate_pinned_runtime(repo: Path) -> None:
152
+ if not os.path.lexists(repo / ".gsd-path/runtime.json"):
153
+ return
154
+ resolved = subprocess.run(
155
+ [sys.executable, "-B", str(repo / ".gsd-path/status_runtime.py"),
156
+ "--repo", str(repo), "--runtime-path"],
157
+ cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
158
+ if resolved.returncode:
159
+ raise DriverStop(resolved.stderr.strip() or "pinned runtime resolution failed")
160
+ _reload_pinned_modules(Path(resolved.stdout.strip()))
161
+
162
+
80
163
  def now() -> str:
81
164
  return datetime.now(timezone.utc).isoformat(timespec="seconds")
82
165
 
@@ -107,6 +190,169 @@ def attempts_used(root: Path, task_id: str) -> int:
107
190
  if load_state(path).get("origin") == "dispatch")
108
191
 
109
192
 
193
+ def dispatch_primary(worktree: Path) -> Path:
194
+ """The bound-branch primary that owns dispatch records for a task checkout."""
195
+ worktree = worktree.resolve()
196
+ if is_bound_branch(isolation.require_attached(worktree)):
197
+ return worktree
198
+ matches = [
199
+ path.resolve()
200
+ for path, branch in isolation._registered_worktrees(worktree).items()
201
+ if branch and is_bound_branch(branch.removeprefix("refs/heads/"))
202
+ ]
203
+ if len(matches) != 1:
204
+ raise isolation.IsolationError(
205
+ "dispatch records require exactly one primary worktree for the bound branch")
206
+ return matches[0]
207
+
208
+
209
+ def should_open_native_shell(root: Path, task_id: str, base: str) -> bool:
210
+ """Whether activate-task should append a native dispatch attempt record."""
211
+ task_dir = root / task_id
212
+ if not task_dir.is_dir():
213
+ return True
214
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
215
+ key=lambda path: attempt_number(path.parent))
216
+ if not attempts:
217
+ return True
218
+ latest = read_attempt(attempts[-1])
219
+ if latest.get("outcome") is None and not latest.get("command"):
220
+ return False
221
+ if latest.get("outcome") == "blocked" and str(latest.get("base")) == base:
222
+ return False
223
+ return True
224
+
225
+
226
+ def open_native_shell(primary: Path, *, task_id: str, base: str, worktree: str, task_file: str,
227
+ task_branch: Optional[str], mode: str, wave: Optional[int] = None,
228
+ title: Optional[str] = None, files: Optional[List[str]] = None,
229
+ sidecar: Optional[object] = None, member: Optional[str] = None,
230
+ member_base: Optional[str] = None, contract_file: Optional[str] = None) -> None:
231
+ """Open a dispatch attempt record for a host-spawned coder before it runs."""
232
+ root = records_root(primary)
233
+ root.mkdir(parents=True, exist_ok=True)
234
+ record: Dict[str, object] = {
235
+ "task_id": task_id,
236
+ "base": base,
237
+ "worktree": worktree,
238
+ "task_file": task_file,
239
+ "task_branch": task_branch,
240
+ "mode": mode,
241
+ "origin": "dispatch",
242
+ "outcome": None,
243
+ "dispatched_at": now(),
244
+ "native": True,
245
+ }
246
+ if wave is not None:
247
+ record["wave"] = wave
248
+ if title is not None:
249
+ record["title"] = title
250
+ if files is not None:
251
+ record["files"] = files
252
+ if sidecar is not None:
253
+ record["sidecar"] = sidecar
254
+ if member is not None:
255
+ record["member"] = member
256
+ if member_base is not None:
257
+ record["member_base"] = member_base
258
+ if contract_file is not None:
259
+ record["contract_file"] = contract_file
260
+ task_dir = root / task_id
261
+ if not should_open_native_shell(root, task_id, base):
262
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
263
+ key=lambda path: attempt_number(path.parent))
264
+ if not attempts:
265
+ return
266
+ state_path = attempts[-1]
267
+ state = load_state(state_path)
268
+ if state.get("outcome") is not None or state.get("command"):
269
+ return
270
+ attempt = state.get("attempt")
271
+ state.update(record)
272
+ if attempt is not None:
273
+ state["attempt"] = attempt
274
+ state["outcome"] = None
275
+ for key in ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit",
276
+ "answer", "command", "usage_recorded"):
277
+ state.pop(key, None)
278
+ save_state(state_path, state)
279
+ exit_path = state_path.with_name("exit.json")
280
+ if exit_path.is_file():
281
+ exit_path.unlink()
282
+ return
283
+ attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
284
+ attempt_dir = task_dir / f"attempt-{attempt}"
285
+ attempt_dir.mkdir(parents=True)
286
+ record["attempt"] = attempt
287
+ save_state(attempt_dir / "state.json", record)
288
+
289
+
290
+ def record_native_activation(worktree: Path, activation: Dict[str, object]) -> None:
291
+ """Record a native task activation in the milestone dispatch ledger."""
292
+ primary = dispatch_primary(worktree)
293
+ task_file = str(activation["task_file"])
294
+ task_path = worktree / task_file
295
+ fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
296
+ task_id = str(activation.get("task_id") or (fields or {}).get("id"))
297
+ wave = None
298
+ if fields and fields.get("wave") is not None:
299
+ wave = int(fields["wave"])
300
+ open_native_shell(
301
+ primary,
302
+ task_id=task_id,
303
+ base=str(activation["base"]),
304
+ worktree=str(activation["worktree"]),
305
+ task_file=task_file,
306
+ task_branch=activation.get("task_branch"),
307
+ mode="parallel" if activation.get("task_branch") else "serial",
308
+ wave=wave,
309
+ title=str(fields.get("title")) if fields else None,
310
+ files=list(fields.get("files") or []) if fields else None,
311
+ )
312
+
313
+
314
+ def record_native_member_activation(coordinator: Path, activation: Dict[str, object]) -> None:
315
+ """Record a native member-task activation in the coordinator dispatch ledger."""
316
+ task_file = str(activation["task_file"])
317
+ sidecar = Path(str(activation["worktree"]))
318
+ copy = Path(str(activation["copy"]))
319
+ fields, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8"))
320
+ task_id = str(activation.get("task_id") or (fields or {}).get("id"))
321
+ wave = None
322
+ if fields and fields.get("wave") is not None:
323
+ wave = int(fields["wave"])
324
+ open_native_shell(
325
+ coordinator,
326
+ task_id=task_id,
327
+ base=str(activation["base"]),
328
+ worktree=str(sidecar),
329
+ task_file=copy.relative_to(sidecar).as_posix(),
330
+ task_branch=str(activation["task_branch"]),
331
+ mode="member",
332
+ wave=wave,
333
+ title=str(fields.get("title")) if fields else None,
334
+ files=list(fields.get("files") or []) if fields else None,
335
+ member=str(activation["member"]),
336
+ member_base=str(activation["member_base"]),
337
+ contract_file=task_file,
338
+ )
339
+
340
+
341
+ def reusable_shell_attempt(root: Path, state: Dict[str, object]) -> Optional[Path]:
342
+ """A native activation shell the driver can attach its child wrapper to."""
343
+ task_dir = root / str(state["task_id"])
344
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
345
+ key=lambda path: attempt_number(path.parent))
346
+ if not attempts:
347
+ return None
348
+ prior = read_attempt(attempts[-1])
349
+ if prior.get("outcome") is not None or prior.get("command") or prior.get("pid"):
350
+ return None
351
+ if str(prior.get("base")) != str(state.get("base")):
352
+ return None
353
+ return attempts[-1].parent
354
+
355
+
110
356
  def acquire_lock(primary: Path) -> BinaryIO:
111
357
  root = records_root(primary)
112
358
  root.mkdir(parents=True, exist_ok=True)
@@ -307,7 +553,7 @@ def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
307
553
  raise DriverStop('cannot redispatch while the previous child is active')
308
554
  if prior.get('model_selection'):
309
555
  state['model_selection'] = prior['model_selection']
310
- else:
556
+ elif prior.get('command'):
311
557
  state['command'] = prior['command']
312
558
  reassignment = task_dir / 'model-reassignment.json'
313
559
  if reassignment.exists():
@@ -323,13 +569,23 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
323
569
  state = retain_selection(root, state)
324
570
  task_dir = root / str(state["task_id"])
325
571
  argv, selection = selected_command(state, options, command or options.child_command)
326
- attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
327
- attempt_dir = task_dir / f"attempt-{attempt}"
328
- attempt_dir.mkdir(parents=True)
572
+ attempt_dir = reusable_shell_attempt(root, state)
573
+ if attempt_dir is None:
574
+ attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
575
+ attempt_dir = task_dir / f"attempt-{attempt}"
576
+ attempt_dir.mkdir(parents=True)
577
+ else:
578
+ attempt = attempt_number(attempt_dir)
329
579
  stale = ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit", "answer",
330
580
  "usage_recorded")
331
- state = {key: value for key, value in state.items()
332
- if not key.startswith("_") and key not in stale}
581
+ incoming = {key: value for key, value in state.items()
582
+ if not key.startswith("_") and key not in stale}
583
+ if (attempt_dir / "state.json").is_file():
584
+ preserved = {key: value for key, value in read_attempt(attempt_dir / "state.json").items()
585
+ if not key.startswith("_") and key not in stale}
586
+ state = {**preserved, **incoming}
587
+ else:
588
+ state = incoming
333
589
  if selection is not None:
334
590
  state['model_selection'] = selection
335
591
  state.update({"attempt": attempt, "command": argv,
@@ -446,14 +702,14 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
446
702
  if not command:
447
703
  raise DriverStop("task has no Verify command", task=task_id)
448
704
  if state["mode"] == "serial":
449
- # ponytail: sidecar reproduction is git plumbing that belongs in isolation.py as the
450
- # inverse of clean_verify; lift it there when a second caller appears.
451
705
  sidecar = state["sidecar"]
452
- sidecar_path = Path(str(sidecar["worktree"]))
706
+ ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
707
+ sidecar_path = Path(str(ensured["worktree"]))
453
708
  tree = snapshot_tree(primary)
454
- isolation.git_output(sidecar_path, "read-tree", "-u", "--reset", tree)
455
- if isolation.git_output(sidecar_path, "write-tree") != tree:
456
- raise DriverStop("sidecar reproduction differs from the primary task diff", task=task_id)
709
+ try:
710
+ isolation.reproduce_verify_sidecar(sidecar_path, tree)
711
+ except isolation.IsolationError as error:
712
+ raise DriverStop(str(error), task=task_id) from error
457
713
  execution = run_verify(command, sidecar_path)
458
714
  location = f"sidecar {sidecar['branch']}"
459
715
  isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
@@ -659,6 +915,26 @@ class Round:
659
915
  verdict = task["verdict"]
660
916
  if verdict in ("recovered", "attested"):
661
917
  self.proven[str(task.get("task_id"))] = str(task.get("commit"))
918
+ if (verdict == "reconcile" and task.get("status") == "in-progress"
919
+ and task.get("task_branch") is None):
920
+ task_id = str(task.get("task_id"))
921
+ verify_name = f"task-{task_id.lower()}-verify"
922
+ verify_path = isolation.sidecar_root(self.primary, "verify", verify_name)
923
+ verify_branch = isolation.verify_branch_name(verify_name)
924
+ diagnose = shlex.join([
925
+ sys.executable,
926
+ "-B",
927
+ str(Path(__file__).with_name("pipeline_diagnose.py")),
928
+ "diagnose",
929
+ "--repo",
930
+ str(self.primary),
931
+ ])
932
+ raise DriverStop(
933
+ f"task {task_id} needs recovery: {task.get('reason')}; inspect Verify "
934
+ f"sidecar {verify_path} ({verify_branch}) before resetting it to pending; "
935
+ f"inventory it with `{diagnose}`",
936
+ recover=task,
937
+ )
662
938
  if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
663
939
  isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
664
940
  if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
@@ -673,12 +949,22 @@ class Round:
673
949
  # settle exited children ---------------------------------------------
674
950
 
675
951
  def isolate_live(self, state: Dict[str, object]) -> bool:
676
- """True while the isolate still holds the task as in-progress; the parent's recovery clears it."""
952
+ """Keep blocked attempts reportable until the corresponding task is reset."""
677
953
  task_path = Path(str(state["worktree"])) / str(state["task_file"])
678
954
  if not task_path.is_file():
679
955
  return False
680
956
  fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
681
- return fields is not None and fields.get("status") == "in-progress"
957
+ if fields is None:
958
+ return False
959
+ if fields.get("status") == "in-progress":
960
+ return True
961
+ return (
962
+ state.get("mode") == "serial"
963
+ and fields.get("status") == "blocked"
964
+ and fields.get("id") == state.get("task_id")
965
+ and fields.get("base") == state.get("base")
966
+ and fields.get("task_branch") == "null"
967
+ )
682
968
 
683
969
  def settle(self) -> bool:
684
970
  """Classify every exited child. Returns True when a landing or redispatch changed the round."""
@@ -717,8 +1003,15 @@ class Round:
717
1003
  elif state.get("finished_at") is None:
718
1004
  if child_running(state):
719
1005
  self.receipt["in_flight"].append(self.summary(state))
1006
+ elif (state.get("native") and not state.get("command") and not state.get("pid")
1007
+ and self.isolate_live(state)):
1008
+ continue
720
1009
  else:
721
- self.fail(state, ORPHANED)
1010
+ state.update(read_attempt(Path(str(state["_path"]))))
1011
+ if state.get("finished_at") is not None:
1012
+ changed = self.classify(state) or changed
1013
+ else:
1014
+ self.fail(state, ORPHANED)
722
1015
  else:
723
1016
  changed = self.classify(state) or changed
724
1017
  return changed
@@ -729,6 +1022,35 @@ class Round:
729
1022
  update_state(state, outcome="blocked", reason=reason)
730
1023
  self.receipt["blocked"].append({**self.summary(state), "reason": reason, **details})
731
1024
 
1025
+ def blocked_result(self, state: Dict[str, object], **diagnostics: object) -> bool:
1026
+ """Record an explicit serial block and retire only its untouched Verify sidecar."""
1027
+ reason = "child returned blocked"
1028
+ details: Dict[str, object] = dict(diagnostics)
1029
+ if state.get("mode") == "serial":
1030
+ try:
1031
+ task = isolation.block_serial_task(
1032
+ self.primary,
1033
+ str(state["task_file"]),
1034
+ str(state["task_id"]),
1035
+ str(state["base"]),
1036
+ )
1037
+ except STOP_ERRORS as error:
1038
+ self.fail(state, f"{reason}; could not record blocked task: {error}")
1039
+ return False
1040
+ details["task"] = task
1041
+ sidecar = state.get("sidecar")
1042
+ if isinstance(sidecar, dict):
1043
+ worktree = Path(str(sidecar["worktree"]))
1044
+ branch = str(sidecar["branch"])
1045
+ try:
1046
+ details["verify_cleanup"] = isolation.retire_unused_verify(
1047
+ self.primary, worktree, str(state["base"]), branch
1048
+ )
1049
+ except STOP_ERRORS as error:
1050
+ reason += f"; Verify sidecar retained at {worktree}: {error}"
1051
+ self.fail(state, reason, **details)
1052
+ return False
1053
+
732
1054
  def classify(self, state: Dict[str, object]) -> bool:
733
1055
  attempt_dir = Path(str(state["_path"])).parent
734
1056
  if self.budgeted and not state.get("usage_recorded"):
@@ -756,6 +1078,10 @@ class Round:
756
1078
  update_state(state, outcome="question", question=unanswered[0], answered=False)
757
1079
  self.receipt["questions"].append({**self.summary(state), "question": unanswered[0],
758
1080
  "log_delta": delta.strip()})
1081
+ elif verdict == "blocked" and named == str(state["task_id"]).lower():
1082
+ return self.blocked_result(
1083
+ state, stdout_tail=tail(message), log_delta=delta.strip()
1084
+ )
759
1085
  elif verdict != "ready" or named != str(state["task_id"]).lower():
760
1086
  self.fail(state, "child returned blocked" if verdict == "blocked"
761
1087
  else "child returned no RESULT line", stdout_tail=tail(message),
@@ -790,8 +1116,14 @@ class Round:
790
1116
  # dispatch -------------------------------------------------------------
791
1117
 
792
1118
  def in_flight_states(self) -> List[Dict[str, object]]:
793
- return [state for state in latest_states(self.root)
794
- if state.get("outcome") is None and state.get("wave") == self.receipt["wave"]]
1119
+ states = []
1120
+ for state in latest_states(self.root):
1121
+ if state.get("outcome") is not None or state.get("wave") != self.receipt["wave"]:
1122
+ continue
1123
+ if state.get("native") and not state.get("command") and not state.get("pid"):
1124
+ continue
1125
+ states.append(state)
1126
+ return states
795
1127
 
796
1128
  def checkpoint_bookkeeping(self) -> None:
797
1129
  open_ids = {state["task_id"] for state in latest_states(self.root)
@@ -815,6 +1147,24 @@ class Round:
815
1147
  ready = build_state.ready(str(self.primary), self.project_dir)
816
1148
  self.receipt["steps"].append({"script": "build_state.py ready", "result": ready})
817
1149
  wave = ready["current_wave"]
1150
+ blocked_review = ready.get("blocked_review")
1151
+ if blocked_review:
1152
+ waiting_waves = blocked_review["waiting_waves"]
1153
+ if self.receipt["wave"] in waiting_waves:
1154
+ raise DriverStop(
1155
+ f"wave {self.receipt['wave']} has unfinished tasks but wave "
1156
+ f"{blocked_review['wave']} review cycle {blocked_review['cycle']} "
1157
+ "is blocked; it must pass before dispatch")
1158
+ self.checkpoint_bookkeeping()
1159
+ return not in_flight
1160
+ repair_for_wave = ready.get("repair_for_wave")
1161
+ if repair_for_wave is not None and self.receipt["wave"] != wave:
1162
+ if self.receipt["wave"] > repair_for_wave:
1163
+ raise DriverStop(
1164
+ f"wave {self.receipt['wave']} is waiting on blocked wave {repair_for_wave}; "
1165
+ f"dispatch repair wave {wave} first")
1166
+ self.checkpoint_bookkeeping()
1167
+ return not in_flight
818
1168
  if wave is not None and wave < self.receipt["wave"]:
819
1169
  raise DriverStop(f"wave {wave} is still unfinished; run round --wave {wave} first")
820
1170
  self.checkpoint_bookkeeping()
@@ -1686,7 +2036,7 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1686
2036
  texts = contracts._task_texts(primary, options.project_dir)
1687
2037
  groups = {group["locator"]: group for group in findings["groups"]}
1688
2038
  today = now()[:10]
1689
- # Same-wave file overlap is forbidden, so repairs land in a newly appended wave.
2039
+ # Unordered same-wave file overlap is rejected, so repairs land in a newly appended wave.
1690
2040
  plan_path = project / "plan/PLAN.md"
1691
2041
  plan_text = plan_path.read_text(encoding="utf-8")
1692
2042
  depths, waves = contracts._plan_waves(plan_text)
@@ -1820,7 +2170,8 @@ Heavy: {'yes' if heavy else 'no'}
1820
2170
  receipt["plan_wave"] = fix_wave
1821
2171
  lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
1822
2172
  "--repo", str(primary), "--base", isolation.current_sha(primary),
1823
- "--tasks-dir", f"{options.project_dir}/tasks"], capture_output=True, encoding="utf-8", errors="replace")
2173
+ "--tasks-dir", f"{options.project_dir}/tasks",
2174
+ "--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
1824
2175
  receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
1825
2176
  "stdout": lint.stdout, "stderr": lint.stderr})
1826
2177
  if lint.returncode:
@@ -1935,7 +2286,11 @@ def positive_int(value: str) -> int:
1935
2286
 
1936
2287
 
1937
2288
  def default_resource(name: str) -> Optional[Path]:
1938
- candidate = Path(__file__).resolve().parent.parent / name
2289
+ script_root = Path(__file__).resolve().parent.parent
2290
+ candidate = script_root / name
2291
+ if candidate.is_file():
2292
+ return candidate
2293
+ candidate = script_root / "skills" / "gsd-path" / name
1939
2294
  return candidate if candidate.is_file() else None
1940
2295
 
1941
2296
 
@@ -2026,6 +2381,7 @@ def main(argv=None) -> int:
2026
2381
  if arguments.action == "_child":
2027
2382
  return child_main(arguments.state)
2028
2383
  primary = arguments.repo.resolve()
2384
+ _activate_pinned_runtime(primary)
2029
2385
  lock = None
2030
2386
  try:
2031
2387
  if arguments.action != "status":
@@ -2078,8 +2434,22 @@ def main(argv=None) -> int:
2078
2434
  raise DriverStop(f"task {arguments.task_id} dispatch record is {record['outcome']}; "
2079
2435
  "use round")
2080
2436
  if record.get("finished_at") is None:
2081
- raise DriverStop(f"task {arguments.task_id} still has a running child")
2082
- current.classify(record)
2437
+ if child_running(record):
2438
+ raise DriverStop(f"task {arguments.task_id} still has a running child")
2439
+ if record.get("command"):
2440
+ raise DriverStop(f"task {arguments.task_id} still has a running child")
2441
+ live = state_from_task(primary, arguments.task_id)
2442
+ merged = dict(record, **live)
2443
+ merged["_path"] = record["_path"]
2444
+ try:
2445
+ landing = finish_task(primary, merged)
2446
+ except STOP_ERRORS as error:
2447
+ update_state(record, outcome="blocked", reason=str(error))
2448
+ raise
2449
+ update_state(record, outcome="landed", commit=landing["commit"])
2450
+ current.receipt["landed"].append(landing)
2451
+ else:
2452
+ current.classify(record)
2083
2453
  else: # a coder dispatched by hand: derive the isolate from the task frontmatter
2084
2454
  current.receipt["landed"].append(
2085
2455
  finish_task(primary, native or state_from_task(primary, arguments.task_id)))
@@ -20,7 +20,7 @@ sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py")
20
20
  sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
21
21
  try:
22
22
  from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
23
- from pipeline_git import is_ship_subject, ship_commit_body, task_commit_body
23
+ from pipeline_git import is_ship_subject, ship_body_matches, ship_commit_body, task_commit_body
24
24
  from pipeline_state import _completion_status
25
25
  import members
26
26
  except ImportError as error: # pragma: no cover - broken install
@@ -82,7 +82,7 @@ INTEGRATION_STATE_FIELDS = LEGACY_STATE_FIELDS | {
82
82
  "integration",
83
83
  }
84
84
  PROVENANCE_STATE_FIELDS = INTEGRATION_STATE_FIELDS | {"integration_source"}
85
- INTEGRATION_MODES = {"direct", "pull-request"}
85
+ INTEGRATION_MODES = {"direct", "pull-request", "external-landing"}
86
86
  INTEGRATION_SOURCES = {"default", "milestone"}
87
87
 
88
88
 
@@ -311,7 +311,7 @@ def ship_contract_violations(entries, subject, body, new_archives, state):
311
311
  if [row["name"] for row in rows] != locked or any(row["status"] != "integrated" for row in rows):
312
312
  found.append("ship commit requires every locked member closed; run archive_milestone.py close-members")
313
313
  expected_body = ship_commit_body(archive, reviewed_head, rows).rstrip("\n")
314
- if body != expected_body:
314
+ if not ship_body_matches(body, expected_body):
315
315
  found.append("ship commit body does not match Archive, Reviewed-HEAD, and closed members")
316
316
  changed_paths = {
317
317
  new if code in "CR" else old