@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
@@ -25,7 +25,8 @@ import signal
25
25
  import subprocess
26
26
  import time
27
27
  from pathlib import Path
28
- from typing import BinaryIO, Iterator, List, Optional, Sequence
28
+ from typing import BinaryIO, Iterable, Iterator, List, Optional, Sequence, Tuple
29
+
29
30
 
30
31
  # Chosen by what imports, not os.name, so tests can emulate Windows on POSIX.
31
32
  try:
@@ -161,6 +162,33 @@ def project_ignore_error(repo: Path) -> Optional[str]:
161
162
  + ". Anchor the product rule (for example `/build/`) in a task that owns it.")
162
163
 
163
164
 
165
+ _DEFAULT_BRANCH_RE = re.compile(
166
+ r"[^\s#\"'\\~^:?*\[\x00-\x1f\x7f-][^\s#\"'\\~^:?*\[\x00-\x1f\x7f]*"
167
+ )
168
+ _RESERVED_BRANCH_PREFIXES = ("gsd-path/", "gsd-path-integrate/", "gsd-path-task/")
169
+
170
+
171
+ def valid_default_branch(name: str) -> bool:
172
+ """A git branch name that STATE frontmatter can hold and Path does not reserve."""
173
+ return (
174
+ bool(_DEFAULT_BRANCH_RE.fullmatch(name))
175
+ and name != "null"
176
+ and not name.startswith(_RESERVED_BRANCH_PREFIXES)
177
+ and not name.endswith(("/", ".", ".lock"))
178
+ and not any(part in name for part in ("..", "//", "/.", "@{"))
179
+ )
180
+
181
+
182
+ def origin_default_branch(repo: Path) -> str:
183
+ """The origin default branch that initialization records; an unset origin/HEAD means main."""
184
+ result = run_git(repo, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
185
+ prefix = "refs/remotes/origin/"
186
+ remote_ref = result.stdout.strip()
187
+ if result.returncode != 0 or not remote_ref.startswith(prefix):
188
+ return "main"
189
+ return remote_ref.removeprefix(prefix)
190
+
191
+
164
192
  def section_body(text: str, heading: str) -> Optional[str]:
165
193
  """The text under `## <heading>` up to the next `## `, or None when absent."""
166
194
  match = re.search(
@@ -169,6 +197,79 @@ def section_body(text: str, heading: str) -> Optional[str]:
169
197
  return match.group("body") if match else None
170
198
 
171
199
 
200
+ def overlapping_paths(left: Sequence[str], right: Sequence[str]) -> List[str]:
201
+ """Return the sorted paths that two task `files` lists both cover.
202
+
203
+ An entry covers itself and the paths below it. For a declared directory
204
+ and a path below it, the result names the lower path. The callers have no
205
+ task base, so this does not ask if an entry is a file: the task-brief
206
+ lint rejects a path below a file at the base.
207
+ """
208
+ return sorted(
209
+ {
210
+ max(one, other, key=len)
211
+ for one in left
212
+ for other in right
213
+ if one == other or one.startswith(other + "/") or other.startswith(one + "/")
214
+ }
215
+ )
216
+
217
+
218
+ # The `- ` of an indented sub-bullet line. A line that would then read as a
219
+ # numbered item (`- 2. text`) keeps its marker, so both readings of a source
220
+ # have the same criteria.
221
+ SUB_BULLET_MARKER = re.compile(r"(?m)^([ \t]+)-[ \t]+(?![ \t]*\d+\.\s)")
222
+
223
+
224
+ def without_sub_bullet_markers(source: str) -> str:
225
+ """`source` with the `- ` marker of each indented sub-bullet line removed."""
226
+ return SUB_BULLET_MARKER.sub(r"\1", source)
227
+
228
+
229
+ class Criterion(str):
230
+ """A criterion as its parser joins it, with the `- ` markers of its sub-bullet lines.
231
+
232
+ `bare` is the same criterion parsed from `without_sub_bullet_markers(source)`.
233
+ The joined text cannot tell a marker from a minus sign; the source lines can.
234
+ """
235
+
236
+ bare: str
237
+
238
+ def __new__(cls, text: str, bare: str) -> "Criterion":
239
+ criterion = super().__new__(cls, text)
240
+ criterion.bare = bare
241
+ return criterion
242
+
243
+
244
+ def quotes_criterion(quote: str, criterion: str) -> bool:
245
+ """Whether a review quote is `criterion`, for every reader.
246
+
247
+ Only whitespace is collapsed. The quote must equal the criterion with all
248
+ the markers of its sub-bullet lines or with none of them; every other
249
+ character, a hyphen inside a line too, is compared.
250
+ """
251
+ forms = (criterion, getattr(criterion, "bare", criterion))
252
+ return " ".join(quote.split()) in {" ".join(form.split()) for form in forms}
253
+
254
+
255
+ def quoted_criterion(item: str, criteria: Sequence[str]) -> Optional[Tuple[int, int]]:
256
+ """Index of the criterion quoted at the start of `item`, and where the quote ends.
257
+
258
+ A criterion can contain ` — ` itself, so each separator and the end of
259
+ `item` is a candidate, the longest quote first. A criterion as written
260
+ wins over any criterion read without its sub-bullet markers.
261
+ """
262
+ ends = [match.start() for match in re.finditer("(?= —(?: |$))|$", item)][::-1]
263
+ for bare in (False, True):
264
+ for end in ends:
265
+ quote = " ".join(item[:end].split())
266
+ for index, criterion in enumerate(criteria):
267
+ form = getattr(criterion, "bare", criterion) if bare else criterion
268
+ if quote == " ".join(form.split()):
269
+ return index, end
270
+ return None
271
+
272
+
172
273
  def task_verify_command(task_text: str) -> str:
173
274
  """The Verify shell text, excluding the closing fence's separator newline."""
174
275
  body = section_body(task_text, "Verify")
@@ -176,6 +277,11 @@ def task_verify_command(task_text: str) -> str:
176
277
  return block.group("block").removesuffix("\n") if block else ""
177
278
 
178
279
 
280
+ def fix_verify(commands: Iterable[str]) -> str:
281
+ """The Verify of a fix task: each distinct source Verify in its own subshell, under `set -e`."""
282
+ return "set -e\n" + "\n".join(f"(\n{command}\n)" for command in dict.fromkeys(commands))
283
+
284
+
179
285
  def latest_verify_entry(entries: list, command: str, commit: str,
180
286
  repo: Optional[str] = None) -> Optional[dict]:
181
287
  """Rows are keyed by (command, repo, commit); a row without `repo` is a coordinator row."""
@@ -22,6 +22,10 @@ MARKER = "build recovery: "
22
22
  RECOVERY_KEYS = frozenset({"base", "branch", "kind", "source"})
23
23
 
24
24
 
25
+ # Only the value of a cap line is exempt; a comment or a line separator after it stays in the compared text.
26
+ CAP_VALUE = re.compile(r"(?m)^(-[ \t]*(?:max_review_cycles|wave_budget):)[ \t\w.,$/:]*")
27
+
28
+
25
29
  def runtime():
26
30
  if __package__:
27
31
  from scripts import pipeline_state
@@ -58,8 +62,17 @@ def _parse_recovery_record(value: object) -> dict:
58
62
  return {**value, "source": source}
59
63
 
60
64
 
61
- def _recovery_base_proves_milestone(before, current, source: str) -> bool:
62
- if before.branch != current.branch or before.milestone != current.milestone:
65
+ def _recovery_base_proves_milestone(before, current, source: str, active: bool = True) -> bool:
66
+ if before.branch != current.branch:
67
+ return False
68
+ if before.milestone != current.milestone and (
69
+ # Issue #374: a legacy base records milestone null when define never
70
+ # named it. The owner names it at ship, after the build resumed, so
71
+ # only a closed build recovery accepts an unset base milestone; the
72
+ # bound branch already encodes M00N. A milestone named by the base
73
+ # must still equal the current one.
74
+ active or source != "build" or before.milestone is not None
75
+ ):
63
76
  return False
64
77
  if source == "plan":
65
78
  return (before.phase, before.status) in {("plan", "active"), ("plan", "blocked")}
@@ -99,7 +112,9 @@ def context(repo: Path, text: str | None = None) -> dict | None:
99
112
  if base_text is None:
100
113
  raise state.PipelineStateError("build recovery base has no STATE.md")
101
114
  before = state._state_from_text(base_text)
102
- if not _recovery_base_proves_milestone(before, current, recovery.get("source", "build")):
115
+ if not _recovery_base_proves_milestone(
116
+ before, current, recovery.get("source", "build"), recovery["active"]
117
+ ):
103
118
  raise state.PipelineStateError("build recovery base does not prove this blocked milestone")
104
119
  return recovery
105
120
 
@@ -289,61 +304,78 @@ def validate_plan(repo: Path) -> None:
289
304
  intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
290
305
  if state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md") != intent:
291
306
  raise state.PipelineStateError("plan repair cannot change approved intent")
292
- unchanged = unchanged_waves(repo, recovery)
293
- for path in (repo / ".project/review").iterdir():
294
- match = re.match(r"wave-([1-9]\d*)[.]", path.name)
295
- if match and int(match[1]) not in unchanged:
296
- raise state.PipelineStateError("changed wave must be reviewed after build resumes")
307
+ if stale_review_reasons(repo, recovery, _review_waves(repo / ".project/review")):
308
+ raise state.PipelineStateError("changed wave must be reviewed after build resumes")
297
309
 
298
310
 
299
- def unchanged_waves(repo: Path, recovery: dict) -> set[int]:
300
- """Reuse evidence only when its intent, wave contract, and task bytes match."""
311
+ def _review_waves(directory: Path) -> set[int]:
312
+ return {int(match[1]) for path in directory.iterdir()
313
+ if (match := re.match(r"wave-([1-9]\d*)[.]", path.name))}
314
+
315
+
316
+ def stale_review_reasons(repo: Path, recovery: dict, waves: set[int]) -> dict[int, str]:
317
+ """Say why a wave's preserved review is stale; a wave not returned has matching intent, plan, and task bytes."""
301
318
  state = runtime()
302
319
  project = repo / ".project"
303
320
  old_intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
304
321
  if state._read_real_file(project / "intent/INTENT.md", "INTENT.md") != old_intent:
305
- return set()
322
+ return dict.fromkeys(waves, "INTENT.md changed")
306
323
  old_plan = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/plan/PLAN.md") or ""
307
324
  new_plan = state._read_real_file(project / "plan/PLAN.md", "PLAN.md")
325
+ panel_lines = r"(?m)^-[ \t]*review_panel:.*$"
326
+ if re.findall(panel_lines, old_plan) != re.findall(panel_lines, new_plan):
327
+ return dict.fromkeys(waves, "PLAN.md `review_panel` setting changed")
308
328
 
309
329
  def sections(text):
310
330
  return {match[1]: match[2] for match in re.finditer(
311
331
  r"(?ms)^## ([^\n]+)\n(.*?)(?=^## |\Z)", text,
312
332
  )}
313
333
 
334
+ def governing(plan):
335
+ # Non-wave settings govern every wave, including coverage and surfaces. Caps only bound
336
+ # later work and Dependency notes are prose, so neither changes what a passed review judged.
337
+ return {title: CAP_VALUE.sub(r"\1", body) if title == "Config" else body
338
+ for title, body in plan.items()
339
+ if not title.startswith("Wave ") and title != "Dependency notes"}
340
+
314
341
  old_sections, new_sections = sections(old_plan), sections(new_plan)
315
- # Non-wave settings govern every wave, including coverage and surfaces.
316
- if ({k: v for k, v in old_sections.items() if not k.startswith("Wave ")}
317
- != {k: v for k, v in new_sections.items() if not k.startswith("Wave ")}):
318
- return set()
342
+ old_governing, new_governing = governing(old_sections), governing(new_sections)
343
+ for title in sorted(old_governing.keys() | new_governing.keys()):
344
+ if old_governing.get(title) != new_governing.get(title):
345
+ return dict.fromkeys(waves, f"PLAN.md section `{title}` changed")
319
346
  old_tasks = checkpoint_runtime()._task_contracts_at(repo, recovery["base"], ".project/tasks")
320
347
  current = settled_tasks(repo)
321
- unchanged = set()
322
- for title, body in new_sections.items():
323
- match = re.match(r"Wave ([1-9]\d*)\b", title)
324
- if not match or old_sections.get(title) != body:
348
+ reasons = {}
349
+ for wave in waves:
350
+ section = {title: body for title, body in new_sections.items() if re.match(rf"Wave {wave}\b", title)}
351
+ if not section or any(old_sections.get(title) != body for title, body in section.items()):
352
+ reasons[wave] = f"PLAN.md section `Wave {wave}` changed"
325
353
  continue
326
- wave = match[1]
327
354
  before = {Path(path).name: text for path, text in old_tasks.values()
328
- if task_fields(text, path).get("wave") == wave}
329
- after = {name: text for name, (text, fields) in current.items() if fields.get("wave") == wave}
330
- if before and before == after:
331
- unchanged.add(int(wave))
332
- return unchanged
355
+ if task_fields(text, path).get("wave") == str(wave)}
356
+ after = {name: text for name, (text, fields) in current.items() if fields.get("wave") == str(wave)}
357
+ if not before or before != after:
358
+ reasons[wave] = f"wave {wave} task contracts changed"
359
+ return reasons
333
360
 
334
361
 
335
- def restore_unchanged_reviews(repo: Path) -> None:
362
+ def restore_unchanged_reviews(repo: Path) -> dict | None:
363
+ """Restore reusable wave reviews and report every preserved wave, so a dropped review is visible at approval."""
336
364
  recovery = context(repo)
337
365
  if not recovery or not recovery["active"]:
338
- return
339
- unchanged = unchanged_waves(repo, recovery)
340
- for path in review_backup(repo, recovery).iterdir():
366
+ return None
367
+ backup = review_backup(repo, recovery)
368
+ waves = _review_waves(backup)
369
+ reasons = stale_review_reasons(repo, recovery, waves)
370
+ for path in backup.iterdir():
341
371
  match = re.match(r"wave-([1-9]\d*)[.]", path.name)
342
- if match and int(match[1]) in unchanged:
372
+ if match and int(match[1]) not in reasons:
343
373
  destination = repo / ".project/review" / path.name
344
374
  content = path.read_text(encoding="utf-8")
345
375
  if not destination.exists() or runtime()._read_real_file(destination, destination.name) != content:
346
376
  runtime()._atomic_write(destination, content)
377
+ return {"restored": sorted(waves - reasons.keys()),
378
+ "review_again": [{"wave": wave, "reason": reasons[wave]} for wave in sorted(reasons)]}
347
379
 
348
380
 
349
381
  def inventory_checkpoint(repo: Path, head: str) -> bool:
@@ -161,7 +161,8 @@ def _reject_placeholder(value: str, label: str) -> None:
161
161
  if placeholder is not None:
162
162
  raise HandoffError(
163
163
  f"{label} still contains the placeholder {placeholder.group(0)}; "
164
- "replace it with the real value"
164
+ "replace it with the real value. A value cannot be only a placeholder; "
165
+ "literal text such as an HTML tag must be inside a sentence, in backticks"
165
166
  )
166
167
 
167
168
 
@@ -667,22 +668,30 @@ def _strip_comments(text: str) -> str:
667
668
  return COMMENT_PATTERN.sub("", text)
668
669
 
669
670
 
670
- def _numbered_items(section: str, *, continuations: bool = False) -> Dict[int, str]:
671
- items: Dict[int, str] = {}
672
- current = None
671
+ def _numbered_entries(section: str, *, continuations: bool = False) -> List[Tuple[int, str]]:
672
+ """Each numbered item in document order; a repeated number stays a separate entry."""
673
+ entries: List[Tuple[int, str]] = []
674
+ open_item = False
673
675
  for line in _strip_comments(section).splitlines():
674
676
  match = NUMBERED_ITEM_PATTERN.fullmatch(line.strip())
675
677
  if not match:
676
- if continuations and current is not None and line[:1].isspace() and line.strip():
677
- items[current] += " " + line.strip()
678
+ if continuations and open_item and line[:1].isspace() and line.strip():
679
+ number, text = entries[-1]
680
+ entries[-1] = (number, text + " " + line.strip())
678
681
  elif line.strip():
679
- current = None
682
+ open_item = False
680
683
  continue
681
- number = int(match.group(1))
684
+ entries.append((int(match.group(1)), match.group(2).strip()))
685
+ open_item = True
686
+ return entries
687
+
688
+
689
+ def _numbered_items(section: str, *, continuations: bool = False) -> Dict[int, str]:
690
+ items: Dict[int, str] = {}
691
+ for number, text in _numbered_entries(section, continuations=continuations):
682
692
  if number in items:
683
693
  raise HandoffError(f"repeated numbered item {number}")
684
- items[number] = match.group(2).strip()
685
- current = number
694
+ items[number] = text
686
695
  return items
687
696
 
688
697
 
@@ -693,7 +702,11 @@ def _success_criteria(intent: str) -> Dict[str, str]:
693
702
  expected = list(range(1, max(items) + 1))
694
703
  if sorted(items) != expected:
695
704
  raise HandoffError("INTENT.md success criteria must be contiguous from 1")
696
- return {f"SC{number}": text for number, text in items.items()}
705
+ # Issue #392: a review can quote a criterion without the markers of its sub-bullet lines.
706
+ bare = _numbered_items(
707
+ _section(_common.without_sub_bullet_markers(intent), "Success criteria"), continuations=True
708
+ )
709
+ return {f"SC{number}": _common.Criterion(text, bare[number]) for number, text in items.items()}
697
710
 
698
711
 
699
712
  def _split_top_level_commas(value: str) -> List[str]:
@@ -1297,18 +1310,22 @@ def _normalize_ws(value: str) -> str:
1297
1310
 
1298
1311
 
1299
1312
  def task_review_observation(item: str, task_text: str) -> str:
1300
- """Separate an exact quoted acceptance criterion from its observed evidence."""
1313
+ """Separate a quoted acceptance criterion from its observed evidence."""
1301
1314
  item = _normalize_ws(item)
1302
1315
  section = _common.section_body(task_text, "Acceptance criteria")
1303
1316
  if section is not None:
1304
- for criterion in _numbered_items(section, continuations=True).values():
1305
- quoted = _normalize_ws(criterion)
1306
- prefix = quoted + " — "
1307
- if item.startswith(prefix):
1308
- return item[len(prefix) :]
1309
- if item in {quoted, quoted + " —"}:
1310
- return ""
1311
- return item
1317
+ # Issue #374: frozen landed bytes may repeat a criterion number. Only the
1318
+ # criterion text matters here, so each repeated entry is still split
1319
+ # from its evidence and the evidence gets the same checks.
1320
+ criteria = [criterion for _number, criterion in _numbered_entries(section, continuations=True)]
1321
+ quoted = _common.quoted_criterion(item, criteria)
1322
+ if quoted is not None:
1323
+ return item[quoted[1] + len(" — ") :]
1324
+ # Issue #392: a quote can leave out the `- ` markers of the criterion's
1325
+ # sub-bullet lines, so read the criteria again without them.
1326
+ bare = _common.without_sub_bullet_markers(task_text)
1327
+ return item if bare == task_text else task_review_observation(item, bare)
1328
+
1312
1329
 
1313
1330
 
1314
1331
  def _verify_command(task_text: str) -> str:
@@ -1528,11 +1545,30 @@ def _validate_task_graph(
1528
1545
  if _task_wave(tasks[dependency], dependency) > wave:
1529
1546
  raise HandoffError(f"{task_id} depends on later-wave {dependency}")
1530
1547
 
1548
+ # `supersedes` retires the regression Verify of an earlier task: the same overlap rule selects both.
1549
+ for task_id, text in tasks.items():
1550
+ superseded = _strict_frontmatter(text, task_id).get("supersedes", [])
1551
+ if (
1552
+ not isinstance(superseded, list)
1553
+ or any(not re.fullmatch(r"T\d{3}", item) for item in superseded)
1554
+ or len(superseded) != len(set(superseded))
1555
+ ):
1556
+ raise HandoffError(f"{task_id} supersedes must be a list of different task ids, for example [T001]")
1557
+ for earlier in superseded:
1558
+ if earlier not in tasks:
1559
+ raise HandoffError(f"{task_id} supersedes unknown task {earlier}")
1560
+ if task_waves[earlier] >= task_waves[task_id]:
1561
+ raise HandoffError(f"{task_id} supersedes {earlier}, which is not in an earlier wave")
1562
+ if task_repos[earlier] != task_repos[task_id]:
1563
+ raise HandoffError(f"{task_id} supersedes {earlier}, which is in another repo")
1564
+ if not _common.overlapping_paths(task_files[earlier], task_files[task_id]):
1565
+ raise HandoffError(f"{task_id} supersedes {earlier}, which shares no declared file with it")
1566
+
1531
1567
  for index, left in enumerate(task_ids):
1532
1568
  for right in task_ids[index + 1 :]:
1533
1569
  if task_waves[left] != task_waves[right] or task_repos[left] != task_repos[right]:
1534
1570
  continue
1535
- overlap = sorted(set(task_files[left]) & set(task_files[right]))
1571
+ overlap = _common.overlapping_paths(task_files[left], task_files[right])
1536
1572
  if overlap:
1537
1573
  if (
1538
1574
  _task_scalar(tasks[left], left, "status") == "done"
@@ -1746,7 +1782,24 @@ def validate_roadmap(
1746
1782
  ) -> Dict[str, object]:
1747
1783
  """Validate roadmap structure and dependency ordering."""
1748
1784
 
1749
- _require_state(root, "roadmap", "active", project_dir)
1785
+ # A milestone-boundary re-slice keeps its entering inspect or define state.
1786
+ _require_state_one_of(
1787
+ root,
1788
+ {("roadmap", "active"), ("inspect", "active"), ("define", "active")},
1789
+ project_dir,
1790
+ )
1791
+ if _require_pipeline(root, project_dir)["phase"] == "define":
1792
+ if __package__:
1793
+ from . import build_recovery
1794
+ else:
1795
+ import build_recovery
1796
+ try:
1797
+ # Define entered again after intent approval is not a boundary.
1798
+ reopened = (build_recovery.context(root.resolve()) or {}).get("active")
1799
+ except PipelineStateError as error:
1800
+ raise HandoffError(str(error)) from error
1801
+ if reopened:
1802
+ raise HandoffError("roadmap re-slice is not legal after intent approval")
1750
1803
  relative = f"{project_dir}/ROADMAP.md"
1751
1804
  text = _read(root, relative)
1752
1805
  if re.search(r"(?m)^## Wave |^\|\s*Task\s*\||^Files:\s*", text):
@@ -2045,25 +2098,28 @@ def validate_wave_evidence(
2045
2098
  if enforce_plan_tasks:
2046
2099
  if reviewed_tasks != expected_tasks:
2047
2100
  raise HandoffError(
2048
- f"{name} must review exactly {', '.join(expected_tasks)} in plan order"
2101
+ f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
2049
2102
  )
2050
2103
  else:
2051
2104
  unknown = [task_id for task_id in reviewed_tasks if task_id not in expected_tasks]
2052
2105
  if unknown:
2053
2106
  raise HandoffError(
2054
- f"{name} must review exactly {', '.join(expected_tasks)} in plan order"
2107
+ f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
2055
2108
  )
2056
2109
  plan_order = [task_id for task_id in expected_tasks if task_id in reviewed_tasks]
2057
2110
  if reviewed_tasks != plan_order:
2058
2111
  raise HandoffError(
2059
- f"{name} must review exactly {', '.join(expected_tasks)} in plan order"
2112
+ f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
2060
2113
  )
2061
2114
  task_verdicts = []
2062
2115
  for heading_index, heading in task_headings:
2063
2116
  task_id = heading.group("task")
2064
2117
  expected_title = _normalize_ws(_task_scalar(tasks[task_id], task_id, "title"))
2065
2118
  if _normalize_ws(heading.group("title")) != expected_title:
2066
- raise HandoffError(f"{name} title for {task_id} differs from task file")
2119
+ raise HandoffError(
2120
+ f"{name} title for {task_id} differs from task file; "
2121
+ f"expected the exact `title:` value: {expected_title}"
2122
+ )
2067
2123
  end = next(
2068
2124
  (
2069
2125
  index
@@ -2080,7 +2136,10 @@ def validate_wave_evidence(
2080
2136
  if line.strip().startswith(f"- {marker} ")
2081
2137
  ]
2082
2138
  if not evidence:
2083
- raise HandoffError(f"{name} task {task_id} lacks {verdict} evidence")
2139
+ raise HandoffError(
2140
+ f"{name} task {task_id} lacks {verdict} evidence; "
2141
+ f"add a line that starts with `- {marker} ` under its heading"
2142
+ )
2084
2143
  for item in evidence:
2085
2144
  item = task_review_observation(item, tasks[task_id])
2086
2145
  _non_placeholder(item, f"{name} task {task_id} {verdict} evidence")
@@ -2132,7 +2191,7 @@ def validate_wave_evidence(
2132
2191
  sc_id in criteria
2133
2192
  and (
2134
2193
  not separator
2135
- or _normalize_ws(heading_text) != _normalize_ws(criteria[sc_id])
2194
+ or not _common.quotes_criterion(heading_text, criteria[sc_id])
2136
2195
  )
2137
2196
  ):
2138
2197
  raise HandoffError(f"{name} {sc_id} heading text differs from INTENT.md")
@@ -2150,7 +2209,10 @@ def validate_wave_evidence(
2150
2209
  if line.strip().startswith(f"- {marker} ")
2151
2210
  ]
2152
2211
  if not evidence:
2153
- raise HandoffError(f"{name} {sc_id} lacks {verdict} evidence")
2212
+ raise HandoffError(
2213
+ f"{name} {sc_id} lacks {verdict} evidence; "
2214
+ f"add a line that starts with `- {marker} ` under its heading"
2215
+ )
2154
2216
  for item in evidence:
2155
2217
  _non_placeholder(item, f"{name} {sc_id} {verdict} evidence")
2156
2218
  verdicts[sc_id] = verdict
@@ -2230,7 +2292,7 @@ def validate_final(
2230
2292
  verdicts: Dict[str, str] = {}
2231
2293
  for index, heading in enumerate(blocks):
2232
2294
  sc_id = heading.group("id")
2233
- if _normalize_ws(heading.group("title")) != _normalize_ws(criteria[sc_id]):
2295
+ if not _common.quotes_criterion(heading.group("title"), criteria[sc_id]):
2234
2296
  raise HandoffError(f"FINAL.md {sc_id} heading text differs from INTENT.md")
2235
2297
  end = blocks[index + 1].start() if index + 1 < len(blocks) else len(text)
2236
2298
  block = text[heading.end() : end]
@@ -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
  )