@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
@@ -210,8 +210,14 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
210
210
  "--base", expected_head, "--task-id", task_id,
211
211
  "--round-size", str(round_size))
212
212
  if task["mode"] == "serial":
213
+ name = f"task-{task_id.lower()}-verify"
214
+ stale = isolation.sidecar_root(repo, "verify", name)
215
+ if os.path.lexists(stale / ".git"):
216
+ # An earlier prepare-task left this sidecar. Only one that is proven untouched is replaced.
217
+ steps.append({"script": "isolation.py", "result": isolation.retire_stale_verify(
218
+ repo, stale, isolation.verify_branch_name(name))})
213
219
  step("isolation.py", "isolate-verify", "--repo", str(repo),
214
- "--base", expected_head, "--name", f"task-{task_id.lower()}-verify")
220
+ "--base", expected_head, "--name", name)
215
221
  elif action == "build-evidence":
216
222
  proof = step("build_state.py", "verify-landed", *common, "--head", expected_head)
217
223
  # The landing proof has one canonical home; lean final-review reuse allows only this path.
@@ -278,7 +284,8 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
278
284
  if action == "approve-plan":
279
285
  step("pipeline_state.py", "approve", *common, "--kind", "plan",
280
286
  "--expected-head", expected_head)
281
- except (StepFailed, OSError, subprocess.CalledProcessError, ValueError, KeyError, TypeError) as error:
287
+ except (StepFailed, isolation.IsolationError, OSError, subprocess.CalledProcessError,
288
+ ValueError, KeyError, TypeError) as error:
282
289
  return {"status": "blocked", "reason": str(error), "steps": steps,
283
290
  "next": next_step}
284
291
  return {"status": "complete", "steps": steps, **outputs}
@@ -11,6 +11,7 @@ task_branch: null # orchestrator-owned: gsd-path-task/<id> while parallel; nul
11
11
  # Optional explicit dispatch overrides: model: <advertised slug|inherit>
12
12
  # effort: <advertised effort|inherit>
13
13
  # repo: <member> # multi-repo only: the MEMBERS.md member this task changes; omit for the coordinator
14
+ # supersedes: [<task id>] # only when INTENT requires it: earlier-wave tasks whose behavior this task replaces; retires their Verify
14
15
  files: # every file this task may touch — dispatch checks overlap
15
16
  - <path>
16
17
  ---
@@ -26,9 +26,17 @@ Tasks reviewed: <count>
26
26
 
27
27
  ## T001 — <title>: <pass | fail>
28
28
 
29
- <!-- Per task criterion: copy the full acceptance/interface criterion verbatim
30
- before the em dash; join wrapped lines with spaces. An AC id or summary
31
- alone cannot be matched by review_findings.py. -->
29
+ <!-- Task sections go in task id order (T001 before T002), not in the order
30
+ of the PLAN.md wave table. The heading title is the exact `title:` of
31
+ the task file. An evidence line starts with `- ✅ ` (pass) or `- ❌ `
32
+ (fail). A section needs one or more lines with the marker of its own
33
+ verdict, so a failed task needs a `- ❌ ` criterion line.
34
+ Per task criterion: copy every word of the acceptance/interface
35
+ criterion before the em dash; join wrapped lines and sub-bullets with
36
+ spaces (the `- ` markers of sub-bullets are optional). An AC id or
37
+ summary alone cannot be matched by review_findings.py.
38
+ Write literal markup and code in backticks inside a sentence: text
39
+ such as <b> outside backticks reads as an unfilled placeholder. -->
32
40
  - ✅ <criterion> — <evidence: verify output / file:line checked>
33
41
  - ❌ <criterion> — found: <what exists instead, file:line>
34
42
  fix: <concrete direction a coder can execute without re-investigating>
@@ -47,13 +55,14 @@ Contract violations (blocking):
47
55
  wave, ending `: pass` or `: fail`. `scripts/check_handoffs.py wave
48
56
  --review <this file>` gates the SC id set and verdicts. Omit this
49
57
  section when no task in the wave owns an SC.
50
- Copy the criterion text from INTENT.md `## Success criteria` exactly as
51
- `_success_criteria` stores it: indented continuation lines join with a
52
- single space and keep their leading `- ` markers; the gate compares
53
- heading text with `_normalize_ws` only (no paraphrase or stripped
54
- sub-bullets). -->
58
+ Copy every word of the criterion from INTENT.md `## Success criteria`:
59
+ join indented continuation lines and sub-bullets with a single space
60
+ (the `- ` markers of sub-bullets are optional). The gate compares every
61
+ other word: no paraphrase, no dropped sub-bullet.
62
+ Each SC block needs an evidence line with the marker of its own
63
+ verdict: `- ✅ ` (pass) or `- ❌ ` (fail). -->
55
64
 
56
- ### SC1 — <criterion as stored by the gate>: <pass | fail>
65
+ ### SC1 — <criterion from INTENT.md>: <pass | fail>
57
66
  - ✅ <evidence: verify output / file:line checked>
58
67
  - ❌ <criterion> — found: <what exists instead, file:line>
59
68
  fix: <concrete direction a coder can execute without re-investigating>
@@ -70,7 +70,9 @@ settings block; their default inheritance remains usable.
70
70
  Resolve the bundled helper to an absolute path. Use one record under the Git
71
71
  common directory's `gsd-path/model-selection/` directory per assignment. Scope
72
72
  the path and `--scope` to the milestone, active/lookahead track, and review cycle
73
- where applicable; later work must not reuse an earlier record.
73
+ where applicable; later work must not reuse an earlier record. The helper
74
+ refuses a relative `--record`, and `release` of a missing record names the
75
+ path and creates nothing.
74
76
 
75
77
  ```text
76
78
  python3 -B <helper> resolve --repo <primary> --scope <milestone/track/cycle> --assignment <logical-name> --role <role> --record <absolute-record> --capabilities <advertised-json>
@@ -38,8 +38,8 @@ bundles this file; spawned agents follow their role brief instead.
38
38
  the lookahead planning track for the next milestone; it follows the same
39
39
  marker rule, owns no task frontmatter, and never binds a branch.
40
40
  - STATE.integration_default is the project closeout choice and
41
- STATE.integration is the current milestone choice. Both are `direct` or
42
- `pull-request`; STATE.integration_source is `default` or `milestone` and
41
+ STATE.integration is the current milestone choice. Both are `direct`,
42
+ `pull-request`, or `external-landing`; STATE.integration_source is `default` or `milestone` and
43
43
  preserves explicit override provenance. Older v2 state defaults to `direct`,
44
44
  and modes may be changed only through `pipeline_state.py
45
45
  configure-integration` before build. A next milestone resets its current
@@ -82,7 +82,9 @@ bundles this file; spawned agents follow their role brief instead.
82
82
  `isolation.py attest` may stand in for it: one `.project/`-only commit that
83
83
  names the base, the attested HEAD, the declared files that changed, the
84
84
  task Verify recorded as passing at that HEAD, and the ruling verbatim.
85
- `recover` reports it as `attested`, never as a proven landing.
85
+ `recover` reports it as `attested`, never as a proven landing. Attestation
86
+ matches the declared files literally: work that is only below a declared
87
+ directory can land but cannot be attested.
86
88
  - All pipeline work for a milestone lives on `gsd-path/M00N` (M001, M002,
87
89
  …) bound in STATE.branch. The next milestone binds a new unused
88
90
  `gsd-path/M00N` at the remote default after the previous ship integrates.
@@ -90,6 +92,12 @@ bundles this file; spawned agents follow their role brief instead.
90
92
  branch — either Path's direct merge or a Path-owned PR merged by the user.
91
93
  The bound branch never receives merges or back-merges and is never the
92
94
  GitHub default.
95
+ - The default branch is the one that STATE `default_branch` records at
96
+ initialization; a STATE file without the field means `main`. `main` and
97
+ `origin/main` in these rules mean that branch. Never change the record and
98
+ never take the name from a later `origin/HEAD` lookup. For a member
99
+ repository, the branch is the one that its `.project/MEMBERS.md` section
100
+ records as `Default branch`; a section without the field means `main`.
93
101
  - After initialization writes STATE.md, and before entering the first pipeline
94
102
  phase in an existing Git repository, the router calls
95
103
  `scripts/pipeline_git.py bind-initial` with the selected M00N and exact
@@ -128,8 +136,9 @@ bundles this file; spawned agents follow their role brief instead.
128
136
  for the current STATE.branch milestone must be an ancestor of origin/main;
129
137
  if it is not, that milestone's integration is incomplete and control routes
130
138
  to ship, not build. Direct mode requires the matching canonical `integrate:`
131
- commit; PR mode requires tag metadata and a two-parent merge whose second
132
- parent is the ship commit. A null branch returns
139
+ commit; PR mode requires tag metadata and either a two-parent merge whose
140
+ second parent is the ship commit or a proven rebase merge of the ship
141
+ commits. A null branch returns
133
142
  to the router; build never creates, selects, switches, or rebinds it.
134
143
  - The ship phase makes exactly one commit on the bound branch — the
135
144
  `.project/`-only ship commit recording
@@ -20,6 +20,16 @@ references to bundled copies of those helpers. Other skill helpers, templates,
20
20
  and role briefs remain in the installed skill bundle. Pass `-B` to Python helper
21
21
  invocations so execution does not create bytecode in the runtime store.
22
22
 
23
+ The selected runtime is authoritative for each helper in its `manifest.json`;
24
+ this includes `review_findings.py` and `task_context.py`. A selected runtime
25
+ that was published before a helper became a runtime file does not list that
26
+ helper: run the bundled copy until the project runtime is upgraded.
27
+ `dispatch_driver.py` and `workflow_run.py` always run from the installed skill
28
+ bundle, because they read role briefs and templates from it; a skill update
29
+ replaces them, a runtime upgrade does not. The driver reloads the helper modules
30
+ that it imports from the selected runtime, and runs the subprocess helpers that
31
+ the runtime lists (such as the fix-task brief lint) from it.
32
+
23
33
  With no declaration, retain the legacy helper lookup. An existing
24
34
  `.gsd-path/runtime/` requires the explicit `--runtime-migrate --project PATH`
25
35
  operation before adopting external storage; review the resulting Git diff.
@@ -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."""
@@ -255,6 +361,49 @@ def run_git(
255
361
  return _run_exact(git_command(repo, *arguments), input=input)
256
362
 
257
363
 
364
+ def _patch_ids(repo: Path, commits: Sequence[str]) -> list[Optional[str]]:
365
+ """`git patch-id --stable` per commit, in order; None for an empty commit."""
366
+ log = run_git(repo, "log", "--no-walk=unsorted", "-p", "--binary", "--no-color",
367
+ "--no-ext-diff", *commits)
368
+ if log.returncode != 0:
369
+ raise ValueError("could not read commit patches")
370
+ ids = run_git(repo, "patch-id", "--stable", input=log.stdout)
371
+ if ids.returncode != 0:
372
+ raise ValueError("could not compute patch ids")
373
+ by_commit = {commit: patch for patch, commit in (line.split() for line in ids.stdout.splitlines())}
374
+ return [by_commit.get(commit) for commit in commits]
375
+
376
+
377
+ def rebase_landing_problem(repo: Path, ship: str, landing: str) -> Optional[str]:
378
+ """Why landing is not a forge rebase merge of ship, or None when it is (#418).
379
+
380
+ A rebase merge replays ship's own commits onto the default branch. Proof:
381
+ the first-parent run that ends at landing has one non-merge commit per
382
+ non-merge commit of landing..ship, with equal patch ids in the same order;
383
+ when that run starts at ship's fork point, landing's tree equals ship's.
384
+ """
385
+ listed = run_git(repo, "rev-list", "--reverse", "--topo-order", "--no-merges", f"{landing}..{ship}")
386
+ if listed.returncode != 0:
387
+ return "could not list the ship commits"
388
+ originals = listed.stdout.split()
389
+ if not originals:
390
+ return "landing already contains the ship commit"
391
+ walked = run_git(repo, "rev-list", "--first-parent", "--parents", "-n", str(len(originals)), landing)
392
+ rows = [line.split() for line in walked.stdout.splitlines()][::-1]
393
+ if walked.returncode != 0 or len(rows) != len(originals) or any(len(row) != 2 for row in rows):
394
+ return "landing is not a linear run of rebased commits"
395
+ try:
396
+ if _patch_ids(repo, originals) != _patch_ids(repo, [row[0] for row in rows]):
397
+ return "rebased commits do not replay the ship commits in order"
398
+ except ValueError as error:
399
+ return str(error)
400
+ fork = run_git(repo, "merge-base", ship, landing).stdout.strip()
401
+ trees = run_git(repo, "rev-parse", f"{ship}^{{tree}}", f"{landing}^{{tree}}").stdout.split()
402
+ if rows[0][1] == fork and (len(trees) != 2 or trees[0] != trees[1]):
403
+ return "rebased tree differs from the ship tree"
404
+ return None
405
+
406
+
258
407
  def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
259
408
  """Git argv for repo, including WSL drvfs workarounds when needed."""
260
409
  return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
@@ -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: