@opengsd/gsd-path 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/CHANGELOG.md +325 -344
  2. package/DOCS.md +12 -2
  3. package/FULL.md +3 -2
  4. package/HOOKS.md +95 -8
  5. package/QUICK.md +1 -1
  6. package/README.md +78 -110
  7. package/RUNTIME.md +1 -1
  8. package/UPDATE.md +4 -3
  9. package/WORKFLOW.md +22 -1
  10. package/package.json +5 -2
  11. package/platforms/shared-agents/dispatch.md +13 -0
  12. package/scripts/_common.py +149 -1
  13. package/scripts/archive_milestone.py +14 -13
  14. package/scripts/build_recovery.py +96 -31
  15. package/scripts/build_state.py +79 -3
  16. package/scripts/check_docs_audit.py +77 -4
  17. package/scripts/check_handoffs.py +546 -30
  18. package/scripts/check_task_briefs.py +279 -23
  19. package/scripts/detect_project.py +243 -18
  20. package/scripts/discussion_validate.py +86 -29
  21. package/scripts/dispatch_driver.py +392 -22
  22. package/scripts/git_guard.py +3 -3
  23. package/scripts/guard_hook.py +1416 -165
  24. package/scripts/install.mjs +23 -8
  25. package/scripts/install.py +49 -9
  26. package/scripts/integration.py +146 -1
  27. package/scripts/isolation.py +392 -60
  28. package/scripts/loop_run.py +16 -4
  29. package/scripts/members.py +11 -9
  30. package/scripts/path_config.py +3 -3
  31. package/scripts/pipeline_git.py +63 -12
  32. package/scripts/pipeline_state.py +111 -33
  33. package/scripts/pipeline_undo.py +101 -8
  34. package/scripts/runtime_store.py +20 -2
  35. package/scripts/skill-resources.json +9 -2
  36. package/scripts/state_checkpoint.py +9 -27
  37. package/scripts/status_runtime.py +22 -4
  38. package/scripts/task_context.py +12 -3
  39. package/scripts/wizard.mjs +1 -0
  40. package/scripts/workflow_run.py +5 -2
  41. package/scripts/worktree_paths.py +54 -2
  42. package/skills/gsd-path/DEFINE.md +28 -3
  43. package/skills/gsd-path/PLAN.md +11 -3
  44. package/skills/gsd-path/RESEARCH.md +11 -0
  45. package/skills/gsd-path/SHIP.md +6 -1
  46. package/skills/gsd-path/references/build-native.md +16 -1
  47. package/skills/gsd-path/references/dispatch.md +13 -0
  48. package/skills/gsd-path/references/docs-auditor.md +5 -0
  49. package/skills/gsd-path/references/planner.md +14 -4
  50. package/skills/gsd-path/references/reviewer-final.md +20 -2
  51. package/skills/gsd-path/references/reviewer-wave.md +15 -4
  52. package/skills/gsd-path/references/reviewer.md +3 -1
  53. package/skills/gsd-path/references/spec-probes.md +150 -0
  54. package/skills/gsd-path/scripts/_common.py +149 -1
  55. package/skills/gsd-path/scripts/archive_milestone.py +14 -13
  56. package/skills/gsd-path/scripts/build_recovery.py +96 -31
  57. package/skills/gsd-path/scripts/build_state.py +79 -3
  58. package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
  59. package/skills/gsd-path/scripts/check_handoffs.py +546 -30
  60. package/skills/gsd-path/scripts/check_task_briefs.py +279 -23
  61. package/skills/gsd-path/scripts/detect_project.py +243 -18
  62. package/skills/gsd-path/scripts/discussion_validate.py +86 -29
  63. package/skills/gsd-path/scripts/dispatch_driver.py +392 -22
  64. package/skills/gsd-path/scripts/git_guard.py +3 -3
  65. package/skills/gsd-path/scripts/integration.py +146 -1
  66. package/skills/gsd-path/scripts/isolation.py +392 -60
  67. package/skills/gsd-path/scripts/loop_run.py +16 -4
  68. package/skills/gsd-path/scripts/members.py +11 -9
  69. package/skills/gsd-path/scripts/path_config.py +3 -3
  70. package/skills/gsd-path/scripts/pipeline_git.py +63 -12
  71. package/skills/gsd-path/scripts/pipeline_state.py +111 -33
  72. package/skills/gsd-path/scripts/state_checkpoint.py +9 -27
  73. package/skills/gsd-path/scripts/status_runtime.py +22 -4
  74. package/skills/gsd-path/scripts/task_context.py +12 -3
  75. package/skills/gsd-path/scripts/workflow_run.py +5 -2
  76. package/skills/gsd-path/scripts/worktree_paths.py +54 -2
  77. package/skills/gsd-path/templates/docs-audit.md +5 -1
  78. package/skills/gsd-path/templates/final-review.md +11 -2
  79. package/skills/gsd-path/templates/intent.md +30 -0
  80. package/skills/gsd-path/templates/patch-findings.md +3 -3
  81. package/skills/gsd-path/templates/plan.md +17 -2
  82. package/skills/gsd-path/templates/research-handoff.md +14 -3
  83. package/skills/gsd-path/templates/state.md +1 -1
  84. package/skills/gsd-path/templates/wave-review.md +16 -6
  85. package/skills/gsd-path-build/references/build-native.md +16 -1
  86. package/skills/gsd-path-build/references/dispatch.md +13 -0
  87. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  88. package/skills/gsd-path-build/references/reviewer-wave.md +15 -4
  89. package/skills/gsd-path-build/references/reviewer.md +3 -1
  90. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  91. package/skills/gsd-path-build/scripts/_common.py +149 -1
  92. package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
  93. package/skills/gsd-path-build/scripts/build_recovery.py +96 -31
  94. package/skills/gsd-path-build/scripts/build_state.py +79 -3
  95. package/skills/gsd-path-build/scripts/check_handoffs.py +546 -30
  96. package/skills/gsd-path-build/scripts/check_task_briefs.py +279 -23
  97. package/skills/gsd-path-build/scripts/detect_project.py +243 -18
  98. package/skills/gsd-path-build/scripts/discussion_validate.py +86 -29
  99. package/skills/gsd-path-build/scripts/dispatch_driver.py +392 -22
  100. package/skills/gsd-path-build/scripts/integration.py +146 -1
  101. package/skills/gsd-path-build/scripts/isolation.py +392 -60
  102. package/skills/gsd-path-build/scripts/loop_run.py +16 -4
  103. package/skills/gsd-path-build/scripts/members.py +11 -9
  104. package/skills/gsd-path-build/scripts/path_config.py +3 -3
  105. package/skills/gsd-path-build/scripts/pipeline_git.py +63 -12
  106. package/skills/gsd-path-build/scripts/pipeline_state.py +111 -33
  107. package/skills/gsd-path-build/scripts/state_checkpoint.py +9 -27
  108. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  109. package/skills/gsd-path-build/scripts/workflow_run.py +5 -2
  110. package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
  111. package/skills/gsd-path-build/templates/wave-review.md +16 -6
  112. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  113. package/skills/gsd-path-decide/scripts/_common.py +149 -1
  114. package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
  115. package/skills/gsd-path-decide/scripts/build_recovery.py +96 -31
  116. package/skills/gsd-path-decide/scripts/check_handoffs.py +546 -30
  117. package/skills/gsd-path-decide/scripts/check_task_briefs.py +279 -23
  118. package/skills/gsd-path-decide/scripts/detect_project.py +243 -18
  119. package/skills/gsd-path-decide/scripts/discussion_validate.py +86 -29
  120. package/skills/gsd-path-decide/scripts/integration.py +146 -1
  121. package/skills/gsd-path-decide/scripts/isolation.py +392 -60
  122. package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
  123. package/skills/gsd-path-decide/scripts/members.py +11 -9
  124. package/skills/gsd-path-decide/scripts/path_config.py +3 -3
  125. package/skills/gsd-path-decide/scripts/pipeline_git.py +63 -12
  126. package/skills/gsd-path-decide/scripts/pipeline_state.py +111 -33
  127. package/skills/gsd-path-decide/scripts/state_checkpoint.py +9 -27
  128. package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
  129. package/skills/gsd-path-define/SKILL.md +28 -3
  130. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  131. package/skills/gsd-path-define/scripts/_common.py +149 -1
  132. package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
  133. package/skills/gsd-path-define/scripts/build_recovery.py +96 -31
  134. package/skills/gsd-path-define/scripts/check_handoffs.py +546 -30
  135. package/skills/gsd-path-define/scripts/check_task_briefs.py +279 -23
  136. package/skills/gsd-path-define/scripts/detect_project.py +243 -18
  137. package/skills/gsd-path-define/scripts/discussion_validate.py +86 -29
  138. package/skills/gsd-path-define/scripts/integration.py +146 -1
  139. package/skills/gsd-path-define/scripts/isolation.py +392 -60
  140. package/skills/gsd-path-define/scripts/loop_run.py +16 -4
  141. package/skills/gsd-path-define/scripts/members.py +11 -9
  142. package/skills/gsd-path-define/scripts/path_config.py +3 -3
  143. package/skills/gsd-path-define/scripts/pipeline_git.py +63 -12
  144. package/skills/gsd-path-define/scripts/pipeline_state.py +111 -33
  145. package/skills/gsd-path-define/scripts/state_checkpoint.py +9 -27
  146. package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
  147. package/skills/gsd-path-define/templates/intent.md +30 -0
  148. package/skills/gsd-path-define/templates/state.md +1 -1
  149. package/skills/gsd-path-discuss/scripts/_common.py +149 -1
  150. package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
  151. package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
  152. package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
  153. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  154. package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
  155. package/skills/gsd-path-docs-audit/scripts/_common.py +149 -1
  156. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
  157. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +96 -31
  158. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
  159. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +546 -30
  160. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +279 -23
  161. package/skills/gsd-path-docs-audit/scripts/detect_project.py +243 -18
  162. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +86 -29
  163. package/skills/gsd-path-docs-audit/scripts/integration.py +146 -1
  164. package/skills/gsd-path-docs-audit/scripts/isolation.py +392 -60
  165. package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
  166. package/skills/gsd-path-docs-audit/scripts/members.py +11 -9
  167. package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
  168. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +63 -12
  169. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +111 -33
  170. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +9 -27
  171. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
  172. package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
  173. package/skills/gsd-path-forensics/scripts/_common.py +149 -1
  174. package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
  175. package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
  176. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +101 -8
  177. package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
  178. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  179. package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
  180. package/skills/gsd-path-inspect/scripts/_common.py +149 -1
  181. package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
  182. package/skills/gsd-path-inspect/scripts/build_recovery.py +96 -31
  183. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
  184. package/skills/gsd-path-inspect/scripts/check_handoffs.py +546 -30
  185. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +279 -23
  186. package/skills/gsd-path-inspect/scripts/detect_project.py +243 -18
  187. package/skills/gsd-path-inspect/scripts/discussion_validate.py +86 -29
  188. package/skills/gsd-path-inspect/scripts/integration.py +146 -1
  189. package/skills/gsd-path-inspect/scripts/isolation.py +392 -60
  190. package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
  191. package/skills/gsd-path-inspect/scripts/members.py +11 -9
  192. package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
  193. package/skills/gsd-path-inspect/scripts/pipeline_git.py +63 -12
  194. package/skills/gsd-path-inspect/scripts/pipeline_state.py +111 -33
  195. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +9 -27
  196. package/skills/gsd-path-inspect/scripts/workflow_run.py +5 -2
  197. package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
  198. package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
  199. package/skills/gsd-path-inspect/templates/state.md +1 -1
  200. package/skills/gsd-path-loop/scripts/_common.py +149 -1
  201. package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
  202. package/skills/gsd-path-loop/scripts/path_config.py +3 -3
  203. package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
  204. package/skills/gsd-path-migrate/scripts/_common.py +149 -1
  205. package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
  206. package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
  207. package/skills/gsd-path-plan/SKILL.md +11 -3
  208. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  209. package/skills/gsd-path-plan/references/planner.md +14 -4
  210. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  211. package/skills/gsd-path-plan/scripts/_common.py +149 -1
  212. package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
  213. package/skills/gsd-path-plan/scripts/build_recovery.py +96 -31
  214. package/skills/gsd-path-plan/scripts/check_handoffs.py +546 -30
  215. package/skills/gsd-path-plan/scripts/check_task_briefs.py +279 -23
  216. package/skills/gsd-path-plan/scripts/detect_project.py +243 -18
  217. package/skills/gsd-path-plan/scripts/discussion_validate.py +86 -29
  218. package/skills/gsd-path-plan/scripts/integration.py +146 -1
  219. package/skills/gsd-path-plan/scripts/isolation.py +392 -60
  220. package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
  221. package/skills/gsd-path-plan/scripts/members.py +11 -9
  222. package/skills/gsd-path-plan/scripts/path_config.py +3 -3
  223. package/skills/gsd-path-plan/scripts/pipeline_git.py +63 -12
  224. package/skills/gsd-path-plan/scripts/pipeline_state.py +111 -33
  225. package/skills/gsd-path-plan/scripts/state_checkpoint.py +9 -27
  226. package/skills/gsd-path-plan/scripts/workflow_run.py +5 -2
  227. package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
  228. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  229. package/skills/gsd-path-plan/templates/plan.md +17 -2
  230. package/skills/gsd-path-research/SKILL.md +11 -0
  231. package/skills/gsd-path-research/references/dispatch.md +13 -0
  232. package/skills/gsd-path-research/scripts/_common.py +149 -1
  233. package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
  234. package/skills/gsd-path-research/scripts/build_recovery.py +96 -31
  235. package/skills/gsd-path-research/scripts/check_handoffs.py +546 -30
  236. package/skills/gsd-path-research/scripts/check_task_briefs.py +279 -23
  237. package/skills/gsd-path-research/scripts/detect_project.py +243 -18
  238. package/skills/gsd-path-research/scripts/discussion_validate.py +86 -29
  239. package/skills/gsd-path-research/scripts/integration.py +146 -1
  240. package/skills/gsd-path-research/scripts/isolation.py +392 -60
  241. package/skills/gsd-path-research/scripts/loop_run.py +16 -4
  242. package/skills/gsd-path-research/scripts/members.py +11 -9
  243. package/skills/gsd-path-research/scripts/path_config.py +3 -3
  244. package/skills/gsd-path-research/scripts/pipeline_git.py +63 -12
  245. package/skills/gsd-path-research/scripts/pipeline_state.py +111 -33
  246. package/skills/gsd-path-research/scripts/state_checkpoint.py +9 -27
  247. package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
  248. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  249. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  250. package/skills/gsd-path-roadmap/scripts/_common.py +149 -1
  251. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
  252. package/skills/gsd-path-roadmap/scripts/build_recovery.py +96 -31
  253. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +546 -30
  254. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +279 -23
  255. package/skills/gsd-path-roadmap/scripts/detect_project.py +243 -18
  256. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +86 -29
  257. package/skills/gsd-path-roadmap/scripts/integration.py +146 -1
  258. package/skills/gsd-path-roadmap/scripts/isolation.py +392 -60
  259. package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
  260. package/skills/gsd-path-roadmap/scripts/members.py +11 -9
  261. package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
  262. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +63 -12
  263. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +111 -33
  264. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +9 -27
  265. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
  266. package/skills/gsd-path-ship/SKILL.md +6 -1
  267. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  268. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  269. package/skills/gsd-path-ship/references/reviewer-wave.md +15 -4
  270. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  271. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  272. package/skills/gsd-path-ship/scripts/_common.py +149 -1
  273. package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
  274. package/skills/gsd-path-ship/scripts/build_recovery.py +96 -31
  275. package/skills/gsd-path-ship/scripts/build_state.py +79 -3
  276. package/skills/gsd-path-ship/scripts/check_handoffs.py +546 -30
  277. package/skills/gsd-path-ship/scripts/check_task_briefs.py +279 -23
  278. package/skills/gsd-path-ship/scripts/detect_project.py +243 -18
  279. package/skills/gsd-path-ship/scripts/discussion_validate.py +86 -29
  280. package/skills/gsd-path-ship/scripts/integration.py +146 -1
  281. package/skills/gsd-path-ship/scripts/isolation.py +392 -60
  282. package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
  283. package/skills/gsd-path-ship/scripts/members.py +11 -9
  284. package/skills/gsd-path-ship/scripts/path_config.py +3 -3
  285. package/skills/gsd-path-ship/scripts/pipeline_git.py +63 -12
  286. package/skills/gsd-path-ship/scripts/pipeline_state.py +111 -33
  287. package/skills/gsd-path-ship/scripts/state_checkpoint.py +9 -27
  288. package/skills/gsd-path-ship/scripts/workflow_run.py +5 -2
  289. package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
  290. package/skills/gsd-path-ship/templates/final-review.md +11 -2
  291. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  292. package/skills/gsd-path-ship/templates/wave-review.md +16 -6
  293. package/skills/gsd-path-undo/scripts/_common.py +149 -1
  294. package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
  295. package/skills/gsd-path-undo/scripts/path_config.py +3 -3
  296. package/skills/gsd-path-undo/scripts/pipeline_undo.py +101 -8
  297. package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
  298. package/skills/path/DEFINE.md +28 -3
  299. package/skills/path/PLAN.md +11 -3
  300. package/skills/path/RESEARCH.md +11 -0
  301. package/skills/path/SHIP.md +6 -1
  302. package/skills/path/references/build-native.md +16 -1
  303. package/skills/path/references/dispatch.md +13 -0
  304. package/skills/path/references/docs-auditor.md +5 -0
  305. package/skills/path/references/planner.md +14 -4
  306. package/skills/path/references/reviewer-final.md +20 -2
  307. package/skills/path/references/reviewer-wave.md +15 -4
  308. package/skills/path/references/reviewer.md +3 -1
  309. package/skills/path/references/spec-probes.md +150 -0
  310. package/skills/path/scripts/_common.py +149 -1
  311. package/skills/path/scripts/archive_milestone.py +14 -13
  312. package/skills/path/scripts/build_recovery.py +96 -31
  313. package/skills/path/scripts/build_state.py +79 -3
  314. package/skills/path/scripts/check_docs_audit.py +77 -4
  315. package/skills/path/scripts/check_handoffs.py +546 -30
  316. package/skills/path/scripts/check_task_briefs.py +279 -23
  317. package/skills/path/scripts/detect_project.py +243 -18
  318. package/skills/path/scripts/discussion_validate.py +86 -29
  319. package/skills/path/scripts/dispatch_driver.py +392 -22
  320. package/skills/path/scripts/git_guard.py +3 -3
  321. package/skills/path/scripts/integration.py +146 -1
  322. package/skills/path/scripts/isolation.py +392 -60
  323. package/skills/path/scripts/loop_run.py +16 -4
  324. package/skills/path/scripts/members.py +11 -9
  325. package/skills/path/scripts/path_config.py +3 -3
  326. package/skills/path/scripts/pipeline_git.py +63 -12
  327. package/skills/path/scripts/pipeline_state.py +111 -33
  328. package/skills/path/scripts/state_checkpoint.py +9 -27
  329. package/skills/path/scripts/status_runtime.py +22 -4
  330. package/skills/path/scripts/task_context.py +12 -3
  331. package/skills/path/scripts/workflow_run.py +5 -2
  332. package/skills/path/scripts/worktree_paths.py +54 -2
  333. package/skills/path/templates/docs-audit.md +5 -1
  334. package/skills/path/templates/final-review.md +11 -2
  335. package/skills/path/templates/intent.md +30 -0
  336. package/skills/path/templates/patch-findings.md +3 -3
  337. package/skills/path/templates/plan.md +17 -2
  338. package/skills/path/templates/research-handoff.md +14 -3
  339. package/skills/path/templates/state.md +1 -1
  340. package/skills/path/templates/wave-review.md +16 -6
  341. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  342. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  343. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  344. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  345. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  346. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  347. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -65,6 +65,49 @@ VERIFY_RESULTS = ("pass", "fail")
65
65
  VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
66
66
  VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
67
67
 
68
+ REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
69
+ REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
70
+
71
+
72
+ def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
73
+ return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
74
+
75
+
76
+ def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
77
+ if inline:
78
+ return inline
79
+ collected: list[str] = []
80
+ for line in lines[index + 1 :]:
81
+ if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
82
+ line
83
+ ):
84
+ break
85
+ if line and not line[0].isspace():
86
+ break
87
+ stripped = line.strip()
88
+ if stripped:
89
+ collected.append(stripped)
90
+ return "\n".join(collected)
91
+
92
+
93
+ def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
94
+ pattern = review_bullet_field_label_pattern(field)
95
+ values: list[str] = []
96
+ for index, line in enumerate(lines):
97
+ match = pattern.fullmatch(line)
98
+ if match is None:
99
+ continue
100
+ values.append(_review_bullet_field_value(lines, index, match.group(1)))
101
+ return values
102
+
103
+
104
+ def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
105
+ """Parse one `- **Field**:` review bullet, including indented continuations."""
106
+ values = find_review_bullet_field_values(lines, field)
107
+ if len(values) != 1:
108
+ raise ValueError(field)
109
+ return values[0]
110
+
68
111
 
69
112
  # Sample paths the pipeline must commit. A product rule such as an unanchored
70
113
  # `build/` would otherwise drop them silently from every commit.
@@ -150,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
150
193
  return parse_verify_ledger(path.read_text(encoding="utf-8"))
151
194
 
152
195
 
196
+ def read_user_text(path: Path) -> str:
197
+ """Read a user-edited text file with CRLF normalized to LF."""
198
+ return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
199
+
200
+
153
201
  def parse_verify_ledger(text: str) -> list:
154
202
  entries = []
155
203
  for number, line in enumerate(text.splitlines(), start=1):
@@ -204,7 +252,43 @@ def run_command(
204
252
  def run_git(
205
253
  repo: Path, *arguments: str, input: Optional[str] = None
206
254
  ) -> subprocess.CompletedProcess[str]:
207
- return _run_exact(("git", "-C", str(repo), *arguments), input=input)
255
+ return _run_exact(git_command(repo, *arguments), input=input)
256
+
257
+
258
+ def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
259
+ """Git argv for repo, including WSL drvfs workarounds when needed."""
260
+ return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
261
+
262
+
263
+ def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
264
+ """Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
265
+ if os.name != "posix":
266
+ return ()
267
+ try:
268
+ root = repo.resolve()
269
+ mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
270
+ except OSError:
271
+ return ()
272
+ best = ""
273
+ fstype = ""
274
+ for line in mounts:
275
+ parts = line.split()
276
+ if len(parts) < 3:
277
+ continue
278
+ mount_point, mount_type = parts[1], parts[2]
279
+ if mount_type not in {"9p", "drvfs"}:
280
+ continue
281
+ if root == Path(mount_point) or root.is_relative_to(mount_point):
282
+ if len(mount_point) >= len(best):
283
+ best, fstype = mount_point, mount_type
284
+ if not fstype:
285
+ return ()
286
+ return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
287
+
288
+
289
+ def git_index_lock_retryable(stderr: str) -> bool:
290
+ text = stderr.casefold()
291
+ return "index.lock" in text and "file exists" in text
208
292
 
209
293
 
210
294
  # OS files that are never pipeline artifacts when git ignores them.
@@ -227,6 +311,70 @@ def git_visible_entries(directory: Path) -> set:
227
311
  return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
228
312
 
229
313
 
314
+ RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
315
+ RUNTIME_PIN_ALLOWANCE = (
316
+ "only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
317
+ )
318
+
319
+
320
+ def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
321
+ """True when a review of `reviewed` still covers `head`.
322
+
323
+ That is the same commit, or `reviewed` is an ancestor of `head` and every
324
+ commit on the first-parent path in ``reviewed..head`` is a non-merge commit
325
+ whose only change is ``.gsd-path/runtime.json``. An empty commit does not
326
+ qualify. Any other path, any merge commit, an unresolvable SHA, or a
327
+ non-ancestor returns False.
328
+ """
329
+ if not reviewed or not head:
330
+ return False
331
+ resolved_repo = Path(repo)
332
+ resolved: List[str] = []
333
+ for value in (reviewed, head):
334
+ result = run_git(
335
+ resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
336
+ )
337
+ if result.returncode != 0 or not result.stdout.strip():
338
+ return False
339
+ resolved.append(result.stdout.strip())
340
+ reviewed_sha, head_sha = resolved
341
+ if reviewed_sha == head_sha:
342
+ return True
343
+ if (
344
+ run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
345
+ != 0
346
+ ):
347
+ return False
348
+ listing = run_git(
349
+ resolved_repo,
350
+ "rev-list",
351
+ "--first-parent",
352
+ "--parents",
353
+ f"{reviewed_sha}..{head_sha}",
354
+ )
355
+ if listing.returncode != 0:
356
+ return False
357
+ for line in listing.stdout.splitlines():
358
+ fields = line.split()
359
+ if len(fields) != 2:
360
+ return False
361
+ commit_sha, parent_sha = fields[0], fields[1]
362
+ changed = run_git(
363
+ resolved_repo,
364
+ "diff",
365
+ "--name-only",
366
+ "--no-renames",
367
+ "--no-ext-diff",
368
+ parent_sha,
369
+ commit_sha,
370
+ )
371
+ if changed.returncode != 0:
372
+ return False
373
+ if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
374
+ return False
375
+ return True
376
+
377
+
230
378
  def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
231
379
  temporary_path.parent.mkdir(parents=True, exist_ok=True)
232
380
  descriptor = None
@@ -426,10 +426,10 @@ def frontmatter_value(content: str, key: str) -> Optional[str]:
426
426
  for line in lines[1:]:
427
427
  if line == "---":
428
428
  break
429
- match = re.match(rf"^{re.escape(key)}:\s*([^#]*?)\s*(?:#.*)?$", line)
429
+ match = re.match(rf"^{re.escape(key)}:(.*)$", line)
430
430
  if match:
431
- value = match.group(1).strip().strip('"').strip("'")
432
- return value
431
+ value = _common.strip_yaml_comment(match.group(1))
432
+ return value.strip().strip('"').strip("'")
433
433
  return None
434
434
 
435
435
 
@@ -586,14 +586,11 @@ def resolved_archive_target(
586
586
  expected_number = milestone_number(branch)
587
587
  configured = parsed_state.archive
588
588
  entries = archive_sequence_entries(project)
589
- missing_prior = [
590
- number for number in range(1, expected_number) if number not in entries
591
- ]
592
589
  later = sorted(number for number in entries if number > expected_number)
593
- if missing_prior or later:
590
+ if later:
594
591
  raise ArchiveError(
595
592
  f"archive sequence does not match bound branch {branch}: "
596
- f"missing={missing_prior}, later={later}"
593
+ f"later={later}"
597
594
  )
598
595
 
599
596
  if configured is not None:
@@ -1065,9 +1062,10 @@ def prepare_locked(project: Path, active_root: Path, slug: str) -> dict:
1065
1062
  final = active_root / "review" / "FINAL.md"
1066
1063
  reviewed = re.search(r"(?m)^Reviewed HEAD:\s*(\S+)", final.read_text(encoding="utf-8")) if final.is_file() else None
1067
1064
  head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve HEAD").strip()
1068
- if reviewed and reviewed.group(1) != head:
1065
+ if reviewed and not _common.reviewed_head_covers(project, reviewed.group(1), head):
1069
1066
  raise ArchiveError(
1070
1067
  f"FINAL.md Reviewed HEAD {reviewed.group(1)} does not match current HEAD {head}; "
1068
+ f"{_common.RUNTIME_PIN_ALLOWANCE}; "
1071
1069
  "re-run the final review on the current commit before archiving"
1072
1070
  )
1073
1071
  target = resolved_archive_target(project, slug, parsed_state)
@@ -1680,6 +1678,7 @@ def abandon_locked(
1680
1678
 
1681
1679
 
1682
1680
  run_git = _common.run_git
1681
+ reviewed_head_covers = _common.reviewed_head_covers
1683
1682
  run_command = _common.run_command
1684
1683
 
1685
1684
 
@@ -1899,9 +1898,10 @@ def preflight(repo: Path) -> dict:
1899
1898
  require_stageable_carry_forward(project, archive)
1900
1899
  require_clean_older_archives(project, configured)
1901
1900
  head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve preflight HEAD")
1902
- if reviewed_head != head:
1901
+ if not reviewed_head_covers(project, reviewed_head, head):
1903
1902
  raise ArchiveError(
1904
- f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}"
1903
+ f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}; "
1904
+ f"{_common.RUNTIME_PIN_ALLOWANCE}"
1905
1905
  )
1906
1906
  return {
1907
1907
  "archive": configured,
@@ -2093,9 +2093,10 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2093
2093
 
2094
2094
  members = member_ship_rows(project, archive, ship_commit)
2095
2095
  ship_parent = require_git_success(run_git(project, "rev-parse", f"{ship_commit}^"), "resolve ship parent")
2096
- if reviewed_head != ship_parent:
2096
+ if not reviewed_head_covers(project, reviewed_head, ship_parent):
2097
2097
  raise ArchiveError(
2098
- f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}"
2098
+ f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}; "
2099
+ f"{_common.RUNTIME_PIN_ALLOWANCE}"
2099
2100
  )
2100
2101
  require_canonical_commit_body(
2101
2102
  project,
@@ -1,5 +1,5 @@
1
1
  # gsd-path project runtime
2
- """Guard a blocked build's return to Define or Plan using its committed base."""
2
+ """Guard return to Define or Plan using a committed recovery base."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
@@ -17,7 +17,9 @@ EVENTS = {
17
17
  "define": "build intent corrections requested",
18
18
  "plan": "build plan repair requested",
19
19
  }
20
+ PLAN_INTENT_EVENT = "plan intent corrections requested"
20
21
  MARKER = "build recovery: "
22
+ RECOVERY_KEYS = frozenset({"base", "branch", "kind", "source"})
21
23
 
22
24
 
23
25
  def runtime():
@@ -36,9 +38,42 @@ def checkpoint_runtime():
36
38
  return state_checkpoint
37
39
 
38
40
 
41
+ def _parse_recovery_record(value: object) -> dict:
42
+ if (
43
+ not isinstance(value, dict)
44
+ or not {"base", "branch", "kind"}.issubset(value)
45
+ or not set(value).issubset(RECOVERY_KEYS)
46
+ or not isinstance(value["base"], str)
47
+ or not re.fullmatch(r"[0-9a-f]{40}", value["base"])
48
+ ):
49
+ raise runtime().PipelineStateError("invalid build recovery record")
50
+ source = value.get("source", "build")
51
+ if source not in {"build", "plan"}:
52
+ raise runtime().PipelineStateError("invalid build recovery record")
53
+ if source == "plan":
54
+ if value["kind"] != "define":
55
+ raise runtime().PipelineStateError("invalid build recovery record")
56
+ elif value["kind"] not in EVENTS:
57
+ raise runtime().PipelineStateError("invalid build recovery record")
58
+ return {**value, "source": source}
59
+
60
+
61
+ def _recovery_base_proves_milestone(before, current, source: str) -> bool:
62
+ if before.branch != current.branch or before.milestone != current.milestone:
63
+ return False
64
+ if source == "plan":
65
+ return (before.phase, before.status) in {("plan", "active"), ("plan", "blocked")}
66
+ return (before.phase, before.status) == ("build", "blocked")
67
+
68
+
39
69
  def context(repo: Path, text: str | None = None) -> dict | None:
40
70
  state = runtime()
41
- current, loaded, _ = state.load_state(repo)
71
+ try:
72
+ current, loaded, _ = state.load_state(repo)
73
+ except state.PipelineStateError as error:
74
+ if "STATE.md must be a real file" not in str(error):
75
+ raise
76
+ return None
42
77
  text = loaded if text is None else text
43
78
  recovery = None
44
79
  for line in text.splitlines():
@@ -47,26 +82,24 @@ def context(repo: Path, text: str | None = None) -> dict | None:
47
82
  event = line.split(" — ", 2)[-1]
48
83
  if event.startswith(MARKER):
49
84
  try:
50
- value = json.loads(event[len(MARKER):])
85
+ value = _parse_recovery_record(json.loads(event[len(MARKER):]))
51
86
  except ValueError as error:
52
87
  raise state.PipelineStateError("invalid build recovery record") from error
53
- if (not isinstance(value, dict) or set(value) != {"base", "branch", "kind"}
54
- or value["kind"] not in EVENTS
55
- or not isinstance(value["base"], str)
56
- or not re.fullmatch(r"[0-9a-f]{40}", value["base"])):
57
- raise state.PipelineStateError("invalid build recovery record")
58
88
  recovery = {**value, "active": True} if value["branch"] == current.branch else None
59
- elif event == "build started" and recovery:
60
- recovery["active"] = False
89
+ elif recovery and recovery.get("active"):
90
+ source = recovery.get("source", "build")
91
+ if source == "build" and event == "build started":
92
+ recovery["active"] = False
93
+ elif source == "plan" and event == "plan approved":
94
+ recovery["active"] = False
61
95
  if recovery:
62
96
  if not state._is_ancestor(repo, recovery["base"], "HEAD"):
63
97
  raise state.PipelineStateError("build recovery base is not an ancestor of HEAD")
64
- source = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/STATE.md")
65
- if source is None:
98
+ base_text = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/STATE.md")
99
+ if base_text is None:
66
100
  raise state.PipelineStateError("build recovery base has no STATE.md")
67
- before = state._state_from_text(source)
68
- if ((before.phase, before.status) != ("build", "blocked")
69
- or before.branch != current.branch or before.milestone != current.milestone):
101
+ before = state._state_from_text(base_text)
102
+ if not _recovery_base_proves_milestone(before, current, recovery.get("source", "build")):
70
103
  raise state.PipelineStateError("build recovery base does not prove this blocked milestone")
71
104
  return recovery
72
105
 
@@ -108,18 +141,20 @@ def settled_tasks(repo: Path) -> dict[str, tuple[str, dict]]:
108
141
  return tasks
109
142
 
110
143
 
111
- def begin(repo: Path, before, after, event: str) -> dict:
144
+ def _begin_common(repo: Path, before, event: str) -> str:
112
145
  state = runtime()
113
- if (before.phase, before.status) != ("build", "blocked") or after.phase not in EVENTS:
114
- raise state.PipelineStateError("build recovery requires build/blocked")
115
- if event != EVENTS[after.phase]:
116
- raise state.PipelineStateError(f"state transition requires event: {EVENTS[after.phase]}")
117
146
  if before.archive is not None or before.branch != state._current_branch(repo):
118
147
  raise state.PipelineStateError("build recovery requires the active bound branch")
119
148
  if state._worktree_changes(repo):
120
149
  raise state.PipelineStateError("checkpoint the blocked build before recovery")
121
150
  if any(state.transaction_journals(repo).values()):
122
151
  raise state.PipelineStateError("finish pending transactions before build recovery")
152
+ state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md")
153
+ return state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
154
+
155
+
156
+ def _settle_build_dispatch(repo: Path) -> None:
157
+ state = runtime()
123
158
  records = records_root(repo)
124
159
  for directory in {path.parent.parent for path in records.rglob("attempt-*/state.json")}:
125
160
  latest = max(directory.glob("attempt-*/state.json"), key=lambda path: int(path.parent.name.split("-")[1]))
@@ -128,19 +163,49 @@ def begin(repo: Path, before, after, event: str) -> dict:
128
163
  or (record.get("worktree") and record.get("branch") and not record.get("cleanup_complete"))
129
164
  or record.get("outcome") not in {"landed", "collected", "blocked", "resolved", "redispatched"}):
130
165
  raise state.PipelineStateError(f"settle dispatch records before recovery: {latest}")
131
- tasks = settled_tasks(repo)
132
- state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
133
- state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md")
134
- base = state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
166
+
167
+
168
+ def _verify_done_tasks(repo: Path, tasks: dict[str, tuple[str, dict]], base: str) -> None:
169
+ state = runtime()
135
170
  done = [repo / ".project/tasks" / name for name, (_, fields) in tasks.items()
136
171
  if fields["status"] == "done"]
137
- if done:
138
- if __package__:
139
- from scripts.isolation import verify_landed_task_files
140
- else:
141
- from isolation import verify_landed_task_files
142
- verify_landed_task_files(repo, done, ".project/tasks", base)
143
- return {"kind": after.phase, "base": base, "branch": before.branch}
172
+ if not done:
173
+ return
174
+ if __package__:
175
+ from scripts.isolation import verify_landed_task_files
176
+ else:
177
+ from isolation import verify_landed_task_files
178
+ verify_landed_task_files(repo, done, ".project/tasks", base)
179
+
180
+
181
+ def begin(repo: Path, before, after, event: str) -> dict:
182
+ state = runtime()
183
+ if (before.phase, before.status) == ("build", "blocked") and after.phase in EVENTS:
184
+ if event != EVENTS[after.phase]:
185
+ raise state.PipelineStateError(f"state transition requires event: {EVENTS[after.phase]}")
186
+ _settle_build_dispatch(repo)
187
+ tasks = settled_tasks(repo)
188
+ state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
189
+ base = _begin_common(repo, before, event)
190
+ _verify_done_tasks(repo, tasks, base)
191
+ return {"kind": after.phase, "base": base, "branch": before.branch, "source": "build"}
192
+ if (
193
+ before.phase == "plan"
194
+ and before.status in {"active", "blocked"}
195
+ and (after.phase, after.status) == ("define", "active")
196
+ ):
197
+ if event != PLAN_INTENT_EVENT:
198
+ raise state.PipelineStateError(f"state transition requires event: {PLAN_INTENT_EVENT}")
199
+ state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
200
+ tasks_dir = repo / ".project/tasks"
201
+ tasks = settled_tasks(repo) if tasks_dir.is_dir() and any(
202
+ path.is_file() and not state._common.is_ignored_junk(path)
203
+ for path in tasks_dir.iterdir()
204
+ ) else {}
205
+ base = _begin_common(repo, before, event)
206
+ _verify_done_tasks(repo, tasks, base)
207
+ return {"kind": "define", "base": base, "branch": before.branch, "source": "plan"}
208
+ raise state.PipelineStateError("build recovery requires build/blocked")
144
209
 
145
210
 
146
211
  def records_root(repo: Path) -> Path:
@@ -24,7 +24,7 @@ if __package__:
24
24
  )
25
25
  from .pipeline_git import task_commit_subject
26
26
  from .pipeline_state import PipelineStateError, load_state
27
- from . import _common
27
+ from . import _common, check_handoffs
28
28
  else:
29
29
  try:
30
30
  from check_task_briefs import _frontmatter, _sections
@@ -37,6 +37,7 @@ else:
37
37
  from pipeline_git import task_commit_subject
38
38
  from pipeline_state import PipelineStateError, load_state
39
39
  import _common
40
+ import check_handoffs
40
41
  except ImportError: # pragma: no cover - package imports used by tests
41
42
  from scripts.check_task_briefs import _frontmatter, _sections
42
43
  from scripts.isolation import (
@@ -47,7 +48,7 @@ else:
47
48
  )
48
49
  from scripts.pipeline_git import task_commit_subject
49
50
  from scripts.pipeline_state import PipelineStateError, load_state
50
- from scripts import _common
51
+ from scripts import _common, check_handoffs
51
52
 
52
53
 
53
54
  DEFAULT_PROJECT_DIR = ".project"
@@ -57,6 +58,10 @@ FULL_SHA_RE = re.compile(r"^[0-9a-f]{40}$")
57
58
  # recover verdicts that count as landed, mapped to their evidence classification.
58
59
  LANDED_VERDICTS = {"recovered": "proven-landed", "attested": "attested"}
59
60
  WAVE_HEADING_RE = re.compile(r"(?m)^## Wave (?P<wave>\d+)\b.*$")
61
+ REVIEW_VERDICT_RE = re.compile(
62
+ r"(?m)^Wave verdict:\s*(?P<verdict>pass|blocked)\s*(?:<!--.*?-->)?\s*$"
63
+ )
64
+ REVIEW_FINDINGS_HEADING = "Review findings"
60
65
  VERIFY_HEAVY_RE = re.compile(r"(?m)^Heavy:\s*(?P<value>yes|no)\s*(?:<!--.*-->)?\s*$")
61
66
  VALID_TASK_STATUSES = {"pending", "in-progress", "done", "failed", "blocked"}
62
67
  VERIFY_RESULTS = _common.VERIFY_RESULTS
@@ -88,6 +93,7 @@ class Task:
88
93
  task_file: str
89
94
  verify_heavy: bool
90
95
  repo: str = ""
96
+ review_findings: bool = False
91
97
 
92
98
 
93
99
  @dataclass(frozen=True)
@@ -241,6 +247,7 @@ def _parse_task(path: Path, relative_path: str, task_file: Optional[str] = None)
241
247
  task_file=task_file or relative_path,
242
248
  verify_heavy=_verify_heavy(text, label),
243
249
  repo=str(fields.get("repo") or ""),
250
+ review_findings=REVIEW_FINDINGS_HEADING in _sections(text),
244
251
  )
245
252
 
246
253
 
@@ -485,6 +492,36 @@ def _overlap(left: Task, right: Task) -> List[str]:
485
492
  return sorted(set(left.files) & set(right.files))
486
493
 
487
494
 
495
+ def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
496
+ """Return waves whose highest canonical review cycle has a blocked lens."""
497
+
498
+ review_dir = project.tasks_dir.parent / "review"
499
+ if review_dir.is_symlink() or not review_dir.is_dir():
500
+ return {}
501
+
502
+ latest: Dict[int, int] = {}
503
+ verdicts: Dict[Tuple[int, int], List[str]] = {}
504
+ for path in sorted(review_dir.iterdir()):
505
+ named = check_handoffs.WAVE_REVIEW_NAME.fullmatch(path.name)
506
+ if named is None or path.is_symlink() or not path.is_file():
507
+ continue
508
+ wave, cycle = int(named.group("wave")), int(named.group("cycle"))
509
+ latest[wave] = max(latest.get(wave, 0), cycle)
510
+ try:
511
+ text = path.read_text(encoding="utf-8")
512
+ except (OSError, UnicodeDecodeError):
513
+ continue
514
+ verdict = REVIEW_VERDICT_RE.search(text)
515
+ if verdict is not None:
516
+ verdicts.setdefault((wave, cycle), []).append(verdict.group("verdict"))
517
+
518
+ return {
519
+ wave: cycle
520
+ for wave, cycle in latest.items()
521
+ if "blocked" in verdicts.get((wave, cycle), [])
522
+ }
523
+
524
+
488
525
  def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
489
526
  allow_done: bool = False) -> Dict[str, object]:
490
527
  """Return task readiness, optionally accepting build/done for completion checks."""
@@ -494,6 +531,39 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
494
531
  by_id = {task.task_id: task for task in project.tasks}
495
532
  unfinished_waves = sorted({task.wave for task in project.tasks if task.status != "done"})
496
533
  current_wave = unfinished_waves[0] if unfinished_waves else None
534
+ blocked_reviews = _blocked_wave_reviews(project)
535
+ blocked_wave = min(
536
+ (
537
+ wave
538
+ for wave in blocked_reviews
539
+ if (current_wave is None or wave < current_wave)
540
+ and any(task.wave == wave for task in project.tasks)
541
+ and all(task.status == "done" for task in project.tasks if task.wave == wave)
542
+ ),
543
+ default=None,
544
+ )
545
+ repair_for_wave: Optional[int] = None
546
+ blocked_review: Optional[Dict[str, object]] = None
547
+ if blocked_wave is not None:
548
+ source_ids = {task.task_id for task in project.tasks if task.wave == blocked_wave}
549
+ repairs = [
550
+ task
551
+ for task in project.tasks
552
+ if task.status != "done"
553
+ and task.wave > blocked_wave
554
+ and task.review_findings
555
+ and source_ids.intersection(task.deps)
556
+ ]
557
+ if repairs:
558
+ repair_for_wave = blocked_wave
559
+ current_wave = min(task.wave for task in repairs)
560
+ else:
561
+ current_wave = blocked_wave
562
+ blocked_review = {
563
+ "wave": blocked_wave,
564
+ "cycle": blocked_reviews[blocked_wave],
565
+ "waiting_waves": unfinished_waves,
566
+ }
497
567
  active = [task for task in project.tasks if task.status == "in-progress"]
498
568
  if current_wave is not None and any(task.wave != current_wave for task in active):
499
569
  task = next(task for task in active if task.wave != current_wave)
@@ -536,7 +606,7 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
536
606
  )
537
607
 
538
608
  wave_active = any(task.wave == current_wave for task in active)
539
- if current_wave is not None and not selectable and not wave_active:
609
+ if current_wave is not None and blocked_review is None and not selectable and not wave_active:
540
610
  stalled = [
541
611
  {
542
612
  "id": task.task_id,
@@ -556,11 +626,17 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
556
626
  {"wave": current_wave, "tasks": stalled},
557
627
  )
558
628
 
629
+ typed: Dict[str, object] = {}
630
+ if repair_for_wave is not None:
631
+ typed["repair_for_wave"] = repair_for_wave
632
+ if blocked_review is not None:
633
+ typed["blocked_review"] = blocked_review
559
634
  return {
560
635
  "command": "ready",
561
636
  "branch": project.branch,
562
637
  "head": project.head,
563
638
  "current_wave": current_wave,
639
+ **typed,
564
640
  "ready": [
565
641
  {
566
642
  "id": task.task_id,