@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
@@ -94,6 +94,8 @@ class Task:
94
94
  verify_heavy: bool
95
95
  repo: str = ""
96
96
  review_findings: bool = False
97
+ verify: str = ""
98
+ supersedes: Tuple[str, ...] = ()
97
99
 
98
100
 
99
101
  @dataclass(frozen=True)
@@ -248,6 +250,8 @@ def _parse_task(path: Path, relative_path: str, task_file: Optional[str] = None)
248
250
  verify_heavy=_verify_heavy(text, label),
249
251
  repo=str(fields.get("repo") or ""),
250
252
  review_findings=REVIEW_FINDINGS_HEADING in _sections(text),
253
+ verify=_common.task_verify_command(text),
254
+ supersedes=_normalize_list(fields.get("supersedes", []), f"{label} supersedes", task_ids=True),
251
255
  )
252
256
 
253
257
 
@@ -420,7 +424,8 @@ def _recovery_reports(project: Project) -> Dict[str, Mapping[str, object]]:
420
424
  return reports
421
425
 
422
426
 
423
- def _validate_ready_metadata(project: Project) -> None:
427
+ def _validate_ready_metadata(project: Project) -> Dict[str, Mapping[str, object]]:
428
+ """Validate every task against its recovery report; return the reports by task id."""
424
429
  by_id = {task.task_id: task for task in project.tasks}
425
430
  recovery = _recovery_reports(project)
426
431
  seen_worktrees: Dict[str, str] = {}
@@ -484,12 +489,155 @@ def _validate_ready_metadata(project: Project) -> None:
484
489
  if report.get("verdict") not in LANDED_VERDICTS:
485
490
  reason = report.get("reason", "landing commit is not proven")
486
491
  _invalid_state(task, str(reason))
492
+ return recovery
487
493
 
488
494
 
489
495
  def _overlap(left: Task, right: Task) -> List[str]:
490
496
  if left.repo != right.repo:
491
497
  return []
492
- return sorted(set(left.files) & set(right.files))
498
+ return _common.overlapping_paths(left.files, right.files)
499
+
500
+
501
+ def _superseded(tasks: Sequence[Task], task: Task) -> Dict[str, str]:
502
+ """Earlier task id -> the task that replaces its behavior: this task, or a `done` task."""
503
+
504
+ return {
505
+ earlier: other.task_id
506
+ for other in tasks
507
+ if other.status == "done" or other.task_id == task.task_id
508
+ for earlier in other.supersedes
509
+ }
510
+
511
+
512
+ def _held(by_id: Mapping[str, Task], task: Task) -> List[Task]:
513
+ """The tasks whose protective role this task inherits: each task it supersedes, through a chain."""
514
+
515
+ held: Dict[str, Task] = {}
516
+ pending = list(task.supersedes)
517
+ while pending:
518
+ other = by_id.get(pending.pop())
519
+ if other is not None and other.task_id not in held: # a cycle ends here
520
+ held[other.task_id] = other
521
+ pending.extend(other.supersedes)
522
+ return [by_id[name] for name in sorted(held)]
523
+
524
+
525
+ def _live_verify(
526
+ by_id: Mapping[str, Task], superseded: Mapping[str, str], source: Task
527
+ ) -> Tuple[str, Optional[str], List[Tuple[str, str]]]:
528
+ """The Verify of a source that still runs, or the task that retired all of it, and the drops.
529
+
530
+ The Verify of a fix task is exactly the composition that fix-tasks generates from the
531
+ Verify of each task in its deps, so it carries a copy of each: only the copies that are
532
+ not retired stay in the command. Any other Verify is one command. Each drop is
533
+ (superseded task, superseding task); a copy whose command a live dependency still runs
534
+ is not a drop.
535
+ """
536
+
537
+ if source.task_id in superseded:
538
+ return "", superseded[source.task_id], [(source.task_id, superseded[source.task_id])]
539
+ deps = [by_id[dependency] for dependency in source.deps if dependency in by_id]
540
+ if not deps or source.verify != _common.fix_verify(dep.verify for dep in deps):
541
+ return source.verify, None, []
542
+ parts = [_live_verify(by_id, superseded, dep) for dep in deps]
543
+ live = [command for command, retired_by, _ in parts if not retired_by]
544
+ dropped = list(dict.fromkeys(drop for _, _, drops in parts for drop in drops
545
+ if by_id[drop[0]].verify not in live))
546
+ if not live:
547
+ return "", parts[0][1], dropped
548
+ return _common.fix_verify(live), None, dropped
549
+
550
+
551
+ def _regression_sources(
552
+ tasks: Sequence[Task], task: Task
553
+ ) -> List[Tuple[Task, List[str], Optional[str], str, List[Tuple[str, str]]]]:
554
+ """Each `done` task whose Verify protects a file of this task:
555
+ (source, replaced, retired_by, command, dropped).
556
+
557
+ A source is a task of an earlier wave that shares a declared file with this task. A task
558
+ that supersedes others inherits their role: it is also a source, in any wave, for a task
559
+ that shares a file with a task it holds, and `replaced` names those tasks.
560
+ `retired_by` names the task that supersedes the source: this task, or a `done` task. A
561
+ retired Verify does not run, also not as a copy in the Verify of a fix task: `command`
562
+ is the Verify that runs and `dropped` names each retired copy (`_live_verify`). The source still counts for the one-at-a-time
563
+ rule, so the superseding task never runs beside a task that shares a file of the
564
+ superseded task.
565
+ """
566
+
567
+ by_id = {other.task_id: other for other in tasks}
568
+ superseded = _superseded(tasks, task)
569
+ sources = []
570
+ for other in tasks:
571
+ if other.status != "done" or other.task_id == task.task_id:
572
+ continue
573
+ replaced = [held.task_id for held in _held(by_id, other) if _overlap(task, held)]
574
+ if replaced or (other.wave < task.wave and _overlap(task, other)):
575
+ command, retired_by, dropped = _live_verify(by_id, superseded, other)
576
+ sources.append((other, replaced, retired_by, command, dropped))
577
+ return sources
578
+
579
+
580
+ def _landing_tasks(repo: Path, task_id: str, project_dir: str) -> Tuple[List[Task], Task]:
581
+ tasks = [
582
+ _parse_task(path, path.relative_to(repo).as_posix())
583
+ for path in sorted((repo / project_dir / "tasks").glob("*.md"))
584
+ ]
585
+ task = next((item for item in tasks if item.task_id == task_id), None)
586
+ if task is None:
587
+ raise BuildStateError("unknown-task", f"task file is missing: {task_id}")
588
+ return tasks, task
589
+
590
+
591
+ def retired_verifies(
592
+ repo: Path, task_id: str, project_dir: str = DEFAULT_PROJECT_DIR
593
+ ) -> List[Dict[str, str]]:
594
+ """Return the earlier-wave tasks whose Verify a landing of this task does not run again.
595
+
596
+ A task's plan-approved `supersedes` names them. The Verify is retired for that task, and
597
+ for every task that lands after that task is `done`: the Verify of the superseding task
598
+ then runs in its place (`regression_verifies`, key `supersedes`). A fix task whose
599
+ generated Verify holds only retired copies is here too. A fix task whose generated Verify
600
+ loses some of its copies has one entry for each, with the superseded task under `part`.
601
+ """
602
+
603
+ tasks, task = _landing_tasks(repo, task_id, project_dir)
604
+ retired: List[Dict[str, str]] = []
605
+ for other, _, retired_by, _, dropped in _regression_sources(tasks, task):
606
+ if retired_by:
607
+ if other.verify:
608
+ retired.append({"task": other.task_id, "superseded_by": retired_by})
609
+ else:
610
+ retired.extend({"task": other.task_id, "part": part, "superseded_by": by}
611
+ for part, by in dropped)
612
+ return retired
613
+
614
+
615
+ def regression_verifies(
616
+ repo: Path, task_id: str, own: str, project_dir: str = DEFAULT_PROJECT_DIR
617
+ ) -> List[Dict[str, object]]:
618
+ """Return the Verify commands of landed tasks that a landing of this task must also pass.
619
+
620
+ One entry per distinct command, with every source task that has it, and under
621
+ `supersedes` the tasks whose retired Verify it replaces for this landing. A command
622
+ that the task's own Verify already runs is left out: an identical command, or
623
+ a dependency's command when the own Verify is exactly the fix-task Verify of
624
+ the task's dependencies. Text that only contains a command proves nothing.
625
+ A retired Verify is not here, also not as a copy in the Verify of a fix task: see
626
+ `retired_verifies`.
627
+ """
628
+
629
+ tasks, task = _landing_tasks(repo, task_id, project_dir)
630
+ by_id = {item.task_id: item for item in tasks}
631
+ composed = [by_id[dependency].verify for dependency in task.deps if dependency in by_id]
632
+ covered = {own, *(composed if own == _common.fix_verify(composed) else ())}
633
+ sources: Dict[str, Dict[str, object]] = {}
634
+ for other, replaced, _, command, _ in _regression_sources(tasks, task):
635
+ if command and command not in covered:
636
+ entry = sources.setdefault(command, {"tasks": [], "command": command})
637
+ entry["tasks"].append(other.task_id)
638
+ if replaced: # this Verify runs in the place of the Verify of each task it replaced
639
+ entry["supersedes"] = sorted({*entry.get("supersedes", ()), *replaced})
640
+ return list(sources.values())
493
641
 
494
642
 
495
643
  def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
@@ -522,12 +670,31 @@ def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
522
670
  }
523
671
 
524
672
 
673
+ def _unreviewed_waves(project: Project, waves: Sequence[int]) -> List[int]:
674
+ """Return the given waves that still need a canonical review file of their own.
675
+
676
+ A repair wave is left out: the next review cycle of its source wave judges it.
677
+ """
678
+
679
+ review_dir = project.tasks_dir.parent / "review"
680
+ reviewed = {
681
+ wave for wave in waves
682
+ if all(task.review_findings for task in project.tasks if task.wave == wave)
683
+ }
684
+ if review_dir.is_dir() and not review_dir.is_symlink():
685
+ for path in review_dir.iterdir():
686
+ named = check_handoffs.WAVE_REVIEW_NAME.fullmatch(path.name)
687
+ if named is not None and path.is_file() and not path.is_symlink():
688
+ reviewed.add(int(named.group("wave")))
689
+ return [wave for wave in waves if wave not in reviewed]
690
+
691
+
525
692
  def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
526
693
  allow_done: bool = False) -> Dict[str, object]:
527
694
  """Return task readiness, optionally accepting build/done for completion checks."""
528
695
 
529
696
  project = _load_project(repo, project_dir, ("active", "done") if allow_done else ("active",))
530
- _validate_ready_metadata(project)
697
+ reports = _validate_ready_metadata(project)
531
698
  by_id = {task.task_id: task for task in project.tasks}
532
699
  unfinished_waves = sorted({task.wave for task in project.tasks if task.status != "done"})
533
700
  current_wave = unfinished_waves[0] if unfinished_waves else None
@@ -626,11 +793,31 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
626
793
  {"wave": current_wave, "tasks": stalled},
627
794
  )
628
795
 
796
+ # Tasks that share a regression source land one at a time, each from the landing of the one
797
+ # before, so the source's Verify runs on their combined edits. A parallel round leaves the
798
+ # primary task file pending: a retained isolate shows that the task is already dispatched.
799
+ # A retired source still counts: its superseding task holds its role.
800
+ sources = {task.task_id: {other.task_id for other, _, _, _, _ in _regression_sources(project.tasks, task)}
801
+ for task in concurrent}
802
+ started = [task for task in selectable if reports[task.task_id].get("verdict") != "none"]
803
+ claimed = {source for task in active + started for source in sources[task.task_id]}
804
+ dispatchable = []
805
+ for task in selectable:
806
+ if task in started or not sources[task.task_id] & claimed:
807
+ dispatchable.append(task)
808
+ claimed |= sources[task.task_id]
809
+
629
810
  typed: Dict[str, object] = {}
630
811
  if repair_for_wave is not None:
631
812
  typed["repair_for_wave"] = repair_for_wave
632
813
  if blocked_review is not None:
633
814
  typed["blocked_review"] = blocked_review
815
+ # Informational: every wave below the lowest unfinished wave is all done.
816
+ unreviewed = _unreviewed_waves(project, sorted(
817
+ {task.wave for task in project.tasks if unfinished_waves and task.wave < unfinished_waves[0]}
818
+ ))
819
+ if unreviewed:
820
+ typed["unreviewed_waves"] = unreviewed
634
821
  return {
635
822
  "command": "ready",
636
823
  "branch": project.branch,
@@ -646,10 +833,13 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
646
833
  "deps": list(task.deps),
647
834
  "files": list(task.files),
648
835
  "task_file": task.task_file,
649
- "verify_heavy": task.verify_heavy,
836
+ # The landing also runs each regression Verify, so a heavy one makes the task heavy.
837
+ "verify_heavy": task.verify_heavy
838
+ or any(other.verify_heavy and not retired_by
839
+ for other, _, retired_by, _, _ in _regression_sources(project.tasks, task)),
650
840
  **({"repo": task.repo} if task.repo else {}),
651
841
  }
652
- for task in selectable
842
+ for task in dispatchable
653
843
  ],
654
844
  }
655
845
 
@@ -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:
@@ -384,7 +387,9 @@ def _lint_task(
384
387
  )
385
388
  break
386
389
 
387
- for name in PROSE_SECTIONS:
390
+ # A landed brief is history that nobody can edit: only an unlanded brief must name present paths.
391
+ landed = task_id in (landed_bases or {})
392
+ for name in () if landed else PROSE_SECTIONS:
388
393
  body = sections.get(name)
389
394
  if body is None or not _clean(body):
390
395
  continue
@@ -406,7 +411,7 @@ def _lint_task(
406
411
  block = VERIFY_BLOCK_PATTERN.search(verify_body)
407
412
  if block is None or not block.group("block").strip():
408
413
  problems.append("## Verify must contain a non-empty fenced bash block")
409
- else:
414
+ elif not landed:
410
415
  for token in _verify_tokens(
411
416
  block.group("block"),
412
417
  repo=repo,
@@ -443,9 +448,10 @@ def validate_task_briefs(
443
448
 
444
449
  Plain lint uses the layer base without overrides. Plan approval and dispatch
445
450
  supply transitive dependency files for unlanded tasks and resolved historical
446
- bases for landed tasks, whose immutable briefs must survive later file changes.
451
+ bases for landed tasks. A landed brief is immutable: its `files` scope is
452
+ checked at that base, and the paths its prose and Verify name are not checked.
447
453
  The caller validates the task graph and landed metadata before supplying
448
- these mappings; landed tasks receive no dependency-file allowance.
454
+ these mappings.
449
455
  """
450
456
 
451
457
  resolved_base = _resolve_base(root, base)
@@ -476,6 +482,9 @@ def validate_task_briefs(
476
482
  for shape, task_ids in contracts.items():
477
483
  if len(task_ids) == 1:
478
484
  task_id = next(iter(task_ids))
485
+ # A landed brief is history: a later repair may reword the bullet in its partner.
486
+ if task_id in (landed_bases or {}):
487
+ continue
479
488
  problems.append(
480
489
  f"{task_id}: interface contract is not shared by any other task: {shape}"
481
490
  )
@@ -587,7 +596,6 @@ def plan_validation_bases(
587
596
  )
588
597
  except IsolationError as error:
589
598
  raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
590
- dependency_files[task_id] = set()
591
599
  except check_handoffs.HandoffError as error:
592
600
  raise BriefError(str(error)) from error
593
601
  return tasks, dependency_files, landed_bases
@@ -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
  )