@opengsd/gsd-path 1.5.0 → 1.6.1

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 (363) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +169 -0
  3. package/DOCS.md +4 -3
  4. package/FULL.md +68 -7
  5. package/HOOKS.md +81 -4
  6. package/MIGRATE.md +12 -0
  7. package/QUICK.md +4 -2
  8. package/README.md +69 -16
  9. package/RUNTIME.md +23 -5
  10. package/UPDATE.md +35 -0
  11. package/WORKFLOW.md +66 -7
  12. package/package.json +1 -1
  13. package/scripts/_common.py +150 -1
  14. package/scripts/build_recovery.py +62 -30
  15. package/scripts/build_state.py +195 -5
  16. package/scripts/check_handoffs.py +92 -30
  17. package/scripts/check_task_briefs.py +27 -19
  18. package/scripts/detect_project.py +17 -2
  19. package/scripts/discussion_records.py +9 -1
  20. package/scripts/discussion_validate.py +7 -6
  21. package/scripts/dispatch_driver.py +174 -36
  22. package/scripts/git_guard.py +42 -7
  23. package/scripts/guard_hook.py +1216 -47
  24. package/scripts/install.mjs +76 -3
  25. package/scripts/install.py +182 -37
  26. package/scripts/integration.py +115 -68
  27. package/scripts/isolation.py +192 -23
  28. package/scripts/members.py +68 -25
  29. package/scripts/migrate_core.py +2 -0
  30. package/scripts/model_policy.py +13 -2
  31. package/scripts/pipeline_diagnose.py +17 -2
  32. package/scripts/pipeline_git.py +87 -35
  33. package/scripts/pipeline_state.py +31 -7
  34. package/scripts/pipeline_undo.py +17 -8
  35. package/scripts/promote_lookahead.py +3 -3
  36. package/scripts/review_findings.py +180 -44
  37. package/scripts/roadmap.py +9 -1
  38. package/scripts/runtime_store.py +1 -0
  39. package/scripts/state_checkpoint.py +30 -9
  40. package/scripts/state_promote.py +92 -5
  41. package/scripts/task_context.py +2 -0
  42. package/scripts/workflow_run.py +9 -2
  43. package/skills/gsd-path/DEFINE.md +6 -2
  44. package/skills/gsd-path/PLAN.md +6 -2
  45. package/skills/gsd-path/ROADMAP.md +16 -6
  46. package/skills/gsd-path/SHIP.md +38 -11
  47. package/skills/gsd-path/SKILL.md +52 -24
  48. package/skills/gsd-path/references/build-native.md +96 -9
  49. package/skills/gsd-path/references/build-recovery.md +13 -0
  50. package/skills/gsd-path/references/coder.md +7 -3
  51. package/skills/gsd-path/references/model-policy.md +3 -1
  52. package/skills/gsd-path/references/operating-rules.md +14 -5
  53. package/skills/gsd-path/references/planner.md +13 -1
  54. package/skills/gsd-path/references/reviewer-final.md +5 -5
  55. package/skills/gsd-path/references/reviewer-wave.md +46 -13
  56. package/skills/gsd-path/references/runtime-selection.md +10 -0
  57. package/skills/gsd-path/scripts/_common.py +150 -1
  58. package/skills/gsd-path/scripts/build_recovery.py +62 -30
  59. package/skills/gsd-path/scripts/build_state.py +195 -5
  60. package/skills/gsd-path/scripts/check_handoffs.py +92 -30
  61. package/skills/gsd-path/scripts/check_task_briefs.py +27 -19
  62. package/skills/gsd-path/scripts/detect_project.py +17 -2
  63. package/skills/gsd-path/scripts/discussion_records.py +9 -1
  64. package/skills/gsd-path/scripts/discussion_validate.py +7 -6
  65. package/skills/gsd-path/scripts/dispatch_driver.py +174 -36
  66. package/skills/gsd-path/scripts/git_guard.py +42 -7
  67. package/skills/gsd-path/scripts/integration.py +115 -68
  68. package/skills/gsd-path/scripts/isolation.py +192 -23
  69. package/skills/gsd-path/scripts/members.py +68 -25
  70. package/skills/gsd-path/scripts/model_policy.py +13 -2
  71. package/skills/gsd-path/scripts/pipeline_git.py +87 -35
  72. package/skills/gsd-path/scripts/pipeline_state.py +31 -7
  73. package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
  74. package/skills/gsd-path/scripts/review_findings.py +180 -44
  75. package/skills/gsd-path/scripts/roadmap.py +9 -1
  76. package/skills/gsd-path/scripts/state_checkpoint.py +30 -9
  77. package/skills/gsd-path/scripts/state_promote.py +92 -5
  78. package/skills/gsd-path/scripts/task_context.py +2 -0
  79. package/skills/gsd-path/scripts/workflow_run.py +9 -2
  80. package/skills/gsd-path/templates/final-review.md +5 -5
  81. package/skills/gsd-path/templates/state.md +1 -0
  82. package/skills/gsd-path/templates/task.md +1 -0
  83. package/skills/gsd-path/templates/wave-review.md +18 -9
  84. package/skills/gsd-path-build/references/build-native.md +96 -9
  85. package/skills/gsd-path-build/references/build-recovery.md +13 -0
  86. package/skills/gsd-path-build/references/coder.md +7 -3
  87. package/skills/gsd-path-build/references/model-policy.md +3 -1
  88. package/skills/gsd-path-build/references/operating-rules.md +14 -5
  89. package/skills/gsd-path-build/references/reviewer-final.md +5 -5
  90. package/skills/gsd-path-build/references/reviewer-wave.md +46 -13
  91. package/skills/gsd-path-build/references/runtime-selection.md +10 -0
  92. package/skills/gsd-path-build/scripts/_common.py +150 -1
  93. package/skills/gsd-path-build/scripts/build_recovery.py +62 -30
  94. package/skills/gsd-path-build/scripts/build_state.py +195 -5
  95. package/skills/gsd-path-build/scripts/check_handoffs.py +92 -30
  96. package/skills/gsd-path-build/scripts/check_task_briefs.py +27 -19
  97. package/skills/gsd-path-build/scripts/detect_project.py +17 -2
  98. package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
  99. package/skills/gsd-path-build/scripts/discussion_validate.py +7 -6
  100. package/skills/gsd-path-build/scripts/dispatch_driver.py +174 -36
  101. package/skills/gsd-path-build/scripts/integration.py +115 -68
  102. package/skills/gsd-path-build/scripts/isolation.py +192 -23
  103. package/skills/gsd-path-build/scripts/members.py +68 -25
  104. package/skills/gsd-path-build/scripts/model_policy.py +13 -2
  105. package/skills/gsd-path-build/scripts/pipeline_git.py +87 -35
  106. package/skills/gsd-path-build/scripts/pipeline_state.py +31 -7
  107. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
  108. package/skills/gsd-path-build/scripts/review_findings.py +180 -44
  109. package/skills/gsd-path-build/scripts/roadmap.py +9 -1
  110. package/skills/gsd-path-build/scripts/state_checkpoint.py +30 -9
  111. package/skills/gsd-path-build/scripts/state_promote.py +92 -5
  112. package/skills/gsd-path-build/scripts/task_context.py +2 -0
  113. package/skills/gsd-path-build/scripts/workflow_run.py +9 -2
  114. package/skills/gsd-path-build/templates/task.md +1 -0
  115. package/skills/gsd-path-build/templates/wave-review.md +18 -9
  116. package/skills/gsd-path-decide/references/model-policy.md +3 -1
  117. package/skills/gsd-path-decide/references/operating-rules.md +14 -5
  118. package/skills/gsd-path-decide/references/runtime-selection.md +10 -0
  119. package/skills/gsd-path-decide/scripts/_common.py +150 -1
  120. package/skills/gsd-path-decide/scripts/build_recovery.py +62 -30
  121. package/skills/gsd-path-decide/scripts/check_handoffs.py +92 -30
  122. package/skills/gsd-path-decide/scripts/check_task_briefs.py +27 -19
  123. package/skills/gsd-path-decide/scripts/detect_project.py +17 -2
  124. package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
  125. package/skills/gsd-path-decide/scripts/discussion_validate.py +7 -6
  126. package/skills/gsd-path-decide/scripts/integration.py +115 -68
  127. package/skills/gsd-path-decide/scripts/isolation.py +192 -23
  128. package/skills/gsd-path-decide/scripts/members.py +68 -25
  129. package/skills/gsd-path-decide/scripts/model_policy.py +13 -2
  130. package/skills/gsd-path-decide/scripts/pipeline_git.py +87 -35
  131. package/skills/gsd-path-decide/scripts/pipeline_state.py +31 -7
  132. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
  133. package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
  134. package/skills/gsd-path-decide/scripts/state_checkpoint.py +30 -9
  135. package/skills/gsd-path-decide/scripts/state_promote.py +92 -5
  136. package/skills/gsd-path-define/SKILL.md +6 -2
  137. package/skills/gsd-path-define/references/build-recovery.md +13 -0
  138. package/skills/gsd-path-define/references/operating-rules.md +14 -5
  139. package/skills/gsd-path-define/references/runtime-selection.md +10 -0
  140. package/skills/gsd-path-define/scripts/_common.py +150 -1
  141. package/skills/gsd-path-define/scripts/build_recovery.py +62 -30
  142. package/skills/gsd-path-define/scripts/check_handoffs.py +92 -30
  143. package/skills/gsd-path-define/scripts/check_task_briefs.py +27 -19
  144. package/skills/gsd-path-define/scripts/detect_project.py +17 -2
  145. package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
  146. package/skills/gsd-path-define/scripts/discussion_validate.py +7 -6
  147. package/skills/gsd-path-define/scripts/integration.py +115 -68
  148. package/skills/gsd-path-define/scripts/isolation.py +192 -23
  149. package/skills/gsd-path-define/scripts/members.py +68 -25
  150. package/skills/gsd-path-define/scripts/model_policy.py +13 -2
  151. package/skills/gsd-path-define/scripts/pipeline_git.py +87 -35
  152. package/skills/gsd-path-define/scripts/pipeline_state.py +31 -7
  153. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
  154. package/skills/gsd-path-define/scripts/roadmap.py +9 -1
  155. package/skills/gsd-path-define/scripts/state_checkpoint.py +30 -9
  156. package/skills/gsd-path-define/scripts/state_promote.py +92 -5
  157. package/skills/gsd-path-define/templates/state.md +1 -0
  158. package/skills/gsd-path-discuss/references/operating-rules.md +14 -5
  159. package/skills/gsd-path-discuss/references/runtime-selection.md +10 -0
  160. package/skills/gsd-path-discuss/scripts/_common.py +150 -1
  161. package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
  162. package/skills/gsd-path-discuss/scripts/model_policy.py +13 -2
  163. package/skills/gsd-path-docs-audit/references/model-policy.md +3 -1
  164. package/skills/gsd-path-docs-audit/references/operating-rules.md +14 -5
  165. package/skills/gsd-path-docs-audit/references/runtime-selection.md +10 -0
  166. package/skills/gsd-path-docs-audit/scripts/_common.py +150 -1
  167. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +62 -30
  168. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +92 -30
  169. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +27 -19
  170. package/skills/gsd-path-docs-audit/scripts/detect_project.py +17 -2
  171. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
  172. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +7 -6
  173. package/skills/gsd-path-docs-audit/scripts/integration.py +115 -68
  174. package/skills/gsd-path-docs-audit/scripts/isolation.py +192 -23
  175. package/skills/gsd-path-docs-audit/scripts/members.py +68 -25
  176. package/skills/gsd-path-docs-audit/scripts/model_policy.py +13 -2
  177. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +87 -35
  178. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +31 -7
  179. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
  180. package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
  181. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +30 -9
  182. package/skills/gsd-path-docs-audit/scripts/state_promote.py +92 -5
  183. package/skills/gsd-path-forensics/references/operating-rules.md +14 -5
  184. package/skills/gsd-path-forensics/references/runtime-selection.md +10 -0
  185. package/skills/gsd-path-forensics/scripts/_common.py +150 -1
  186. package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
  187. package/skills/gsd-path-forensics/scripts/model_policy.py +13 -2
  188. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
  189. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +17 -8
  190. package/skills/gsd-path-inspect/references/model-policy.md +3 -1
  191. package/skills/gsd-path-inspect/references/operating-rules.md +14 -5
  192. package/skills/gsd-path-inspect/references/runtime-selection.md +10 -0
  193. package/skills/gsd-path-inspect/scripts/_common.py +150 -1
  194. package/skills/gsd-path-inspect/scripts/build_recovery.py +62 -30
  195. package/skills/gsd-path-inspect/scripts/check_handoffs.py +92 -30
  196. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +27 -19
  197. package/skills/gsd-path-inspect/scripts/detect_project.py +17 -2
  198. package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
  199. package/skills/gsd-path-inspect/scripts/discussion_validate.py +7 -6
  200. package/skills/gsd-path-inspect/scripts/integration.py +115 -68
  201. package/skills/gsd-path-inspect/scripts/isolation.py +192 -23
  202. package/skills/gsd-path-inspect/scripts/members.py +68 -25
  203. package/skills/gsd-path-inspect/scripts/model_policy.py +13 -2
  204. package/skills/gsd-path-inspect/scripts/pipeline_git.py +87 -35
  205. package/skills/gsd-path-inspect/scripts/pipeline_state.py +31 -7
  206. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
  207. package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
  208. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +30 -9
  209. package/skills/gsd-path-inspect/scripts/state_promote.py +92 -5
  210. package/skills/gsd-path-inspect/scripts/workflow_run.py +9 -2
  211. package/skills/gsd-path-inspect/templates/state.md +1 -0
  212. package/skills/gsd-path-loop/references/operating-rules.md +14 -5
  213. package/skills/gsd-path-loop/references/runtime-selection.md +10 -0
  214. package/skills/gsd-path-loop/scripts/_common.py +150 -1
  215. package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
  216. package/skills/gsd-path-loop/scripts/model_policy.py +13 -2
  217. package/skills/gsd-path-migrate/SKILL.md +8 -0
  218. package/skills/gsd-path-migrate/references/operating-rules.md +14 -5
  219. package/skills/gsd-path-migrate/references/runtime-selection.md +10 -0
  220. package/skills/gsd-path-migrate/scripts/_common.py +150 -1
  221. package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
  222. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
  223. package/skills/gsd-path-migrate/scripts/model_policy.py +13 -2
  224. package/skills/gsd-path-plan/SKILL.md +6 -2
  225. package/skills/gsd-path-plan/references/build-recovery.md +13 -0
  226. package/skills/gsd-path-plan/references/model-policy.md +3 -1
  227. package/skills/gsd-path-plan/references/operating-rules.md +14 -5
  228. package/skills/gsd-path-plan/references/planner.md +13 -1
  229. package/skills/gsd-path-plan/references/runtime-selection.md +10 -0
  230. package/skills/gsd-path-plan/scripts/_common.py +150 -1
  231. package/skills/gsd-path-plan/scripts/build_recovery.py +62 -30
  232. package/skills/gsd-path-plan/scripts/check_handoffs.py +92 -30
  233. package/skills/gsd-path-plan/scripts/check_task_briefs.py +27 -19
  234. package/skills/gsd-path-plan/scripts/detect_project.py +17 -2
  235. package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
  236. package/skills/gsd-path-plan/scripts/discussion_validate.py +7 -6
  237. package/skills/gsd-path-plan/scripts/integration.py +115 -68
  238. package/skills/gsd-path-plan/scripts/isolation.py +192 -23
  239. package/skills/gsd-path-plan/scripts/members.py +68 -25
  240. package/skills/gsd-path-plan/scripts/model_policy.py +13 -2
  241. package/skills/gsd-path-plan/scripts/pipeline_git.py +87 -35
  242. package/skills/gsd-path-plan/scripts/pipeline_state.py +31 -7
  243. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
  244. package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
  245. package/skills/gsd-path-plan/scripts/state_checkpoint.py +30 -9
  246. package/skills/gsd-path-plan/scripts/state_promote.py +92 -5
  247. package/skills/gsd-path-plan/scripts/workflow_run.py +9 -2
  248. package/skills/gsd-path-plan/templates/task.md +1 -0
  249. package/skills/gsd-path-research/references/model-policy.md +3 -1
  250. package/skills/gsd-path-research/references/operating-rules.md +14 -5
  251. package/skills/gsd-path-research/references/runtime-selection.md +10 -0
  252. package/skills/gsd-path-research/scripts/_common.py +150 -1
  253. package/skills/gsd-path-research/scripts/build_recovery.py +62 -30
  254. package/skills/gsd-path-research/scripts/check_handoffs.py +92 -30
  255. package/skills/gsd-path-research/scripts/check_task_briefs.py +27 -19
  256. package/skills/gsd-path-research/scripts/detect_project.py +17 -2
  257. package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
  258. package/skills/gsd-path-research/scripts/discussion_validate.py +7 -6
  259. package/skills/gsd-path-research/scripts/integration.py +115 -68
  260. package/skills/gsd-path-research/scripts/isolation.py +192 -23
  261. package/skills/gsd-path-research/scripts/members.py +68 -25
  262. package/skills/gsd-path-research/scripts/model_policy.py +13 -2
  263. package/skills/gsd-path-research/scripts/pipeline_git.py +87 -35
  264. package/skills/gsd-path-research/scripts/pipeline_state.py +31 -7
  265. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
  266. package/skills/gsd-path-research/scripts/roadmap.py +9 -1
  267. package/skills/gsd-path-research/scripts/state_checkpoint.py +30 -9
  268. package/skills/gsd-path-research/scripts/state_promote.py +92 -5
  269. package/skills/gsd-path-roadmap/SKILL.md +16 -6
  270. package/skills/gsd-path-roadmap/references/model-policy.md +3 -1
  271. package/skills/gsd-path-roadmap/references/operating-rules.md +14 -5
  272. package/skills/gsd-path-roadmap/references/runtime-selection.md +10 -0
  273. package/skills/gsd-path-roadmap/scripts/_common.py +150 -1
  274. package/skills/gsd-path-roadmap/scripts/build_recovery.py +62 -30
  275. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +92 -30
  276. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +27 -19
  277. package/skills/gsd-path-roadmap/scripts/detect_project.py +17 -2
  278. package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
  279. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +7 -6
  280. package/skills/gsd-path-roadmap/scripts/integration.py +115 -68
  281. package/skills/gsd-path-roadmap/scripts/isolation.py +192 -23
  282. package/skills/gsd-path-roadmap/scripts/members.py +68 -25
  283. package/skills/gsd-path-roadmap/scripts/model_policy.py +13 -2
  284. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +87 -35
  285. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +31 -7
  286. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
  287. package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
  288. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +30 -9
  289. package/skills/gsd-path-roadmap/scripts/state_promote.py +92 -5
  290. package/skills/gsd-path-ship/SKILL.md +38 -11
  291. package/skills/gsd-path-ship/references/model-policy.md +3 -1
  292. package/skills/gsd-path-ship/references/operating-rules.md +14 -5
  293. package/skills/gsd-path-ship/references/reviewer-final.md +5 -5
  294. package/skills/gsd-path-ship/references/reviewer-wave.md +46 -13
  295. package/skills/gsd-path-ship/references/runtime-selection.md +10 -0
  296. package/skills/gsd-path-ship/scripts/_common.py +150 -1
  297. package/skills/gsd-path-ship/scripts/build_recovery.py +62 -30
  298. package/skills/gsd-path-ship/scripts/build_state.py +195 -5
  299. package/skills/gsd-path-ship/scripts/check_handoffs.py +92 -30
  300. package/skills/gsd-path-ship/scripts/check_task_briefs.py +27 -19
  301. package/skills/gsd-path-ship/scripts/detect_project.py +17 -2
  302. package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
  303. package/skills/gsd-path-ship/scripts/discussion_validate.py +7 -6
  304. package/skills/gsd-path-ship/scripts/integration.py +115 -68
  305. package/skills/gsd-path-ship/scripts/isolation.py +192 -23
  306. package/skills/gsd-path-ship/scripts/members.py +68 -25
  307. package/skills/gsd-path-ship/scripts/model_policy.py +13 -2
  308. package/skills/gsd-path-ship/scripts/pipeline_git.py +87 -35
  309. package/skills/gsd-path-ship/scripts/pipeline_state.py +31 -7
  310. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
  311. package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
  312. package/skills/gsd-path-ship/scripts/state_checkpoint.py +30 -9
  313. package/skills/gsd-path-ship/scripts/state_promote.py +92 -5
  314. package/skills/gsd-path-ship/scripts/workflow_run.py +9 -2
  315. package/skills/gsd-path-ship/templates/final-review.md +5 -5
  316. package/skills/gsd-path-ship/templates/wave-review.md +18 -9
  317. package/skills/gsd-path-undo/references/operating-rules.md +14 -5
  318. package/skills/gsd-path-undo/references/runtime-selection.md +10 -0
  319. package/skills/gsd-path-undo/scripts/_common.py +150 -1
  320. package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
  321. package/skills/gsd-path-undo/scripts/model_policy.py +13 -2
  322. package/skills/gsd-path-undo/scripts/pipeline_undo.py +17 -8
  323. package/skills/path/DEFINE.md +6 -2
  324. package/skills/path/PLAN.md +6 -2
  325. package/skills/path/ROADMAP.md +16 -6
  326. package/skills/path/SHIP.md +38 -11
  327. package/skills/path/SKILL.md +52 -24
  328. package/skills/path/references/build-native.md +96 -9
  329. package/skills/path/references/build-recovery.md +13 -0
  330. package/skills/path/references/coder.md +7 -3
  331. package/skills/path/references/model-policy.md +3 -1
  332. package/skills/path/references/operating-rules.md +14 -5
  333. package/skills/path/references/planner.md +13 -1
  334. package/skills/path/references/reviewer-final.md +5 -5
  335. package/skills/path/references/reviewer-wave.md +46 -13
  336. package/skills/path/references/runtime-selection.md +10 -0
  337. package/skills/path/scripts/_common.py +150 -1
  338. package/skills/path/scripts/build_recovery.py +62 -30
  339. package/skills/path/scripts/build_state.py +195 -5
  340. package/skills/path/scripts/check_handoffs.py +92 -30
  341. package/skills/path/scripts/check_task_briefs.py +27 -19
  342. package/skills/path/scripts/detect_project.py +17 -2
  343. package/skills/path/scripts/discussion_records.py +9 -1
  344. package/skills/path/scripts/discussion_validate.py +7 -6
  345. package/skills/path/scripts/dispatch_driver.py +174 -36
  346. package/skills/path/scripts/git_guard.py +42 -7
  347. package/skills/path/scripts/integration.py +115 -68
  348. package/skills/path/scripts/isolation.py +192 -23
  349. package/skills/path/scripts/members.py +68 -25
  350. package/skills/path/scripts/model_policy.py +13 -2
  351. package/skills/path/scripts/pipeline_git.py +87 -35
  352. package/skills/path/scripts/pipeline_state.py +31 -7
  353. package/skills/path/scripts/promote_lookahead.py +3 -3
  354. package/skills/path/scripts/review_findings.py +180 -44
  355. package/skills/path/scripts/roadmap.py +9 -1
  356. package/skills/path/scripts/state_checkpoint.py +30 -9
  357. package/skills/path/scripts/state_promote.py +92 -5
  358. package/skills/path/scripts/task_context.py +2 -0
  359. package/skills/path/scripts/workflow_run.py +9 -2
  360. package/skills/path/templates/final-review.md +5 -5
  361. package/skills/path/templates/state.md +1 -0
  362. package/skills/path/templates/task.md +1 -0
  363. package/skills/path/templates/wave-review.md +18 -9
@@ -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"}
@@ -109,9 +117,21 @@ def _refresh_stop_errors() -> None:
109
117
  model_policy.PolicyError)
110
118
 
111
119
 
120
+ # The selected Project runtime directory once _reload_pinned_modules ran; None keeps bundled helpers.
121
+ _RUNTIME_ROOT: Optional[Path] = None
122
+
123
+
124
+ def helper_script(name: str) -> Path:
125
+ """A subprocess helper from the selected runtime when it ships one, else the bundled sibling."""
126
+ if _RUNTIME_ROOT is not None and (_RUNTIME_ROOT / name).is_file():
127
+ return _RUNTIME_ROOT / name
128
+ return Path(__file__).resolve().parent / name
129
+
130
+
112
131
  def _reload_pinned_modules(runtime_root: Path) -> None:
113
132
  global isolation, build_state, task_context, contracts, pipeline_state
114
- global archive_milestone, review_findings, model_policy
133
+ global archive_milestone, review_findings, model_policy, _RUNTIME_ROOT
134
+ _RUNTIME_ROOT = runtime_root
115
135
  if _bundled_helpers():
116
136
  path = str(runtime_root)
117
137
  if path not in sys.path:
@@ -640,6 +660,15 @@ def run_verify(command: str, cwd: Path) -> Dict[str, object]:
640
660
  return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
641
661
 
642
662
 
663
+ def landing_verify(command: str, regressions: List[Dict[str, object]],
664
+ cwd: Path) -> Tuple[Dict[str, object], List[Dict[str, object]]]:
665
+ """The task's own Verify, then every regression Verify when the own one passes."""
666
+ execution = run_verify(command, cwd)
667
+ if execution["exit_code"]:
668
+ return execution, []
669
+ return execution, [{**item, **run_verify(str(item["command"]), cwd)} for item in regressions]
670
+
671
+
643
672
  def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
644
673
  """A finish-able state derived from the task frontmatter alone, for coders dispatched by hand."""
645
674
  tasks_dir = primary / ".project/tasks"
@@ -693,6 +722,22 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
693
722
 
694
723
  def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
695
724
  """Verify in the isolate, land, record the ledger, retire. Returns the landing receipt."""
725
+ landing = land_task(primary, state)
726
+ try:
727
+ if state["mode"] == "member":
728
+ isolation.retire_member_task(primary, str(state["member"]), str(state["task_id"]))
729
+ else:
730
+ isolation.retire(primary, Path(str(state["worktree"])), state.get("task_branch") or None, False)
731
+ except isolation.IsolationError as error:
732
+ # The landing is proven, so a failed removal is a warning; Round.recover checks the isolate again.
733
+ landing["cleanup_warning"] = (f"{error}; the task is landed, but its isolate was not removed. The next "
734
+ "round or finish checks the isolate again and stops if the isolate is "
735
+ "still there")
736
+ return landing
737
+
738
+
739
+ def land_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
740
+ """Verify in the isolate, land, record the ledger. Returns the landing receipt."""
696
741
  worktree = Path(str(state["worktree"]))
697
742
  task_id = str(state["task_id"])
698
743
  task_file = str(state["task_file"])
@@ -701,6 +746,11 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
701
746
  command = _common.task_verify_command(task_path.read_text(encoding="utf-8"))
702
747
  if not command:
703
748
  raise DriverStop("task has no Verify command", task=task_id)
749
+ # A later task can break an earlier task that shares a file; that task's Verify also decides this landing.
750
+ regressions = build_state.regression_verifies(primary, task_id, command)
751
+ # A plan-approved `supersedes` retires an earlier Verify; the receipt and the Log show each one.
752
+ retired = build_state.retired_verifies(primary, task_id)
753
+ superseded = {"retired": retired} if retired else {}
704
754
  if state["mode"] == "serial":
705
755
  sidecar = state["sidecar"]
706
756
  ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
@@ -710,30 +760,45 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
710
760
  isolation.reproduce_verify_sidecar(sidecar_path, tree)
711
761
  except isolation.IsolationError as error:
712
762
  raise DriverStop(str(error), task=task_id) from error
713
- execution = run_verify(command, sidecar_path)
763
+ execution, reruns = landing_verify(command, regressions, sidecar_path)
714
764
  location = f"sidecar {sidecar['branch']}"
715
765
  isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
716
766
  isolation.retire(primary, sidecar_path, str(sidecar["branch"]), False)
717
767
  else:
718
- execution = run_verify(command, worktree)
768
+ execution, reruns = landing_verify(command, regressions, worktree)
719
769
  location = f"isolate {state['task_branch']}"
720
770
  passed = execution["exit_code"] == 0
771
+ broken = [item for item in reruns if item["exit_code"]]
721
772
  execution.update({"worktree": str(worktree), "location": location})
722
773
  evidence_dir = (Path(str(state["_path"])).parent if state.get("_path")
723
774
  else records_root(primary) / task_id / f"manual-{now().replace(':', '')}")
724
775
  evidence_dir.mkdir(parents=True, exist_ok=True)
725
776
  _common.atomic_write(evidence_dir / "verify.json",
726
- json.dumps(execution, indent=2, sort_keys=True) + "\n")
777
+ json.dumps({**execution, "regressions": reruns, **superseded},
778
+ indent=2, sort_keys=True) + "\n")
727
779
  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:
780
+ # The Verify of a superseding task runs in the place of the retired Verify: the label names both.
781
+ for label, run in [("Verify", execution),
782
+ *((f"regression Verify of {', '.join(item['tasks'])}"
783
+ + (f" (supersedes {', '.join(item['supersedes'])})" if item.get("supersedes") else ""),
784
+ item) for item in reruns)]:
785
+ output = "\n".join(f" {line}" for line in tail(run["stdout"] + run["stderr"]).splitlines())
786
+ append_log(task_path, f"- {now()[:10]} — orchestrator {label} ({location}): "
787
+ f"{'fail' if run['exit_code'] else 'pass'}, exit {run['exit_code']}"
788
+ + (f"; output tail:\n ```\n{output}\n ```" if output else ""))
789
+ for item in retired:
790
+ part = f": part of {item['part']}" if item.get("part") else ""
791
+ append_log(task_path, f"- {now()[:10]} — orchestrator regression Verify of {item['task']}{part} retired: "
792
+ f"superseded by {item['superseded_by']}")
793
+ if not passed or broken:
733
794
  if state["mode"] == "parallel":
734
795
  isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
735
796
  elif state["mode"] == "member":
736
797
  isolation.deactivate_member_task(primary, str(state["member"]), task_id)
798
+ if passed:
799
+ names = ", ".join(name for item in broken for name in item["tasks"])
800
+ raise DriverStop(f"regression Verify of {names} failed in the isolate", task=task_id,
801
+ execution=execution, regressions=broken)
737
802
  raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
738
803
  if state["mode"] == "member":
739
804
  member = str(state["member"])
@@ -744,9 +809,9 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
744
809
  ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
745
810
  if ledger:
746
811
  build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
747
- isolation.retire_member_task(primary, member, task_id)
748
812
  return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
749
- "landing": landing, "verify": execution, "ledger": ledger}
813
+ "landing": landing, "verify": execution, "regressions": reruns, **superseded,
814
+ "ledger": ledger}
750
815
  landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
751
816
  list(state["files"]))
752
817
  commit = str(landed["commit"])
@@ -754,9 +819,8 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
754
819
  ledger = isolation.git_output(primary, "rev-parse", f"{commit}^") == base
755
820
  if ledger:
756
821
  build_state.verify_record(str(primary), command, commit, "pass", execution=execution)
757
- isolation.retire(primary, worktree, state.get("task_branch") or None, False)
758
822
  return {"task": task_id, "commit": commit, "mode": landed["mode"], "verify": execution,
759
- "ledger": ledger}
823
+ "regressions": reruns, **superseded, "ledger": ledger}
760
824
 
761
825
 
762
826
  # --- shared by round and review --------------------------------------------
@@ -772,6 +836,11 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
772
836
  report = isolation.recover(primary, Path(project_dir) / "tasks")
773
837
  receipt["steps"].append({"script": "isolation.py recover", "result": report})
774
838
  if report["verdict"] == "block":
839
+ blocked = [task for task in report["tasks"] if task["verdict"] == "block"]
840
+ if blocked and all(task.get("leftover") for task in blocked):
841
+ # Every blocked task is landed: the stop names only the isolates to remove, and the build stays active.
842
+ raise DriverStop("; ".join(LEFTOVER_ISOLATE.format(task["task_id"], task["leftover"])
843
+ for task in blocked), recover=blocked)
775
844
  raise DriverStop(RECOVERY_BLOCKED, recover=report)
776
845
  return report
777
846
 
@@ -907,7 +976,7 @@ class Round:
907
976
  self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
908
977
  "result": result})
909
978
  if result["state"] == "landed":
910
- isolation.retire_member_task(self.primary, str(result["member"]), journal.stem)
979
+ self.retire_member(str(result["member"]), journal.stem, str(result["landing"]))
911
980
  retired_members.add(journal.stem)
912
981
  report = recover_report(self.primary, self.project_dir, self.receipt)
913
982
  for task in report["tasks"]:
@@ -924,7 +993,7 @@ class Round:
924
993
  diagnose = shlex.join([
925
994
  sys.executable,
926
995
  "-B",
927
- str(Path(__file__).with_name("pipeline_diagnose.py")),
996
+ str(helper_script("pipeline_diagnose.py")),
928
997
  "diagnose",
929
998
  "--repo",
930
999
  str(self.primary),
@@ -936,16 +1005,36 @@ class Round:
936
1005
  recover=task,
937
1006
  )
938
1007
  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"]))
1008
+ self.retire_member(str(task["member"]), str(task["task_id"]), str(task["landing"]))
940
1009
  if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
941
1010
  isolation.retire(self.primary, Path(str(worktree["path"])),
942
1011
  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
1012
+ elif verdict == "recovered" and worktree is None and task.get("task_branch"):
1013
+ # The isolate is gone and the landing is proven: retire proves the branch, then deletes it.
1014
+ isolation.retire(self.primary, None, str(task["task_branch"]), True, str(task["commit"]))
1015
+ elif verdict == "resume" and (task.get("landing_retry") or task.get("dispatch_retry")):
1016
+ # ponytail: interrupted retries stay with the parent's documented
946
1017
  # procedure; failed/blocked reconciliation surfaces through ready's typed error.
947
1018
  raise DriverStop(f"task {task.get('task_id')} needs recovery: {verdict}", recover=task)
948
1019
 
1020
+ def retire_member(self, member: str, task_id: str, landing: str) -> None:
1021
+ """Retire a landed member task's sidecar; a proven leftover of it is a stop that names only its path."""
1022
+ checkout, project, _ = isolation._member_context(self.primary, member)
1023
+ sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}")
1024
+ branch = f"refs/heads/{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}"
1025
+ tip = isolation.run_git(checkout, "rev-parse", "--verify", "--quiet", branch)
1026
+ commits = [landing, *tip.stdout.split()]
1027
+ try:
1028
+ isolation.retire_member_task(self.primary, member, task_id)
1029
+ except isolation.RemovalError as error:
1030
+ # Missing files are a removal that stopped part way; every other state keeps the refusal.
1031
+ if not (isolation.only_missing_files(sidecar)
1032
+ or isolation.holds_only_landed_files(checkout, sidecar, commits)):
1033
+ raise
1034
+ raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar), cleanup_error=str(error))
1035
+ if isolation.holds_only_landed_files(checkout, sidecar, commits):
1036
+ raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar))
1037
+
949
1038
  # settle exited children ---------------------------------------------
950
1039
 
951
1040
  def isolate_live(self, state: Dict[str, object]) -> bool:
@@ -1231,7 +1320,7 @@ class Round:
1231
1320
  continue
1232
1321
  prepared = self.runner("prepare-task", head, task["id"], round_size)
1233
1322
  isolate = prepared[0]["result"]
1234
- sidecar = prepared[1]["result"] if len(prepared) > 1 else None
1323
+ sidecar = prepared[-1]["result"] if len(prepared) > 1 else None # the isolate-verify step is last
1235
1324
  worktree = Path(str(isolate["worktree"]))
1236
1325
  isolation.activate_task(worktree, head, task["id"], f"build_{task['id'].lower()}",
1237
1326
  task["task_file"], isolate["task_branch"])
@@ -1600,8 +1689,8 @@ class Review:
1600
1689
 
1601
1690
 
1602
1691
  def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
1603
- """Run a bundled helper and return its JSON stdout; a non-zero exit is a typed stop."""
1604
- command = [sys.executable, "-B", str(Path(__file__).resolve().parent / script), *arguments]
1692
+ """Run a helper (runtime-selected, else bundled) and return its JSON stdout; a non-zero exit is a typed stop."""
1693
+ command = [sys.executable, "-B", str(helper_script(script)), *arguments]
1605
1694
  completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
1606
1695
  step = {"script": script, "command": command, "exit_code": completed.returncode,
1607
1696
  "stdout": completed.stdout, "stderr": completed.stderr}
@@ -2007,6 +2096,23 @@ def next_task_id(tasks_dir: Path) -> str:
2007
2096
  return f"T{max(numbers, default=0) + 1:03d}"
2008
2097
 
2009
2098
 
2099
+ def replaced_review_base(primary: Path, project_dir: str, wave: int, cycle: int) -> Optional[str]:
2100
+ """The recovery base when this cycle's review replaces the one a build recovery set aside, else None."""
2101
+ recovery = pipeline_state._build_recovery().context(primary)
2102
+ if not recovery:
2103
+ return None
2104
+ relative = f"{project_dir}/review/wave-{wave}.cycle{cycle}.md"
2105
+ before = isolation.run_git(primary, "show", f"{recovery['base']}:{relative}")
2106
+ current = primary / relative
2107
+ # Plan approval restores the review of an unchanged wave before its checkpoint, so no commit
2108
+ # since the base touches that file. A set-aside review is deleted by that checkpoint.
2109
+ rewritten = isolation.run_git(primary, "log", "-1", "--format=%H", f"{recovery['base']}..HEAD", "--", relative)
2110
+ if (not rewritten.stdout.strip() and before.returncode == 0 and current.is_file()
2111
+ and current.read_text(encoding="utf-8") == before.stdout):
2112
+ return None # the restored review keeps its repairs
2113
+ return recovery["base"]
2114
+
2115
+
2010
2116
  def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2011
2117
  """Step 7: one fix task per findings batch, carrying failed criteria and observations verbatim."""
2012
2118
  receipt: Dict[str, object] = {"status": None, "wave": options.wave, "cycle": options.cycle,
@@ -2015,14 +2121,32 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2015
2121
  project = primary / options.project_dir
2016
2122
  tasks_dir = project / "tasks"
2017
2123
  repairs = []
2124
+ pending = []
2018
2125
  for path in sorted(tasks_dir.glob("*-fix-wave-*-cycle-*.md")):
2019
2126
  text = path.read_text(encoding="utf-8")
2020
2127
  fields, _ = isolation.task_frontmatter(text)
2021
2128
  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 "")})
2129
+ "wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or ""),
2130
+ **({"repo": fields["repo"]} if fields.get("repo") else {})})
2131
+ if fields["status"] != "done":
2132
+ pending.append(repairs[-1])
2023
2133
  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"]}
2134
+ # A build recovery restarts a changed wave at cycle 1, so the repairs and the PLAN heading
2135
+ # of the review it set aside carry the same names. They belong to that review, not this one.
2136
+ replaced = replaced_review_base(primary, options.project_dir, options.wave, options.cycle)
2137
+
2138
+ def of_replaced_review(relative: str) -> bool:
2139
+ if not replaced or isolation.run_git(primary, "show", f"{replaced}:{relative}").returncode != 0:
2140
+ return False
2141
+ return not isolation.run_git(primary, "log", "--diff-filter=AD", "--format=%H",
2142
+ f"{replaced}..HEAD", "--", relative).stdout.strip()
2143
+
2144
+ stale = [item for item in repairs if item["path"].endswith(suffix) and of_replaced_review(item["path"])]
2145
+ existing = [item for item in repairs if item["path"].endswith(suffix) and item not in stale]
2146
+ # A landed repair no longer carries its finding: a repeat proves that fix failed and needs the next one.
2147
+ # Two repos can carry the same locator, so a repair carries a finding only in its own repo.
2148
+ carried = {(item.get("repo"), locator) for item in pending if not item["path"].endswith(suffix)
2149
+ for locator in item["locators"]}
2026
2150
  findings = review_findings.compute(primary, options.project_dir, options.wave, options.cycle)
2027
2151
  receipt["findings"] = findings
2028
2152
  escalation = [key for key in ("structural_blockers", "skeptic_groups", "cap_reached", "all_refuted")
@@ -2043,8 +2167,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2043
2167
  repair_waves = {item["wave"] for item in existing}
2044
2168
  if len(repair_waves) > 1:
2045
2169
  raise DriverStop("repair tasks for this cycle disagree on their wave")
2046
- repair_heading = re.search(
2170
+ stale_waves = {item["wave"] for item in stale}
2171
+ repair_heading = next((heading for heading in re.finditer(
2047
2172
  rf"(?m)^## Wave (\d+) — fix wave {options.wave} cycle {options.cycle} review findings$", plan_text)
2173
+ if int(heading.group(1)) not in stale_waves), None)
2048
2174
  if repair_waves:
2049
2175
  fix_wave = next(iter(repair_waves))
2050
2176
  elif repair_heading:
@@ -2055,9 +2181,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2055
2181
  raise DriverStop("repair tasks disagree with the PLAN repair wave")
2056
2182
  receipt["existing"] = existing
2057
2183
  for batch in findings["fix_batches"]:
2058
- if any(set(batch["locators"]) <= set(item["locators"]) for item in existing):
2184
+ repo = batch.get("repo") # a member name, or None for the coordinator
2185
+ if any(item.get("repo") == repo and set(batch["locators"]) <= set(item["locators"]) for item in existing):
2059
2186
  continue
2060
- if all(locator in carried for locator in batch["locators"]):
2187
+ if all((repo, locator) in carried for locator in batch["locators"]):
2061
2188
  receipt["carried"].append(batch)
2062
2189
  continue
2063
2190
  batch_groups = [groups[locator] for locator in batch["locators"]]
@@ -2065,9 +2192,15 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2065
2192
  for source in sources:
2066
2193
  if source not in texts:
2067
2194
  raise DriverStop(f"fix batch names unknown task {source}")
2195
+ # One task changes one repo: the fix depends on, and runs the Verify of, the source tasks of its repo.
2196
+ sources = [source for source in sources if isolation.task_frontmatter(texts[source])[0].get("repo") == repo]
2197
+ # A repeat builds on the landed repairs of its finding: repair-evidence links them by this dependency.
2198
+ deps = sources + [item["task"] for item in repairs
2199
+ if item not in pending and item.get("repo") == repo
2200
+ and f"-fix-wave-{options.wave}-cycle-" in item["path"] and not item["path"].endswith(suffix)
2201
+ and set(item["locators"]) & set(batch["locators"])]
2068
2202
  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))
2203
+ verify = _common.fix_verify(_common.task_verify_command(texts[source]) for source in sources)
2071
2204
  heavy = any(re.search(r"(?m)^Heavy:\s*yes", texts[source]) for source in sources)
2072
2205
  criteria = "\n".join(f"{index}. {criterion}" for index, criterion in
2073
2206
  enumerate(dict.fromkeys(group["criterion"] for group in batch_groups), 1))
@@ -2075,18 +2208,19 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
2075
2208
  f"### {group['locator']}\nCriterion: {group['criterion']}\n"
2076
2209
  + "\n".join(f"- {item['text']}" for item in group["observations"]) for group in batch_groups)
2077
2210
  files = "\n".join(f" - {path}" for path in batch["files"])
2211
+ member = f"repo: {repo}\n" if repo else "" # the task template puts `repo:` before `files:`
2078
2212
  title = f"Fix wave {options.wave} cycle {options.cycle} review findings in {', '.join(sources)}"
2079
2213
  text = f"""---
2080
2214
  id: {task_id}
2081
2215
  title: {title}
2082
2216
  wave: {fix_wave}
2083
- deps: [{', '.join(sources)}]
2217
+ deps: [{', '.join(deps)}]
2084
2218
  status: pending
2085
2219
  agent: null
2086
2220
  base: null
2087
2221
  worktree: null
2088
2222
  task_branch: null
2089
- files:
2223
+ {member}files:
2090
2224
  {files}
2091
2225
  ---
2092
2226
 
@@ -2135,8 +2269,8 @@ Heavy: {'yes' if heavy else 'no'}
2135
2269
  path = tasks_dir / f"{task_id}-fix-wave-{options.wave}-cycle-{options.cycle}.md"
2136
2270
  _common.atomic_write(path, text)
2137
2271
  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"]})
2272
+ "wave": fix_wave, "deps": deps, "files": batch["files"],
2273
+ "locators": batch["locators"], **({"repo": repo} if repo else {})})
2140
2274
  rows = []
2141
2275
  for item in [*existing, *receipt["created"]]:
2142
2276
  fields, _ = isolation.task_frontmatter((primary / item["path"]).read_text(encoding="utf-8"))
@@ -2168,7 +2302,7 @@ Heavy: {'yes' if heavy else 'no'}
2168
2302
  if plan_changed:
2169
2303
  _common.atomic_write(plan_path, updated)
2170
2304
  receipt["plan_wave"] = fix_wave
2171
- lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
2305
+ lint = subprocess.run([sys.executable, "-B", str(helper_script("check_task_briefs.py")),
2172
2306
  "--repo", str(primary), "--base", isolation.current_sha(primary),
2173
2307
  "--tasks-dir", f"{options.project_dir}/tasks",
2174
2308
  "--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
@@ -2421,6 +2555,10 @@ def main(argv=None) -> int:
2421
2555
  if native and native["base"] != records[0].get("base"):
2422
2556
  records = []
2423
2557
  else:
2558
+ if (native and records[0]["outcome"] == "blocked" and records[0].get("native")
2559
+ and not records[0].get("command")):
2560
+ # A failed finish leaves a host-dispatched task active at its base: finish may run again.
2561
+ update_state(records[0], outcome=None, reason=None)
2424
2562
  native = None
2425
2563
  if arguments.task_id in current.proven:
2426
2564
  commit = current.proven[arguments.task_id]
@@ -19,9 +19,10 @@ _HERE = Path(__file__).resolve().parent
19
19
  sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py").is_file() else _HERE))
20
20
  sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
21
21
  try:
22
- from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
22
+ from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter, undeclared_paths
23
23
  from pipeline_git import is_ship_subject, ship_body_matches, ship_commit_body, task_commit_body
24
24
  from pipeline_state import _completion_status
25
+ from _common import valid_default_branch
25
26
  import members
26
27
  except ImportError as error: # pragma: no cover - broken install
27
28
  print(
@@ -68,6 +69,11 @@ PUBLICATION_HINT = (
68
69
  "(STATE shipped/done) through archive_milestone.py integrate; findings during "
69
70
  "ship reopen through the patch plan (see SHIP.md)"
70
71
  )
72
+ AUTHORIZED_PUSH_HINT = (
73
+ "an owner-approved push of one exact commit to a branch other than the bound "
74
+ "or default branch needs refs/gsd-path/authorizations/<project>/push/<ref> "
75
+ "at that commit (see HOOKS.md)"
76
+ )
71
77
  LEGACY_STATE_FIELDS = {
72
78
  "pipeline",
73
79
  "project",
@@ -240,7 +246,10 @@ def frontmatter_of(content, label):
240
246
 
241
247
 
242
248
  def strict_ship_state(state, archive):
243
- fields = frozenset(state)
249
+ # default_branch is optional beside each accepted field set.
250
+ if "default_branch" in state and not valid_default_branch(state["default_branch"]):
251
+ return False
252
+ fields = frozenset(state) - {"default_branch"}
244
253
  if fields not in {
245
254
  frozenset(LEGACY_STATE_FIELDS),
246
255
  frozenset(INTEGRATION_STATE_FIELDS),
@@ -509,7 +518,7 @@ def product_commit_violations(entries, subject, body):
509
518
  if error:
510
519
  problems.append(error)
511
520
  if contract_paths is not None:
512
- stray = sorted(set(staged) - contract_paths)
521
+ stray = undeclared_paths(repo_root(), base, set(staged), contract_paths)
513
522
  if stray:
514
523
  problems.append("undeclared paths: " + ", ".join(stray))
515
524
  if body != task_commit_body(task_file, staged, base).strip():
@@ -559,8 +568,10 @@ def member_pre_push_violations(lines, role):
559
568
  """Member mode: the member's own STATE is ignored; coordinator refs need authorization."""
560
569
  checkout, project = repo_root(), role["project"]
561
570
  reserved = members.member_ref_prefixes(project)
562
- members.require_origin_main(checkout)
563
- exclude = ["--not", "refs/remotes/origin/main"]
571
+ # The coordinator's MEMBERS.md records this member's default branch.
572
+ default = members.member_default_branch(role["coordinator"], role["name"])
573
+ members.require_origin_branch(checkout, default)
574
+ exclude = ["--not", f"refs/remotes/origin/{default}"]
564
575
  member_work = None
565
576
  found = []
566
577
  for _, local_sha, remote_ref, remote_sha in _push_updates(lines):
@@ -615,14 +626,25 @@ def pre_push_violations(lines):
615
626
  continue
616
627
  state = state_at(local_sha)
617
628
  owed = unshipped_bound_branch(state) if state is not None else None
618
- if owed is not None:
629
+ if owed is not None and not authorized_publication(state, remote_ref, local_sha):
619
630
  found.append(
620
631
  f"{remote_ref} <- {local_sha[:12]} carries unshipped {owed} work "
621
- f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}"
632
+ f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}; "
633
+ f"{AUTHORIZED_PUSH_HINT}"
622
634
  )
623
635
  return found
624
636
 
625
637
 
638
+ def authorized_publication(state, remote_ref, sha):
639
+ """The owner authorized this exact commit on this one non-default branch."""
640
+ project = state.get("project")
641
+ if not isinstance(project, str) or not members.NAME_RE.fullmatch(project):
642
+ return False
643
+ if not remote_ref.startswith("refs/heads/") or remote_ref == f"refs/heads/{default_branch()}":
644
+ return False
645
+ return members.authorized(repo_root(), project, "push", remote_ref, sha)
646
+
647
+
626
648
  def is_ship_commit(subject):
627
649
  return bool(subject and subject.strip().casefold().startswith("ship:"))
628
650
 
@@ -641,7 +663,20 @@ def abandon_contract_violations(subject, body, abandon):
641
663
  return []
642
664
 
643
665
 
666
+ def recorded_default_branch():
667
+ """STATE default_branch in the worktree: main without the field; None without a readable STATE."""
668
+ try:
669
+ content = (repo_root() / ".project" / "STATE.md").read_text(encoding="utf-8")
670
+ name = frontmatter_of(content, "STATE.md").get("default_branch", "main")
671
+ except (OSError, ValueError, subprocess.CalledProcessError):
672
+ return None
673
+ return name if valid_default_branch(name) else None
674
+
675
+
644
676
  def default_branch():
677
+ recorded = recorded_default_branch()
678
+ if recorded:
679
+ return recorded
645
680
  origin_head = subprocess.run(
646
681
  ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
647
682
  capture_output=True,