@opengsd/gsd-path 1.5.0 → 1.6.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 (352) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/DOCS.md +4 -3
  3. package/FULL.md +64 -4
  4. package/HOOKS.md +63 -2
  5. package/MIGRATE.md +12 -0
  6. package/QUICK.md +4 -2
  7. package/README.md +67 -15
  8. package/RUNTIME.md +23 -5
  9. package/UPDATE.md +35 -0
  10. package/WORKFLOW.md +58 -3
  11. package/package.json +1 -1
  12. package/scripts/_common.py +107 -1
  13. package/scripts/build_recovery.py +62 -30
  14. package/scripts/build_state.py +195 -5
  15. package/scripts/check_handoffs.py +92 -30
  16. package/scripts/check_task_briefs.py +19 -14
  17. package/scripts/detect_project.py +17 -2
  18. package/scripts/discussion_records.py +9 -1
  19. package/scripts/discussion_validate.py +7 -6
  20. package/scripts/dispatch_driver.py +157 -31
  21. package/scripts/git_guard.py +24 -5
  22. package/scripts/guard_hook.py +1216 -47
  23. package/scripts/install.mjs +76 -3
  24. package/scripts/install.py +182 -37
  25. package/scripts/integration.py +98 -64
  26. package/scripts/isolation.py +148 -11
  27. package/scripts/members.py +68 -25
  28. package/scripts/migrate_core.py +2 -0
  29. package/scripts/model_policy.py +5 -1
  30. package/scripts/pipeline_diagnose.py +17 -2
  31. package/scripts/pipeline_git.py +68 -30
  32. package/scripts/pipeline_state.py +28 -6
  33. package/scripts/pipeline_undo.py +17 -8
  34. package/scripts/promote_lookahead.py +3 -3
  35. package/scripts/review_findings.py +180 -44
  36. package/scripts/roadmap.py +9 -1
  37. package/scripts/runtime_store.py +1 -0
  38. package/scripts/state_checkpoint.py +30 -9
  39. package/scripts/state_promote.py +15 -4
  40. package/scripts/task_context.py +2 -0
  41. package/scripts/workflow_run.py +9 -2
  42. package/skills/gsd-path/DEFINE.md +6 -2
  43. package/skills/gsd-path/PLAN.md +3 -0
  44. package/skills/gsd-path/ROADMAP.md +16 -6
  45. package/skills/gsd-path/SHIP.md +26 -4
  46. package/skills/gsd-path/SKILL.md +52 -24
  47. package/skills/gsd-path/references/build-native.md +96 -9
  48. package/skills/gsd-path/references/build-recovery.md +13 -0
  49. package/skills/gsd-path/references/coder.md +7 -3
  50. package/skills/gsd-path/references/operating-rules.md +11 -3
  51. package/skills/gsd-path/references/planner.md +13 -1
  52. package/skills/gsd-path/references/reviewer-final.md +5 -5
  53. package/skills/gsd-path/references/reviewer-wave.md +46 -13
  54. package/skills/gsd-path/references/runtime-selection.md +9 -0
  55. package/skills/gsd-path/scripts/_common.py +107 -1
  56. package/skills/gsd-path/scripts/build_recovery.py +62 -30
  57. package/skills/gsd-path/scripts/build_state.py +195 -5
  58. package/skills/gsd-path/scripts/check_handoffs.py +92 -30
  59. package/skills/gsd-path/scripts/check_task_briefs.py +19 -14
  60. package/skills/gsd-path/scripts/detect_project.py +17 -2
  61. package/skills/gsd-path/scripts/discussion_records.py +9 -1
  62. package/skills/gsd-path/scripts/discussion_validate.py +7 -6
  63. package/skills/gsd-path/scripts/dispatch_driver.py +157 -31
  64. package/skills/gsd-path/scripts/git_guard.py +24 -5
  65. package/skills/gsd-path/scripts/integration.py +98 -64
  66. package/skills/gsd-path/scripts/isolation.py +148 -11
  67. package/skills/gsd-path/scripts/members.py +68 -25
  68. package/skills/gsd-path/scripts/model_policy.py +5 -1
  69. package/skills/gsd-path/scripts/pipeline_git.py +68 -30
  70. package/skills/gsd-path/scripts/pipeline_state.py +28 -6
  71. package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
  72. package/skills/gsd-path/scripts/review_findings.py +180 -44
  73. package/skills/gsd-path/scripts/roadmap.py +9 -1
  74. package/skills/gsd-path/scripts/state_checkpoint.py +30 -9
  75. package/skills/gsd-path/scripts/state_promote.py +15 -4
  76. package/skills/gsd-path/scripts/task_context.py +2 -0
  77. package/skills/gsd-path/scripts/workflow_run.py +9 -2
  78. package/skills/gsd-path/templates/final-review.md +5 -5
  79. package/skills/gsd-path/templates/state.md +1 -0
  80. package/skills/gsd-path/templates/task.md +1 -0
  81. package/skills/gsd-path/templates/wave-review.md +18 -9
  82. package/skills/gsd-path-build/references/build-native.md +96 -9
  83. package/skills/gsd-path-build/references/build-recovery.md +13 -0
  84. package/skills/gsd-path-build/references/coder.md +7 -3
  85. package/skills/gsd-path-build/references/operating-rules.md +11 -3
  86. package/skills/gsd-path-build/references/reviewer-final.md +5 -5
  87. package/skills/gsd-path-build/references/reviewer-wave.md +46 -13
  88. package/skills/gsd-path-build/references/runtime-selection.md +9 -0
  89. package/skills/gsd-path-build/scripts/_common.py +107 -1
  90. package/skills/gsd-path-build/scripts/build_recovery.py +62 -30
  91. package/skills/gsd-path-build/scripts/build_state.py +195 -5
  92. package/skills/gsd-path-build/scripts/check_handoffs.py +92 -30
  93. package/skills/gsd-path-build/scripts/check_task_briefs.py +19 -14
  94. package/skills/gsd-path-build/scripts/detect_project.py +17 -2
  95. package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
  96. package/skills/gsd-path-build/scripts/discussion_validate.py +7 -6
  97. package/skills/gsd-path-build/scripts/dispatch_driver.py +157 -31
  98. package/skills/gsd-path-build/scripts/integration.py +98 -64
  99. package/skills/gsd-path-build/scripts/isolation.py +148 -11
  100. package/skills/gsd-path-build/scripts/members.py +68 -25
  101. package/skills/gsd-path-build/scripts/model_policy.py +5 -1
  102. package/skills/gsd-path-build/scripts/pipeline_git.py +68 -30
  103. package/skills/gsd-path-build/scripts/pipeline_state.py +28 -6
  104. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
  105. package/skills/gsd-path-build/scripts/review_findings.py +180 -44
  106. package/skills/gsd-path-build/scripts/roadmap.py +9 -1
  107. package/skills/gsd-path-build/scripts/state_checkpoint.py +30 -9
  108. package/skills/gsd-path-build/scripts/state_promote.py +15 -4
  109. package/skills/gsd-path-build/scripts/task_context.py +2 -0
  110. package/skills/gsd-path-build/scripts/workflow_run.py +9 -2
  111. package/skills/gsd-path-build/templates/task.md +1 -0
  112. package/skills/gsd-path-build/templates/wave-review.md +18 -9
  113. package/skills/gsd-path-decide/references/operating-rules.md +11 -3
  114. package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
  115. package/skills/gsd-path-decide/scripts/_common.py +107 -1
  116. package/skills/gsd-path-decide/scripts/build_recovery.py +62 -30
  117. package/skills/gsd-path-decide/scripts/check_handoffs.py +92 -30
  118. package/skills/gsd-path-decide/scripts/check_task_briefs.py +19 -14
  119. package/skills/gsd-path-decide/scripts/detect_project.py +17 -2
  120. package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
  121. package/skills/gsd-path-decide/scripts/discussion_validate.py +7 -6
  122. package/skills/gsd-path-decide/scripts/integration.py +98 -64
  123. package/skills/gsd-path-decide/scripts/isolation.py +148 -11
  124. package/skills/gsd-path-decide/scripts/members.py +68 -25
  125. package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
  126. package/skills/gsd-path-decide/scripts/pipeline_git.py +68 -30
  127. package/skills/gsd-path-decide/scripts/pipeline_state.py +28 -6
  128. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
  129. package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
  130. package/skills/gsd-path-decide/scripts/state_checkpoint.py +30 -9
  131. package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
  132. package/skills/gsd-path-define/SKILL.md +6 -2
  133. package/skills/gsd-path-define/references/build-recovery.md +13 -0
  134. package/skills/gsd-path-define/references/operating-rules.md +11 -3
  135. package/skills/gsd-path-define/references/runtime-selection.md +9 -0
  136. package/skills/gsd-path-define/scripts/_common.py +107 -1
  137. package/skills/gsd-path-define/scripts/build_recovery.py +62 -30
  138. package/skills/gsd-path-define/scripts/check_handoffs.py +92 -30
  139. package/skills/gsd-path-define/scripts/check_task_briefs.py +19 -14
  140. package/skills/gsd-path-define/scripts/detect_project.py +17 -2
  141. package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
  142. package/skills/gsd-path-define/scripts/discussion_validate.py +7 -6
  143. package/skills/gsd-path-define/scripts/integration.py +98 -64
  144. package/skills/gsd-path-define/scripts/isolation.py +148 -11
  145. package/skills/gsd-path-define/scripts/members.py +68 -25
  146. package/skills/gsd-path-define/scripts/model_policy.py +5 -1
  147. package/skills/gsd-path-define/scripts/pipeline_git.py +68 -30
  148. package/skills/gsd-path-define/scripts/pipeline_state.py +28 -6
  149. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
  150. package/skills/gsd-path-define/scripts/roadmap.py +9 -1
  151. package/skills/gsd-path-define/scripts/state_checkpoint.py +30 -9
  152. package/skills/gsd-path-define/scripts/state_promote.py +15 -4
  153. package/skills/gsd-path-define/templates/state.md +1 -0
  154. package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
  155. package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
  156. package/skills/gsd-path-discuss/scripts/_common.py +107 -1
  157. package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
  158. package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
  159. package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
  160. package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
  161. package/skills/gsd-path-docs-audit/scripts/_common.py +107 -1
  162. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +62 -30
  163. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +92 -30
  164. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +19 -14
  165. package/skills/gsd-path-docs-audit/scripts/detect_project.py +17 -2
  166. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
  167. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +7 -6
  168. package/skills/gsd-path-docs-audit/scripts/integration.py +98 -64
  169. package/skills/gsd-path-docs-audit/scripts/isolation.py +148 -11
  170. package/skills/gsd-path-docs-audit/scripts/members.py +68 -25
  171. package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
  172. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +68 -30
  173. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +28 -6
  174. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
  175. package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
  176. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +30 -9
  177. package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
  178. package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
  179. package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
  180. package/skills/gsd-path-forensics/scripts/_common.py +107 -1
  181. package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
  182. package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
  183. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
  184. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +17 -8
  185. package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
  186. package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
  187. package/skills/gsd-path-inspect/scripts/_common.py +107 -1
  188. package/skills/gsd-path-inspect/scripts/build_recovery.py +62 -30
  189. package/skills/gsd-path-inspect/scripts/check_handoffs.py +92 -30
  190. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +19 -14
  191. package/skills/gsd-path-inspect/scripts/detect_project.py +17 -2
  192. package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
  193. package/skills/gsd-path-inspect/scripts/discussion_validate.py +7 -6
  194. package/skills/gsd-path-inspect/scripts/integration.py +98 -64
  195. package/skills/gsd-path-inspect/scripts/isolation.py +148 -11
  196. package/skills/gsd-path-inspect/scripts/members.py +68 -25
  197. package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
  198. package/skills/gsd-path-inspect/scripts/pipeline_git.py +68 -30
  199. package/skills/gsd-path-inspect/scripts/pipeline_state.py +28 -6
  200. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
  201. package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
  202. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +30 -9
  203. package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
  204. package/skills/gsd-path-inspect/scripts/workflow_run.py +9 -2
  205. package/skills/gsd-path-inspect/templates/state.md +1 -0
  206. package/skills/gsd-path-loop/references/operating-rules.md +11 -3
  207. package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
  208. package/skills/gsd-path-loop/scripts/_common.py +107 -1
  209. package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
  210. package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
  211. package/skills/gsd-path-migrate/SKILL.md +8 -0
  212. package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
  213. package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
  214. package/skills/gsd-path-migrate/scripts/_common.py +107 -1
  215. package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
  216. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
  217. package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
  218. package/skills/gsd-path-plan/SKILL.md +3 -0
  219. package/skills/gsd-path-plan/references/build-recovery.md +13 -0
  220. package/skills/gsd-path-plan/references/operating-rules.md +11 -3
  221. package/skills/gsd-path-plan/references/planner.md +13 -1
  222. package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
  223. package/skills/gsd-path-plan/scripts/_common.py +107 -1
  224. package/skills/gsd-path-plan/scripts/build_recovery.py +62 -30
  225. package/skills/gsd-path-plan/scripts/check_handoffs.py +92 -30
  226. package/skills/gsd-path-plan/scripts/check_task_briefs.py +19 -14
  227. package/skills/gsd-path-plan/scripts/detect_project.py +17 -2
  228. package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
  229. package/skills/gsd-path-plan/scripts/discussion_validate.py +7 -6
  230. package/skills/gsd-path-plan/scripts/integration.py +98 -64
  231. package/skills/gsd-path-plan/scripts/isolation.py +148 -11
  232. package/skills/gsd-path-plan/scripts/members.py +68 -25
  233. package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
  234. package/skills/gsd-path-plan/scripts/pipeline_git.py +68 -30
  235. package/skills/gsd-path-plan/scripts/pipeline_state.py +28 -6
  236. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
  237. package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
  238. package/skills/gsd-path-plan/scripts/state_checkpoint.py +30 -9
  239. package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
  240. package/skills/gsd-path-plan/scripts/workflow_run.py +9 -2
  241. package/skills/gsd-path-plan/templates/task.md +1 -0
  242. package/skills/gsd-path-research/references/operating-rules.md +11 -3
  243. package/skills/gsd-path-research/references/runtime-selection.md +9 -0
  244. package/skills/gsd-path-research/scripts/_common.py +107 -1
  245. package/skills/gsd-path-research/scripts/build_recovery.py +62 -30
  246. package/skills/gsd-path-research/scripts/check_handoffs.py +92 -30
  247. package/skills/gsd-path-research/scripts/check_task_briefs.py +19 -14
  248. package/skills/gsd-path-research/scripts/detect_project.py +17 -2
  249. package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
  250. package/skills/gsd-path-research/scripts/discussion_validate.py +7 -6
  251. package/skills/gsd-path-research/scripts/integration.py +98 -64
  252. package/skills/gsd-path-research/scripts/isolation.py +148 -11
  253. package/skills/gsd-path-research/scripts/members.py +68 -25
  254. package/skills/gsd-path-research/scripts/model_policy.py +5 -1
  255. package/skills/gsd-path-research/scripts/pipeline_git.py +68 -30
  256. package/skills/gsd-path-research/scripts/pipeline_state.py +28 -6
  257. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
  258. package/skills/gsd-path-research/scripts/roadmap.py +9 -1
  259. package/skills/gsd-path-research/scripts/state_checkpoint.py +30 -9
  260. package/skills/gsd-path-research/scripts/state_promote.py +15 -4
  261. package/skills/gsd-path-roadmap/SKILL.md +16 -6
  262. package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
  263. package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
  264. package/skills/gsd-path-roadmap/scripts/_common.py +107 -1
  265. package/skills/gsd-path-roadmap/scripts/build_recovery.py +62 -30
  266. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +92 -30
  267. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +19 -14
  268. package/skills/gsd-path-roadmap/scripts/detect_project.py +17 -2
  269. package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
  270. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +7 -6
  271. package/skills/gsd-path-roadmap/scripts/integration.py +98 -64
  272. package/skills/gsd-path-roadmap/scripts/isolation.py +148 -11
  273. package/skills/gsd-path-roadmap/scripts/members.py +68 -25
  274. package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
  275. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +68 -30
  276. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +28 -6
  277. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
  278. package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
  279. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +30 -9
  280. package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
  281. package/skills/gsd-path-ship/SKILL.md +26 -4
  282. package/skills/gsd-path-ship/references/operating-rules.md +11 -3
  283. package/skills/gsd-path-ship/references/reviewer-final.md +5 -5
  284. package/skills/gsd-path-ship/references/reviewer-wave.md +46 -13
  285. package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
  286. package/skills/gsd-path-ship/scripts/_common.py +107 -1
  287. package/skills/gsd-path-ship/scripts/build_recovery.py +62 -30
  288. package/skills/gsd-path-ship/scripts/build_state.py +195 -5
  289. package/skills/gsd-path-ship/scripts/check_handoffs.py +92 -30
  290. package/skills/gsd-path-ship/scripts/check_task_briefs.py +19 -14
  291. package/skills/gsd-path-ship/scripts/detect_project.py +17 -2
  292. package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
  293. package/skills/gsd-path-ship/scripts/discussion_validate.py +7 -6
  294. package/skills/gsd-path-ship/scripts/integration.py +98 -64
  295. package/skills/gsd-path-ship/scripts/isolation.py +148 -11
  296. package/skills/gsd-path-ship/scripts/members.py +68 -25
  297. package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
  298. package/skills/gsd-path-ship/scripts/pipeline_git.py +68 -30
  299. package/skills/gsd-path-ship/scripts/pipeline_state.py +28 -6
  300. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
  301. package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
  302. package/skills/gsd-path-ship/scripts/state_checkpoint.py +30 -9
  303. package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
  304. package/skills/gsd-path-ship/scripts/workflow_run.py +9 -2
  305. package/skills/gsd-path-ship/templates/final-review.md +5 -5
  306. package/skills/gsd-path-ship/templates/wave-review.md +18 -9
  307. package/skills/gsd-path-undo/references/operating-rules.md +11 -3
  308. package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
  309. package/skills/gsd-path-undo/scripts/_common.py +107 -1
  310. package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
  311. package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
  312. package/skills/gsd-path-undo/scripts/pipeline_undo.py +17 -8
  313. package/skills/path/DEFINE.md +6 -2
  314. package/skills/path/PLAN.md +3 -0
  315. package/skills/path/ROADMAP.md +16 -6
  316. package/skills/path/SHIP.md +26 -4
  317. package/skills/path/SKILL.md +52 -24
  318. package/skills/path/references/build-native.md +96 -9
  319. package/skills/path/references/build-recovery.md +13 -0
  320. package/skills/path/references/coder.md +7 -3
  321. package/skills/path/references/operating-rules.md +11 -3
  322. package/skills/path/references/planner.md +13 -1
  323. package/skills/path/references/reviewer-final.md +5 -5
  324. package/skills/path/references/reviewer-wave.md +46 -13
  325. package/skills/path/references/runtime-selection.md +9 -0
  326. package/skills/path/scripts/_common.py +107 -1
  327. package/skills/path/scripts/build_recovery.py +62 -30
  328. package/skills/path/scripts/build_state.py +195 -5
  329. package/skills/path/scripts/check_handoffs.py +92 -30
  330. package/skills/path/scripts/check_task_briefs.py +19 -14
  331. package/skills/path/scripts/detect_project.py +17 -2
  332. package/skills/path/scripts/discussion_records.py +9 -1
  333. package/skills/path/scripts/discussion_validate.py +7 -6
  334. package/skills/path/scripts/dispatch_driver.py +157 -31
  335. package/skills/path/scripts/git_guard.py +24 -5
  336. package/skills/path/scripts/integration.py +98 -64
  337. package/skills/path/scripts/isolation.py +148 -11
  338. package/skills/path/scripts/members.py +68 -25
  339. package/skills/path/scripts/model_policy.py +5 -1
  340. package/skills/path/scripts/pipeline_git.py +68 -30
  341. package/skills/path/scripts/pipeline_state.py +28 -6
  342. package/skills/path/scripts/promote_lookahead.py +3 -3
  343. package/skills/path/scripts/review_findings.py +180 -44
  344. package/skills/path/scripts/roadmap.py +9 -1
  345. package/skills/path/scripts/state_checkpoint.py +30 -9
  346. package/skills/path/scripts/state_promote.py +15 -4
  347. package/skills/path/scripts/task_context.py +2 -0
  348. package/skills/path/scripts/workflow_run.py +9 -2
  349. package/skills/path/templates/final-review.md +5 -5
  350. package/skills/path/templates/state.md +1 -0
  351. package/skills/path/templates/task.md +1 -0
  352. package/skills/path/templates/wave-review.md +18 -9
@@ -100,13 +100,15 @@ def _supplied_contract(repo: Path, task: Path, token: str) -> bool:
100
100
  return path.is_file() and path.resolve() == repo.resolve() / token
101
101
 
102
102
 
103
+ def _pipeline_workspace_path(repo: Path, task: Path, token: str) -> bool:
104
+ track = _project_track(task, repo)
105
+ return track is not None and token.startswith(
106
+ tuple(f"{track}/{name}/" for name in PIPELINE_WORKSPACE_DIRS))
107
+
108
+
103
109
  def _pipeline_workspace_file(repo: Path, task: Path, token: str) -> bool:
104
110
  """Pipeline-owned paths may exist only in the working tree before checkpoint."""
105
- track = _project_track(task, repo)
106
- if track is None:
107
- return False
108
- prefixes = tuple(f"{track}/{name}/" for name in PIPELINE_WORKSPACE_DIRS)
109
- if not any(token.startswith(prefix) for prefix in prefixes):
111
+ if not _pipeline_workspace_path(repo, task, token):
110
112
  return False
111
113
  path = repo / token
112
114
  return path.is_file() and path.resolve() == repo.resolve() / token
@@ -277,7 +279,7 @@ def _verify_tokens(
277
279
 
278
280
 
279
281
  def _member_bases(root: Path):
280
- """Resolve a member name to (checkout, origin/main SHA), reading MEMBERS.md once."""
282
+ """Resolve a member name to (checkout, base SHA), reading MEMBERS.md once."""
281
283
  cache: Dict[str, Optional[Tuple[Path, str]]] = {}
282
284
 
283
285
  def locate(name: str) -> Optional[Tuple[Path, str]]:
@@ -290,21 +292,22 @@ def _member_bases(root: Path):
290
292
  except ImportError: # pragma: no cover - package import used by tests
291
293
  from scripts import members
292
294
  try:
293
- listed = {member["name"]: Path(member["checkout"]) for member in members.read_members(root)}
294
- checkout = listed.get(name)
295
- if checkout is None:
295
+ listed = {member["name"]: member for member in members.read_members(root)}
296
+ if name not in listed:
296
297
  cache[name] = None
297
298
  else:
299
+ checkout = Path(listed[name]["checkout"])
298
300
  # A build lock selects the bound tip, including during Plan recovery;
299
- # without the lock, use origin/main before the first build start.
301
+ # without the lock, use the member's recorded default branch on origin
302
+ # before the first build start.
300
303
  lock = root / members.LOCK_PATH
301
304
  locked = [entry for entry in (json.loads(lock.read_text(encoding="utf-8"))["members"]
302
305
  if lock.is_file() else []) if entry["name"] == name]
303
306
  if locked:
304
307
  cache[name] = (checkout, _resolve_base(checkout, f"refs/heads/{locked[0]['branch']}"))
305
308
  else:
306
- members.require_origin_main(checkout)
307
- cache[name] = (checkout, _resolve_base(checkout, "refs/remotes/origin/main"))
309
+ cache[name] = (checkout, members.require_origin_branch(
310
+ checkout, members.default_branch(listed[name])))
308
311
  except (members.MembersError, BriefError) as error:
309
312
  raise BriefError(f"member {name} is unavailable: {error}") from error
310
313
  return cache[name]
@@ -328,8 +331,8 @@ def _lint_task(
328
331
  checked = 0
329
332
  if fields is None:
330
333
  return task_id, [error or "invalid frontmatter"], None, checked
331
- # A member task's paths resolve in the member at its origin/main, where the
332
- # member bound branch starts; `repo:` absent means the coordinator.
334
+ # A member task's paths resolve in the member at its default branch on origin,
335
+ # where the member bound branch starts; `repo:` absent means the coordinator.
333
336
  git_root = repo
334
337
  member = fields.get("repo")
335
338
  if member is not None:
@@ -397,6 +400,8 @@ def _lint_task(
397
400
  and token not in supplied
398
401
  and not _supplied_contract(repo, path, token)
399
402
  and not _pipeline_workspace_file(repo, path, token)
403
+ # A landed brief is history: the pipeline moves its own files (build recovery, archive) later.
404
+ and not (task_id in (landed_bases or {}) and _pipeline_workspace_path(repo, path, token))
400
405
  and not _base_exists(git_root, base, token)
401
406
  ):
402
407
  problems.append(f"## {name} names a path missing at the layer base: {token}")
@@ -1291,10 +1291,17 @@ def project_slug(root: Path) -> str:
1291
1291
  return slug or "project"
1292
1292
 
1293
1293
 
1294
- def filled_state_template(template: str, slug: str, phase: str) -> str:
1294
+ def filled_state_template(
1295
+ template: str, slug: str, phase: str, default_branch: str = "main"
1296
+ ) -> str:
1295
1297
  if "<slug>" not in template:
1296
1298
  raise DetectError("state template missing slug placeholder")
1297
1299
  text = template.replace("<slug>", slug)
1300
+ text, count = re.subn(
1301
+ r"(?m)^default_branch: main\b", lambda _: f"default_branch: {default_branch}", text, count=1
1302
+ )
1303
+ if count != 1:
1304
+ raise DetectError("state template missing default_branch: main line")
1298
1305
  text, count = re.subn(r"(?m)^phase: define\b", f"phase: {phase}", text, count=1)
1299
1306
  if count != 1:
1300
1307
  raise DetectError("state template missing phase: define line")
@@ -2030,6 +2037,14 @@ def initialize(
2030
2037
  )
2031
2038
  if git.returncode != 0 or git.stdout.strip() != "true":
2032
2039
  return {**payload, "route": "setup-repository", "wrote_state": False}
2040
+ default_branch = _common.origin_default_branch(root)
2041
+ if not _common.valid_default_branch(default_branch):
2042
+ return {**payload, "wrote_state": False, "error": (
2043
+ f"origin default branch {default_branch!r} cannot be recorded in STATE.md: "
2044
+ "it is a GSD Path reserved name or it has a character that STATE frontmatter "
2045
+ "cannot hold. Rename the default branch, then run "
2046
+ "`git remote set-head origin --auto`."
2047
+ )}
2033
2048
  expected = "inspect" if payload["verdict"] == "brownfield" else "define"
2034
2049
  if phase is None:
2035
2050
  phase = expected
@@ -2043,7 +2058,7 @@ def initialize(
2043
2058
  return payload
2044
2059
  write_state_anchored(
2045
2060
  root,
2046
- filled_state_template(raw, project_slug(root), phase),
2061
+ filled_state_template(raw, project_slug(root), phase, default_branch),
2047
2062
  root_status,
2048
2063
  project_status,
2049
2064
  )
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import shutil
9
9
  import sys
10
+ import traceback
10
11
 
11
12
  # Runtime helpers must not modify their immutable installation.
12
13
  sys.dont_write_bytecode = True
@@ -33,7 +34,7 @@ def _load_pipeline_modules():
33
34
  try:
34
35
  from scripts import archive_milestone, pipeline_state, _common
35
36
  return _common, archive_milestone, pipeline_state
36
- except ModuleNotFoundError as error:
37
+ except ImportError as error: # a foreign `scripts` package
37
38
  if error.name not in {
38
39
  "scripts",
39
40
  "scripts.archive_milestone",
@@ -41,6 +42,13 @@ def _load_pipeline_modules():
41
42
  "scripts._common",
42
43
  }:
43
44
  raise
45
+ # A plain ImportError raised while a `scripts` module loaded is not a
46
+ # foreign package. A missing module falls back, as before.
47
+ if not isinstance(error, ModuleNotFoundError) and any(
48
+ frame.f_globals.get("__name__", "").startswith("scripts.")
49
+ for frame, _ in traceback.walk_tb(error.__traceback__)
50
+ ):
51
+ raise
44
52
  shared_scripts = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
45
53
  sys.path.insert(0, str(shared_scripts))
46
54
  import archive_milestone
@@ -507,7 +507,8 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
507
507
  criteria = check_handoffs._success_criteria(text)
508
508
  except check_handoffs.HandoffError as error:
509
509
  raise ArchiveError(str(error)) from error
510
- return {int(key[2:]): " ".join(value.split()) for key, value in criteria.items()}
510
+ # Each value keeps the form without sub-bullet markers that quotes_criterion reads.
511
+ return {int(key[2:]): value for key, value in criteria.items()}
511
512
 
512
513
 
513
514
  def parse_final_review(archive: Path) -> tuple:
@@ -563,7 +564,7 @@ def _final_review(archive: Path) -> tuple:
563
564
  if len(headings) != len(intent_criteria):
564
565
  raise ArchiveError("FINAL.md success criteria do not exactly cover INTENT.md")
565
566
  for _, number, criterion in headings:
566
- if " ".join(criterion.split()) != intent_criteria[number]:
567
+ if not _common.quotes_criterion(criterion, intent_criteria[number]):
567
568
  raise ArchiveError(
568
569
  f"FINAL.md SC{number} heading text differs from archived INTENT.md"
569
570
  )
@@ -951,10 +952,10 @@ def validate_wave_review(
951
952
  (index, match.group(1), " ".join(match.group(2).split()), match.group(3))
952
953
  )
953
954
  actual = [(sc_id, text) for _, sc_id, text, _ in criterion_headings]
954
- normalized_expected_criteria = [
955
- (sc_id, " ".join(text.split())) for sc_id, text in expected_criteria
956
- ]
957
- if actual != normalized_expected_criteria:
955
+ if len(actual) != len(expected_criteria) or any(
956
+ sc_id != expected_id or not _common.quotes_criterion(text, expected)
957
+ for (sc_id, text), (expected_id, expected) in zip(actual, expected_criteria)
958
+ ):
958
959
  raise ArchiveError(
959
960
  f"{path.name} Intent coverage does not match its owned success criteria"
960
961
  )
@@ -27,7 +27,7 @@ import tempfile
27
27
  import time
28
28
  from datetime import datetime, timezone
29
29
  from pathlib import Path
30
- from typing import BinaryIO, Callable, Dict, List, Optional
30
+ from typing import BinaryIO, Callable, Dict, List, Optional, Tuple
31
31
 
32
32
  try:
33
33
  from scripts import (_common, archive_milestone, build_state, discussion_validate, isolation,
@@ -77,6 +77,9 @@ STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError
77
77
  pipeline_state.PipelineStateError, archive_milestone.ArchiveError, model_policy.PolicyError)
78
78
  _RUNTIME_ACTIVATION_ORDER = (
79
79
  "_common",
80
+ "roadmap",
81
+ "review_panel",
82
+ "loop_run",
80
83
  "pipeline_git",
81
84
  "worktree_paths",
82
85
  "build_recovery",
@@ -87,11 +90,16 @@ _RUNTIME_ACTIVATION_ORDER = (
87
90
  "build_state",
88
91
  "check_handoffs",
89
92
  "task_context",
93
+ # discussion_validate and archive_milestone import each other: the second
94
+ # reload binds the reloaded ArchiveError that STOP_ERRORS catches.
95
+ "discussion_validate",
90
96
  "archive_milestone",
97
+ "discussion_validate",
91
98
  "review_findings",
92
99
  "model_policy",
93
100
  )
94
101
  RECOVERY_BLOCKED = "recovery blocked"
102
+ LEFTOVER_ISOLATE = "task {} is landed; remove its leftover isolate: {}"
95
103
  # The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
96
104
  # the same stops from review, panel, or skeptics stay with the parent.
97
105
  BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
@@ -640,6 +648,15 @@ def run_verify(command: str, cwd: Path) -> Dict[str, object]:
640
648
  return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
641
649
 
642
650
 
651
+ def landing_verify(command: str, regressions: List[Dict[str, object]],
652
+ cwd: Path) -> Tuple[Dict[str, object], List[Dict[str, object]]]:
653
+ """The task's own Verify, then every regression Verify when the own one passes."""
654
+ execution = run_verify(command, cwd)
655
+ if execution["exit_code"]:
656
+ return execution, []
657
+ return execution, [{**item, **run_verify(str(item["command"]), cwd)} for item in regressions]
658
+
659
+
643
660
  def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
644
661
  """A finish-able state derived from the task frontmatter alone, for coders dispatched by hand."""
645
662
  tasks_dir = primary / ".project/tasks"
@@ -693,6 +710,22 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
693
710
 
694
711
  def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
695
712
  """Verify in the isolate, land, record the ledger, retire. Returns the landing receipt."""
713
+ landing = land_task(primary, state)
714
+ try:
715
+ if state["mode"] == "member":
716
+ isolation.retire_member_task(primary, str(state["member"]), str(state["task_id"]))
717
+ else:
718
+ isolation.retire(primary, Path(str(state["worktree"])), state.get("task_branch") or None, False)
719
+ except isolation.IsolationError as error:
720
+ # The landing is proven, so a failed removal is a warning; Round.recover checks the isolate again.
721
+ landing["cleanup_warning"] = (f"{error}; the task is landed, but its isolate was not removed. The next "
722
+ "round or finish checks the isolate again and stops if the isolate is "
723
+ "still there")
724
+ return landing
725
+
726
+
727
+ def land_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
728
+ """Verify in the isolate, land, record the ledger. Returns the landing receipt."""
696
729
  worktree = Path(str(state["worktree"]))
697
730
  task_id = str(state["task_id"])
698
731
  task_file = str(state["task_file"])
@@ -701,6 +734,11 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
701
734
  command = _common.task_verify_command(task_path.read_text(encoding="utf-8"))
702
735
  if not command:
703
736
  raise DriverStop("task has no Verify command", task=task_id)
737
+ # A later task can break an earlier task that shares a file; that task's Verify also decides this landing.
738
+ regressions = build_state.regression_verifies(primary, task_id, command)
739
+ # A plan-approved `supersedes` retires an earlier Verify; the receipt and the Log show each one.
740
+ retired = build_state.retired_verifies(primary, task_id)
741
+ superseded = {"retired": retired} if retired else {}
704
742
  if state["mode"] == "serial":
705
743
  sidecar = state["sidecar"]
706
744
  ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
@@ -710,30 +748,45 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
710
748
  isolation.reproduce_verify_sidecar(sidecar_path, tree)
711
749
  except isolation.IsolationError as error:
712
750
  raise DriverStop(str(error), task=task_id) from error
713
- execution = run_verify(command, sidecar_path)
751
+ execution, reruns = landing_verify(command, regressions, sidecar_path)
714
752
  location = f"sidecar {sidecar['branch']}"
715
753
  isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
716
754
  isolation.retire(primary, sidecar_path, str(sidecar["branch"]), False)
717
755
  else:
718
- execution = run_verify(command, worktree)
756
+ execution, reruns = landing_verify(command, regressions, worktree)
719
757
  location = f"isolate {state['task_branch']}"
720
758
  passed = execution["exit_code"] == 0
759
+ broken = [item for item in reruns if item["exit_code"]]
721
760
  execution.update({"worktree": str(worktree), "location": location})
722
761
  evidence_dir = (Path(str(state["_path"])).parent if state.get("_path")
723
762
  else records_root(primary) / task_id / f"manual-{now().replace(':', '')}")
724
763
  evidence_dir.mkdir(parents=True, exist_ok=True)
725
764
  _common.atomic_write(evidence_dir / "verify.json",
726
- json.dumps(execution, indent=2, sort_keys=True) + "\n")
765
+ json.dumps({**execution, "regressions": reruns, **superseded},
766
+ indent=2, sort_keys=True) + "\n")
727
767
  execution["evidence"] = str(evidence_dir / "verify.json")
728
- output = "\n".join(f" {line}" for line in tail(execution["stdout"] + execution["stderr"]).splitlines())
729
- append_log(task_path, f"- {now()[:10]} — orchestrator Verify ({location}): "
730
- f"{'pass' if passed else 'fail'}, exit {execution['exit_code']}"
731
- + (f"; output tail:\n ```\n{output}\n ```" if output else ""))
732
- if not passed:
768
+ # The Verify of a superseding task runs in the place of the retired Verify: the label names both.
769
+ for label, run in [("Verify", execution),
770
+ *((f"regression Verify of {', '.join(item['tasks'])}"
771
+ + (f" (supersedes {', '.join(item['supersedes'])})" if item.get("supersedes") else ""),
772
+ item) for item in reruns)]:
773
+ output = "\n".join(f" {line}" for line in tail(run["stdout"] + run["stderr"]).splitlines())
774
+ append_log(task_path, f"- {now()[:10]} — orchestrator {label} ({location}): "
775
+ f"{'fail' if run['exit_code'] else 'pass'}, exit {run['exit_code']}"
776
+ + (f"; output tail:\n ```\n{output}\n ```" if output else ""))
777
+ for item in retired:
778
+ part = f": part of {item['part']}" if item.get("part") else ""
779
+ append_log(task_path, f"- {now()[:10]} — orchestrator regression Verify of {item['task']}{part} retired: "
780
+ f"superseded by {item['superseded_by']}")
781
+ if not passed or broken:
733
782
  if state["mode"] == "parallel":
734
783
  isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
735
784
  elif state["mode"] == "member":
736
785
  isolation.deactivate_member_task(primary, str(state["member"]), task_id)
786
+ if passed:
787
+ names = ", ".join(name for item in broken for name in item["tasks"])
788
+ raise DriverStop(f"regression Verify of {names} failed in the isolate", task=task_id,
789
+ execution=execution, regressions=broken)
737
790
  raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
738
791
  if state["mode"] == "member":
739
792
  member = str(state["member"])
@@ -744,9 +797,9 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
744
797
  ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
745
798
  if ledger:
746
799
  build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
747
- isolation.retire_member_task(primary, member, task_id)
748
800
  return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
749
- "landing": landing, "verify": execution, "ledger": ledger}
801
+ "landing": landing, "verify": execution, "regressions": reruns, **superseded,
802
+ "ledger": ledger}
750
803
  landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
751
804
  list(state["files"]))
752
805
  commit = str(landed["commit"])
@@ -754,9 +807,8 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
754
807
  ledger = isolation.git_output(primary, "rev-parse", f"{commit}^") == base
755
808
  if ledger:
756
809
  build_state.verify_record(str(primary), command, commit, "pass", execution=execution)
757
- isolation.retire(primary, worktree, state.get("task_branch") or None, False)
758
810
  return {"task": task_id, "commit": commit, "mode": landed["mode"], "verify": execution,
759
- "ledger": ledger}
811
+ "regressions": reruns, **superseded, "ledger": ledger}
760
812
 
761
813
 
762
814
  # --- shared by round and review --------------------------------------------
@@ -772,6 +824,11 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
772
824
  report = isolation.recover(primary, Path(project_dir) / "tasks")
773
825
  receipt["steps"].append({"script": "isolation.py recover", "result": report})
774
826
  if report["verdict"] == "block":
827
+ blocked = [task for task in report["tasks"] if task["verdict"] == "block"]
828
+ if blocked and all(task.get("leftover") for task in blocked):
829
+ # Every blocked task is landed: the stop names only the isolates to remove, and the build stays active.
830
+ raise DriverStop("; ".join(LEFTOVER_ISOLATE.format(task["task_id"], task["leftover"])
831
+ for task in blocked), recover=blocked)
775
832
  raise DriverStop(RECOVERY_BLOCKED, recover=report)
776
833
  return report
777
834
 
@@ -907,7 +964,7 @@ class Round:
907
964
  self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
908
965
  "result": result})
909
966
  if result["state"] == "landed":
910
- isolation.retire_member_task(self.primary, str(result["member"]), journal.stem)
967
+ self.retire_member(str(result["member"]), journal.stem, str(result["landing"]))
911
968
  retired_members.add(journal.stem)
912
969
  report = recover_report(self.primary, self.project_dir, self.receipt)
913
970
  for task in report["tasks"]:
@@ -936,16 +993,36 @@ class Round:
936
993
  recover=task,
937
994
  )
938
995
  if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
939
- isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
996
+ self.retire_member(str(task["member"]), str(task["task_id"]), str(task["landing"]))
940
997
  if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
941
998
  isolation.retire(self.primary, Path(str(worktree["path"])),
942
999
  str(task["task_branch"]), False)
943
- elif (verdict == "recovered" and worktree is None and task.get("task_branch")) or (
944
- verdict == "resume" and (task.get("landing_retry") or task.get("dispatch_retry"))):
945
- # ponytail: interrupted retirements and retries stay with the parent's documented
1000
+ elif verdict == "recovered" and worktree is None and task.get("task_branch"):
1001
+ # The isolate is gone and the landing is proven: retire proves the branch, then deletes it.
1002
+ isolation.retire(self.primary, None, str(task["task_branch"]), True, str(task["commit"]))
1003
+ elif verdict == "resume" and (task.get("landing_retry") or task.get("dispatch_retry")):
1004
+ # ponytail: interrupted retries stay with the parent's documented
946
1005
  # procedure; failed/blocked reconciliation surfaces through ready's typed error.
947
1006
  raise DriverStop(f"task {task.get('task_id')} needs recovery: {verdict}", recover=task)
948
1007
 
1008
+ def retire_member(self, member: str, task_id: str, landing: str) -> None:
1009
+ """Retire a landed member task's sidecar; a proven leftover of it is a stop that names only its path."""
1010
+ checkout, project, _ = isolation._member_context(self.primary, member)
1011
+ sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}")
1012
+ branch = f"refs/heads/{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}"
1013
+ tip = isolation.run_git(checkout, "rev-parse", "--verify", "--quiet", branch)
1014
+ commits = [landing, *tip.stdout.split()]
1015
+ try:
1016
+ isolation.retire_member_task(self.primary, member, task_id)
1017
+ except isolation.RemovalError as error:
1018
+ # Missing files are a removal that stopped part way; every other state keeps the refusal.
1019
+ if not (isolation.only_missing_files(sidecar)
1020
+ or isolation.holds_only_landed_files(checkout, sidecar, commits)):
1021
+ raise
1022
+ raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar), cleanup_error=str(error))
1023
+ if isolation.holds_only_landed_files(checkout, sidecar, commits):
1024
+ raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar))
1025
+
949
1026
  # settle exited children ---------------------------------------------
950
1027
 
951
1028
  def isolate_live(self, state: Dict[str, object]) -> bool:
@@ -1231,7 +1308,7 @@ class Round:
1231
1308
  continue
1232
1309
  prepared = self.runner("prepare-task", head, task["id"], round_size)
1233
1310
  isolate = prepared[0]["result"]
1234
- sidecar = prepared[1]["result"] if len(prepared) > 1 else None
1311
+ sidecar = prepared[-1]["result"] if len(prepared) > 1 else None # the isolate-verify step is last
1235
1312
  worktree = Path(str(isolate["worktree"]))
1236
1313
  isolation.activate_task(worktree, head, task["id"], f"build_{task['id'].lower()}",
1237
1314
  task["task_file"], isolate["task_branch"])
@@ -2007,6 +2084,23 @@ def next_task_id(tasks_dir: Path) -> str:
2007
2084
  return f"T{max(numbers, default=0) + 1:03d}"
2008
2085
 
2009
2086
 
2087
+ def replaced_review_base(primary: Path, project_dir: str, wave: int, cycle: int) -> Optional[str]:
2088
+ """The recovery base when this cycle's review replaces the one a build recovery set aside, else None."""
2089
+ recovery = pipeline_state._build_recovery().context(primary)
2090
+ if not recovery:
2091
+ return None
2092
+ relative = f"{project_dir}/review/wave-{wave}.cycle{cycle}.md"
2093
+ before = isolation.run_git(primary, "show", f"{recovery['base']}:{relative}")
2094
+ current = primary / relative
2095
+ # Plan approval restores the review of an unchanged wave before its checkpoint, so no commit
2096
+ # since the base touches that file. A set-aside review is deleted by that checkpoint.
2097
+ rewritten = isolation.run_git(primary, "log", "-1", "--format=%H", f"{recovery['base']}..HEAD", "--", relative)
2098
+ if (not rewritten.stdout.strip() and before.returncode == 0 and current.is_file()
2099
+ and current.read_text(encoding="utf-8") == before.stdout):
2100
+ return None # the restored review keeps its repairs
2101
+ return recovery["base"]
2102
+
2103
+
2010
2104
  def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2011
2105
  """Step 7: one fix task per findings batch, carrying failed criteria and observations verbatim."""
2012
2106
  receipt: Dict[str, object] = {"status": None, "wave": options.wave, "cycle": options.cycle,
@@ -2015,14 +2109,32 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2015
2109
  project = primary / options.project_dir
2016
2110
  tasks_dir = project / "tasks"
2017
2111
  repairs = []
2112
+ pending = []
2018
2113
  for path in sorted(tasks_dir.glob("*-fix-wave-*-cycle-*.md")):
2019
2114
  text = path.read_text(encoding="utf-8")
2020
2115
  fields, _ = isolation.task_frontmatter(text)
2021
2116
  repairs.append({"task": fields["id"], "path": path.relative_to(primary).as_posix(),
2022
- "wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or "")})
2117
+ "wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or ""),
2118
+ **({"repo": fields["repo"]} if fields.get("repo") else {})})
2119
+ if fields["status"] != "done":
2120
+ pending.append(repairs[-1])
2023
2121
  suffix = f"-fix-wave-{options.wave}-cycle-{options.cycle}.md"
2024
- existing = [item for item in repairs if item["path"].endswith(suffix)]
2025
- carried = {locator for item in repairs if item not in existing for locator in item["locators"]}
2122
+ # A build recovery restarts a changed wave at cycle 1, so the repairs and the PLAN heading
2123
+ # of the review it set aside carry the same names. They belong to that review, not this one.
2124
+ replaced = replaced_review_base(primary, options.project_dir, options.wave, options.cycle)
2125
+
2126
+ def of_replaced_review(relative: str) -> bool:
2127
+ if not replaced or isolation.run_git(primary, "show", f"{replaced}:{relative}").returncode != 0:
2128
+ return False
2129
+ return not isolation.run_git(primary, "log", "--diff-filter=AD", "--format=%H",
2130
+ f"{replaced}..HEAD", "--", relative).stdout.strip()
2131
+
2132
+ stale = [item for item in repairs if item["path"].endswith(suffix) and of_replaced_review(item["path"])]
2133
+ existing = [item for item in repairs if item["path"].endswith(suffix) and item not in stale]
2134
+ # A landed repair no longer carries its finding: a repeat proves that fix failed and needs the next one.
2135
+ # Two repos can carry the same locator, so a repair carries a finding only in its own repo.
2136
+ carried = {(item.get("repo"), locator) for item in pending if not item["path"].endswith(suffix)
2137
+ for locator in item["locators"]}
2026
2138
  findings = review_findings.compute(primary, options.project_dir, options.wave, options.cycle)
2027
2139
  receipt["findings"] = findings
2028
2140
  escalation = [key for key in ("structural_blockers", "skeptic_groups", "cap_reached", "all_refuted")
@@ -2043,8 +2155,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2043
2155
  repair_waves = {item["wave"] for item in existing}
2044
2156
  if len(repair_waves) > 1:
2045
2157
  raise DriverStop("repair tasks for this cycle disagree on their wave")
2046
- repair_heading = re.search(
2158
+ stale_waves = {item["wave"] for item in stale}
2159
+ repair_heading = next((heading for heading in re.finditer(
2047
2160
  rf"(?m)^## Wave (\d+) — fix wave {options.wave} cycle {options.cycle} review findings$", plan_text)
2161
+ if int(heading.group(1)) not in stale_waves), None)
2048
2162
  if repair_waves:
2049
2163
  fix_wave = next(iter(repair_waves))
2050
2164
  elif repair_heading:
@@ -2055,9 +2169,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2055
2169
  raise DriverStop("repair tasks disagree with the PLAN repair wave")
2056
2170
  receipt["existing"] = existing
2057
2171
  for batch in findings["fix_batches"]:
2058
- if any(set(batch["locators"]) <= set(item["locators"]) for item in existing):
2172
+ repo = batch.get("repo") # a member name, or None for the coordinator
2173
+ if any(item.get("repo") == repo and set(batch["locators"]) <= set(item["locators"]) for item in existing):
2059
2174
  continue
2060
- if all(locator in carried for locator in batch["locators"]):
2175
+ if all((repo, locator) in carried for locator in batch["locators"]):
2061
2176
  receipt["carried"].append(batch)
2062
2177
  continue
2063
2178
  batch_groups = [groups[locator] for locator in batch["locators"]]
@@ -2065,9 +2180,15 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2065
2180
  for source in sources:
2066
2181
  if source not in texts:
2067
2182
  raise DriverStop(f"fix batch names unknown task {source}")
2183
+ # One task changes one repo: the fix depends on, and runs the Verify of, the source tasks of its repo.
2184
+ sources = [source for source in sources if isolation.task_frontmatter(texts[source])[0].get("repo") == repo]
2185
+ # A repeat builds on the landed repairs of its finding: repair-evidence links them by this dependency.
2186
+ deps = sources + [item["task"] for item in repairs
2187
+ if item not in pending and item.get("repo") == repo
2188
+ and f"-fix-wave-{options.wave}-cycle-" in item["path"] and not item["path"].endswith(suffix)
2189
+ and set(item["locators"]) & set(batch["locators"])]
2068
2190
  task_id = next_task_id(tasks_dir)
2069
- verify = "set -e\n" + "\n".join( # each source Verify in its own subshell
2070
- f"(\n{command}\n)" for command in dict.fromkeys(_common.task_verify_command(texts[source]) for source in sources))
2191
+ verify = _common.fix_verify(_common.task_verify_command(texts[source]) for source in sources)
2071
2192
  heavy = any(re.search(r"(?m)^Heavy:\s*yes", texts[source]) for source in sources)
2072
2193
  criteria = "\n".join(f"{index}. {criterion}" for index, criterion in
2073
2194
  enumerate(dict.fromkeys(group["criterion"] for group in batch_groups), 1))
@@ -2075,18 +2196,19 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2075
2196
  f"### {group['locator']}\nCriterion: {group['criterion']}\n"
2076
2197
  + "\n".join(f"- {item['text']}" for item in group["observations"]) for group in batch_groups)
2077
2198
  files = "\n".join(f" - {path}" for path in batch["files"])
2199
+ member = f"repo: {repo}\n" if repo else "" # the task template puts `repo:` before `files:`
2078
2200
  title = f"Fix wave {options.wave} cycle {options.cycle} review findings in {', '.join(sources)}"
2079
2201
  text = f"""---
2080
2202
  id: {task_id}
2081
2203
  title: {title}
2082
2204
  wave: {fix_wave}
2083
- deps: [{', '.join(sources)}]
2205
+ deps: [{', '.join(deps)}]
2084
2206
  status: pending
2085
2207
  agent: null
2086
2208
  base: null
2087
2209
  worktree: null
2088
2210
  task_branch: null
2089
- files:
2211
+ {member}files:
2090
2212
  {files}
2091
2213
  ---
2092
2214
 
@@ -2135,8 +2257,8 @@ Heavy: {'yes' if heavy else 'no'}
2135
2257
  path = tasks_dir / f"{task_id}-fix-wave-{options.wave}-cycle-{options.cycle}.md"
2136
2258
  _common.atomic_write(path, text)
2137
2259
  receipt["created"].append({"task": task_id, "path": path.relative_to(primary).as_posix(),
2138
- "wave": fix_wave, "deps": sources, "files": batch["files"],
2139
- "locators": batch["locators"]})
2260
+ "wave": fix_wave, "deps": deps, "files": batch["files"],
2261
+ "locators": batch["locators"], **({"repo": repo} if repo else {})})
2140
2262
  rows = []
2141
2263
  for item in [*existing, *receipt["created"]]:
2142
2264
  fields, _ = isolation.task_frontmatter((primary / item["path"]).read_text(encoding="utf-8"))
@@ -2421,6 +2543,10 @@ def main(argv=None) -> int:
2421
2543
  if native and native["base"] != records[0].get("base"):
2422
2544
  records = []
2423
2545
  else:
2546
+ if (native and records[0]["outcome"] == "blocked" and records[0].get("native")
2547
+ and not records[0].get("command")):
2548
+ # A failed finish leaves a host-dispatched task active at its base: finish may run again.
2549
+ update_state(records[0], outcome=None, reason=None)
2424
2550
  native = None
2425
2551
  if arguments.task_id in current.proven:
2426
2552
  commit = current.proven[arguments.task_id]